diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b038083..caf6606f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [2.11.3] - 2021-09-29 +### Deleted +- Removed vaccine taken event handling [#715](https://github.com/rokwire/safer-illinois-app/issues/715). +- Removed MicroBlink scan support [#717](https://github.com/rokwire/safer-illinois-app/issues/717). + +## [2.11.2] - 2021-09-24 +### Changed +- Updated vaccination widget content and strings [#711](https://github.com/rokwire/safer-illinois-app/issues/711). + +## [2.11.1] - 2021-09-23 +### Changed +- Updated vaccination widget strings, load related values and URL from app config [#708](https://github.com/rokwire/safer-illinois-app/issues/708). + +## [2.11.0] - 2021-09-10 +### Changed +- "Feedback" section in Settings Home panel changed to "Get Help" [#705](https://github.com/rokwire/safer-illinois-app/issues/705). + +## [2.10.38] - 2021-09-08 +### Fixed +- Fixed Health model classes equality operators. +- Fixed Storage.healthUserTestMonitorInterval to be able to read null values. +### Added +- Added HealthRulesSet.toJson() getter. + +## [2.10.37] - 2021-09-07 +### Changed +- Updated strings for vaccination effective status change [#698](https://github.com/rokwire/safer-illinois-app/issues/698). +- Updated strings for next step for vaccinated users in UIN override list who has fulfilled their tests [#700](https://github.com/rokwire/safer-illinois-app/issues/700). +- Defined notice, noticeHtml, reason and reasonHtml in health status definitions. Acknowledged for vaccination and vaccination suspension events. [#702](https://github.com/rokwire/safer-illinois-app/issues/702). + +## [2.10.36] - 2021-09-01 +### Changed +- Updated next step text for vaccinated users that are forced to resume testing [#692](https://github.com/rokwire/safer-illinois-app/issues/692). +### Added +- Added vaccination widget in Home panel [#696](https://github.com/rokwire/safer-illinois-app/issues/696). + +## [2.10.35] - 2021-08-19 +### Changed +- Disable vaccinated status if user has UserTestMonitorInterval defined [#684](https://github.com/rokwire/safer-illinois-app/issues/684). +### Fixed +- Android: CME crash when processing exposures [#688](https://github.com/rokwire/safer-illinois-app/issues/688). +- Android: RSE crash when scanning for exposures [#690](https://github.com/rokwire/safer-illinois-app/issues/690). + +## [2.10.34] - 2021-08-04 +### Changed +- Button text for requesting vaccine and latest test [#680](https://github.com/rokwire/safer-illinois-app/issues/680). + +## [2.10.33] - 2021-08-03 +### Added +- Force onboarding from app config [#681](https://github.com/rokwire/safer-illinois-app/issues/681). + +## [2.10.32] - 2021-08-02 +### Added +- Fire user updated notification when updating user's private RSA key. +- Added weekdays extension capability for health rule intervals [#678](https://github.com/rokwire/safer-illinois-app/issues/678). +### Changed +- Updated test intervals for undergraduate students and others [#676](https://github.com/rokwire/safer-illinois-app/issues/676). + ## [2.10.31] - 2021-07-17 ### Changed - Upgrade to Flutter 2.2.2 [#664](https://github.com/rokwire/safer-illinois-app/issues/664). @@ -14,7 +72,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix gradle build for Android [#670](https://github.com/rokwire/safer-illinois-app/issues/670). - Check string parameters match case insensitively [#668](https://github.com/rokwire/safer-illinois-app/issues/668). - ## [2.10.30] - 2021-06-16 ### Added - Added Consent Health Provider Vaccine Information flag and related UI [#661](https://github.com/rokwire/safer-illinois-app/issues/661). diff --git a/android/app/build.gradle b/android/app/build.gradle index f2e00ad0..6e76aac2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -163,11 +163,6 @@ dependencies { implementation 'com.google.zxing:core:3.3.0' //Use zxing 3.3.0 because we have minSdk < 24 implementation ('com.journeyapps:zxing-android-embedded:4.1.0@aar') { transitive = false } - //BlinkID - implementation('com.microblink:blinkid:5.3.0@aar') { - transitive = true - } - // BLESSED - BLE library used for Exposures implementation 'com.github.weliem:blessed-android:1.19' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 62e2f961..6fb21f5d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -39,11 +39,6 @@ - - - - - diff --git a/android/app/src/main/java/edu/illinois/covid/Constants.java b/android/app/src/main/java/edu/illinois/covid/Constants.java index 0f07a429..6865be2a 100644 --- a/android/app/src/main/java/edu/illinois/covid/Constants.java +++ b/android/app/src/main/java/edu/illinois/covid/Constants.java @@ -32,7 +32,6 @@ public class Constants { static final String APP_DISMISS_SAFARI_VC_KEY = "dismissSafariVC"; static final String APP_DISMISS_LAUNCH_SCREEN_KEY = "dismissLaunchScreen"; static final String APP_ADD_CARD_TO_WALLET_KEY = "addToWallet"; - static final String APP_MICRO_BLINK_SCAN_KEY = "microBlinkScan"; static final String APP_ENABLED_ORIENTATIONS_KEY = "enabledOrientations"; static final String APP_NOTIFICATIONS_AUTHORIZATION = "notifications_authorization"; static final String APP_LOCATION_SERVICES_PERMISSION = "location_services_permission"; diff --git a/android/app/src/main/java/edu/illinois/covid/MainActivity.java b/android/app/src/main/java/edu/illinois/covid/MainActivity.java index d406eb0f..7337604c 100644 --- a/android/app/src/main/java/edu/illinois/covid/MainActivity.java +++ b/android/app/src/main/java/edu/illinois/covid/MainActivity.java @@ -17,7 +17,6 @@ package edu.illinois.covid; import android.Manifest; -import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.Intent; @@ -41,18 +40,6 @@ import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import com.journeyapps.barcodescanner.BarcodeEncoder; -import com.microblink.MicroblinkSDK; -import com.microblink.entities.recognizers.Recognizer; -import com.microblink.entities.recognizers.RecognizerBundle; -import com.microblink.entities.recognizers.blinkid.generic.BlinkIdCombinedRecognizer; -import com.microblink.entities.recognizers.blinkid.generic.DriverLicenseDetailedInfo; -import com.microblink.entities.recognizers.blinkid.mrtd.MrzResult; -import com.microblink.entities.recognizers.blinkid.passport.PassportRecognizer; -import com.microblink.intent.IntentDataTransferMode; -import com.microblink.recognition.InvalidLicenceKeyException; -import com.microblink.results.date.Date; -import com.microblink.uisettings.ActivityRunner; -import com.microblink.uisettings.BlinkIdUISettings; import java.io.ByteArrayOutputStream; import java.security.SecureRandom; @@ -62,7 +49,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Set; import java.util.UUID; @@ -97,15 +83,6 @@ public class MainActivity extends FlutterActivity implements MethodChannel.Metho private RequestLocationCallback rlCallback; - // BlinkId - private static final int BLINK_ID_REQUEST_CODE = 3; - private BlinkIdCombinedRecognizer blinkIdCombinedRecognizer; - private PassportRecognizer blinkIdPassportRecognizer; - private RecognizerBundle blinkIdRecognizerBundle; - private boolean scanning = false; - private boolean microblinkInitialized = false; - private MethodChannel.Result scanMethodChannelResult; - // Gallery Plugin private GalleryPlugin galleryPlugin; @@ -468,236 +445,6 @@ private String handleBarcode(Object params) { return barcodeImageData; } - //region BlinkId - - private void handleMicroBlinkScan(Object params) { - if (!microblinkInitialized) { - initMicroblinkSdk(); - } - if (!microblinkInitialized) { - Log.i(TAG, "Cannot start scanning! Microblink has not been initialized!"); - if (scanMethodChannelResult != null) { - scanMethodChannelResult.success(null); - } - return; - } - if (scanning) { - Log.d(TAG, "Blink Id is currently scanning!"); - if (scanMethodChannelResult != null) { - scanMethodChannelResult.success(null); - } - } else { - scanning = true; - List recognizersList = Arrays.asList("combined", "passport"); // by default - if (params instanceof HashMap) { - HashMap paramsMap = (HashMap) params; - Object recognizersObject = paramsMap.get("recognizers"); - if (recognizersObject instanceof List) { - recognizersList = (List) recognizersObject; - } - } - List recognizers = new ArrayList<>(); - for (String recognizerParam : recognizersList) { - if ("combined".equals(recognizerParam)) { - blinkIdCombinedRecognizer = new BlinkIdCombinedRecognizer(); - blinkIdCombinedRecognizer.setEncodeFaceImage(true); - recognizers.add(blinkIdCombinedRecognizer); - } else if ("passport".equals(recognizerParam)) { - blinkIdPassportRecognizer = new PassportRecognizer(); - blinkIdPassportRecognizer.setEncodeFaceImage(true); - recognizers.add(blinkIdPassportRecognizer); - } - } - blinkIdRecognizerBundle = new RecognizerBundle(recognizers); - BlinkIdUISettings uiSettings = new BlinkIdUISettings(blinkIdRecognizerBundle); - ActivityRunner.startActivityForResult(this, BLINK_ID_REQUEST_CODE, uiSettings); - } - } - - private void initMicroblinkSdk() { - String blinkIdLicenseKey = Utils.Map.getValueFromPath(keys, "microblink.blink_id.license_key", null); - if (Utils.Str.isEmpty(blinkIdLicenseKey)) { - Log.e(TAG, "Microblink BlinkId license key is missing from config keys!"); - return; - } - try { - MicroblinkSDK.setLicenseKey(blinkIdLicenseKey, this); - MicroblinkSDK.setIntentDataTransferMode(IntentDataTransferMode.PERSISTED_OPTIMISED); - microblinkInitialized = true; - } catch (InvalidLicenceKeyException | NullPointerException e) { - Log.e(TAG, "Microblink failed to initialize:"); - e.printStackTrace(); - } - } - - private void onBlinkIdScanSuccess(Intent data) { - Log.d(TAG, "onBlinkIdScanSuccess"); - if (blinkIdRecognizerBundle != null) { - blinkIdRecognizerBundle.loadFromIntent(data); - } - if ((blinkIdCombinedRecognizer != null) && (blinkIdCombinedRecognizer.getResult().getResultState() == Recognizer.Result.State.Valid)) { - onCombinedRecognizerResult(blinkIdCombinedRecognizer.getResult()); - } else if ((blinkIdPassportRecognizer != null) && (blinkIdPassportRecognizer.getResult().getResultState() == Recognizer.Result.State.Valid)) { - onPassportRecognizerResult(blinkIdPassportRecognizer.getResult()); - } - } - - private void onBlinkIdScanCanceled() { - Log.d(TAG, "onBlinkIdScanCanceled"); - unInitBlinkId(); - if (scanMethodChannelResult != null) { - scanMethodChannelResult.success(null); - } - } - - private void onCombinedRecognizerResult(BlinkIdCombinedRecognizer.Result combinedRecognizerResult) { - Log.d(TAG, "onCombinedRecognizerResult"); - HashMap scanResult = null; - if (combinedRecognizerResult != null) { - scanResult = new HashMap<>(); - - String base64FaceImage = Base64.encodeToString(combinedRecognizerResult.getEncodedFaceImage(), Base64.NO_WRAP); - - scanResult.put("firstName", Utils.Str.nullIfEmpty(combinedRecognizerResult.getFirstName())); - scanResult.put("lastName", Utils.Str.nullIfEmpty(combinedRecognizerResult.getLastName())); - scanResult.put("fullName", Utils.Str.nullIfEmpty(combinedRecognizerResult.getFullName())); - scanResult.put("sex", Utils.Str.nullIfEmpty(combinedRecognizerResult.getSex())); - scanResult.put("address", Utils.Str.nullIfEmpty(combinedRecognizerResult.getAddress())); - - scanResult.put("dateOfBirth", Utils.Str.nullIfEmpty(formatBlinkIdDate(combinedRecognizerResult.getDateOfBirth().getDate()))); - scanResult.put("dateOfExpiry", Utils.Str.nullIfEmpty(formatBlinkIdDate(combinedRecognizerResult.getDateOfExpiry().getDate()))); - scanResult.put("dateOfIssue", Utils.Str.nullIfEmpty(formatBlinkIdDate(combinedRecognizerResult.getDateOfIssue().getDate()))); - - scanResult.put("documentNumber", Utils.Str.nullIfEmpty(combinedRecognizerResult.getDocumentNumber())); - - scanResult.put("placeOfBirth", Utils.Str.nullIfEmpty(combinedRecognizerResult.getPlaceOfBirth())); - scanResult.put("nationality", Utils.Str.nullIfEmpty(combinedRecognizerResult.getNationality())); - scanResult.put("race", Utils.Str.nullIfEmpty(combinedRecognizerResult.getRace())); - scanResult.put("religion", Utils.Str.nullIfEmpty(combinedRecognizerResult.getReligion())); - scanResult.put("profession", Utils.Str.nullIfEmpty(combinedRecognizerResult.getProfession())); - scanResult.put("maritalStatus", Utils.Str.nullIfEmpty(combinedRecognizerResult.getMaritalStatus())); - scanResult.put("residentialStatus", Utils.Str.nullIfEmpty(combinedRecognizerResult.getResidentialStatus())); - scanResult.put("employer", Utils.Str.nullIfEmpty(combinedRecognizerResult.getEmployer())); - scanResult.put("personalIdNumber", Utils.Str.nullIfEmpty(combinedRecognizerResult.getPersonalIdNumber())); - scanResult.put("documentAdditionalNumber", Utils.Str.nullIfEmpty(combinedRecognizerResult.getDocumentAdditionalNumber())); - scanResult.put("issuingAuthority", Utils.Str.nullIfEmpty(combinedRecognizerResult.getIssuingAuthority())); - - scanResult.put("mrz", getScanRezultFromMrz(combinedRecognizerResult.getMrzResult())); - scanResult.put("driverLicenseDetailedInfo", getScanResultFromDriverLicenseDetailedInfo(combinedRecognizerResult.getDriverLicenseDetailedInfo())); - - scanResult.put("base64FaceImage", Utils.Str.nullIfEmpty(base64FaceImage)); - } - unInitBlinkId(); - if (scanMethodChannelResult != null) { - scanMethodChannelResult.success(scanResult); - } - } - - private void onPassportRecognizerResult(PassportRecognizer.Result passportRecognizerResult) { - Log.d(TAG, "onPassportRecognizerResult"); - HashMap scanResult = null; - if (passportRecognizerResult != null) { - scanResult = new HashMap<>(); - - String base64FaceImage = Base64.encodeToString(passportRecognizerResult.getEncodedFaceImage(), Base64.NO_WRAP); - - scanResult.put("firstName", null); - scanResult.put("lastName", null); - scanResult.put("fullName", null); - scanResult.put("sex", null); - scanResult.put("address", null); - - scanResult.put("dateOfBirth", null); - scanResult.put("dateOfExpiry", null); - scanResult.put("dateOfIssue", null); - - scanResult.put("documentNumber", null); - - scanResult.put("placeOfBirth", null); - scanResult.put("nationality", null); - scanResult.put("race", null); - scanResult.put("religion", null); - scanResult.put("profession", null); - scanResult.put("maritalStatus", null); - scanResult.put("residentialStatus", null); - scanResult.put("employer", null); - scanResult.put("personalIdNumber", null); - scanResult.put("documentAdditionalNumber", null); - scanResult.put("issuingAuthority", null); - - scanResult.put("mrz", getScanRezultFromMrz(passportRecognizerResult.getMrzResult())); - scanResult.put("driverLicenseDetailedInfo", null); - - scanResult.put("base64FaceImage", Utils.Str.nullIfEmpty(base64FaceImage)); - } - unInitBlinkId(); - if (scanMethodChannelResult != null) { - scanMethodChannelResult.success(scanResult); - } - } - - private HashMap getScanResultFromDriverLicenseDetailedInfo(DriverLicenseDetailedInfo driverLicenseDetailedInfo) { - HashMap scanResult = null; - if (driverLicenseDetailedInfo != null) { - scanResult = new HashMap<>(); - - scanResult.put("restrictions", Utils.Str.nullIfEmpty(driverLicenseDetailedInfo.getRestrictions())); - scanResult.put("endorsements", Utils.Str.nullIfEmpty(driverLicenseDetailedInfo.getEndorsements())); - scanResult.put("vehicleClass", Utils.Str.nullIfEmpty(driverLicenseDetailedInfo.getVehicleClass())); - } - return scanResult; - } - - private HashMap getScanRezultFromMrz(MrzResult mrzRezult) { - HashMap scanResult = null; - if (mrzRezult != null) { - scanResult = new HashMap<>(); - - scanResult.put("primaryID", Utils.Str.nullIfEmpty(mrzRezult.getPrimaryId())); - scanResult.put("secondaryID", Utils.Str.nullIfEmpty(mrzRezult.getSecondaryId())); - scanResult.put("issuer", Utils.Str.nullIfEmpty(mrzRezult.getIssuer())); - scanResult.put("issuerName", Utils.Str.nullIfEmpty(mrzRezult.getIssuerName())); - scanResult.put("dateOfBirth", Utils.Str.nullIfEmpty(formatBlinkIdDate(mrzRezult.getDateOfBirth().getDate()))); - scanResult.put("dateOfExpiry", Utils.Str.nullIfEmpty(formatBlinkIdDate(mrzRezult.getDateOfExpiry().getDate()))); - scanResult.put("documentNumber", Utils.Str.nullIfEmpty(mrzRezult.getDocumentNumber())); - scanResult.put("nationality", Utils.Str.nullIfEmpty(mrzRezult.getNationality())); - scanResult.put("nationalityName", Utils.Str.nullIfEmpty(mrzRezult.getNationalityName())); - scanResult.put("gender", Utils.Str.nullIfEmpty(mrzRezult.getGender())); - scanResult.put("documentCode", Utils.Str.nullIfEmpty(mrzRezult.getDocumentCode())); - scanResult.put("alienNumber", Utils.Str.nullIfEmpty(mrzRezult.getAlienNumber())); - scanResult.put("applicationReceiptNumber", Utils.Str.nullIfEmpty(mrzRezult.getApplicationReceiptNumber())); - scanResult.put("immigrantCaseNumber", Utils.Str.nullIfEmpty(mrzRezult.getImmigrantCaseNumber())); - - scanResult.put("opt1", Utils.Str.nullIfEmpty(mrzRezult.getOpt1())); - scanResult.put("opt2", Utils.Str.nullIfEmpty(mrzRezult.getOpt2())); - scanResult.put("mrzText", Utils.Str.nullIfEmpty(mrzRezult.getMrzText())); - - scanResult.put("sanitizedOpt1", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedOpt1())); - scanResult.put("sanitizedOpt2", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedOpt2())); - scanResult.put("sanitizedNationality", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedNationality())); - scanResult.put("sanitizedIssuer", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedIssuer())); - scanResult.put("sanitizedDocumentCode", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedDocumentCode())); - scanResult.put("sanitizedDocumentNumber", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedDocumentNumber())); - } - return scanResult; - } - - private void unInitBlinkId() { - blinkIdRecognizerBundle = null; - blinkIdCombinedRecognizer = null; - blinkIdPassportRecognizer = null; - scanning = false; - } - - private String formatBlinkIdDate(Date date) { - if (date == null) { - return null; - } - return String.format(Locale.getDefault(), "%02d/%02d/%4d", date.getMonth(), date.getDay(), date.getYear()); - } - - //endregion - //region Health RSA keys private Object handleHealthRsaPrivateKey(Object params) { @@ -797,19 +544,6 @@ private Object handleEncryptionKey(Object params) { //endregion - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == BLINK_ID_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK) { - onBlinkIdScanSuccess(data); - } else { - onBlinkIdScanCanceled(); - } - } - - super.onActivityResult(requestCode, resultCode, data); - } - /** * Overrides {@link io.flutter.plugin.common.MethodChannel.MethodCallHandler} onMethodCall() */ @@ -846,11 +580,6 @@ public void onMethodCall(MethodCall methodCall, @NonNull MethodChannel.Result re case Constants.APP_ADD_CARD_TO_WALLET_KEY: result.success(false); break; - case Constants.APP_MICRO_BLINK_SCAN_KEY: - scanMethodChannelResult = result; - handleMicroBlinkScan(methodCall.arguments); - // Result is called on latter step - break; case Constants.APP_ENABLED_ORIENTATIONS_KEY: Object orientations = methodCall.argument("orientations"); List orientationsList = handleEnabledOrientations(orientations); diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java b/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java index 9af5db1f..79005a1f 100644 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java +++ b/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java @@ -66,6 +66,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; import androidx.annotation.NonNull; import at.favre.lib.crypto.HKDF; @@ -161,8 +162,8 @@ public ExposurePlugin(MainActivity activity) { this.peripherals = new HashMap<>(); this.peripheralsRPIs = new HashMap<>(); - this.iosExposures = new HashMap<>(); - this.androidExposures = new HashMap<>(); + this.iosExposures = new ConcurrentHashMap<>(); + this.androidExposures = new ConcurrentHashMap<>(); this.i_TEK_map = loadTeksFromStorage(); this.peripherals_bg = new HashMap<>(); @@ -493,7 +494,9 @@ private void processExposures() { // 1. Collect all iOS expired records (not updated after exposureTimeoutIntervalInMillis) if ((iosExposures != null) && !iosExposures.isEmpty()) { - for (String peripheralAddress : iosExposures.keySet()) { + Iterator iosExposuresIterator = iosExposures.keySet().iterator(); + while (iosExposuresIterator.hasNext()) { + String peripheralAddress = iosExposuresIterator.next(); ExposureRecord record = iosExposures.get(peripheralAddress); if (record != null) { long lastHeardInterval = currentTimestamp - record.getTimestampUpdated(); @@ -503,10 +506,10 @@ private void processExposures() { expiredPeripheralAddress = new HashSet<>(); } expiredPeripheralAddress.add(peripheralAddress); - } else if(exposurePingIntervalInMillis <= lastHeardInterval) { + } else if (exposurePingIntervalInMillis <= lastHeardInterval) { Log.d(TAG, "ios exposure ping: " + peripheralAddress); BluetoothPeripheral peripheral = (peripherals != null) ? peripherals.get(peripheralAddress) : null; - if(peripheral != null) { + if (peripheral != null) { peripheral.readRemoteRssi(); } } @@ -517,8 +520,10 @@ private void processExposures() { if ((expiredPeripheralAddress != null) && !expiredPeripheralAddress.isEmpty()) { // Create copy so that to prevent crash with ConcurrentModificationException. Set expiredPeripheralAddressCopy = new HashSet<>(expiredPeripheralAddress); - for (String address : expiredPeripheralAddressCopy) { - // remove expired records from iosExposures + // remove expired records from iosExposures + Iterator expiredPeripheralIterator = expiredPeripheralAddressCopy.iterator(); + while (expiredPeripheralIterator.hasNext()) { + String address = expiredPeripheralIterator.next(); disconnectIosPeripheral(address); } } @@ -526,13 +531,15 @@ private void processExposures() { // 2. Collect all Android expired records (not updated after exposureTimeoutIntervalInMillis) Set expiredRPIs = null; if((androidExposures != null) && !androidExposures.isEmpty()) { - for(String encodedRpi : androidExposures.keySet()) { + Iterator androidExposuresIterator = androidExposures.keySet().iterator(); + while (androidExposuresIterator.hasNext()) { + String encodedRpi = androidExposuresIterator.next(); ExposureRecord record = androidExposures.get(encodedRpi); - if(record != null) { + if (record != null) { long lastHeardInterval = currentTimestamp - record.getTimestampUpdated(); - if(exposureTimeoutIntervalInMillis <= lastHeardInterval) { + if (exposureTimeoutIntervalInMillis <= lastHeardInterval) { Log.d(TAG, "Expired android exposure: " + encodedRpi); - if(expiredRPIs == null) { + if (expiredRPIs == null) { expiredRPIs = new HashSet<>(); } expiredRPIs.add(encodedRpi); @@ -545,7 +552,9 @@ private void processExposures() { // Create copy so that to prevent crash with ConcurrentModificationException. Set expiredRPIsCopy = new HashSet<>(expiredRPIs); // remove expired records from androidExposures - for (String encodedRpi : expiredRPIsCopy) { + Iterator expiredRPIsIterator = expiredRPIsCopy.iterator(); + while (expiredRPIsIterator.hasNext()) { + String encodedRpi = expiredRPIsIterator.next(); removeAndroidRpi(encodedRpi); } } @@ -553,13 +562,13 @@ private void processExposures() { private void clearExposures() { if ((iosExposures != null) && !iosExposures.isEmpty()) { - Map iosExposureCopy = new HashMap<>(iosExposures); + Map iosExposureCopy = new ConcurrentHashMap<>(iosExposures); for (String address : iosExposureCopy.keySet()) { disconnectIosPeripheral(address); } } if ((androidExposures != null) && !androidExposures.isEmpty()) { - Map androidExposureCopy = new HashMap<>(androidExposures); + Map androidExposureCopy = new ConcurrentHashMap<>(androidExposures); for (String encodedRpi : androidExposureCopy.keySet()) { removeAndroidRpi(encodedRpi); } diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java index 3310ae50..99035632 100644 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java +++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java @@ -106,6 +106,11 @@ public void onCreate() { public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand - " + startId); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // This call is required to be made after startForegroundService() since API 26 + startForegroundClientService(); + } + if (intent != null) { Bundle extras = intent.getExtras(); if (extras != null) { diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java index 703f7382..d845ec4d 100644 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java +++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java @@ -28,6 +28,7 @@ static Notification getNotification(Context context) { .setContentTitle(context.getString(R.string.exposure_notification_title)) .setSmallIcon(R.drawable.app_icon) .setContentIntent(pendingIntent) + .setOnlyAlertOnce(true) .setTicker(context.getString(R.string.exposure_notification_ticker)); notification = builder.build(); diff --git a/android/build.gradle b/android/build.gradle index 72a3775c..96d73503 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,6 @@ allprojects { flatDir { dirs '../lib' } - maven { url 'https://maven.microblink.com' } maven { url 'https://jitpack.io' } } } diff --git a/assets/flexUI.json b/assets/flexUI.json index cec2ab5f..d869212e 100644 --- a/assets/flexUI.json +++ b/assets/flexUI.json @@ -7,11 +7,11 @@ "home": ["connect", "stay_healthy", "your_health"], "home.connect": ["netid", "phone"], - "home.stay_healthy" : ["recent_event", "next_step", "symptom_checkin", "add_test_result"], + "home.stay_healthy" : ["vaccination", "recent_event", "next_step", "symptom_checkin", "add_test_result"], "home.your_health" : [ "health_status", "tiles", "health_history", "find_test_location", "wellness_center", "_groups", "switch_account"], "home.your_health.tiles": ["county_guidelines", "care_team"], - "settings": ["user_info", "connect", "customizations", "connected", "notifications", "covid19", "privacy", "account", "feedback"], + "settings": ["user_info", "connect", "customizations", "connected", "notifications", "covid19", "privacy", "account", "get_help"], "settings.connect": ["netid", "phone"], "settings.customizations": ["roles"], "settings.connected": ["netid", "phone"], diff --git a/assets/health.rules.json b/assets/health.rules.json index dee6683d..f26e2176 100644 --- a/assets/health.rules.json +++ b/assets/health.rules.json @@ -7,8 +7,8 @@ }, "intervals": { - "DefaultTestMonitorInterval": 8, - "UndergraduateTestMonitorInterval": 8, + "DefaultTestMonitorInterval": 4, + "UndergraduateTestMonitorInterval": 2, "UserTestMonitorInterval": null, "TestMonitorInterval": { @@ -19,7 +19,24 @@ "success": "UndergraduateTestMonitorInterval", "fail": "DefaultTestMonitorInterval" } - } + }, + + "DefaultTestMonitorWeekdaysExtent": ["Fri", "Sat", "Sun", "Mon"], + "UndergraduateTestMonitorWeekdaysExtent": ["Sat", "Sun"], + + "TestMonitorWeekdaysExtent": { + "condition": "test-user", "params": { "card.role": "Undergraduate", "card.student_level": "1U" }, + "success": "UndergraduateTestMonitorWeekdaysExtent", + "fail": "DefaultTestMonitorWeekdaysExtent" + }, + + "Mon": 1, + "Tue": 2, + "Wed": 3, + "Thu": 4, + "Fri": 5, + "Sat": 6, + "Sun": 7 }, "constants": { @@ -82,27 +99,38 @@ "interval": { "scope": "past" }, "vaccine": "effective" }, - "success": null, - "fail": { - "condition": "require-test", + "success": { + "condition": "test-interval", "params": { - "interval": { "min": 11, "max": null, "scope": "future" } + "interval": "UserTestMonitorInterval" }, - "success": null, - "fail": { - "code": "red", - "priority": 11, - "next_step": "test.now.step" - } - } + "success": "PCR.positive.finish", + "fail": null + }, + "fail": "PCR.positive.finish" }, + "fail": "PCR.positive.quarantine" + }, + + "PCR.positive.finish": { + "condition": "require-test", + "params": { + "interval": { "min": 11, "max": null, "scope": "future" } + }, + "success": null, "fail": { "code": "red", "priority": 11, - "next_step_html": "positive.step.html" + "next_step": "test.now.step" } }, - + + "PCR.positive.quarantine": { + "code": "red", + "priority": 11, + "next_step_html": "positive.step.html" + }, + "PCR.positive-IP": { "condition": "timeout", "params": { @@ -114,30 +142,41 @@ "interval": { "scope": "past" }, "vaccine": "effective" }, - "success": null, - "fail": { - "condition": "require-test", + "success": { + "condition": "test-interval", "params": { - "interval": { "min": 9, "max": null, "scope": "future" } + "interval": "UserTestMonitorInterval" }, - "success": null, - "fail": { - "code": "red", - "priority": 11, - "next_step": "test.now.step", - "fcm_topic": "PCR.positive-IP" - } - } + "success": "PCR.positive-IP.finish", + "fail": null + }, + "fail": "PCR.positive-IP.finish" }, + "fail": "PCR.positive-IP.quarantine" + }, + + "PCR.positive-IP.finish": { + "condition": "require-test", + "params": { + "interval": { "min": 9, "max": null, "scope": "future" } + }, + "success": null, "fail": { "code": "red", "priority": 11, - "next_step_html": "positive-ip.step.html", - "event_explanation": "positive-ip.explanation", + "next_step": "test.now.step", "fcm_topic": "PCR.positive-IP" } }, + "PCR.positive-IP.quarantine": { + "code": "red", + "priority": 11, + "next_step_html": "positive-ip.step.html", + "event_explanation": "positive-ip.explanation", + "fcm_topic": "PCR.positive-IP" + }, + "PCR.positive-NIP.0": { "condition": "timeout", "params": { @@ -190,19 +229,70 @@ "test-monitor": { "condition": "require-test", "params": { - "interval": { "min": 0, "max": "TestMonitorInterval", "scope": "future", "current": true } + "interval": { "min": 0, "max": "TestMonitorInterval", "max-weekdays-extent": "TestMonitorWeekdaysExtent", "scope": "future", "current": true } + }, + "success": "test-fulfilled", + "fail": "test-required" + }, + + "test-fulfilled": { + "condition": "require-vaccine", + "params": { + "interval": { "scope": "past" }, + "vaccine": "effective" }, "success": { - "code": "yellow", - "priority": 1, - "next_step_html": "test.monitor.step.html", - "next_step_interval": "TestMonitorInterval", - "warning": "test.future.warning" + "condition": "test-interval", + "params": { + "interval": "UserTestMonitorInterval" + }, + "success": "test-fulfilled-vaccinated", + "fail": "test-fulfilled-default" }, - "fail": "test-required" + "fail": "test-fulfilled-default" + }, + + "test-fulfilled-vaccinated": { + "code": "yellow", + "priority": 1, + "next_step_html": "test.monitor.vaccinated.step.html", + "next_step_interval": "TestMonitorInterval", + "warning": "test.future.warning" + }, + + "test-fulfilled-default": { + "code": "yellow", + "priority": 1, + "next_step_html": "test.monitor.step.html", + "next_step_interval": "TestMonitorInterval", + "warning": "test.future.warning" }, "test-required": { + "condition": "require-vaccine", + "params": { + "interval": { "scope": "past" }, + "vaccine": "effective" + }, + "success": { + "condition": "test-interval", + "params": { + "interval": "UserTestMonitorInterval" + }, + "success": "test-required-vaccinated", + "fail": "test-required-default" + }, + "fail": "test-required-default" + }, + + "test-required-vaccinated": { + "code": "orange", + "priority": 1, + "next_step_html": "test.now.vaccinated.step.html", + "reason": "test.now.reason" + }, + + "test-required-default": { "code": "orange", "priority": 1, "next_step": "test.now.step", @@ -238,14 +328,41 @@ } }, + "vaccinated-handler": { + "condition": "test-interval", + "params": { + "interval": "UserTestMonitorInterval" + }, + "success": "vaccinated-suspended", + "fail": "vaccinated" + }, + "vaccinated": { "code": "green", "priority": 4, "next_step_html": "vaccinated.step.html", + "notice": "vaccinated.notice", "reason": "vaccinated.reason", "fcm_topic": "vaccinated" }, + "vaccinated-force": { + "code": "green", + "priority": -4, + "next_step_html": "vaccinated.step.html", + "notice": "vaccinated.notice", + "reason": "vaccinated.reason", + "fcm_topic": "vaccinated" + }, + + "vaccinated-suspended": { + "code": null, + "priority": null, + "next_step_html": "test.now.vaccinated.step.html", + "notice": "vaccinated.suspended.notice", + "reason": "vaccinated.suspended.reason" + }, + "quarantine-on": { "code": "orange", "priority": 10, @@ -261,30 +378,33 @@ "vaccine": "effective" }, "success": { - "code": "green", - "priority": -4, - "next_step_html": "vaccinated.step.html", - "reason": "vaccinated.reason", - "fcm_topic": "vaccinated" - }, - "fail": { - "condition": "require-test", + "condition": "test-interval", "params": { - "interval": { "min": 0, "max": "TestMonitorInterval", "scope": "future", "current": true } + "interval": "UserTestMonitorInterval" }, - "success": { - "code": "yellow", - "priority": -1, - "next_step": "test.resume.step", - "next_step_interval": "TestMonitorInterval", - "warning": "test.future.warning" - }, - "fail": { - "code": "orange", - "priority": -1, - "next_step": "test.now.step", - "reason": "test.now.reason" - } + "success": "quarantine-off.unvaccinated", + "fail": "vaccinated-force" + }, + "fail": "quarantine-off.unvaccinated" + }, + + "quarantine-off.unvaccinated": { + "condition": "require-test", + "params": { + "interval": { "min": 0, "max": "TestMonitorInterval", "scope": "future", "current": true } + }, + "success": { + "code": "yellow", + "priority": -1, + "next_step": "test.resume.step", + "next_step_interval": "TestMonitorInterval", + "warning": "test.future.warning" + }, + "fail": { + "code": "orange", + "priority": -1, + "next_step": "test.now.step", + "reason": "test.now.reason" } }, @@ -332,14 +452,41 @@ "params": { "interval": { "min": -1, "max": 1, "current": true } }, + "success": "out-of-test-compliance-fulfilled", + "fail": "test-required" + }, + + "out-of-test-compliance-fulfilled": { + "condition": "require-vaccine", + "params": { + "interval": { "scope": "past" }, + "vaccine": "effective" + }, "success": { - "code": null, - "priority": 1, - "next_step_html": "test.monitor.step.html", - "next_step_interval": 1, - "warning": "test.future.warning" + "condition": "test-interval", + "params": { + "interval": "UserTestMonitorInterval" + }, + "success": "out-of-test-compliance-fulfilled-vaccinated", + "fail": "out-of-test-compliance-fulfilled-default" }, - "fail": "test-required" + "fail": "out-of-test-compliance-fulfilled-default" + }, + + "out-of-test-compliance-fulfilled-vaccinated": { + "code": null, + "priority": 1, + "next_step_html": "test.monitor.vaccinated.step.html", + "next_step_interval": 1, + "warning": "test.future.warning" + }, + + "out-of-test-compliance-fulfilled-default": { + "code": null, + "priority": 1, + "next_step_html": "test.monitor.step.html", + "next_step_interval": 1, + "warning": "test.future.warning" }, "exempt-on": { @@ -352,15 +499,7 @@ "params": { "interval": { "min": 0, "max": "ExemptInterval", "scope": "future" } }, - "success": { - "condition": "require-vaccine", - "params": { - "interval": { "scope": "past" }, - "vaccine": "effective" - }, - "success": "vaccinated", - "fail": "test-required" - }, + "success": null, "fail": { "code": "yellow", "priority": 12, @@ -386,20 +525,23 @@ "vaccine": "effective" }, "success": { - "code": "green", - "priority": -4, - "next_step_html": "vaccinated.step.html", - "reason": "exempt-off.green.reason", - "fcm_topic": "vaccinated" - }, - "fail": { - "code": "orange", - "priority": -1, - "next_step": "test.now.step", - "reason": "exempt-off.orange.reason" - } + "condition": "test-interval", + "params": { + "interval": "UserTestMonitorInterval" + }, + "success": "exempt-off.unvaccinated", + "fail": "vaccinated-force" + }, + "fail": "exempt-off.unvaccinated" }, - + + "exempt-off.unvaccinated": { + "code": "orange", + "priority": -1, + "next_step": "test.now.step", + "reason": "exempt-off.orange.reason" + }, + "release": { "condition": "require-vaccine", "params": { @@ -407,21 +549,24 @@ "vaccine": "effective" }, "success": { - "code": "green", - "priority": -4, - "next_step_html": "vaccinated.step.html", - "reason": "vaccinated.reason", - "fcm_topic": "vaccinated" + "condition": "test-interval", + "params": { + "interval": "UserTestMonitorInterval" + }, + "success": "release.unvaccinated", + "fail": "vaccinated-force" }, - "fail": { - "code": "orange", - "priority": -1, - "next_step": "release.step", - "reason": "release.reason" - } + "fail": "release.unvaccinated" } }, + "release.unvaccinated": { + "code": "orange", + "priority": -1, + "next_step": "release.step", + "reason": "release.reason" + }, + "tests" : { "rules": [ { @@ -807,7 +952,7 @@ "rules": [ { "vaccine": "effective", - "status": "vaccinated" + "status": "vaccinated-handler" } ] }, @@ -821,8 +966,10 @@ "positive-nip.step.html": "

You are not required to self-isolate unless you have COVID-like symptoms and are directed to do so by your licensed health professional.

", "positive-nip.explanation": "Your Saliva PCR test shows the VIRUS IS DETECTED in your REPEAT POSITIVE at a NON-INFECTIOUS LEVEL.", "test.monitor.step.html": "

Monitor your test results

The university encourages you to be vaccinated if you are able to do so. Visit vaccinefinder.org to find nearby appointments.

", + "test.monitor.vaccinated.step.html": "

Monitor your test results

We have a verified record of your completed COVID-19 vaccination on file.

You have been identified as being in an area with a significant increase in positive COVID-19 cases over a short period of time. You are required to receive an on-campus COVID-19 test to maintain compliance and have “Granted” Building Access status until cases improve and you are notified otherwise.

", "test.now.step": "Get a test now", "test.now.reason": "Your status changed to Orange because you are past due for a test.", + "test.now.vaccinated.step.html": "

Get a test now.

You have been identified as being in an area with a significant increase in positive COVID-19 cases over a short period of time. Starting now, you are required to receive an on-campus COVID-19 test every other day (even if you are fully vaccinated and your vaccination record has been verified) to maintain compliance and have “Granted” Building Access status until cases improve and you are notified otherwise.

", "test.another.asap.step": "Get another test asap", "test.another.now.step.html": "

Get your second test now.

  • Limit yourself to essential activities until you get the second negative result.
  • Your building access will change to Granted (Yellow) with the second negative test result.

See testing schedule and rules.

", "test.after.step.html": "

Get your second test after {next_step_date}. You must take two on-campus tests by Jan. 25.

  • Separate the tests by three days of quarantine: if the first test is on day one, the second test will be on day five.
  • Limit yourself to essential activities until you get the second negative result.
  • Your building access will change to Granted (Yellow) with the second negative test result.

See testing schedule and rules.

", @@ -837,7 +984,10 @@ "exposure.step.html": "

You have likely been exposed to a person who is infected with COVID-19.

  • You must quarantine yourself immediately.
  • Stay home. Do not go to work, school, or public areas.
  • Separate yourself from others in your home.
  • Contact covidwellness@illinois.edu for guidance.
  • Get tested after {next_step_date} to see if you have developed the disease.
  • More Info: Quarantine and Isolation
", "exposure.reason": "Your status changed to Orange because you received an exposure notification.", "vaccinated.step.html": "

We have a verified record of your completed COVID-19 vaccination on file.

Your vaccination status replaces testing for compliance and building access until further notice.

Please get an on-campus COVID-19 test if you experience symptoms.

Continue to monitor university communications for any changes to your testing policy.

", - "vaccinated.reason": "Your status changed to Green because your vaccination is already effective.", + "vaccinated.notice": "Your COVID-19 vaccination has been verified", + "vaccinated.reason": "We have a verified record of your completed COVID-19 vaccination on file; your status has changed to Green.", + "vaccinated.suspended.notice": "Your vaccination status is replaced by testing for compliance and building access.", + "vaccinated.suspended.reason": "Your status change to Orange because you have been identified as being in an area with a significant increase in positive COVID-19 cases over a short period of time.", "quarantine-on.step": "Stay at home and avoid contacts", "quarantine-on.reason": "Your status changed to Orange because the Public Health department placed you in Quarantine.", "exempt-on.step": "You are exempted from testing", @@ -892,8 +1042,10 @@ "positive-nip.step.html": "

No es necesario que se aísle a sí mismo a menos que tenga síntomas similares a los de COVID y su profesional de la salud autorizado le indique que lo haga.

", "positive-nip.explanation": "Su prueba de PCR de saliva muestra que el VIRUS ESTÁ DETECTADO en su REPETICIÓN POSITIVA en un NIVEL NO INFECCIOSO.", "test.monitor.step.html": "

Controle los resultados de su prueba

La universidad le anima a vacunarse si puede hacerlo. Visite vacunafinder.org para encontrar citas cercanas.

", + "test.monitor.vaccinated.step.html": "

Controle los resultados de su prueba

Tenemos un registro verificado de su vacunación COVID-19 completa en el archivo.

Se ha identificado que se encuentra en un área con un aumento significativo de casos positivos de COVID-19 durante un corto período de tiempo. Debe recibir una prueba COVID-19 en el campus para mantener el cumplimiento y tener el estado de Acceso al edificio “Concedido” hasta que los casos mejoren y se le notifique lo contrario.

", "test.now.step": "Haz una prueba ahora", "test.now.reason": "Su estado cambió a Naranja porque está atrasado en un examen.", + "test.now.vaccinated.step.html": "

Hágase una prueba ahora.

Se ha identificado que se encuentra en un área con un aumento significativo de casos positivos de COVID-19 durante un corto período de tiempo. A partir de ahora, debe recibir una prueba de COVID-19 en el campus cada dos días (incluso si está completamente vacunado y su registro de vacunación ha sido verificado) para mantener el cumplimiento y tener el estado de Acceso al edificio “Otorgado” hasta que los casos mejoren y usted se notifica de otra manera.

", "test.another.asap.step": "Obtenga otra prueba lo antes posible", "test.another.now.step.html": "

Obtenga su segunda prueba ahora. Debes de tomar dos pruebas en el campus antes del 25 de enero.

  • Separe las pruebas con tres días de cuarentena:  si la primera prueba es el día uno, la segunda prueba será el día cinco.
  • Limítese a las actividades esenciales hasta que obtenga el segundo resultado negativo.
  • El acceso al edificio cambiará a Concedido (amarillo) con el segundo resultado negativo de la prueba.

Ver el calendario y las reglas de las pruebas.

", "test.after.step.html": "

Obtenga su segunda prueba después del {next_step_date}. Debes de tomar dos pruebas en el campus antes del 25 de enero.

  • Separe las pruebas con tres días de cuarentena:  si la primera prueba es el día uno, la segunda prueba será el día cinco.
  • Limítese a las actividades esenciales hasta que obtenga el segundo resultado negativo.
  • El acceso al edificio cambiará a Concedido (amarillo) con el segundo resultado negativo de la prueba.

Ver el calendario y las reglas de las pruebas.

", @@ -908,7 +1060,10 @@ "exposure.step.html": "

Es probable que haya estado expuesto a una persona infectada con COVID-19.

  • Debe ponerse en cuarentena inmediatamente.
  • Quedarse en casa. No vaya al trabajo, la escuela o áreas públicas.
  • Sepárate de las demás en tu casa.
  • Póngase en contacto con covidwellness@illinois.edu para obtener orientación.
  • Hágase la prueba después del {next_step_date} para ver si ha desarrollado la enfermedad.
  • Más información: Cuarentena y aislamiento
", "exposure.reason": "Su estado cambió a Naranja porque recibió una notificación de exposición.", "vaccinated.step.html": "

Tenemos un registro verificado de su vacunación COVID-19 completa en el archivo.

Su estado de vacunación reemplaza las pruebas de cumplimiento y acceso al edificio hasta nuevo aviso.

Hágase una prueba de COVID-19 en el campus si experimenta síntomas.

Continúe monitoreando las comunicaciones de la universidad para detectar cualquier cambio en su política de exámenes.

", - "vaccinated.reason": "Su estado cambió a Verde porque su vacunación ya es efectiva.", + "vaccinated.notice": "Se ha verificado su vacuna COVID-19.", + "vaccinated.reason": "Tenemos un registro verificado de su vacunación COVID-19 completa en el archivo; su estado ha cambiado a verde.", + "vaccinated.suspended.notice": "Su estado de vacunación se reemplaza por pruebas de cumplimiento y acceso al edificio.", + "vaccinated.suspended.reason": "Su estado cambia a Naranja porque se ha identificado que se encuentra en un área con un aumento significativo de casos positivos de COVID-19 durante un corto período de tiempo.", "quarantine-on.step": "Quédese en casa y evite los contactos", "quarantine-on.reason": "Su estado cambió a Orange porque el departamento de Salud Pública lo puso en cuarentena.", "exempt-on.step": "Estas exenta de pruebas", @@ -963,8 +1118,10 @@ "positive-nip.step.html": "

除非您有類似COVID的症狀並且由您的有執照的衛生專業人員指示這樣做,否則您無需自我隔離。

", "positive-nip.explanation": "您的唾液PCR測試顯示病毒在非陽性水平的重複陽性中被檢測到。", "test.monitor.step.html": "

監控您的測試結果

如果您有能力,大學鼓勵您接種疫苗。 訪問 vaccinefinder.org 查找附近的約會。

", + "test.monitor.vaccinated.step.html": "

監控您的測試結果

我們記錄了您完成的COVID-19疫苗接種的經過驗證的記錄。

您已被確定為在短時間內 COVID-19 陽性病例顯著增加的地區。 您需要接受校內 COVID-19 測試以保持合規性並具有“授予”建築物訪問權限狀態,直到情況有所改善並且您收到其他通知。

", "test.now.step": "立即獲得測試", "test.now.reason": "您的狀態更改為“橙色”,因為您已逾期進行測試。", + "test.now.vaccinated.step.html": "

立即進行測試。

您已被確定為在短時間內 COVID-19 陽性病例顯著增加的地區。 從現在開始,您必須每隔一天接受一次校內 COVID-19 測試(即使您已完全接種疫苗並且您的疫苗接種記錄已得到驗證)以保持合規性並具有“授予”建築物訪問權限狀態,直到病例有所改善並且您 另行通知。

", "test.another.asap.step": "盡快獲得另一個測試", "test.another.now.step.html": "

现在进行第二次测试. 你必须在1月25日之前检测两次.

  • 两次测试之间需隔离三天:如果第一次测试在第一天,第二次测试将在第五天
  • 在得到第二个阴性测试结果之前,尽量只做必要的出行
  • 在第二个阴性结果出来后,您的建筑访问权限将更改为“已授予”(黄色)

见测试计划和规则.

", "test.after.step.html": "

在{next_step_date}之后进行第二次测试. 你必须在1月25日之前检测两次.

  • 两次测试之间需隔离三天:如果第一次测试在第一天,第二次测试将在第五天
  • 在得到第二个阴性测试结果之前,尽量只做必要的出行
  • 在第二个阴性结果出来后,您的建筑访问权限将更改为“已授予”(黄色)

见测试计划和规则.

", @@ -979,7 +1136,10 @@ "exposure.step.html": "

您可能已經接觸了感染了COVID-19的人。

  • 您必須立即隔離自己。
  • 您已被強制隔離。
  • 與家中的其他人分開。
  • 請與 covidwellness@illinois.edu 聯繫以獲取指導。
  • 在{next_step_date}之後進行測試,看看您是否已患上這種疾病。
  • 更多信息: 隔離與隔離
", "exposure.reason": "您的狀態更改為橙色,因為您收到了曝光通知。", "vaccinated.step.html": "

我們記錄了您完成的COVID-19疫苗接種的經過驗證的記錄。

您的疫苗接種狀態將取代合規性測試和建立訪問權限,直至另行通知。

如果您出現症狀,請進行校園 COVID-19 測試。

繼續監控大學通訊,了解您的考試政策是否有任何變化。

", - "vaccinated.reason": "您的狀態已更改為“綠色”,因為您的疫苗接種已經有效。", + "vaccinated.notice": "COVID-19ワクチン接種が確認されました。", + "vaccinated.reason": "我們有您完成的 COVID-19 疫苗接種的經過驗證的記錄存檔; 您的狀態已更改為綠色。", + "vaccinated.suspended.notice": "您的疫苗接種狀態將被合規性測試和建築物訪問所取代。", + "vaccinated.suspended.reason": "您的狀態更改為橙色,因為您已被確定為在短時間內 COVID-19 陽性病例顯著增加的地區。", "quarantine-on.step": "呆在家裡,避免接觸", "quarantine-on.reason": "您的狀態更改為“橙色”,因為公共衛生部門已將您隔離。", "exempt-on.step": "您免於測試", @@ -1034,8 +1194,10 @@ "positive-nip.step.html": "
認可された医療従事者から指示された場合以外は自己隔離する必要やキャンパスで行う検査を60日間受ける必要はありません。Safer Illinoisアプリは、この期間にアクセスを許可するように設定されています。質問や困ったことがある場合は、covidwellness@illinois.edu まで連絡してください。
", "positive-nip.explanation": "唾液PCR検査は、不感染性レベルにある場合、2回目の検査結果が陽性検査を出す可能性があります。", "test.monitor.step.html": "

検査結果をモニタリングする

大学は、可能であれば予防接種を受けることを推奨しています。 vaccinefinder.org にアクセスして、近くの予定を見つけてください。

", + "test.monitor.vaccinated.step.html": "

検査結果をモニタリングする

完了したCOVID-19ワクチン接種の確認済みの記録がファイルにあります。

あなたは、短期間に陽性のCOVID-19症例が大幅に増加している地域にいると特定されました。 コンプライアンスを維持するためにキャンパス内のCOVID-19テストを受け、ケースが改善されて別の方法で通知されるまで「許可された」建物アクセスステータスを取得する必要があります。

", "test.now.step": "今検査を受ける", "test.now.reason": "検査期日が過ぎたため、ステータスがオレンジに変更されました。", + "test.now.vaccinated.step.html": "

今すぐテストを受けてください。

あなたは、短期間に陽性のCOVID-19症例が大幅に増加している地域にいると特定されました。 今から、コンプライアンスを維持し、ケースが改善してあなたが それ以外の場合は通知されます。

", "test.another.asap.step": "今すぐ別の検査を受ける", "test.another.now.step.html": "

2回目の検査をいま受ける。

  • 2回目の陰性結果を受け取るまでは必要な行動だけに制限してください。
  • 2回目の陰性結果とともに建物に入る許可が「GRANTED」(黄)に変更されます。

で検査予定とルールについての情報を確認してください。.

", "test.after.step.html": "

2回目の検査を{next_step_date}の後で受ける。 1月25日までにキャンプの検査を2回受ける必要があります。

  • 2つの検査の間に、3日間の隔離期間を設けて下さい。: 1回目の検査を初日に受けた場合、2回目の検査は5日目になります。
  • 2回目の陰性結果を受けるまでには必要な行動だけに制限してください。
  • 2回目の陰性結果とともに建物に入る許可が「GRANTED」(黄)に変更されます。

で検査予定とルールについての情報を確認してください。.

", @@ -1050,7 +1212,10 @@ "exposure.step.html": "

COVID-19に感染している人と接触した可能性があります。

  • 直ちに隔離してください。
  • 家にいてください。仕事や学校や公共の場へ行かないでください。
  • 家にいる人から離れてください。
  • ガイダンスを受けるにはcovidwellness@illinois.eduに連絡してください。
  • {next_step_date}の後で検査を受けて、自分が発症しているかどうかを確認してください。
  • より詳しい情報: 隔離と孤立
", "exposure.reason": "接触通知を受信したため、ステータスがオレンジに変更されました。", "vaccinated.step.html": "

完了したCOVID-19ワクチン接種の確認済みの記録がファイルにあります。

あなたのワクチン接種状況は、追って通知があるまで、コンプライアンスと建物へのアクセスのテストに取って代わります。

症状が出た場合は、キャンパス内のCOVID-19検査を受けてください。

テストポリシーに変更がないか、大学のコミュニケーションを引き続き監視します。

", - "vaccinated.reason": "予防接種がすでに有効になっているため、ステータスが緑に変わりました。", + "vaccinated.notice": "您的 COVID-19 疫苗接種已通過驗證。", + "vaccinated.reason": "完了したCOVID-19ワクチン接種の確認済みの記録がファイルにあります。 ステータスが緑に変わりました。", + "vaccinated.suspended.notice": "予防接種のステータスは、コンプライアンスと建物へのアクセスのテストに置き換えられます。", + "vaccinated.suspended.reason": "短期間に陽性のCOVID-19症例が大幅に増加している地域にいることが確認されたため、ステータスがオレンジに変わります。", "quarantine-on.step": "家にいて、他の人との接触を避けてください。", "quarantine-on.reason": "Public Health departmentにより隔離状態に置かれているため、ステータスがオレンジに変更されました。", "exempt-on.step": "あなたはテストから免除されています", diff --git a/assets/strings.en.json b/assets/strings.en.json index 3dba4dc8..4ffe7914 100644 --- a/assets/strings.en.json +++ b/assets/strings.en.json @@ -164,11 +164,11 @@ "panel.onboarding.base.not_now.hint": "", "panel.onboarding.base.not_now.title": "Not right now", - "panel.settings.feedback.label.title": "Provide Feedback", + "panel.settings.get_help.label.title": "Get Help", "panel.settings.privacy_statement.label.title": "Privacy Statement", - "panel.settings.label.offline.feedback": "Providing a feedback is not available while offline.", + "panel.settings.label.offline.get_help": "Getting a help is not available while offline.", "panel.settings.home.settings.header": "Settings", "panel.settings.home.connect.not_logged_in.title": "Connect to Illinois", @@ -213,10 +213,8 @@ "panel.settings.home.button.debug.hint": "", "panel.settings.home.button.test.title": "Test", "panel.settings.home.button.test.hint": "", - "panel.settings.home.feedback.title": "We need your ideas!", - "panel.settings.home.feedback.description": "Enjoying the app? Missing something? Tap on the bottom to submit your idea.", - "panel.settings.home.button.feedback.title": "Submit Feedback", - "panel.settings.home.button.feedback.hint": "", + "panel.settings.home.button.get_help.title": "Get Help", + "panel.settings.home.button.get_help.hint": "", "panel.settings.home.covid19.exposure_notifications": "Exposure Notifications", "panel.settings.home.covid19.provider_test_result": "Health Provider Test Results", "panel.settings.home.covid19.provider_vaccine_info": "Health Provider Vaccine Information", @@ -327,7 +325,6 @@ "panel.covid19home.label.contact_trace.title": "Contact Trace", "panel.covid19home.label.reported_symptoms.title": "Self Reported Symptoms", "panel.covid19home.label.vaccine.effective.title": "Vaccine Effective", - "panel.covid19home.label.vaccine.taken.title": "Vaccine Taken", "panel.covid19home.label.vaccine.title": "Vaccine", "panel.covid19home.button.info.title": "Info ", "panel.covid19home.label.access.granted": "Building access granted", @@ -338,6 +335,14 @@ "panel.covid19home.button.groups.title": "Groups", "panel.covid19home.button.groups.hint": "", + "panel.covid19home.vaccination.heading.title": "VACCINATION", + "panel.covid19home.vaccination.none.title": "Get a vaccine now", + "panel.covid19home.vaccination.none.description": "• COVID-19 vaccines are safe\n• COVID-19 vaccines are effective\n• COVID-19 vaccines allow you to safely do more\n• COVID-19 vaccines build safer protection", + "panel.covid19home.vaccination.vaccinated.title": "Vaccinated", + "panel.covid19home.vaccination.vaccinated.description": "Your vaccination is not effective yet.", + "panel.covid19home.vaccination.button.appointment.title": "Make an appointment", + "panel.covid19home.vaccination.button.appointment.hint": "", + "panel.covid19_test_locations.header.title": "Test Locations", "panel.covid19_test_locations.label.contact.title": "Contact", "panel.covid19_test_locations.distance.text": "mi away", @@ -611,7 +616,6 @@ "panel.health.covid19.history.label.contact_trace.details": "contact trace: ", "panel.health.covid19.history.label.vaccine.effective.title": "Vaccine Effective", "panel.health.covid19.history.label.vaccine.effective.details": "vaccine effective: ", - "panel.health.covid19.history.label.vaccine.taken.title": "Vaccine Taken", "panel.health.covid19.history.label.vaccine.taken.details": "vaccine taken: ", "panel.health.covid19.history.label.vaccine.title": "Vaccine", "panel.health.covid19.history.label.vaccine.details": "vaccine: ", @@ -626,7 +630,7 @@ "panel.health.covid19.history.label.verified": "Verified", "panel.health.covid19.history.label.verification_pending": "Verification Pending", "panel.health.covid19.history.message.clear_failed": "Failed to clear COVID-19 event history", - "panel.health.covid19.history.button.repost_history.title": "Request my latest test again", + "panel.health.covid19.history.button.repost_history.title": "Request my vaccine and latest test again", "panel.health.covid19.history.button.repost_history.hint": "", "panel.health.covid19.history.message.request_tests": "Your request has been submitted. You should receive your latest test within an hour", @@ -708,8 +712,7 @@ "panel.health.status_update.label.reason.symptoms.title": "You reported new symptoms", "panel.health.status_update.label.reason.exposed.title": "You were exposed to someone who was likely infected", "panel.health.status_update.label.reason.exposure.detail": "Duration of exposure: ", - "panel.health.status_update.label.reason.vaccine.effective.title": "Your vaccine is already effective.", - "panel.health.status_update.label.reason.vaccine.taken.title": "Your vaccine is taken.", + "panel.health.status_update.label.reason.vaccine.effective.title": "Your COVID-19 vaccination has been verified.", "panel.health.status_update.label.reason.vaccine.title": "Your vaccine is applied.", "panel.health.status_update.label.reason.action.title": "Health authorities require you to take an action.", "panel.health.status_update.label.reason.action.detail": "Action Required: ", diff --git a/assets/strings.es.json b/assets/strings.es.json index de38287e..84578310 100644 --- a/assets/strings.es.json +++ b/assets/strings.es.json @@ -164,11 +164,11 @@ "panel.onboarding.base.not_now.hint": "", "panel.onboarding.base.not_now.title": "No en este momento", - "panel.settings.feedback.label.title": "Proporcionar comentarios", + "panel.settings.get_help.label.title": "Consigue Ayuda", "panel.settings.privacy_statement.label.title": "Declaración de privacidad", - "panel.settings.label.offline.feedback": "Proporcionar un comentario no está disponible sin conexión.", + "panel.settings.label.offline.get_help": "No es posible obtener ayuda sin conexión.", "panel.settings.home.settings.header": "Configuración", "panel.settings.home.connect.not_logged_in.title": "Conéctate a Illinois", @@ -213,10 +213,8 @@ "panel.settings.home.button.debug.hint": "", "panel.settings.home.button.test.title": "Prueba", "panel.settings.home.button.test.hint": "", - "panel.settings.home.feedback.title": "¡Necesitamos tus ideas!", - "panel.settings.home.feedback.description": "¿Estás disfrutando la aplicación? ¿Echando de menos algo? Toque en la parte inferior para enviar su idea", - "panel.settings.home.button.feedback.title": "Enviar comentarios", - "panel.settings.home.button.feedback.hint": "", + "panel.settings.home.button.get_help.title": "Consigue Ayuda", + "panel.settings.home.button.get_help.hint": "", "panel.settings.home.covid19.exposure_notifications": "Notificaciones de exposición", "panel.settings.home.covid19.provider_test_result": "Resultados de la prueba del proveedor de salud", "panel.settings.home.covid19.provider_vaccine_info": "Información sobre vacunas del proveedor de salud", @@ -327,7 +325,6 @@ "panel.covid19home.label.contact_trace.title": "Seguimiento de contacto", "panel.covid19home.label.reported_symptoms.title": "Síntomas autoinformados", "panel.covid19home.label.vaccine.effective.title": "Vacuna Eficaz", - "panel.covid19home.label.vaccine.taken.title": "Vacuna Tomada", "panel.covid19home.label.vaccine.title": "Vacuna", "panel.covid19home.button.info.title": "Información ", "panel.covid19home.label.access.granted": "Acceso al edificio concedido", @@ -338,6 +335,14 @@ "panel.covid19home.button.groups.title": "Groups", "panel.covid19home.button.groups.hint": "", + "panel.covid19home.vaccination.heading.title": "VACUNACIÓN", + "panel.covid19home.vaccination.none.title": "Obtenga una vacuna ahora", + "panel.covid19home.vaccination.none.description": "• Las vacunas COVID-19 son seguras\n• Las vacunas COVID-19 son efectivas\n• Las vacunas COVID-19 le permiten hacer más de manera segura\n• Las vacunas COVID-19 crean una protección más segura", + "panel.covid19home.vaccination.vaccinated.title": "Vacunado", + "panel.covid19home.vaccination.vaccinated.description": "Su vacunación aún no es efectiva.", + "panel.covid19home.vaccination.button.appointment.title": "Haga una cita", + "panel.covid19home.vaccination.button.appointment.hint": "", + "panel.covid19_test_locations.header.title": "Lugares de prueba", "panel.covid19_test_locations.label.contact.title": "Contacto", "panel.covid19_test_locations.distance.text": "mi distancia", @@ -611,7 +616,6 @@ "panel.health.covid19.history.label.contact_trace.details": "seguimiento de contacto: ", "panel.health.covid19.history.label.vaccine.effective.title": "Vacuna Eficaz", "panel.health.covid19.history.label.vaccine.effective.details": "vacuna eficaz: ", - "panel.health.covid19.history.label.vaccine.taken.title": "Vacuna Tomada", "panel.health.covid19.history.label.vaccine.taken.details": "vacuna tomada: ", "panel.health.covid19.history.label.vaccine.title": "Vacuna", "panel.health.covid19.history.label.vaccine.details": "vacuna: ", @@ -626,7 +630,7 @@ "panel.health.covid19.history.label.verified": "Verificado", "panel.health.covid19.history.label.verification_pending": "Verificación pendiente", "panel.health.covid19.history.message.clear_failed": "No se pudo borrar el historial de eventos de COVID-19", - "panel.health.covid19.history.button.repost_history.title": "Solicitar mi última prueba nuevamente", + "panel.health.covid19.history.button.repost_history.title": "Solicitar mi vacuna y la última prueba nuevamente", "panel.health.covid19.history.button.repost_history.hint": "", "panel.health.covid19.history.message.request_tests": "Su solicitud ha sido enviada. Debería recibir su última prueba en una hora", @@ -708,8 +712,7 @@ "panel.health.status_update.label.reason.symptoms.title": "Reportaste nuevos síntomas", "panel.health.status_update.label.reason.exposed.title": "Estuvo expuesto a alguien que probablemente estaba infectado", "panel.health.status_update.label.reason.exposure.detail": "Duración de exposición:", - "panel.health.status_update.label.reason.vaccine.effective.title": "Tu vacuna ya es eficaz.", - "panel.health.status_update.label.reason.vaccine.taken.title": "Se toma su vacuna.", + "panel.health.status_update.label.reason.vaccine.effective.title": "Se ha verificado su vacuna COVID-19.", "panel.health.status_update.label.reason.vaccine.title": "Se aplica su vacuna.", "panel.health.status_update.label.reason.action.title": "Las autoridades sanitarias le exigen que actúe.", "panel.health.status_update.label.reason.action.detail": "Acción requerida:", diff --git a/assets/strings.ja.json b/assets/strings.ja.json index f685c753..2b28685b 100644 --- a/assets/strings.ja.json +++ b/assets/strings.ja.json @@ -164,11 +164,11 @@ "panel.onboarding.base.not_now.hint": "", "panel.onboarding.base.not_now.title": "今はしない", - "panel.settings.feedback.label.title": "Provide Feedback", + "panel.settings.get_help.label.title": "得到幫助", "panel.settings.privacy_statement.label.title": "Privacy Statement", - "panel.settings.label.offline.feedback": "Providing a feedback is not available while offline.", + "panel.settings.label.offline.get_help": "オフライン中はヘルプを利用できません。", "panel.settings.home.settings.header": "設定", "panel.settings.home.connect.not_logged_in.title": "Connect to Illinois", @@ -213,10 +213,8 @@ "panel.settings.home.button.debug.hint": "", "panel.settings.home.button.test.title": "Test", "panel.settings.home.button.test.hint": "", - "panel.settings.home.feedback.title": "We need your ideas!", - "panel.settings.home.feedback.description": "Enjoying the app? Missing something? Tap on the bottom to submit your idea.", - "panel.settings.home.button.feedback.title": "Submit Feedback", - "panel.settings.home.button.feedback.hint": "", + "panel.settings.home.button.get_help.title": "助けを得ます", + "panel.settings.home.button.get_help.hint": "", "panel.settings.home.covid19.exposure_notifications": "接続通知", "panel.settings.home.covid19.provider_test_result": "医療提供者のテスト結果", "panel.settings.home.covid19.provider_vaccine_info": "医療提供者のワクチン情報", @@ -327,7 +325,6 @@ "panel.covid19home.label.contact_trace.title": "Contact Trace", "panel.covid19home.label.reported_symptoms.title": "Self Reported Symptoms", "panel.covid19home.label.vaccine.effective.title": "Vaccine Effective", - "panel.covid19home.label.vaccine.taken.title": "Vaccine Taken", "panel.covid19home.label.vaccine.title": "Vaccine", "panel.covid19home.button.info.title": "情報 ", "panel.covid19home.label.access.granted": "建物への出入りが承認されました", @@ -338,6 +335,14 @@ "panel.covid19home.button.groups.title": "グループ", "panel.covid19home.button.groups.hint": "", + "panel.covid19home.vaccination.heading.title": "ワクチン", + "panel.covid19home.vaccination.none.title": "今すぐワクチンを入手", + "panel.covid19home.vaccination.none.description": "• COVID-19 疫苗是安全的\n• COVID-19ワクチンは効果的です\n• COVID-19ワクチンはあなたが安全にもっと多くのことをすることを可能にします\n• COVID-19ワクチンはより安全な保護を構築します", + "panel.covid19home.vaccination.vaccinated.title": "ワクチン接種", + "panel.covid19home.vaccination.vaccinated.description": "あなたの予防接種はまだ効果的ではありません。", + "panel.covid19home.vaccination.button.appointment.title": "予約する", + "panel.covid19home.vaccination.button.appointment.hint": "", + "panel.covid19_test_locations.header.title": "検査現場", "panel.covid19_test_locations.label.contact.title": "Contact", "panel.covid19_test_locations.distance.text": "mi away", @@ -611,7 +616,6 @@ "panel.health.covid19.history.label.contact_trace.details": "contact trace: ", "panel.health.covid19.history.label.vaccine.effective.title": "Vaccine Effective", "panel.health.covid19.history.label.vaccine.effective.details": "vaccine effective: ", - "panel.health.covid19.history.label.vaccine.taken.title": "Vaccine Taken", "panel.health.covid19.history.label.vaccine.taken.details": "vaccine taken: ", "panel.health.covid19.history.label.vaccine.title": "Vaccine", "panel.health.covid19.history.label.vaccine.details": "vaccine: ", @@ -626,7 +630,7 @@ "panel.health.covid19.history.label.verified": "確認しました", "panel.health.covid19.history.label.verification_pending": "Verification Pending", "panel.health.covid19.history.message.clear_failed": "Failed to clear COVID-19 event history", - "panel.health.covid19.history.button.repost_history.title": "最新の検査結果を再度要求する", + "panel.health.covid19.history.button.repost_history.title": "ワクチンと最新の検査をもう一度リクエストしてください", "panel.health.covid19.history.button.repost_history.hint": "", "panel.health.covid19.history.message.request_tests": "要求は提出されました。1時間以内に最新の検査結果を受け取るはずです", @@ -708,8 +712,7 @@ "panel.health.status_update.label.reason.symptoms.title": "You reported new symptoms", "panel.health.status_update.label.reason.exposed.title": "You were exposed to someone who was likely infected", "panel.health.status_update.label.reason.exposure.detail": "Duration of exposure: ", - "panel.health.status_update.label.reason.vaccine.effective.title": "Your vaccine is already effective.", - "panel.health.status_update.label.reason.vaccine.taken.title": "Your vaccine is taken.", + "panel.health.status_update.label.reason.vaccine.effective.title": "Your COVID-19 vaccination has been verified.", "panel.health.status_update.label.reason.vaccine.title": "Your vaccine is applied.", "panel.health.status_update.label.reason.action.title": "Health authorities require you to take an action.", "panel.health.status_update.label.reason.action.detail": "Action Required: ", diff --git a/assets/strings.zh.json b/assets/strings.zh.json index 251b5122..2c74cf7e 100644 --- a/assets/strings.zh.json +++ b/assets/strings.zh.json @@ -164,11 +164,11 @@ "panel.onboarding.base.not_now.hint": "", "panel.onboarding.base.not_now.title": "暂时不", - "panel.settings.feedback.label.title": "提供反馈", + "panel.settings.get_help.label.title": "得到幫助", "panel.settings.privacy_statement.label.title": "隐私声明", - "panel.settings.label.offline.feedback": "离线时无法提供反馈。", + "panel.settings.label.offline.get_help": "離線時無法獲得幫助。", "panel.settings.home.settings.header": "设置", "panel.settings.home.connect.not_logged_in.title": "連接到伊利諾伊州", @@ -213,10 +213,8 @@ "panel.settings.home.button.debug.hint": "", "panel.settings.home.button.test.title": "测试", "panel.settings.home.button.test.hint": "", - "panel.settings.home.feedback.title": " 我们需要你的主意!", - "panel.settings.home.feedback.description": " 喜欢这个应用程序吗?我们遗漏了什么?点击底部提交您的想法。", - "panel.settings.home.button.feedback.title": "提交反馈", - "panel.settings.home.button.feedback.hint": "", + "panel.settings.home.button.get_help.title": "得到幫助", + "panel.settings.home.button.get_help.hint": "", "panel.settings.home.covid19.exposure_notifications": "接触通知", "panel.settings.home.covid19.provider_test_result": "健康提供者測試結果", "panel.settings.home.covid19.provider_vaccine_info": "衛生提供者疫苗信息", @@ -327,7 +325,6 @@ "panel.covid19home.label.contact_trace.title": "接触痕迹", "panel.covid19home.label.reported_symptoms.title": "自述症状", "panel.covid19home.label.vaccine.effective.title": "疫苗有效", - "panel.covid19home.label.vaccine.taken.title": "接種疫苗", "panel.covid19home.label.vaccine.title": "疫苗", "panel.covid19home.button.info.title": "信息 ", "panel.covid19home.label.access.granted": "授予建築物訪問權限", @@ -338,6 +335,14 @@ "panel.covid19home.button.groups.title": "Groups", "panel.covid19home.button.groups.hint": "", + "panel.covid19home.vaccination.heading.title": "疫苗接種", + "panel.covid19home.vaccination.none.title": "立即接種疫苗", + "panel.covid19home.vaccination.none.description": "• COVID-19 疫苗是安全的\n• COVID-19 疫苗是有效的\n• COVID-19 疫苗可讓您安全地做更多事情\n• COVID-19 疫苗可建立更安全的保護", + "panel.covid19home.vaccination.vaccinated.title": "已接種", + "panel.covid19home.vaccination.vaccinated.description": "您的疫苗接種尚未生效。", + "panel.covid19home.vaccination.button.appointment.title": "預約", + "panel.covid19home.vaccination.button.appointment.hint": "", + "panel.covid19_test_locations.header.title": "测试位置", "panel.covid19_test_locations.label.contact.title": "联系人", "panel.covid19_test_locations.distance.text": "英里遠", @@ -611,7 +616,6 @@ "panel.health.covid19.history.label.contact_trace.details": "接触者追踪: ", "panel.health.covid19.history.label.vaccine.effective.title": "疫苗有效", "panel.health.covid19.history.label.vaccine.effective.details": "疫苗有效: ", - "panel.health.covid19.history.label.vaccine.taken.title": "接種疫苗", "panel.health.covid19.history.label.vaccine.taken.details": "接種疫苗: ", "panel.health.covid19.history.label.vaccine.title": "疫苗", "panel.health.covid19.history.label.vaccine.details": "疫苗: ", @@ -626,7 +630,7 @@ "panel.health.covid19.history.label.verified": "已验证", "panel.health.covid19.history.label.verification_pending": "等待验证", "panel.health.covid19.history.message.clear_failed": "无法清除COVID-19事件历史记录", - "panel.health.covid19.history.button.repost_history.title": "再次请求我的最新测试", + "panel.health.covid19.history.button.repost_history.title": "再次請求我的疫苗和最新測試", "panel.health.covid19.history.button.repost_history.hint": "", "panel.health.covid19.history.message.request_tests": "您的请求已提.你应该在一小时内收到你的最新测验", @@ -708,8 +712,7 @@ "panel.health.status_update.label.reason.symptoms.title": "你报告了新的症状", "panel.health.status_update.label.reason.exposed.title": "你接触过可能被感染的人", "panel.health.status_update.label.reason.exposure.detail": "暴露时间: ", - "panel.health.status_update.label.reason.vaccine.effective.title": "您的疫苗已經有效。", - "panel.health.status_update.label.reason.vaccine.taken.title": "您的疫苗已經服用。", + "panel.health.status_update.label.reason.vaccine.effective.title": "您的 COVID-19 疫苗接種已通過驗證。", "panel.health.status_update.label.reason.vaccine.title": "您的疫苗已接種。", "panel.health.status_update.label.reason.action.title": "衛生當局要求您採取行動。", "panel.health.status_update.label.reason.action.detail": "待办行动: ", diff --git a/ios/Podfile b/ios/Podfile index 6fa5a192..07dff8c1 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -34,7 +34,6 @@ target 'Runner' do pod 'GoogleMaps', '3.3.0' pod 'ZXingObjC', '3.6.4' - pod 'PPBlinkID', '~> 5.3.0' pod 'HKDFKit', '0.0.3' # 'Firebase/MLVisionBarcodeModel' is required by 'firebase_ml_vision' plugin from pubspec.yaml. diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 48f3625a..fc4f09cf 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -621,7 +621,6 @@ "${BUILT_PRODUCTS_DIR}/HKDFKit/HKDFKit.framework", "${BUILT_PRODUCTS_DIR}/MTBBarcodeScanner/MTBBarcodeScanner.framework", "${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework", - "${PODS_ROOT}/PPBlinkID/Microblink.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework", @@ -667,7 +666,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HKDFKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MTBBarcodeScanner.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Microblink.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", @@ -709,12 +707,10 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", - "${PODS_ROOT}/PPBlinkID/Microblink.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Microblink.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m index 3e24864b..5022f994 100644 --- a/ios/Runner/AppDelegate.m +++ b/ios/Runner/AppDelegate.m @@ -38,7 +38,6 @@ #import #import #import -#import #import #import @@ -59,7 +58,7 @@ @interface LaunchScreenView : UIView UIInterfaceOrientation _interfaceOrientationFromMask(UIInterfaceOrientationMask value); UIInterfaceOrientationMask _interfaceOrientationToMask(UIInterfaceOrientation value); -@interface AppDelegate() { +@interface AppDelegate() { } // Flutter @@ -74,13 +73,6 @@ @interface AppDelegate() implements NotificationsListener { + String _lastRunVersion; String _upgradeRequiredVersion; String _upgradeAvailableVersion; Key key = UniqueKey(); @@ -128,6 +129,7 @@ class _AppState extends State implements NotificationsListener { Onboarding.notifyFinished, Config.notifyUpgradeAvailable, Config.notifyUpgradeRequired, + Config.notifyOnboardingRequired, Organizations.notifyOrganizationChanged, Organizations.notifyEnvironmentChanged, UserProfile.notifyProfileDeleted, @@ -135,18 +137,13 @@ class _AppState extends State implements NotificationsListener { AppLivecycle.instance.ensureBinding(); + _lastRunVersion = Storage().lastRunVersion; _upgradeRequiredVersion = Config().upgradeRequiredVersion; _upgradeAvailableVersion = Config().upgradeAvailableVersion; - // This is just a placeholder to take some action on app upgrade. - String lastRunVersion = Storage().lastRunVersion; - if ((lastRunVersion == null) || (lastRunVersion != Config().appVersion)) { - - // Force unboarding to concent vaccination (#651) - if (AppVersion.compareVersions(lastRunVersion, '2.10.28') < 0) { - Storage().onBoardingPassed = false; - } - + _checkForceOnboarding(); + + if ((_lastRunVersion == null) || (_lastRunVersion != Config().appVersion)) { Storage().lastRunVersion = Config().appVersion; } @@ -211,6 +208,20 @@ class _AppState extends State implements NotificationsListener { Navigator.pushAndRemoveUntil(context, routeToHome, (_) => false); } + bool _checkForceOnboarding() { + // Action: Force unboarding to concent vaccination (#651, #681) + String onboardingRequiredVersion = Config().onboardingRequiredVersion; + if ((Storage().onBoardingPassed == true) && + (_lastRunVersion != null) && + (onboardingRequiredVersion != null) && + (AppVersion.compareVersions(_lastRunVersion, onboardingRequiredVersion) < 0) && + (AppVersion.compareVersions(onboardingRequiredVersion, Config().appVersion) <= 0)) { + Storage().onBoardingPassed = false; + return true; + } + return false; + } + // NotificationsListener @override @@ -228,6 +239,11 @@ class _AppState extends State implements NotificationsListener { _upgradeAvailableVersion = param; }); } + else if (name == Config.notifyOnboardingRequired) { + if (_checkForceOnboarding()) { + _resetUI(); + } + } else if (name == Organizations.notifyOrganizationChanged) { _resetUI(); } diff --git a/lib/model/Health.dart b/lib/model/Health.dart index 4afb08cd..16349a49 100644 --- a/lib/model/Health.dart +++ b/lib/model/Health.dart @@ -126,13 +126,17 @@ class HealthStatusBlob { final String nextStepHtml; final DateTime nextStepDateUtc; + final String warning; + final String warningHtml; + final String eventExplanation; final String eventExplanationHtml; - final String warning; - final String warningHtml; + final String statusUpdateNotice; + final String statusUpdateNoticeHtml; - final String reason; + final String statusUpdateReason; + final String statusUpdateReasonHtml; final dynamic fcmTopic; @@ -141,7 +145,13 @@ class HealthStatusBlob { static const String _nextStepDateMacro = '{next_step_date}'; static const String _nextStepDateFormat = 'EEEE, MMM d'; - HealthStatusBlob({this.code, this.priority, this.nextStep, this.nextStepHtml, this.nextStepDateUtc, this.eventExplanation, this.eventExplanationHtml, this.warning, this.warningHtml, this.reason, this.fcmTopic, this.historyBlob}); + HealthStatusBlob({this.code, this.priority, + this.nextStep, this.nextStepHtml, this.nextStepDateUtc, + this.warning, this.warningHtml, + this.eventExplanation, this.eventExplanationHtml, + this.statusUpdateNotice, this.statusUpdateNoticeHtml, + this.statusUpdateReason, this.statusUpdateReasonHtml, + this.fcmTopic, this.historyBlob}); factory HealthStatusBlob.fromJson(Map json) { return (json != null) ? HealthStatusBlob( @@ -150,11 +160,14 @@ class HealthStatusBlob { nextStep: json['next_step'], nextStepHtml: json['next_step_html'], nextStepDateUtc: healthDateTimeFromString(json['next_step_date']), - eventExplanation: json['event_explanation'], - eventExplanationHtml: json['event_explanation_html'], warning: json['warning'], warningHtml: json['warning_html'], - reason: json['reason'], + eventExplanation: json['event_explanation'], + eventExplanationHtml: json['event_explanation_html'], + statusUpdateNotice: json['notice'], + statusUpdateNoticeHtml: json['notice_html'], + statusUpdateReason: json['reason'], + statusUpdateReasonHtml: json['reason_html'], fcmTopic: json['fcm_topic'], historyBlob: HealthHistoryBlob.fromJson(json['history_blob']), ) : null; @@ -167,11 +180,14 @@ class HealthStatusBlob { 'next_step': nextStep, 'next_step_html': nextStepHtml, 'next_step_date': healthDateTimeToString(nextStepDateUtc), - 'event_explanation': eventExplanation, - 'event_explanation_html': eventExplanationHtml, 'warning': warning, 'warning_html': warningHtml, - 'reason': reason, + 'event_explanation': eventExplanation, + 'event_explanation_html': eventExplanationHtml, + 'notice': statusUpdateNotice, + 'notice_html': statusUpdateNoticeHtml, + 'reason': statusUpdateReason, + 'reason_html': statusUpdateReasonHtml, 'fcm_topic': fcmTopic, 'history_blob': historyBlob?.toJson(), }; @@ -184,11 +200,14 @@ class HealthStatusBlob { (o.nextStep == nextStep) && (o.nextStepHtml == nextStepHtml) && (o.nextStepDateUtc == nextStepDateUtc) && - (o.eventExplanation == eventExplanation) && - (o.eventExplanationHtml == eventExplanationHtml) && (o.warning == warning) && (o.warningHtml == warningHtml) && - (o.reason == reason) && + (o.eventExplanation == eventExplanation) && + (o.eventExplanationHtml == eventExplanationHtml) && + (o.statusUpdateNotice == statusUpdateNotice) && + (o.statusUpdateNoticeHtml == statusUpdateNoticeHtml) && + (o.statusUpdateReason == statusUpdateReason) && + (o.statusUpdateReasonHtml == statusUpdateReasonHtml) && DeepCollectionEquality().equals(o.fcmTopic, fcmTopic) && (o.historyBlob == historyBlob); } @@ -199,14 +218,37 @@ class HealthStatusBlob { (nextStep?.hashCode ?? 0) ^ (nextStepHtml?.hashCode ?? 0) ^ (nextStepDateUtc?.hashCode ?? 0) ^ - (eventExplanation?.hashCode ?? 0) ^ - (eventExplanationHtml?.hashCode ?? 0) ^ (warning?.hashCode ?? 0) ^ (warningHtml?.hashCode ?? 0) ^ - (reason?.hashCode ?? 0) ^ + (eventExplanation?.hashCode ?? 0) ^ + (eventExplanationHtml?.hashCode ?? 0) ^ + (statusUpdateNotice?.hashCode ?? 0) ^ + (statusUpdateNoticeHtml?.hashCode ?? 0) ^ + (statusUpdateReason?.hashCode ?? 0) ^ + (statusUpdateReasonHtml?.hashCode ?? 0) ^ (DeepCollectionEquality().hash(fcmTopic) ?? 0) ^ (historyBlob?.hashCode ?? 0); + factory HealthStatusBlob.fromRuleStatus(HealthRuleStatus ruleStatus, { HealthRulesSet rules, HealthStatusBlob previousStatusBlob, HealthHistoryBlob historyBlob }) { + return (ruleStatus != null) ? HealthStatusBlob( + code: (ruleStatus.code != null) ? ruleStatus.code : previousStatusBlob?.code, + priority: (ruleStatus.priority != null) ? ruleStatus.priority.abs() : previousStatusBlob?.priority, + nextStep: ((ruleStatus.nextStep != null) || (ruleStatus.nextStepHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.nextStep) : previousStatusBlob?.nextStep, + nextStepHtml: ((ruleStatus.nextStep != null) || (ruleStatus.nextStepHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.nextStepHtml) : previousStatusBlob?.nextStepHtml, + nextStepDateUtc: ((ruleStatus.nextStepInterval != null) || (ruleStatus.nextStep != null) || (ruleStatus.nextStepHtml != null) || (ruleStatus.code != null)) ? ruleStatus.nextStepDateUtc : previousStatusBlob?.nextStepDateUtc, + eventExplanation: ((ruleStatus.eventExplanation != null) || (ruleStatus.eventExplanationHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.eventExplanation) : previousStatusBlob?.eventExplanation, + eventExplanationHtml: ((ruleStatus.eventExplanation != null) || (ruleStatus.eventExplanationHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.eventExplanationHtml) : previousStatusBlob?.eventExplanationHtml, + warning: ((ruleStatus.warning != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.warning) : previousStatusBlob?.warning, + warningHtml: ((ruleStatus.warningHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.warningHtml) : previousStatusBlob?.warningHtml, + statusUpdateNotice: ((ruleStatus.statusUpdateNotice != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.statusUpdateNotice) : previousStatusBlob?.statusUpdateNotice, + statusUpdateNoticeHtml: ((ruleStatus.statusUpdateNoticeHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.statusUpdateNoticeHtml) : previousStatusBlob?.statusUpdateNoticeHtml, + statusUpdateReason: ((ruleStatus.statusUpdateReason != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.statusUpdateReason) : previousStatusBlob?.statusUpdateReason, + statusUpdateReasonHtml: ((ruleStatus.statusUpdateReasonHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.statusUpdateReasonHtml) : previousStatusBlob?.statusUpdateReasonHtml, + fcmTopic: ((ruleStatus.fcmTopic != null) || (ruleStatus.code != null)) ? ruleStatus.fcmTopic : previousStatusBlob?.fcmTopic, + historyBlob: historyBlob, + ) : null; + } + String get displayNextStep { return _processMacros(nextStep); } @@ -231,6 +273,14 @@ class HealthStatusBlob { return null; } + String get displayWarning { + return _processMacros(warning); + } + + String get displayWarningHtml { + return _processMacros(warningHtml); + } + String get displayEventExplanation { return _processMacros(eventExplanation); } @@ -239,16 +289,20 @@ class HealthStatusBlob { return _processMacros(eventExplanationHtml); } - String get displayWarning { - return _processMacros(warning); + String get displayStatusUpdateNotice { + return _processMacros(statusUpdateNotice); } - String get displayWarningHtml { - return _processMacros(warningHtml); + String get displayStatusUpdateNoticeHtml { + return _processMacros(statusUpdateNoticeHtml); } - String get displayReason { - return _processMacros(reason); + String get displayStatusUpdateReason { + return _processMacros(statusUpdateReason); + } + + String get displayStatusUpdateReasonHtml { + return _processMacros(statusUpdateReasonHtml); } String _processMacros(String value) { @@ -467,7 +521,7 @@ class HealthHistory implements Comparable { (this.blob?.providerId == event?.providerId) && (this.blob?.testType == event?.blob?.testType) && (this.blob?.testResult == event?.blob?.testResult) && - (ListEquality().equals(this.blob.extras, event.blob.extras)); + (DeepCollectionEquality().equals(this.blob.extras, event.blob.extras)); } else if (event.isVaccine) { return this.isVaccine && @@ -482,8 +536,8 @@ class HealthHistory implements Comparable { (this.blob?.actionType == event?.blob?.actionType) && (DeepCollectionEquality().equals(this.blob?.actionText, event?.blob?.actionText)) && (DeepCollectionEquality().equals(this.blob?.actionTitle, event?.blob?.actionTitle)) && - (MapEquality().equals(this.blob?.actionParams, event?.blob?.actionParams)) && - (ListEquality().equals(this.blob.extras, event.blob.extras)); + (DeepCollectionEquality().equals(this.blob?.actionParams, event?.blob?.actionParams)) && + (DeepCollectionEquality().equals(this.blob.extras, event.blob.extras)); } else { return false; @@ -678,7 +732,6 @@ class HealthHistoryBlob { final List extras; static const String VaccineEffective = "Effective"; - static const String VaccineTaken = "Taken"; HealthHistoryBlob({ this.provider, this.providerId, this.location, this.locationId, this.countyId, this.testType, this.testResult, @@ -752,7 +805,7 @@ class HealthHistoryBlob { (o.testType == testType) && (o.testResult == testResult) && - ListEquality().equals(o.symptoms, symptoms) && + DeepCollectionEquality().equals(o.symptoms, symptoms) && (o.traceDuration == traceDuration) && (o.traceTEK == traceTEK) && @@ -762,9 +815,9 @@ class HealthHistoryBlob { (o.actionType == actionType) && DeepCollectionEquality().equals(o.actionTitle, actionTitle) && DeepCollectionEquality().equals(o.actionText, actionText) && - MapEquality().equals(o.actionParams, actionParams) && + DeepCollectionEquality().equals(o.actionParams, actionParams) && - ListEquality().equals(o.extras, extras); + DeepCollectionEquality().equals(o.extras, extras); } int get hashCode => @@ -776,7 +829,7 @@ class HealthHistoryBlob { (testType?.hashCode ?? 0) ^ (testResult?.hashCode ?? 0) ^ - ListEquality().hash(symptoms) ^ + DeepCollectionEquality().hash(symptoms) ^ (traceDuration?.hashCode ?? 0) ^ (traceTEK?.hashCode ?? 0) ^ @@ -786,9 +839,9 @@ class HealthHistoryBlob { (actionType?.hashCode ?? 0) ^ (DeepCollectionEquality().hash(actionTitle) ?? 0) ^ (DeepCollectionEquality().hash(actionText) ?? 0) ^ - (MapEquality().hash(actionParams) ?? 0) ^ + (DeepCollectionEquality().hash(actionParams) ?? 0) ^ - (ListEquality().hash(extras) ?? 0); + (DeepCollectionEquality().hash(extras) ?? 0); bool get isTest { return (testType != null) && (testResult != null); @@ -810,10 +863,6 @@ class HealthHistoryBlob { return (vaccine != null) && (vaccine.toLowerCase() == VaccineEffective.toLowerCase()); } - bool get isVaccineTaken { - return (vaccine != null) && (vaccine.toLowerCase() == VaccineTaken.toLowerCase()); - } - bool get isAction { return (actionType != null) || (actionTitle != null) || (actionText != null) || (actionParams != null); } @@ -1280,7 +1329,7 @@ class HealthUser { o.consentVaccineInformation == consentVaccineInformation && o.consentExposureNotification == consentExposureNotification && o.repost == repost && - ListEquality().equals(o.accounts, accounts) && + DeepCollectionEquality().equals(o.accounts, accounts) && o.encryptedKey == encryptedKey && o.encryptedBlob == encryptedBlob; @@ -1291,7 +1340,7 @@ class HealthUser { (consentVaccineInformation?.hashCode ?? 0) ^ (consentExposureNotification?.hashCode ?? 0) ^ (repost?.hashCode ?? 0) ^ - ListEquality().hash(accounts) ^ + DeepCollectionEquality().hash(accounts) ^ (encryptedKey?.hashCode ?? 0) ^ (encryptedBlob?.hashCode ?? 0); @@ -2537,14 +2586,14 @@ class HealthSymptomsGroup { (o.name == name) && (o.visible == visible) && (o.group == group) && - ListEquality().equals(o.symptoms, symptoms); + DeepCollectionEquality().equals(o.symptoms, symptoms); int get hashCode => (id?.hashCode ?? 0) ^ (name?.hashCode ?? 0) ^ (visible?.hashCode ?? 0) ^ (group?.hashCode ?? 0) ^ - ListEquality().hash(symptoms); + DeepCollectionEquality().hash(symptoms); static Map getCounts(List groups, Set selected) { Map counts = Map(); @@ -2646,6 +2695,21 @@ class HealthRulesSet { ) : null; } + Map toJson() { + return { + 'tests': tests?.toJson(), + 'symptoms': symptoms?.toJson(), + 'contact_trace': contactTrace?.toJson(), + 'vaccines': vaccines?.toJson(), + 'actions': actions?.toJson(), + 'defaults': defaults?.toJson(), + 'codes': codes?.toJson(), + 'statuses': _HealthRuleStatus.mapToJson(statuses), + 'intervals': _HealthRuleInterval.mapToJson(intervals), + 'strings': strings, + }; + } + bool operator ==(o) { return (o is HealthRulesSet) && (o.tests == tests) && @@ -2655,8 +2719,8 @@ class HealthRulesSet { (o.actions == actions) && (o.defaults == defaults) && (o.codes == codes) && - MapEquality().equals(o.statuses, statuses) && - MapEquality().equals(o.intervals, intervals) && + DeepCollectionEquality().equals(o.statuses, statuses) && + DeepCollectionEquality().equals(o.intervals, intervals) && DeepCollectionEquality().equals(o.strings, strings); } @@ -2668,8 +2732,8 @@ class HealthRulesSet { (actions?.hashCode ?? 0) ^ (defaults?.hashCode ?? 0) ^ (codes?.hashCode ?? 0) ^ - MapEquality().hash(statuses) ^ - MapEquality().hash(intervals) ^ + DeepCollectionEquality().hash(statuses) ^ + DeepCollectionEquality().hash(intervals) ^ DeepCollectionEquality().hash(strings); _HealthRuleInterval _getInterval(String name) { @@ -2719,6 +2783,12 @@ class HealthDefaultsSet { ) : null; } + Map toJson() { + return { + 'status': status?.toJson(), + }; + } + bool operator ==(o) => (o is HealthDefaultsSet) && (o.status == status); @@ -2748,14 +2818,21 @@ class HealthCodesSet { ) : null; } + Map toJson() { + return { + 'list': HealthCodeData.listToJson(_codesList), + 'info': _info + }; + } + bool operator ==(o) => (o is HealthCodesSet) && - ListEquality().equals(o._codesList, _codesList) && - ListEquality().equals(o._info, _info); + DeepCollectionEquality().equals(o._codesList, _codesList) && + DeepCollectionEquality().equals(o._info, _info); int get hashCode => - ListEquality().hash(_codesList) ^ - ListEquality().hash(_info); + DeepCollectionEquality().hash(_codesList) ^ + DeepCollectionEquality().hash(_info); List get list { return _codesList; @@ -2809,6 +2886,18 @@ class HealthCodeData { ) : null; } + Map toJson() { + return { + 'code': code, + 'color': _colorString, + 'name': _name, + 'description': _description, + 'long_description': _longDescription, + 'visible': visible, + 'reports_exposures': reportsExposures + }; + } + bool operator ==(o) => (o is HealthCodeData) && (o.code == code) && @@ -2862,6 +2951,17 @@ class HealthCodeData { return values; } + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthCodeData value in values) { + json.add(value?.toJson()); + } + } + return json; + } + static Map mapFromList(List list) { Map map; if (list != null) { @@ -2890,12 +2990,18 @@ class HealthTestRulesSet { ) : null; } + Map toJson() { + return { + 'rules': HealthTestRule.listToJson(_rules), + }; + } + bool operator ==(o) => (o is HealthTestRulesSet) && - ListEquality().equals(o._rules, _rules); + DeepCollectionEquality().equals(o._rules, _rules); int get hashCode => - ListEquality().hash(_rules); + DeepCollectionEquality().hash(_rules); HealthTestRuleResult matchRuleResult({ HealthHistoryBlob blob, HealthRulesSet rules }) { if ((_rules != null) && (blob != null) && (blob.testType != null) && (blob.testResult != null)) { @@ -2932,16 +3038,24 @@ class HealthTestRule { ) : null; } + Map toJson() { + return { + 'test_type': testType, + 'category': category, + 'results': HealthTestRuleResult.listToJson(results), + }; + } + bool operator ==(o) => (o is HealthTestRule) && (o.testType == testType) && (o.category == category) && - ListEquality().equals(o.results, results); + DeepCollectionEquality().equals(o.results, results); int get hashCode => (testType?.hashCode ?? 0) ^ (category?.hashCode ?? 0) ^ - ListEquality().hash(results); + DeepCollectionEquality().hash(results); static List listFromJson(List json) { List values; @@ -2954,6 +3068,17 @@ class HealthTestRule { } return values; } + + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthTestRule value in values) { + json.add(value?.toJson()); + } + } + return json; + } } /////////////////////////////// @@ -2976,12 +3101,21 @@ class HealthTestRuleResult { ) : null; } + Map toJson() { + return { + 'result': testResult, + 'category': category, + 'disclaimer_html': disclaimerHtml, + 'status': status?.toJson(), + }; + } + bool operator ==(o) => (o is HealthTestRuleResult) && (o.testResult == testResult) && (o.category == category) && (o.disclaimerHtml == disclaimerHtml) && - (status == status); + (o.status == status); int get hashCode => (testResult?.hashCode ?? 0) ^ @@ -3001,6 +3135,17 @@ class HealthTestRuleResult { return values; } + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthTestRuleResult value in values) { + json.add(value?.toJson()); + } + } + return json; + } + static HealthTestRuleResult matchRuleResult(List results, { HealthHistoryBlob blob }) { if (results != null) { for (HealthTestRuleResult result in results) { @@ -3033,14 +3178,21 @@ class HealthSymptomsRulesSet { ) : null; } + Map toJson() { + return { + 'rules': HealthSymptomsRule.listToJson(_rules), + 'groups': HealthSymptomsGroup.listToJson(groups), + }; + } + bool operator ==(o) => (o is HealthSymptomsRulesSet) && - ListEquality().equals(o._rules, _rules) && - ListEquality().equals(o.groups, groups); + DeepCollectionEquality().equals(o._rules, _rules) && + DeepCollectionEquality().equals(o.groups, groups); int get hashCode => - ListEquality().hash(_rules) ^ - ListEquality().hash(groups); + DeepCollectionEquality().hash(_rules) ^ + DeepCollectionEquality().hash(groups); HealthSymptomsRule matchRule({ HealthHistoryBlob blob, HealthRulesSet rules }) { if ((_rules != null) && (groups != null) && (blob?.symptomsIds != null)) { @@ -3071,13 +3223,20 @@ class HealthSymptomsRule { ) : null; } + Map toJson() { + return { + 'counts': _HealthRuleInterval.mapToJson(counts), + 'status': status?.toJson() + }; + } + bool operator ==(o) => (o is HealthSymptomsRule) && - MapEquality().equals(o.counts, counts) && + DeepCollectionEquality().equals(o.counts, counts) && (o.status == status); int get hashCode => - MapEquality().hash(counts) ^ + DeepCollectionEquality().hash(counts) ^ (status?.hashCode ?? 0); static List listFromJson(List json) { @@ -3092,6 +3251,17 @@ class HealthSymptomsRule { return values; } + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthSymptomsRule value in values) { + json.add(value?.toJson()); + } + } + return json; + } + bool _matchCounts(Map testCounts, { HealthRulesSet rules }) { if (this.counts != null) { for (String groupName in this.counts.keys) { @@ -3121,12 +3291,18 @@ class HealthContactTraceRulesSet { ) : null; } + Map toJson() { + return { + 'rules': HealthContactTraceRule.listToJson(_rules), + }; + } + bool operator ==(o) => (o is HealthContactTraceRulesSet) && - ListEquality().equals(o._rules, _rules); + DeepCollectionEquality().equals(o._rules, _rules); int get hashCode => - ListEquality().hash(_rules); + DeepCollectionEquality().hash(_rules); HealthContactTraceRule matchRule({ HealthHistoryBlob blob, HealthRulesSet rules }) { @@ -3166,6 +3342,13 @@ class HealthContactTraceRule { ) : null; } + Map toJson() { + return { + 'duration': duration?.toJson(), + 'status': status?.toJson(), + }; + } + static List listFromJson(List json) { List values; if (json != null) { @@ -3178,6 +3361,17 @@ class HealthContactTraceRule { return values; } + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthContactTraceRule value in values) { + json.add(value?.toJson()); + } + } + return json; + } + bool _matchBlob(HealthHistoryBlob blob, { HealthRulesSet rules }) { return (duration != null) && duration.match(blob?.traceDurationInMinutes, rules: rules); } @@ -3197,12 +3391,18 @@ class HealthVaccineRulesSet { ) : null; } + Map toJson() { + return { + 'rules': HealthVaccineRule.listToJson(_rules), + }; + } + bool operator ==(o) => (o is HealthVaccineRulesSet) && - ListEquality().equals(o._rules, _rules); + DeepCollectionEquality().equals(o._rules, _rules); int get hashCode => - ListEquality().hash(_rules); + DeepCollectionEquality().hash(_rules); HealthVaccineRule matchRule({ HealthHistoryBlob blob, HealthRulesSet rules }) { if (_rules != null) { @@ -3232,6 +3432,13 @@ class HealthVaccineRule { ) : null; } + Map toJson() { + return { + 'vaccine': vaccine, + 'status': status?.toJson(), + }; + } + bool operator ==(o) => (o is HealthVaccineRule) && (o.vaccine == vaccine) && @@ -3253,6 +3460,17 @@ class HealthVaccineRule { return values; } + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthVaccineRule value in values) { + json.add(value?.toJson()); + } + } + return json; + } + bool _matchBlob(HealthHistoryBlob blob, {HealthRulesSet rules}) { return (vaccine != null) && (vaccine.toLowerCase() == blob?.vaccine?.toLowerCase()); } @@ -3272,12 +3490,18 @@ class HealthActionRulesSet { ) : null; } + Map toJson() { + return { + 'rules': HealthActionRule.listToJson(_rules), + }; + } + bool operator ==(o) => (o is HealthActionRulesSet) && - ListEquality().equals(o._rules, _rules); + DeepCollectionEquality().equals(o._rules, _rules); int get hashCode => - ListEquality().hash(_rules); + DeepCollectionEquality().hash(_rules); HealthActionRule matchRule({ HealthHistoryBlob blob, HealthRulesSet rules }) { if (_rules != null) { @@ -3307,6 +3531,13 @@ class HealthActionRule { ) : null; } + Map toJson() { + return { + 'type': type, + 'status': status?.toJson() + }; + } + bool operator ==(o) => (o is HealthActionRule) && (o.type == type) && @@ -3328,6 +3559,17 @@ class HealthActionRule { return values; } + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthActionRule value in values) { + json.add(value?.toJson()); + } + } + return json; + } + bool _matchBlob(HealthHistoryBlob blob, {HealthRulesSet rules}) { return (type != null) && (type.toLowerCase() == blob?.actionType?.toLowerCase()); } @@ -3357,6 +3599,8 @@ abstract class _HealthRuleStatus { return null; } + dynamic toJson(); + static Map mapFromJson(Map json) { Map result; if (json != null) { @@ -3369,6 +3613,17 @@ abstract class _HealthRuleStatus { return result; } + static Map mapToJson(Map values) { + Map json; + if (values != null) { + json = Map(); + values.forEach((key, value) { + json[key] = value?.toJson(); + }); + } + return json; + } + HealthRuleStatus eval({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }); } @@ -3385,52 +3640,85 @@ class HealthRuleStatus extends _HealthRuleStatus { final _HealthRuleInterval nextStepInterval; final DateTime nextStepDateUtc; + final dynamic warning; + final dynamic warningHtml; + final dynamic eventExplanation; final dynamic eventExplanationHtml; - final dynamic warning; - final dynamic warningHtml; + final dynamic statusUpdateNotice; + final dynamic statusUpdateNoticeHtml; - final dynamic reason; + final dynamic statusUpdateReason; + final dynamic statusUpdateReasonHtml; final dynamic fcmTopic; HealthRuleStatus({this.code, this.priority, this.nextStep, this.nextStepHtml, this.nextStepInterval, this.nextStepDateUtc, + this.warning, this.warningHtml, this.eventExplanation, this.eventExplanationHtml, - this.warning, this.warningHtml, this.reason, this.fcmTopic }); + this.statusUpdateNotice, this.statusUpdateNoticeHtml, + this.statusUpdateReason, this.statusUpdateReasonHtml, + this.fcmTopic }); factory HealthRuleStatus.fromJson(Map json) { return (json != null) ? HealthRuleStatus( - code: json['code'], - priority: json['priority'], - nextStep: json['next_step'], - nextStepHtml: json['next_step_html'], - nextStepInterval: _HealthRuleInterval.fromJson(json['next_step_interval']), - eventExplanation: json['event_explanation'], - eventExplanationHtml: json['event_explanation_html'], - warning: json['warning'], - warningHtml: json['warning_html'], - reason: json['reason'], - fcmTopic: json['fcm_topic'] + code: json['code'], + priority: json['priority'], + nextStep: json['next_step'], + nextStepHtml: json['next_step_html'], + nextStepInterval: _HealthRuleInterval.fromJson(json['next_step_interval']), + warning: json['warning'], + warningHtml: json['warning_html'], + eventExplanation: json['event_explanation'], + eventExplanationHtml: json['event_explanation_html'], + statusUpdateNotice: json['notice'], + statusUpdateNoticeHtml: json['notice_html'], + statusUpdateReason: json['reason'], + statusUpdateReasonHtml: json['reason_html'], + fcmTopic: json['fcm_topic'] ) : null; } + @override + dynamic toJson() { + return { + 'code': code, + 'priority': priority, + 'next_step': nextStep, + 'next_step_html': nextStepHtml, + 'next_step_interval': nextStepInterval?.toJson(), + 'warning': warning, + 'warning_html': warningHtml, + 'event_explanation': eventExplanation, + 'event_explanation_html': eventExplanationHtml, + 'notice': statusUpdateNotice, + 'notice_html': statusUpdateNoticeHtml, + 'reason': statusUpdateReason, + 'reason_html': statusUpdateReasonHtml, + 'fcm_topic': fcmTopic, + }; + } + factory HealthRuleStatus.fromStatus(HealthRuleStatus status, { DateTime nextStepDateUtc, }) { return (status != null) ? HealthRuleStatus( - code: status.code, - priority: status.priority, - nextStep: status.nextStep, - nextStepHtml: status.nextStepHtml, - nextStepInterval: status.nextStepInterval, - nextStepDateUtc: nextStepDateUtc ?? status.nextStepDateUtc, - eventExplanation: status.eventExplanation, - eventExplanationHtml: status.eventExplanationHtml, - warning: status.warning, - warningHtml: status.warningHtml, - reason: status.reason, - fcmTopic: status.fcmTopic, + code: status.code, + priority: status.priority, + nextStep: status.nextStep, + nextStepHtml: status.nextStepHtml, + nextStepInterval: status.nextStepInterval, + nextStepDateUtc: nextStepDateUtc ?? status.nextStepDateUtc, + warning: status.warning, + warningHtml: status.warningHtml, + eventExplanation: status.eventExplanation, + eventExplanationHtml: status.eventExplanationHtml, + statusUpdateNotice: status.statusUpdateNotice, + statusUpdateNoticeHtml: status.statusUpdateNoticeHtml, + statusUpdateReason: status.statusUpdateReason, + statusUpdateReasonHtml: status.statusUpdateReasonHtml, + fcmTopic: status.fcmTopic, ) : null; } @@ -3444,13 +3732,17 @@ class HealthRuleStatus extends _HealthRuleStatus { (o.nextStepInterval == nextStepInterval) && (o.nextStepDateUtc == nextStepDateUtc) && + (o.warning == warning) && + (o.warningHtml == warningHtml) && + (o.eventExplanation == eventExplanation) && (o.eventExplanationHtml == eventExplanationHtml) && - (o.warning == warning) && - (o.warningHtml == warningHtml) && + (o.statusUpdateNotice == statusUpdateNotice) && + (o.statusUpdateNoticeHtml == statusUpdateNoticeHtml) && - (o.reason == reason) && + (o.statusUpdateReason == statusUpdateReason) && + (o.statusUpdateReasonHtml == statusUpdateReasonHtml) && (o.fcmTopic == fcmTopic); @@ -3463,13 +3755,17 @@ class HealthRuleStatus extends _HealthRuleStatus { (nextStepInterval?.hashCode ?? 0) ^ (nextStepDateUtc?.hashCode ?? 0) ^ + (warning?.hashCode ?? 0) ^ + (warningHtml?.hashCode ?? 0) ^ + (eventExplanation?.hashCode ?? 0) ^ (eventExplanationHtml?.hashCode ?? 0) ^ - (warning?.hashCode ?? 0) ^ - (warningHtml?.hashCode ?? 0) ^ + (statusUpdateNotice?.hashCode ?? 0) ^ + (statusUpdateNoticeHtml?.hashCode ?? 0) ^ - (reason?.hashCode ?? 0) ^ + (statusUpdateReason?.hashCode ?? 0) ^ + (statusUpdateReasonHtml?.hashCode ?? 0) ^ (fcmTopic?.hashCode ?? 0); @@ -3505,6 +3801,11 @@ class HealthRuleReferenceStatus extends _HealthRuleStatus { ) : null; } + @override + dynamic toJson() { + return reference; + } + bool operator ==(o) => (o is HealthRuleReferenceStatus) && (o.reference == reference); @@ -3539,6 +3840,16 @@ class HealthRuleConditionalStatus extends _HealthRuleStatus with HealthRuleCondi ) : null; } + @override + dynamic toJson() { + return { + 'condition': condition, + 'params': conditionParams, + 'success': successStatus?.toJson(), + 'fail': failStatus?.toJson(), + }; + } + static bool isJsonCompatible(dynamic json) { return (json is Map) && (json['condition'] is String); } @@ -3546,13 +3857,13 @@ class HealthRuleConditionalStatus extends _HealthRuleStatus with HealthRuleCondi bool operator ==(o) => (o is HealthRuleConditionalStatus) && (o.condition == condition) && - (MapEquality().equals(o.conditionParams, conditionParams)) && + (DeepCollectionEquality().equals(o.conditionParams, conditionParams)) && (o.successStatus == successStatus) && (o.failStatus == failStatus); int get hashCode => (condition?.hashCode ?? 0) ^ - (MapEquality().hash(conditionParams)) ^ + (DeepCollectionEquality().hash(conditionParams)) ^ (successStatus?.hashCode ?? 0) ^ (failStatus?.hashCode ?? 0); @@ -3577,6 +3888,10 @@ abstract class _HealthRuleInterval { else if (json is String) { return HealthRuleIntervalReference.fromJson(json); } + else if (json is List) { + try { return HealthRuleIntervalSet.fromJson(json.cast()); } + catch (e) { print(e?.toString()); } + } else if (json is Map) { if (HealthRuleIntervalCondition.isJsonCompatible(json)) { try { return HealthRuleIntervalCondition.fromJson(json.cast()); } @@ -3590,7 +3905,9 @@ abstract class _HealthRuleInterval { return null; } - bool match(int value, { List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }); + dynamic toJson(); + + bool match(int value, { DateTime orgDate, List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }); int value({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }); bool valid({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }); int scope({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }); @@ -3607,6 +3924,55 @@ abstract class _HealthRuleInterval { } return result; } + + static Map mapToJson(Map values) { + Map json; + if (values != null) { + json = Map(); + values.forEach((key, value) { + json[key] = value?.toJson(); + }); + } + return json; + } + + static List<_HealthRuleInterval> listFromJson(List json) { + List<_HealthRuleInterval> values; + if (json != null) { + values = <_HealthRuleInterval>[]; + for (dynamic entry in json) { + try { values.add(_HealthRuleInterval.fromJson(entry)); } + catch(e) { print(e?.toString()); } + } + } + return values; + } + + static List listToJson(List<_HealthRuleInterval> values) { + List json; + if (values != null) { + json = []; + for (_HealthRuleInterval value in values) { + json.add(value?.toJson()); + } + } + return json; + } + + static int applyWeekdayExtent(_HealthRuleInterval weekdayExtent, DateTime orgDate, int value, int step, { List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params } ) { + if ((weekdayExtent != null) && (orgDate != null) && (value != null) && (step != null)) { + //DateTime dateExt = orgDate.add(Duration(days: value + step)); + DateTime dateExt = DateTime(orgDate.year, orgDate.month, orgDate.day + value + step, orgDate.hour, orgDate.minute, orgDate.second); + while (weekdayExtent.match(dateExt.weekday, orgDate: orgDate, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params)) { + value += step; + // dateExt = dateExt.add(Duration(days: step)); + dateExt = DateTime(dateExt.year, dateExt.month, dateExt.day + step, orgDate.hour, orgDate.minute, orgDate.second); + } + return value; + } + return null; + } + } enum HealthRuleIntervalOrigin { historyDate, referenceDate } @@ -3624,6 +3990,11 @@ class HealthRuleIntervalValue extends _HealthRuleInterval { return (json is int) ? HealthRuleIntervalValue(value: json) : null; } + @override + dynamic toJson() { + return _value; + } + bool operator ==(o) => (o is HealthRuleIntervalValue) && (o._value == _value); @@ -3631,7 +4002,7 @@ class HealthRuleIntervalValue extends _HealthRuleInterval { int get hashCode => (_value?.hashCode ?? 0); - @override bool match(int value, { List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) {return (_value == value); } + @override bool match(int value, { DateTime orgDate, List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) {return (_value == value); } @override int value({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return _value; } @override bool valid({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return (_value != null); } @override int scope({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return null; } @@ -3655,14 +4026,18 @@ class HealthRuleInterval extends _HealthRuleInterval { final int _scope; final bool _current; final HealthRuleIntervalOrigin _origin; + final _HealthRuleInterval _minWeekdaysExtent; + final _HealthRuleInterval _maxWeekdaysExtent; - HealthRuleInterval({_HealthRuleInterval min, _HealthRuleInterval max, _HealthRuleInterval value, int scope, bool current, HealthRuleIntervalOrigin origin}) : + HealthRuleInterval({_HealthRuleInterval min, _HealthRuleInterval max, _HealthRuleInterval value, int scope, bool current, HealthRuleIntervalOrigin origin, _HealthRuleInterval minWeekdaysExtent, _HealthRuleInterval maxWeekdaysExtent }) : _min = min, _max = max, _value = value, _scope = scope, _current = current, - _origin = origin; + _origin = origin, + _minWeekdaysExtent = minWeekdaysExtent, + _maxWeekdaysExtent = maxWeekdaysExtent; factory HealthRuleInterval.fromJson(Map json) { return (json != null) ? HealthRuleInterval( @@ -3672,9 +4047,25 @@ class HealthRuleInterval extends _HealthRuleInterval { scope: _scopeFromJson(json['scope']), current: json['current'], origin: _originFromJson(json['origin']), + minWeekdaysExtent: _HealthRuleInterval.fromJson(json['min-weekdays-extent']), + maxWeekdaysExtent: _HealthRuleInterval.fromJson(json['max-weekdays-extent']), ) : null; } + @override + dynamic toJson() { + return { + 'min': _min?.toJson(), + 'max': _max?.toJson(), + 'value': _value?.toJson(), + 'scope': _scopeToJson(_scope), + 'current': _current, + 'origin': _originToJson(_origin), + 'min-weekdays-extent': _minWeekdaysExtent?.toJson(), + 'max-weekdays-extent': _maxWeekdaysExtent?.toJson(), + }; + } + bool operator ==(o) => (o is HealthRuleInterval) && (o._min == _min) && @@ -3682,7 +4073,9 @@ class HealthRuleInterval extends _HealthRuleInterval { (o._value == _value) && (o._scope == _scope) && (o._current == _current) && - (o._origin == _origin); + (o._origin == _origin) && + (o._minWeekdaysExtent == _minWeekdaysExtent) && + (o._maxWeekdaysExtent == _maxWeekdaysExtent); int get hashCode => (_min?.hashCode ?? 0) ^ @@ -3690,25 +4083,39 @@ class HealthRuleInterval extends _HealthRuleInterval { (_value?.hashCode ?? 0) ^ (_scope?.hashCode ?? 0) ^ (_current?.hashCode ?? 0) ^ - (_origin?.hashCode ?? 0); + (_origin?.hashCode ?? 0) ^ + (_minWeekdaysExtent?.hashCode ?? 0) ^ + (_maxWeekdaysExtent?.hashCode ?? 0); @override - bool match(int value, { List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { + bool match(int value, { DateTime orgDate, List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { if (value != null) { if (_min != null) { int minValue = _min.value(history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); + minValue = _HealthRuleInterval.applyWeekdayExtent(_minWeekdaysExtent, orgDate, minValue, -1, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params) ?? minValue; if ((minValue == null) || (minValue > value)) { return false; } } if (_max != null) { int maxValue = _max.value(history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); + maxValue = _HealthRuleInterval.applyWeekdayExtent(_maxWeekdaysExtent, orgDate, maxValue, 1, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params) ?? maxValue; if ((maxValue == null) || (maxValue < value)) { return false; } } if (_value != null) { int valueValue = _value.value(history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); + + int minValue = _HealthRuleInterval.applyWeekdayExtent(_minWeekdaysExtent, orgDate, valueValue, -1, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); + if ((minValue != null) && (minValue > value)) { + return false; + } + int maxValue = _HealthRuleInterval.applyWeekdayExtent(_maxWeekdaysExtent, orgDate, valueValue, 1, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); + if ((maxValue != null) && (maxValue < value)) { + return false; + } + if ((valueValue == null) || (valueValue != value)) { return false; } @@ -3755,6 +4162,22 @@ class HealthRuleInterval extends _HealthRuleInterval { return null; } + static String _scopeToJson(int value) { + if (value == FutureScope) { + return 'future'; + } + else if (value == FutureAndCurrentScope) { + return 'future-and-current'; + } + else if (value == PastScope) { + return 'past'; + } + else if (value == PastAndCurrentScope) { + return 'past-and-current'; + } + return null; + } + static HealthRuleIntervalOrigin _originFromJson(dynamic value) { if (value == 'historyDate') { return HealthRuleIntervalOrigin.historyDate; @@ -3766,6 +4189,70 @@ class HealthRuleInterval extends _HealthRuleInterval { return null; } } + + static String _originToJson(HealthRuleIntervalOrigin value) { + if (value == HealthRuleIntervalOrigin.historyDate) { + return 'historyDate'; + } + else if (value == HealthRuleIntervalOrigin.referenceDate) { + return 'referenceDate'; + } + else { + return null; + } + } +} + +/////////////////////////////// +// HealthRuleIntervalSet + +class HealthRuleIntervalSet extends _HealthRuleInterval { + List<_HealthRuleInterval> _entries; + + HealthRuleIntervalSet({List<_HealthRuleInterval> entries}) : + _entries = entries; + + factory HealthRuleIntervalSet.fromJson(List json) { + List<_HealthRuleInterval> entries = _HealthRuleInterval.listFromJson(json); + return (entries != null) ? HealthRuleIntervalSet(entries: entries) : null; + } + + @override + dynamic toJson() { + return _HealthRuleInterval.listToJson(_entries); + } + + bool operator ==(o) => + (o is HealthRuleIntervalSet) && + DeepCollectionEquality().equals(o._entries, _entries); + + int get hashCode => + (DeepCollectionEquality().hash(_entries) ?? 0); + + @override + bool match(int value, { DateTime orgDate, List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { + for (_HealthRuleInterval entry in _entries) { + if (entry.match(value, orgDate: orgDate, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params)) { + return true; + } + } + return false; + } + + @override + bool valid({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { + for (_HealthRuleInterval entry in _entries) { + if (!entry.valid(history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params)) { + return false; + } + } + return 0 < _entries.length; + } + + @override int value({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return null; } + @override int scope({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return null; } + @override bool current({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return null; } + @override HealthRuleIntervalOrigin origin({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return null; } } /////////////////////////////// @@ -3781,6 +4268,11 @@ class HealthRuleIntervalReference extends _HealthRuleInterval { return (json is String) ? HealthRuleIntervalReference(reference: json) : null; } + @override + dynamic toJson() { + return _reference; + } + bool operator ==(o) => (o is HealthRuleIntervalReference) && (o._reference == _reference); @@ -3794,8 +4286,8 @@ class HealthRuleIntervalReference extends _HealthRuleInterval { } @override - bool match(int value, { List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { - return _referenceInterval(rules: rules, params: params)?.match(value, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params) ?? false; + bool match(int value, { DateTime orgDate, List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { + return _referenceInterval(rules: rules, params: params)?.match(value, orgDate: orgDate, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params) ?? false; } @override bool valid({ List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { return _referenceInterval(rules: rules, params: params)?.valid(history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params) ?? false; } @@ -3825,6 +4317,16 @@ class HealthRuleIntervalCondition extends _HealthRuleInterval with HealthRuleCon ) : null; } + @override + dynamic toJson() { + return { + 'condition': condition, + 'params': conditionParams, + 'success': successInterval?.toJson(), + 'fail': failInterval?.toJson(), + }; + } + static bool isJsonCompatible(dynamic json) { return (json is Map) && (json['condition'] is String); } @@ -3832,21 +4334,21 @@ class HealthRuleIntervalCondition extends _HealthRuleInterval with HealthRuleCon bool operator ==(o) => (o is HealthRuleIntervalCondition) && (o.condition == condition) && - (MapEquality().equals(o.conditionParams, conditionParams)) && + (DeepCollectionEquality().equals(o.conditionParams, conditionParams)) && (o.successInterval == successInterval) && (o.failInterval == failInterval); int get hashCode => (condition?.hashCode ?? 0) ^ - (MapEquality().hash(conditionParams)) ^ + (DeepCollectionEquality().hash(conditionParams)) ^ (successInterval?.hashCode ?? 0) ^ (failInterval?.hashCode ?? 0); @override - bool match(int value, { List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { + bool match(int value, { DateTime orgDate, List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { HealthRuleConditionResult conditionResult = evalCondition(history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); _HealthRuleInterval interval = (conditionResult?.result != null) ? (conditionResult.result ? successInterval : failInterval) : null; - return interval?.match(value, history: history, historyIndex: historyIndex, referenceIndex: conditionResult?.referenceIndex ?? referenceIndex, rules: rules, params: params) ?? false; + return interval?.match(value, orgDate: orgDate, history: history, historyIndex: historyIndex, referenceIndex: conditionResult?.referenceIndex ?? referenceIndex, rules: rules, params: params) ?? false; } @override @@ -3996,7 +4498,7 @@ abstract class HealthRuleCondition { //#572 Building access calculation issue //int difference = entryDateMidnightLocal.difference(originDateMidnightLocal).inDays; int difference = AppDateTime.midnightsDifferenceInDays(originDateMidnightLocal, entryDateMidnightLocal); - if (interval.match(difference, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params)) { + if (interval.match(difference, orgDate: originDateMidnightLocal, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params)) { // check filters before returning successfull match if (_matchValue(historyType, HealthHistoryType.test)) { @@ -4047,7 +4549,7 @@ abstract class HealthRuleCondition { //#572 Building access calculation issue //int difference = AppDateTime.todayMidnightLocal.difference(originDateMidnightLocal).inDays; int difference = AppDateTime.midnightsDifferenceInDays(originDateMidnightLocal, AppDateTime.todayMidnightLocal); - if (currentInterval.match(difference, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params)) { + if (currentInterval.match(difference, orgDate: originDateMidnightLocal, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params)) { return true; } } diff --git a/lib/service/Config.dart b/lib/service/Config.dart index 2d4b0be4..5ea61268 100644 --- a/lib/service/Config.dart +++ b/lib/service/Config.dart @@ -41,6 +41,7 @@ class Config with Service implements NotificationsListener { static const String notifyUpgradeRequired = "edu.illinois.rokwire.config.upgrade.required"; static const String notifyUpgradeAvailable = "edu.illinois.rokwire.config.upgrade.available"; + static const String notifyOnboardingRequired = "edu.illinois.rokwire.config.onboarding.required"; static const String notifyConfigChanged = "edu.illinois.rokwire.config.changed"; Map _config; @@ -94,6 +95,7 @@ class Config with Service implements NotificationsListener { if (_config != null) { _checkUpgrade(); + _checkOnboarding(); _updateFromNet(); } else if (Organizations().organization != null) { @@ -105,6 +107,7 @@ class Config with Service implements NotificationsListener { NotificationService().notify(notifyConfigChanged, null); _checkUpgrade(); + _checkOnboarding(); } } else { @@ -164,7 +167,7 @@ class Config with Service implements NotificationsListener { for (int index = jsonList.length - 1; index >= 0; index--) { Map cfg = jsonList[index]; - if (AppVersion.compareVersions(cfg['mobileAppVersion'], _packageInfo.version) <= 0) { + if (AppVersion.compareVersions(cfg['mobileAppVersion'], appVersion) <= 0) { _decodeSecretKeys(cfg); return cfg; } @@ -196,6 +199,7 @@ class Config with Service implements NotificationsListener { NotificationService().notify(notifyConfigChanged, null); _checkUpgrade(); + _checkOnboarding(); } }); } @@ -239,7 +243,7 @@ class Config with Service implements NotificationsListener { String get upgradeRequiredVersion { dynamic requiredVersion = _upgradeStringEntry('required_version'); - if ((requiredVersion is String) && (AppVersion.compareVersions(_packageInfo.version, requiredVersion) < 0)) { + if ((requiredVersion is String) && (AppVersion.compareVersions(appVersion, requiredVersion) < 0)) { return requiredVersion; } return null; @@ -248,7 +252,7 @@ class Config with Service implements NotificationsListener { String get upgradeAvailableVersion { dynamic availableVersion = _upgradeStringEntry('available_version'); bool upgradeAvailable = (availableVersion is String) && - (AppVersion.compareVersions(_packageInfo.version, availableVersion) < 0) && + (AppVersion.compareVersions(appVersion, availableVersion) < 0) && !Storage().reportedUpgradeVersions.contains(availableVersion) && !_reportedUpgradeVersions.contains(availableVersion); return upgradeAvailable ? availableVersion : null; @@ -291,6 +295,23 @@ class Config with Service implements NotificationsListener { } } + // Onboarding + + String get onboardingRequiredVersion { + dynamic requiredVersion = onboardingInfo['required_version']; + if ((requiredVersion is String) && (AppVersion.compareVersions(requiredVersion, appVersion) <= 0)) { + return requiredVersion; + } + return null; + } + + void _checkOnboarding() { + String value; + if ((value = this.onboardingRequiredVersion) != null) { + NotificationService().notify(notifyOnboardingRequired, value); + } + } + // Assets cache path Directory get appDocumentsDir { @@ -333,12 +354,12 @@ class Config with Service implements NotificationsListener { Map get secretOsf { return secretKeys['osf'] ?? {}; } Map get secretHealth { return secretKeys['health'] ?? {}; } - Map get upgradeInfo { return (_config != null) ? (_config['upgrade'] ?? {}) : {}; } - Map get settings { return (_config != null) ? (_config['settings'] ?? {}) : {}; } + Map get upgradeInfo { return (_config != null) ? (_config['upgrade'] ?? {}) : {}; } + Map get onboardingInfo { return (_config != null) ? (_config['onboarding'] ?? {}) : {}; } String get assetsUrl { return otherUniversityServices['assets_url']; } // "https://rokwire-assets.s3.us-east-2.amazonaws.com" - String get feedbackUrl { return otherUniversityServices['feedback_url']; } // "https://forms.illinois.edu/sec/1971889" + String get getHelpUrl { return otherUniversityServices['get_help_url']; } // "https://forms.illinois.edu/sec/4961936" String get iCardUrl { return otherUniversityServices['icard_url']; } // "https://www.icard.uillinois.edu/rest/rw/rwIDData/rwCardInfo" String get privacyPolicyUrl { return otherUniversityServices['privacy_policy_url']; } // "https://www.vpaa.uillinois.edu/resources/web_privacy" String get exposureLogUrl { return otherUniversityServices['exposure_log_url']; } // "http://ec2-18-191-37-235.us-east-2.compute.amazonaws.com:8003/PostSessionData" @@ -363,6 +384,7 @@ class Config with Service implements NotificationsListener { String get imagesServiceUrl { return platformBuildingBlocks['images_service_url']; } // "https://api-dev.rokwire.illinois.edu/images-service"; String get osfBaseUrl { return thirdPartyServices['osf_base_url']; } // "https://ssproxy.osfhealthcare.org/fhir-proxy" + String get vaccinationAppointUrl { return thirdPartyServices['vaccination_appointment_url']; } // "https://ssproxy.osfhealthcare.org/fhir-proxy" String get rokwireApiKey { return secretRokwire['api_key']; } @@ -376,8 +398,8 @@ class Config with Service implements NotificationsListener { int get refreshTimeout { return kReleaseMode ? (settings['refreshTimeout'] ?? 0) : 0; } - bool get residentRoleEnabled { return false; } - bool get capitolStaffRoleEnabled { return (settings['roleCapitolStaffEnabled'] == true); } + bool get residentRoleEnabled { return false; } + bool get capitolStaffRoleEnabled { return (settings['roleCapitolStaffEnabled'] == true); } } diff --git a/lib/service/Health.dart b/lib/service/Health.dart index 9a877a2b..317b5812 100644 --- a/lib/service/Health.dart +++ b/lib/service/Health.dart @@ -262,6 +262,8 @@ class Health with Service implements NotificationsListener { } Future _refreshInternal(_RefreshOptions options) async { + //Log.d("Health._refreshInternal($options)"); + _refreshOptions = options; NotificationService().notify(notifyRefreshing); @@ -592,6 +594,7 @@ class Health with Service implements NotificationsListener { Future setUserPrivateKey(PrivateKey privateKey) async { if (await _saveUserPrivateKey(privateKey)) { _userPrivateKey = privateKey; + _notify(notifyUserUpdated); _refresh(_RefreshOptions.fromList([_RefreshOption.history])); return true; } @@ -694,6 +697,7 @@ class Health with Service implements NotificationsListener { } Future _loadUserTestMonitorInterval() async { +//TMP: return 8; if (this._isUserAuthenticated && (Config().healthUrl != null)) { String url = "${Config().healthUrl}/covid19/uin-override"; Response response = await Network().get(url, auth: Network.HealthUserAuth); @@ -825,20 +829,9 @@ class Health with Service implements NotificationsListener { HealthStatus status = HealthStatus( dateUtc: null, - blob: HealthStatusBlob( - code: defaultStatus.code, - priority: defaultStatus.priority, - nextStep: rules.localeString(defaultStatus.nextStep), - nextStepHtml: rules.localeString(defaultStatus.nextStepHtml), - nextStepDateUtc: null, - eventExplanation: rules.localeString(defaultStatus.eventExplanation), - eventExplanationHtml: rules.localeString(defaultStatus.eventExplanationHtml), - warning: rules.localeString(defaultStatus.warning), - warningHtml: rules.localeString(defaultStatus.warningHtml), - reason: rules.localeString(defaultStatus.reason), - fcmTopic: defaultStatus.fcmTopic, - historyBlob: null, - ), + blob: HealthStatusBlob.fromRuleStatus(defaultStatus, + rules: rules, + ) ); // Start from older @@ -873,18 +866,9 @@ class Health with Service implements NotificationsListener { if ((ruleStatus != null) && ruleStatus.canUpdateStatus(blob: status.blob)) { status = HealthStatus( dateUtc: historyEntry.dateUtc, - blob: HealthStatusBlob( - code: (ruleStatus.code != null) ? ruleStatus.code : status.blob.code, - priority: (ruleStatus.priority != null) ? ruleStatus.priority.abs() : status.blob.priority, - nextStep: ((ruleStatus.nextStep != null) || (ruleStatus.nextStepHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.nextStep) : status.blob.nextStep, - nextStepHtml: ((ruleStatus.nextStep != null) || (ruleStatus.nextStepHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.nextStepHtml) : status.blob.nextStepHtml, - nextStepDateUtc: ((ruleStatus.nextStepInterval != null) || (ruleStatus.nextStep != null) || (ruleStatus.nextStepHtml != null) || (ruleStatus.code != null)) ? ruleStatus.nextStepDateUtc : status.blob.nextStepDateUtc, - eventExplanation: ((ruleStatus.eventExplanation != null) || (ruleStatus.eventExplanationHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.eventExplanation) : status.blob.eventExplanation, - eventExplanationHtml: ((ruleStatus.eventExplanation != null) || (ruleStatus.eventExplanationHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.eventExplanationHtml) : status.blob.eventExplanationHtml, - warning: ((ruleStatus.warning != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.warning) : status.blob.warning, - warningHtml: ((ruleStatus.warningHtml != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.warningHtml) : status.blob.warningHtml, - reason: ((ruleStatus.reason != null) || (ruleStatus.code != null)) ? rules.localeString(ruleStatus.reason) : status.blob.reason, - fcmTopic: ((ruleStatus.fcmTopic != null) || (ruleStatus.code != null)) ? ruleStatus.fcmTopic : status.blob.fcmTopic, + blob: HealthStatusBlob.fromRuleStatus(ruleStatus, + rules: rules, + previousStatusBlob: status.blob, historyBlob: historyEntry.blob, ), ); @@ -952,8 +936,12 @@ class Health with Service implements NotificationsListener { } Future clearHistory() async { + List history = _history; if (await _clearNetHistory()) { - await _rebuildStatus(); + if (!ListEquality().equals(history, _history)) { + _notify(notifyHistoryUpdated); + await _rebuildStatus(); + } return true; } return false; @@ -1656,13 +1644,14 @@ class Health with Service implements NotificationsListener { // Vaccination bool get isVaccinated { - return (HealthHistory.mostRecentVaccine(_history, vaccine: HealthHistoryBlob.VaccineEffective) != null); + HealthHistory vaccine = HealthHistory.mostRecentVaccine(Health().history); + return (vaccine.blob != null) && (vaccine?.blob?.isVaccineEffective ?? false) && (vaccine.dateUtc != null) && vaccine.dateUtc.isBefore(DateTime.now().toUtc()); } // Current Server Time Future getServerTimeUtc() async { - //TMP: return DateTime.now().toUtc(); +//TMP: return DateTime.now().toUtc(); String url = (Config().healthUrl != null) ? "${Config().healthUrl}/covid19/time" : null; Response response = (url != null) ? await Network().get(url, auth: Network.AppAuth) : null; String responseBody = (response?.statusCode == 200) ? response.body : null; @@ -1915,6 +1904,19 @@ class _RefreshOptions { _RefreshOptions difference(_RefreshOptions other) { return _RefreshOptions.fromSet(options?.difference(other?.options)); } + + String toString() { + String list = ''; + for (_RefreshOption option in _RefreshOption.values) { + if (options.contains(option)) { + if (list.isNotEmpty) { + list += ', '; + } + list += option.toString(); + } + } + return '[$list]'; + } } enum _RefreshOption { diff --git a/lib/service/NativeCommunicator.dart b/lib/service/NativeCommunicator.dart index 69e49114..7616b154 100644 --- a/lib/service/NativeCommunicator.dart +++ b/lib/service/NativeCommunicator.dart @@ -142,15 +142,6 @@ class NativeCommunicator with Service { } } - Future microBlinkScan({List recognizers}) async { - try { - return await _platformChannel.invokeMethod('microBlinkScan', { 'recognizers' : recognizers }); - } on PlatformException catch (e) { - print(e.message); - } - return null; - } - Future> enabledOrientations(List orientationsList) async { List result; try { diff --git a/lib/service/Onboarding.dart b/lib/service/Onboarding.dart index 023f805f..911c6edd 100644 --- a/lib/service/Onboarding.dart +++ b/lib/service/Onboarding.dart @@ -26,8 +26,6 @@ import 'package:illinois/ui/onboarding/OnboardingHealthFinalPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingHealthHowItWorksPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingHealthIntroPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingHealthQrCodePanel.dart'; -import 'package:illinois/ui/onboarding/OnboardingResidentInfoPanel.dart'; -import 'package:illinois/ui/onboarding/OnboardingReviewScanPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingAuthBluetoothPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingLoginPhoneConfirmPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingGetStartedPanel.dart'; @@ -164,8 +162,6 @@ class Onboarding extends Service implements NotificationsListener{ case "login_phone": return OnboardingLoginPhonePanel(onboardingContext: context); case "verify_phone": return OnboardingLoginPhoneVerifyPanel(onboardingContext: context); case "confirm_phone": return OnboardingLoginPhoneConfirmPanel(onboardingContext: context); - case "resident_info": return OnboardingResidentInfoPanel(onboardingContext: context); - case "review_scan": return OnboardingReviewScanPanel(onboardingContext: context); case "health_intro": return OnboardingHealthIntroPanel(onboardingContext: context); case "health_how_it_works": return OnboardingHealthHowItWorksPanel(onboardingContext: context); case "health_disclosure": return OnBoardingHealthDisclosurePanel(onboardingContext: context); @@ -207,12 +203,6 @@ class Onboarding extends Service implements NotificationsListener{ else if (panel is OnboardingLoginPhoneConfirmPanel) { return 'confirm_phone'; } - else if (panel is OnboardingResidentInfoPanel) { - return 'resident_info'; - } - else if (panel is OnboardingReviewScanPanel) { - return 'review_scan'; - } else if (panel is OnboardingHealthIntroPanel) { return 'health_intro'; } diff --git a/lib/service/Storage.dart b/lib/service/Storage.dart index 1fe60b7d..eb0a198c 100644 --- a/lib/service/Storage.dart +++ b/lib/service/Storage.dart @@ -487,7 +487,7 @@ class Storage with Service { static const String _healthUserTestMonitorIntervalKey = 'health_user_test_monitor_interval'; int get healthUserTestMonitorInterval { - return getInt(_healthUserTestMonitorIntervalKey); + return getInt(_healthUserTestMonitorIntervalKey, defaultValue: null); } set healthUserTestMonitorInterval(int value) { diff --git a/lib/ui/debug/DebugCreateEventPanel.dart b/lib/ui/debug/DebugCreateEventPanel.dart index 2035c0e5..13c2c7b3 100644 --- a/lib/ui/debug/DebugCreateEventPanel.dart +++ b/lib/ui/debug/DebugCreateEventPanel.dart @@ -295,7 +295,7 @@ class _DebugCreateEventPanelState extends State { Row(children: [ Expanded(child: - RoundedButton(label: "Vacc Effective", + RoundedButton(label: "Vaccine Effective", textColor: Styles().colors.fillColorPrimary, borderColor: Styles().colors.fillColorSecondary, backgroundColor: Styles().colors.white, @@ -306,14 +306,7 @@ class _DebugCreateEventPanelState extends State { ), Container(width: 4,), Expanded(child: - RoundedButton(label: "Vacc Taken", - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onPopulate(this._sampleVacineTakenBlob); } - ), + Container() ), ],), @@ -546,25 +539,6 @@ class _DebugCreateEventPanelState extends State { ] }''';} - String get _sampleVacineTakenBlob { - DateTime nowLocal = DateTime.now(); - String dateString = healthDateTimeToString(nowLocal.toUtc()); - - String dose2String = DateFormat("MMMM d, yyyy HH:mm").format(nowLocal); - - DateTime dose1Local = nowLocal.subtract(Duration(days: 21)); - String dose1String = DateFormat("MMMM d, yyyy HH:mm").format(dose1Local); - - return '''{ - "Date": "$dateString", - "Vaccine": "${HealthHistoryBlob.VaccineTaken}", - "Extra": [ - {"display_name": "Vaccine", "display_value": "Sputnik V"}, - {"display_name": "2nd Dose", "display_value": "$dose2String"}, - {"display_name": "1st Dose", "display_value": "$dose1String"} - ] -}''';} - String get _sampleActionQuarantineOnBlob { DateTime nowLocal = DateTime.now(); String dateString = healthDateTimeToString(nowLocal.toUtc()); diff --git a/lib/ui/debug/DebugHealthRulesPanel.dart b/lib/ui/debug/DebugHealthRulesPanel.dart index 4bec5770..4beb5fd4 100644 --- a/lib/ui/debug/DebugHealthRulesPanel.dart +++ b/lib/ui/debug/DebugHealthRulesPanel.dart @@ -85,12 +85,12 @@ class _DebugHealthRulesPanelState extends State{ Health().loadRulesJson(countyId: _selectedCountyId).then((Map rules) { if (mounted) { if ((rules != null) && (Health().userTestMonitorInterval != null)) { - dynamic constants = rules['constants']; - if (constants == null) { - rules['constants'] = constants = {}; + dynamic intervals = rules['intervals']; + if (intervals == null) { + rules['intervals'] = intervals = {}; } - if (constants is Map) { - constants[HealthRulesSet.UserTestMonitorInterval] = Health().userTestMonitorInterval; + if (intervals is Map) { + intervals[HealthRulesSet.UserTestMonitorInterval] = Health().userTestMonitorInterval; } } diff --git a/lib/ui/debug/DebugHomePanel.dart b/lib/ui/debug/DebugHomePanel.dart index c929dcab..30b5d778 100644 --- a/lib/ui/debug/DebugHomePanel.dart +++ b/lib/ui/debug/DebugHomePanel.dart @@ -23,6 +23,7 @@ import 'package:flutter/services.dart'; import 'package:illinois/model/Organization.dart'; import 'package:illinois/service/Auth.dart'; import 'package:illinois/service/FirebaseMessaging.dart'; +import 'package:illinois/service/Health.dart'; import 'package:illinois/service/Localization.dart'; import 'package:illinois/service/NotificationService.dart'; import 'package:illinois/service/Organizations.dart'; @@ -57,6 +58,7 @@ class _DebugHomePanelState extends State implements Notification String _environment; bool _switchingEnvironment; + bool _removingHistory; @override void initState() { @@ -218,13 +220,25 @@ class _DebugHomePanelState extends State implements Notification onTap: _onTapCreateEvent)), Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), - child: RoundedButton( - label: "COVID-19 Create Exposure", + child: Stack(children: [ + RoundedButton( + label: "COVID-19 Clear History", backgroundColor: Styles().colors.background, fontSize: 16.0, textColor: Styles().colors.fillColorPrimary, borderColor: Styles().colors.fillColorPrimary, - onTap: _onTapTraceCovid19Exposure)), + onTap: _onTapClearHistory), + Visibility(visible: _removingHistory == true, child: + Padding(padding: EdgeInsets.symmetric(vertical: 12), child: + Center(child: + Container(width: 24, height: 24, child: + CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary)), + ), + ), + ), + ), + + ],),), Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), child: RoundedButton( @@ -234,6 +248,15 @@ class _DebugHomePanelState extends State implements Notification textColor: Styles().colors.fillColorPrimary, borderColor: Styles().colors.fillColorPrimary, onTap: _onTapReportCovid19Symptoms)), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: RoundedButton( + label: "COVID-19 Create Exposure", + backgroundColor: Styles().colors.background, + fontSize: 16.0, + textColor: Styles().colors.fillColorPrimary, + borderColor: Styles().colors.fillColorPrimary, + onTap: _onTapTraceCovid19Exposure)), Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), child: RoundedButton( @@ -381,14 +404,20 @@ class _DebugHomePanelState extends State implements Notification Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugCreateEventPanel())); } - void _onTapTraceCovid19Exposure() { - Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugContactTraceReportPanel())); + void _onTapClearHistory() { + if (_removingHistory != true) { + showDialog(context: context, builder: (context) => _buildRemoveHistoryDialog(context)); + } } void _onTapReportCovid19Symptoms() { Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugSymptomsReportPanel())); } + void _onTapTraceCovid19Exposure() { + Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugContactTraceReportPanel())); + } + void _onTapCovid19Exposures() { Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugExposurePanel())); } @@ -607,4 +636,82 @@ class _DebugHomePanelState extends State implements Notification }); }); } + + + ////////////////////////// + // Delete History + + Widget _buildRemoveHistoryDialog(BuildContext context) { + return StatefulBuilder(builder: (context, setState) { + return ClipRRect(borderRadius: BorderRadius.all(Radius.circular(8)), child: + Dialog(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8),), child: + Column(mainAxisSize: MainAxisSize.min, children: [ + Row(children: [ + Expanded(child: + Container(decoration: BoxDecoration(color: Styles().colors.fillColorPrimary, borderRadius: BorderRadius.vertical(top: Radius.circular(8)),), child: + Padding(padding: EdgeInsets.all(8), child: + Row(children: [ + Expanded(child: + Center(child: + Text("Clear COVID-19 event history?", style: TextStyle(fontSize: 20, color: Colors.white),), + ), + ), + GestureDetector(onTap: () => Navigator.pop(context), child: + Container(height: 30, width: 30, decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(15)), border: Border.all(color: Styles().colors.white, width: 2), ), child: + Center(child: + Text('\u00D7', style: TextStyle(fontSize: 24, color: Colors.white, ), ), + ), + ), + ), + ],), + ), + ), + ), + ],), + Container(height: 26,), + Padding(padding: const EdgeInsets.symmetric(horizontal: 18), child: + Text("This will permanently remove all COVID-19 event history.", textAlign: TextAlign.left, style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Colors.black),), + ), + Container(height: 26,), + Text("Are you sure?", textAlign: TextAlign.center, style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Colors.black), ), + Container(height: 16,), + Padding(padding: const EdgeInsets.all(8.0), child: + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Expanded(child: + RoundedButton( + onTap: () { Navigator.pop(context); }, + backgroundColor: Colors.transparent, + borderColor: Styles().colors.fillColorPrimary, + textColor: Styles().colors.fillColorPrimary, + label: 'No'), + ), + Container(width: 10,), + Expanded(child: + RoundedButton( + onTap: () => _onClearHistory(), + backgroundColor: Styles().colors.fillColorSecondaryVariant, + borderColor: Styles().colors.fillColorSecondaryVariant, + textColor: Styles().colors.surface, + label: 'Yes', + height: 48,), + ), + ],), + ), + ],), + ), + ); + },); + } + + void _onClearHistory() { + Navigator.pop(context); + + if (_removingHistory != true) { + setState(() { _removingHistory = true; }); + Health().clearHistory().then((bool result) { + setState(() {_removingHistory = false;}); + AppAlert.showDialogResult(context, (result == true) ? 'COVID-19 event history successfully cleared.' : 'Failed to clear COVID-19 event history.'); + }); + } + } } diff --git a/lib/ui/health/HealthHistoryPanel.dart b/lib/ui/health/HealthHistoryPanel.dart index 2951d1db..053561a7 100644 --- a/lib/ui/health/HealthHistoryPanel.dart +++ b/lib/ui/health/HealthHistoryPanel.dart @@ -165,6 +165,7 @@ class _HealthHistoryPanelState extends State implements Noti style: TextStyle(fontSize: 16, fontFamily: Styles().fontFamilies.regular, color: Styles().colors.textSurface)), Expanded(child: Container(),), _buildRepostButton(), + Visibility(visible: !kReleaseMode || Organizations().isDevEnvironment, child: _buildRemoveMyInfoButton()), Container(height: 10,), ], ))); @@ -205,7 +206,7 @@ class _HealthHistoryPanelState extends State implements Noti alignment: Alignment.center, children: [ ScalableRoundedButton( - label: Localization().getStringEx("panel.health.covid19.history.button.repost_history.title", "Request my latest test again"), + label: Localization().getStringEx("panel.health.covid19.history.button.repost_history.title", "Request my vaccine and latest test again"), hint: Localization().getStringEx("panel.health.covid19.history.button.repost_history.hint", ""), backgroundColor: Styles().colors.surface, fontSize: 16.0, @@ -530,11 +531,8 @@ class _HealthHistoryEntryState extends State<_HealthHistoryEntry> with SingleTic if (widget.historyEntry?.blob?.isVaccineEffective ?? false) { title = Localization().getStringEx("panel.health.covid19.history.label.vaccine.effective.title", "Vaccine Effective"); } - else if (widget.historyEntry?.blob?.isVaccineTaken ?? false) { - title = Localization().getStringEx("panel.health.covid19.history.label.vaccine.taken.title", "Vaccine Taken"); - } else { - title = Localization().getStringEx("panel.health.covid19.history.label.vaccine.title", "Vaccine Taken"); + title = Localization().getStringEx("panel.health.covid19.history.label.vaccine.title", "Vaccine"); } String providerTitle = widget.historyEntry?.blob?.provider ?? Localization().getStringEx("app.common.label.other", "Other"); details.addAll([ diff --git a/lib/ui/health/HealthHomePanel.dart b/lib/ui/health/HealthHomePanel.dart index 1e0a89d6..aa8717e9 100644 --- a/lib/ui/health/HealthHomePanel.dart +++ b/lib/ui/health/HealthHomePanel.dart @@ -22,6 +22,7 @@ import 'package:flutter_html/style.dart'; import 'package:illinois/model/Health.dart'; import 'package:illinois/service/Analytics.dart'; import 'package:illinois/service/Auth.dart'; +import 'package:illinois/service/Config.dart'; import 'package:illinois/service/FlexUI.dart'; import 'package:illinois/service/Health.dart'; import 'package:illinois/service/Organizations.dart'; @@ -265,6 +266,8 @@ class _HealthHomePanelState extends State implements Notificati contentList.add(_buildSymptomCheckInSection()); } else if (code == 'add_test_result') { contentList.add(_buildAddTestResultSection()); + } else if (code == 'vaccination') { + contentList.add(_buildVaccinationSection()); } } @@ -371,9 +374,6 @@ class _HealthHomePanelState extends State implements Notificati if (blob.isVaccineEffective) { historyTitle = Localization().getStringEx("panel.covid19home.label.vaccine.effective.title", "Vaccine Effective"); } - else if (blob.isVaccineTaken) { - historyTitle = Localization().getStringEx("panel.covid19home.label.vaccine.taken.title", "Vaccine Taken"); - } else { historyTitle = Localization().getStringEx("panel.covid19home.label.vaccine.title", "Vaccine"); } @@ -543,7 +543,7 @@ class _HealthHomePanelState extends State implements Notificati borderColor: Styles().colors.fillColorSecondary, backgroundColor: Styles().colors.surface, textColor: Styles().colors.fillColorPrimary, - onTap: ()=> _onTapFindLocations(), + onTap: ()=> _onTapFindTestLocations(), )), ) ],), @@ -678,6 +678,122 @@ class _HealthHomePanelState extends State implements Notificati )); } + Widget _buildVaccinationSection() { + + String headingDate; + String statusTitleText, statusTitleHtml; + String statusDescriptionText, statusDescriptionHtml; + String headingTitle = Localization().getStringEx('panel.covid19home.vaccination.heading.title', 'VACCINATION'); + + HealthHistory vaccine = HealthHistory.mostRecentVaccine(Health().history); + if (vaccine == null) { + // No vaccine at all - promote it. + statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.none.title', 'Get a vaccine now'); + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.none.description', """ +• COVID-19 vaccines are safe +• COVID-19 vaccines are effective +• COVID-19 vaccines allow you to safely do more +• COVID-19 vaccines build safer protection"""); + } + else if ((vaccine.blob != null) && (vaccine.blob.isVaccineEffective) && (vaccine.dateUtc != null) && vaccine.dateUtc.isBefore(DateTime.now().toUtc())) { + // 5.2.4 When effective then hide the widget + return null; + } + else { + // Vaccinated, but not effective yet. + headingDate = AppDateTime.formatDateTime(vaccine.dateUtc?.toLocal(), format:"MMMM dd, yyyy", locale: Localization().currentLocale?.languageCode) ?? ''; + statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.title', 'Vaccinated'); + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.description', 'Your vaccination is not effective yet.'); + } + + List contentWidgets = [ + Row(children: [ + Text(headingTitle ?? '', style: TextStyle(letterSpacing: 0.5, fontFamily: Styles().fontFamilies.bold, fontSize: 12, color: Styles().colors.fillColorPrimary),), + Expanded(child: Container(),), + Text(headingDate ?? '', style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 12, color: Styles().colors.textSurface),) + ],), + ]; + + if (AppString.isStringNotEmpty(statusTitleText)) { + contentWidgets.addAll([ + Container(height: 12,), + Text(statusTitleText, style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, color: Styles().colors.fillColorPrimary),), + ]); + } + + if (AppString.isStringNotEmpty(statusTitleHtml)) { + contentWidgets.addAll([ + Container(height: 12,), + Html(data: statusTitleHtml, onLinkTap: (url) => _onTapLink(url), + style: { + "body": Style(fontFamily: Styles().fontFamilies.medium, fontSize: FontSize(16), color: Styles().colors.fillColorPrimary, padding: EdgeInsets.zero, margin: EdgeInsets.zero) + }, + ), + ]); + } + + if (AppString.isStringNotEmpty(statusDescriptionText)) { + contentWidgets.addAll([ + Container(height: 12,), + Text(statusDescriptionText, style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Styles().colors.fillColorPrimary),), + ]); + } + + if (AppString.isStringNotEmpty(statusDescriptionHtml)) { + contentWidgets.addAll([ + Container(height: 12,), + Html(data: statusDescriptionHtml, onLinkTap: (url) => _onTapLink(url), + style: { + "body": Style(fontFamily: Styles().fontFamilies.medium, fontSize: FontSize(16), color: Styles().colors.fillColorPrimary, padding: EdgeInsets.zero, margin: EdgeInsets.zero) + }, + ), + ]); + } + + List contentList = [ + Stack(children: [ + Visibility(visible: true, + child: Padding(padding: EdgeInsets.symmetric(horizontal: 16), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: contentWidgets), + ), + ), + Visibility(visible: (_isRefreshing == true), + child: Container( + height: 80, + child: Align(alignment: Alignment.center, + child: SizedBox(height: 24, width: 24, + child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), ) + ), + ), + ), + ), + ],), + ]; + + if ((vaccine == null) && (Config().vaccinationAppointUrl != null)) { + contentList.addAll([ + Container(margin: EdgeInsets.only(top: 14, bottom: 14), height: 1, color: Styles().colors.fillColorPrimaryTransparent015,), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Semantics(explicitChildNodes: true, child: ScalableRoundedButton( + label: Localization().getStringEx('panel.covid19home.vaccination.button.appointment.title', 'Make an appointment'), + hint: Localization().getStringEx('panel.covid19home.vaccination.button.appointment.hint', ''), + borderColor: Styles().colors.fillColorSecondary, + backgroundColor: Styles().colors.surface, + textColor: Styles().colors.fillColorPrimary, + onTap: _onTapMakeVaccineAppointment, + )), + ) + ]); + } + + return Semantics(container: true, child: + Container(padding: EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration(color: Styles().colors.surface, borderRadius: BorderRadius.all(Radius.circular(4)), boxShadow: [BoxShadow(color: Styles().colors.blackTransparent018, spreadRadius: 2.0, blurRadius: 6.0, offset: Offset(2, 2))] ), child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: contentList), + )); + } + Widget _buildTileButtons() { List contentList = []; @@ -774,7 +890,7 @@ class _HealthHomePanelState extends State implements Notificati hint: Localization().getStringEx("panel.covid19home.button.find_test_locations.hint", ""), borderRadius: BorderRadius.circular(4), height: null, - onTap: ()=>_onTapFindLocations(), + onTap: ()=>_onTapFindTestLocations(), ), ); } @@ -929,7 +1045,7 @@ class _HealthHomePanelState extends State implements Notificati } } - void _onTapFindLocations() { + void _onTapFindTestLocations() { if (Connectivity().isNotOffline) { Analytics.instance.logSelect(target: "COVID-19 Find Test Locations"); Navigator.push(context, CupertinoPageRoute(builder: (context) => HealthTestLocationsPanel())); @@ -938,6 +1054,17 @@ class _HealthHomePanelState extends State implements Notificati } } + void _onTapMakeVaccineAppointment() { + if (Config().vaccinationAppointUrl != null) { + if (Connectivity().isNotOffline) { + Analytics.instance.logSelect(target: "COVID-19 Make Vaccine Appointment"); + Navigator.push(context, CupertinoPageRoute(builder: (context) => WebPanel(url: Config().vaccinationAppointUrl))); + } else { + AppAlert.showOfflineMessage(context); + } + } + } + void _onTapGroups() { if (Connectivity().isNotOffline) { Analytics.instance.logSelect(target: "COVID-19 Groups"); diff --git a/lib/ui/health/HealthStatusUpdatePanel.dart b/lib/ui/health/HealthStatusUpdatePanel.dart index 6579ab20..d1d1c97a 100644 --- a/lib/ui/health/HealthStatusUpdatePanel.dart +++ b/lib/ui/health/HealthStatusUpdatePanel.dart @@ -16,8 +16,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/style.dart'; import 'package:illinois/model/Health.dart'; +import 'package:illinois/service/Connectivity.dart'; import 'package:illinois/service/Health.dart'; +import 'package:illinois/ui/WebPanel.dart'; import 'package:illinois/utils/AppDateTime.dart'; import 'package:illinois/service/Localization.dart'; import 'package:illinois/service/Styles.dart'; @@ -25,6 +29,7 @@ import 'package:illinois/ui/health/HealthNextStepsPanel.dart'; import 'package:illinois/ui/widgets/RoundedButton.dart'; import 'package:illinois/ui/widgets/StatusInfoDialog.dart'; import 'package:illinois/utils/Utils.dart'; +import 'package:url_launcher/url_launcher.dart'; class HealthStatusUpdatePanel extends StatefulWidget { final HealthStatus status; @@ -154,36 +159,42 @@ class _HealthStatusUpdatePanelState extends State { padding: EdgeInsets.symmetric(horizontal: 8), child: Container(height: 1, color: Styles().colors.surfaceAccent ,), ), - _buildReasonContent(), + _buildDetails(), ], ), ); } - Widget _buildReasonContent(){ + Widget _buildDetails(){ String date = AppDateTime.formatDateTime(widget.status?.dateUtc?.toLocal(), format: "MMMM dd, yyyy", locale: Localization().currentLocale?.languageCode); - String reasonStatusText = widget.status?.blob?.displayReason; - HealthHistoryBlob reasonHistory = widget.status?.blob?.historyBlob; - String reasonHistoryName; - Widget reasonHistoryDetail; - if (reasonHistory != null) { - if (reasonHistory.isTest) { - reasonHistoryName = reasonHistory.testType; + HealthHistoryBlob history = widget.status?.blob?.historyBlob; + + String reasonText = widget.status?.blob?.displayStatusUpdateReason; + String reasonHtml = widget.status?.blob?.displayStatusUpdateReasonHtml; + + String noticeText = widget.status?.blob?.displayStatusUpdateNotice; + String noticeHtml = widget.status?.blob?.displayStatusUpdateNoticeHtml; + Widget noticeDetailWidget; + + if ((noticeText == null) && (noticeHtml == null) && (history != null)) { + // Build default notice content + if (history.isTest) { + noticeText = history.testType; - reasonHistoryDetail = Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + noticeDetailWidget = Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset("images/icon-selected.png", excludeFromSemantics: true,), Container(width: 7,), Text(Localization().getStringEx("panel.health.status_update.label.reason.result", "Result:"), style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.bold)), Container(width: 5,), - Text(reasonHistory.testResult ?? '', style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.regular)), + Text(history.testResult ?? '', style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.regular)), ],); } - else if (reasonHistory.isSymptoms) { - reasonHistoryName = Localization().getStringEx("panel.health.status_update.label.reason.symptoms.title", "You reported new symptoms"); + else if (history.isSymptoms) { + noticeText = Localization().getStringEx("panel.health.status_update.label.reason.symptoms.title", "You reported new symptoms"); List symptomLayouts = []; - List symptoms = reasonHistory.symptoms; + List symptoms = history.symptoms; if (symptoms?.isNotEmpty ?? false) { symptoms.forEach((HealthSymptom symptom){ String symptomName = Health().rules?.localeString(symptom?.name) ?? symptom?.name; @@ -193,45 +204,42 @@ class _HealthStatusUpdatePanelState extends State { }); } - reasonHistoryDetail = Column(mainAxisAlignment: MainAxisAlignment.center, children: symptomLayouts,); + noticeDetailWidget = Column(mainAxisAlignment: MainAxisAlignment.center, children: symptomLayouts,); } - else if (reasonHistory.isContactTrace) { - reasonHistoryName = Localization().getStringEx("panel.health.status_update.label.reason.exposed.title", "You were exposed to someone who was likely infected"); + else if (history.isContactTrace) { + noticeText = Localization().getStringEx("panel.health.status_update.label.reason.exposed.title", "You were exposed to someone who was likely infected"); - reasonHistoryDetail = Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + noticeDetailWidget = Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset("images/icon-selected.png", excludeFromSemantics: true,), Container(width: 7,), Text(Localization().getStringEx("panel.health.status_update.label.reason.exposure.detail", "Duration of exposure: "), style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.bold)), Container(width: 5,), - Text(reasonHistory.traceDurationDisplayString ?? "", style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.regular)), + Text(history.traceDurationDisplayString ?? "", style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.regular)), ],); } - else if (reasonHistory.isVaccine) { - if (reasonHistory.isVaccineEffective) { - reasonHistoryName = Localization().getStringEx("panel.health.status_update.label.reason.vaccine.effective.title", "Your vaccine is already effective."); - } - else if (reasonHistory.isVaccineTaken) { - reasonHistoryName = Localization().getStringEx("panel.health.status_update.label.reason.vaccine.taken.title", "Your vaccine is taken."); + else if (history.isVaccine) { + if (history.isVaccineEffective) { + noticeText = Localization().getStringEx("panel.health.status_update.label.reason.vaccine.effective.title", "Your COVID-19 vaccination has been verified."); } else { - reasonHistoryName = Localization().getStringEx("panel.health.status_update.label.reason.vaccine.title", "Your vaccine is applied."); + noticeText = Localization().getStringEx("panel.health.status_update.label.reason.vaccine.title", "Your vaccine is applied."); } } - else if (reasonHistory.isAction) { - reasonHistoryName = Localization().getStringEx("panel.health.status_update.label.reason.action.title", "Health authorities require you to take an action."); + else if (history.isAction) { + noticeText = Localization().getStringEx("panel.health.status_update.label.reason.action.title", "Health authorities require you to take an action."); - reasonHistoryDetail = Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ + noticeDetailWidget = Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset("images/icon-selected.png", excludeFromSemantics: true,), Container(width: 7,), - Text(reasonHistory.localeActionTitle ?? Localization().getStringEx("panel.health.status_update.label.reason.action.detail", "Action Required: "), style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.bold)), + Text(history.localeActionTitle ?? Localization().getStringEx("panel.health.status_update.label.reason.action.detail", "Action Required: "), style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.bold)), ],), - Text(reasonHistory.localeActionText ?? "", style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.regular)), + Text(history.localeActionText ?? "", style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.regular)), ],); } } - if ((reasonStatusText != null) || (reasonHistoryName != null) || (reasonHistoryDetail != null)) { + if ((reasonText != null) || (reasonHtml != null) || (noticeText != null) || (noticeHtml != null) || (noticeDetailWidget != null)) { List content = [ Container(height: 30,), Text(Localization().getStringEx("panel.health.status_update.label.reason.title", "STATUS CHANGED BECAUSE:"), textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 12, fontFamily: Styles().fontFamilies.bold),), @@ -245,25 +253,46 @@ class _HealthStatusUpdatePanelState extends State { ]); } - if (reasonHistoryName != null) { - content.addAll([ - Text(reasonHistoryName,textAlign: TextAlign.center,style: TextStyle(color: Colors.white, fontSize: 18, fontFamily: Styles().fontFamilies.extraBold),), - Container(height: 9,), - ]); + if ((noticeText != null) || (noticeHtml != null)) { + if (noticeText != null) { + content.add(Text(noticeText, textAlign: TextAlign.center, style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 18, color: Colors.white, ),), ); + } + if ((noticeText != null) && (noticeHtml != null)) { + content.add(Container(height: 9,)); + } + if (noticeHtml != null) { + content.add(Html(data: noticeHtml, onLinkTap: (url) => _onTapLink(url), + style: { + "body": Style(fontFamily: Styles().fontFamilies.medium, fontSize: FontSize(18), color: Styles().colors.white, padding: EdgeInsets.zero, margin: EdgeInsets.zero), + }, + ),); + } + content.add(Container(height: 9,)); } - if (reasonHistoryDetail != null) { + if (noticeDetailWidget != null) { content.addAll([ - reasonHistoryDetail, + noticeDetailWidget, ]); } - if (reasonStatusText != null) { - content.addAll([ - Container(height: 60,), - Text(reasonStatusText, textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 14, fontFamily: Styles().fontFamilies.bold),), - Container(height: 30,), - ]); + if ((reasonText != null) || (reasonHtml != null)) { + content.add(Container(height: 60,)); + if (reasonText != null) { + content.add(Text(reasonText, textAlign: TextAlign.center, style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 14, color: Styles().colors.white,),)); + } + if ((reasonText != null) && (reasonHtml != null)) { + content.add(Container(height: 12,)); + } + if (reasonHtml != null) { + content.add(Html(data: reasonHtml, onLinkTap: (url) => _onTapLink(url), + style: { + "body": Style(fontFamily: Styles().fontFamilies.medium, fontSize: FontSize(14), color: Styles().colors.white, padding: EdgeInsets.zero, margin: EdgeInsets.zero), + }, + ),); + } + + content.add(Container(height: 30,)); } return Container( @@ -304,4 +333,18 @@ class _HealthStatusUpdatePanelState extends State { ]) ); }*/ + + void _onTapLink(String url) { + if (Connectivity().isNotOffline) { + if (AppString.isStringNotEmpty(url)) { + if (AppUrl.launchInternal(url)) { + Navigator.push(context, CupertinoPageRoute(builder: (context) => WebPanel(url: url))); + } else { + launch(url); + } + } + } else { + AppAlert.showOfflineMessage(context); + } + } } \ No newline at end of file diff --git a/lib/ui/onboarding/OnboardingResidentInfoPanel.dart b/lib/ui/onboarding/OnboardingResidentInfoPanel.dart deleted file mode 100644 index 7c264f76..00000000 --- a/lib/ui/onboarding/OnboardingResidentInfoPanel.dart +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:illinois/model/UserProfile.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/service/NativeCommunicator.dart'; -import 'package:illinois/service/Onboarding.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/service/UserProfile.dart'; -import 'package:illinois/ui/onboarding/OnboardingHealthProgress.dart'; -import 'package:illinois/ui/onboarding/OnboardingBackButton.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/ui/widgets/ScalableScrollView.dart'; - -class OnboardingResidentInfoPanel extends StatelessWidget with OnboardingPanel { - - final Map onboardingContext; - final Function(Map) onSucceed; - final Function onCancel; - - OnboardingResidentInfoPanel({this.onboardingContext, this.onSucceed, this.onCancel}); - - @override - bool get onboardingCanDisplay { - return !UserProfile().isStudentOrEmployee && (onboardingContext != null) && onboardingContext['shouldDisplayResidentInfo'] == true; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Styles().colors.background, - body: ScalableScrollView( - scrollableChild: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container(color: Styles().colors.white, child: Stack(children: [ - OnboardingHealthProgress(progress: 0.50,), - Align(alignment: Alignment.topLeft, - child: OnboardingBackButton(image: 'images/chevron-left-blue.png', padding: EdgeInsets.only(top: 16, right: 20, bottom: 20), onTap: () => _goBack(context)), - ), - Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: EdgeInsets.only(left: 24, right: 24, top: 24, bottom: 12), - child: Semantics( header: true, hint: Localization().getStringEx("app.common.heading.one.hint","Header 1"), - child:Text( Localization().getStringEx('panel.health.onboarding.covid19.resident_info.label.title', 'Verify your identity with a government-issued ID',), - textAlign: TextAlign.center, - style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, color: Styles().colors.fillColorPrimary), - ))), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, bottom: 19), - child: Text( - Localization().getStringEx('panel.health.onboarding.covid19.resident_info.label.description', 'After verifying you will receive a color-coded health status based on your county guidelines, symptoms, and any COVID-19 related tests.'), - textAlign: TextAlign.center, - style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Styles().colors.fillColorPrimary), - )), - ],) - ],),),]), - bottomNotScrollableWidget: Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding(padding: EdgeInsets.symmetric(vertical: 17, horizontal: 16), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child:ScalableRoundedButton( - label: Localization().getStringEx('panel.health.onboarding.covid19.resident_info.button.passport.title', 'Passport'), - hint: Localization().getStringEx('panel.health.onboarding.covid19.resident_info.button.passport.hint', ''), - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - padding: EdgeInsets.symmetric(horizontal: 22), - onTap: () => _doScan(context, UserDocumentType.passport), - )), - Container(width: 16,), - Expanded(child: ScalableRoundedButton( - label: Localization().getStringEx('panel.health.onboarding.covid19.resident_info.button.drivers_license.title', "Driver's License"), - hint: Localization().getStringEx('panel.health.onboarding.covid19.resident_info.button.drivers_license.hint', ''), - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - onTap: () => _doScan(context, UserDocumentType.drivingLicense), - ),) - ],),), - GestureDetector( - onTap: () => _onTapVerifyLater(context), - behavior: HitTestBehavior.translucent, - child: Container( - child: Padding( - padding: EdgeInsets.only(bottom: 20), - child: Text( - Localization().getStringEx('panel.health.onboarding.covid19.resident_info.button.verify_later.title', "Verify later"), - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 16, - color: Styles().colors.fillColorPrimary, - decoration: TextDecoration.underline, - decorationColor: Styles().colors.fillColorSecondary, - decorationThickness: 1, - decorationStyle: TextDecorationStyle.solid), - )), - ), - ) - ], - ),) - )); - } - - void _goBack(BuildContext context) { - Analytics.instance.logSelect(target: "Back"); - Navigator.of(context).pop(); - } - - void _doScan(BuildContext context, UserDocumentType documentType) { - - String analyticsScanType; - List recognizers; - if (documentType == UserDocumentType.drivingLicense) { - Analytics.instance.logSelect(target: "Driver's License") ; - analyticsScanType = Analytics.LogDocumentScanDrivingLicenseType; - recognizers = ['combined']; - } - else if (documentType == UserDocumentType.passport) { - Analytics.instance.logSelect(target: 'Passport') ; - analyticsScanType = Analytics.LogDocumentScanPassportType; - recognizers = ['passport']; - } - - NativeCommunicator().microBlinkScan(recognizers: recognizers).then((dynamic result) { - Analytics().logDocumentScan(type: analyticsScanType, result: (result != null)); - if (result != null) { - _didScan(context, documentType, result); - } - }); - } - - void _didScan(BuildContext context, UserDocumentType documentType, Map scanData) { - if(onboardingContext != null) { - onboardingContext['shouldDisplayReviewScan'] = true; - onboardingContext['userDocumentType'] = documentType; - onboardingContext['scanData'] = scanData; - Onboarding().next(context, this); - } - else if(onSucceed != null){ - onSucceed({ - 'userDocumentType': documentType, - 'scanData': scanData - }); - } - else{ - Navigator.pop(context); - } - } - - void _onTapVerifyLater(BuildContext context) { - if(onboardingContext != null) { - Analytics.instance.logSelect(target: 'Verify later'); - onboardingContext['shouldDisplayReviewScan'] = false; - if (Auth().isLoggedIn) { - onboardingContext['shouldDisplayQrCode'] = true; - } else { - onboardingContext['shouldDisplayQrCode'] = false; - } - Onboarding().next(context, this); - } - else if(onCancel != null){ - onCancel(); - } - else{ - Navigator.pop(context); - } - } -} diff --git a/lib/ui/onboarding/OnboardingReviewScanPanel.dart b/lib/ui/onboarding/OnboardingReviewScanPanel.dart deleted file mode 100644 index e6ccd01e..00000000 --- a/lib/ui/onboarding/OnboardingReviewScanPanel.dart +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:illinois/model/UserProfile.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/service/NativeCommunicator.dart'; -import 'package:illinois/service/Onboarding.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/service/UserProfile.dart'; -import 'package:illinois/ui/onboarding/OnboardingHealthProgress.dart'; -import 'package:illinois/ui/onboarding/OnboardingBackButton.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/utils/Utils.dart'; - -class OnboardingReviewScanPanel extends StatefulWidget with OnboardingPanel { - - final Map onboardingContext; - - OnboardingReviewScanPanel({this.onboardingContext}); - - _OnboardingReviewScanPanelState createState() => _OnboardingReviewScanPanelState(); - - @override - bool get onboardingCanDisplay { - return !UserProfile().isStudentOrEmployee && (onboardingContext != null) && onboardingContext['shouldDisplayReviewScan'] == true; - } -} - -class _OnboardingReviewScanPanelState extends State { - - static const String kFirstNameFieldName = 'firstName'; - static const String kMiddleNameFieldName = 'middleName'; - static const String kLastNameFieldName = 'lastName'; - static const String kFullNameFieldName = 'fullName'; - - static const String kBirthYearFieldName = 'birthYear'; - - static const String kAddressFieldName = 'address'; - static const String kStateFieldName = 'state'; - static const String kZipFieldName = 'zip'; - static const String kCountryFieldName = 'country'; - static const String kFullAddressFieldName = 'fullAddress'; - - /*static const String kHomeCountyFieldName = 'homeCounty'; - static const String kWorkCountyFieldName = 'workCounty'; - static const String kProvidersFieldName = 'providers'; - static const String kConsentFieldName = 'consent';*/ - - static const String kFaceImageFieldName = 'faceImage'; - static const String kFaceBase64FieldName = 'faceBase64'; - - Map _scanResult; - bool _processingScanResult; - bool _applyingScanResult; - UserDocumentType _documenType; - Map _scanData; - - @override - void initState() { - _processingScanResult = true; - _documenType = widget.onboardingContext['userDocumentType']; - _scanData = widget.onboardingContext['scanData']; - compute(_buildScanResult, _scanData).then((Map scanResult) { - setState(() { - _processingScanResult = false; - _scanResult = scanResult; - }); - }); - - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Styles().colors.background, - body: SafeArea(child: Column(mainAxisAlignment: MainAxisAlignment.start, children: [ - Container(color: Styles().colors.white, child: Stack(children: [ - OnboardingHealthProgress(progress: 0.75,), - Align(alignment: Alignment.topLeft, - child: OnboardingBackButton(image: 'images/chevron-left-blue.png', padding: EdgeInsets.only(top: 16, right: 20, bottom: 20), onTap: () => _goBack()), - ), - Align(alignment: Alignment.topCenter, child: - Padding(padding: EdgeInsets.only(left: 24, right: 24, top: 24, bottom: 12), child: - Semantics( header: true, hint: Localization().getStringEx("app.common.heading.one.hint","Header 1"), - child:Text(Localization().getStringEx('panel.health.onboarding.covid19.review_scan.label.title', 'Review your scan',), - textAlign: TextAlign.center, - style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, color: Styles().colors.fillColorPrimary), - )) - ), - ), - ],),), - Expanded(child: - SingleChildScrollView(child: - Column(children: [ - _buildPreviewWidget(), - ],) - ), - ), - Container(height: 12,), - Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: - RoundedButton( - label: Localization().getStringEx('panel.health.onboarding.covid19.review_scan.button.rescan.title', 'Re-scan'), - hint: Localization().getStringEx('panel.health.onboarding.covid19.review_scan.button.rescan.hint', ''), - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - padding: EdgeInsets.symmetric(horizontal: 22), - onTap: () => _onRescan(), - height: 48, - ),), - Container(height: 12,), - Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: - Stack(children: [ - RoundedButton( - label: Localization().getStringEx('panel.health.onboarding.covid19.review_scan.button.use_scan.title', "Use This Scan"), - hint: Localization().getStringEx('panel.health.onboarding.covid19.review_scan.button.use_scan.hint', ''), - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - onTap: () => _onUseScan(), - height: 48, - ), - Visibility(visible: (_applyingScanResult == true), - child: Container(height: 48, - child: Align(alignment: Alignment.center, - child: SizedBox(height: 24, width: 24, - child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), ) - ), - ), - ), - ), - ],), - ), - Container(height: 24,), - ],),), - ); - } - - Widget _buildPreviewWidget() { - MemoryImage faceImage = (_scanResult != null) ? _scanResult[kFaceImageFieldName] : null; - - String nameText = ((_scanResult != null) ? _scanResult[kFullNameFieldName] : null) ?? ''; - String nameLabel = nameText.isNotEmpty ? Localization().getStringEx('panel.health.onboarding.covid19.review_scan.label.name.title', 'Name',) : ''; - - String birthYearText = ((_scanResult != null) ? _scanResult[kBirthYearFieldName] : null) ?? ''; - String birthYearLabel = birthYearText.isNotEmpty ? Localization().getStringEx('panel.health.onboarding.covid19.review_scan.label.birth_year.title', 'Birth Year',) : ''; - - return Padding(padding: EdgeInsets.symmetric(horizontal: 32, vertical: 32), - child: Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: Styles().colors.white, - borderRadius: BorderRadius.all(Radius.circular(4)), - boxShadow: [BoxShadow(color: Styles().colors.fillColorPrimaryTransparent015, spreadRadius: 2.0, blurRadius: 6.0, offset: Offset(0, 2))], - ), - child: Stack(children: [ - Visibility(visible: (_processingScanResult != true), child: - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container(width: 55, height: 70, - decoration: BoxDecoration( - color: Styles().colors.fillColorPrimaryTransparent03, - borderRadius: BorderRadius.all(Radius.circular(2)), - image: (faceImage != null) ? DecorationImage(fit: BoxFit.cover, alignment: Alignment.center, image: faceImage) : null, - ), - ), - Expanded( - child: Padding(padding: EdgeInsets.only(left: 16), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(nameLabel, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Styles().colors.textSurface)), - Text(nameText, style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, color: Styles().colors.fillColorPrimary)), - Container(height: 16,), - Text(birthYearLabel, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Styles().colors.textSurface)), - Text(birthYearText, style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, color: Styles().colors.fillColorPrimary)), - ],), - ), - ), - ],), - ), - Visibility(visible: (_processingScanResult == true), - child: Container( - height: 70, - child: Align(alignment: Alignment.center, - child: SizedBox(height: 24, width: 24, - child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), ) - ), - ), - ), - ), - ],) - - - ), - ); - } - - static Map _buildScanResult(Map rawResult) { - - Map rawMrz = rawResult['mrz']; - - String rawFirstName = rawResult['firstName']; // "WILLIAM C III" - if ((rawFirstName == null) && (rawMrz != null)) { - rawFirstName = rawMrz['secondaryID']; // "PETER MARK" - } - String firstName = _buildName(rawFirstName); - String middleName = _buildName(rawFirstName, index: 1); - - String rawLastName = rawResult['lastName']; // "SULLIVAN" - if ((rawLastName == null) && (rawMrz != null)) { - rawLastName = rawMrz['primaryID']; // "HENNESSY" - } - String lastName = _buildName(rawLastName); - - String dateOfBirth = rawResult['dateOfBirth']; // "09/30/1958" - if ((dateOfBirth == null) && (rawMrz != null)) { - dateOfBirth = rawMrz['dateOfBirth']; // "11/22/1960" - } - String birthYear = ((dateOfBirth != null) && RegExp('[0-9]{2}/[0-9]{2}/[0-9]{4}').hasMatch(dateOfBirth)) ? dateOfBirth.substring(6, 10) : null; - - String country; - if (rawMrz != null) { - for (String key in ['sanitizedNationality', 'nationality', 'sanitizedIssuer', 'issuer']) { - String entry = rawMrz[key]; - if ((entry != null) && (0 < entry.length)) { - country = entry; - break; - } - } - } - - String rawAddress = rawResult['address']; // "1804 PLEASANT ST, URBANA, IL, 618010000" - String address = rawAddress, state, zip; - if (rawAddress != null) { - List addressComponents = rawAddress.split(','); - int componentsCount = addressComponents.length; - if ((addressComponents != null) && (1 < componentsCount)) { - - String aZip = addressComponents[componentsCount - 1].trim(); - bool hasZip = RegExp('[0-9]{5,}').hasMatch(aZip); - - String aState = addressComponents[componentsCount - 2].trim(); - bool hasState = RegExp('[a-zA-Z]{2,}').hasMatch(aState); - - if (hasZip && hasState) { - zip = aZip.substring(0, 5); - state = aState; - if (country == null) { - country = 'USA'; - } - - address = ''; - for (int index = 0; (index + 2) < componentsCount; index++) { - if (0 < index) { - address += ','; - } - address += addressComponents[index]; - } - } - } - } - - String fullName = ''; - if ((firstName != null) && (0 < firstName.length)) { - fullName += "${(0 < fullName.length) ? ' ' : ''}$firstName"; - } - if ((middleName != null) && (0 < middleName.length)) { - fullName += "${(0 < fullName.length) ? ' ' : ''}$middleName"; - } - if ((lastName != null) && (0 < lastName.length)) { - fullName += "${(0 < fullName.length) ? ' ' : ''}$lastName"; - } - - String fullAddress = address ?? ''; - if ((state != null) && (zip != null)) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$state $zip"; - } - else if (state != null) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$state"; - } - else if (zip != null) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$zip"; - } - if (country != null) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$country"; - } - - String base64FaceImage = rawResult['base64FaceImage']; - Uint8List faceImageData = (base64FaceImage != null) ? base64Decode(base64FaceImage) : null; - MemoryImage faceImage = (faceImageData != null) ? MemoryImage(faceImageData) : null; - - return { - // These should go to PII - kFirstNameFieldName : firstName, - kMiddleNameFieldName : middleName, - kLastNameFieldName : lastName, - kBirthYearFieldName : birthYear, - kAddressFieldName : address, - kStateFieldName : state, - kZipFieldName : zip, - kCountryFieldName : country, - kFaceBase64FieldName : base64FaceImage, - - // These are for display purpose only - kFullNameFieldName : fullName, - kFullAddressFieldName : fullAddress, - kFaceImageFieldName : faceImage, - }; - } - - static String _buildName(String rawName, {int index = 0}) { - String resultName; - if (rawName != null) { - List firstNameComponents = rawName.split(' '); - if ((firstNameComponents != null) && (0 <= index) && (index < firstNameComponents.length)) { - resultName = firstNameComponents[index]; - } - else if (index == 0) { - resultName = rawName; - } - resultName = (resultName != null) ? AppString.capitalize(resultName) : null; - } - return resultName; - } - - Future _applyScan() async { - - UserPiiData updatedUserPiiData; - UserPiiData userPiiData = UserPiiData.fromObject(await Auth().reloadUserPiiData()); - if (userPiiData != null) { - _applyScanResult(userPiiData); - updatedUserPiiData = await Auth().storeUserPiiData(userPiiData); - } - - return (updatedUserPiiData != null); - } - - void _applyScanResult(UserPiiData userPiiData) { - - String photoBase64 = _scanResult[kFaceBase64FieldName]; - if (photoBase64 != null) { - userPiiData.photoBase64 = photoBase64; - } - - String firstName = _scanResult[kFirstNameFieldName]; - if (firstName != null) { - userPiiData.firstName = firstName; - } - - String middleName = _scanResult[kMiddleNameFieldName]; - if (middleName != null) { - userPiiData.middleName = middleName; - } - - String lastName = _scanResult[kLastNameFieldName]; - if (lastName != null) { - userPiiData.lastName = lastName; - } - - String birthYearString = _scanResult[kBirthYearFieldName]; - int birthYear = ((birthYearString != null) && (0 < birthYearString.length)) ? int.tryParse(birthYearString) : null; - if (birthYear != null) { - userPiiData.birthYear = birthYear; - } - - //Don't store this data in PiiData for now -/* String address = _scanResult[kAddressFieldName]; - if ((address != null) && (0 < address.length)) { - userPiiData.address = address; - } - - String state = _scanResult[kStateFieldName]; - if ((state != null) && (0 < state.length)) { - userPiiData.state = state; - } - - String zip = _scanResult[kZipFieldName]; - if ((zip != null) && (0 < zip.length)) { - userPiiData.zip = zip; - } - - String country = _scanResult[kCountryFieldName]; - if ((country != null) && (0 < country.length)) { - userPiiData.country = country; - }*/ - - if (_documenType != null) { - userPiiData.documentType = _documenType; - } -} - - void _goBack() { - Analytics.instance.logSelect(target: "Back"); - Navigator.of(context).pop(); - } - - void _goNext() { - if (Auth().isLoggedIn) { - widget.onboardingContext['shouldDisplayQrCode'] = true; - } else { - widget.onboardingContext['shouldDisplayQrCode'] = false; - } - Onboarding().next(context, widget); - } - - void _onRescan() { - Analytics.instance.logSelect(target: 'Re-scan') ; - - String analyticsScanType; - List recognizers; - if (_documenType == UserDocumentType.drivingLicense) { - analyticsScanType = Analytics.LogDocumentScanDrivingLicenseType; - recognizers = ['combined']; - } - else if (_documenType == UserDocumentType.passport) { - analyticsScanType = Analytics.LogDocumentScanPassportType; - recognizers = ['passport']; - } - - NativeCommunicator().microBlinkScan(recognizers: recognizers).then((dynamic result) { - Analytics().logDocumentScan(type: analyticsScanType, result: (result != null)); - if (result != null) { - _didRescan(result); - } - }); - } - - void _didRescan(Map scanData) { - setState(() { - _processingScanResult = true; - }); - compute(_buildScanResult, _scanData).then((Map scanResult) { - setState(() { - _processingScanResult = false; - _scanResult = scanResult; - }); - }); - - } - - void _onUseScan() { - Analytics.instance.logSelect(target: 'Use This Scan') ; - - if (_scanResult == null) { - return; - } - - setState(() { - _applyingScanResult = true; - }); - - _applyScan().then((bool result){ - - setState(() { - _applyingScanResult = false; - }); - - if (result) { - _goNext(); - } - else { - AppAlert.showDialogResult(context, Localization().getStringEx('panel.health.onboarding.covid19.review_scan.message.failed', 'Failed to apply scanned data',)); - } - }); - } - -} \ No newline at end of file diff --git a/lib/ui/settings/SettingsHomePanel.dart b/lib/ui/settings/SettingsHomePanel.dart index 2b53e3f8..04d57bbc 100644 --- a/lib/ui/settings/SettingsHomePanel.dart +++ b/lib/ui/settings/SettingsHomePanel.dart @@ -160,8 +160,8 @@ class _SettingsHomePanelState extends State implements Notifi else if (code == 'account') { contentList.add(_buildAccount()); } - else if (code == 'feedback') { - contentList.add(_buildFeedback(),); + else if (code == 'get_help') { + contentList.add(_buildGetHelp(),); } } @@ -1139,55 +1139,40 @@ class _SettingsHomePanelState extends State implements Notifi } } - // Feedback + // Get Help - Widget _buildFeedback(){ + Widget _buildGetHelp(){ return Column( children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - Localization().getStringEx("panel.settings.home.feedback.title", "We need your ideas!"), - style: TextStyle(color: Styles().colors.fillColorPrimary, fontSize: 20), - ), - Container(height: 5,), - Text( - Localization().getStringEx("panel.settings.home.feedback.description", "Enjoying the app? Missing something? Tap on the bottom to submit your idea."), - style: TextStyle(fontFamily: Styles().fontFamilies.regular,color: Styles().colors.textBackground, fontSize: 16), - ), - ]) - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 18, vertical: 12), + padding: EdgeInsets.only(left: 18, right: 18, top: 24, bottom: 6), child: ScalableRoundedButton( - label: Localization().getStringEx("panel.settings.home.button.feedback.title", "Submit Feedback"), - hint: Localization().getStringEx("panel.settings.home.button.feedback.hint", ""), + label: Localization().getStringEx("panel.settings.home.button.get_help.title", "Get Help"), + hint: Localization().getStringEx("panel.settings.home.button.get_help.hint", ""), backgroundColor: Styles().colors.background, fontSize: 16.0, textColor: Styles().colors.fillColorPrimary, borderColor: Styles().colors.fillColorSecondary, showExternalLink: true, - onTap: _onFeedbackClicked, + onTap: _onGetHelpClicked, ), ), ], ); } - void _onFeedbackClicked() { + void _onGetHelpClicked() { if (Connectivity().isNotOffline) { - Analytics.instance.logSelect(target: "Provide Feedback"); + Analytics.instance.logSelect(target: "Get Help"); - if (Connectivity().isNotOffline && (Config().feedbackUrl != null)) { - String feedbackUrl = Config().feedbackUrl; - - String panelTitle = Localization().getStringEx('panel.settings.feedback.label.title', 'PROVIDE FEEDBACK'); - Navigator.push( - context, CupertinoPageRoute(builder: (context) => WebPanel(url: feedbackUrl, title: panelTitle,))); + if (Connectivity().isNotOffline && (Config().getHelpUrl != null)) { + Navigator.push(context, CupertinoPageRoute(builder: (context) => WebPanel( + url: Config().getHelpUrl, + title: Localization().getStringEx('panel.settings.get_help.label.title', 'Get Help'),) + )); } else { - AppAlert.showOfflineMessage(context, Localization().getStringEx('panel.settings.label.offline.feedback', 'Providing a Feedback is not available while offline.')); + AppAlert.showOfflineMessage(context, Localization().getStringEx('panel.settings.label.offline.get_help', 'Getting a help is not available while offline.')); } } else { AppAlert.showOfflineMessage(context); @@ -1198,7 +1183,7 @@ class _SettingsHomePanelState extends State implements Notifi Widget _buildDebug() { return Padding( - padding: EdgeInsets.symmetric(horizontal: 18, vertical: 24), + padding: EdgeInsets.only(left: 18, right: 18, top: 6, bottom: 6), child: ScalableRoundedButton( label: Localization().getStringEx("panel.profile_info.button.debug.title", "Debug"), hint: Localization().getStringEx("panel.profile_info.button.debug.hint", ""), @@ -1231,12 +1216,15 @@ class _SettingsHomePanelState extends State implements Notifi // Version Info Widget _buildVersionInfo(){ - return Container( - alignment: Alignment.center, - child: Text( - "Version: $_versionName", - style: TextStyle(color: Styles().colors.textBackground, fontFamily: Styles().fontFamilies.regular, fontSize: 16), - )); + return Padding( + padding: EdgeInsets.only(left: 18, right: 18, top: 6, bottom: 6), + child: Container( + alignment: Alignment.center, + child: Text( + "Version: $_versionName", + style: TextStyle(color: Styles().colors.textBackground, fontFamily: Styles().fontFamilies.regular, fontSize: 16), + )) + ); } void _loadVersionInfo() async { diff --git a/lib/ui/settings2/Settings2ConsentPanel.dart b/lib/ui/settings2/Settings2ConsentPanel.dart deleted file mode 100644 index 401f695c..00000000 --- a/lib/ui/settings2/Settings2ConsentPanel.dart +++ /dev/null @@ -1,245 +0,0 @@ - - -import 'package:flutter/material.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/Health.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/ui/widgets/HeaderBar.dart'; -import 'package:illinois/ui/widgets/RibbonButton.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; - -class Settings2ConsentPanel extends StatefulWidget{ - Settings2ConsentPanel(); - _Settings2ConsentPanelState createState() => _Settings2ConsentPanelState(); -} - -class _Settings2ConsentPanelState extends State implements NotificationsListener{ - - bool _isDisabling = false; - bool _isEnabling = false; - - @override - void initState() { - super.initState(); - NotificationService().subscribe(this, [Health.notifyUserUpdated]); - } - - @override - void dispose() { - super.dispose(); - NotificationService().unsubscribe(this); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: SimpleHeaderBarWithBack( - context: context, - titleWidget: Text( - 'Automatic Test Results', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: Styles().fontFamilies.extraBold, - letterSpacing: 1.0, - ), - textAlign: TextAlign.center, - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(22), - child: Column( - children: [ - Text('This feature allows you to receive COVID-19 test results from your healthcare provider directly in the app. Results are encrypted, so only you can see them.', - style: TextStyle( - color: Styles().colors.fillColorPrimary, - fontFamily: Styles().fontFamilies.regular, - fontSize: 16, - ), - ), - Container(height: 18,), - Stack( - alignment: Alignment.center, - children: [ - ToggleRibbonButton( - label: "I consent to connect test results from my healthcare provider with the Safer Illinois app.", - style: TextStyle( - color: Styles().colors.fillColorPrimary, - fontFamily: Styles().fontFamilies.medium, - fontSize: 14 - ), - height: null, - border: Border.all(width: 1, color: Styles().colors.surfaceAccent), - borderRadius: BorderRadius.all(Radius.circular(4)), - toggled: Health().user.consentTestResults, - onTap: (){ - if(!Health().user.consentTestResults){ - _onConsentEnabled(); - } - else{ - showDialog(context: context, builder: (context) => _buildConsentDialog(context)); - } - }, - ), - _isEnabling ? Align(alignment: Alignment.center, child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary), strokeWidth: 2,)) : Container() - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildConsentDialog(BuildContext context) { - return StatefulBuilder( - builder: (context, setState){ - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8.0)) - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: Styles().colors.fillColorPrimary, - borderRadius: BorderRadius.vertical(top: Radius.circular(8)) - ), - child: Padding( - padding: EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: Center( - child: Text( - "Automatic Test Results", - style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 24, color: Colors.white), - ), - ), - ), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(15)), - border: Border.all(color: Styles().colors.white, width: 2), - ), - child: Center( - child: Text( - '\u00D7', - style: TextStyle( - fontSize: 24, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - Container( - height: 26, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: Text( - "By removing your consent, you will no longer receive automatic test results from your health provider.\n\nPrevious test results will remain in your COVID-19 event history. You can delete them by accessing Your COVID-19 Event History in the Privacy Center.", - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Colors.black), - ), - ), - Container( - height: 26, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: RoundedButton( - onTap: () { - Analytics.instance.logAlert(text: "Consent", selection: "No"); - Navigator.pop(context); - }, - backgroundColor: Colors.transparent, - borderColor: Styles().colors.fillColorPrimary, - textColor: Styles().colors.fillColorPrimary, - label: "No"), - ), - Container( - width: 10, - ), - Expanded( - child: Stack( - alignment: Alignment.center, - children: [ - RoundedButton( - onTap: () => _onConsentDisabled(context, setState), - backgroundColor: Styles().colors.fillColorSecondaryVariant, - borderColor: Styles().colors.fillColorSecondaryVariant, - textColor: Styles().colors.surface, - label: 'Remove Consent'), - _isDisabling ? Align(alignment: Alignment.center, child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary), strokeWidth: 2,)) : Container() - ], - ), - ), - ], - ), - ), - ], - ), - ); - }, - ); - } - - void _onConsentDisabled(BuildContext context, StateSetter _setState){ - if(_isDisabling){ - return; - } - _setState((){ - _isDisabling = true; - }); - Health().loginUser(consentTestResults: false).whenComplete((){ - _setState((){ - _isDisabling = false; - }); - Navigator.pop(context); - }); - } - - void _onConsentEnabled(){ - if(_isEnabling){ - return; - } - setState((){ - _isEnabling = true; - }); - Health().loginUser(consentTestResults: true).whenComplete((){ - setState((){ - _isEnabling = false; - }); - }); - } - - @override - void onNotification(String name, param) { - if(name == Health.notifyUserUpdated){ - setState(() {}); - } - } -} \ No newline at end of file diff --git a/lib/ui/settings2/Settings2ExposureNotificationsPanel.dart b/lib/ui/settings2/Settings2ExposureNotificationsPanel.dart deleted file mode 100644 index cc573de8..00000000 --- a/lib/ui/settings2/Settings2ExposureNotificationsPanel.dart +++ /dev/null @@ -1,248 +0,0 @@ - - -import 'package:flutter/material.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/Health.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/ui/widgets/HeaderBar.dart'; -import 'package:illinois/ui/widgets/RibbonButton.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; - -class Settings2ExposureNotificationsPanel extends StatefulWidget{ - Settings2ExposureNotificationsPanel(); - _Settings2ExposureNotificationsPanelState createState() => _Settings2ExposureNotificationsPanelState(); -} - -class _Settings2ExposureNotificationsPanelState extends State implements NotificationsListener{ - - bool _isDisabling = false; - bool _isEnabling = false; - - @override - void initState() { - super.initState(); - NotificationService().subscribe(this, [Health.notifyUserUpdated]); - } - - @override - void dispose() { - super.dispose(); - NotificationService().unsubscribe(this); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: SimpleHeaderBarWithBack( - context: context, - titleWidget: Text( - 'Exposure Notificaitons', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: Styles().fontFamilies.extraBold, - letterSpacing: 1.0, - ), - textAlign: TextAlign.center, - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(22), - child: Column( - children: [ - Text('If you opt in to exposure notifications, you allow your phone to send an anonymous Bluetooth signal to nearby Safer Illinois app users who are also using this feature. Your phone will receive and record a signal from their phones as well. If one of those users tests positive for COVID-19 in the next 14 days, the app will alert you to your potential exposure and advise you on next steps.\n\nYour identity and health status will remain anonymous, as will the identity and health status of all other users.', - style: TextStyle( - color: Styles().colors.fillColorPrimary, - fontFamily: Styles().fontFamilies.regular, - fontSize: 16, - ), - ), - Container(height: 18,), - Stack( - alignment: Alignment.center, - children: [ - ToggleRibbonButton( - label: "I opt in to participate in the Exposure Notification System (requires Bluetooth to be ON)", - style: TextStyle( - color: Styles().colors.fillColorPrimary, - fontFamily: Styles().fontFamilies.medium, - fontSize: 14 - ), - height: null, - border: Border.all(width: 1, color: Styles().colors.surfaceAccent), - borderRadius: BorderRadius.all(Radius.circular(4)), - toggled: Health().user.consentExposureNotification, - onTap: (){ - if(!Health().user.consentExposureNotification){ - _onConsentEnabled(); - } - else{ - showDialog(context: context, builder: (context) => _buildConsentDialog(context)); - } - }, - ), - _isEnabling ? Align(alignment: Alignment.center, child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary), strokeWidth: 2,)) : Container() - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildConsentDialog(BuildContext context) { - return StatefulBuilder( - builder: (context, setState){ - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8.0)) - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: Styles().colors.fillColorPrimary, - borderRadius: BorderRadius.vertical(top: Radius.circular(8)) - ), - child: Padding( - padding: EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: Center( - child: Text( - "Exposure Notifications", - style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 24, color: Colors.white), - ), - ), - ), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(15)), - border: Border.all(color: Styles().colors.white, width: 2), - ), - child: Center( - child: Text( - '\u00D7', - style: TextStyle( - fontSize: 24, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - Container( - height: 26, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: Text( - "By opting out of exposure notifications, your phone will no longer send and recieve anonymous Bluetooth signals to alert you or others of potential exposure to COVID-19.", - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Colors.black), - ), - ), - Container( - height: 26, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: RoundedButton( - onTap: () { - Analytics.instance.logAlert(text: "Remove My Information", selection: "No"); - Navigator.pop(context); - }, - backgroundColor: Colors.transparent, - borderColor: Styles().colors.fillColorPrimary, - textColor: Styles().colors.fillColorPrimary, - label: Localization().getStringEx("panel.profile_info.dialog.remove_my_information.no.title", "No")), - ), - Container( - width: 10, - ), - Expanded( - child: Stack( - alignment: Alignment.center, - children: [ - RoundedButton( - onTap: () => _onConsentDisabled(context, setState), - backgroundColor: Styles().colors.fillColorSecondaryVariant, - borderColor: Styles().colors.fillColorSecondaryVariant, - textColor: Styles().colors.surface, - label: 'Opt-Out'), - _isDisabling ? Align(alignment: Alignment.center, child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary), strokeWidth: 2,)) : Container() - ], - ), - ), - ], - ), - ), - ], - ), - ); - }, - ); - } - - void _onConsentDisabled(BuildContext context, StateSetter _setState){ - if(_isDisabling){ - return; - } - _setState((){ - _isDisabling = true; - }); - Health().loginUser(consentExposureNotification: false).whenComplete((){ - _setState((){ - _isDisabling = false; - }); - Navigator.pop(context); - }); - } - - void _onConsentEnabled(){ - if(_isEnabling){ - return; - } - setState((){ - _isEnabling = true; - }); - Health().loginUser(consentExposureNotification: true).whenComplete((){ - setState((){ - _isEnabling = false; - }); - }); - } - - @override - void onNotification(String name, param) { - if(name == Health.notifyUserUpdated){ - setState(() { - - }); - } - } -} \ No newline at end of file diff --git a/lib/ui/settings2/Settings2GovernmentIdPanel.dart b/lib/ui/settings2/Settings2GovernmentIdPanel.dart deleted file mode 100644 index 1a3005ff..00000000 --- a/lib/ui/settings2/Settings2GovernmentIdPanel.dart +++ /dev/null @@ -1,694 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:illinois/model/UserProfile.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/Exposure.dart'; -import 'package:illinois/service/Health.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/service/NativeCommunicator.dart'; -import 'package:illinois/service/Onboarding.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/service/UserProfile.dart'; -import 'package:illinois/ui/widgets/HeaderBar.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/utils/Utils.dart'; - -class Settings2GovernmentIdPanel extends StatefulWidget with OnboardingPanel { - - final Map initialData; - - Settings2GovernmentIdPanel({this.initialData}); - - _Settings2GovernmentIdPanelPanelState createState() => _Settings2GovernmentIdPanelPanelState(); - - @override - bool get onboardingCanDisplay { - return (onboardingContext != null) && onboardingContext['shouldDisplayReviewScan'] == true; - } -} - -class _Settings2GovernmentIdPanelPanelState extends State { - - static const String kFirstNameFieldName = 'firstName'; - static const String kMiddleNameFieldName = 'middleName'; - static const String kLastNameFieldName = 'lastName'; - static const String kFullNameFieldName = 'fullName'; - - static const String kBirthYearFieldName = 'birthYear'; - - static const String kAddressFieldName = 'address'; - static const String kStateFieldName = 'state'; - static const String kZipFieldName = 'zip'; - static const String kCountryFieldName = 'country'; - static const String kFullAddressFieldName = 'fullAddress'; - - static const String kFaceImageFieldName = 'faceImage'; - static const String kFaceBase64FieldName = 'faceBase64'; - - Map _scanResult; - bool _processingScanResult; - bool _applyingScanResult; - UserDocumentType _documenType; - Map _scanData; - - String _fullName; - String _birthYear; - MemoryImage _photoImage; - - bool _isDeleting = false; - - @override - void initState() { - super.initState(); - - _processingScanResult = false; - _documenType = Auth()?.userPiiData?.documentType; - _scanData = {}; - - _loadInitialData(); - } - - void _loadInitialData(){ - if(widget.initialData != null){ - _scanData = widget.initialData['scanData']; - _documenType = widget.initialData['userDocumentType']; - _loadScanResult(); - } - else{ - _fullName = Auth()?.userPiiData?.fullName; - _birthYear = Auth()?.userPiiData?.birthYear?.toString() ?? ''; - _loadAsyncPhotoBytes(); - } - - } - - Future _deleteUserData() async{ - Analytics.instance.logAlert(text: "Remove My Information", selection: "Yes"); - - await Health().deleteUser(); - await Exposure().deleteUser(); - bool piiDeleted = await Auth().deleteUserPiiData(); - if(piiDeleted) { - await UserProfile().deleteProfile(); - } - Auth().logout(); - } - - Future _loadAsyncPhotoBytes() async { - Uint8List photoBytes = await Auth()?.userPiiData?.photoBytes; - if(AppCollection.isCollectionNotEmpty(photoBytes)){ - _photoImage = await compute(AppImage.memoryImageWithBytes, photoBytes); - setState(() {}); - } - } - - Future _loadScanResult() async{ - compute(_buildScanResult, _scanData).then((Map scanResult) { - setState(() { - _processingScanResult = false; - _scanResult = scanResult; - }); - }); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: SimpleHeaderBarWithBack( - context: context, - titleWidget: Text( - 'Your Government ID', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: Styles().fontFamilies.extraBold, - letterSpacing: 1.0, - ), - textAlign: TextAlign.center, - ), - ), - backgroundColor: Styles().colors.background, - body: SafeArea(child: Column(mainAxisAlignment: MainAxisAlignment.start, children: [ - Expanded(child: - SingleChildScrollView(child: - Column(children: [ - Container(height: 20,), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text('You provided this information to verify your identity during COVID-19 onboarding. You may delete or replace the information below.', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - color: Styles().colors.textSurface, - fontSize: 16 - ), - ), - ), - _buildPreviewWidget(), - ],) - ), - ), - Container(height: 12,), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: RoundedButton( - label: 'Re-scan', - hint: '', - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - padding: EdgeInsets.symmetric(horizontal: 22), - onTap: () => _onRescan(), - height: 48, - ), - ), - Container(width: 12,), - Expanded( - child: Stack(children: [ - RoundedButton( - label: "Use This Scan", - hint: '', - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - onTap: () => _onUseScan(), - height: 48, - ), - Visibility(visible: (_applyingScanResult == true), - child: Container(height: 48, - child: Align(alignment: Alignment.center, - child: SizedBox(height: 24, width: 24, - child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), ) - ), - ), - ), - ), - ],), - ), - ], - ), - ), - Container(height: 16,), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: RoundedButton( - label: 'Delete my COVID-19 Information', - hint: '', - backgroundColor: Styles().colors.surface, - fontSize: 16.0, - textColor: Styles().colors.fillColorSecondary, - borderColor: Styles().colors.surfaceAccent, - onTap: _onRemoveMyInfoClicked, - - ), - ), - Container(height: 16,) - ],),), - ); - } - - Widget _buildPreviewWidget() { - MemoryImage faceImage = (_scanResult != null) ? _scanResult[kFaceImageFieldName] : _photoImage; - - String nameText = ((_scanResult != null) ? _scanResult[kFullNameFieldName] : _fullName) ?? ''; - String nameLabel = nameText.isNotEmpty ? 'Name' : ''; - - String birthYearText = ((_scanResult != null) ? _scanResult[kBirthYearFieldName] : _birthYear) ?? ''; - String birthYearLabel = birthYearText.isNotEmpty ? 'Birth Year' : ''; - - return Padding(padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), - child: Container( - decoration: BoxDecoration( - color: Styles().colors.white, - borderRadius: BorderRadius.all(Radius.circular(4)), - boxShadow: [BoxShadow(color: Styles().colors.fillColorPrimaryTransparent015, spreadRadius: 2.0, blurRadius: 6.0, offset: Offset(0, 2))], - ), - child: Stack(children: [ - Visibility(visible: (_processingScanResult != true), child: - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container(width: 55, height: 70, - decoration: BoxDecoration( - color: Styles().colors.fillColorPrimaryTransparent03, - borderRadius: BorderRadius.all(Radius.circular(2)), - image: (faceImage != null) ? DecorationImage(fit: BoxFit.cover, alignment: Alignment.center, image: faceImage) : null, - ), - ), - Expanded( - child: Padding(padding: EdgeInsets.only(left: 16), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(nameLabel, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Styles().colors.textSurface)), - Text(nameText, style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, color: Styles().colors.fillColorPrimary)), - Container(height: 16,), - Text(birthYearLabel, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Styles().colors.textSurface)), - Text(birthYearText, style: TextStyle(fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, color: Styles().colors.fillColorPrimary)), - ],), - ), - ), - ],), - ), - Visibility(visible: (_processingScanResult == true), - child: Container( - height: 70, - child: Align(alignment: Alignment.center, - child: SizedBox(height: 24, width: 24, - child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), ) - ), - ), - ), - ), - ],) - - - ), - ); - } - - static Map _buildScanResult(Map rawResult) { - - Map rawMrz = rawResult['mrz']; - - String rawFirstName = rawResult['firstName']; // "WILLIAM C III" - if ((rawFirstName == null) && (rawMrz != null)) { - rawFirstName = rawMrz['secondaryID']; // "PETER MARK" - } - String firstName = _buildName(rawFirstName); - String middleName = _buildName(rawFirstName, index: 1); - - String rawLastName = rawResult['lastName']; // "SULLIVAN" - if ((rawLastName == null) && (rawMrz != null)) { - rawLastName = rawMrz['primaryID']; // "HENNESSY" - } - String lastName = _buildName(rawLastName); - - String dateOfBirth = rawResult['dateOfBirth']; // "09/30/1958" - if ((dateOfBirth == null) && (rawMrz != null)) { - dateOfBirth = rawMrz['dateOfBirth']; // "11/22/1960" - } - String birthYear = ((dateOfBirth != null) && RegExp('[0-9]{2}/[0-9]{2}/[0-9]{4}').hasMatch(dateOfBirth)) ? dateOfBirth.substring(6, 10) : null; - - String country; - if (rawMrz != null) { - for (String key in ['sanitizedNationality', 'nationality', 'sanitizedIssuer', 'issuer']) { - String entry = rawMrz[key]; - if ((entry != null) && (0 < entry.length)) { - country = entry; - break; - } - } - } - - String rawAddress = rawResult['address']; // "1804 PLEASANT ST, URBANA, IL, 618010000" - String address = rawAddress, state, zip; - if (rawAddress != null) { - List addressComponents = rawAddress.split(','); - int componentsCount = addressComponents.length; - if ((addressComponents != null) && (1 < componentsCount)) { - - String aZip = addressComponents[componentsCount - 1].trim(); - bool hasZip = RegExp('[0-9]{5,}').hasMatch(aZip); - - String aState = addressComponents[componentsCount - 2].trim(); - bool hasState = RegExp('[a-zA-Z]{2,}').hasMatch(aState); - - if (hasZip && hasState) { - zip = aZip.substring(0, 5); - state = aState; - if (country == null) { - country = 'USA'; - } - - address = ''; - for (int index = 0; (index + 2) < componentsCount; index++) { - if (0 < index) { - address += ','; - } - address += addressComponents[index]; - } - } - } - } - - String fullName = ''; - if ((firstName != null) && (0 < firstName.length)) { - fullName += "${(0 < fullName.length) ? ' ' : ''}$firstName"; - } - if ((middleName != null) && (0 < middleName.length)) { - fullName += "${(0 < fullName.length) ? ' ' : ''}$middleName"; - } - if ((lastName != null) && (0 < lastName.length)) { - fullName += "${(0 < fullName.length) ? ' ' : ''}$lastName"; - } - - String fullAddress = address ?? ''; - if ((state != null) && (zip != null)) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$state $zip"; - } - else if (state != null) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$state"; - } - else if (zip != null) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$zip"; - } - if (country != null) { - fullAddress += "${(0 < fullAddress.length) ? ', ' : ''}$country"; - } - - String base64FaceImage = rawResult['base64FaceImage']; - Uint8List faceImageData = (base64FaceImage != null) ? base64Decode(base64FaceImage) : null; - MemoryImage faceImage = (faceImageData != null) ? MemoryImage(faceImageData) : null; - - return { - // These should go to PII - kFirstNameFieldName : firstName, - kMiddleNameFieldName : middleName, - kLastNameFieldName : lastName, - kBirthYearFieldName : birthYear, - kAddressFieldName : address, - kStateFieldName : state, - kZipFieldName : zip, - kCountryFieldName : country, - kFaceBase64FieldName : base64FaceImage, - - // These are for display purpose only - kFullNameFieldName : fullName, - kFullAddressFieldName : fullAddress, - kFaceImageFieldName : faceImage, - }; - } - - static String _buildName(String rawName, {int index = 0}) { - String resultName; - if (rawName != null) { - List firstNameComponents = rawName.split(' '); - if ((firstNameComponents != null) && (0 <= index) && (index < firstNameComponents.length)) { - resultName = firstNameComponents[index]; - } - else if (index == 0) { - resultName = rawName; - } - resultName = (resultName != null) ? AppString.capitalize(resultName) : null; - } - return resultName; - } - - Widget _buildRemoveMyInfoDialog(BuildContext context) { - return StatefulBuilder( - builder: (context, setState){ - return ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(8)), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Container( - color: Styles().colors.fillColorPrimary, - child: Padding( - padding: EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: Center( - child: Text( - "Remove My Info", - style: TextStyle(fontSize: 20, color: Colors.white), - ), - ), - ), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(15)), - border: Border.all(color: Styles().colors.white, width: 2), - ), - child: Center( - child: Text( - '\u00D7', - style: TextStyle( - fontSize: 24, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - Container( - height: 26, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: Text( - "By answering YES all your personal information and preferences will be deleted from our systems. This action can not be recovered. After deleting the information we will return you to the first screen when you installed the app so you can start again or delete the app.", - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Colors.black), - ), - ), - Container( - height: 26, - ), - Text( - "Are you sure?", - textAlign: TextAlign.center, - style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Colors.black), - ), - Container( - height: 26, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - children: [ - RoundedButton( - onTap: () => _onConfirmRemoveMyInfo(context, setState), - backgroundColor: Colors.transparent, - borderColor: Styles().colors.fillColorSecondary, - textColor: Styles().colors.fillColorPrimary, - label: Localization().getStringEx("panel.profile_info.dialog.remove_my_information.yes.title", "Yes")), - _isDeleting ? Align(alignment: Alignment.center, child: CircularProgressIndicator()) : Container() - ], - ), - Container( - height: 10, - ), - RoundedButton( - onTap: () { - Analytics.instance.logAlert(text: "Remove My Information", selection: "No"); - Navigator.pop(context); - }, - backgroundColor: Colors.transparent, - borderColor: Styles().colors.fillColorSecondary, - textColor: Styles().colors.fillColorPrimary, - label: Localization().getStringEx("panel.profile_info.dialog.remove_my_information.no.title", "No")) - ], - ), - ), - ], - ), - ), - ); - }, - ); - } - - Future _applyScan() async { - - UserPiiData updatedUserPiiData; - UserPiiData userPiiData = UserPiiData.fromObject(await Auth().reloadUserPiiData()); - if (userPiiData != null) { - _applyScanResult(userPiiData); - updatedUserPiiData = await Auth().storeUserPiiData(userPiiData); - } - - return (updatedUserPiiData != null); - } - - void _applyScanResult(UserPiiData userPiiData) { - - String photoBase64 = _scanResult[kFaceBase64FieldName]; - if (photoBase64 != null) { - userPiiData.photoBase64 = photoBase64; - } - - String firstName = _scanResult[kFirstNameFieldName]; - if (firstName != null) { - userPiiData.firstName = firstName; - } - - String middleName = _scanResult[kMiddleNameFieldName]; - if (middleName != null) { - userPiiData.middleName = middleName; - } - - String lastName = _scanResult[kLastNameFieldName]; - if (lastName != null) { - userPiiData.lastName = lastName; - } - - String birthYearString = _scanResult[kBirthYearFieldName]; - int birthYear = ((birthYearString != null) && (0 < birthYearString.length)) ? int.tryParse(birthYearString) : null; - if (birthYear != null) { - userPiiData.birthYear = birthYear; - } - - //Don't store this data in PiiData for now -/* String address = _scanResult[kAddressFieldName]; - if ((address != null) && (0 < address.length)) { - userPiiData.address = address; - } - - String state = _scanResult[kStateFieldName]; - if ((state != null) && (0 < state.length)) { - userPiiData.state = state; - } - - String zip = _scanResult[kZipFieldName]; - if ((zip != null) && (0 < zip.length)) { - userPiiData.zip = zip; - } - - String country = _scanResult[kCountryFieldName]; - if ((country != null) && (0 < country.length)) { - userPiiData.country = country; - }*/ - - if (_documenType != null) { - userPiiData.documentType = _documenType; - } - } - - - - void _onRescan() { - Analytics.instance.logSelect(target: 'Re-scan') ; - - String analyticsScanType; - List recognizers; - if (_documenType == UserDocumentType.drivingLicense) { - analyticsScanType = Analytics.LogDocumentScanDrivingLicenseType; - recognizers = ['combined']; - } - else if (_documenType == UserDocumentType.passport) { - analyticsScanType = Analytics.LogDocumentScanPassportType; - recognizers = ['passport']; - } - - NativeCommunicator().microBlinkScan(recognizers: recognizers).then((dynamic result) { - Analytics().logDocumentScan(type: analyticsScanType, result: (result != null)); - if (result != null) { - _didRescan(result); - } - }); - } - - void _didRescan(Map scanData) { - setState(() { - _processingScanResult = true; - }); - compute(_buildScanResult, scanData).then((Map scanResult) { - setState(() { - _processingScanResult = false; - _scanResult = scanResult; - }); - }); - - } - - void _onUseScan() { - Analytics.instance.logSelect(target: 'Use This Scan') ; - - if (_scanResult == null) { - Navigator.pop(context); - } - - setState(() { - _applyingScanResult = true; - }); - - _applyScan().then((bool result){ - - setState(() { - _applyingScanResult = false; - }); - - if (result) { - Navigator.pop(context); - } - else { - AppAlert.showDialogResult(context, 'Failed to apply scanned data'); - } - }); - } - - void _onConfirmRemoveMyInfo(BuildContext context, Function setState){ - setState(() { - _isDeleting = true; - }); - _deleteUserData() - .then((_){ - Navigator.pop(context); - }) - .whenComplete((){ - setState(() { - _isDeleting = false; - }); - }) - .catchError((error){ - AppAlert.showDialogResult(context, error.toString()).then((_){ - Navigator.pop(context); - }); - }); - - - } - - void _onRemoveMyInfoClicked() { - showDialog(context: context, builder: (context) => _buildRemoveMyInfoDialog(context)); - } -} \ No newline at end of file diff --git a/lib/ui/settings2/Settings2HomePanel.dart b/lib/ui/settings2/Settings2HomePanel.dart deleted file mode 100644 index 3735811b..00000000 --- a/lib/ui/settings2/Settings2HomePanel.dart +++ /dev/null @@ -1,845 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/AppNavigation.dart'; -import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/Connectivity.dart'; -import 'package:illinois/service/Exposure.dart'; -import 'package:illinois/service/Health.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/service/Log.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Organizations.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/service/UserProfile.dart'; -import 'package:illinois/ui/health/HealthHistoryPanel.dart'; -import 'package:illinois/ui/settings2/Settings2TransferEncryptionKeyPanel.dart'; -import 'package:illinois/ui/onboarding/OnboardingResidentInfoPanel.dart'; -import 'package:illinois/ui/onboarding/OnboardingLoginPhoneVerifyPanel.dart'; -import 'package:illinois/ui/settings2/Settings2ConsentPanel.dart'; -import 'package:illinois/ui/settings2/Settings2GovernmentIdPanel.dart'; -import 'package:illinois/ui/settings2/Settings2ExposureNotificationsPanel.dart'; -import 'package:illinois/ui/debug/DebugHomePanel.dart'; -import 'package:illinois/ui/widgets/HeaderBar.dart'; -import 'package:illinois/ui/widgets/RibbonButton.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/utils/Utils.dart'; -import 'package:intl/intl.dart'; - - -class Settings2HomePanel extends StatefulWidget { - @override - _Settings2HomePanelState createState() => _Settings2HomePanelState(); -} - -class _Settings2HomePanelState extends State implements NotificationsListener { - - bool _isLoading = false; - bool _isDeleting = false; - - @override - void initState() { - super.initState(); - - NotificationService().subscribe(this, [ - Auth.notifyUserPiiDataChanged, - UserProfile.notifyProfileUpdated, - Health.notifyUserUpdated, - ]); - - _loadHealthUser(); - } - - @override - void dispose() { - super.dispose(); - NotificationService().unsubscribe(this); - } - - void _updateState() { - if (mounted) { - setState(() {}); - } - } - - Future _deleteUserData() async{ - Analytics.instance.logAlert(text: "Remove My Information", selection: "Yes"); - - await Health().deleteUser(); - await Exposure().deleteUser(); - bool piiDeleted = await Auth().deleteUserPiiData(); - if(piiDeleted) { - await UserProfile().deleteProfile(); - } - Auth().logout(); - } - - void _loadHealthUser() { - setState(() { - _isLoading = true; - }); - Health().refreshUser().whenComplete((){ - setState(() { - _isLoading = false; - }); - }); - } - - // NotificationsListener - - @override - void onNotification(String name, dynamic param) { - if (name == Auth.notifyUserPiiDataChanged) { - _updateState(); - } else if (name == UserProfile.notifyProfileUpdated){ - _updateState(); - } else if (name == Health.notifyUserUpdated){ - _updateState(); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: SimpleHeaderBarWithBack( - context: context, - titleWidget: _DebugContainer( - child: Text( - Localization().getStringEx("panel.settings.home.settings.header", "Settings"), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: Styles().fontFamilies.extraBold, - letterSpacing: 1.0, - ), - textAlign: TextAlign.center, - )), - ), - body: Stack( - children: [ - SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("About You", - style: TextStyle( - fontFamily: Styles().fontFamilies.extraBold, - color: Styles().colors.fillColorPrimary, - fontSize: 20, - ), - ), - Container(height: 12,), - _buildConnected(), - Container(height: 12,), - CustomRibbonButton( - height: null, - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Add A Government ID', - descriptionLabel: 'Verify your identity by adding a government-issued ID', - leftIcon: 'images/icon-passport.png', - onTap: _onAddGovernmentId, - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'COVID-19 Event History', - descriptionLabel: 'View or delete test results, symptom updates, or contact tracing information', - leftIcon: 'images/icon-identity.png', - onTap: _onEventHistoryTapped, - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Transfer Your COVID-19 Encryption Key', - descriptionLabel: 'View, scan, or save your COVID-19 Encryption Key to transfer to another device.', - leftIcon: 'images/icon-key.png', - onTap: _onTransferKeyTapped, - ), - Container(height: 40,), - Text("Special Consent", - style: TextStyle( - fontFamily: Styles().fontFamilies.extraBold, - color: Styles().colors.fillColorPrimary, - fontSize: 20, - ), - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Exposure Notifications', - value: (Health().user?.consentExposureNotification ?? false) ? 'Enabled' : 'Disabled', - descriptionLabel: 'Learn more information about exposure notifications and manage your settings.', - onTap: _onConsentExposureNotificationsTapped, - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - value: (Health().user?.consentTestResults ?? false) ? 'Enabled' : 'Disabled', - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Automatic Test Results', - descriptionLabel: 'Learn more information about automatic test results and manage your settings.', - onTap: _onConsentTestResultsTapped, - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - value: (Health().user?.consentVaccineInformation ?? false) ? 'Enabled' : 'Disabled', - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Automatic Vaccine Information', - descriptionLabel: 'Learn more information about automatic vaccine information and manage your settings.', - onTap: _onConsentVaccineInformationTapped, - ), - Container(height: 40,), - Text("System Settings", - style: TextStyle( - fontFamily: Styles().fontFamilies.extraBold, - color: Styles().colors.fillColorPrimary, - fontSize: 20, - ), - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - value: 'Disabled', - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Access device’s location', - descriptionLabel: 'To get the most out of our features, enable location in your device’s settings.', - leftIcon: 'images/icon-location-1.png', - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - value: 'Disabled', - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Access device\'s bluetooth', - descriptionLabel: 'To use Bluetooth enable in your device\'s settings.', - leftIcon: 'images/icon-bluetooth.png', - ), - Container(height: 12,), - CustomRibbonButton( - height: null, - value: 'Disabled', - borderRadius: BorderRadius.all(Radius.circular(4)), - label: 'Notifications', - descriptionLabel: 'To receive notifications enable in your device\'s settings.', - leftIcon: 'images/icon-notifications-blue.png', - ), - Container(height: 1, color: Styles().colors.surfaceAccent, margin: EdgeInsets.symmetric(vertical: 20),), - RoundedButton( - label: 'Delete my COVID-19 Information', - hint: '', - backgroundColor: Styles().colors.surface, - fontSize: 16.0, - textColor: Styles().colors.fillColorSecondary, - borderColor: Styles().colors.surfaceAccent, - onTap: _onRemoveMyInfoClicked, - - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 10), - child: Text( - 'Delete your government issued ID information, COVID-19 event history, and encryption key.', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - color: Styles().colors.textBackground, - fontSize: 12 - ), - ), - ) - ], - ), - ), - ), - _isLoading ? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 2,),) : Container() - ], - ) - ); - } - - Widget _buildConnected() { - return Column( - children: [ - _buildConnectedNetIdLayout(), - _buildConnectedPhoneLayout() - ], - ); - } - - Widget _buildConnectedNetIdLayout() { - List contentList = []; - - if(Auth().isShibbolethLoggedIn){ - contentList.add(Container( - width: double.infinity, - decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(4)), border: Border.all(color: Styles().colors.surfaceAccent, width: 0.5)), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Connected as ", - style: TextStyle(color: Styles().colors.textBackground, fontFamily: Styles().fontFamilies.regular, fontSize: 16)), - Text(Auth().userPiiData?.fullName ?? "", - style: TextStyle(color: Styles().colors.fillColorPrimary, fontSize: 20)), - ])))); - contentList.add( - Semantics( explicitChildNodes: true, - child:RibbonButton( - borderRadius: BorderRadius.only(bottomLeft: Radius.circular(4), bottomRight: Radius.circular(4)), - border: Border.all(color: Styles().colors.surfaceAccent, width: 0), - label: "Disconnect your NetID", - onTap: _onDisconnectNetIdClicked))); - } - else if(!Auth().isLoggedIn){ - contentList.add( - Semantics( explicitChildNodes: true, - child: RibbonButton( - borderRadius: BorderRadius.all(Radius.circular(4)), - border: Border.all(color: Styles().colors.surfaceAccent, width: 0), - label: "Connect your NetID", - onTap: _onConnectNetIdClicked))); - } - - return Semantics( container:true, - child: Container(child: Column(children: contentList,))); - } - - Widget _buildConnectedPhoneLayout() { - List contentList = []; - - if(Auth().isPhoneLoggedIn){ - String full = Auth()?.userPiiData?.fullName ?? ""; - bool hasFull = AppString.isStringNotEmpty(full); - - contentList.add(Container( - width: double.infinity, - decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(4)), border: Border.all(color: Styles().colors.surfaceAccent, width: 0.5)), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Verified as ", - style: TextStyle(color: Styles().colors.textBackground, fontFamily: Styles().fontFamilies.regular, fontSize: 16)), - Visibility(visible: hasFull, child: Text(full ?? "", style: TextStyle(color: Styles().colors.fillColorPrimary, fontSize: 20)),), - Text(Auth().phoneToken?.phone ?? "", style: TextStyle(color: Styles().colors.fillColorPrimary, fontSize: 20)), - ])))); - contentList.add( - Semantics( explicitChildNodes: true, - child:RibbonButton( - borderRadius: BorderRadius.only(bottomLeft: Radius.circular(4), bottomRight: Radius.circular(4)), - border: Border.all(color: Styles().colors.surfaceAccent, width: 0), - label: "Disconnect your Phone", - onTap: _onDisconnectNetIdClicked))); - } - else if(!Auth().isLoggedIn){ - contentList.add( - Semantics( explicitChildNodes: true, - child:RibbonButton( - borderRadius:BorderRadius.all(Radius.circular(4)), - border: Border.all(color: Styles().colors.surfaceAccent, width: 0), - label: "Verify Your Phone Number", - onTap: _onPhoneVerClicked))); - } - return Column(children: contentList,); - } - - Widget _buildRemoveMyInfoDialog(BuildContext context) { - return StatefulBuilder( - builder: (context, setState){ - return ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(8)), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: Styles().colors.fillColorPrimary, - borderRadius: BorderRadius.vertical(top: Radius.circular(8)), - ), - child: Padding( - padding: EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: Center( - child: Text( - "Delete your COVID-19 event history?", - style: TextStyle(fontSize: 20, color: Colors.white), - ), - ), - ), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(15)), - border: Border.all(color: Styles().colors.white, width: 2), - ), - child: Center( - child: Text( - '\u00D7', - style: TextStyle( - fontSize: 24, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - Container( - height: 26, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: Text( - "This will permanently delete all of your COVID-19 event history information. Are you sure you want to continue?", - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Colors.black), - ), - ), - Container( - height: 26, - ), - Text( - "Are you sure?", - textAlign: TextAlign.center, - style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Colors.black), - ), - Container( - height: 16, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: RoundedButton( - onTap: () { - Analytics.instance.logAlert(text: "Remove My Information", selection: "No"); - Navigator.pop(context); - }, - backgroundColor: Colors.transparent, - borderColor: Styles().colors.fillColorPrimary, - textColor: Styles().colors.fillColorPrimary, - label: 'No'), - ), - Container( - width: 10, - ), - Expanded( - child: Stack( - children: [ - RoundedButton( - onTap: () => _onConfirmRemoveMyInfo(context, setState), - backgroundColor: Styles().colors.fillColorSecondaryVariant, - borderColor: Styles().colors.fillColorSecondaryVariant, - textColor: Styles().colors.surface, - label: Localization().getStringEx("panel.profile_info.dialog.remove_my_information.yes.title", "Yes")), - _isDeleting ? Align(alignment: Alignment.center, child: CircularProgressIndicator()) : Container() - ], - ), - ), - ], - ), - ), - ], - ), - ), - ); - }, - ); - } - - Widget _buildLogoutDialog(BuildContext context) { - return Dialog( - child: Padding( - padding: EdgeInsets.all(18), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Safer Illinois", - style: TextStyle(fontSize: 24, color: Colors.black), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 26), - child: Text( - "Are you sure you want to sign out?", - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Colors.black), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Analytics.instance.logAlert(text: "Sign out", selection: "Yes"); - Navigator.pop(context); - Auth().logout(); - }, - child: Text("Yes")), - TextButton( - onPressed: () { - Analytics.instance.logAlert(text: "Sign out", selection: "No"); - Navigator.pop(context); - }, - child: Text("No")) - ], - ), - ], - ), - ), - ); - } - - void _onConnectNetIdClicked() { - Analytics.instance.logSelect(target: "Connect netId"); - Auth().authenticateWithShibboleth(); - } - - void _onDisconnectNetIdClicked() { - if(Auth().isShibbolethLoggedIn) { - Analytics.instance.logSelect(target: "Disconnect netId"); - } else { - Analytics.instance.logSelect(target: "Disconnect phone"); - } - showDialog(context: context, builder: (context) => _buildLogoutDialog(context)); - } - - void _onPhoneVerClicked() { - Analytics.instance.logSelect(target: "Phone Verification"); - if (Connectivity().isNotOffline) { - Navigator.push(context, CupertinoPageRoute(settings: RouteSettings(), builder: (context) => OnboardingLoginPhoneVerifyPanel(onFinish: _didPhoneVer,))); - } else { - AppAlert.showOfflineMessage(context, 'Verify Your Phone Number is not available while offline.'); - } - } - - void _onAddGovernmentId(){ - if(Auth()?.userPiiData?.hasPasportInfo ?? false){ - Navigator.push(context, CupertinoPageRoute( - builder: (context) => Settings2GovernmentIdPanel() - )); - } - else { - Navigator.push(context, CupertinoPageRoute( - builder: (context) => OnboardingResidentInfoPanel( - onSucceed: (Map data){ - Navigator.pushReplacement(context, CupertinoPageRoute(builder: (context) => Settings2GovernmentIdPanel(initialData: data,))); - }, - onCancel: ()=>Navigator.pop(context), - ) - )); - } - } - - void _onRemoveMyInfoClicked() { - showDialog(context: context, builder: (context) => _buildRemoveMyInfoDialog(context)); - } - - void _didPhoneVer(_) { - Navigator.of(context)?.popUntil((Route route){ - return AppNavigation.routeRootWidget(route, context: context)?.runtimeType == widget.runtimeType; - }); - } - - void _onConfirmRemoveMyInfo(BuildContext context, Function setState){ - setState(() { - _isDeleting = true; - }); - _deleteUserData() - .then((_){ - Navigator.pop(context); - }) - .whenComplete((){ - setState(() { - _isDeleting = false; - }); - }) - .catchError((error){ - AppAlert.showDialogResult(context, error.toString()).then((_){ - Navigator.pop(context); - }); - }); - - - } - - void _onEventHistoryTapped(){ - Analytics.instance.logSelect(target: "COVID-19 Test History"); - Navigator.push(context, CupertinoPageRoute(builder: (context) => HealthHistoryPanel())); - } - - void _onTransferKeyTapped() { - Analytics.instance.logSelect(target: "Transfer Your COVID-19 Encryption Key"); - Navigator.push(context, CupertinoPageRoute(builder: (context) => Settings2TransferEncryptionKeyPanel())); - } - - void _onConsentExposureNotificationsTapped(){ - Analytics.instance.logSelect(target: "Consent Exposure Notifications"); - Navigator.push(context, CupertinoPageRoute(builder: (context) => Settings2ExposureNotificationsPanel())); - } - - void _onConsentTestResultsTapped(){ - Analytics.instance.logSelect(target: "Consent Test Results"); - Navigator.push(context, CupertinoPageRoute(builder: (context) => Settings2ConsentPanel())); - } - - void _onConsentVaccineInformationTapped(){ - Analytics.instance.logSelect(target: "Consent Vaccine Information"); - //TBD: Navigator.push(context, CupertinoPageRoute(builder: (context) => Settings2ConsentPanel())); - } - -} - -class CustomRibbonButton extends StatelessWidget { - final String label; - final String value; - final String descriptionLabel; - - final GestureTapCallback onTap; - final EdgeInsets padding; - final BorderRadius borderRadius; - final BoxBorder border; - final TextStyle style; - final double height; - final String leftIcon; - final String icon; - final BuildContext context; - final String hint; - - CustomRibbonButton({ - @required this.label, - this.value, - this.descriptionLabel, - this.onTap, - this.borderRadius = BorderRadius.zero, - this.border, - this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - this.style, - this.height = 48.0, - this.icon = 'images/chevron-right.png', - this.leftIcon, - this.context, - this.hint, - }); - - @override - Widget build(BuildContext context) { - return getSemantics(); - } - - Semantics getSemantics() { - return Semantics(label: label, hint : hint, button: true, excludeSemantics: true, child: _content()); - } - - Widget _content() { - bool hasDescription = AppString.isStringNotEmpty(descriptionLabel); - bool hasValue = AppString.isStringNotEmpty(value); - Widget image = getImage(); - Widget leftIconWidget = AppString.isStringNotEmpty(leftIcon) ? Padding(padding: EdgeInsets.only(right: 7), child: Image.asset(leftIcon)) : Container(); - Widget leftIconHiddenWidget = Opacity(opacity: 0, child: AppString.isStringNotEmpty(leftIcon) ? Padding(padding: EdgeInsets.only(right: 7), child: Image.asset(leftIcon)) : Container(),); - return GestureDetector( - onTap: () { onTap(); anaunceChange(); }, - child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Container( - decoration: BoxDecoration(color: Colors.white, border:border, borderRadius: borderRadius, boxShadow: [ - BoxShadow( - color: Styles().colors.lightGray, - spreadRadius: 3, - blurRadius: 3, - offset: Offset(2, 2), // changes position of shadow - ), - ]), - height: this.height, - child: Padding( - padding: padding, - child: Column( - children: [ - Row( - children: [ - leftIconWidget, - Expanded(child: - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, - style: style ?? TextStyle(color: Styles().colors.fillColorPrimary, fontSize: 16, fontFamily: Styles().fontFamilies.bold), - ), - ], - ) - ), - (image != null) ? Padding(padding: EdgeInsets.only(left: 7), child: image) : Container(), - ], - ), - hasValue ? Row( - children: [ - leftIconHiddenWidget, - Expanded(child: hasValue ? Container( - child: Text(value, - style: style ?? TextStyle(color: Styles().colors.textSurface, fontSize: 14, fontFamily: Styles().fontFamilies.regular), - ), - ) : Container() - ,) - ], - ) : Container(), - Row( - children: [ - leftIconHiddenWidget, - Expanded(child: hasDescription ? Container( - margin: EdgeInsets.only(top: 4), - child: Text(descriptionLabel, - style: style ?? TextStyle(color: Styles().colors.textSurface, fontSize: 14, fontFamily: Styles().fontFamilies.regular), - ), - ) : Container() - ,) - ], - ), - ], - ), - ), - ) - ),],), - ); - } - - Widget getImage() { - return (icon != null) ? Image.asset(icon) : null; - } - - void anaunceChange() {} -} - -class _DebugContainer extends StatefulWidget { - - final Widget _child; - - _DebugContainer({@required Widget child}) : _child = child; - - _DebugContainerState createState() => _DebugContainerState(); -} - -class _DebugContainerState extends State<_DebugContainer> { - - int _clickedCount = 0; - - @override - Widget build(BuildContext context) { - return GestureDetector( - child: widget._child, - onTap: () { - Log.d("On tap debug widget"); - _clickedCount++; - - if (_clickedCount == 7) { - _showPinDialog(); - _clickedCount = 0; - } - }, - ); - } - - void _showPinDialog(){ - TextEditingController pinController = TextEditingController(text: (!kReleaseMode || Organizations().isDevEnvironment) ? this.pinOfTheDay : ''); - showDialog(context: context, barrierDismissible: false, builder: (context) => Dialog( - child: Padding( - padding: EdgeInsets.all(18), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - Localization().getStringEx('app.title', 'Safer Illinois'), - style: TextStyle(fontSize: 24, color: Colors.black), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 26), - child: Text( - Localization().getStringEx('panel.debug.label.pin', 'Please enter pin'), - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Colors.black), - ), - ), - Container(height: 6,), - TextField(controller: pinController, autofocus: true, keyboardType: TextInputType.number, obscureText: true, - onSubmitted:(String value){ - _onEnterPin(value); - } - ,), - Container(height: 6,), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.pop(context); - //_finish(); - }, - child: Text(Localization().getStringEx('dialog.cancel.title', 'Cancel'))), - Container(width: 6), - TextButton( - onPressed: () { - _onEnterPin(pinController?.text); - //_finish(); - }, - child: Text(Localization().getStringEx('dialog.ok.title', 'OK'))) - ], - ) - ], - ), - ), - )); - } - - String get pinOfTheDay { - return DateFormat('MMdd').format(DateTime.now()); - } - - void _onEnterPin(String pin){ - if (this.pinOfTheDay == pin) { - Navigator.pop(context); - Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugHomePanel())); - } else { - AppToast.show("Invalid pin"); - } - } -} \ No newline at end of file diff --git a/lib/ui/settings2/Settings2TransferEncryptionKeyPanel.dart b/lib/ui/settings2/Settings2TransferEncryptionKeyPanel.dart deleted file mode 100644 index 9c820bf6..00000000 --- a/lib/ui/settings2/Settings2TransferEncryptionKeyPanel.dart +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:typed_data'; - -import 'package:barcode_scan/barcode_scan.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/Health.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/service/NativeCommunicator.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/ui/widgets/HeaderBar.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/utils/Covid19.dart'; -import 'package:illinois/utils/Crypt.dart'; -import 'package:illinois/utils/Utils.dart'; -import 'package:pointycastle/export.dart' as PointyCastle; - -class Settings2TransferEncryptionKeyPanel extends StatefulWidget { - - const Settings2TransferEncryptionKeyPanel({Key key}) : super(key: key); - @override - _Settings2TransferEncryptionKeyPanelState createState() => _Settings2TransferEncryptionKeyPanelState(); -} - -class _Settings2TransferEncryptionKeyPanelState extends State { - - PointyCastle.PublicKey _userPublicKey; - PointyCastle.PrivateKey _userPrivateKey; - bool _prepairing, _userKeysPaired; - Uint8List _qrCodeBytes; - bool _saving = false; - - @override - void initState() { - super.initState(); - - _prepairing = true; - _userPublicKey = Health().user?.publicKey; - _userPrivateKey = Health().userPrivateKey; - _verifyHealthRSAKeys(); - } - - void _verifyHealthRSAKeys() { - - if ((_userPrivateKey != null) && (_userPublicKey != null)) { - RsaKeyHelper.verifyRsaKeyPair(PointyCastle.AsymmetricKeyPair(_userPublicKey, _userPrivateKey)).then((bool result) { - if (mounted) { - _userKeysPaired = result; - _buildHealthRSAQRCode(); - } - }); - } - else { - _finishPrepare(); - } - } - - void _buildHealthRSAQRCode() { - if (_userKeysPaired && (_userPrivateKey != null)) { - RsaKeyHelper.compressRsaPrivateKey(_userPrivateKey).then((String privateKeyString) { - if (mounted) { - if (privateKeyString != null) { - NativeCommunicator().getBarcodeImageData({ - 'content': privateKeyString, - 'format': 'qrCode', - 'width': 1024, - 'height': 1024, - }).then((Uint8List qrCodeBytes) { - if (mounted) { - _qrCodeBytes = qrCodeBytes; - _finishPrepare(); - } - }); - } - else { - _finishPrepare(); - } - } - }); - } - else { - _finishPrepare(); - } - } - - void _finishPrepare() { - setState(() { - _prepairing = false; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: SimpleHeaderBarWithBack( - context: context, - titleWidget: Text( - Localization().getStringEx('panel.covid19.transfer.title', 'Transfer Encryption Key'), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w900, - letterSpacing: 1.0, - ), - textAlign: TextAlign.center, - ), - ), - body: Column(children: [ - Expanded(child: - Padding(padding: EdgeInsets.all(24), child: - (_prepairing == true) ? _buildWaitingContent() : _buildPrivateKeyContent() - ), - ), - ],), - backgroundColor: Styles().colors.background, - ); - } - - Widget _buildWaitingContent() { - return Center(child: - CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 2,) - ); - } - - Widget _buildPrivateKeyContent(){ - return SingleChildScrollView( - child: (_qrCodeBytes != null) ? _buildQrCodeContent() : _buildNoQrCodeContent(), - ); - } - - Widget _buildQrCodeContent(){ - return Column( children: [ - Container(height: 15,), - Semantics( header: true, hint: Localization().getStringEx("app.common.heading.one.hint","Header 1"), - child:Text(Localization().getStringEx("panel.covid19.transfer.primary.heading.title", "Your COVID-19 Encryption Key"), - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 28, color:Styles().colors.fillColorPrimary), - )), - Container(height: 30,), - _buildQrCode(), - Container(height: 20,), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: RoundedButton( - label: Localization().getStringEx("panel.covid19.transfer.primary.button.save.title", "Save Your Encryption Key"), - hint: Localization().getStringEx("panel.covid19.transfer.primary.button.save.hint", ""), - borderColor: Styles().colors.fillColorSecondaryVariant, - backgroundColor: Styles().colors.surface, - fontSize: 16, - height: 40, - padding: EdgeInsets.symmetric(vertical: 5), - textColor: Styles().colors.fillColorPrimary, - onTap: _onSaveImage, - ), - ), - Container(height: 30,) - ], - ); - } - - Widget _buildNoQrCodeContent(){ - return Column( children: [ - Container(height: 15,), - Semantics( header: true, hint: Localization().getStringEx("app.common.heading.one.hint","Header 1"), - child:Text(Localization().getStringEx("panel.covid19.transfer.secondary.heading.title", "Missing COVID-19 Encryption Key"), - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 28, color:Styles().colors.fillColorPrimary), - )), - Container(height: 30,), - _buildAction( - heading: Localization().getStringEx("panel.covid19.transfer.secondary.button.scan.heading", "If you are adding a second device:"), - description: Localization().getStringEx("panel.covid19.transfer.secondary.button.scan.description", "If you still have access to your primary device, you can directly scan the COVID-19 Encryption Key QR code from that device."), - title: Localization().getStringEx("panel.covid19.transfer.secondary.button.scan.title", "Scan Your QR Code"), - iconRes: "images/fill-1.png", - onTap: _onScan - ), - Container(height: 12,), - _buildAction( - heading: Localization().getStringEx("panel.covid19.transfer.secondary.button.retrieve.heading", "If you are using a replacement device:"), - description: Localization().getStringEx("panel.covid19.transfer.secondary.button.retrieve.description", "If you no longer have access to your primary device, but saved your QR code to a cloud photo service, you can transfer your COVID-19 Encryption Key by retrieving it from your photos."), - title: Localization().getStringEx("panel.covid19.transfer.secondary.button.retrieve.title", "Retrieve Your QR Code"), - iconRes: "images/group-10.png", - onTap: _onRetrieve - ), - Container(height: 12,), - Container(height: 40,) - ], -); - } - - Widget _buildQrCode(){ - return Container( - decoration: BoxDecoration( - color: Styles().colors.white, - borderRadius: BorderRadius.all( Radius.circular(5))), - padding: EdgeInsets.all(1), - child: Semantics(child: - Image.memory(_qrCodeBytes, fit: BoxFit.fitWidth, semanticLabel: Localization().getStringEx("panel.covid19.transfer.primary.heading.title", "Your COVID-19 Encryption Key"), - )), - ); - } - - Widget _buildAction({String heading, String description, String title, String iconRes, Function onTap}){ - return Semantics(container: true, child:Container( - decoration: BoxDecoration( - color: Styles().colors.white, - borderRadius: BorderRadius.all( Radius.circular(5))), - child: Column( - children: [ - Container(height: 18,), - Container( padding: EdgeInsets.symmetric(horizontal: 20), - child: Text(heading, style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color:Styles().colors.fillColorPrimary))), - Container(height: 9,), - Container( padding: EdgeInsets.symmetric(horizontal: 20), - child: Text(description, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 14, color:Styles().colors.fillColorPrimary))), - Container(height: 14,), - Semantics( - explicitChildNodes: true, - child: Container(child: - GestureDetector( - onTap: onTap, - child:Container( - decoration: BoxDecoration( - color: Styles().colors.background, - borderRadius: BorderRadius.only( bottomLeft: Radius.circular(5), bottomRight: Radius.circular(5)), - border: Border.all(color: Styles().colors.surfaceAccent,) - ), - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15), - child: Row( - children: [ - Image.asset(iconRes, excludeFromSemantics: true,), - Container(width: 7,), - Semantics(button: true, excludeSemantics:false, child: - Text(title, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 14, color:Styles().colors.fillColorPrimary))), - Expanded(child: Container(),), - Image.asset('images/chevron-right.png',excludeFromSemantics: true,), - ], - ))))), - ], - ) - )); - } - - void _onSaveImage(){ - Analytics.instance.logSelect(target: "Save Your Encryption Key"); - if(!_saving) { - setState(() { - _saving = true; - }); - Covid19Utils.saveQRCodeImageToPictures(qrCodeBytes: _qrCodeBytes, title: Localization().getStringEx("panel.covid19.transfer.label.qr_image_label", "Safer Illinois COVID-19 Code")).then((bool result) { - setState(() { - _saving = false; - }); - String platformTargetText = (defaultTargetPlatform == TargetPlatform.android) ? Localization().getStringEx("panel.covid19.transfer.alert.save.success.pictures", "Pictures") : Localization().getStringEx("panel.covid19.transfer.alert.save.success.gallery", "Gallery"); - String message = result - ? (Localization().getStringEx("panel.covid19.transfer.alert.save.success.msg", "Successfully saved qr code in ") + platformTargetText) - : Localization().getStringEx("panel.covid19.transfer.alert.save.fail.msg", "Failed to save qr code in ") + platformTargetText; - AppAlert.showDialogResult(context, message); - }); - } - } - - void _onScan(){ - Analytics.instance.logSelect(target: "Scan Your QR Code"); - BarcodeScanner.scan().then((result) { - // barcode_scan plugin returns 8 digits when it cannot read the qr code. Prevent it from storing such values - if (AppString.isStringEmpty(result?.rawContent) || ((result?.rawContent?.length ?? 0) <= 8)) { - AppAlert.showDialogResult(context, Localization().getStringEx('panel.covid19.transfer.alert.qr_code.scan.failed.msg', 'Failed to read QR code.')); - } - else { - _onCovid19QrCodeScanSucceeded(result?.rawContent); - } - }); - } - - void _onRetrieve() { - Analytics.instance.logSelect(target: "Retrieve Your QR Code"); - Covid19Utils.loadQRCodeImageFromPictures().then((String qrCodeString) { - _onCovid19QrCodeScanSucceeded(qrCodeString); - }); - } - - void _onCovid19QrCodeScanSucceeded(String result) { - - RsaKeyHelper.decompressRsaPrivateKey(result).then((PointyCastle.PrivateKey privateKey) { - if (mounted) { - if (privateKey != null) { - RsaKeyHelper.verifyRsaKeyPair(PointyCastle.AsymmetricKeyPair(_userPublicKey, privateKey)).then((bool result) { - if (mounted) { - if (result == true) { - Health().setUserPrivateKey(privateKey).then((success) { - if (mounted) { - String resultMessage = success ? - Localization().getStringEx("panel.covid19.transfer.alert.qr_code.transfer.succeeded.msg", "COVID-19 secret transferred successfully.") : - Localization().getStringEx("panel.covid19.transfer.alert.qr_code.transfer.failed.msg", "Failed to transfer COVID-19 secret."); - AppAlert.showDialogResult(context, resultMessage); - } - }); - } - else { - AppAlert.showDialogResult(context, Localization().getStringEx('panel.covid19.transfer.alert.qr_code.not_match.msg', 'COVID-19 secret key does not match existing public RSA key.')); - } - } - }); - } - else { - AppAlert.showDialogResult(context, Localization().getStringEx('panel.covid19.transfer.alert.qr_code.invalid.msg', 'Invalid QR code.')); - } - } - }); - } - -} diff --git a/lib/utils/Utils.dart b/lib/utils/Utils.dart index 5f1ab617..3478edd4 100644 --- a/lib/utils/Utils.dart +++ b/lib/utils/Utils.dart @@ -328,6 +328,18 @@ class AppJson { catch(e) { print(e?.toString()); } return null; } + + static List listStringValue(dynamic value) { + try { return (value is List) ? value.cast() : null; } + catch(e) { print(e?.toString()); } + return null; + } + + static List listIntValue(dynamic value) { + try { return (value is List) ? value.cast() : null; } + catch(e) { print(e?.toString()); } + return null; + } } class AppFile { diff --git a/pubspec.yaml b/pubspec.yaml index 1f04f17a..b00799c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Illinois client application. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.10.31+1031 +version: 2.11.3+1103 environment: sdk: ">=2.2.0 <3.0.0"