diff --git a/.gitignore b/.gitignore index 9be145fde..2161f403b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ .dart_tool/ .packages build/ +temp.enc \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a1134d06b..e0fea7fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Upgrade to Flutter 3.19.0 [#435](https://github.com/rokwire/app-flutter-plugin/issues/435). -- Init application services in parallel [#408](https://github.com/rokwire/app-flutter-plugin/issues/408). +- Integrate auth refactor [#379](https://github.com/rokwire/app-flutter-plugin/issues/379) +- Use CCT for Android OIDC login [#404](https://github.com/rokwire/app-flutter-plugin/issues/404) +- Improve services init on app startup [#408](https://github.com/rokwire/app-flutter-plugin/issues/408) +- Improve services failure processing [#408](https://github.com/rokwire/app-flutter-plugin/issues/408) + +### Added +- Web app authentication support [#291](https://github.com/rokwire/app-flutter-plugin/issues/291) +- Web app passkey support [#299](https://github.com/rokwire/app-flutter-plugin/issues/299) +- Anonymous account association [#305](https://github.com/rokwire/app-flutter-plugin/issues/305) +- Add timestamps to anonymous IDs [#309](https://github.com/rokwire/app-flutter-plugin/issues/309) +- Web app authentication support [#291](https://github.com/rokwire/app-flutter-plugin/issues/291) +- Handle more identifiers using passkeys and linking [#330](https://github.com/rokwire/app-flutter-plugin/issues/330) +- Style code generation tools + +### Fixed +- Improve exception handling for passkeys [#396](https://github.com/rokwire/app-flutter-plugin/issues/396) ## [1.6.3] - 2024-02-19 ### Added diff --git a/README.md b/README.md index 99e6ba6b7..9daf6437a 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,38 @@ git submodule add https://github.com/rokwire/services-flutter-pligin.git ``` + +## Tools +This plugin provides several tools that make it easier to use and manage the functionality it provides + +### Generating Static Style Types +The `tools/gen_styles.dart` tool can be used to generate classes from the `styles.json` asset which +expose static references that can be easily accessed programmatically. To use this tool, navigate +to the root directory of your application and ensure that this plugin is cloned as a submodule in +the `plugin` directory. Ensure that `assets/styles.json` is valid and up to date with your desired +changes. To use the tool, run the following command: + +``` +dart plugin/tools/gen_styles.dart +``` + +If successful, you will see a new or updates `gen/styles.dart` file available with the generated classes. +You can then import this file and use the included references throughout the application. + +This tool will also attempt to merge any new additions from `plugin/assets/styles.json` into `assets/styles.json` +Any existing entries in `assets/styles.json` will not be overridden. To run this tool without attempting +to merge plugin asset changes, or to run this tool within the plugin itself, provide the `-p` flag: + +``` +dart plugin/tools/gen_styles.dart -p +``` + +Note that this tool also includes a utility which can find and replace all existing references +in the codebase with the new static class members. To use this util after generating the classes, +provide the `-u` flag: + +``` +dart plugin/tools/gen_styles.dart -u +``` + + diff --git a/android/build.gradle b/android/build.gradle index 03cc91c9b..cad3df0c1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,13 +2,18 @@ group 'edu.illinois.rokwire.rokwire_plugin' version '1.0' buildscript { + ext { + gradle_version = '7.0.0' + kotlin_version = '1.8.10' + } repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' + classpath "com.android.tools.build:gradle:$gradle_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -20,6 +25,7 @@ rootProject.allprojects { } apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { compileSdkVersion 33 @@ -30,7 +36,7 @@ android { } defaultConfig { - minSdkVersion 16 + minSdkVersion 26 } } @@ -45,5 +51,9 @@ dependencies { //AltBeacon - Android Beacon Library implementation 'org.altbeacon:android-beacon-library:2.19.5-beta7' + //Passkeys + implementation "androidx.credentials:credentials:1.0.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.0.0-alpha03" + //End Common Dependencies } diff --git a/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/GeofenceMonitor.java b/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/GeofenceMonitor.java index ed06fc230..4b7323cb2 100644 --- a/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/GeofenceMonitor.java +++ b/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/GeofenceMonitor.java @@ -51,6 +51,7 @@ import java.util.UUID; import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -88,8 +89,8 @@ public static GeofenceMonitor getInstance() { public void init() { Context activityContext = RokwirePlugin.getInstance().getActivity(); if ((activityContext != null) && - (ContextCompat.checkSelfPermission(activityContext, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) && - (ContextCompat.checkSelfPermission(activityContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)) { + (ContextCompat.checkSelfPermission(activityContext, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) && + (ContextCompat.checkSelfPermission(activityContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)) { Log.d(TAG, "Location Permissions Granted."); initGeofenceClient(); initBeaconManager(); @@ -352,6 +353,10 @@ private void startMonitorGeofenceRegions(List geofenceList) { builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); builder.addGeofences(geofenceList); GeofencingRequest geofencingRequest = builder.build(); + Context context = RokwirePlugin.getInstance().getActivity(); + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + return; + } geofencingClient.addGeofences(geofencingRequest, getGeofencePendingIntent()). addOnSuccessListener(addGeofencesSuccessListener). addOnFailureListener(addGeofencesFailureListener); @@ -381,15 +386,15 @@ private PendingIntent getGeofencePendingIntent() { private void notifyCurrentGeofencesUpdated() { //TBD - RokwirePlugin.getInstance().notifyGeoFence​("onCurrentRegionsChanged", getCurrentIds()); + RokwirePlugin.getInstance().notifyGeoFence("onCurrentRegionsChanged", getCurrentIds()); } private void notifyRegionEnter(String regionId) { - RokwirePlugin.getInstance().notifyGeoFence​("onEnterRegion", regionId); + RokwirePlugin.getInstance().notifyGeoFence("onEnterRegion", regionId); } private void notifyRegionExit(String regionId) { - RokwirePlugin.getInstance().notifyGeoFence​("onExitRegion", regionId); + RokwirePlugin.getInstance().notifyGeoFence("onExitRegion", regionId); } //region Add Geofences Listeners @@ -539,7 +544,7 @@ private void notifyBeacons(List beaconsList, String regionId) { HashMap parameters = new HashMap<>(); parameters.put("regionId", regionId); parameters.put("beacons", beaconsList); - RokwirePlugin.getInstance().notifyGeoFence​("onBeaconsInRegionChanged", parameters); + RokwirePlugin.getInstance().notifyGeoFence("onBeaconsInRegionChanged", parameters); } @Override diff --git a/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/RokwirePlugin.java b/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/RokwirePlugin.java index 43267703c..35518d01a 100644 --- a/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/RokwirePlugin.java +++ b/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/RokwirePlugin.java @@ -14,6 +14,7 @@ import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; +import androidx.credentials.exceptions.GetCredentialException; import java.lang.ref.WeakReference; @@ -25,6 +26,7 @@ import android.util.Log; import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; @@ -93,7 +95,7 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { // ActivityAware @Override - public void onAttachedToActivity​(ActivityPluginBinding binding) { + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { _applyActivityBinding(binding); } @@ -103,7 +105,7 @@ public void onDetachedFromActivity() { } @Override - public void onReattachedToActivityForConfigChanges​(ActivityPluginBinding binding) { + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { _applyActivityBinding(binding); } @@ -130,45 +132,48 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { nextMethodComponents = call.method.substring(pos + 1); } - if (firstMethodComponent.equals("getPlatformVersion")) { - result.success("Android " + android.os.Build.VERSION.RELEASE); - } - else if (firstMethodComponent.equals("createAndroidNotificationChannel")) { - result.success(createNotificationChannel(call)); - } - else if (firstMethodComponent.equals("showNotification")) { - result.success(showNotification(call)); - } - else if (firstMethodComponent.equals("getDeviceId")) { - result.success(getDeviceId(call.arguments)); - } - else if (firstMethodComponent.equals("getEncryptionKey")) { - result.success(getEncryptionKey(call.arguments)); - } - else if (firstMethodComponent.equals("dismissSafariVC")) { - result.success(null); // Safari VV not available in Android - } - else if (firstMethodComponent.equals("launchApp")) { - result.success(launchApp(call.arguments)); - } - else if (firstMethodComponent.equals("launchAppSettings")) { - result.success(launchAppSettings(call.arguments)); - } - else if (firstMethodComponent.equals("locationServices")) { - LocationServices.getInstance().handleMethodCall(nextMethodComponents, call.arguments, result); - } - else if (firstMethodComponent.equals("trackingServices")) { - result.success("allowed"); // tracking is allowed in Android by default - } - else if (firstMethodComponent.equals("geoFence")) { - GeofenceMonitor.getInstance().handleMethodCall(nextMethodComponents, call.arguments, result); - } - else { - result.notImplemented(); + switch (firstMethodComponent) { + case "getPlatformVersion": + result.success("Android " + Build.VERSION.RELEASE); + break; + case "createAndroidNotificationChannel": + result.success(createNotificationChannel(call)); + break; + case "showNotification": + result.success(showNotification(call)); + break; + case "getDeviceId": + result.success(getDeviceId(call.arguments)); + break; + case "getEncryptionKey": + result.success(getEncryptionKey(call.arguments)); + break; + case "dismissSafariVC": + result.success(null); // Safari VV not available in Android + break; + case "launchApp": + result.success(launchApp(call.arguments)); + break; + case "launchAppSettings": + result.success(launchAppSettings(call.arguments)); + break; + case "locationServices": + assert nextMethodComponents != null; + LocationServices.getInstance().handleMethodCall(nextMethodComponents, call.arguments, result); + break; + case "trackingServices": + result.success("allowed"); // tracking is allowed in Android by default + break; + case "geoFence": + GeofenceMonitor.getInstance().handleMethodCall(nextMethodComponents, call.arguments, result); + break; + default: + result.notImplemented(); + break; } } - public void notifyGeoFence​(String event, Object arguments) { + public void notifyGeoFence(String event, Object arguments) { Activity activity = getActivity(); if ((activity != null) && (_channel != null)) { activity.runOnUiThread(() -> _channel.invokeMethod(String.format("geoFence.%s", event), arguments)); @@ -185,7 +190,7 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { // PluginRegistry.RequestPermissionsResultListener @Override - public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + public boolean onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == LocationServices.LOCATION_PERMISSION_REQUEST_CODE) { return LocationServices.getInstance().onRequestPermissionsResult(requestCode, permissions, grantResults); } @@ -212,7 +217,7 @@ private String getDeviceId(Object params) { { UUID uuid; final String androidId = Settings.Secure.getString(getActivity().getContentResolver(), Settings.Secure.ANDROID_ID); - uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8")); + uuid = UUID.nameUUIDFromBytes(androidId.getBytes(StandardCharsets.UTF_8)); deviceId = uuid.toString(); } catch (Exception e) @@ -226,12 +231,11 @@ private boolean createNotificationChannel(MethodCall call) { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library Context appContext = (_flutterBinding != null) ? _flutterBinding.getApplicationContext() : null; - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) && (appContext != null)) { - + if (appContext != null) { try { - String id = call.hasArgument​("id") ? call.argument("id") : "edu.illinois.rokwire.firebase_messaging.notification_channel"; - String name = call.hasArgument​("name") ? call.argument("name") : "Rokwire"; - int importance = call.hasArgument​("importance") ? call.argument("importance") : android.app.NotificationManager.IMPORTANCE_DEFAULT; + String id = call.hasArgument("id") ? call.argument("id") : "edu.illinois.rokwire.firebase_messaging.notification_channel"; + String name = call.hasArgument("name") ? call.argument("name") : "Rokwire"; + int importance = call.hasArgument("importance") ? call.argument("importance") : android.app.NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(id, name, importance); String description = call.argument("description"); @@ -340,16 +344,14 @@ private Object getEncryptionKey(Object params) { } String base64KeyValue = Utils.AppSecureSharedPrefs.getString(getActivity(), identifier, null); byte[] encryptionKey = Utils.Base64.decode(base64KeyValue); - if ((encryptionKey != null) && (encryptionKey.length == keySize)) { - return base64KeyValue; - } else { + if ((encryptionKey == null) || (encryptionKey.length != keySize)) { byte[] keyBytes = new byte[keySize]; SecureRandom secRandom = new SecureRandom(); secRandom.nextBytes(keyBytes); base64KeyValue = Utils.Base64.encode(keyBytes); Utils.AppSecureSharedPrefs.saveString(getActivity(), identifier, base64KeyValue); - return base64KeyValue; } + return base64KeyValue; } // Helpers diff --git a/assets/styles.json b/assets/styles.json new file mode 100644 index 000000000..13037bda1 --- /dev/null +++ b/assets/styles.json @@ -0,0 +1,221 @@ +{ + "themes": { + "light": {}, + "dark": {}, + "system": {} + }, + "color": { + "_exampleComment" :"This is an example of a comment ('_' prefixed key) which will be ignored by code generation", + "fillColorPrimary" :"#002855", + "fillColorPrimaryVariant" :"#0F2040", + "fillColorSecondary" :"#E84A27", + "fillColorSecondaryVariant" :"#CF3C1B", + + "textPrimary" :"#FFFFFF", + "textAccent" :"#002855", + "textDark" :"#FFFFFF", + "textMedium" :"#FFFFFF", + "textLight" :"#FFFFFF", + "textDisabled" :"#BDBDBD", + + "iconPrimary" :"#002855", + "iconLight" :"#FFFFFF", + "iconDark" :"#404040", + "iconMedium" :"#FFFFFF", + "iconDisabled" :"#BDBDBD", + + "surface" :"#FFFFFF", + "surfaceAccent" :"#DADDE1", + "background" :"#F5F5F5", + "backgroundVariant" :"#E8E9EA", + + "shadow" :"#30000000", + + "gradientColorPrimary" :"#244372", + + "accentColor1" :"#E84A27", + "accentColor2" :"#5FA7A3", + "accentColor3" :"#5182CF", + "accentColor4" :"#9318BB", + + "success" :"#2E7D32", + "alert" :"#ff0000", + + "dividerLine" :"#535353" + }, + "font_family": { + "black": "ProximaNovaBlack", + "black_italic": "ProximaNovaBlackIt", + "bold": "ProximaNovaBold", + "bold_italic": "ProximaNovaBoldIt", + "extra_bold": "ProximaNovaExtraBold", + "extra_bold_italic": "ProximaNovaExtraBoldIt", + "light": "ProximaNovaLight", + "light_italic": "ProximaNovaLightIt", + "medium": "ProximaNovaMedium", + "medium_italic": "ProximaNovaMediumIt", + "regular": "ProximaNovaRegular", + "regular_italic": "ProximaNovaRegularIt", + "semi_bold": "ProximaNovaSemiBold", + "semi_bold_italic": "ProximaNovaSemiBoldIt", + "thin": "ProximaNovaThin", + "thin_italic": "ProximaNovaThinIt" + }, + "text_style": { + "app_title": { "font_family": "bold", "size": 42.0, "color": "textLight"}, + + "header_bar": { "font_family": "bold", "size": 20.0, "color": "textLight"}, + "header_bar.accent": { "font_family": "bold", "size": 20.0, "color": "textLight"}, + + "widget.heading.extra_large": { "font_family": "regular", "size": 30.0, "color": "textLight"}, + "widget.heading.extra_large.bold": { "font_family": "bold", "size": 30.0, "color": "textLight"}, + "widget.heading.large": { "font_family": "regular", "size": 20.0, "color": "textLight"}, + "widget.heading.large.bold": { "font_family": "bold", "size": 20.0, "color": "textLight"}, + "widget.heading.regular": { "font_family": "regular", "size": 16.0, "color": "textLight"}, + "widget.heading.regular.bold": { "font_family": "bold", "size": 16.0, "color": "textLight"}, + "widget.heading.medium": { "font_family": "bold", "size": 14.0, "color": "textLight"}, + "widget.heading.small": { "font_family": "bold", "size": 12.0, "color": "textLight"}, + + "widget.message.dark.extra_large": {"font_family": "regular", "size": 24.0, "color": "textDark"}, + "widget.message.dark.medium": {"font_family": "medium", "size": 16.0, "color": "textDark"}, + + "widget.message.extra_large.bold": {"font_family": "bold", "size": 24.0, "color": "textPrimary", "height": 1}, + "widget.message.large.bold": {"font_family": "bold", "size": 20.0, "color": "textPrimary", "height": 1}, + "widget.message.large": {"font_family": "medium", "size": 20.0, "color": "textPrimary", "height": 1}, + "widget.message.large.dark.bold": {"font_family": "bold", "size": 20.0, "color": "textDark", "height": 1}, + "widget.message.regular.primary.bold": {"font_family": "bold", "size": 16.0, "color": "fillColorPrimary", "height": 1}, + "widget.message.regular.primary": {"font_family": "regular", "size": 16.0, "color": "fillColorPrimary", "height": 1}, + "widget.message.light.bold.primary": {"font_family": "bold", "size": 16.0, "color": "textMedium", "height": 1}, + "widget.message.medium": {"font_family": "bold", "size": 18.0, "color": "textPrimary", "height": 1}, + "widget.message.regular": {"font_family": "regular", "size": 16.0, "color": "textPrimary", "height": 1}, + "widget.message.regular.bold": {"font_family": "bold", "size": 16.0, "color": "textPrimary", "height": 1}, + "widget.message.regular.bold.accent": {"font_family": "bold", "size": 16.0, "color": "textDark", "height": 1}, + "widget.message.small": {"font_family": "regular", "size": 14.0, "color": "textPrimary", "height": 1}, + "widget.message.small.primary.bold": {"font_family": "bold", "size": 14.0, "color": "fillColorPrimary", "height": 1}, + "widget.message.light.regular": {"font_family": "regular", "size": 16.0, "color": "textMedium", "height": 1}, + + "widget.title.extra_large": { "font_family": "bold", "size": 24.0, "color": "textPrimary"}, + "widget.title.large": { "font_family": "regular", "size": 20.0, "color": "textPrimary"}, + "widget.title.large.bold": { "font_family": "bold", "size": 20.0, "color": "textPrimary"}, + "widget.title.medium" : {"font_family": "regular", "size": 18.0, "color": "textPrimary"}, + "widget.title.medium.bold" : {"font_family": "bold", "size": 18.0, "color": "textPrimary"}, + "widget.title.regular" : {"font_family": "bold", "size": 16.0, "color": "textPrimary"}, + "widget.title.small.bold" : {"font_family": "bold", "size": 14.0, "color": "textPrimary"}, + "widget.title.tiny" : {"font_family": "bold", "size": 12.0, "color": "textPrimary"}, + + "widget.title.accent.extra_large": { "font_family": "bold", "size": 24.0, "color": "textAccent"}, + "widget.title.accent.large": { "font_family": "regular", "size": 20.0, "color": "textAccent"}, + "widget.title.accent.large.bold": { "font_family": "bold", "size": 20.0, "color": "textAccent"}, + "widget.title.accent.medium" : {"font_family": "regular", "size": 18.0, "color": "textAccent"}, + "widget.title.accent.medium.bold" : {"font_family": "bold", "size": 18.0, "color": "textAccent"}, + "widget.title.accent.regular" : {"font_family": "bold", "size": 16.0, "color": "textAccent"}, + "widget.title.accent.small.bold" : {"font_family": "bold", "size": 14.0, "color": "textAccent"}, + "widget.title.accent.tiny" : {"font_family": "bold", "size": 12.0, "color": "textAccent"}, + + "widget.detail.large": { "font_family": "regular", "size": 20.0, "color": "textDark"}, + "widget.detail.large.bold": { "font_family": "bold", "size": 20.0, "color": "textDark"}, + "widget.detail.regular.bold": { "font_family": "bold", "size": 16.0, "color": "textDark"}, + "widget.detail.regular": { "font_family": "regular", "size": 16.0, "color": "textDark"}, + "widget.detail.medium": { "font_family": "medium", "size": 16.0, "color": "textDark"}, + "widget.detail.small": { "font_family": "regular", "size": 14.0, "color": "textDark"}, + "widget.detail.light.regular": {"font_family": "regular", "size": 16.0, "color": "textMedium"}, + + "widget.description.large": { "font_family": "regular", "size": 18.0, "color": "textPrimary"}, + "widget.description.medium": { "font_family": "regular", "size": 18.0, "color": "textPrimary"}, + "widget.description.regular.thin": { "font_family": "medium", "size": 16.0, "color": "textPrimary"}, + "widget.description.regular": { "font_family": "regular", "size": 16.0, "color": "textPrimary"}, + "widget.description.regular.bold": { "font_family": "bold", "size": 16.0, "color": "textPrimary"}, + "widget.description.small": { "font_family": "medium", "size": 14.0, "color": "textPrimary"}, + "widget.description.small_underline": { "font_family": "medium", "size": 14.0, "decoration": "underline", "color": "textPrimary"}, + "widget.description.small.bold": { "font_family": "bold", "size": 14.0, "color": "textPrimary"}, + "widget.description.small.bold.semi_expanded": { "font_family": "bold", "size": 14.0, "color": "textPrimary", "letter_spacing": 0.86}, + + "widget.success.regular": { "font_family": "regular", "size": 16.0, "color": "success"}, + "widget.success.regular.bold": { "font_family": "bold", "size": 16.0, "color": "success"}, + + "widget.error.regular": { "font_family": "regular", "size": 16.0, "color": "alert"}, + "widget.error.regular.bold": { "font_family": "bold", "size": 16.0, "color": "alert"}, + + "widget.item.medium.bold": { "font_family": "bold", "size": 18.0, "color": "textDark"}, + "widget.item.medium": { "font_family": "regular", "size": 18.0, "color": "textDark"}, + "widget.item.regular.bold": { "font_family": "bold", "size": 16.0, "color": "textDark"}, + "widget.item.regular.thin": { "font_family": "regular", "size": 16.0, "color": "textDark"}, + "widget.item.regular": { "font_family": "medium", "size": 16.0, "color": "textDark"}, + "widget.item.small.bold": { "font_family": "bold", "size": 14.0, "color": "textDark"}, + "widget.item.small": { "font_family": "medium", "size": 14.0, "color": "textDark"}, + "widget.item.small.thin": { "font_family": "regular", "size": 14.0, "color": "textDark"}, + "widget.item.tiny.bold": { "font_family": "bold", "size": 12.0, "color": "textDark"}, + "widget.item.tiny": { "font_family": "medium", "size": 12.0, "color": "textDark"}, + "widget.item.tiny.thin": { "font_family": "regular", "size": 12.0, "color": "textDark"}, + + "widget.info.regular": { "font_family": "regular", "size": 16.0, "color": "textLight"}, + "widget.info.regular.bold": { "font_family": "bold", "size": 16.0, "color": "textLight"}, + "widget.info.small": { "font_family": "regular", "size": 14.0, "color": "textLight"}, + "widget.info.small.bold": { "font_family": "bold", "size": 14.0, "color": "textLight"}, + + "widget.tab.selected" : {"font_family": "bold", "size": 16.0, "color": "textPrimary"}, + "widget.tab.not_selected" : {"font_family": "medium", "size": 16.0, "color": "textPrimary"}, + + "widget.button.title.regular" : { "font_family": "bold", "size": 18.0, "color": "textPrimary"}, + "widget.button.title.medium" : { "font_family": "medium", "size": 16.0, "color": "textPrimary"}, + "widget.button.title.medium.bold" : { "font_family": "bold", "size": 16.0, "color": "textPrimary"}, + "widget.button.title.medium.thin" : { "font_family": "regular", "size": 16.0, "color": "textPrimary"}, + "widget.button.title.medium.bold.underline" : { "font_family": "bold", "size": 16.0, "color": "textPrimary", "decoration": "underline", "decoration_style": "solid", "decoration_thickness" : 1.0, "decoration_color": "fillColorSecondary"}, + "widget.button.title.medium.underline" : { "font_family": "medium", "size": 16.0, "color": "textPrimary", "decoration": "underline", "decoration_style": "solid", "decoration_thickness" : 1.0, "decoration_color": "fillColorSecondary"}, + "widget.button.title.medium.light.underline" : { "font_family": "medium", "size": 16.0, "color": "textLight", "decoration": "underline", "decoration_style": "solid", "decoration_thickness" : 1.0, "decoration_color": "textLight"}, + "widget.button.title.enabled": { "font_family": "bold", "size": 16.0, "color": "textPrimary"}, + "widget.button.title.disabled": { "font_family": "bold", "size": 16.0, "color": "textDisabled"}, + "widget.button.title.small.underline" : { "font_family": "regular", "size": 14.0, "color": "textPrimary", "decoration": "underline", "decoration_style": "solid", "decoration_thickness" : 1.0, "decoration_color": "fillColorSecondary"}, + "widget.button.description.small" : { "font_family": "regular", "size": 14.0, "color": "textDark"}, + "widget.button.description.tiny" : { "font_family": "regular", "size": 12.0, "color": "textDark"}, + + "widget.colourful_button.title.title.regular" : { "font_family": "regular", "size": 14.0, "color": "textLight"}, + "widget.colourful_button.title.title.accent" : { "font_family": "bold", "size": 14.0, "color": "textLight"}, + + "widget.input_field.text.medium": { "font_family": "regular", "size": 18.0, "color": "textDark"}, + "widget.input_field.text.regular": { "font_family": "regular", "size": 16.0, "color": "textDark"}, + + "widget.dialog.button.close" : {"font_family": "medium", "size": 50.0, "color": "textLight"}, + "widget.dialog.message.medium" : {"font_family": "medium", "size": 16.0, "color": "textLight"}, + "widget.dialog.message.medium.thin" : {"font_family": "regular", "size": 16.0, "color": "textLight"}, + "widget.dialog.message.regular.bold" : {"font_family": "bold", "size": 20.0, "color": "textLight"}, + "widget.dialog.message.large": { "font_family": "medium", "size": 24.0, "color": "textLight"}, + "widget.dialog.message.large.bold": { "font_family": "bold", "size": 24.0, "color": "textLight"}, + + "widget.dialog.message.dark.large.bold": { "font_family": "bold", "size": 24.0, "color": "textDark"}, + "widget.dialog.message.dark.large": { "font_family": "regular", "size": 24.0, "color": "textDark"}, + "widget.dialog.message.dark.medium": { "font_family": "medium", "size": 16.0, "color": "textDark"}, + + "widget.card.title.large": { "font_family": "bold", "size": 24.0, "color": "textPrimary"}, + "widget.card.title.medium": { "font_family": "regular", "size": 20.0, "color": "textPrimary"}, + "widget.card.title.regular.bold": { "font_family": "bold", "size": 18.0, "color": "textPrimary"}, + "widget.card.title.small": { "font_family": "regular", "size": 16.0, "color": "textPrimary"}, + "widget.card.title.small.bold": { "font_family": "bold", "size": 16.0, "color": "textPrimary"}, + "widget.card.title.tiny": { "font_family": "regular", "size": 14.0, "color": "textPrimary"}, + "widget.card.title.tiny.bold": { "font_family": "bold", "size": 14.0, "color": "textPrimary"}, + "widget.card.detail.regular_variant": { "font_family": "regular", "size": 16.0, "color": "textDark"}, + "widget.card.detail.regular": { "font_family": "regular", "size": 16.0, "color": "textDark"}, + "widget.card.detail.regular.bold": { "font_family": "bold", "size": 16.0, "color": "textDark"}, + "widget.card.detail.medium": { "font_family": "medium", "size": 16.0, "color": "textDark"}, + "widget.card.detail.small_variant": { "font_family": "regular", "size": 14.0, "color": "textDark"}, + "widget.card.detail.small_variant2": { "font_family": "medium", "size": 14.0, "color": "textDark"}, + "widget.card.detail.small": { "font_family": "regular", "size": 14.0, "color": "textDark"}, + "widget.card.detail.tiny": { "font_family": "regular", "size": 12.0, "color": "textDark"}, + "widget.card.detail.tiny.bold": { "font_family": "bold", "size": 12.0, "color": "textDark"}, + "widget.card.detail.tiny_variant2": { "font_family": "medium", "size": 12.0, "color": "textDark"} + }, + "image": { + "home": {"src": "0xf015", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + "bug": {"src": "0xf188", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + "notification": {"src": "0xf0f3", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + "profile": {"src": "0xf2bd", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + + "chevron-up": {"src": "0xf077", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + "chevron-down": {"src": "0xf078", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + "chevron-left": {"src": "0xf053", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + "chevron-right": {"src": "0xf054", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconPrimary"}, + "close": {"src": "0xf00d", "type": "fa.icon", "weight": "solid", "size": 24.0, "color": "iconPrimary"}, + + "retry-medium": {"src": "0xf2f9", "type": "fa.icon", "weight": "solid", "size": 18.0, "color": "iconMedium"} + } +} \ No newline at end of file diff --git a/assets/timezone.tzf b/assets/timezone.tzf new file mode 100644 index 000000000..3e73678ec Binary files /dev/null and b/assets/timezone.tzf differ diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0..562c5e444 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/example/pubspec.lock b/example/pubspec.lock index 8402ebab5..be4d0948d 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,26 +5,34 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 + sha256: "8eb354cb8ebed8a9fdf63699d15deff533bc133128898afaf754926b57d611b6" url: "https://pub.dev" source: hosted - version: "1.3.16" + version: "1.3.1" + app_links: + dependency: transitive + description: + name: app_links + sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb" + url: "https://pub.dev" + source: hosted + version: "3.5.0" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" asn1lib: dependency: transitive description: name: asn1lib - sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6 + sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "1.4.0" async: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" clock: dependency: transitive description: @@ -101,10 +117,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "74a4727e030347edff3b6e5256b7fb0c3de8af8ed278e6c56718760786a1fa40" + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.3+4" crypto: dependency: transitive description: @@ -125,26 +141,26 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.5" dbus: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.8" device_calendar: dependency: transitive description: name: device_calendar - sha256: "991b55bb9e0a0850ec9367af8227fe25185210da4f5fa7bd15db4cc813b1e2e5" + sha256: "5a1ce7887b4ffbaf3743078c8314dede5e694cddd69bab43f35ce815c5d82a7d" url: "https://pub.dev" source: hosted - version: "4.3.2" + version: "4.3.1" device_info: dependency: transitive description: @@ -165,10 +181,10 @@ packages: dependency: transitive description: name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "5.0.1" fake_async: dependency: transitive description: @@ -181,18 +197,18 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.0.2" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.1.4" file_selector_linux: dependency: transitive description: @@ -205,18 +221,18 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: "182c3f8350cee659f7b115e956047ee3dc672a96665883a545e81581b9a82c72" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.3+2" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.6.1" file_selector_windows: dependency: transitive description: @@ -229,66 +245,66 @@ packages: dependency: transitive description: name: firebase_core - sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" + sha256: "250678b816279b3240c3a33e1f76bf712c00718f1fbeffc85873a5da8c077379" url: "https://pub.dev" source: hosted - version: "2.24.2" + version: "2.13.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 + sha256: "8c0f4c87d20e2d001a5915df238c1f9c88704231f591324205f5a5d2a7740a45" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.5.0" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - sha256: "5125b7f3fcef2bfdd7e071afe7edcefd9597968003e44e073456c773d91694ee" + sha256: "0d74cca3085f144f99aa4bd82cc4d33280d4cb72bac0b733cbf97c2d7d126df8" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.3.1" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "359197344def001589c84f8d1d57c05f6e2e773f559205610ce58c25e2045a57" + sha256: "13880033d5f2055f53bcda28024e16607b8400445a425f86732c1935da9260db" url: "https://pub.dev" source: hosted - version: "3.6.16" + version: "3.6.1" firebase_messaging: dependency: transitive description: name: firebase_messaging - sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8" + sha256: "9cfe5c4560fb83393511ca7620f8fb3f22c9a80303052f10290e732fcfb801bd" url: "https://pub.dev" source: hosted - version: "14.7.10" + version: "14.6.1" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "54e283a0e41d81d854636ad0dad73066adc53407a60a7c3189c9656e2f1b6107" + sha256: "7e25cb71019ccef8b1fd7b37969af79f04c467974cce4dfc291fa36974edd7ba" url: "https://pub.dev" source: hosted - version: "4.5.18" + version: "4.5.1" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" + sha256: "5d9840cc8126ea723b1bda901389cb542902f664f2653c16d4f8114e95f13cec" url: "https://pub.dev" source: hosted - version: "3.5.18" + version: "3.5.1" flutter: dependency: "direct main" description: flutter @@ -302,6 +318,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + flutter_hooks: + dependency: transitive + description: + name: flutter_hooks + sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + url: "https://pub.dev" + source: hosted + version: "0.18.6" flutter_html: dependency: transitive description: @@ -350,19 +374,92 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_passkey: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: f44af5d1905242ff7090317af9c59ff7f111d7b2 + url: "https://github.com/rokmetro/flutter_passkey.git" + source: git + version: "1.0.3" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.15" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: "0da41e631a368e02366fc1a9b79dd8da191e700a836878bc54466fff51c07df2" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: f6fa7059ff3428c19cd756c02fef8eb0147131c7e64591f9060c90b5ab84f094 + url: "https://pub.dev" + source: hosted + version: "2.1.4" flutter_web_plugins: dependency: transitive description: flutter @@ -372,10 +469,10 @@ packages: dependency: transitive description: name: fluttertoast - sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c" url: "https://pub.dev" source: hosted - version: "8.2.4" + version: "8.2.2" font_awesome_flutter: dependency: transitive description: @@ -396,58 +493,146 @@ packages: dependency: transitive description: name: geolocator - sha256: "5c496b46e245d006760e643cedde7c9fa785a34391b5eca857a46358f9bde02b" + sha256: "5c23f3613f50586c0bbb2b8f970240ae66b3bd992088cf60dd5ee2e6f7dde3a8" url: "https://pub.dev" source: hosted - version: "8.2.1" + version: "9.0.2" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: "3fa9215caf1e4463adbdf1f21b07fdcb9bc2af2ef1df3715a52376b87bebb087" + sha256: "55c4a81ea15b664a2fdbfd39ba37f2e9c31e1b57237bbeb8deeeaea9979bc97c" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "4.2.3" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "2f2d4ee16c4df269e93c0e382be075cc01d5db6703c3196e4af20a634fe49ef4" + sha256: "22b60ca3b8c0f58e6a9688ff855ee39ab813ca3f0c0609a48d282f6631266f2e" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.2.5" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: "009a21c4bc2761e58dccf07c24f219adaebe0ff707abdfd40b0a763d4003fab9" + sha256: af4d69231452f9620718588f41acc4cb58312368716bfff2e92e770b46ce6386 url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.0.7" geolocator_web: dependency: transitive description: name: geolocator_web - sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + sha256: f68a122da48fcfff68bbc9846bb0b74ef651afe84a1b1f6ec20939de4d6860e1 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.1.6" geolocator_windows: dependency: transitive description: name: geolocator_windows - sha256: "4f4218f122a6978d0ad655fa3541eea74c67417440b09f0657238810d5af6bdc" + sha256: f5911c88e23f48b598dd506c7c19eff0e001645bdc03bb6fecb9f4549208354d + url: "https://pub.dev" + source: hosted + version: "0.1.1" + gql: + dependency: transitive + description: + name: gql + sha256: "998304fbb88a3956cfea10cd27a56f8e5d4b3bc110f03c952c18a9310774e8bb" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + gql_dedupe_link: + dependency: transitive + description: + name: gql_dedupe_link + sha256: "89681048cf956348e865da872a40081499b8c087fc84dd4d4b9c134bd70d27b3" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "2.0.3+1" + gql_error_link: + dependency: transitive + description: + name: gql_error_link + sha256: e7bfdd2b6232f3e15861cd96c2ad6b7c9c94693843b3dea18295136a5fb5b534 + url: "https://pub.dev" + source: hosted + version: "0.2.3+1" + gql_exec: + dependency: transitive + description: + name: gql_exec + sha256: "0d1fdb2e4154efbfc1dcf3f35ec36d19c8428ff0d560eb4c45b354f8f871dc50" + url: "https://pub.dev" + source: hosted + version: "0.4.3" + gql_http_link: + dependency: transitive + description: + name: gql_http_link + sha256: "89ef87b32947acf4189f564c095f1148b0ab9bb9996fe518716dbad66708b834" + url: "https://pub.dev" + source: hosted + version: "0.4.5" + gql_link: + dependency: transitive + description: + name: gql_link + sha256: f7973279126bc922d465c4f4da6ed93d187085e597b3480f5e14e74d28fe14bd + url: "https://pub.dev" + source: hosted + version: "0.5.1" + gql_transform_link: + dependency: transitive + description: + name: gql_transform_link + sha256: b1735a9a92d25a92960002a8b40dfaede95ec1e5ed848906125d69efd878661f + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + graphql: + dependency: transitive + description: + name: graphql + sha256: b061201579040e9548cec2bae17bbdea0ab30666cb4e7ba48b9675f14d982199 + url: "https://pub.dev" + source: hosted + version: "5.1.3" + graphql_flutter: + dependency: transitive + description: + name: graphql_flutter + sha256: "06059ac9e8417c71582f05e28a59b1416d43959d34a6a0d9565341e3a362e117" + url: "https://pub.dev" + source: hosted + version: "5.1.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" html: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.3" http: dependency: transitive description: @@ -468,34 +653,34 @@ packages: dependency: transitive description: name: image_picker - sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" url: "https://pub.dev" source: hosted - version: "0.8.9" + version: "1.0.4" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + sha256: "47da2161c2e9f8f8a9cbbd89d466d174333fbdd769aeed848912e0b16d9cb369" url: "https://pub.dev" source: hosted - version: "0.8.9+3" + version: "0.8.8" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.1" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.8+2" image_picker_linux: dependency: transitive description: @@ -516,10 +701,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.9.1" image_picker_windows: dependency: transitive description: @@ -588,10 +773,10 @@ packages: dependency: transitive description: name: logger - sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -620,10 +805,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.4" mime_type: dependency: transitive description: @@ -632,6 +817,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + native_flutter_proxy: + dependency: transitive + description: + name: native_flutter_proxy + sha256: d8109940d3412ecb0e88e841b5c7efee2e4ef6016e0d2dc1ebaa07a87af7a45b + url: "https://pub.dev" + source: hosted + version: "0.1.15" nm: dependency: transitive description: @@ -640,14 +833,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" - package_info: + normalize: dependency: transitive description: - name: package_info - sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" + name: normalize + sha256: baf8caf2d8b745af5737cca6c24f7fe3cf3158897fdbcde9a909b9c8d3e2e5af url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "0.7.2" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: @@ -660,90 +869,98 @@ packages: dependency: transitive description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.0.15" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.0.27" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.2.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.1.7" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "5.4.0" pinch_zoom: dependency: transitive description: name: pinch_zoom - sha256: ad12872281742726afaf03438d99a4572c584a612630768953beb6dfd6f9389a + sha256: "8e430a215db198099a708f334620e223cbea3fdc477b717d6535ab852994985e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.4" pointycastle: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "4.2.4" rokwire_plugin: dependency: "direct main" description: @@ -751,62 +968,70 @@ packages: relative: true source: path version: "1.7.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" shared_preferences: dependency: transitive description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.1.4" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.2.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.2.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -832,18 +1057,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.2.8+4" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555 url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.4.5" stack_trace: dependency: transitive description: @@ -872,10 +1097,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.1.0" term_glyph: dependency: transitive description: @@ -908,94 +1133,86 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uni_links: - dependency: transitive - description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - uni_links_platform_interface: + universal_html: dependency: transitive description: - name: uni_links_platform_interface - sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" url: "https://pub.dev" source: hosted - version: "1.0.0" - uni_links_web: + version: "2.2.4" + universal_io: dependency: transitive description: - name: uni_links_web - sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "2.2.2" url_launcher: dependency: transitive description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.0.34" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.0.5" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.0.17" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.0.6" uuid: dependency: transitive description: @@ -1020,70 +1237,78 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" - web: + web_socket_channel: dependency: transitive description: - name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + name: web_socket_channel + sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "2.2.0" webview_flutter: dependency: transitive description: name: webview_flutter - sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" + sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "4.4.1" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" + sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff url: "https://pub.dev" source: hosted - version: "2.10.4" + version: "3.12.0" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" + sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" url: "https://pub.dev" source: hosted - version: "1.9.5" + version: "2.6.0" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 + sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974" url: "https://pub.dev" source: hosted - version: "2.9.5" + version: "3.9.1" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.0.6" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "0.2.0+3" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.2.0-0 <4.0.0" + flutter: ">=3.10.0" diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart new file mode 100644 index 000000000..9cb6920fb --- /dev/null +++ b/lib/gen/styles.dart @@ -0,0 +1,199 @@ +// Code generated by plugin/utils/gen_styles.dart DO NOT EDIT. + +import 'package:rokwire_plugin/service/styles.dart'; +import 'package:rokwire_plugin/ui/widgets/ui_image.dart'; +import 'package:flutter/material.dart'; + +class AppThemes { + static String get light => 'light'; + static String get dark => 'dark'; + static String get system => 'system'; +} + +class AppColors { + static Color get fillColorPrimary => Styles().colors.getColor('fillColorPrimary') ?? const Color(0xFF002855); + static Color get fillColorPrimaryVariant => Styles().colors.getColor('fillColorPrimaryVariant') ?? const Color(0xFF0F2040); + static Color get fillColorSecondary => Styles().colors.getColor('fillColorSecondary') ?? const Color(0xFFE84A27); + static Color get fillColorSecondaryVariant => Styles().colors.getColor('fillColorSecondaryVariant') ?? const Color(0xFFCF3C1B); + static Color get textPrimary => Styles().colors.getColor('textPrimary') ?? const Color(0xFFFFFFFF); + static Color get textAccent => Styles().colors.getColor('textAccent') ?? const Color(0xFF002855); + static Color get textDark => Styles().colors.getColor('textDark') ?? const Color(0xFFFFFFFF); + static Color get textMedium => Styles().colors.getColor('textMedium') ?? const Color(0xFFFFFFFF); + static Color get textLight => Styles().colors.getColor('textLight') ?? const Color(0xFFFFFFFF); + static Color get textDisabled => Styles().colors.getColor('textDisabled') ?? const Color(0xFFBDBDBD); + static Color get iconPrimary => Styles().colors.getColor('iconPrimary') ?? const Color(0xFF002855); + static Color get iconLight => Styles().colors.getColor('iconLight') ?? const Color(0xFFFFFFFF); + static Color get iconDark => Styles().colors.getColor('iconDark') ?? const Color(0xFF404040); + static Color get iconMedium => Styles().colors.getColor('iconMedium') ?? const Color(0xFFFFFFFF); + static Color get iconDisabled => Styles().colors.getColor('iconDisabled') ?? const Color(0xFFBDBDBD); + static Color get surface => Styles().colors.getColor('surface') ?? const Color(0xFFFFFFFF); + static Color get surfaceAccent => Styles().colors.getColor('surfaceAccent') ?? const Color(0xFFDADDE1); + static Color get background => Styles().colors.getColor('background') ?? const Color(0xFFF5F5F5); + static Color get backgroundVariant => Styles().colors.getColor('backgroundVariant') ?? const Color(0xFFE8E9EA); + static Color get shadow => Styles().colors.getColor('shadow') ?? const Color(0x30000000); + static Color get gradientColorPrimary => Styles().colors.getColor('gradientColorPrimary') ?? const Color(0xFF244372); + static Color get accentColor1 => Styles().colors.getColor('accentColor1') ?? const Color(0xFFE84A27); + static Color get accentColor2 => Styles().colors.getColor('accentColor2') ?? const Color(0xFF5FA7A3); + static Color get accentColor3 => Styles().colors.getColor('accentColor3') ?? const Color(0xFF5182CF); + static Color get accentColor4 => Styles().colors.getColor('accentColor4') ?? const Color(0xFF9318BB); + static Color get success => Styles().colors.getColor('success') ?? const Color(0xFF2E7D32); + static Color get alert => Styles().colors.getColor('alert') ?? const Color(0xFFff0000); + static Color get dividerLine => Styles().colors.getColor('dividerLine') ?? const Color(0xFF535353); +} + +class AppFontFamilies { + static String get black => Styles().fontFamilies.fromCode('black') ?? 'ProximaNovaBlack'; + static String get blackItalic => Styles().fontFamilies.fromCode('black_italic') ?? 'ProximaNovaBlackIt'; + static String get bold => Styles().fontFamilies.fromCode('bold') ?? 'ProximaNovaBold'; + static String get boldItalic => Styles().fontFamilies.fromCode('bold_italic') ?? 'ProximaNovaBoldIt'; + static String get extraBold => Styles().fontFamilies.fromCode('extra_bold') ?? 'ProximaNovaExtraBold'; + static String get extraBoldItalic => Styles().fontFamilies.fromCode('extra_bold_italic') ?? 'ProximaNovaExtraBoldIt'; + static String get light => Styles().fontFamilies.fromCode('light') ?? 'ProximaNovaLight'; + static String get lightItalic => Styles().fontFamilies.fromCode('light_italic') ?? 'ProximaNovaLightIt'; + static String get medium => Styles().fontFamilies.fromCode('medium') ?? 'ProximaNovaMedium'; + static String get mediumItalic => Styles().fontFamilies.fromCode('medium_italic') ?? 'ProximaNovaMediumIt'; + static String get regular => Styles().fontFamilies.fromCode('regular') ?? 'ProximaNovaRegular'; + static String get regularItalic => Styles().fontFamilies.fromCode('regular_italic') ?? 'ProximaNovaRegularIt'; + static String get semiBold => Styles().fontFamilies.fromCode('semi_bold') ?? 'ProximaNovaSemiBold'; + static String get semiBoldItalic => Styles().fontFamilies.fromCode('semi_bold_italic') ?? 'ProximaNovaSemiBoldIt'; + static String get thin => Styles().fontFamilies.fromCode('thin') ?? 'ProximaNovaThin'; + static String get thinItalic => Styles().fontFamilies.fromCode('thin_italic') ?? 'ProximaNovaThinIt'; +} + +class AppTextStyles { + static TextStyle get appTitle => Styles().textStyles.getTextStyle('app_title') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 42.0, color: AppColors.textLight); + static TextStyle get headerBar => Styles().textStyles.getTextStyle('header_bar') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textLight); + static TextStyle get headerBarAccent => Styles().textStyles.getTextStyle('header_bar.accent') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textLight); + static TextStyle get widgetHeadingExtraLarge => Styles().textStyles.getTextStyle('widget.heading.extra_large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 30.0, color: AppColors.textLight); + static TextStyle get widgetHeadingExtraLargeBold => Styles().textStyles.getTextStyle('widget.heading.extra_large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 30.0, color: AppColors.textLight); + static TextStyle get widgetHeadingLarge => Styles().textStyles.getTextStyle('widget.heading.large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 20.0, color: AppColors.textLight); + static TextStyle get widgetHeadingLargeBold => Styles().textStyles.getTextStyle('widget.heading.large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textLight); + static TextStyle get widgetHeadingRegular => Styles().textStyles.getTextStyle('widget.heading.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textLight); + static TextStyle get widgetHeadingRegularBold => Styles().textStyles.getTextStyle('widget.heading.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textLight); + static TextStyle get widgetHeadingMedium => Styles().textStyles.getTextStyle('widget.heading.medium') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textLight); + static TextStyle get widgetHeadingSmall => Styles().textStyles.getTextStyle('widget.heading.small') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 12.0, color: AppColors.textLight); + static TextStyle get widgetMessageDarkExtraLarge => Styles().textStyles.getTextStyle('widget.message.dark.extra_large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 24.0, color: AppColors.textDark); + static TextStyle get widgetMessageDarkMedium => Styles().textStyles.getTextStyle('widget.message.dark.medium') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetMessageExtraLargeBold => Styles().textStyles.getTextStyle('widget.message.extra_large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 24.0, color: AppColors.textPrimary, height: 1); + static TextStyle get widgetMessageLargeBold => Styles().textStyles.getTextStyle('widget.message.large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textPrimary, height: 1); + static TextStyle get widgetMessageLarge => Styles().textStyles.getTextStyle('widget.message.large') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 20.0, color: AppColors.textPrimary, height: 1); + static TextStyle get widgetMessageLargeDarkBold => Styles().textStyles.getTextStyle('widget.message.large.dark.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textDark, height: 1); + static TextStyle get widgetMessageRegularPrimaryBold => Styles().textStyles.getTextStyle('widget.message.regular.primary.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.fillColorPrimary, height: 1); + static TextStyle get widgetMessageRegularPrimary => Styles().textStyles.getTextStyle('widget.message.regular.primary') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.fillColorPrimary, height: 1); + static TextStyle get widgetMessageLightBoldPrimary => Styles().textStyles.getTextStyle('widget.message.light.bold.primary') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textMedium, height: 1); + static TextStyle get widgetMessageMedium => Styles().textStyles.getTextStyle('widget.message.medium') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 18.0, color: AppColors.textPrimary, height: 1); + static TextStyle get widgetMessageRegular => Styles().textStyles.getTextStyle('widget.message.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textPrimary, height: 1); + static TextStyle get widgetMessageRegularBold => Styles().textStyles.getTextStyle('widget.message.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary, height: 1); + static TextStyle get widgetMessageRegularBoldAccent => Styles().textStyles.getTextStyle('widget.message.regular.bold.accent') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textDark, height: 1); + static TextStyle get widgetMessageSmall => Styles().textStyles.getTextStyle('widget.message.small') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textPrimary, height: 1); + static TextStyle get widgetMessageSmallPrimaryBold => Styles().textStyles.getTextStyle('widget.message.small.primary.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.fillColorPrimary, height: 1); + static TextStyle get widgetMessageLightRegular => Styles().textStyles.getTextStyle('widget.message.light.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textMedium, height: 1); + static TextStyle get widgetTitleExtraLarge => Styles().textStyles.getTextStyle('widget.title.extra_large') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 24.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleLarge => Styles().textStyles.getTextStyle('widget.title.large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 20.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleLargeBold => Styles().textStyles.getTextStyle('widget.title.large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleMedium => Styles().textStyles.getTextStyle('widget.title.medium') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 18.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleMediumBold => Styles().textStyles.getTextStyle('widget.title.medium.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 18.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleRegular => Styles().textStyles.getTextStyle('widget.title.regular') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleSmallBold => Styles().textStyles.getTextStyle('widget.title.small.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleTiny => Styles().textStyles.getTextStyle('widget.title.tiny') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 12.0, color: AppColors.textPrimary); + static TextStyle get widgetTitleAccentExtraLarge => Styles().textStyles.getTextStyle('widget.title.accent.extra_large') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 24.0, color: AppColors.textAccent); + static TextStyle get widgetTitleAccentLarge => Styles().textStyles.getTextStyle('widget.title.accent.large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 20.0, color: AppColors.textAccent); + static TextStyle get widgetTitleAccentLargeBold => Styles().textStyles.getTextStyle('widget.title.accent.large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textAccent); + static TextStyle get widgetTitleAccentMedium => Styles().textStyles.getTextStyle('widget.title.accent.medium') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 18.0, color: AppColors.textAccent); + static TextStyle get widgetTitleAccentMediumBold => Styles().textStyles.getTextStyle('widget.title.accent.medium.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 18.0, color: AppColors.textAccent); + static TextStyle get widgetTitleAccentRegular => Styles().textStyles.getTextStyle('widget.title.accent.regular') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textAccent); + static TextStyle get widgetTitleAccentSmallBold => Styles().textStyles.getTextStyle('widget.title.accent.small.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textAccent); + static TextStyle get widgetTitleAccentTiny => Styles().textStyles.getTextStyle('widget.title.accent.tiny') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 12.0, color: AppColors.textAccent); + static TextStyle get widgetDetailLarge => Styles().textStyles.getTextStyle('widget.detail.large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 20.0, color: AppColors.textDark); + static TextStyle get widgetDetailLargeBold => Styles().textStyles.getTextStyle('widget.detail.large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textDark); + static TextStyle get widgetDetailRegularBold => Styles().textStyles.getTextStyle('widget.detail.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetDetailRegular => Styles().textStyles.getTextStyle('widget.detail.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetDetailMedium => Styles().textStyles.getTextStyle('widget.detail.medium') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetDetailSmall => Styles().textStyles.getTextStyle('widget.detail.small') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetDetailLightRegular => Styles().textStyles.getTextStyle('widget.detail.light.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textMedium); + static TextStyle get widgetDescriptionLarge => Styles().textStyles.getTextStyle('widget.description.large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 18.0, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionMedium => Styles().textStyles.getTextStyle('widget.description.medium') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 18.0, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionRegularThin => Styles().textStyles.getTextStyle('widget.description.regular.thin') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionRegular => Styles().textStyles.getTextStyle('widget.description.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionRegularBold => Styles().textStyles.getTextStyle('widget.description.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionSmall => Styles().textStyles.getTextStyle('widget.description.small') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 14.0, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionSmallUnderline => Styles().textStyles.getTextStyle('widget.description.small_underline') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 14.0, decoration: TextDecoration.underline, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionSmallBold => Styles().textStyles.getTextStyle('widget.description.small.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textPrimary); + static TextStyle get widgetDescriptionSmallBoldSemiExpanded => Styles().textStyles.getTextStyle('widget.description.small.bold.semi_expanded') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textPrimary, letterSpacing: 0.86); + static TextStyle get widgetSuccessRegular => Styles().textStyles.getTextStyle('widget.success.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.success); + static TextStyle get widgetSuccessRegularBold => Styles().textStyles.getTextStyle('widget.success.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.success); + static TextStyle get widgetErrorRegular => Styles().textStyles.getTextStyle('widget.error.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.alert); + static TextStyle get widgetErrorRegularBold => Styles().textStyles.getTextStyle('widget.error.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.alert); + static TextStyle get widgetItemMediumBold => Styles().textStyles.getTextStyle('widget.item.medium.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 18.0, color: AppColors.textDark); + static TextStyle get widgetItemMedium => Styles().textStyles.getTextStyle('widget.item.medium') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 18.0, color: AppColors.textDark); + static TextStyle get widgetItemRegularBold => Styles().textStyles.getTextStyle('widget.item.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetItemRegularThin => Styles().textStyles.getTextStyle('widget.item.regular.thin') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetItemRegular => Styles().textStyles.getTextStyle('widget.item.regular') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetItemSmallBold => Styles().textStyles.getTextStyle('widget.item.small.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetItemSmall => Styles().textStyles.getTextStyle('widget.item.small') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetItemSmallThin => Styles().textStyles.getTextStyle('widget.item.small.thin') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetItemTinyBold => Styles().textStyles.getTextStyle('widget.item.tiny.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 12.0, color: AppColors.textDark); + static TextStyle get widgetItemTiny => Styles().textStyles.getTextStyle('widget.item.tiny') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 12.0, color: AppColors.textDark); + static TextStyle get widgetItemTinyThin => Styles().textStyles.getTextStyle('widget.item.tiny.thin') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 12.0, color: AppColors.textDark); + static TextStyle get widgetInfoRegular => Styles().textStyles.getTextStyle('widget.info.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textLight); + static TextStyle get widgetInfoRegularBold => Styles().textStyles.getTextStyle('widget.info.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textLight); + static TextStyle get widgetInfoSmall => Styles().textStyles.getTextStyle('widget.info.small') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textLight); + static TextStyle get widgetInfoSmallBold => Styles().textStyles.getTextStyle('widget.info.small.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textLight); + static TextStyle get widgetTabSelected => Styles().textStyles.getTextStyle('widget.tab.selected') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetTabNotSelected => Styles().textStyles.getTextStyle('widget.tab.not_selected') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetButtonTitleRegular => Styles().textStyles.getTextStyle('widget.button.title.regular') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 18.0, color: AppColors.textPrimary); + static TextStyle get widgetButtonTitleMedium => Styles().textStyles.getTextStyle('widget.button.title.medium') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetButtonTitleMediumBold => Styles().textStyles.getTextStyle('widget.button.title.medium.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetButtonTitleMediumThin => Styles().textStyles.getTextStyle('widget.button.title.medium.thin') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetButtonTitleMediumBoldUnderline => Styles().textStyles.getTextStyle('widget.button.title.medium.bold.underline') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary, decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.solid, decorationThickness: 1.0, decorationColor: AppColors.fillColorSecondary); + static TextStyle get widgetButtonTitleMediumUnderline => Styles().textStyles.getTextStyle('widget.button.title.medium.underline') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textPrimary, decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.solid, decorationThickness: 1.0, decorationColor: AppColors.fillColorSecondary); + static TextStyle get widgetButtonTitleMediumLightUnderline => Styles().textStyles.getTextStyle('widget.button.title.medium.light.underline') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textLight, decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.solid, decorationThickness: 1.0, decorationColor: AppColors.textLight); + static TextStyle get widgetButtonTitleEnabled => Styles().textStyles.getTextStyle('widget.button.title.enabled') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetButtonTitleDisabled => Styles().textStyles.getTextStyle('widget.button.title.disabled') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textDisabled); + static TextStyle get widgetButtonTitleSmallUnderline => Styles().textStyles.getTextStyle('widget.button.title.small.underline') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textPrimary, decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.solid, decorationThickness: 1.0, decorationColor: AppColors.fillColorSecondary); + static TextStyle get widgetButtonDescriptionSmall => Styles().textStyles.getTextStyle('widget.button.description.small') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetButtonDescriptionTiny => Styles().textStyles.getTextStyle('widget.button.description.tiny') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 12.0, color: AppColors.textDark); + static TextStyle get widgetColourfulButtonTitleTitleRegular => Styles().textStyles.getTextStyle('widget.colourful_button.title.title.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textLight); + static TextStyle get widgetColourfulButtonTitleTitleAccent => Styles().textStyles.getTextStyle('widget.colourful_button.title.title.accent') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textLight); + static TextStyle get widgetInputFieldTextMedium => Styles().textStyles.getTextStyle('widget.input_field.text.medium') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 18.0, color: AppColors.textDark); + static TextStyle get widgetInputFieldTextRegular => Styles().textStyles.getTextStyle('widget.input_field.text.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetDialogButtonClose => Styles().textStyles.getTextStyle('widget.dialog.button.close') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 50.0, color: AppColors.textLight); + static TextStyle get widgetDialogMessageMedium => Styles().textStyles.getTextStyle('widget.dialog.message.medium') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textLight); + static TextStyle get widgetDialogMessageMediumThin => Styles().textStyles.getTextStyle('widget.dialog.message.medium.thin') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textLight); + static TextStyle get widgetDialogMessageRegularBold => Styles().textStyles.getTextStyle('widget.dialog.message.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 20.0, color: AppColors.textLight); + static TextStyle get widgetDialogMessageLarge => Styles().textStyles.getTextStyle('widget.dialog.message.large') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 24.0, color: AppColors.textLight); + static TextStyle get widgetDialogMessageLargeBold => Styles().textStyles.getTextStyle('widget.dialog.message.large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 24.0, color: AppColors.textLight); + static TextStyle get widgetDialogMessageDarkLargeBold => Styles().textStyles.getTextStyle('widget.dialog.message.dark.large.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 24.0, color: AppColors.textDark); + static TextStyle get widgetDialogMessageDarkLarge => Styles().textStyles.getTextStyle('widget.dialog.message.dark.large') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 24.0, color: AppColors.textDark); + static TextStyle get widgetDialogMessageDarkMedium => Styles().textStyles.getTextStyle('widget.dialog.message.dark.medium') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetCardTitleLarge => Styles().textStyles.getTextStyle('widget.card.title.large') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 24.0, color: AppColors.textPrimary); + static TextStyle get widgetCardTitleMedium => Styles().textStyles.getTextStyle('widget.card.title.medium') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 20.0, color: AppColors.textPrimary); + static TextStyle get widgetCardTitleRegularBold => Styles().textStyles.getTextStyle('widget.card.title.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 18.0, color: AppColors.textPrimary); + static TextStyle get widgetCardTitleSmall => Styles().textStyles.getTextStyle('widget.card.title.small') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetCardTitleSmallBold => Styles().textStyles.getTextStyle('widget.card.title.small.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textPrimary); + static TextStyle get widgetCardTitleTiny => Styles().textStyles.getTextStyle('widget.card.title.tiny') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textPrimary); + static TextStyle get widgetCardTitleTinyBold => Styles().textStyles.getTextStyle('widget.card.title.tiny.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 14.0, color: AppColors.textPrimary); + static TextStyle get widgetCardDetailRegularVariant => Styles().textStyles.getTextStyle('widget.card.detail.regular_variant') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailRegular => Styles().textStyles.getTextStyle('widget.card.detail.regular') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailRegularBold => Styles().textStyles.getTextStyle('widget.card.detail.regular.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailMedium => Styles().textStyles.getTextStyle('widget.card.detail.medium') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailSmallVariant => Styles().textStyles.getTextStyle('widget.card.detail.small_variant') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailSmallVariant2 => Styles().textStyles.getTextStyle('widget.card.detail.small_variant2') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailSmall => Styles().textStyles.getTextStyle('widget.card.detail.small') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 14.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailTiny => Styles().textStyles.getTextStyle('widget.card.detail.tiny') ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 12.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailTinyBold => Styles().textStyles.getTextStyle('widget.card.detail.tiny.bold') ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 12.0, color: AppColors.textDark); + static TextStyle get widgetCardDetailTinyVariant2 => Styles().textStyles.getTextStyle('widget.card.detail.tiny_variant2') ?? TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 12.0, color: AppColors.textDark); +} + +class AppImages { + static UiImage get home => Styles().images.getImage('home') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf015","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get bug => Styles().images.getImage('bug') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf188","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get notification => Styles().images.getImage('notification') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf0f3","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get profile => Styles().images.getImage('profile') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf2bd","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get chevronUp => Styles().images.getImage('chevron-up') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf077","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get chevronDown => Styles().images.getImage('chevron-down') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf078","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get chevronLeft => Styles().images.getImage('chevron-left') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf053","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get chevronRight => Styles().images.getImage('chevron-right') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf054","type":"fa.icon","weight":"solid","size":18.0,"color":"iconPrimary"})); + static UiImage get close => Styles().images.getImage('close') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf00d","type":"fa.icon","weight":"solid","size":24.0,"color":"iconPrimary"})); + static UiImage get retryMedium => Styles().images.getImage('retry-medium') ?? UiImage(spec: ImageSpec.fromJson({"src":"0xf2f9","type":"fa.icon","weight":"solid","size":18.0,"color":"iconMedium"})); +} diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 3867a5438..fe980c5b7 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -1,8 +1,10 @@ import 'dart:collection'; +import 'dart:core'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:rokwire_plugin/service/app_datetime.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -14,7 +16,7 @@ class Auth2Token { final String? accessToken; final String? refreshToken; final String? tokenType; - + Auth2Token({this.accessToken, this.refreshToken, this.idToken, this.tokenType}); static Auth2Token? fromOther(Auth2Token? value, {String? idToken, String? accessToken, String? refreshToken, String? tokenType }) { @@ -60,7 +62,7 @@ class Auth2Token { (tokenType?.hashCode ?? 0); bool get isValid { - return StringUtils.isNotEmpty(accessToken) && StringUtils.isNotEmpty(refreshToken) && StringUtils.isNotEmpty(tokenType); + return StringUtils.isNotEmpty(accessToken) && (kIsWeb || StringUtils.isNotEmpty(refreshToken)) && StringUtils.isNotEmpty(tokenType); } bool get isValidUiuc { @@ -68,77 +70,39 @@ class Auth2Token { } } -//////////////////////////////// -// Auth2LoginType - -enum Auth2LoginType { anonymous, apiKey, email, phone, username, phoneTwilio, oidc, oidcIllinois } - -String? auth2LoginTypeToString(Auth2LoginType value) { - switch (value) { - case Auth2LoginType.anonymous: return 'anonymous'; - case Auth2LoginType.apiKey: return 'api_key'; - case Auth2LoginType.email: return 'email'; - case Auth2LoginType.phone: return 'phone'; - case Auth2LoginType.username: return 'username'; - case Auth2LoginType.phoneTwilio: return 'twilio_phone'; - case Auth2LoginType.oidc: return 'oidc'; - case Auth2LoginType.oidcIllinois: return 'illinois_oidc'; - } -} - -Auth2LoginType? auth2LoginTypeFromString(String? value) { - if (value == 'anonymous') { - return Auth2LoginType.anonymous; - } - else if (value == 'api_key') { - return Auth2LoginType.apiKey; - } - else if (value == 'email') { - return Auth2LoginType.email; - } - else if (value == 'phone') { - return Auth2LoginType.phone; - } - else if (value == 'username') { - return Auth2LoginType.username; - } - else if (value == 'twilio_phone') { - return Auth2LoginType.phoneTwilio; - } - else if (value == 'oidc') { - return Auth2LoginType.oidc; - } - else if (value == 'illinois_oidc') { - return Auth2LoginType.oidcIllinois; - } - return null; -} - //////////////////////////////// // Auth2Account class Auth2Account { + static const String notifySecretsChanged = "edu.illinois.rokwire.account.secrets.changed"; + final String? id; - final String? username; final Auth2UserProfile? profile; final Auth2UserPrefs? prefs; - final List? permissions; - final List? roles; - final List? groups; + final Map secrets; + final List? permissions; + final List? roles; + final List? groups; + final List? identifiers; final List? authTypes; final Map? systemConfigs; - - Auth2Account({this.id, this.username, this.profile, this.prefs, this.permissions, this.roles, this.groups, this.authTypes, this.systemConfigs}); - factory Auth2Account.fromOther(Auth2Account? other, {String? id, String? username, Auth2UserProfile? profile, Auth2UserPrefs? prefs, List? permissions, List? roles, List? groups, List? authTypes, Map? systemConfigs}) { + Auth2Account({this.id, this.profile, this.prefs, this.secrets = const {}, this.permissions, + this.roles, this.groups, this.identifiers, this.authTypes, this.systemConfigs}); + + factory Auth2Account.fromOther(Auth2Account? other, {String? id, String? username, + Auth2UserProfile? profile, Auth2UserPrefs? prefs, Map? secrets, + List? permissions, List? roles, List? groups, + List? identifiers, List? authTypes, Map? systemConfigs}) { return Auth2Account( id: id ?? other?.id, - username: username ?? other?.username, profile: profile ?? other?.profile, prefs: prefs ?? other?.prefs, + secrets: secrets ?? other?.secrets ?? {}, permissions: permissions ?? other?.permissions, roles: roles ?? other?.roles, groups: groups ?? other?.groups, + identifiers: identifiers ?? other?.identifiers, authTypes: authTypes ?? other?.authTypes, systemConfigs: systemConfigs ?? other?.systemConfigs, ); @@ -147,12 +111,13 @@ class Auth2Account { static Auth2Account? fromJson(Map? json, { Auth2UserPrefs? prefs, Auth2UserProfile? profile }) { return (json != null) ? Auth2Account( id: JsonUtils.stringValue(json['id']), - username: JsonUtils.stringValue(json['username']), profile: Auth2UserProfile.fromJson(JsonUtils.mapValue(json['profile'])) ?? profile, prefs: Auth2UserPrefs.fromJson(JsonUtils.mapValue(json['preferences'])) ?? prefs, //TBD Auth2 - permissions: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['permissions'])), - roles: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['roles'])), - groups: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['groups'])), + secrets: JsonUtils.mapValue(json['secrets']) ?? {}, //TBD Auth2 + permissions: Auth2Permission.listFromJson(JsonUtils.listValue(json['permissions'])), + roles: Auth2Role.listFromJson(JsonUtils.listValue(json['roles'])), + groups: Auth2Group.listFromJson(JsonUtils.listValue(json['groups'])), + identifiers: Auth2Identifier.listFromJson(JsonUtils.listValue(json['identifiers'])), authTypes: Auth2Type.listFromJson(JsonUtils.listValue(json['auth_types'])), systemConfigs: JsonUtils.mapValue(json['system_configs']), ) : null; @@ -161,12 +126,13 @@ class Auth2Account { Map toJson() { return { 'id' : id, - 'username' : username, 'profile': profile, 'preferences': prefs, + 'secrets': secrets, 'permissions': permissions, 'roles': roles, 'groups': groups, + 'identifiers': identifiers, 'auth_types': authTypes, 'system_configs': systemConfigs, }; @@ -176,22 +142,22 @@ class Auth2Account { bool operator ==(other) => (other is Auth2Account) && (other.id == id) && - (other.username == username) && (other.profile == profile) && const DeepCollectionEquality().equals(other.permissions, permissions) && const DeepCollectionEquality().equals(other.roles, roles) && const DeepCollectionEquality().equals(other.groups, groups) && + const DeepCollectionEquality().equals(other.identifiers, identifiers) && const DeepCollectionEquality().equals(other.authTypes, authTypes) && const DeepCollectionEquality().equals(other.systemConfigs, systemConfigs); @override int get hashCode => (id?.hashCode ?? 0) ^ - (username?.hashCode ?? 0) ^ (profile?.hashCode ?? 0) ^ (const DeepCollectionEquality().hash(permissions)) ^ (const DeepCollectionEquality().hash(roles)) ^ (const DeepCollectionEquality().hash(groups)) ^ + (const DeepCollectionEquality().hash(identifiers)) ^ (const DeepCollectionEquality().hash(authTypes)) ^ (const DeepCollectionEquality().hash(systemConfigs)); @@ -199,14 +165,61 @@ class Auth2Account { return (id != null) && id!.isNotEmpty /* && (profile != null) && profile.isValid*/; } + Auth2Identifier? get identifier { + return ((identifiers != null) && identifiers!.isNotEmpty) ? identifiers?.first : null; + } + + String? get username { + List usernameIdentifiers = getLinkedForIdentifierType(Auth2Identifier.typeUsername); + if (usernameIdentifiers.isNotEmpty) { + return usernameIdentifiers.first.identifier; + } + return null; + } + + bool isIdentifierLinked(String code) { + if (identifiers != null) { + for (Auth2Identifier identifier in identifiers!) { + if (identifier.code == code) { + return true; + } + } + } + return false; + } + + List getLinkedForIdentifierType(String code) { + List linkedTypes = []; + if (identifiers != null) { + for (Auth2Identifier identifier in identifiers!) { + if (identifier.code == code) { + linkedTypes.add(identifier); + } + } + } + return linkedTypes; + } + + List getLinkedForAuthTypeId(String id) { + List linkedTypes = []; + if (identifiers != null) { + for (Auth2Identifier identifier in identifiers!) { + if (identifier.accountAuthTypeId == id) { + linkedTypes.add(identifier); + } + } + } + return linkedTypes; + } + Auth2Type? get authType { return ((authTypes != null) && authTypes!.isNotEmpty) ? authTypes?.first : null; } - bool isAuthTypeLinked(Auth2LoginType loginType) { + bool isAuthTypeLinked(String code) { if (authTypes != null) { for (Auth2Type authType in authTypes!) { - if (authType.loginType == loginType) { + if (authType.code == code) { return true; } } @@ -214,11 +227,11 @@ class Auth2Account { return false; } - List getLinkedForAuthType(Auth2LoginType loginType) { + List getLinkedForAuthType(String code) { List linkedTypes = []; if (authTypes != null) { for (Auth2Type authType in authTypes!) { - if (authType.loginType == loginType) { + if (authType.code == code) { linkedTypes.add(authType); } } @@ -238,10 +251,51 @@ class Auth2Account { return hasPermission('research_group_admin'); //TBD: These names might go to app config in settings.groups section. } - bool hasRole(String role) => (Auth2StringEntry.findInList(roles, name: role) != null); - bool hasPermission(String premission) => (Auth2StringEntry.findInList(permissions, name: premission) != null); + bool hasPermission(String permission) { + if (Auth2StringEntry.findInList(permissions, name: permission) != null) { + return true; + } + for (Auth2Role role in roles ?? []) { + if (role.hasPermission(permission)) { + return true; + } + } + for (Auth2Group group in groups ?? []) { + if (group.hasPermission(permission)) { + return true; + } + } + return false; + } + bool hasRole(String role) { + if (Auth2StringEntry.findInList(roles, name: role) != null) { + return true; + } + for (Auth2Group group in groups ?? []) { + if (group.hasRole(role)) { + return true; + } + } + return false; + } bool belongsToGroup(String group) => (Auth2StringEntry.findInList(groups, name: group) != null); bool get isAnalyticsProcessed => (MapUtils.get(systemConfigs, 'analytics_processed_date') != null); + + // Secrets + + String? getSecretString(String? name, { String? defaultValue }) => + JsonUtils.stringValue(getSecret(name)) ?? defaultValue; + + dynamic getSecret(String? name) => secrets[name]; + + void applySecret(String name, dynamic value) { + if (value != null) { + secrets[name] = value; + } else { + secrets.remove(name); + } + NotificationService().notify(notifySecretsChanged, secrets); + } } class Auth2AccountScope { @@ -267,9 +321,6 @@ class Auth2UserProfile { int? _birthYear; String? _photoUrl; - String? _email; - String? _phone; - String? _address; String? _state; String? _zip; @@ -278,9 +329,8 @@ class Auth2UserProfile { Map? _data; Auth2UserProfile({String? id, String? firstName, String? middleName, String? lastName, - int? birthYear, String? photoUrl, String? email, String? phone, - String? address, String? state, String? zip, String? country, - Map? data + int? birthYear, String? photoUrl, String? address, String? state, String? zip, + String? country, Map? data }): _id = id, _firstName = firstName, @@ -289,8 +339,6 @@ class Auth2UserProfile { _birthYear = birthYear, _photoUrl = photoUrl, - _email = email, - _phone = phone, _address = address, _state = state, @@ -310,8 +358,6 @@ class Auth2UserProfile { birthYear: JsonUtils.intValue(json['birth_year']), photoUrl: JsonUtils.stringValue(json['photo_url']), - email: JsonUtils.stringValue(json['email']), - phone: JsonUtils.stringValue(json['phone']), address: JsonUtils.stringValue(json['address']), state: JsonUtils.stringValue(json['state']), @@ -341,9 +387,6 @@ class Auth2UserProfile { birthYear: birthYear ?? other._birthYear, photoUrl: photoUrl ?? other._photoUrl, - email: email ?? other._email, - phone: phone ?? other._phone, - address: address ?? other._address, state: state ?? other._state, zip: zip ?? other._zip, @@ -362,8 +405,6 @@ class Auth2UserProfile { 'birth_year': _birthYear, 'photo_url': _photoUrl, - 'email': _email, - 'phone': _phone, 'address': _address, 'state': _state, @@ -384,8 +425,6 @@ class Auth2UserProfile { (other._birthYear == _birthYear) && (other._photoUrl == _photoUrl) && - (other._email == _email) && - (other._phone == _phone) && (other._address == _address) && (other._state == _state) && @@ -403,8 +442,6 @@ class Auth2UserProfile { (_birthYear?.hashCode ?? 0) ^ (_photoUrl?.hashCode ?? 0) ^ - (_email?.hashCode ?? 0) ^ - (_phone?.hashCode ?? 0) ^ (_address?.hashCode ?? 0) ^ (_state?.hashCode ?? 0) ^ @@ -456,20 +493,6 @@ class Auth2UserProfile { _photoUrl = profile._photoUrl; modified = true; } - if ((profile._email != _email) && ( - (scope?.contains(Auth2UserProfileScope.email) ?? false) || - ((profile._email?.isNotEmpty ?? false) && (_email?.isEmpty ?? true)) - )) { - _email = profile._email; - modified = true; - } - if ((profile._phone != _phone) && ( - (scope?.contains(Auth2UserProfileScope.phone) ?? false) || - ((profile._phone?.isNotEmpty ?? false) && (_phone?.isEmpty ?? true)) - )) { - _phone = profile._phone; - modified = true; - } if ((profile._address != _address) && ( (scope?.contains(Auth2UserProfileScope.address) ?? false) || @@ -518,8 +541,6 @@ class Auth2UserProfile { int? get birthYear => _birthYear; String? get photoUrl => _photoUrl; - String? get email => _email; - String? get phone => _phone; String? get address => _address; String? get state => _state; @@ -571,6 +592,176 @@ class Auth2UserProfile { } } +//////////////////////////////// +// Auth2Permission + +class Auth2Permission extends Auth2StringEntry { + Auth2Permission({super.id, super.name}); + + static Auth2Permission? fromJson(Map? json) { + if (json == null) { + return null; + } + Auth2StringEntry? entry = Auth2StringEntry.fromJson(json); + if (entry == null) { + return null; + } + return Auth2Permission(id: entry.id, name: entry.name); + } + + Map toJson() => super.toJson(); + + @override + bool operator ==(other) => + (other is Auth2Permission) && + (other.id == id) && + (other.name == name); + + @override + int get hashCode => super.hashCode; + + static List? listFromJson(List? jsonList) { + List? result; + if (jsonList != null) { + result = []; + for (dynamic jsonEntry in jsonList) { + ListUtils.add(result, Auth2Permission.fromJson(JsonUtils.mapValue(jsonEntry))); + } + } + return result; + } +} + +//////////////////////////////// +// Auth2Role + +class Auth2Role extends Auth2StringEntry { + final List permissions; + + Auth2Role({super.id, super.name, this.permissions = const []}); + + static Auth2Role? fromJson(Map? json) { + if (json == null) { + return null; + } + Auth2StringEntry? entry = Auth2StringEntry.fromJson(json); + if (entry == null) { + return null; + } + return Auth2Role( + id: entry.id, + name: entry.name, + permissions: Auth2Permission.listFromJson(json['permissions']) ?? [], + ); + } + + Map toJson() { + return { + 'id' : id, + 'name': name, + 'permissions': permissions, + }; + } + + @override + bool operator ==(other) => + (other is Auth2Role) && + (other.id == id) && + (other.name == name) && + const DeepCollectionEquality().equals(other.permissions, permissions); + + @override + int get hashCode => super.hashCode ^ + const DeepCollectionEquality().hash(permissions); + + static List? listFromJson(List? jsonList) { + List? result; + if (jsonList != null) { + result = []; + for (dynamic jsonEntry in jsonList) { + ListUtils.add(result, Auth2Role.fromJson(JsonUtils.mapValue(jsonEntry))); + } + } + return result; + } + + bool hasPermission(String permission) => Auth2StringEntry.findInList(permissions, name: permission) != null; +} + +//////////////////////////////// +// Auth2Group + +class Auth2Group extends Auth2StringEntry { + final List permissions; + final List roles; + + Auth2Group({super.id, super.name, this.permissions = const [], + this.roles = const []}); + + static Auth2Group? fromJson(Map? json) { + if (json == null) { + return null; + } + Auth2StringEntry? entry = Auth2StringEntry.fromJson(json); + if (entry == null) { + return null; + } + return Auth2Group( + id: entry.id, + name: entry.name, + permissions: Auth2Permission.listFromJson(json['permissions']) ?? [], + roles: Auth2Role.listFromJson(json['roles']) ?? [], + ); + } + + Map toJson() { + return { + 'id' : id, + 'name': name, + 'permissions': permissions, + 'roles': roles, + }; + } + + @override + bool operator ==(other) => + (other is Auth2Group) && + (other.id == id) && + (other.name == name) && + const DeepCollectionEquality().equals(other.permissions, permissions) && + const DeepCollectionEquality().equals(other.roles, roles); + + @override + int get hashCode => super.hashCode ^ + const DeepCollectionEquality().hash(permissions) ^ + const DeepCollectionEquality().hash(roles); + + static List? listFromJson(List? jsonList) { + List? result; + if (jsonList != null) { + result = []; + for (dynamic jsonEntry in jsonList) { + ListUtils.add(result, Auth2Group.fromJson(JsonUtils.mapValue(jsonEntry))); + } + } + return result; + } + + bool hasPermission(String permission) { + if (Auth2StringEntry.findInList(permissions, name: permission) != null) { + return true; + } + for (Auth2Role role in roles) { + if (role.hasPermission(permission)) { + return true; + } + } + return false; + } + + bool hasRole(String role) => Auth2StringEntry.findInList(roles, name: role) != null; +} + //////////////////////////////// // Auth2StringEntry @@ -641,46 +832,153 @@ class Auth2StringEntry { enum Auth2UserProfileScope { firstName, middleName, lastName, birthYear, photoUrl, email, phone, address, state, zip, country, data } +//////////////////////////////// +// Auth2Identifier + +class Auth2Identifier { + static const String typeEmail = 'email'; + static const String typePhone = 'phone'; + static const String typeUsername = 'username'; + static const String typeUin = 'uin'; + static const String typeNetId = 'net_id'; + + final String? id; + final String? code; + final String? identifier; + final bool? verified; + final bool? linked; + final bool? sensitive; + final String? accountAuthTypeId; + + Auth2Identifier({this.id, this.code, this.identifier, this.verified, this.linked, this.sensitive, this.accountAuthTypeId}); + + static Auth2Identifier? fromJson(Map? json) { + return (json != null) ? Auth2Identifier( + id: JsonUtils.stringValue(json['id']), + code: JsonUtils.stringValue(json['code']), + identifier: JsonUtils.stringValue(json['identifier']), + verified: JsonUtils.boolValue(json['verified']), + linked: JsonUtils.boolValue(json['linked']), + sensitive: JsonUtils.boolValue(json['sensitive']), + accountAuthTypeId: JsonUtils.stringValue(json['account_auth_type_id']), + ) : null; + } + + Map toJson() { + return { + 'id' : id, + 'code': code, + 'identifier': identifier, + 'verified': verified, + 'linked': linked, + 'sensitive': sensitive, + 'account_auth_type_id': accountAuthTypeId, + }; + } + + @override + bool operator ==(other) => + (other is Auth2Identifier) && + (other.id == id) && + (other.code == code) && + (other.identifier == identifier) && + (other.verified == verified) && + (other.linked == linked) && + (other.sensitive == sensitive) && + (other.accountAuthTypeId == accountAuthTypeId); + + @override + int get hashCode => + (id?.hashCode ?? 0) ^ + (identifier?.hashCode ?? 0) ^ + (code?.hashCode ?? 0) ^ + (verified?.hashCode ?? 0) ^ + (linked?.hashCode ?? 0) ^ + (sensitive?.hashCode ?? 0) ^ + (accountAuthTypeId?.hashCode ?? 0); + + String? get uin { + return (code == typeUin) ? identifier : null; + } + + String? get phone { + return (code == typePhone) ? identifier : null; + } + + String? get email { + return (code == typeEmail) ? identifier : null; + } + + String? get username { + return (code == typeUsername) ? identifier : null; + } + + static List? listFromJson(List? jsonList) { + List? result; + if (jsonList != null) { + result = []; + for (dynamic jsonEntry in jsonList) { + ListUtils.add(result, Auth2Identifier.fromJson(JsonUtils.mapValue(jsonEntry))); + } + } + return result; + } + + static List? listToJson(List? contentList) { + List? jsonList; + if (contentList != null) { + jsonList = []; + for (dynamic contentEntry in contentList) { + jsonList.add(contentEntry?.toJson()); + } + } + return jsonList; + } +} + //////////////////////////////// // Auth2Type class Auth2Type { + static const String typeAnonymous = 'anonymous'; + static const String typeApiKey = 'api_key'; + static const String typePassword = 'password'; + static const String typeCode = 'code'; + static const String typeOidc = 'oidc'; + static const String typeOidcIllinois = 'illinois_oidc'; + static const String typePasskey = 'webauthn'; + final String? id; - final String? identifier; - final bool? active; - final bool? active2fa; - final bool? unverified; final String? code; + final bool? active; final Map? params; - + final DateTime? dateCreated; + final DateTime? dateUpdated; + final Auth2UiucUser? uiucUser; - final Auth2LoginType? loginType; - Auth2Type({this.id, this.identifier, this.active, this.active2fa, this.unverified, this.code, this.params}) : - uiucUser = (params != null) ? Auth2UiucUser.fromJson(JsonUtils.mapValue(params['user'])) : null, - loginType = auth2LoginTypeFromString(code); + Auth2Type({this.id, this.code, this.active, this.params, this.dateCreated, this.dateUpdated}) : + uiucUser = (params != null) ? Auth2UiucUser.fromJson(JsonUtils.mapValue(params['user'])) : null; static Auth2Type? fromJson(Map? json) { return (json != null) ? Auth2Type( id: JsonUtils.stringValue(json['id']), - identifier: JsonUtils.stringValue(json['identifier']), + code: JsonUtils.stringValue(json['auth_type_code']), active: JsonUtils.boolValue(json['active']), - active2fa: JsonUtils.boolValue(json['active_2fa']), - unverified: JsonUtils.boolValue(json['unverified']), - code: JsonUtils.stringValue(json['code']), params: JsonUtils.mapValue(json['params']), + dateCreated: AppDateTime().dateTimeLocalFromJson(json['date_created']), + dateUpdated: AppDateTime().dateTimeLocalFromJson(json['date_updated']), ) : null; } Map toJson() { return { 'id' : id, - 'identifier': identifier, + 'auth_type_code': code, 'active': active, - 'active_2fa': active2fa, - 'unverified': unverified, - 'code': code, 'params': params, + 'date_created': AppDateTime().dateTimeLocalToJson(dateCreated), + 'date_updated': AppDateTime().dateTimeLocalToJson(dateUpdated), }; } @@ -688,35 +986,17 @@ class Auth2Type { bool operator ==(other) => (other is Auth2Type) && (other.id == id) && - (other.identifier == identifier) && - (other.active == active) && - (other.active2fa == active2fa) && - (other.unverified == unverified) && (other.code == code) && + (other.active == active) && const DeepCollectionEquality().equals(other.params, params); @override int get hashCode => (id?.hashCode ?? 0) ^ - (identifier?.hashCode ?? 0) ^ - (active?.hashCode ?? 0) ^ - (active2fa?.hashCode ?? 0) ^ - (unverified?.hashCode ?? 0) ^ (code?.hashCode ?? 0) ^ + (active?.hashCode ?? 0) ^ (const DeepCollectionEquality().hash(params)); - String? get uin { - return (loginType == Auth2LoginType.oidcIllinois) ? identifier : null; - } - - String? get phone { - return (loginType == Auth2LoginType.phoneTwilio) ? identifier : null; - } - - String? get email { - return (loginType == Auth2LoginType.email) ? identifier : null; - } - static List? listFromJson(List? jsonList) { List? result; if (jsonList != null) { @@ -740,6 +1020,43 @@ class Auth2Type { } } + +//////////////////////////////// +// Auth2Message + +class Auth2Message { + final String? message; + final Map? params; + + Auth2Message({this.message, this.params}); + + static Auth2Message? fromJson(Map? json) { + return (json != null) ? Auth2Message( + message: JsonUtils.stringValue(json['message']), + params: JsonUtils.mapValue(json['params']), + ) : null; + } + + Map toJson() { + return { + 'message': message, + 'params' : params, + }; + } + + @override + bool operator ==(other) => + (other is Auth2Message) && + (other.params == params) && + (other.message == message); + + @override + int get hashCode => + (params?.hashCode ?? 0) ^ + (message?.hashCode ?? 0); + +} + //////////////////////////////// // Auth2Error @@ -885,6 +1202,7 @@ class Auth2UserPrefs { static const String notifyFoodChanged = "edu.illinois.rokwire.user.prefs.food.changed"; static const String notifyTagsChanged = "edu.illinois.rokwire.user.prefs.tags.changed"; static const String notifySettingsChanged = "edu.illinois.rokwire.user.prefs.settings.changed"; + static const String notifyAnonymousIdsChanged = "edu.illinois.rokwire.user.prefs.anonymous_ids.changed"; static const String notifyVoterChanged = "edu.illinois.rokwire.user.prefs.voter.changed"; static const String notifyChanged = "edu.illinois.rokwire.user.prefs.changed"; @@ -899,8 +1217,9 @@ class Auth2UserPrefs { Map? _tags; Map? _settings; Auth2VoterPrefs? _voter; + Map? _anonymousIds; - Auth2UserPrefs({int? privacyLevel, Set? roles, Map>? favorites, Map>? interests, Map>? foodFilters, Map? tags, Map? answers, Map? settings, Auth2VoterPrefs? voter}) { + Auth2UserPrefs({int? privacyLevel, Set? roles, Map>? favorites, Map>? interests, Map>? foodFilters, Map? tags, Map? answers, Map? settings, Auth2VoterPrefs? voter, Map? anonymousIds}) { _privacyLevel = privacyLevel; _roles = roles; _favorites = favorites; @@ -909,6 +1228,7 @@ class Auth2UserPrefs { _tags = tags; _settings = settings; _voter = Auth2VoterPrefs.fromOther(voter, onChanged: _onVoterChanged); + _anonymousIds = anonymousIds; } static Auth2UserPrefs? fromJson(Map? json) { @@ -922,6 +1242,7 @@ class Auth2UserPrefs { answers: JsonUtils.mapValue(json['answers']), settings: JsonUtils.mapValue(json['settings']), voter: Auth2VoterPrefs.fromJson(JsonUtils.mapValue(json['voter'])), + anonymousIds: _anonymousIdsFromJson(JsonUtils.mapValue(json['anonymous_ids'])), ) : null; } @@ -939,6 +1260,7 @@ class Auth2UserPrefs { answers: {}, settings: {}, voter: Auth2VoterPrefs(), + anonymousIds: null, ); } @@ -950,6 +1272,7 @@ class Auth2UserPrefs { Map>? interests = (profile != null) ? _interestsFromProfileList(JsonUtils.listValue(profile['interests'])) : null; Map? tags = (profile != null) ? _tagsFromProfileLists(positive: JsonUtils.listValue(profile['positiveInterestTags']), negative: JsonUtils.listValue(profile['negativeInterestTags'])) : null; Auth2VoterPrefs? voter = (profile != null) ? Auth2VoterPrefs.fromJson(profile) : null; + Map? anonymousIds = (profile != null) ? _anonymousIdsFromJson(JsonUtils.mapValue(profile['anonymous_ids'])) : null; return Auth2UserPrefs( privacyLevel: privacyLevel, @@ -964,6 +1287,7 @@ class Auth2UserPrefs { answers: answers ?? {}, settings: settings ?? {}, voter: voter ?? Auth2VoterPrefs(), + anonymousIds: anonymousIds, ); } @@ -976,7 +1300,8 @@ class Auth2UserPrefs { 'food': JsonUtils.mapOfStringToSetOfStringsJsonValue(_foodFilters), 'tags': _tags, 'settings': _settings, - 'voter': _voter + 'voter': _voter, + 'anonymous_ids': _anonymousIdsToJson(), }; } @@ -990,6 +1315,7 @@ class Auth2UserPrefs { const DeepCollectionEquality().equals(other._foodFilters, _foodFilters) && const DeepCollectionEquality().equals(other._tags, _tags) && const DeepCollectionEquality().equals(other._settings, _settings) && + const DeepCollectionEquality().equals(other._anonymousIds, _anonymousIds) && (other._voter == _voter); @override @@ -1001,9 +1327,10 @@ class Auth2UserPrefs { (const DeepCollectionEquality().hash(_foodFilters)) ^ (const DeepCollectionEquality().hash(_tags)) ^ (const DeepCollectionEquality().hash(_settings)) ^ + (const DeepCollectionEquality().hash(_anonymousIds)) ^ (_voter?.hashCode ?? 0); - bool apply(Auth2UserPrefs? prefs, { Set? scope }) { + bool apply(Auth2UserPrefs? prefs, { bool? notify, Set? scope }) { bool modified = false; if (prefs != null) { @@ -1063,10 +1390,21 @@ class Auth2UserPrefs { modified = true; } + if (prefs._anonymousIds?.isNotEmpty ?? false) { + _anonymousIds ??= {}; + for (MapEntry id in prefs._anonymousIds!.entries) { + modified = !_anonymousIds!.containsKey(id.key); + _anonymousIds!.putIfAbsent(id.key, () => id.value); + } + if (notify == true) { + NotificationService().notify(notifyAnonymousIdsChanged); + } + } + if ((prefs._voter != _voter) && ( (scope?.contains(Auth2UserPrefsScope.voter) ?? false) || - ((prefs._voter?.isNotEmpty ?? false) && (_voter?.isEmpty ?? true)) - )) { + ((prefs._voter?.isNotEmpty ?? false) && (_voter?.isEmpty ?? true)) + )) { _voter = Auth2VoterPrefs.fromOther(prefs._voter, onChanged: _onVoterChanged); modified = true; } @@ -1078,11 +1416,11 @@ class Auth2UserPrefs { bool modified = false; if (_privacyLevel != null) { - _privacyLevel = null; - if (notify == true) { - NotificationService().notify(notifyPrivacyLevelChanged); - } - modified = true; + _privacyLevel = null; + if (notify == true) { + NotificationService().notify(notifyPrivacyLevelChanged); + } + modified = true; } if (_roles != null) { @@ -1264,7 +1602,7 @@ class Auth2UserPrefs { if (value && !isFavorite) { if (favoriteIdsForKey == null) { _favorites ??= >{}; - // ignore: prefer_collection_literals + // ignore: prefer_collection_literals _favorites![favorite.favoriteKey] = favoriteIdsForKey = LinkedHashSet(); } favoriteIdsForKey.add(favorite.favoriteId!); @@ -1679,6 +2017,31 @@ class Auth2UserPrefs { NotificationService().notify(notifyChanged, this); } + // Anonymous IDs + + Map? get anonymousIds => _anonymousIds; + + void addAnonymousId(String? id) { + if (id != null) { + _anonymousIds ??= {}; + _anonymousIds!.putIfAbsent(id, () => DateTime.now().toUtc()); + } + } + + Map? _anonymousIdsToJson() { + Map? json; + if (_anonymousIds?.isNotEmpty ?? false) { + json = {}; + for (MapEntry anonymousId in _anonymousIds!.entries) { + String? dateAdded = DateTimeUtils.utcDateTimeToString(anonymousId.value); + if (dateAdded != null) { + json[anonymousId.key] = dateAdded; + } + } + } + return json; + } + // Helpers static Map? _tagsFromJson(Map? json) { @@ -1724,6 +2087,22 @@ class Auth2UserPrefs { } return result; } + + static Map? _anonymousIdsFromJson(Map? json) { + Map? anonymousIds; + if (json is Map) { + anonymousIds = {}; + for (MapEntry anonymousId in json!.entries) { + if (anonymousId.key is String && anonymousId.value is String) { + DateTime? dateAdded = DateTimeUtils.parseDateTime(anonymousId.value, format: DateTimeUtils.defaultDateTimeFormat, isUtc: true); + if (dateAdded != null) { + anonymousIds[anonymousId.key] = dateAdded; + } + } + } + } + return anonymousIds; + } } enum Auth2UserPrefsScope { privacyLevel, roles, favorites, interests, foodFilters, tags, settings, voter } @@ -1954,7 +2333,7 @@ class UserRole { //////////////////////////////// // Favorite -abstract class Favorite { +abstract mixin class Favorite { String get favoriteKey; String? get favoriteId; diff --git a/lib/model/explore.dart b/lib/model/explore.dart index 326b45367..8a567a673 100644 --- a/lib/model/explore.dart +++ b/lib/model/explore.dart @@ -19,7 +19,7 @@ import 'package:rokwire_plugin/utils/utils.dart'; ////////////////////////////// /// Explore -abstract class Explore implements Comparable { +abstract mixin class Explore implements Comparable { String? get exploreId => null; String? get exploreTitle => null; diff --git a/lib/platform_impl/base.dart b/lib/platform_impl/base.dart new file mode 100644 index 000000000..966ae941d --- /dev/null +++ b/lib/platform_impl/base.dart @@ -0,0 +1,19 @@ +// Copyright 2023 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. + +abstract class BasePasskey { + Future arePasskeysSupported(); + Future getPasskey(String? optionsJson); + Future createPasskey(String? optionsJson); +} \ No newline at end of file diff --git a/lib/platform_impl/mobile.dart b/lib/platform_impl/mobile.dart new file mode 100644 index 000000000..2c225bcaf --- /dev/null +++ b/lib/platform_impl/mobile.dart @@ -0,0 +1,49 @@ +// Copyright 2023 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:rokwire_plugin/platform_impl/base.dart'; +import 'package:rokwire_plugin/utils/utils.dart'; + +import 'package:flutter_passkey/flutter_passkey.dart'; + +class PasskeyImpl extends BasePasskey { + final flutterPasskeyPlugin = FlutterPasskey(); + + @override + Future arePasskeysSupported() async { + return await flutterPasskeyPlugin.isSupported(); + } + + @override + Future getPasskey(String? optionsJson) async { + dynamic options = JsonUtils.decode(optionsJson ?? ''); + if (options is Map) { + Map? pubKeyRequest = options['publicKey']; + return await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + } + + return null; + } + + @override + Future createPasskey(String? optionsJson) async { + dynamic options = JsonUtils.decode(optionsJson ?? ''); + if (options is Map) { + Map? pubKeyRequest = options['publicKey']; + return await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + } + + return null; + } +} \ No newline at end of file diff --git a/lib/platform_impl/stub.dart b/lib/platform_impl/stub.dart new file mode 100644 index 000000000..6feba40c3 --- /dev/null +++ b/lib/platform_impl/stub.dart @@ -0,0 +1,32 @@ +// Copyright 2023 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:rokwire_plugin/platform_impl/base.dart'; + +class PasskeyImpl extends BasePasskey { + @override + Future arePasskeysSupported() { + throw Exception("Unimplemented"); + } + + @override + Future getPasskey(String? optionsJson) { + throw Exception("Unimplemented"); + } + + @override + Future createPasskey(String? optionsJson) { + throw Exception("Unimplemented"); + } +} \ No newline at end of file diff --git a/lib/platform_impl/web.dart b/lib/platform_impl/web.dart new file mode 100644 index 000000000..7c17eff1b --- /dev/null +++ b/lib/platform_impl/web.dart @@ -0,0 +1,43 @@ +// Copyright 2023 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:js/js.dart'; +import 'package:js/js_util.dart'; +import 'package:rokwire_plugin/platform_impl/base.dart'; + +@JS('isSupported') +external bool isSupported(); + +@JS('getPasskey') +external dynamic getPasskeyJS(String? optionsJson); + +@JS('createPasskey') +external dynamic createPasskeyJS(String? optionsJson); + +class PasskeyImpl extends BasePasskey { + @override + Future arePasskeysSupported() { + return Future.value(isSupported()); + } + + @override + Future getPasskey(String? optionsJson) { + return promiseToFuture(getPasskeyJS(optionsJson)); + } + + @override + Future createPasskey(String? optionsJson) { + return promiseToFuture(createPasskeyJS(optionsJson)); + } +} \ No newline at end of file diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index 573469d77..77270517c 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -4,8 +4,13 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:rokwire_plugin/service/auth2.dart'; import 'package:rokwire_plugin/service/geo_fence.dart'; +import 'package:rokwire_plugin/platform_impl/stub.dart' + if (dart.library.io) 'package:rokwire_plugin/platform_impl/mobile.dart' + if (dart.library.html) 'package:rokwire_plugin/platform_impl/web.dart'; + class RokwirePlugin { static final MethodChannel _channel = _createChannel('edu.illinois.rokwire/plugin', _handleChannelCall); @@ -45,11 +50,17 @@ class RokwirePlugin { } static Future getDeviceId([String? identifier, String? identifier2]) async { + if (kIsWeb) { + return null; + } + try { return await _channel.invokeMethod('getDeviceId', { 'identifier': identifier, 'identifier2': identifier2 }); } - catch(e) { debugPrint(e.toString()); } + catch(e) { + debugPrint(e.toString()); + } return null; } @@ -80,6 +91,20 @@ class RokwirePlugin { return false; } + static Future arePasskeysSupported() async { + try { return await PasskeyImpl().arePasskeysSupported(); } + catch(e) { debugPrint(e.toString()); } + return false; + } + + static Future getPasskey(String? optionsJson) async { + return await PasskeyImpl().getPasskey(optionsJson); + } + + static Future createPasskey(String? optionsJson) async { + return await PasskeyImpl().createPasskey(optionsJson); + } + // Compound APIs static Future locationServices(String method, [dynamic arguments]) async { @@ -114,5 +139,8 @@ class RokwirePlugin { if (firstMethodComponent == 'geoFence') { GeoFence().onPluginNotification(nextMethodComponents, call.arguments); } + else if (firstMethodComponent == 'passkey') { + Auth2().onPluginNotification(nextMethodComponents, call.arguments); + } } } diff --git a/lib/service/analytics.dart b/lib/service/analytics.dart index ffacc6302..4860578b7 100644 --- a/lib/service/analytics.dart +++ b/lib/service/analytics.dart @@ -27,7 +27,7 @@ import 'package:rokwire_plugin/utils/utils.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:device_info/device_info.dart'; @@ -100,9 +100,17 @@ class Analytics with Service implements NotificationsListener { @override void createService() { NotificationService().subscribe(this, [ + Service.notifyInitialized, Connectivity.notifyStatusChanged, ]); + } + @override + void destroyService() { + NotificationService().unsubscribe(this); + closeDatabase(); + closeTimer(); + super.destroyService(); } @override @@ -147,24 +155,14 @@ class Analytics with Service implements NotificationsListener { } } - @override - void destroyService() { - NotificationService().unsubscribe(this, Connectivity.notifyStatusChanged); - closeDatabase(); - closeTimer(); - super.destroyService(); - } - - @override - Set get serviceDependsOn { - return { Connectivity(), Config() }; - } - // NotificationsListener @override void onNotification(String name, dynamic param) { - if (name == Connectivity.notifyStatusChanged) { + if (name == Service.notifyInitialized) { + onServiceInitialized(param is Service ? param : null); + } + else if (name == Connectivity.notifyStatusChanged) { applyConnectivityStatus(param); } } @@ -237,7 +235,7 @@ class Analytics with Service implements NotificationsListener { @protected void onTimer(_) { - if ((_database != null) && !_inTimer && (_connectionStatus != ConnectivityStatus.none)) { + if ((_database != null) && !_inTimer && (_connectionStatus != ConnectivityStatus.none) && StringUtils.isNotEmpty(Config().loggingUrl)) { _inTimer = true; int? deliveryTimeout = Config().analyticsDeliveryTimeout; @@ -296,11 +294,12 @@ class Analytics with Service implements NotificationsListener { @protected Future sendPacket(String? packet) async { - if (packet != null) { + String? loggingUrl = Config().loggingUrl; + if ((loggingUrl != null) && (packet != null)) { try { //TMP: Temporarly use ConfugApiKeyNetworkAuth auth until logging service gets updated to acknowledge the new Core BB token. //TBD: Remove this when logging service gets updated. - final response = await Network().post(Config().loggingUrl, body: packet, headers: { "Accept": "application/json", "Content-type": "application/json" }, auth: Config() /* Auth2() */, sendAnalytics: false); + final response = await Network().post(loggingUrl, body: packet, headers: { "Accept": "application/json", "Content-type": "application/json" }, auth: Config() /* Auth2() */, sendAnalytics: false); return (response != null) && ((response.statusCode == 200) || (response.statusCode == 201)); } catch (e) { @@ -311,11 +310,25 @@ class Analytics with Service implements NotificationsListener { return false; } + // Services + + @protected + Future onServiceInitialized(Service? service) async { + if (isInitialized) { + if (service == Connectivity()) { + updateConnectivity(); + } + } + } + // Connectivity @protected void updateConnectivity() { - applyConnectivityStatus(Connectivity().status); + ConnectivityStatus? connectionStatus = Connectivity().status; + if (_connectionStatus != connectionStatus) { + applyConnectivityStatus(Connectivity().status); + } } @protected diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index 8c937bd7e..5111ac72a 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -16,6 +16,7 @@ import 'package:flutter/foundation.dart'; +import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/localization.dart'; import 'package:rokwire_plugin/service/service.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -70,16 +71,20 @@ class AppDateTime with Service { return DateTime.now(); } - Future get timezoneDatabase async => null; + @protected + Future get timezoneDatabase async { + ByteData? byteData = await AppBundle.loadBytes('packages/rokwire_plugin/assets/timezone.tzf'); + return byteData?.buffer.asUint8List(); + } - String? get universityLocationName => null; + String? get universityLocationName => Config().timezoneLocation; timezone.Location? get universityLocation { String? locationName = universityLocationName; return (locationName != null) ? timezone.getLocation(locationName) : null; } - bool get useDeviceLocalTimeZone => false; + bool get useDeviceLocalTimeZone => true; DateTime? getUtcTimeFromDeviceTime(DateTime? dateTime) { if (dateTime == null) { @@ -126,14 +131,14 @@ class AppDateTime with Service { } DateFormat dateFormat = DateFormat(format, locale); if (ignoreTimeZone!) { - formattedDateTime = dateFormat.format(dateTime); + formattedDateTime = dateFormat.format(dateTime); } else if (useDeviceLocalTimeZone) { DateTime? dt = (dateTime.isUtc) ? getDeviceTimeFromUtcTime(dateTime) : dateTime; formattedDateTime = (dt != null) ? dateFormat.format(dt) : null; } else { - timezone.Location? uniLocation = universityLocation; - timezone.TZDateTime? tzDateTime = (uniLocation != null) ? timezone.TZDateTime.from(dateTime, uniLocation) : null; - formattedDateTime = (tzDateTime != null) ? dateFormat.format(tzDateTime) : null; + timezone.Location? uniLocation = universityLocation; + timezone.TZDateTime? tzDateTime = (uniLocation != null) ? timezone.TZDateTime.from(dateTime, uniLocation) : null; + formattedDateTime = (tzDateTime != null) ? dateFormat.format(tzDateTime) : null; } if (showTzSuffix && (formattedDateTime != null)) { formattedDateTime = '$formattedDateTime CT'; @@ -142,7 +147,7 @@ class AppDateTime with Service { catch (e) { debugPrint(e.toString()); } - return formattedDateTime; + return formattedDateTime != null ? StringUtils.capitalize(formattedDateTime) : null; } DateTime? dateTimeLocalFromJson(dynamic json) { @@ -153,24 +158,33 @@ class AppDateTime with Service { return DateTimeUtils.utcDateTimeToString(getUtcTimeFromDeviceTime(dateTime)); } - String getDisplayDateTime(DateTime dateTimeUtc, {String? format, bool allDay = false, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false}) { + String getDisplayDateTime(DateTime? dateTimeUtc, {String? format, bool allDay = false, bool includeToday = true, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false, bool multiLine = false}) { + if (dateTimeUtc == null) { + return ''; + } if (format != null) { DateTime dateTimeToCompare = _getDateTimeToCompare(dateTimeUtc: dateTimeUtc, considerSettingsDisplayTime: considerSettingsDisplayTime)!; return formatDateTime(dateTimeToCompare, format: format, ignoreTimeZone: false, showTzSuffix: true) ?? ''; } - String? timePrefix = getDisplayDay(dateTimeUtc: dateTimeUtc, allDay: allDay, considerSettingsDisplayTime: considerSettingsDisplayTime, includeAtSuffix: includeAtSuffix); + String? timePrefix = getDisplayDay(dateTimeUtc: dateTimeUtc, allDay: allDay, includeToday: includeToday, considerSettingsDisplayTime: considerSettingsDisplayTime, includeAtSuffix: includeAtSuffix); String? timeSuffix = getDisplayTime(dateTimeUtc: dateTimeUtc, allDay: allDay, considerSettingsDisplayTime: considerSettingsDisplayTime); - return '$timePrefix $timeSuffix'; + if (timePrefix == null) { + return timeSuffix ?? ''; + } + return '$timePrefix,${multiLine ? '\n' : ' '}$timeSuffix'; } - String? getDisplayDay({DateTime? dateTimeUtc, bool allDay = false, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false}) { + String? getDisplayDay({DateTime? dateTimeUtc, bool allDay = false, bool includeToday = true, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false}) { String? displayDay = ''; if (dateTimeUtc != null) { DateTime dateTimeToCompare = _getDateTimeToCompare(dateTimeUtc: dateTimeUtc, considerSettingsDisplayTime: considerSettingsDisplayTime)!; timezone.Location? location = useDeviceLocalTimeZone ? null : universityLocation; if (DateTimeUtils.isToday(dateTimeToCompare, location: location)) { + if (!includeToday) { + return null; + } displayDay = Localization().getStringEx('model.explore.date_time.today', 'Today'); if (!allDay && includeAtSuffix) { displayDay += " ${Localization().getStringEx('model.explore.date_time.at', 'at')}"; @@ -188,7 +202,7 @@ class AppDateTime with Service { } else if (DateTimeUtils.isThisWeek(dateTimeToCompare, location: location)) { displayDay = formatDateTime(dateTimeToCompare, format: "EE", ignoreTimeZone: true, showTzSuffix: false); } else { - displayDay = formatDateTime(dateTimeToCompare, format: "MMM dd", ignoreTimeZone: true, showTzSuffix: false); + displayDay = formatDateTime(dateTimeToCompare, format: "MMM d", ignoreTimeZone: true, showTzSuffix: false); } } return displayDay; @@ -198,12 +212,50 @@ class AppDateTime with Service { String? timeToString = ''; if (dateTimeUtc != null && !allDay) { DateTime dateTimeToCompare = _getDateTimeToCompare(dateTimeUtc: dateTimeUtc, considerSettingsDisplayTime: considerSettingsDisplayTime)!; - String format = (dateTimeToCompare.minute == 0) ? 'ha' : 'h:mma'; + String format = (dateTimeToCompare.minute == 0) ? 'h a' : 'h:mm a'; timeToString = formatDateTime(dateTimeToCompare, format: format, ignoreTimeZone: true, showTzSuffix: !useDeviceLocalTimeZone); } return timeToString; } + String getRelativeDisplayTime(DateTime time) { + Duration difference = DateTime.now().difference(time); + + String suffix = Localization().getStringEx('model.explore.date_time.ago', ' ago'); + if (difference.inSeconds < 0) { + difference *= -1; + suffix = Localization().getStringEx('model.explore.date_time.left', ' left'); + } + + if (difference.inSeconds < 60) { + return Localization().getStringEx('model.explore.date_time.now', 'just now'); + } + else if (difference.inMinutes < 60) { + return difference.inMinutes.toString() + + Localization().getStringEx('model.explore.date_time.minutes', 'm') + suffix; + } + else if (difference.inHours < 24) { + return difference.inHours.toString() + + Localization().getStringEx('model.explore.date_time.hours', 'h') + suffix; + } + else if (difference.inDays < 30) { + return difference.inDays.toString() + + Localization().getStringEx('model.explore.date_time.days', 'd') + suffix; + } + else { + int differenceInMonths = difference.inDays ~/ 30; + if (differenceInMonths < 12) { + return differenceInMonths.toString() + + Localization().getStringEx('model.explore.date_time.months', 'mo') + suffix; + } else { + int differenceInYears = difference.inDays ~/ 360; + return differenceInYears.toString() + + Localization().getStringEx('model.explore.date_time.years', 'y') + suffix; + } + } + // return DateFormat("MMM dd, yyyy").format(deviceDateTime); + } + DateTime? _getDateTimeToCompare({DateTime? dateTimeUtc, bool considerSettingsDisplayTime = true}) { if (dateTimeUtc == null) { return null; diff --git a/lib/service/app_livecycle.dart b/lib/service/app_lifecycle.dart similarity index 71% rename from lib/service/app_livecycle.dart rename to lib/service/app_lifecycle.dart index 4b5e7dff2..cc04c3667 100644 --- a/lib/service/app_livecycle.dart +++ b/lib/service/app_lifecycle.dart @@ -20,21 +20,21 @@ import 'package:rokwire_plugin/service/service.dart'; typedef AppLifecycleCallback = void Function(AppLifecycleState state); -class AppLivecycleWidgetsBindingObserver extends WidgetsBindingObserver { - final AppLifecycleCallback? onAppLivecycleChange; - AppLivecycleWidgetsBindingObserver({this.onAppLivecycleChange}); +class AppLifecycleWidgetsBindingObserver extends WidgetsBindingObserver { + final AppLifecycleCallback? onAppLifecycleChange; + AppLifecycleWidgetsBindingObserver({this.onAppLifecycleChange}); @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (onAppLivecycleChange != null) { - onAppLivecycleChange!(state); + if (onAppLifecycleChange != null) { + onAppLifecycleChange!(state); } } } -class AppLivecycle with Service { +class AppLifecycle with Service { - static const String notifyStateChanged = "edu.illinois.rokwire.applivecycle.state.changed"; + static const String notifyStateChanged = "edu.illinois.rokwire.applifecycle.state.changed"; WidgetsBindingObserver? _bindingObserver; AppLifecycleState _state = AppLifecycleState.resumed; // initial value @@ -42,17 +42,17 @@ class AppLivecycle with Service { // Singletone Factory - static AppLivecycle? _instance; + static AppLifecycle? _instance; - static AppLivecycle? get instance => _instance; + static AppLifecycle? get instance => _instance; @protected - static set instance(AppLivecycle? value) => _instance = value; + static set instance(AppLifecycle? value) => _instance = value; - factory AppLivecycle() => _instance ?? (_instance = AppLivecycle.internal()); + factory AppLifecycle() => _instance ?? (_instance = AppLifecycle.internal()); @protected - AppLivecycle.internal(); + AppLifecycle.internal(); // Service @@ -74,7 +74,7 @@ class AppLivecycle with Service { @protected void initBinding() { if (_bindingObserver == null) { - _bindingObserver = AppLivecycleWidgetsBindingObserver(onAppLivecycleChange:_onAppLivecycleChangeState); + _bindingObserver = AppLifecycleWidgetsBindingObserver(onAppLifecycleChange: _onAppLifecycleChangeState); WidgetsBinding.instance.addObserver(_bindingObserver!); } } @@ -86,7 +86,7 @@ class AppLivecycle with Service { } } - void _onAppLivecycleChangeState(AppLifecycleState state) { + void _onAppLifecycleChangeState(AppLifecycleState state) { _state = state; NotificationService().notify(notifyStateChanged, state); } diff --git a/lib/service/app_navigation.dart b/lib/service/app_navigation.dart index 41e0acf3d..dfce9a82c 100644 --- a/lib/service/app_navigation.dart +++ b/lib/service/app_navigation.dart @@ -28,20 +28,6 @@ class AppNavigation extends NavigatorObserver { static const String notifyParamRoute = 'route'; static const String notifyParamPreviousRoute = 'previous_route'; - // Singletone Factory - - static AppNavigation ? _instance; - - static AppNavigation? get instance => _instance; - - @protected - static set instance(AppNavigation? value) => _instance = value; - - factory AppNavigation() => _instance ?? (_instance = AppNavigation.internal()); - - @protected - AppNavigation.internal(); - // NavigatorObserver @override diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index ddef5f78f..aff7ba2a6 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1,12 +1,12 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:rokwire_plugin/model/auth2.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/deep_link.dart'; import 'package:rokwire_plugin/service/log.dart'; import 'package:rokwire_plugin/service/network.dart'; @@ -16,21 +16,38 @@ import 'package:rokwire_plugin/service/service.dart'; import 'package:rokwire_plugin/service/storage.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { - static const String notifyLoginStarted = "edu.illinois.rokwire.auth2.login.started"; - static const String notifyLoginSucceeded = "edu.illinois.rokwire.auth2.login.succeeded"; - static const String notifyLoginFailed = "edu.illinois.rokwire.auth2.login.failed"; - static const String notifyLoginChanged = "edu.illinois.rokwire.auth2.login.changed"; - static const String notifyLoginFinished = "edu.illinois.rokwire.auth2.login.finished"; - static const String notifyLogout = "edu.illinois.rokwire.auth2.logout"; - static const String notifyLinkChanged = "edu.illinois.rokwire.auth2.link.changed"; - static const String notifyAccountChanged = "edu.illinois.rokwire.auth2.account.changed"; - static const String notifyProfileChanged = "edu.illinois.rokwire.auth2.profile.changed"; - static const String notifyPrefsChanged = "edu.illinois.rokwire.auth2.prefs.changed"; - static const String notifyUserDeleted = "edu.illinois.rokwire.auth2.user.deleted"; - static const String notifyPrepareUserDelete = "edu.illinois.rokwire.auth2.user.prepare.delete"; + static const String notifyLoginStarted = "edu.illinois.rokwire.auth2.login.started"; + static const String notifyLoginSucceeded = "edu.illinois.rokwire.auth2.login.succeeded"; + static const String notifyLoginFailed = "edu.illinois.rokwire.auth2.login.failed"; + static const String notifyLoginChanged = "edu.illinois.rokwire.auth2.login.changed"; + static const String notifyLoginFinished = "edu.illinois.rokwire.auth2.login.finished"; + static const String notifyLoginError = "edu.illinois.rokwire.auth2.login.error"; + static const String notifyLogoutStarted = "edu.illinois.rokwire.auth2.logout.started"; + static const String notifyRefreshStarted = "edu.illinois.rokwire.auth2.refresh.started"; + static const String notifyRefreshError = "edu.illinois.rokwire.auth2.refresh.error"; + static const String notifyRefreshSucceeded = "edu.illinois.rokwire.auth2.refresh.succeeded"; + static const String notifyRefreshFinished = "edu.illinois.rokwire.auth2.refresh.finished"; + static const String notifyLogout = "edu.illinois.rokwire.auth2.logout"; + static const String notifyLinkChanged = "edu.illinois.rokwire.auth2.link.changed"; + static const String notifyAccountChanged = "edu.illinois.rokwire.auth2.account.changed"; + static const String notifyProfileChanged = "edu.illinois.rokwire.auth2.profile.changed"; + static const String notifyPrefsChanged = "edu.illinois.rokwire.auth2.prefs.changed"; + static const String notifySecretsChanged = "edu.illinois.rokwire.auth2.secrets.changed"; + static const String notifyUserDeleted = "edu.illinois.rokwire.auth2.user.deleted"; + static const String notifyPrepareUserDelete = "edu.illinois.rokwire.auth2.user.prepare.delete"; + + + //TODO: Remove if not needed + static const String notifyGetPasskeySuccess = "edu.illinois.rokwire.auth2.passkey.get.succeeded"; + static const String notifyGetPasskeyFailed = "edu.illinois.rokwire.auth2.passkey.get.failed"; + static const String notifyCreatePasskeySuccess = "edu.illinois.rokwire.auth2.passkey.create.succeeded"; + static const String notifyCreatePasskeyFailed = "edu.illinois.rokwire.auth2.passkey.create.failed"; + // static const String _deviceIdIdentifier = 'edu.illinois.rokwire.device_id'; @@ -42,12 +59,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _OidcLogin? _oidcLogin; Auth2AccountScope? _oidcScope; bool? _oidcLink; + bool _oidcLoginInProgress = false; List>? _oidcAuthenticationCompleters; bool? _processingOidcAuthentication; Timer? _oidcAuthenticationTimer; final Map> _refreshTokenFutures = {}; - final Map _refreshTonenFailCounts = {}; + final Map _refreshTokenFailCounts = {}; Client? _updateUserPrefsClient; Timer? _updateUserPrefsTimer; @@ -55,9 +73,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Client? _updateUserProfileClient; Timer? _updateUserProfileTimer; + Client? _updateUserSecretsClient; + Timer? _updateUserSecretsTimer; + Auth2Token? _token; Auth2Account? _account; + Auth2Token? _oidcToken; + String? _anonymousId; Auth2Token? _anonymousToken; Auth2UserPrefs? _anonymousPrefs; @@ -87,9 +110,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { void createService() { NotificationService().subscribe(this, [ DeepLink.notifyUri, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, Auth2UserProfile.notifyChanged, Auth2UserPrefs.notifyChanged, + Auth2Account.notifySecretsChanged, ]); } @@ -100,25 +124,50 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @override Future initService() async { - _token = Storage().auth2Token; - _account = Storage().auth2Account; - _anonymousId = Storage().auth2AnonymousId; - _anonymousToken = Storage().auth2AnonymousToken; - _anonymousPrefs = Storage().auth2AnonymousPrefs; - _anonymousProfile = Storage().auth2AnonymousProfile; - _deviceId = await RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2); + List> futures = [ + RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2), + + Storage().getAuth2Token(), + Storage().getAuth2Account(), + Storage().getAuth2OidcToken(), + ]; - if ((_account == null) && (_anonymousPrefs == null)) { - Storage().auth2AnonymousPrefs = _anonymousPrefs = defaultAnonimousPrefs; + if (isAnonymousAuthenticationSupported) { + futures.addAll([ + Storage().getAuth2AnonymousToken(), + Storage().getAuth2AnonymousPrefs(), + Storage().getAuth2AnonymousProfile(), + ]); } - if ((_account == null) && (_anonymousProfile == null)) { - Storage().auth2AnonymousProfile = _anonymousProfile = defaultAnonimousProfile; + List results = await Future.wait(futures); + _deviceId = results[0]; + _token = results[1]; + _account = results[2]; + _oidcToken = results[3]; + + if (isAnonymousAuthenticationSupported) { + _anonymousToken = results[4]; + _anonymousPrefs = results[5]; + _anonymousProfile = results[6]; + } + + futures.clear(); + if ((_account == null) && (_anonymousPrefs == null) && isAnonymousAuthenticationSupported) { + futures.add(Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonymousPrefs)); } - if ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { + if ((_account == null) && (_anonymousProfile == null) && isAnonymousAuthenticationSupported) { + futures.add(Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonymousProfile)); + } + + if (futures.isNotEmpty) { + await Future.wait(futures); + } + + if (isAnonymousAuthenticationSupported && ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid)) { if (!await authenticateAnonymously()) { throw ServiceError( source: this, @@ -129,7 +178,17 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - _refreshAccount(); + if (kIsWeb && (_token == null)) { + refreshToken(ignoreUnauthorized: true).then((token) { + if (token != null) { + _refreshAccount(); + NotificationService().notify(notifyLoginSucceeded, null); + NotificationService().notify(notifyLoginChanged); + } + }); + } else { + _refreshAccount(); + } await super.initService(); } @@ -143,7 +202,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @override void onNotification(String name, dynamic param) { - if (name == DeepLink.notifyUri) { + //TODO: try to do this without explicit web check + if (name == DeepLink.notifyUri && !kIsWeb) { onDeepLinkUri(param); } else if (name == Auth2UserProfile.notifyChanged) { @@ -152,13 +212,16 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { else if (name == Auth2UserPrefs.notifyChanged) { onUserPrefsChanged(param); } - else if (name == AppLivecycle.notifyStateChanged) { - onAppLivecycleStateChanged(param); + else if (name == AppLifecycle.notifyStateChanged) { + onAppLifecycleStateChanged(param); + } + else if (name == Auth2Account.notifySecretsChanged) { + onAccountSecretsChanged(param); } } @protected - void onAppLivecycleStateChanged(AppLifecycleState? state) { + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } @@ -182,14 +245,17 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected void onDeepLinkUri(Uri? uri) { if (uri != null) { - Uri? redirectUri = Uri.tryParse(oidcRedirectUrl); - if ((redirectUri != null) && - (redirectUri.scheme == uri.scheme) && - (redirectUri.authority == uri.authority) && - (redirectUri.path == uri.path)) - { - handleOidcAuthentication(uri); + if (!kIsWeb) { + Uri? redirectUri = Uri.tryParse(oidcRedirectUrl); + if ((redirectUri == null) || + (redirectUri.scheme != uri.scheme) || + (redirectUri.authority != uri.authority) || + (redirectUri.path != uri.path)) { + return; + } } + + handleOidcAuthentication(uri); } } @@ -200,7 +266,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String? accessToken = token?.accessToken; if ((accessToken != null) && accessToken.isNotEmpty) { String? tokenType = token?.tokenType ?? 'Bearer'; - return { HttpHeaders.authorizationHeader : "$tokenType $accessToken" }; + return { 'Authorization' : "$tokenType $accessToken" }; } return null; } @@ -210,54 +276,92 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @override Future refreshNetworkAuthTokenIfNeeded(BaseResponse? response, dynamic token) async { - if ((response?.statusCode == 401) && (token is Auth2Token) && (this.token == token)) { - return (await refreshToken(token) != null); + if ((response?.statusCode == 401) && (token is Auth2Token) && (this.token == token) && + (!(Config().coreUrl?.contains('http://') ?? true) || (response?.request?.url.origin.contains('http://') ?? false))) { + return (await refreshToken(token: token) != null); } return false; } // Getters - Auth2LoginType get oidcLoginType => Auth2LoginType.oidcIllinois; - Auth2LoginType get phoneLoginType => Auth2LoginType.phoneTwilio; - Auth2LoginType get emailLoginType => Auth2LoginType.email; - Auth2LoginType get usernameLoginType => Auth2LoginType.username; - Auth2Token? get token => _token ?? _anonymousToken; Auth2Token? get userToken => _token; Auth2Token? get anonymousToken => _anonymousToken; Auth2Account? get account => _account; String? get deviceId => _deviceId; - + + Auth2Token? get oidcToken => _oidcToken; + + bool get associateAnonymousIds => false; + bool get isAnonymousAuthenticationSupported => Config().supportsAnonymousAuth; String? get accountId => _account?.id ?? _anonymousId; + String? get anonymousId => _anonymousId; Auth2UserPrefs? get prefs => _account?.prefs ?? _anonymousPrefs; Auth2UserProfile? get profile => _account?.profile ?? _anonymousProfile; - Auth2LoginType? get loginType => _account?.authType?.loginType; - - bool get isLoggedIn => (_account?.id != null); - bool get isOidcLoggedIn => (_account?.authType?.loginType == oidcLoginType); - bool get isPhoneLoggedIn => (_account?.authType?.loginType == phoneLoginType); - bool get isEmailLoggedIn => (_account?.authType?.loginType == emailLoginType); - bool get isUsernameLoggedIn => (_account?.authType?.loginType == usernameLoginType); - - bool get isOidcLinked => _account?.isAuthTypeLinked(oidcLoginType) ?? false; - bool get isPhoneLinked => _account?.isAuthTypeLinked(phoneLoginType) ?? false; - bool get isEmailLinked => _account?.isAuthTypeLinked(emailLoginType) ?? false; - bool get isUsernameLinked => _account?.isAuthTypeLinked(usernameLoginType) ?? false; + String? get loginType => _account?.authType?.code; + + bool get isLoggedIn => (_account?.id != null) && _token != null; + bool get isOidcLoggedIn => (_account?.authType?.code == oidcAuthType || _account?.authType?.code == Auth2Type.typeOidc) && _token != null; + bool get isCodeLoggedIn => (_account?.authType?.code == Auth2Type.typeCode) && _token != null; + bool get isPasswordLoggedIn => (_account?.authType?.code == Auth2Type.typePassword) && _token != null; + bool get isPasskeyLoggedIn => (_account?.authType?.code == Auth2Type.typePasskey) && _token != null; + + bool get isEmailLinked => _account?.isIdentifierLinked(Auth2Identifier.typeEmail) ?? false; + bool get isPhoneLinked => _account?.isIdentifierLinked(Auth2Identifier.typePhone) ?? false; + bool get isUsernameLinked => _account?.isIdentifierLinked(Auth2Identifier.typeUsername) ?? false; + + bool get isOidcLinked => _account?.isAuthTypeLinked(oidcAuthType) ?? false; + bool get isCodeLinked => _account?.isAuthTypeLinked(Auth2Type.typeCode) ?? false; + bool get isPasswordLinked => _account?.isAuthTypeLinked(Auth2Type.typePassword) ?? false; + bool get isPasskeyLinked => _account?.isAuthTypeLinked(Auth2Type.typePasskey) ?? false; + + List get linkedEmail => _account?.getLinkedForIdentifierType(Auth2Identifier.typeEmail) ?? []; + List get linkedPhone => _account?.getLinkedForIdentifierType(Auth2Identifier.typePhone) ?? []; + List get linkedUsername => _account?.getLinkedForIdentifierType(Auth2Identifier.typeUsername) ?? []; + List get linkedOidcIdentifiers { + List identifiers = []; + for (Auth2Type oidcType in linkedOidc) { + if (oidcType.id != null) { + identifiers.addAll(_account?.getLinkedForAuthTypeId(oidcType.id!) ?? []); + } + } + return identifiers; + } - List get linkedOidc => _account?.getLinkedForAuthType(oidcLoginType) ?? []; - List get linkedPhone => _account?.getLinkedForAuthType(phoneLoginType) ?? []; - List get linkedEmail => _account?.getLinkedForAuthType(emailLoginType) ?? []; - List get linkedUsername => _account?.getLinkedForAuthType(usernameLoginType) ?? []; + List get linkedOidc => _account?.getLinkedForAuthType(oidcAuthType) ?? []; + List get linkedCode => _account?.getLinkedForAuthType(Auth2Type.typeCode) ?? []; + List get linkedPassword => _account?.getLinkedForAuthType(Auth2Type.typePassword) ?? []; + List get linkedPasskey => _account?.getLinkedForAuthType(Auth2Type.typePasskey) ?? []; bool get hasUin => (0 < (uin?.length ?? 0)); String? get uin => _account?.authType?.uiucUser?.uin; String? get netId => _account?.authType?.uiucUser?.netId; - String? get fullName => StringUtils.ensureNotEmpty(profile?.fullName, defaultValue: _account?.authType?.uiucUser?.fullName ?? ''); - String? get firstName => StringUtils.ensureNotEmpty(profile?.firstName, defaultValue: _account?.authType?.uiucUser?.firstName ?? ''); - String? get email => StringUtils.ensureNotEmpty(profile?.email, defaultValue: _account?.authType?.uiucUser?.email ?? ''); - String? get phone => StringUtils.ensureNotEmpty(profile?.phone, defaultValue: _account?.authType?.phone ?? ''); + String? get fullName => StringUtils.notEmptyString(profile?.fullName, _account?.authType?.uiucUser?.fullName); + String? get firstName => StringUtils.notEmptyString(profile?.firstName, _account?.authType?.uiucUser?.firstName); String? get username => _account?.username; + String? get email => StringUtils.notEmptyString(_account?.authType?.uiucUser?.email, ListUtils.first(emails)); + String? get phone => StringUtils.notEmptyString(ListUtils.first(phones)); + + List get emails { + List emailStrings = []; + for (Auth2Identifier emailIdentifier in linkedEmail) { + if (emailIdentifier.identifier != null) { + emailStrings.add(emailIdentifier.identifier!); + } + } + return emailStrings; + } + + List get phones { + List phoneStrings = []; + for (Auth2Identifier phoneIdentifier in linkedPhone) { + if (phoneIdentifier.identifier != null) { + phoneStrings.add(phoneIdentifier.identifier!); + } + } + return phoneStrings; + } bool get isEventEditor => hasRole("event approvers"); bool get isStadiumPollManager => hasRole("stadium poll manager"); @@ -280,12 +384,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String? get votePlace => prefs?.voter?.votePlace; // Overrides + @protected + String get oidcAuthType => Auth2Type.typeOidcIllinois; @protected - Auth2UserPrefs get defaultAnonimousPrefs => Auth2UserPrefs.empty(); + Auth2UserPrefs get defaultAnonymousPrefs => Auth2UserPrefs.empty(); @protected - Auth2UserProfile get defaultAnonimousProfile => Auth2UserProfile.empty(); + Auth2UserProfile get defaultAnonymousProfile => Auth2UserProfile.empty(); @protected String? get deviceIdIdentifier => _deviceIdIdentifier; @@ -296,29 +402,35 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Anonymous Authentication Future authenticateAnonymously() async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (Config().rokwireApiKey != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + if (Config().supportsAnonymousAuth && (Config().authBaseUrl != null)) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(Auth2LoginType.anonymous), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'device': deviceInfo - }); + Map postData = { + 'auth_type': Auth2Type.typeAnonymous, + 'device': deviceInfo, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return false; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); Map? responseJson = (response?.statusCode == 200) ? JsonUtils.decodeMap(response?.body) : null; if (responseJson != null) { Auth2Token? anonymousToken = Auth2Token.fromJson(JsonUtils.mapValue(responseJson['token'])); Map? params = JsonUtils.mapValue(responseJson['params']); String? anonymousId = (params != null) ? JsonUtils.stringValue(params['anonymous_id']) : null; if ((anonymousToken != null) && anonymousToken.isValid && (anonymousId != null) && anonymousId.isNotEmpty) { - _refreshTonenFailCounts.remove(_anonymousToken?.refreshToken); - Storage().auth2AnonymousId = _anonymousId = anonymousId; - Storage().auth2AnonymousToken = _anonymousToken = anonymousToken; + _refreshTokenFailCounts.remove(_anonymousToken?.refreshToken); + await Future.wait([ + Storage().setAuth2AnonymousId(_anonymousId = anonymousId), + Storage().setAuth2AnonymousToken(_anonymousToken = anonymousToken), + ]); _log("Auth2: anonymous auth succeeded: ${response?.statusCode}\n${response?.body}"); return true; } @@ -328,14 +440,296 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return false; } + // Passkey authentication + + Future authenticateWithPasskey({String? identifier, String identifierType = Auth2Identifier.typeUsername, String? identifierId}) async { + String? errorMessage; + if (Config().authBaseUrl != null) { + if (!await RokwirePlugin.arePasskeysSupported()) { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotSupported); + } + + String url = "${Config().authBaseUrl}/auth/login"; + Map headers = { + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', + }; + Map creds = {}; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } + Map postData = { + 'auth_type': Auth2Type.typePasskey, + 'creds': creds, + 'params': { + 'sign_up': false, + }, + 'username': identifierType == Auth2Identifier.typeUsername ? identifier : null, + 'profile': profile?.toJson(), + 'preferences': _anonymousPrefs?.toJson(), + 'device': deviceInfo, + 'account_identifier_id': identifierId, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed); + } + + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); + if (response != null && response.statusCode == 200) { + // Obtain creationOptions from the server + String? responseBody = response.body; + Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(responseBody)); + try { + String? responseData = await RokwirePlugin.getPasskey(message?.message); + debugPrint(responseData); + return _completeSignInWithPasskey(responseData, identifier: identifier, identifierType: identifierType, identifierId: identifierId); + } catch(error) { + if (error is PlatformException) { + switch (error.code) { + // no credentials found + case "NoCredentialException": return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNoCredentials); + // user cancelled on device auth + case "GetPublicKeyCredentialDomException": return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedCancelled); + // user cancelled on select passkey + case "GetCredentialCancellationException": return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedCancelled); + } + } + errorMessage = error.toString(); + debugPrint(errorMessage); + Log.e(errorMessage); + } + } else { + Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); + if (error?.status == 'unverified') { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotActivated); + } + else if (error?.status == 'not-found') { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotFound); + } + else if (error?.status == 'verification-expired') { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedActivationExpired); + } + } + } + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed, error: errorMessage); + } + + Future _completeSignInWithPasskey(String? responseData, {String? identifier, String identifierType = Auth2Identifier.typeUsername, String? identifierId}) async { + if ((Config().authBaseUrl != null) && (responseData != null)) { + String url = "${Config().authBaseUrl}/auth/login"; + Map? requestJson = JsonUtils.decode(responseData); + // TODO: remove if statement once plugin is fixed + if (Config().operatingSystem == 'ios') { + String? userHandle = requestJson?['response']['userHandle']; + requestJson?['response']['userHandle'] = StringUtils.base64UrlDecode(userHandle ?? ''); + } + Map headers = { + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', + }; + Map creds = { + "response": JsonUtils.encode(requestJson), + }; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } + Map postData = { + 'auth_type': Auth2Type.typePasskey, + 'creds': creds, + 'username': identifierType == Auth2Identifier.typeUsername ? identifier : null, + 'device': deviceInfo, + 'account_identifier_id': identifierId, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed); + } + + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); + if (response != null && response.statusCode == 200) { + Map? responseJson = JsonUtils.decode(response.body); + bool success = await processLoginResponse(responseJson); + if (success) { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.succeeded); + } + } else { + Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); + if (error?.status == 'unverified') { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotActivated); + } + else if (error?.status == 'not-found') { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotFound); + } + else if (error?.status == 'verification-expired') { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedActivationExpired); + } + } + } + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed); + } + + Future signUpWithPasskey(String identifier, {String? displayName, String identifierType = Auth2Identifier.typeUsername, bool? public = false, bool verifyIdentifier = false}) async { + String? errorMessage; + if (Config().authBaseUrl != null) { + if (!await RokwirePlugin.arePasskeysSupported()) { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNotSupported); + } + + Auth2UserProfile? profile = _anonymousProfile; + List? nameParts = displayName?.split(' '); + if (nameParts != null && nameParts.length >= 2) { + Auth2UserProfile nameData = Auth2UserProfile(firstName: nameParts[0], lastName: nameParts.skip(1).join(' ')); + if (profile != null) { + profile.apply(nameData); + } else { + profile = nameData; + } + } + + String url = "${Config().authBaseUrl}/auth/login"; + Map headers = { + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', + }; + Map postData = { + 'auth_type': Auth2Type.typePasskey, + 'creds': { + identifierType: identifier, + }, + 'params': { + "display_name": displayName, + }, + 'privacy': { + 'public': public, + }, + 'username': identifierType == Auth2Identifier.typeUsername ? identifier : null, + 'profile': profile?.toJson(), + 'preferences': _anonymousPrefs?.toJson(), + 'device': deviceInfo, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed); + } + + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); + if (response != null && response.statusCode == 200) { + // Obtain creationOptions from the server + Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(response.body)); + if (message != null) { + if (verifyIdentifier) { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded, creationOptions: message.message); + } + try { + String? responseData = await RokwirePlugin.createPasskey(message.message); + return completeSignUpWithPasskey(identifier, responseData, identifierType: identifierType); + } catch(error) { + try { + String? responseData = await RokwirePlugin.getPasskey(message.message); + Auth2PasskeySignInResult result = await _completeSignInWithPasskey(responseData, identifier: identifier, identifierType: identifierType); + if (result.status == Auth2PasskeySignInResultStatus.succeeded) { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); + } + } catch(error) { + if (error is PlatformException && error.code == "NoCredentialException") { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNoCredentials); + } + + debugPrint(error.toString()); + } + + if (error is PlatformException) { + switch (error.code) { + // user cancelled on device auth + case "GetPublicKeyCredentialDomException": return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedCancelled); + // user cancelled on select passkey + case "GetCredentialCancellationException": return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedCancelled); + } + } + + debugPrint(error.toString()); + } + } else { + Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response.body)); + if (error?.status == 'unverified') { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNotActivated); + } + else if (error?.status == 'verification-expired') { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedActivationExpired); + } + else if (error?.status == 'already-exists') { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedAccountExist); + } + } + } + // else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { + // return Auth2PasskeySignUpResult.failedAccountExist; + // } + } + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed, error: errorMessage); + } + + Future completeSignUpWithPasskey(String identifier, String? responseData, {String identifierType = Auth2Identifier.typeUsername}) async { + if ((Config().authBaseUrl != null) && (responseData != null)) { + String url = "${Config().authBaseUrl}/auth/login"; + Map headers = { + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', + }; + Map postData = { + 'auth_type': Auth2Type.typePasskey, + 'creds': { + identifierType: identifier, + "response": responseData, + }, + 'username': identifierType == Auth2Identifier.typeUsername ? identifier : null, + 'device': deviceInfo, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed); + } + + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); + if (response != null && response.statusCode == 200) { + Map? responseJson = JsonUtils.decode(response.body); + bool success = await processLoginResponse(responseJson); + if (success) { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); + } + } else { + Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); + if (error?.status == 'unverified') { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNotActivated); + } + else if (error?.status == 'not-found') { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNotFound); + } + else if (error?.status == 'verification-expired') { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedActivationExpired); + } + } + } + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed); + } + // OIDC Authentication - Future authenticateWithOidc({ Auth2AccountScope? scope = defaultLoginScope, bool? link}) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { + Future authenticateWithOidc({Auth2AccountScope? scope = defaultLoginScope, bool? link}) async { + if (Config().authBaseUrl != null) { if (_oidcAuthenticationCompleters == null) { _oidcAuthenticationCompleters = >[]; - NotificationService().notify(notifyLoginStarted, oidcLoginType); + NotificationService().notify(notifyLoginStarted, oidcAuthType); _OidcLogin? oidcLogin = await getOidcData(); if (oidcLogin?.loginUrl != null) { @@ -345,35 +739,50 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { await _launchUrl(_oidcLogin?.loginUrl); } else { - completeOidcAuthentication(Auth2OidcAuthenticateResult.failed); - return Auth2OidcAuthenticateResult.failed; + Auth2OidcAuthenticateResult result = Auth2OidcAuthenticateResult( + Auth2OidcAuthenticateResultStatus.failed, + error: "error getting login url: ${oidcLogin?.error}" + ); + completeOidcAuthentication(result); + return result; } } + _oidcLoginInProgress = true; Completer completer = Completer(); _oidcAuthenticationCompleters!.add(completer); return completer.future; } - return Auth2OidcAuthenticateResult.failed; + return Auth2OidcAuthenticateResult(Auth2OidcAuthenticateResultStatus.failed, + error: "auth url is null"); } @protected - Future handleOidcAuthentication(Uri uri) async { + Future handleOidcAuthentication(Uri uri) async { RokwirePlugin.dismissSafariVC(); - + + if (!_oidcLoginInProgress) { + NotificationService().notify(notifyLoginError, 'no login in progress'); + return null; + } + _oidcLoginInProgress = false; + cancelOidcAuthenticationTimer(); _processingOidcAuthentication = true; Auth2OidcAuthenticateResult result; if (_oidcLink == true) { - Auth2LinkResult linkResult = await linkAccountAuthType(oidcLoginType, uri.toString(), _oidcLogin?.params); + Auth2LinkResult linkResult = await linkAccountAuthType(oidcAuthType, uri.toString(), _oidcLogin?.params); result = auth2OidcAuthenticateResultFromAuth2LinkResult(linkResult); } else { - bool processResult = await processOidcAuthentication(uri); - result = processResult ? Auth2OidcAuthenticateResult.succeeded : Auth2OidcAuthenticateResult.failed; + String? processResult = await processOidcAuthentication(uri); + result = processResult == null ? + Auth2OidcAuthenticateResult(Auth2OidcAuthenticateResultStatus.succeeded) + : Auth2OidcAuthenticateResult( + Auth2OidcAuthenticateResultStatus.failed, error: processResult); } _processingOidcAuthentication = false; @@ -382,38 +791,48 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } @protected - Future processOidcAuthentication(Uri? uri) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + Future processOidcAuthentication(Uri? uri) async { + if (Config().authBaseUrl != null) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(oidcLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, + Map postData = { + 'auth_type': oidcAuthType, 'creds': uri?.toString(), 'params': _oidcLogin?.params, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return 'could not get config params'; + } _oidcLogin = null; - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); Log.d("Login: ${response?.statusCode}, ${response?.body}", lineLength: 512); Map? responseJson = (response?.statusCode == 200) ? JsonUtils.decodeMap(response?.body) : null; bool result = await processLoginResponse(responseJson, scope: _oidcScope); _oidcScope = null; _log(result ? "Auth2: login succeeded: ${response?.statusCode}\n${response?.body}" : "Auth2: login failed: ${response?.statusCode}\n${response?.body}"); - return result; + if (result) { + return null; + } + if (response?.statusCode != 200) { + return '${response?.statusCode} - ${response?.body}'; + } + return 'invalid token or account response'; } - return false; + return 'auth url is null'; } @protected - Future processLoginResponse(Map? responseJson, { Auth2AccountScope? scope }) async { + Future processLoginResponse(Map? responseJson, { Auth2AccountScope? scope = defaultLoginScope}) async { if (responseJson != null) { Auth2Token? token = Auth2Token.fromJson(JsonUtils.mapValue(responseJson['token'])); Auth2Account? account = Auth2Account.fromJson(JsonUtils.mapValue(responseJson['account']), @@ -429,16 +848,33 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } @protected - Future applyLogin(Auth2Account account, Auth2Token token, { Auth2AccountScope? scope, Map? params }) async { + Future applyLogin(Auth2Account account, Auth2Token token, { Auth2AccountScope? scope = defaultLoginScope, Map? params }) async { + Auth2Token? oidcToken = (params != null) ? Auth2Token.fromJson(JsonUtils.mapValue(params['oidc_token'])) : null; - _refreshTonenFailCounts.remove(_token?.refreshToken); + _refreshTokenFailCounts.remove(_token?.refreshToken); + if (associateAnonymousIds) { + _anonymousPrefs?.addAnonymousId(_anonymousId); + } bool? prefsUpdated = account.prefs?.apply(_anonymousPrefs, scope: scope?.prefs); bool? profileUpdated = account.profile?.apply(_anonymousProfile, scope: scope?.profile); - Storage().auth2Token = _token = token; - Storage().auth2Account = _account = account; - Storage().auth2AnonymousPrefs = _anonymousPrefs = null; - Storage().auth2AnonymousProfile = _anonymousProfile = null; + _token = token; + _oidcToken = oidcToken; + _account = account; + + List> futures = [ + Storage().setAuth2AnonymousPrefs(_anonymousPrefs = null), + Storage().setAuth2AnonymousProfile(_anonymousProfile = null), + ]; + + if (!kIsWeb) { + futures.addAll([ + Storage().setAuth2Token(token), + Storage().setAuth2OidcToken(oidcToken), + Storage().setAuth2Account(account), + ]); + } + await Future.wait(futures); if (prefsUpdated == true) { _saveAccountUserPrefs(); @@ -455,23 +891,30 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Future<_OidcLogin?> getOidcData() async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { + if (Config().authBaseUrl != null) { - String url = "${Config().coreUrl}/services/auth/login-url"; + String url = "${Config().authBaseUrl}/auth/login-url"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(oidcLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'redirect_uri': oidcRedirectUrl, - }); - Response? response = await Network().post(url, headers: headers, body: post); - return _OidcLogin.fromJson(JsonUtils.decodeMap(response?.body)); + Map postData = { + 'auth_type': oidcAuthType, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + postData['redirect_uri'] = oidcRedirectUrl; + } else { + return _OidcLogin(error: 'config params are null'); + } + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); + if (response?.statusCode == 200) { + return _OidcLogin.fromJson(JsonUtils.decodeMap(response?.body)); + } else { + return _OidcLogin(error: '${response?.statusCode} - ${response?.body}'); + } } - return null; + return _OidcLogin(error: 'auth url is null');; } @protected @@ -480,7 +923,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (_oidcAuthenticationTimer != null) { _oidcAuthenticationTimer!.cancel(); } - _oidcAuthenticationTimer = Timer(const Duration(milliseconds: 100), () { + _oidcAuthenticationTimer = Timer(Duration(milliseconds: Config().oidcAuthenticationTimeout), () { + NotificationService().notify(notifyLoginError, 'oidc login timeout'); completeOidcAuthentication(null); _oidcAuthenticationTimer = null; }); @@ -498,10 +942,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected void completeOidcAuthentication(Auth2OidcAuthenticateResult? result) { - _notifyLogin(oidcLoginType, result == Auth2OidcAuthenticateResult.succeeded); + _notifyLogin(oidcAuthType, result?.status == Auth2OidcAuthenticateResultStatus.succeeded); - _oidcLogin = null; - _oidcScope = null; + // _oidcLogin = null; + // _oidcScope = null; _oidcLink = null; if (_oidcAuthenticationCompleters != null) { @@ -514,408 +958,453 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - // Phone Authentication + // Code Authentication - Future authenticateWithPhone(String? phoneNumber) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (phoneNumber != null)) { - NotificationService().notify(notifyLoginStarted, phoneLoginType); + Future authenticateWithCode(String? identifier, {String identifierType = Auth2Identifier.typePhone, bool? public = false, String? identifierId}) async { + if ((Config().authBaseUrl != null) && (identifier != null || identifierId != null)) { + NotificationService().notify(notifyLoginStarted, Auth2Type.typeCode); - String url = "${Config().coreUrl}/services/auth/login"; + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(phoneLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'creds': { - "phone": phoneNumber, + Map creds = {}; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } + Map postData = { + 'auth_type': Auth2Type.typeCode, + 'creds': creds, + 'privacy': { + 'public': public, }, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + 'account_identifier_id': identifierId, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2RequestCodeResult.failed; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { - return Auth2PhoneRequestCodeResult.succeeded; + return Auth2RequestCodeResult.succeeded; } else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { - return Auth2PhoneRequestCodeResult.failedAccountExist; + return Auth2RequestCodeResult.failedAccountExist; } } - return Auth2PhoneRequestCodeResult.failed; + return Auth2RequestCodeResult.failed; } - Future handlePhoneAuthentication(String? phoneNumber, String? code, { Auth2AccountScope? scope = defaultLoginScope }) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (phoneNumber != null) && (code != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + Future handleCodeAuthentication(String? identifier, String? code, {String identifierType = Auth2Identifier.typePhone, String? identifierId, Auth2AccountScope? scope = defaultLoginScope}) async { + if ((Config().authBaseUrl != null) && (identifier != null || identifierId != null) && (code != null)) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(phoneLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'creds': { - "phone": phoneNumber, - "code": code, - }, + Map creds = { + "code": code, + }; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } + Map postData = { + 'auth_type': Auth2Type.typeCode, + 'creds': creds, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + 'account_identifier_id': identifierId, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2SendCodeResult.failed; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { bool result = await processLoginResponse(JsonUtils.decodeMap(response?.body), scope: scope); - _notifyLogin(phoneLoginType, result); - return result ? Auth2PhoneSendCodeResult.succeeded : Auth2PhoneSendCodeResult.failed; + _notifyLogin(Auth2Type.typeCode, result); + return result ? Auth2SendCodeResult.succeeded : Auth2SendCodeResult.failed; } else { - _notifyLogin(phoneLoginType, false); + _notifyLogin(Auth2Type.typeCode, false); Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); if (error?.status == 'invalid') { - return Auth2PhoneSendCodeResult.failedInvalid; + return Auth2SendCodeResult.failedInvalid; } } } - return Auth2PhoneSendCodeResult.failed; + return Auth2SendCodeResult.failed; } - // Email Authentication + // Password Authentication - Future authenticateWithEmail(String? email, String? password, { Auth2AccountScope? scope = defaultLoginScope }) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (email != null) && (password != null)) { + Future authenticateWithPassword(String? identifier, String? password, {String identifierType = Auth2Identifier.typeEmail, String? identifierId, Auth2AccountScope? scope = defaultLoginScope}) async { + if ((Config().authBaseUrl != null) && (identifier != null || identifierId != null) && (password != null)) { - NotificationService().notify(notifyLoginStarted, emailLoginType); + NotificationService().notify(notifyLoginStarted, Auth2Type.typePassword); - String url = "${Config().coreUrl}/services/auth/login"; + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'creds': { - "email": email, - "password": password - }, + Map creds = { + "password": password, + }; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } + Map postData = { + 'auth_type': Auth2Type.typePassword, + 'creds': creds, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + 'account_identifier_id': identifierId, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasswordSignInResult.failed; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { bool result = await processLoginResponse(JsonUtils.decodeMap(response?.body), scope: scope); - _notifyLogin(emailLoginType, result); - return result ? Auth2EmailSignInResult.succeeded : Auth2EmailSignInResult.failed; + _notifyLogin(Auth2Type.typePassword, result); + return result ? Auth2PasswordSignInResult.succeeded : Auth2PasswordSignInResult.failed; } else { - _notifyLogin(emailLoginType, false); + _notifyLogin(Auth2Type.typePassword, false); Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); if (error?.status == 'unverified') { - return Auth2EmailSignInResult.failedNotActivated; + return Auth2PasswordSignInResult.failedNotActivated; + } + else if (error?.status == 'not-found') { + return Auth2PasswordSignInResult.failedNotFound; } else if (error?.status == 'verification-expired') { - return Auth2EmailSignInResult.failedActivationExpired; + return Auth2PasswordSignInResult.failedActivationExpired; } else if (error?.status == 'invalid') { - return Auth2EmailSignInResult.failedInvalid; + return Auth2PasswordSignInResult.failedInvalid; } } } - return Auth2EmailSignInResult.failed; + return Auth2PasswordSignInResult.failed; } - Future signUpWithEmail(String? email, String? password) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (email != null) && (password != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + Future signUpWithPassword(String? identifier, String? password, {String identifierType = Auth2Identifier.typeEmail, bool? public = false}) async { + if ((Config().authBaseUrl != null) && (identifier != null) && (password != null)) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, + Map postData = { + 'auth_type': Auth2Type.typePassword, 'creds': { - "email": email, + identifierType: identifier, "password": password }, 'params': { "sign_up": true, "confirm_password": password }, + 'privacy': { + 'public': public, + }, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasswordSignUpResult.failed; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { - return Auth2EmailSignUpResult.succeeded; + return Auth2PasswordSignUpResult.succeeded; } else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { - return Auth2EmailSignUpResult.failedAccountExist; + return Auth2PasswordSignUpResult.failedAccountExist; } } - return Auth2EmailSignUpResult.failed; + return Auth2PasswordSignUpResult.failed; } - Future checkEmailAccountState(String? email) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (email != null)) { - String url = "${Config().coreUrl}/services/auth/account/exists"; + Future checkAccountState(String? identifier, {String identifierType = Auth2Identifier.typeEmail}) async { + if ((Config().authBaseUrl != null) && (identifier != null)) { + String url = "${Config().authBaseUrl}/auth/account/exists"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'user_identifier': email, - }); + Map postData = { + 'identifier': { + identifierType: identifier, + } + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return null; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { - //TBD: handle Auth2EmailAccountState.unverified - return JsonUtils.boolValue(JsonUtils.decode(response?.body))! ? Auth2EmailAccountState.verified : Auth2EmailAccountState.nonExistent; + //TBD: handle Auth2AccountState.unverified + return JsonUtils.boolValue(JsonUtils.decode(response?.body))! ? Auth2AccountState.verified : Auth2AccountState.nonExistent; } } return null; } - Future resetEmailPassword(String? email) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (email != null)) { - String url = "${Config().coreUrl}/services/auth/credential/forgot/initiate"; + Future resetPassword(String? identifier, {String identifierType = Auth2Identifier.typeEmail}) async { + if ((Config().authBaseUrl != null) && (identifier != null)) { + String url = "${Config().authBaseUrl}/auth/credential/forgot/initiate"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'user_identifier': email, - 'identifier': email, - }); + Map postData = { + 'auth_type': Auth2Type.typePassword, + 'identifier': { + identifierType: identifier, + }, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2ForgotPasswordResult.failed; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { - return Auth2EmailForgotPasswordResult.succeeded; + return Auth2ForgotPasswordResult.succeeded; } else { Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); if (error?.status == 'verification-expired') { - return Auth2EmailForgotPasswordResult.failedActivationExpired; + return Auth2ForgotPasswordResult.failedActivationExpired; } else if (error?.status == 'unverified') { - return Auth2EmailForgotPasswordResult.failedNotActivated; + return Auth2ForgotPasswordResult.failedNotActivated; } } } - return Auth2EmailForgotPasswordResult.failed; + return Auth2ForgotPasswordResult.failed; } - Future resentActivationEmail(String? email) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (email != null)) { - String url = "${Config().coreUrl}/services/auth/credential/send-verify"; + Future resendIdentifierVerification(String? identifier, {String identifierType = Auth2Identifier.typeEmail}) async { + if ((Config().authBaseUrl != null) && (identifier != null)) { + String url = "${Config().authBaseUrl}/auth/identifier/send-verify"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'user_identifier': email, - 'identifier': email, - }); + Map postData = { + 'identifier': { + identifierType: identifier, + }, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return false; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); return (response?.statusCode == 200); } return false; } - // Username Authentication + // Notify Login - Future authenticateWithUsername(String? username, String? password, { Auth2AccountScope? scope = defaultLoginScope }) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null) && (password != null)) { + void _notifyLogin(String loginType, bool? result) { + if (result != null) { + NotificationService().notify(result ? notifyLoginSucceeded : notifyLoginFailed, loginType); + NotificationService().notify(notifyLoginFinished, loginType); + } + } - NotificationService().notify(notifyLoginStarted, usernameLoginType); + // Account Checks - String url = "${Config().coreUrl}/services/auth/login"; + Future canSignIn(String? identifier, String identifierType) async { + if ((Config().authBaseUrl != null) && (identifier != null)) { + String url = "${Config().authBaseUrl}/auth/account/can-sign-in"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'creds': { - "username": username, - "password": password - }, - 'params': { - "sign_up": false, + Map postData = { + 'identifier': { + identifierType: identifier, }, - 'profile': _anonymousProfile?.toJson(), - 'preferences': _anonymousPrefs?.toJson(), - 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return null; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { - bool result = await processLoginResponse(JsonUtils.decodeMap(response?.body), scope: scope); - _notifyLogin(usernameLoginType, result); - return result ? Auth2UsernameSignInResult.succeeded : Auth2UsernameSignInResult.failed; - } - else { - _notifyLogin(usernameLoginType, false); - Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); - if (error?.status == 'not-found') { - return Auth2UsernameSignInResult.failedNotFound; - } else if (error?.status == 'invalid') { - return Auth2UsernameSignInResult.failedInvalid; - } + return JsonUtils.boolValue(JsonUtils.decode(response?.body))!; } } - return Auth2UsernameSignInResult.failed; + return null; } - Future signUpWithUsername(String? username, String? password, { Auth2AccountScope? scope = defaultLoginScope }) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null) && (password != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + Future canLink(String? identifier, String identifierType) async { + if ((Config().authBaseUrl != null) && (identifier != null)) { + String url = "${Config().authBaseUrl}/auth/account/can-link"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'creds': { - "username": username, - "password": password + Map postData = { + 'identifier': { + identifierType: identifier, }, - 'params': { - "sign_up": true, - "confirm_password": password - }, - 'profile': _anonymousProfile?.toJson(), - 'preferences': _anonymousPrefs?.toJson(), - 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return null; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { - bool result = await processLoginResponse(JsonUtils.decodeMap(response?.body), scope: scope); - _notifyLogin(usernameLoginType, result); - return result ? Auth2UsernameSignUpResult.succeeded : Auth2UsernameSignUpResult.failed; - } - else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { - return Auth2UsernameSignUpResult.failedAccountExist; + return JsonUtils.boolValue(JsonUtils.decode(response?.body))!; } } - return Auth2UsernameSignUpResult.failed; + return null; } - Future checkUsernameAccountState(String? username) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { - String url = "${Config().coreUrl}/services/auth/account/exists"; + // Sign in options + + Future signInOptions(String? identifier, String identifierType) async { + if ((Config().authBaseUrl != null) && (identifier != null)) { + String url = "${Config().authBaseUrl}/auth/account/sign-in-options"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'user_identifier': username, - }); + Map postData = { + 'identifier': { + identifierType: identifier, + }, + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return null; + } - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response?.statusCode == 200) { - //TBD: handle Auth2EmailAccountState.unverified - return JsonUtils.boolValue(JsonUtils.decode(response?.body))! ? Auth2UsernameAccountState.exists : Auth2UsernameAccountState.nonExistent; + Map? responseJson = JsonUtils.decodeMap(response?.body); + List? identifiers = (responseJson != null) ? Auth2Identifier.listFromJson(JsonUtils.listValue(responseJson['identifiers'])) : null; + List? authTypes = (responseJson != null) ? Auth2Type.listFromJson(JsonUtils.listValue(responseJson['auth_types'])) : null; + return Auth2SignInOptionsResult(identifierOptions: identifiers, authTypeOptions: authTypes); } } return null; } - // Notify Login - - void _notifyLogin(Auth2LoginType loginType, bool? result) { - if (result != null) { - NotificationService().notify(result ? notifyLoginSucceeded : notifyLoginFailed, loginType); - NotificationService().notify(notifyLoginFinished, loginType); - } - } + // Account Identifier Linking - // Account Checks - - Future canSignIn(String? identifier, Auth2LoginType loginType) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (identifier != null)) { - String url = "${Config().coreUrl}/services/auth/account/can-sign-in"; + Future linkAccountIdentifier(String? identifier, String identifierType) async { + if ((Config().coreUrl != null) && (identifier != null)) { + String url = "${Config().coreUrl}/services/auth/account/identifier/link"; Map headers = { 'Content-Type': 'application/json' }; String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(loginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'user_identifier': identifier, + 'identifier': { + identifierType: identifier, + }, }); - Response? response = await Network().post(url, headers: headers, body: post); + Response? response = await Network().post(url, headers: headers, body: post, auth: Auth2()); if (response?.statusCode == 200) { - return JsonUtils.boolValue(JsonUtils.decode(response?.body))!; + Map? responseJson = JsonUtils.decodeMap(response?.body); + List? identifiers = (responseJson != null) ? Auth2Identifier.listFromJson(JsonUtils.listValue(responseJson['identifiers'])) : null; + String? message = (responseJson != null) ? JsonUtils.stringValue(responseJson['message']) : null; + if (identifiers != null) { + await Storage().setAuth2Account(_account = Auth2Account.fromOther(_account, identifiers: identifiers)); + NotificationService().notify(notifyLinkChanged); + return Auth2LinkResult(Auth2LinkResultStatus.succeeded, message: message); + } + } + else { + Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); + if (error?.status == 'verification-expired') { + return Auth2LinkResult(Auth2LinkResultStatus.failedActivationExpired); + } + else if (error?.status == 'unverified') { + return Auth2LinkResult(Auth2LinkResultStatus.failedNotActivated); + } + else if (error?.status == 'already-exists') { + return Auth2LinkResult(Auth2LinkResultStatus.failedAccountExist); + } + else if (error?.status == 'invalid') { + return Auth2LinkResult(Auth2LinkResultStatus.failedInvalid); + } } } - return null; + return Auth2LinkResult(Auth2LinkResultStatus.failed); } - Future canLink(String? identifier, Auth2LoginType loginType) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (identifier != null)) { - String url = "${Config().coreUrl}/services/auth/account/can-link"; + Future unlinkAccountIdentifier(String? id) async { + if ((Config().coreUrl != null) && (id != null)) { + String url = "${Config().coreUrl}/services/auth/account/identifier/link"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(loginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'user_identifier': identifier, + String? body = JsonUtils.encode({ + 'id': id }); - Response? response = await Network().post(url, headers: headers, body: post); - if (response?.statusCode == 200) { - return JsonUtils.boolValue(JsonUtils.decode(response?.body))!; + Response? response = await Network().delete(url, headers: headers, body: body, auth: Auth2()); + Map? responseJson = (response?.statusCode == 200) ? JsonUtils.decodeMap(response?.body) : null; + List? identifiers = (responseJson != null) ? Auth2Identifier.listFromJson(JsonUtils.listValue(responseJson['identifiers'])) : null; + if (identifiers != null) { + await Storage().setAuth2Account(_account = Auth2Account.fromOther(_account, identifiers: identifiers)); + NotificationService().notify(notifyLinkChanged); + return true; } } - return null; + return false; } - // Account Linking + // Account Auth Type Linking - Future linkAccountAuthType(Auth2LoginType? loginType, dynamic creds, Map? params) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (loginType != null)) { + Future linkAccountAuthType(String? loginType, dynamic creds, Map? params) async { + if ((Config().coreUrl != null) && (loginType != null)) { String url = "${Config().coreUrl}/services/auth/account/auth-type/link"; Map headers = { 'Content-Type': 'application/json' }; String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(loginType), + 'auth_type': loginType, 'app_type_identifier': Config().appPlatformId, 'creds': creds, 'params': params, @@ -925,49 +1414,51 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Response? response = await Network().post(url, headers: headers, body: post, auth: Auth2()); if (response?.statusCode == 200) { Map? responseJson = JsonUtils.decodeMap(response?.body); + List? identifiers = (responseJson != null) ? Auth2Identifier.listFromJson(JsonUtils.listValue(responseJson['identifiers'])) : null; List? authTypes = (responseJson != null) ? Auth2Type.listFromJson(JsonUtils.listValue(responseJson['auth_types'])) : null; + String? message = (responseJson != null) ? JsonUtils.stringValue(responseJson['message']) : null; + // Map? requestJson = JsonUtils.decode(message ?? ''); if (authTypes != null) { - Storage().auth2Account = _account = Auth2Account.fromOther(_account, authTypes: authTypes); + await Storage().setAuth2Account(_account = Auth2Account.fromOther(_account, identifiers: identifiers, authTypes: authTypes)); NotificationService().notify(notifyLinkChanged); - return Auth2LinkResult.succeeded; + return Auth2LinkResult(Auth2LinkResultStatus.succeeded, message: message); } } else { Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); if (error?.status == 'verification-expired') { - return Auth2LinkResult.failedActivationExpired; + return Auth2LinkResult(Auth2LinkResultStatus.failedActivationExpired); } else if (error?.status == 'unverified') { - return Auth2LinkResult.failedNotActivated; + return Auth2LinkResult(Auth2LinkResultStatus.failedNotActivated); } else if (error?.status == 'already-exists') { - return Auth2LinkResult.failedAccountExist; + return Auth2LinkResult(Auth2LinkResultStatus.failedAccountExist); } else if (error?.status == 'invalid') { - return Auth2LinkResult.failedInvalid; + return Auth2LinkResult(Auth2LinkResultStatus.failedInvalid); } } } - return Auth2LinkResult.failed; + return Auth2LinkResult(Auth2LinkResultStatus.failed); } - Future unlinkAccountAuthType(Auth2LoginType? loginType, String identifier) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (loginType != null)) { + Future unlinkAccountAuthType(String? id) async { + if ((Config().coreUrl != null) && (id != null)) { String url = "${Config().coreUrl}/services/auth/account/auth-type/link"; Map headers = { 'Content-Type': 'application/json' }; String? body = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(loginType), - 'app_type_identifier': Config().appPlatformId, - 'identifier': identifier, + 'id': id }); Response? response = await Network().delete(url, headers: headers, body: body, auth: Auth2()); Map? responseJson = (response?.statusCode == 200) ? JsonUtils.decodeMap(response?.body) : null; + List? identifiers = (responseJson != null) ? Auth2Identifier.listFromJson(JsonUtils.listValue(responseJson['identifiers'])) : null; List? authTypes = (responseJson != null) ? Auth2Type.listFromJson(JsonUtils.listValue(responseJson['auth_types'])) : null; if (authTypes != null) { - Storage().auth2Account = _account = Auth2Account.fromOther(_account, authTypes: authTypes); + await Storage().setAuth2Account(_account = Auth2Account.fromOther(_account, identifiers: identifiers, authTypes: authTypes)); NotificationService().notify(notifyLinkChanged); return true; } @@ -980,41 +1471,53 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Map get deviceInfo { return { - 'type': "mobile", - 'device_id': _deviceId, - 'os': Platform.operatingSystem, + 'type': kIsWeb ? 'web' : 'mobile', + 'device_id': kIsWeb ? 'web' : _deviceId, + 'os': Config().operatingSystem, }; } // Logout - void logout({ Auth2UserPrefs? prefs }) { - if (_token != null) { - _log("Auth2: logout"); - _refreshTonenFailCounts.remove(_token?.refreshToken); + Future logout({ Auth2UserPrefs? prefs }) async { + NotificationService().notify(notifyLogoutStarted); + _log("Auth2: logout"); + _refreshTokenFailCounts.remove(_token?.refreshToken); + + if (Config().authBaseUrl != null) { + Map headers = { + 'Content-Type': 'application/json' + }; + String? body = JsonUtils.encode({ + 'all_sessions': false, + }); + await Network().post("${Config().authBaseUrl}/auth/logout", headers: headers, body: body, auth: Auth2Csrf(token: token)); + } - Storage().auth2AnonymousPrefs = _anonymousPrefs = prefs ?? _account?.prefs ?? Auth2UserPrefs.empty(); - Storage().auth2AnonymousProfile = _anonymousProfile = Auth2UserProfile.empty(); - Storage().auth2Token = _token = null; - Storage().auth2Account = _account = null; + await Future.wait([ + Storage().setAuth2AnonymousPrefs(_anonymousPrefs = prefs ?? _account?.prefs ?? Auth2UserPrefs.empty()), + Storage().setAuth2AnonymousProfile(_anonymousProfile = Auth2UserProfile.empty()), + Storage().setAuth2Token(_token = null), + Storage().setAuth2OidcToken(_oidcToken = null), + Storage().setAuth2Account(_account = null), + ]); - _updateUserPrefsTimer?.cancel(); - _updateUserPrefsTimer = null; + _updateUserPrefsTimer?.cancel(); + _updateUserPrefsTimer = null; - _updateUserPrefsClient?.close(); - _updateUserPrefsClient = null; + _updateUserPrefsClient?.close(); + _updateUserPrefsClient = null; - _updateUserProfileTimer?.cancel(); - _updateUserProfileTimer = null; + _updateUserProfileTimer?.cancel(); + _updateUserProfileTimer = null; - _updateUserProfileClient?.close(); - _updateUserProfileClient = null; + _updateUserProfileClient?.close(); + _updateUserProfileClient = null; - NotificationService().notify(notifyProfileChanged); - NotificationService().notify(notifyPrefsChanged); - NotificationService().notify(notifyLoginChanged); - NotificationService().notify(notifyLogout); - } + NotificationService().notify(notifyProfileChanged); + NotificationService().notify(notifyPrefsChanged); + NotificationService().notify(notifyLoginChanged); + NotificationService().notify(notifyLogout); } // Delete @@ -1040,47 +1543,67 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Refresh - Future refreshToken(Auth2Token token) async { - if ((Config().coreUrl != null) && (token.refreshToken != null)) { + Future refreshToken({Auth2Token? token, bool ignoreUnauthorized = false}) async { + //TODO: validate that using CSRF token as futures and fail counts key works on web + NotificationService().notify(notifyRefreshStarted); + String futureKey = token?.refreshToken ?? WebUtils.getCookie(Auth2Csrf.csrfTokenName); + if (Config().authBaseUrl != null) { try { - Future? refreshTokenFuture = _refreshTokenFutures[token.refreshToken]; + Future? refreshTokenFuture = futureKey.isNotEmpty ? _refreshTokenFutures[futureKey] : null; if (refreshTokenFuture != null) { - _log("Auth2: will await refresh token:\nSource Token: ${token.refreshToken}"); + _log("Auth2: will await refresh token:\nSource Token: ${token?.refreshToken}"); Response? response = await refreshTokenFuture; Map? responseJson = (response?.statusCode == 200) ? JsonUtils.decodeMap(response?.body) : null; Auth2Token? responseToken = (responseJson != null) ? Auth2Token.fromJson(JsonUtils.mapValue(responseJson['token'])) : null; - _log("Auth2: did await refresh token: ${responseToken?.isValid}\nSource Token: ${token.refreshToken}"); - return ((responseToken != null) && responseToken.isValid) ? responseToken : null; + _log("Auth2: did await refresh token: ${responseToken?.isValid}\nSource Token: ${token?.refreshToken}"); + NotificationService().notify(notifyRefreshFinished); + if ((responseToken != null) && responseToken.isValid) { + NotificationService().notify(notifyRefreshSucceeded); + return responseToken; + } + NotificationService().notify(notifyRefreshError, responseToken?.isValid); + return null; } else { - _log("Auth2: will refresh token:\nSource Token: ${token.refreshToken}"); + _log("Auth2: will refresh token:\nSource Token: ${token?.refreshToken}"); - _refreshTokenFutures[token.refreshToken!] = refreshTokenFuture = _refreshToken(token.refreshToken); + refreshTokenFuture = _refreshToken(token?.refreshToken); + if (futureKey.isNotEmpty) { + _refreshTokenFutures[futureKey] = refreshTokenFuture; + } Response? response = await refreshTokenFuture; - _refreshTokenFutures.remove(token.refreshToken); + _refreshTokenFutures.remove(futureKey); Map? responseJson = (response?.statusCode == 200) ? JsonUtils.decodeMap(response?.body) : null; if (responseJson != null) { Auth2Token? responseToken = Auth2Token.fromJson(JsonUtils.mapValue(responseJson['token'])); if ((responseToken != null) && responseToken.isValid) { - _log("Auth2: did refresh token:\nResponse Token: ${responseToken.refreshToken}\nSource Token: ${token.refreshToken}"); - _refreshTonenFailCounts.remove(token.refreshToken); + _log("Auth2: did refresh token:\nResponse Token: ${responseToken.refreshToken}\nSource Token: ${token?.refreshToken}"); + _refreshTokenFailCounts.remove(futureKey); if (token == _token) { - applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); + await applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); + NotificationService().notify(notifyRefreshSucceeded); + NotificationService().notify(notifyRefreshFinished); return responseToken; } else if (token == _anonymousToken) { - Storage().auth2AnonymousToken = _anonymousToken = responseToken; + await Storage().setAuth2AnonymousToken(_anonymousToken = responseToken); + NotificationService().notify(notifyRefreshSucceeded); + NotificationService().notify(notifyRefreshFinished); return responseToken; } } } - _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token.refreshToken}"); - int refreshTonenFailCount = (_refreshTonenFailCounts[token.refreshToken] ?? 0) + 1; - if (((response?.statusCode == 400) || (response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTonenFailCount)) { + _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token?.refreshToken}"); + NotificationService().notify(notifyRefreshError, '${response?.statusCode} - ${response?.body}'); + int refreshTokenFailCount = 1; + if (futureKey.isNotEmpty) { + refreshTokenFailCount += _refreshTokenFailCounts[futureKey] ?? 0; + } + if (((response?.statusCode == 400) || (!ignoreUnauthorized && response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTokenFailCount)) { if (token == _token) { logout(); } @@ -1088,39 +1611,57 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { await authenticateAnonymously(); } } - else { - _refreshTonenFailCounts[token.refreshToken!] = refreshTonenFailCount; + else if (futureKey.isNotEmpty) { + _refreshTokenFailCounts[futureKey] = refreshTokenFailCount; } } } catch(e) { debugPrint(e.toString()); - _refreshTokenFutures.remove(token.refreshToken); // make sure to clear this in case something went wrong. + NotificationService().notify(notifyRefreshError, e); + _refreshTokenFutures.remove(futureKey); // make sure to clear this in case something went wrong. } } + NotificationService().notify(notifyRefreshFinished); return null; } @protected - void applyToken(Auth2Token token, { Map? params }) { - Storage().auth2Token = _token = token; + Future applyToken(Auth2Token token, { Map? params }) async { + Auth2Token? oidcToken = (params != null) ? Auth2Token.fromJson(JsonUtils.mapValue(params['oidc_token'])) : null; + + _token = token; + _oidcToken = oidcToken; + if (!kIsWeb) { + await Future.wait([ + Storage().setAuth2Token(token), + Storage().setAuth2OidcToken(oidcToken), + ]); + } } - static Future _refreshToken(String? refreshToken) async { - if ((Config().coreUrl != null) && (refreshToken != null)) { - String url = "${Config().coreUrl}/services/auth/refresh"; + Future _refreshToken(String? refreshToken) { + if (Config().authBaseUrl != null) { + String url = "${Config().authBaseUrl}/auth/refresh"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; - String? post = JsonUtils.encode({ - 'api_key': Config().rokwireApiKey, - 'refresh_token': refreshToken - }); + String? post; + if (!Config().isReleaseWeb) { + if (refreshToken == null) { + return Future.value(null); + } + post = JsonUtils.encode({ + 'api_key': Config().rokwireApiKey, + 'refresh_token': refreshToken + }); + } - return Network().post(url, headers: headers, body: post); + return Network().post(url, headers: headers, body: post, auth: Auth2Csrf()); } - return null; + return Future.value(null); } // User Prefs @@ -1128,11 +1669,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Future onUserPrefsChanged(Auth2UserPrefs? prefs) async { if (identical(prefs, _anonymousPrefs)) { - Storage().auth2AnonymousPrefs = _anonymousPrefs; + await Storage().setAuth2AnonymousPrefs(_anonymousPrefs); NotificationService().notify(notifyPrefsChanged); } else if (identical(prefs, _account?.prefs)) { - Storage().auth2Account = _account; + await Storage().setAuth2Account(_account); NotificationService().notify(notifyPrefsChanged); return _saveAccountUserPrefs(); } @@ -1170,6 +1711,49 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } + // Account Secrets + + @protected + Future onAccountSecretsChanged(Map? secrets) async { + if (identical(secrets, _account?.secrets)) { + await Storage().setAuth2Account(_account); + NotificationService().notify(notifySecretsChanged); + return _saveAccountSecrets(); + } + return; + } + + Future _saveAccountSecrets() async { + if ((Config().coreUrl != null) && (_token?.accessToken != null) && (_account?.secrets != null)) { + String url = "${Config().coreUrl}/services/account/secrets"; + Map headers = { + 'Content-Type': 'application/json' + }; + String? post = JsonUtils.encode(_account!.secrets); + + Client client = Client(); + _updateUserSecretsClient?.close(); + _updateUserSecretsClient = client; + + Response? response = await Network().put(url, auth: Auth2(), headers: headers, body: post, client: _updateUserSecretsClient); + + if (identical(client, _updateUserSecretsClient)) { + if (response?.statusCode == 200) { + _updateUserSecretsTimer?.cancel(); + _updateUserSecretsClient = null; + } + else { + _updateUserSecretsTimer ??= Timer.periodic(const Duration(seconds: 3), (_) { + if (_updateUserSecretsClient == null) { + _saveAccountSecrets(); + } + }); + } + _updateUserSecretsClient = null; + } + } + } + /*Future _loadAccountUserPrefs() async { if ((Config().coreUrl != null) && (_token?.accessToken != null)) { String url = "${Config().coreUrl}/services/account/preferences"; @@ -1192,13 +1776,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // User Profile @protected - void onUserProfileChanged(Auth2UserProfile? profile) { + Future onUserProfileChanged(Auth2UserProfile? profile) async { if (identical(profile, _anonymousProfile)) { - Storage().auth2AnonymousProfile = _anonymousProfile; + await Storage().setAuth2AnonymousProfile(_anonymousProfile); NotificationService().notify(notifyProfileChanged); } else if (identical(profile, _account?.profile)) { - Storage().auth2Account = _account; + await Storage().setAuth2Account(_account); NotificationService().notify(notifyProfileChanged); _saveAccountUserProfile(); } @@ -1211,7 +1795,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future saveAccountUserProfile(Auth2UserProfile? profile) async { if (await _saveExternalAccountUserProfile(profile)) { if (_account?.profile?.apply(profile) ?? false) { - Storage().auth2Account = _account; + await Storage().setAuth2Account(_account); NotificationService().notify(notifyProfileChanged); } return true; @@ -1272,6 +1856,25 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return false; } + Future updateUsername(String username) async { + if ((Config().coreUrl != null) && (_token?.accessToken != null)) { + String url = "${Config().coreUrl}/services/account/username"; + Map headers = { + 'Content-Type': 'application/json' + }; + Map body = { + 'username': username.toLowerCase().trim(), + }; + String? bodyJson = JsonUtils.encode(body); + Response? response = await Network().put(url, auth: Auth2(), headers: headers, body: bodyJson); + if (response?.statusCode == 200) { + _refreshAccount(); + return true; + } + } + return false; + } + /*Future _refreshAccountUserProfile() async { Auth2UserProfile? profile = await _loadAccountUserProfile(); if ((profile != null) && (profile != _account?.profile)) { @@ -1299,8 +1902,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { bool profileUpdated = (account.profile != _account?.profile); bool prefsUpdated = (account.prefs != _account?.prefs); - - Storage().auth2Account = _account = account; + + await Storage().setAuth2Account(_account = account); NotificationService().notify(notifyAccountChanged); if (profileUpdated) { @@ -1314,10 +1917,18 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Helpers - static Future _launchUrl(String? urlStr) async { + Future _launchUrl(String? urlStr) async { try { - if ((urlStr != null) && await canLaunchUrlString(urlStr)) { - await launchUrlString(urlStr, mode: Platform.isAndroid ? LaunchMode.externalApplication : LaunchMode.platformDefault); + if ((urlStr != null)) { + if (kIsWeb) { + FlutterWebAuth2.authenticate(url: urlStr, + callbackUrlScheme: DeepLink().appScheme ?? '' + ).then((String url) { + onDeepLinkUri(Uri.tryParse(url)); + }); + } else if (await canLaunchUrlString(urlStr)) { + await launchUrlString(urlStr); + } } } catch(e) { @@ -1329,13 +1940,48 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Log.d(message, lineLength: 512); // max line length of VS Code Debug Console } + Map? _getConfigParams(Map params) { + if (!Config().isReleaseWeb) { + if (Config().appPlatformId == null || Config().coreOrgId == null || Config().rokwireApiKey == null) { + return null; + } + params['app_type_identifier'] = Config().appPlatformId; + params['api_key'] = Config().rokwireApiKey; + params['org_id'] = Config().coreOrgId; + } + return params; + } + + // Plugin + + Future onPluginNotification(String? name, dynamic arguments) async { + switch (name) { + case 'onGetPasskeySuccess': + String? responseJson = JsonUtils.stringValue(arguments); + NotificationService().notify(notifyGetPasskeySuccess, responseJson); + break; + case 'onGetPasskeyFailed': + String? error = JsonUtils.stringValue(arguments); + NotificationService().notify(notifyGetPasskeyFailed, error); + break; + case 'onCreatePasskeySuccess': + String? responseJson = JsonUtils.stringValue(arguments); + NotificationService().notify(notifyCreatePasskeySuccess, responseJson); + break; + case 'onCreatePasskeyFailed': + String? error = JsonUtils.stringValue(arguments); + NotificationService().notify(notifyCreatePasskeyFailed, error); + break; + } + } } class _OidcLogin { final String? loginUrl; final Map? params; - - _OidcLogin({this.loginUrl, this.params}); + final String? error; + + _OidcLogin({this.loginUrl, this.params, this.error}); static _OidcLogin? fromJson(Map? json) { return (json != null) ? _OidcLogin( @@ -1353,85 +1999,172 @@ class _OidcLogin { } -// Auth2PhoneRequestCodeResult +class Auth2Csrf with NetworkAuthProvider { + Auth2Token? token; + + Auth2Csrf({this.token}); + + static const String csrfTokenName = 'rokwire-csrf-token'; + + @override + Map? get networkAuthHeaders { + String cookieName = csrfTokenName; + if (Config().authBaseUrl?.contains("localhost") == false) { + cookieName = '__Host-' + cookieName; + } + + Map headers = {}; + String cookieValue = WebUtils.getCookie(cookieName); + if (cookieValue.isNotEmpty) { + headers[csrfTokenName] = cookieValue; + } + + if (StringUtils.isNotEmpty(token?.accessToken)) { + String tokenType = token!.tokenType ?? 'Bearer'; + headers['Authorization'] = "$tokenType ${token!.accessToken}"; + } + return headers; + } + + @override + dynamic get networkAuthToken => token; + + @override + Future refreshNetworkAuthTokenIfNeeded(BaseResponse? response, dynamic token) async { + if ((response?.statusCode == 401) && (token is Auth2Token) && (Auth2().token == token) && + (!(Config().coreUrl?.contains('http://') ?? true) || (response?.request?.url.origin.contains('http://') ?? false))) { + return (await Auth2().refreshToken(token: token) != null); + } + return false; + } +} + +// Auth2PasskeySignUpResult + +class Auth2PasskeySignUpResult { + Auth2PasskeySignUpResultStatus status; + String? error; + String? creationOptions; + Auth2PasskeySignUpResult(this.status, {this.error, this.creationOptions}); +} -enum Auth2PhoneRequestCodeResult { +enum Auth2PasskeySignUpResultStatus { succeeded, failed, + failedNotSupported, failedAccountExist, + failedNotFound, + failedActivationExpired, + failedNotActivated, + failedNoCredentials, + failedCancelled, } -Auth2PhoneRequestCodeResult auth2PhoneRequestCodeResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2PhoneRequestCodeResult.succeeded; - case Auth2LinkResult.failedAccountExist: return Auth2PhoneRequestCodeResult.failedAccountExist; - default: return Auth2PhoneRequestCodeResult.failed; +// Auth2PasskeySignInResult +class Auth2PasskeySignInResult { + Auth2PasskeySignInResultStatus status; + String? error; + Auth2PasskeySignInResult(this.status, {this.error}); +} + +enum Auth2PasskeySignInResultStatus { + succeeded, + failed, + failedNotFound, + failedActivationExpired, + failedNotActivated, + failedNotSupported, + failedNoCredentials, + failedCancelled, +} + +// Auth2RequestCodeResult + +enum Auth2RequestCodeResult { + succeeded, + failed, + failedAccountExist, +} + +Auth2RequestCodeResult auth2RequestCodeResultFromAuth2LinkResult(Auth2LinkResult value) { + switch (value.status) { + case Auth2LinkResultStatus.succeeded: return Auth2RequestCodeResult.succeeded; + case Auth2LinkResultStatus.failedAccountExist: return Auth2RequestCodeResult.failedAccountExist; + default: return Auth2RequestCodeResult.failed; } } -// Auth2PhoneSendCodeResult +// Auth2SendCodeResult -enum Auth2PhoneSendCodeResult { +enum Auth2SendCodeResult { succeeded, failed, failedInvalid, } -Auth2PhoneSendCodeResult auth2PhoneSendCodeResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2PhoneSendCodeResult.succeeded; - case Auth2LinkResult.failedInvalid: return Auth2PhoneSendCodeResult.failedInvalid; - default: return Auth2PhoneSendCodeResult.failed; +Auth2SendCodeResult auth2SendCodeResultFromAuth2LinkResult(Auth2LinkResult value) { + switch (value.status) { + case Auth2LinkResultStatus.succeeded: return Auth2SendCodeResult.succeeded; + case Auth2LinkResultStatus.failedInvalid: return Auth2SendCodeResult.failedInvalid; + default: return Auth2SendCodeResult.failed; } } -// Auth2EmailAccountState +// Auth2AccountState -enum Auth2EmailAccountState { +enum Auth2AccountState { nonExistent, unverified, verified, } -// Auth2EmailSignUpResult +// Auth2SignInOptionsResult +class Auth2SignInOptionsResult { + List? identifierOptions; + List? authTypeOptions; + Auth2SignInOptionsResult({this.identifierOptions, this.authTypeOptions}); +} + +// Auth2PasswordSignUpResult -enum Auth2EmailSignUpResult { +enum Auth2PasswordSignUpResult { succeeded, failed, failedAccountExist, } -Auth2EmailSignUpResult auth2EmailSignUpResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2EmailSignUpResult.succeeded; - case Auth2LinkResult.failedAccountExist: return Auth2EmailSignUpResult.failedAccountExist; - default: return Auth2EmailSignUpResult.failed; +Auth2PasswordSignUpResult auth2PasswordSignUpResultFromAuth2LinkResult(Auth2LinkResult value) { + switch (value.status) { + case Auth2LinkResultStatus.succeeded: return Auth2PasswordSignUpResult.succeeded; + case Auth2LinkResultStatus.failedAccountExist: return Auth2PasswordSignUpResult.failedAccountExist; + default: return Auth2PasswordSignUpResult.failed; } } -// Auth2EmailSignInResult +// Auth2PasswordSignInResult -enum Auth2EmailSignInResult { +enum Auth2PasswordSignInResult { succeeded, failed, + failedNotFound, failedActivationExpired, failedNotActivated, failedInvalid, } -Auth2EmailSignInResult auth2EmailSignInResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2EmailSignInResult.succeeded; - case Auth2LinkResult.failedNotActivated: return Auth2EmailSignInResult.failedNotActivated; - case Auth2LinkResult.failedActivationExpired: return Auth2EmailSignInResult.failedActivationExpired; - case Auth2LinkResult.failedInvalid: return Auth2EmailSignInResult.failedInvalid; - default: return Auth2EmailSignInResult.failed; +Auth2PasswordSignInResult auth2PasswordSignInResultFromAuth2LinkResult(Auth2LinkResult value) { + switch (value.status) { + case Auth2LinkResultStatus.succeeded: return Auth2PasswordSignInResult.succeeded; + case Auth2LinkResultStatus.failedNotActivated: return Auth2PasswordSignInResult.failedNotActivated; + case Auth2LinkResultStatus.failedActivationExpired: return Auth2PasswordSignInResult.failedActivationExpired; + case Auth2LinkResultStatus.failedInvalid: return Auth2PasswordSignInResult.failedInvalid; + default: return Auth2PasswordSignInResult.failed; } } -// Auth2EmailForgotPasswordResult +// Auth2ForgotPasswordResult -enum Auth2EmailForgotPasswordResult { +enum Auth2ForgotPasswordResult { succeeded, failed, failedActivationExpired, @@ -1440,63 +2173,43 @@ enum Auth2EmailForgotPasswordResult { // Auth2OidcAuthenticateResult -enum Auth2OidcAuthenticateResult { - succeeded, - failed, - failedAccountExist, -} +class Auth2OidcAuthenticateResult { + Auth2OidcAuthenticateResultStatus status; + String? message; + String? error; + Auth2OidcAuthenticateResult(this.status, {this.message, this.error}); -Auth2OidcAuthenticateResult auth2OidcAuthenticateResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2OidcAuthenticateResult.succeeded; - case Auth2LinkResult.failedAccountExist: return Auth2OidcAuthenticateResult.failedAccountExist; - default: return Auth2OidcAuthenticateResult.failed; - } -} - -// Auth2UsernameAccountState - -enum Auth2UsernameAccountState { - nonExistent, - exists, + bool get succeeded => (status == Auth2OidcAuthenticateResultStatus.succeeded); + bool get failed => (status == Auth2OidcAuthenticateResultStatus.failed); + bool get failedAccountExist => (status == Auth2OidcAuthenticateResultStatus.failedAccountExist); } -// Auth2UsernameSignUpResult - -enum Auth2UsernameSignUpResult { +enum Auth2OidcAuthenticateResultStatus { succeeded, failed, failedAccountExist, } -Auth2UsernameSignUpResult auth2UsernameSignUpResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2UsernameSignUpResult.succeeded; - case Auth2LinkResult.failedAccountExist: return Auth2UsernameSignUpResult.failedAccountExist; - default: return Auth2UsernameSignUpResult.failed; - } -} - -// Auth2UsernameSignInResult - -enum Auth2UsernameSignInResult { - succeeded, - failed, - failedNotFound, - failedInvalid, +Auth2OidcAuthenticateResult auth2OidcAuthenticateResultFromAuth2LinkResult(Auth2LinkResult value) { + return Auth2OidcAuthenticateResult(auth2OidcAuthenticateResultStatusFromAuth2LinkResultStatus(value.status), message: value.message); } - -Auth2UsernameSignInResult auth2UsernameSignInResultFromAuth2LinkResult(Auth2LinkResult value) { +Auth2OidcAuthenticateResultStatus auth2OidcAuthenticateResultStatusFromAuth2LinkResultStatus(Auth2LinkResultStatus value) { switch (value) { - case Auth2LinkResult.succeeded: return Auth2UsernameSignInResult.succeeded; - case Auth2LinkResult.failedInvalid: return Auth2UsernameSignInResult.failedInvalid; - default: return Auth2UsernameSignInResult.failed; + case Auth2LinkResultStatus.succeeded: return Auth2OidcAuthenticateResultStatus.succeeded; + case Auth2LinkResultStatus.failedAccountExist: return Auth2OidcAuthenticateResultStatus.failedAccountExist; + default: return Auth2OidcAuthenticateResultStatus.failed; } } // Auth2LinkResult -enum Auth2LinkResult { +class Auth2LinkResult { + Auth2LinkResultStatus status; + String? message; + Auth2LinkResult(this.status, {this.message}); +} + +enum Auth2LinkResultStatus { succeeded, failed, failedActivationExpired, diff --git a/lib/service/config.dart b/lib/service/config.dart index f573f62f5..c059d6364 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -20,18 +20,19 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:flutter/services.dart' show rootBundle; -import 'package:rokwire_plugin/service/app_livecycle.dart'; -import 'package:rokwire_plugin/service/connectivity.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; +import 'package:rokwire_plugin/service/auth2.dart'; import 'package:rokwire_plugin/service/log.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; -import 'package:package_info/package_info.dart'; import 'package:rokwire_plugin/service/storage.dart'; import 'package:rokwire_plugin/service/network.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rokwire_plugin/utils/crypt.dart'; +import 'package:universal_html/html.dart' as html; class Config with Service, NetworkAuthProvider, NotificationsListener { @@ -39,7 +40,6 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { 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"; - static const String notifyEnvironmentChanged = "edu.illinois.rokwire.config.environment.changed"; static const String _configsAsset = "configs.json.enc"; static const String _configKeysAsset = "config.keys.json"; @@ -79,7 +79,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @override void createService() { NotificationService().subscribe(this, [ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, //TBD: FirebaseMessaging.notifyConfigUpdate ]); } @@ -92,34 +92,73 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @override Future initService() async { - _configEnvironment = configEnvFromString(Storage().configEnvironment) ?? _defaultConfigEnvironment ?? defaultConfigEnvironment; + _configEnvironment = _defaultConfigEnvironment ?? defaultConfigEnvironment; _packageInfo = await PackageInfo.fromPlatform(); - _appDocumentsDir = await getApplicationDocumentsDirectory(); - Log.d('Application Documents Directory: ${_appDocumentsDir!.path}'); + if (!kIsWeb) { + _appDocumentsDir = await getApplicationDocumentsDirectory(); + Log.d('Application Documents Directory: ${_appDocumentsDir!.path}'); + } + + if (!isReleaseWeb) { + _encryptionKeys = await loadEncryptionKeysFromAssets(); + if (_encryptionKeys == null) { + throw ServiceError( + source: this, + severity: ServiceErrorSeverity.fatal, + title: 'Config Initialization Failed', + description: 'Failed to load config encryption keys.', + ); + } + } + + if (!kIsWeb) { + _config = await loadFromFile(configFile); + } + + if (_config == null) { + if (!isReleaseWeb) { + _configAsset = await loadFromAssets(); + } + String? configString = await loadAsStringFromNet(); + _configAsset = null; + + _config = (configString != null) ? await configFromJsonString(configString) : null; + //TODO: decide how best to handle secret keys + if (_config != null) { // && secretKeys.isNotEmpty) { + configFile.writeAsStringSync(configString!, flush: true); + checkUpgrade(); + } + else { + throw ServiceError( + source: this, + severity: ServiceErrorSeverity.fatal, + title: 'Config Initialization Failed', + description: 'Failed to initialize application configuration.', + ); + } + } + else { + checkUpgrade(); + updateFromNet(); + } - await init(); await super.initService(); } - @override - Set get serviceDependsOn { - return { Storage(), Connectivity() }; - } - // NotificationsListener @override void onNotification(String name, dynamic param) { - if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + if (name == AppLifecycle.notifyStateChanged) { + _onAppLifecycleStateChanged(param); } //else if (name == FirebaseMessaging.notifyConfigUpdate) { // updateFromNet(); //} } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + void _onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); @@ -189,7 +228,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected Future loadAsStringFromNet() async { - return loadAsStringFromAppConfig(); + return loadAsStringFromCore(); } Future loadAsStringFromAppConfig() async { @@ -205,12 +244,17 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { Future loadAsStringFromCore() async { Map body = { 'version': appVersion, - 'app_type_identifier': appPlatformId, - 'api_key': rokwireApiKey, }; - String? bodyString = JsonUtils.encode(body); + if (!isReleaseWeb) { + if (appPlatformId == null || rokwireApiKey == null) { + return null; + } + body['app_type_identifier'] = appPlatformId; + body['api_key'] = rokwireApiKey; + } + try { - http.Response? response = await Network().post(appConfigUrl, body: bodyString); + http.Response? response = await Network().post(appConfigUrl, body: JsonUtils.encode(body), headers: {'content-type': 'application/json'}, auth: Auth2Csrf()); return ((response != null) && (response.statusCode == 200)) ? response.body : null; } catch (e) { debugPrint(e.toString()); @@ -220,9 +264,24 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected Future?> configFromJsonString(String? configJsonString) async { + return configFromJsonObjectString(configJsonString); + } + + @protected + Map? configFromJsonObjectString(String? configJsonString) { + Map? configJson = JsonUtils.decode(configJsonString); + Map? configData = configJson?["data"]; + if (configData != null) { + decryptSecretKeys(configData); + return configData; + } + return null; + } + + @protected + Future?> configFromJsonListString(String? configJsonString) async { List? jsonList = await JsonUtils.decodeListAsync(configJsonString); if (jsonList != null) { - jsonList.sort((dynamic cfg1, dynamic cfg2) { return ((cfg1 is Map) && (cfg2 is Map)) ? AppVersion.compareVersions(cfg1['mobileAppVersion'], cfg2['mobileAppVersion']) : 0; }); @@ -272,46 +331,6 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { return null; } - @protected - Future init() async { - - _encryptionKeys = await loadEncryptionKeysFromAssets(); - if (_encryptionKeys == null) { - throw ServiceError( - source: this, - severity: ServiceErrorSeverity.fatal, - title: 'Config Initialization Failed', - description: 'Failed to load config encryption keys.', - ); - } - - _config = await loadFromFile(configFile); - - if (_config == null) { - _configAsset = await loadFromAssets(); - String? configString = await loadAsStringFromNet(); - _configAsset = null; - - _config = (configString != null) ? await configFromJsonString(configString) : null; - if (_config != null) { - configFile.writeAsStringSync(configString!, flush: true); - checkUpgrade(); - } - else { - throw ServiceError( - source: this, - severity: ServiceErrorSeverity.fatal, - title: 'Config Initialization Failed', - description: 'Failed to initialize application configuration.', - ); - } - } - else { - checkUpgrade(); - updateFromNet(); - } - } - @protected Future updateFromNet() async { String? configString = await loadAsStringFromNet(); @@ -327,6 +346,9 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { // App Id & Version + String get operatingSystem => kIsWeb ? 'web' : Platform.operatingSystem; + String get localeName => kIsWeb ? 'unknown' : Platform.localeName; + String? get appId { return _packageInfo?.packageName; } @@ -335,7 +357,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { if (_appCanonicalId == null) { _appCanonicalId = appId; - String platformSuffix = ".${Platform.operatingSystem.toLowerCase()}"; + String platformSuffix = ".${operatingSystem.toLowerCase()}"; if ((_appCanonicalId != null) && _appCanonicalId!.endsWith(platformSuffix)) { _appCanonicalId = _appCanonicalId!.substring(0, _appCanonicalId!.length - platformSuffix.length); } @@ -344,10 +366,12 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } String? get appPlatformId { - if (_appPlatformId == null) { + if (kIsWeb) { + return authBaseUrl; + } else if (_appPlatformId == null) { _appPlatformId = appId; - String platformSuffix = ".${Platform.operatingSystem.toLowerCase()}"; + String platformSuffix = ".${operatingSystem.toLowerCase()}"; if ((_appPlatformId != null) && !_appPlatformId!.endsWith(platformSuffix)) { _appPlatformId = _appPlatformId! + platformSuffix; } @@ -368,15 +392,19 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } String? get appStoreId { - String? appStoreUrl = MapPathKey.entry(Config().upgradeInfo, 'url.ios'); + String? appStoreUrl = MapPathKey.entry(upgradeInfo, 'url.ios'); Uri? uri = (appStoreUrl != null) ? Uri.tryParse(appStoreUrl) : null; return ((uri != null) && uri.pathSegments.isNotEmpty) ? uri.pathSegments.last : null; } + String? get webServiceId => null; // Getters: Config Asset Acknowledgement String? get appConfigUrl { + if (isReleaseWeb) { + return "$authBaseUrl/application/configs"; + } String? assetUrl = (_configAsset != null) ? JsonUtils.stringValue(_configAsset!['config_url']) : null; return assetUrl ?? JsonUtils.stringValue(platformBuildingBlocks['appconfig_url']); } @@ -404,9 +432,10 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { String? get upgradeAvailableVersion { dynamic availableVersion = upgradeStringEntry('available_version'); + var upgradeVersions = Storage().reportedUpgradeVersions; bool upgradeAvailable = (availableVersion is String) && (AppVersion.compareVersions(_packageInfo!.version, availableVersion) < 0) && - !Storage().reportedUpgradeVersions.contains(availableVersion) && + !upgradeVersions.contains(availableVersion) && !_reportedUpgradeVersions.contains(availableVersion); return upgradeAvailable ? availableVersion : null; } @@ -441,7 +470,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { return entry; } else if (entry is Map) { - dynamic value = entry[Platform.operatingSystem.toLowerCase()]; + dynamic value = entry[operatingSystem.toLowerCase()]; return (value is String) ? value : null; } else { @@ -449,20 +478,9 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } } - // Environment - - set configEnvironment(ConfigEnvironment? configEnvironment) { - if (_configEnvironment != configEnvironment) { - _configEnvironment = configEnvironment; - Storage().configEnvironment = configEnvToString(_configEnvironment); - - init().catchError((e){ - debugPrint(e.toString()); - }).whenComplete((){ - NotificationService().notify(notifyEnvironmentChanged, null); - }); - } - } + bool get isProduction => _configEnvironment == ConfigEnvironment.production; + bool get isTest => _configEnvironment == ConfigEnvironment.test; + bool get isDev => _configEnvironment == ConfigEnvironment.dev; ConfigEnvironment? get configEnvironment { return _configEnvironment; @@ -497,6 +515,8 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { return (assetsCacheDir != null) ? Directory(assetsCacheDir) : null; } + bool get supportsAnonymousAuth => true; + // Getters: compound entries Map get content => _config ?? {}; @@ -523,6 +543,17 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { String? get calendarUrl => JsonUtils.stringValue(platformBuildingBlocks["calendar_url"]); String? get surveysUrl => JsonUtils.stringValue(platformBuildingBlocks["surveys_url"]); + // Getters: web + String? get webIdentifierOrigin => html.window.location.origin; + String? get authBaseUrl { + if (isReleaseWeb) { + return '${html.window.location.origin}/$webServiceId'; + } else if (isAdmin) { + return coreUrl != null ? '$coreUrl/admin': null; + } + return coreUrl != null ? '$coreUrl/services' : null; + } + // Getters: otherUniversityServices String? get assetsUrl => JsonUtils.stringValue(otherUniversityServices['assets_url']); @@ -536,11 +567,17 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { int get event2StartTimeOffsetIfNullEndTime => JsonUtils.intValue(settings['event2StartTimeOffsetIfNullEndTime']) ?? 1200; double get event2NearbyDistanceInMiles => JsonUtils.doubleValue(settings['event2NearbyDistanceInMiles']) ?? 1.0; + int get oidcAuthenticationTimeout => JsonUtils.intValue(settings['oidcAuthenticationTimeout']) ?? 1000; + String? get timezoneLocation => JsonUtils.stringValue(settings['timezoneLocation']) ?? 'America/Chicago'; + // Getters: other String? get deepLinkRedirectUrl { Uri? assetsUri = StringUtils.isNotEmpty(assetsUrl) ? Uri.tryParse(assetsUrl!) : null; return (assetsUri != null) ? "${assetsUri.scheme}://${assetsUri.host}/html/redirect.html" : null; } + + bool get isAdmin => false; + bool get isReleaseWeb => kIsWeb && !kDebugMode; } enum ConfigEnvironment { production, test, dev } diff --git a/lib/service/connectivity.dart b/lib/service/connectivity.dart index cd86b5e85..08fe97995 100644 --- a/lib/service/connectivity.dart +++ b/lib/service/connectivity.dart @@ -17,14 +17,15 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart' as connectivity; -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/log.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; enum ConnectivityStatus { wifi, mobile, none } -class Connectivity with Service { +class Connectivity with Service implements NotificationsListener { static const String notifyStatusChanged = "edu.illinois.rokwire.connectivity.status.changed"; @@ -49,6 +50,9 @@ class Connectivity with Service { @override void createService() { + NotificationService().subscribe(this, [ + AppLifecycle.notifyStateChanged, + ]); _connectivitySubscription = connectivity.Connectivity().onConnectivityChanged.listen(_onConnectivityChanged); } @@ -71,6 +75,7 @@ class Connectivity with Service { @override void destroyService() { + NotificationService().unsubscribe(this); _connectivitySubscription?.cancel(); _connectivitySubscription = null; } @@ -99,6 +104,26 @@ class Connectivity with Service { // Connectivity + Future checkStatus() async { + ConnectivityStatus? connectivityStatus = _statusFromResult(await connectivity.Connectivity().checkConnectivity()); + _setConnectivityStatus(connectivityStatus); + return connectivityStatus; + } + + @override + void onNotification(String name, dynamic param) { + if (name == AppLifecycle.notifyStateChanged) { + onAppLifecycleStateChanged(param); + } + } + + @protected + void onAppLifecycleStateChanged(AppLifecycleState? state) { + if (state == AppLifecycleState.resumed) { + checkStatus(); + } + } + ConnectivityStatus? get status { return _connectivityStatus; } diff --git a/lib/service/content.dart b/lib/service/content.dart index 4ae6bd140..3b361e7a3 100644 --- a/lib/service/content.dart +++ b/lib/service/content.dart @@ -23,7 +23,7 @@ import 'package:flutter_exif_rotation/flutter_exif_rotation.dart'; import 'package:http/http.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rokwire_plugin/model/content_attributes.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/auth2.dart'; import 'package:rokwire_plugin/service/network.dart'; @@ -77,7 +77,7 @@ class Content with Service implements NotificationsListener, ContentItemCategory @override void createService() { NotificationService().subscribe(this,[ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, ]); super.createService(); } @@ -129,12 +129,12 @@ class Content with Service implements NotificationsListener, ContentItemCategory @override void onNotification(String name, dynamic param) { - if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + if (name == AppLifecycle.notifyStateChanged) { + _onAppLifecycleStateChanged(param); } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + void _onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/deep_link.dart b/lib/service/deep_link.dart index acdcfd091..d9f69f453 100644 --- a/lib/service/deep_link.dart +++ b/lib/service/deep_link.dart @@ -14,16 +14,17 @@ * limitations under the License. */ +import 'package:app_links/app_links.dart'; import 'package:flutter/foundation.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; -import 'package:uni_links/uni_links.dart'; class DeepLink with Service { - + static const String notifyUri = "edu.illinois.rokwire.deeplink.uri"; + static const String notifyUriError = "edu.illinois.rokwire.deeplink.uri.error"; - // Singletone Factory + // Singleton Factory static DeepLink? _instance; @@ -37,24 +38,39 @@ class DeepLink with Service { @protected DeepLink.internal(); + Uri? _initialUri; + Uri? get initialUri => _initialUri; + // Service @override Future initService() async { + final _appLinks = AppLinks(); // 1. Initial Uri - getInitialUri().then((uri) { + _appLinks.getInitialAppLink().then((uri) { if (uri != null) { + _initialUri = uri; + debugPrint('Launch URI: $uri'); NotificationService().notify(notifyUri, uri); } }); // 2. Updated uri - uriLinkStream.listen((Uri? uri) { - if (uri != null) { - NotificationService().notify(notifyUri, uri); - } - }); + if (!kIsWeb) { + _appLinks.uriLinkStream.listen((Uri? uri) { + debugPrint('Received URI: $uri'); + if (uri != null) { + NotificationService().notify(notifyUri, uri); + } else { + NotificationService().notify(notifyUriError, 'deeplink null'); + } + }, onError: (e) { + NotificationService().notify(notifyUriError, e); + }, onDone: () { + NotificationService().notify(notifyUriError, 'deeplink handler done'); + }); + } await super.initService(); } diff --git a/lib/service/events.dart b/lib/service/events.dart index 713cc2faf..ce6abfb0b 100644 --- a/lib/service/events.dart +++ b/lib/service/events.dart @@ -82,11 +82,6 @@ class Events with Service implements NotificationsListener { processCachedEventDetails(); } - @override - Set get serviceDependsOn { - return { DeepLink() }; - } - // NotificationsListener @override diff --git a/lib/service/events2.dart b/lib/service/events2.dart index 99d8576c5..1f1f56520 100644 --- a/lib/service/events2.dart +++ b/lib/service/events2.dart @@ -60,11 +60,6 @@ class Events2 with Service implements NotificationsListener { processCachedEventDetails(); } - @override - Set get serviceDependsOn { - return { DeepLink() }; - } - // NotificationsListener @override diff --git a/lib/service/firebase_core.dart b/lib/service/firebase_core.dart index 4051b17bd..1f5cb2327 100644 --- a/lib/service/firebase_core.dart +++ b/lib/service/firebase_core.dart @@ -21,6 +21,11 @@ import 'package:rokwire_plugin/service/service.dart'; class FirebaseCore extends Service { google.FirebaseApp? _firebaseApp; + google.FirebaseOptions? _options; + + set options(google.FirebaseOptions options) { + _options = options; + } // Singletone Factory @@ -35,6 +40,8 @@ class FirebaseCore extends Service { @protected FirebaseCore.internal(); + + // Service @@ -56,7 +63,7 @@ class FirebaseCore extends Service { } Future initFirebase() async{ - _firebaseApp ??= await google.Firebase.initializeApp(); + _firebaseApp ??= await google.Firebase.initializeApp(options: _options); } google.FirebaseApp? get app => _firebaseApp; diff --git a/lib/service/firebase_crashlytics.dart b/lib/service/firebase_crashlytics.dart index eda525455..9d8342dc0 100644 --- a/lib/service/firebase_crashlytics.dart +++ b/lib/service/firebase_crashlytics.dart @@ -40,13 +40,16 @@ class FirebaseCrashlytics with Service { @override Future initService() async{ - // Enable automatic data collection - google.FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); - // Pass all uncaught errors to Firebase.Crashlytics. FlutterError.onError = handleFlutterError; - await super.initService(); + // Enable automatic data collection + try { + google.FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); + await super.initService(); + } catch (e) { + debugPrint(e.toString()); + } } void handleFlutterError(FlutterErrorDetails details) { @@ -55,17 +58,23 @@ class FirebaseCrashlytics with Service { } void handleZoneError(dynamic exception, StackTrace stack) { - debugPrint(exception?.toString()); - google.FirebaseCrashlytics.instance.recordError(exception, stack); + if (isInitialized) { + debugPrint(exception?.toString()); + google.FirebaseCrashlytics.instance.recordError(exception, stack); + } } void recordError(dynamic exception, StackTrace? stack) { - debugPrint(exception?.toString()); - google.FirebaseCrashlytics.instance.recordError(exception, stack); + if (isInitialized) { + debugPrint(exception?.toString()); + google.FirebaseCrashlytics.instance.recordError(exception, stack); + } } void log(String message) { - google.FirebaseCrashlytics.instance.log(message); + if (isInitialized) { + google.FirebaseCrashlytics.instance.log(message); + } } @override diff --git a/lib/service/firebase_messaging.dart b/lib/service/firebase_messaging.dart index 1255d4125..56be799d1 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -18,13 +18,13 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:firebase_messaging/firebase_messaging.dart' as firebase_messaging; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; +import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/inbox.dart'; import 'package:rokwire_plugin/service/firebase_core.dart'; import 'package:rokwire_plugin/service/log.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; -import 'package:rokwire_plugin/service/storage.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -92,7 +92,43 @@ class FirebaseMessaging with Service { @override Set get serviceDependsOn { - return { FirebaseCore(), Storage(), }; + return { FirebaseCore() }; + } + + Future get authorizationStatus async { + firebase_messaging.NotificationSettings settings = await firebase_messaging.FirebaseMessaging.instance.getNotificationSettings(); + return _convertStatus(settings.authorizationStatus); + } + + Future get requiresAuthorization async { + firebase_messaging.NotificationSettings settings = await firebase_messaging.FirebaseMessaging.instance.getNotificationSettings(); + firebase_messaging.AuthorizationStatus authorizationStatus = settings.authorizationStatus; + // There is not "notDetermined" status for android. Treat "denied" in Android like "notDetermined" in iOS + if (Config().operatingSystem == "android") { + return (authorizationStatus != firebase_messaging.AuthorizationStatus.denied); + } else { + return (authorizationStatus == firebase_messaging.AuthorizationStatus.notDetermined); + } + } + + Future requestAuthorization() async { + firebase_messaging.FirebaseMessaging messagingInstance = firebase_messaging.FirebaseMessaging.instance; + firebase_messaging.NotificationSettings requestSettings = await messagingInstance.requestPermission( + alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true); + return _convertStatus(requestSettings.authorizationStatus); + } + + NotificationsAuthorizationStatus _convertStatus(firebase_messaging.AuthorizationStatus status) { + switch(status) { + case firebase_messaging.AuthorizationStatus.authorized: + return NotificationsAuthorizationStatus.authorized; + case firebase_messaging.AuthorizationStatus.denied: + return NotificationsAuthorizationStatus.denied; + case firebase_messaging.AuthorizationStatus.notDetermined: + return NotificationsAuthorizationStatus.notDetermined; + case firebase_messaging.AuthorizationStatus.provisional: + return NotificationsAuthorizationStatus.provisional; + } } // Token @@ -122,7 +158,7 @@ class FirebaseMessaging with Service { Future onFirebaseMessage(firebase_messaging.RemoteMessage message) async { Log.d("FCM: onFirebaseMessage: $message"); try { - if ((AppLivecycle.instance?.state == AppLifecycleState.resumed) && StringUtils.isNotEmpty(message.notification?.body)) { + if ((AppLifecycle.instance?.state == AppLifecycleState.resumed) && StringUtils.isNotEmpty(message.notification?.body)) { NotificationService().notify(notifyForegroundMessage, { "body": message.notification?.body, "onComplete": () { @@ -143,3 +179,5 @@ class FirebaseMessaging with Service { void processDataMessage(Map? data) { } } + +enum NotificationsAuthorizationStatus { authorized, denied, notDetermined, provisional } \ No newline at end of file diff --git a/lib/service/flex_ui.dart b/lib/service/flex_ui.dart index b384471df..519d650f9 100644 --- a/lib/service/flex_ui.dart +++ b/lib/service/flex_ui.dart @@ -22,7 +22,7 @@ import 'package:http/http.dart'; import 'package:collection/collection.dart'; import 'package:rokwire_plugin/model/auth2.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/auth2.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/geo_fence.dart'; @@ -69,14 +69,15 @@ class FlexUI with Service implements NotificationsListener { @override void createService() { - NotificationService().subscribe(this,[ + NotificationService().subscribe(this, [ + Service.notifyInitialized, Auth2.notifyPrefsChanged, Auth2.notifyUserDeleted, Auth2UserPrefs.notifyRolesChanged, Auth2UserPrefs.notifyPrivacyLevelChanged, Auth2.notifyLoginChanged, Auth2.notifyLinkChanged, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, Groups.notifyUserGroupsUpdated, GeoFence.notifyCurrentRegionsUpdated, Config.notifyConfigChanged, @@ -90,11 +91,25 @@ class FlexUI with Service implements NotificationsListener { @override Future initService() async { - _assetsDir = await getAssetsDir(); - _defContentSource = await loadFromAssets(assetsKey); - _appContentSource = await loadFromAssets(appAssetsKey); - _netContentSource = await loadFromCache(netCacheFileName); + List> futures = [ + loadFromAssets(assetsKey), + if (!kIsWeb) + loadFromAssets(appAssetsKey), + if (!kIsWeb) + getAssetsDir(), + ]; + + List results = await Future.wait(futures); + _defContentSource = (0 < results.length) ? results[0] : null; + _appContentSource = (1 < results.length) ? results[1] : null; + _assetsDir = (2 < results.length) ? results[2] : null; + + if (_assetsDir != null) { + _netContentSource = await loadFromCache(netCacheFileName); + } + build(); + if (_defaultContent != null) { updateFromNet(); await super.initService(); @@ -109,34 +124,45 @@ class FlexUI with Service implements NotificationsListener { } } - @override - Set get serviceDependsOn { - return { Config(), Auth2(), Groups(), GeoFence() }; - } - // NotificationsListener @override void onNotification(String name, dynamic param) { - if ((name == Auth2.notifyPrefsChanged) || - (name == Auth2.notifyUserDeleted) || - (name == Auth2UserPrefs.notifyRolesChanged) || - (name == Auth2UserPrefs.notifyPrivacyLevelChanged) || - (name == Auth2.notifyLoginChanged) || - (name == Auth2.notifyLinkChanged) || - (name == Groups.notifyUserGroupsUpdated) || - (name == GeoFence.notifyCurrentRegionsUpdated) || - (name == Config.notifyConfigChanged)) + if (name == Service.notifyInitialized) { + onServiceInitialized(param is Service ? param : null); + } + else if ((name == Auth2.notifyPrefsChanged) || + (name == Auth2.notifyUserDeleted) || + (name == Auth2UserPrefs.notifyRolesChanged) || + (name == Auth2UserPrefs.notifyPrivacyLevelChanged) || + (name == Auth2.notifyLoginChanged) || + (name == Auth2.notifyLinkChanged) || + (name == Groups.notifyUserGroupsUpdated) || + (name == GeoFence.notifyCurrentRegionsUpdated) || + (name == Config.notifyConfigChanged)) { updateContent(); } - else if (name == AppLivecycle.notifyStateChanged) { - onAppLivecycleStateChanged(param); + else if (name == AppLifecycle.notifyStateChanged) { + onAppLifecycleStateChanged((param is AppLifecycleState) ? param : null); + } + } + + @protected + Future onServiceInitialized(Service? service) async { + if (isInitialized) { + if ((service == Config()) && !kIsWeb) { + _assetsDir = await getAssetsDir(); + _netContentSource = await loadFromCache(netCacheFileName); + } + if (((service == Config()) && (_netContentSource != null)) || (service == Auth2()) || (service == Groups()) || (service == GeoFence())) { + build(); + } } } @protected - void onAppLivecycleStateChanged(AppLifecycleState? state) { + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } @@ -213,7 +239,7 @@ class FlexUI with Service implements NotificationsListener { @protected Future loadContentStringFromNet() async { - if (Config().assetsUrl != null) { + if (StringUtils.isNotEmpty(Config().assetsUrl)) { Response? response = await Network().get("${Config().assetsUrl}/$netAssetFileName"); return (response?.statusCode == 200) ? response?.body : null; } @@ -223,14 +249,16 @@ class FlexUI with Service implements NotificationsListener { @protected Future updateFromNet() async { String? netContentSourceString = await loadContentStringFromNet(); - Map? netContentSource = JsonUtils.decodeMap(netContentSourceString); - if (((netContentSource != null) && !const DeepCollectionEquality().equals(netContentSource, _netContentSource)) || - ((netContentSource == null) && (_netContentSource != null))) - { - _netContentSource = netContentSource; - await saveToCache(netCacheFileName, netContentSourceString); - build(); - NotificationService().notify(notifyChanged, null); + if (netContentSourceString != null) { + Map? netContentSource = JsonUtils.decodeMap(netContentSourceString); + if (((netContentSource != null) && !const DeepCollectionEquality().equals(netContentSource, _netContentSource)) || + ((netContentSource == null) && (_netContentSource != null))) + { + _netContentSource = netContentSource; + await saveToCache(netCacheFileName, netContentSourceString); + build(); + NotificationService().notify(notifyChanged, null); + } } } @@ -593,17 +621,14 @@ class FlexUI with Service implements NotificationsListener { else if ((key == 'shibbolethLoggedIn') && (value is bool)) { result = result && (Auth2().isOidcLoggedIn == value); } - else if ((key == 'phoneLoggedIn') && (value is bool)) { - result = result && (Auth2().isPhoneLoggedIn == value); + else if ((key == 'codeLoggedIn') && (value is bool)) { + result = result && (Auth2().isCodeLoggedIn == value); } - else if ((key == 'emailLoggedIn') && (value is bool)) { - result = result && (Auth2().isEmailLoggedIn == value); + else if ((key == 'passwordLoggedIn') && (value is bool)) { + result = result && (Auth2().isPasswordLoggedIn == value); } - else if ((key == 'usernameLoggedIn') && (value is bool)) { - result = result && (Auth2().isUsernameLoggedIn == value); - } - else if ((key == 'phoneOrEmailLoggedIn') && (value is bool)) { - result = result && ((Auth2().isPhoneLoggedIn || Auth2().isEmailLoggedIn) == value) ; + else if ((key == 'passkeyLoggedIn') && (value is bool)) { + result = result && (Auth2().isPasskeyLoggedIn == value); } else if ((key == 'shibbolethLinked') && (value is bool)) { result = result && (Auth2().isOidcLinked == value); @@ -617,6 +642,9 @@ class FlexUI with Service implements NotificationsListener { else if ((key == 'usernameLinked') && (value is bool)) { result = result && (Auth2().isUsernameLinked == value); } + else if ((key == 'passkeyLinked') && (value is bool)) { + result = result && (Auth2().isPasskeyLinked == value); + } else if ((key == 'accountRole') && (value is String)) { result = result && Auth2().hasRole(value); } @@ -643,7 +671,7 @@ class FlexUI with Service implements NotificationsListener { if (key is String) { String? target; if (key == 'os') { - target = Platform.operatingSystem; + target = Config().operatingSystem; } else if (key == 'environment') { target = configEnvToString(Config().configEnvironment); diff --git a/lib/service/geo_fence.dart b/lib/service/geo_fence.dart index 21a929395..e44fc6697 100644 --- a/lib/service/geo_fence.dart +++ b/lib/service/geo_fence.dart @@ -88,7 +88,7 @@ class GeoFence with Service implements NotificationsListener, ContentItemCategor @override Set get serviceDependsOn { - return {Storage(), Content()}; + return { Storage(), Content() }; } // NotificationsListener @@ -185,7 +185,7 @@ class GeoFence with Service implements NotificationsListener, ContentItemCategor _updateCurrentBeacons(); monitorRegions(); } - + // ContentItemCategoryClient @override diff --git a/lib/service/graph_ql.dart b/lib/service/graph_ql.dart new file mode 100644 index 000000000..cf211b6e9 --- /dev/null +++ b/lib/service/graph_ql.dart @@ -0,0 +1,92 @@ +/* + * 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/foundation.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:rokwire_plugin/service/service.dart'; + +class GraphQL with Service { + + // Singleton Factory + + static GraphQL? _instance; + + static GraphQL? get instance => _instance; + + @protected + static set instance(GraphQL? value) => _instance = value; + + factory GraphQL() => _instance ?? (_instance = GraphQL.internal()); + + @protected + GraphQL.internal(); + + // Service + + @override + Future initService() async { + await initHiveForFlutter(); + await super.initService(); + } + + GraphQLClient getClient(String url, {Map> possibleTypes = const {}, + Map defaultHeaders = const {}, AuthLink? authLink, + FetchPolicy? defaultFetchPolicy, bool useCache = true}) { + Link link = HttpLink(url, defaultHeaders: defaultHeaders); + if (authLink != null) { + link = authLink.concat(link); + } + GraphQLClient client = GraphQLClient( + link: link, + defaultPolicies: DefaultPolicies( + query: Policies( + fetch: defaultFetchPolicy, + error: ErrorPolicy.all, + cacheReread: CacheRereadPolicy.ignoreAll + ), + mutate: Policies( + fetch: defaultFetchPolicy, + error: ErrorPolicy.all, + cacheReread: CacheRereadPolicy.ignoreAll + ) + ), + cache: GraphQLCache( + store: useCache ? HiveStore() : NoOpStore(), + possibleTypes: possibleTypes, + partialDataPolicy: PartialDataCachePolicy.accept + ), + ); + return client; + } + + ValueNotifier getNotifier(GraphQLClient client) { + return ValueNotifier(client); + } +} + +class NoOpStore implements Store { + Map? get(String dataId) => null; + + void put(String dataId, Map? value) => null; + + void putAll(Map?> data) => null; + + void delete(String dataId) => null; + + void reset() => null; + + Map?> toMap() => {}; +} diff --git a/lib/service/groups.dart b/lib/service/groups.dart index 742a3eb29..6601ae2e0 100644 --- a/lib/service/groups.dart +++ b/lib/service/groups.dart @@ -27,7 +27,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:rokwire_plugin/model/content_attributes.dart'; import 'package:rokwire_plugin/model/event2.dart'; import 'package:rokwire_plugin/model/group.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/auth2.dart'; import 'package:rokwire_plugin/service/connectivity.dart'; @@ -112,7 +112,7 @@ class Groups with Service implements NotificationsListener { NotificationService().subscribe(this,[ DeepLink.notifyUri, Auth2.notifyLoginChanged, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, FirebaseMessaging.notifyGroupsNotification, Connectivity.notifyStatusChanged ]); @@ -155,7 +155,7 @@ class Groups with Service implements NotificationsListener { @override Set get serviceDependsOn { - return { DeepLink(), Config(), Auth2() }; + return { Config(), Auth2() }; } // NotificationsListener @@ -168,8 +168,8 @@ class Groups with Service implements NotificationsListener { else if (name == Auth2.notifyLoginChanged) { _onLoginChanged(); } - else if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + else if (name == AppLifecycle.notifyStateChanged) { + _onAppLifecycleStateChanged(param); } else if (name == FirebaseMessaging.notifyGroupsNotification){ _onFirebaseMessageForGroupUpdate(); @@ -192,7 +192,7 @@ class Groups with Service implements NotificationsListener { } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + void _onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } @@ -459,7 +459,7 @@ class Groups with Service implements NotificationsListener { try { await _ensureLogin(); Map json = group.toJson(/*withId: false*/); - json["creator_email"] = Auth2().account?.profile?.email ?? ""; + json["creator_email"] = Auth2().account?.authType?.uiucUser?.email ?? ""; json["creator_name"] = Auth2().account?.profile?.fullName ?? ""; String? body = JsonUtils.encode(json); Response? response = await Network().post(url, auth: Auth2(), body: body); @@ -660,7 +660,7 @@ class Groups with Service implements NotificationsListener { try { await _ensureLogin(); Map json = {}; - json["email"] = Auth2().account?.profile?.email ?? ""; + json["email"] = Auth2().account?.authType?.uiucUser?.email ?? ""; json["name"] = Auth2().account?.profile?.fullName ?? ""; json["member_answers"] = CollectionUtils.isNotEmpty(answers) ? answers!.map((e) => e.toJson()).toList() : []; String? body = JsonUtils.encode(json); diff --git a/lib/service/http_proxy.dart b/lib/service/http_proxy.dart index 2e1191632..1955c292e 100644 --- a/lib/service/http_proxy.dart +++ b/lib/service/http_proxy.dart @@ -17,13 +17,13 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:rokwire_plugin/service/notification_service.dart'; +import 'package:native_flutter_proxy/native_proxy_reader.dart'; import 'package:rokwire_plugin/service/service.dart'; import 'package:rokwire_plugin/service/storage.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/utils/utils.dart'; -class HttpProxy extends Service implements NotificationsListener { +class HttpProxy extends Service { // Singletone Factory @@ -44,32 +44,43 @@ class HttpProxy extends Service implements NotificationsListener { @override void createService() { super.createService(); - - NotificationService().subscribe(this, [Config.notifyEnvironmentChanged]); } @override Future initService() async { _handleChanged(); + await applySystemProxy(); await super.initService(); } @override void destroyService() { super.destroyService(); - NotificationService().unsubscribe(this); } @override Set get serviceDependsOn { - return {Storage(), Config()}; + return { Storage(), Config() }; } - @override - void onNotification(String name, dynamic param){ - if(name == Config.notifyEnvironmentChanged){ - _handleChanged(); + Future applySystemProxy() async { + if (kIsWeb) { + return; } + bool enabled = false; + String? host; + int? port; + try { + ProxySetting settings = await NativeProxyReader.proxySetting; + enabled = settings.enabled; + host = settings.host; + port = settings.port; + } catch (e) { + print(e); + } + httpProxyHost = host; + httpProxyPort = port?.toString(); + httpProxyEnabled = enabled; } @@ -78,6 +89,9 @@ class HttpProxy extends Service implements NotificationsListener { } set httpProxyEnabled(bool? value){ + if (kIsWeb) { + return; + } if(Storage().httpProxyEnabled != value) { Storage().httpProxyEnabled = value; _handleChanged(); diff --git a/lib/service/inbox.dart b/lib/service/inbox.dart index 3f8fa9bda..4c2cc1755 100644 --- a/lib/service/inbox.dart +++ b/lib/service/inbox.dart @@ -1,10 +1,7 @@ - -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:rokwire_plugin/model/inbox.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/auth2.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/firebase_messaging.dart'; @@ -51,7 +48,7 @@ class Inbox with Service implements NotificationsListener { FirebaseMessaging.notifyToken, Auth2.notifyLoginChanged, Auth2.notifyPrepareUserDelete, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, ]); } @@ -90,14 +87,14 @@ class Inbox with Service implements NotificationsListener { _loadUserInfo(); _loadUnreadMessagesCount(); } - else if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + else if (name == AppLifecycle.notifyStateChanged) { + _onAppLifecycleStateChanged(param); } else if (name == Auth2.notifyPrepareUserDelete){ _deleteUser(); } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + void _onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } @@ -295,7 +292,7 @@ class Inbox with Service implements NotificationsListener { String? body = JsonUtils.encode({ 'token': token, 'previous_token': previousToken, - 'app_platform': Platform.operatingSystem, + 'app_platform': Config().operatingSystem, 'app_version': Config().appVersion, }); Response? response = await Network().post(url, body: body, auth: Auth2()); diff --git a/lib/service/localization.dart b/lib/service/localization.dart index c44366ea9..99f554810 100644 --- a/lib/service/localization.dart +++ b/lib/service/localization.dart @@ -18,7 +18,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart' show rootBundle; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/network.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; @@ -27,6 +27,7 @@ import 'package:rokwire_plugin/service/storage.dart'; import 'package:http/http.dart' as http; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:path/path.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; class Localization with Service implements NotificationsListener { @@ -75,7 +76,10 @@ class Localization with Service implements NotificationsListener { @override void createService() { - NotificationService().subscribe(this, AppLivecycle.notifyStateChanged); + NotificationService().subscribe(this,[ + Service.notifyInitialized, + AppLifecycle.notifyStateChanged, + ]); } @override @@ -109,7 +113,7 @@ class Localization with Service implements NotificationsListener { @override Set get serviceDependsOn { - return { Storage(), Config() }; + return { Storage() }; } // Locale @@ -174,7 +178,7 @@ class Localization with Service implements NotificationsListener { Future initDefaultStirngs(String language) async { _defaultStrings = _buildStrings( asset: _defaultAssetsStrings = await loadAssetsStrings(language), - appAsset: _defaultAppAssetsStrings = await loadAssetsStrings(language, app: true), + appAsset: _defaultAppAssetsStrings = kIsWeb ? null : await loadAssetsStrings(language, app: true), net: _defaultNetStrings = await loadNetStringsFromCache(language)); updateDefaultStrings(); } @@ -183,7 +187,7 @@ class Localization with Service implements NotificationsListener { Future initLocaleStirngs(String language) async { _localeStrings = _buildStrings( asset: _localeAssetsStrings = await loadAssetsStrings(language), - appAsset: _localeAppAssetsStrings = await loadAssetsStrings(language, app: true), + appAsset: _localeAppAssetsStrings = kIsWeb ? null : await loadAssetsStrings(language, app: true), net: _localeNetStrings = await loadNetStringsFromCache(language)); updateLocaleStrings(); } @@ -253,7 +257,7 @@ class Localization with Service implements NotificationsListener { Map? jsonData; try { String assetName = getNetworkAssetName(language); - http.Response? response = (Config().assetsUrl != null) ? await Network().get("${Config().assetsUrl}/$assetName") : null; + http.Response? response = StringUtils.isNotEmpty(Config().assetsUrl) ? await Network().get("${Config().assetsUrl}/$assetName") : null; String? jsonString = ((response != null) && (response.statusCode == 200)) ? response.body : null; jsonData = (jsonString != null) ? JsonUtils.decode(jsonString) : null; if ((jsonString != null) && (jsonData != null) && ((cache == null) || !const DeepCollectionEquality().equals(jsonData, cache))) { @@ -288,12 +292,36 @@ class Localization with Service implements NotificationsListener { @override void onNotification(String name, dynamic param) { - if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + if (name == Service.notifyInitialized) { + onServiceInitialized((param is Service) ? param : null); + } + else if (name == AppLifecycle.notifyStateChanged) { + onAppLifecycleStateChanged((param is AppLifecycleState) ? param : null); + } + } + + @protected + void onServiceInitialized(Service? service) async { + if (this.isInitialized) { + if (service == Config()) { + _assetsDir = await getAssetsDir(); + + String defaultLanguage = supportedLanguages[0]; + if (_defaultLocale?.languageCode != defaultLanguage) { + _defaultLocale = Locale.fromSubtags(languageCode : defaultLanguage); + await initDefaultStirngs(defaultLanguage); + } + + String? currentLanguage = _currentLocale?.languageCode; + if (currentLanguage != null) { + await initLocaleStirngs(currentLanguage); + } + } } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + @protected + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/location_services.dart b/lib/service/location_services.dart index 5fd9d3cce..00d805257 100644 --- a/lib/service/location_services.dart +++ b/lib/service/location_services.dart @@ -19,7 +19,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:geolocator/geolocator.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -59,7 +59,7 @@ class LocationServices with Service implements NotificationsListener { @override void createService() { NotificationService().subscribe(this, [ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, ]); } @@ -179,12 +179,12 @@ class LocationServices with Service implements NotificationsListener { @override void onNotification(String name, dynamic param) { - if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + if (name == AppLifecycle.notifyStateChanged) { + _onAppLifecycleStateChanged(param); } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + void _onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.resumed) { LocationServicesStatus? lastStatus = _lastStatus; status.then((_) { diff --git a/lib/service/network.dart b/lib/service/network.dart index e558778a6..66cd46b66 100644 --- a/lib/service/network.dart +++ b/lib/service/network.dart @@ -28,7 +28,7 @@ import 'package:rokwire_plugin/service/log.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/utils/utils.dart'; -abstract class NetworkAuthProvider { +abstract mixin class NetworkAuthProvider { Map? get networkAuthHeaders; dynamic get networkAuthToken => null; Future refreshNetworkAuthTokenIfNeeded(http.BaseResponse? response, dynamic token) async => false; @@ -40,6 +40,7 @@ class Network { static const String notifyHttpRequestUrl = "requestUrl"; static const String notifyHttpRequestMethod = "requestMethod"; static const String notifyHttpResponseCode = "responseCode"; + static const String notifyHttpResponseError = "edu.illinois.rokwire.network.http_response.error"; // Singleton Factory @@ -103,6 +104,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } return null; } @@ -135,6 +137,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -144,15 +147,15 @@ class Network { http.Response? response; try { - dynamic token = auth?.networkAuthToken; response = await _get(url, headers: headers, body: body, encoding: encoding, auth: auth, client: client, timeout: timeout); - if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { + if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { response = await _get(url, body: body, headers: headers, auth: auth, client: client, timeout: timeout); } } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -177,6 +180,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -186,15 +190,15 @@ class Network { http.Response? response; try { - dynamic token = auth?.networkAuthToken; response = await _post(url, body: body, encoding: encoding, headers: headers, client: client, auth: auth, timeout: timeout); - if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { + if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { response = await _post(url, body: body, encoding: encoding, headers: headers, client: client, auth: auth, timeout: timeout); } } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -219,6 +223,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -228,15 +233,15 @@ class Network { http.Response? response; try { - dynamic token = auth?.networkAuthToken; response = await _put(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout, client: client); - if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { + if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { response = await _put(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout, client: client); } } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -257,6 +262,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -265,16 +271,16 @@ class Network { Future patch(url, {Object? body, Encoding? encoding, Map? headers, NetworkAuthProvider? auth, int? timeout = 60, bool sendAnalytics = true, String? analyticsUrl }) async { http.Response? response; - try { - dynamic token = auth?.networkAuthToken; + try { response = await _patch(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout); - if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { + if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { response = await _patch(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout); } } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -295,6 +301,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -303,15 +310,15 @@ class Network { Future delete(url, {Object? body, Encoding? encoding, Map? headers, NetworkAuthProvider? auth, int? timeout = 60, bool sendAnalytics = true, String? analyticsUrl }) async { http.Response? response; try { - dynamic token = auth?.networkAuthToken; response = await _delete(url, body: body, encoding:encoding, headers: headers, auth: auth, timeout: timeout); - if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { + if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { response = await _delete(url, body: body, encoding:encoding, headers: headers, auth: auth, timeout: timeout); } } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -332,6 +339,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -344,6 +352,7 @@ class Network { catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } return null; } @@ -357,6 +366,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -370,15 +380,15 @@ class Network { http.StreamedResponse? response; try { - dynamic token = auth?.networkAuthToken; response = await _multipartPost(url: url, fileKey: fileKey, fileBytes: fileBytes, fileName: fileName, contentType: contentType, headers: headers, fields: fields, auth: auth); - if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { + if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { response = await _multipartPost(url: url, fileKey: fileKey, fileBytes: fileBytes, fileName: fileName, contentType: contentType, headers: headers, fields: fields, auth: auth); } } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -415,6 +425,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -429,6 +440,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -437,16 +449,16 @@ class Network { Future head(url, { Map? headers, NetworkAuthProvider? auth, int? timeout = 60, bool sendAnalytics = true, String? analyticsUrl }) async { http.Response? response; - try { - dynamic token = auth?.networkAuthToken; + try { response = await _head(url, headers: headers, auth: auth, timeout: timeout); - if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { + if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { response = await _head(url, headers: headers, auth: auth, timeout: timeout); } } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { diff --git a/lib/service/notification_service.dart b/lib/service/notification_service.dart index 15b2e2c0b..c5f5db430 100644 --- a/lib/service/notification_service.dart +++ b/lib/service/notification_service.dart @@ -14,6 +14,8 @@ * limitations under the License. */ +import 'dart:async'; + import 'package:flutter/foundation.dart'; class NotificationService { @@ -31,7 +33,7 @@ class NotificationService { @protected NotificationService.internal(); - + final Map> _listeners = {}; @@ -107,6 +109,6 @@ class NotificationService { } -abstract class NotificationsListener { - void onNotification(String name, dynamic param); +abstract mixin class NotificationsListener { + FutureOr onNotification(String name, dynamic param); } diff --git a/lib/service/onboarding.dart b/lib/service/onboarding.dart index 52dfb2e3c..dfa05d1f5 100644 --- a/lib/service/onboarding.dart +++ b/lib/service/onboarding.dart @@ -97,12 +97,21 @@ class Onboarding with Service implements NotificationsListener { void next(BuildContext context, OnboardingPanel panel, {bool replace = false}) { nextPanel(panel).then((dynamic nextPanel) { + if (!context.mounted) { + return; + } if (nextPanel is Widget) { if (replace) { - Navigator.pushReplacement(context, CupertinoPageRoute(builder: (context) => nextPanel)); + Navigator.pushReplacement(context, CupertinoPageRoute( + builder: (context) => nextPanel, + settings: nextPanel is OnboardingPanel ? RouteSettings(name: getPanelCode(panel: nextPanel as OnboardingPanel)) : null, + )); } else { - Navigator.push(context, CupertinoPageRoute(builder: (context) => nextPanel)); + Navigator.push(context, CupertinoPageRoute( + builder: (context) => nextPanel, + settings: nextPanel is OnboardingPanel ? RouteSettings(name: getPanelCode(panel: nextPanel as OnboardingPanel)) : null, + )); } } else if ((nextPanel is bool) && !nextPanel) { @@ -137,9 +146,7 @@ class Onboarding with Service implements NotificationsListener { if ((nextPanel != null) && (nextPanel is Widget) && nextPanel.onboardingCanDisplay && await nextPanel.onboardingCanDisplayAsync) { return nextPanel as Widget; } - else { - nextPanelIndex++; - } + nextPanelIndex++; } return false; } @@ -154,7 +161,7 @@ class Onboarding with Service implements NotificationsListener { String? getPanelCode({OnboardingPanel? panel}) => null; } -abstract class OnboardingPanel { +abstract mixin class OnboardingPanel { Map? get onboardingContext { return null; diff --git a/lib/service/polls.dart b/lib/service/polls.dart index 081be69a5..a915a9190 100644 --- a/lib/service/polls.dart +++ b/lib/service/polls.dart @@ -22,7 +22,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:rokwire_plugin/model/poll.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/auth2.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/geo_fence.dart'; @@ -468,7 +468,7 @@ class Polls with Service implements NotificationsListener { NotificationService().notify(notifyCreated, pollId); if (!poll.hasGroup) { presentWaiting(); - if (AppLivecycle().state == AppLifecycleState.paused) { + if (AppLifecycle().state == AppLifecycleState.paused) { launchPollNotification(poll); } } diff --git a/lib/service/rules.dart b/lib/service/rules.dart index a0b0a9629..ca40765f1 100644 --- a/lib/service/rules.dart +++ b/lib/service/rules.dart @@ -15,7 +15,6 @@ import 'package:flutter/material.dart'; import 'package:rokwire_plugin/model/alert.dart'; -import 'package:rokwire_plugin/model/auth2.dart'; import 'package:rokwire_plugin/model/rules.dart'; import 'package:rokwire_plugin/model/survey.dart'; import 'package:rokwire_plugin/service/app_datetime.dart'; @@ -487,12 +486,8 @@ class Rules { return Auth2().uin; case "net_id": return Auth2().netId; - case "email": - return Auth2().email; - case "phone": - return Auth2().phone; case "login_type": - return Auth2().loginType != null ? auth2LoginTypeToString(Auth2().loginType!) : null; + return Auth2().loginType; case "full_name": return Auth2().fullName; case "first_name": @@ -529,10 +524,12 @@ class Rules { return Auth2().isLoggedIn; case "is_oidc_logged_in": return Auth2().isOidcLoggedIn; - case "is_email_logged_in": - return Auth2().isEmailLoggedIn; - case "is_phone_logged_in": - return Auth2().isPhoneLoggedIn; + case "is_password_logged_in": + return Auth2().isPasswordLoggedIn; + case "is_passkey_logged_in": + return Auth2().isPasskeyLoggedIn; + case "is_code_logged_in": + return Auth2().isCodeLoggedIn; } return null; } @@ -549,10 +546,6 @@ class Rules { return Auth2().profile?.birthYear; case "photo_url": return Auth2().profile?.photoUrl; - case "email": - return Auth2().profile?.email; - case "phone": - return Auth2().profile?.phone; case "address": return Auth2().profile?.address; case "state": diff --git a/lib/service/service.dart b/lib/service/service.dart index 3fb12201b..0b9624f9e 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -18,8 +18,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:rokwire_plugin/service/notification_service.dart'; -abstract class Service { +abstract mixin class Service { + + static const String notifyInitialized = "edu.illinois.rokwire.service.initialized"; bool? _isInitialized; @@ -31,6 +34,7 @@ abstract class Service { Future initService() async { _isInitialized = true; + NotificationService().notify(notifyInitialized, this); } void initServiceUI() async { @@ -62,6 +66,9 @@ class Services { List? _services; + Future? _initialzeFuture; + bool? _isInitialized; + void create(List services) { if (_services == null) { _services = services; @@ -77,19 +84,25 @@ class Services { service.destroyService(); } _services = null; + ; } } - Future init() async => - (_services != null) ? await _ServicesInitializer(initService).process(_services!) : null; + Future init() async { + if (_initialzeFuture != null) { + return await _initialzeFuture; + } + else { + _initialzeFuture = _ServicesInitializer(initService).process(_services); + ServiceError? error = await _initialzeFuture; + _isInitialized = (error == null); + _initialzeFuture = null; + return error; + } + } - /*TMP: - ServiceError( - source: null, - severity: ServiceErrorSeverity.fatal, - title: 'Text Initialization Error', - description: 'This is a test initialization error.', - );*/ + bool get isInitialized => (_isInitialized == true); + bool get isInitializeFailed => (_isInitialized == false); @protected Future initService(Service service) async { @@ -129,11 +142,9 @@ class _ServicesInitializer { _ServicesInitializer(this.initService); - Future process(List services) async { + Future process(List? services) async { - for (Service service in services) { - (service.isInitialized ? _done : _toDo).add(service); - } + _prepareServices(services); if (_toDo.isNotEmpty) { _completer = Completer(); @@ -145,14 +156,29 @@ class _ServicesInitializer { } } + void _prepareServices(Iterable? services) { + if (services != null) { + for (Service service in services) { + _prepareService(service); + } + } + } + + void _prepareService(Service service) { + if (!_done.contains(service) && !_toDo.contains(service)) { + (service.isInitialized ? _done : _toDo).add(service); + _prepareServices(service.serviceDependsOn); + } + } + void _run() { if (_toDo.isNotEmpty) { for (Service service in _toDo) { if (_canStartService(service) && !_inProgress.contains(service)) { _inProgress.add(service); - initService(service).then((ServiceError? error) { + initService(service).then((ServiceError? error) async { _inProgress.remove(service); - if (_completer != null) { + if ((_completer != null) && (_completer?.isCompleted != true)) { if (error?.severity == ServiceErrorSeverity.fatal) { _completer?.complete(error); _completer = null; @@ -169,11 +195,11 @@ class _ServicesInitializer { if (_inProgress.isEmpty) { _completer?.complete(ServiceError( - source: null, - severity: ServiceErrorSeverity.fatal, - title: 'Services Initialization Error', - description: 'Service dependency cycle detected.', - )); + source: null, + severity: ServiceErrorSeverity.fatal, + title: 'Services Initialization Error', + description: 'Service dependency cycle detected.', + )); _completer = null; } } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index d1a3ee555..e35204a85 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -15,6 +15,7 @@ */ import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:rokwire_plugin/model/auth2.dart'; import 'package:rokwire_plugin/model/inbox.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; @@ -28,10 +29,12 @@ class Storage with Service { static const String notifySettingChanged = 'edu.illinois.rokwire.setting.changed'; - static const String _ecryptionKeyId = 'edu.illinois.rokwire.encryption.storage.key'; + static const String _encryptionKeyId = 'edu.illinois.rokwire.encryption.storage.key'; static const String _encryptionIVId = 'edu.illinois.rokwire.encryption.storage.iv'; SharedPreferences? _sharedPreferences; + FlutterSecureStorage? _secureStorage; + String? _encryptionKey; String? _encryptionIV; @@ -54,9 +57,15 @@ class Storage with Service { @override Future initService() async { _sharedPreferences = await SharedPreferences.getInstance(); - _encryptionKey = await RokwirePlugin.getEncryptionKey(identifier: encryptionKeyId, size: AESCrypt.kCCBlockSizeAES128); - _encryptionIV = await RokwirePlugin.getEncryptionKey(identifier: encryptionIVId, size: AESCrypt.kCCBlockSizeAES128); - + + AndroidOptions _getAndroidOptions() => const AndroidOptions( + encryptedSharedPreferences: true, + ); + IOSOptions _getIOSOptions() => const IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device); + _secureStorage = FlutterSecureStorage(aOptions: _getAndroidOptions(), iOptions: _getIOSOptions()); + + _encryptionKey = await RokwirePlugin.getEncryptionKey(identifier: _encryptionKeyId, size: AESCrypt.kCCBlockSizeAES128); + _encryptionIV = await RokwirePlugin.getEncryptionKey(identifier: _encryptionIVId, size: AESCrypt.kCCBlockSizeAES128); if (_sharedPreferences == null) { throw ServiceError( source: this, @@ -65,26 +74,52 @@ class Storage with Service { description: 'Failed to initialize application preferences storage.', ); } - else if ((_encryptionKey == null) || (_encryptionIV == null)) { - throw ServiceError( - source: this, - severity: ServiceErrorSeverity.fatal, - title: 'Storage Initialization Failed', - description: 'Failed to initialize encryption keys.', - ); - } - else { - await super.initService(); - } + // else if ((_encryptionKey == null) || (_encryptionIV == null)) { + // throw ServiceError( + // source: this, + // severity: ServiceErrorSeverity.fatal, + // title: 'Storage Initialization Failed', + // description: 'Failed to initialize encryption keys.', + // ); + // } + migrateEncryptedToSecureStorage(); + await super.initService(); } // Encryption - String get encryptionKeyId => _ecryptionKeyId; - String? get encryptionKey => _encryptionKey; - - String get encryptionIVId => _encryptionIVId; - String? get encryptionIV => _encryptionIV; + @protected + List get secureKeys => [ + auth2AnonymousTokenKey, auth2AnonymousPrefsKey, auth2AnonymousProfileKey, + auth2TokenKey, auth2AccountKey + ]; + + @protected + Future migrateEncryptedToSecureStorage() async { + if (encryptedMigratedToSecureStorage == true || _encryptionKey == null || + _encryptionIV == null) { + return; + } + + try { + for (String key in secureKeys) { + String? value = getEncryptedStringWithName(key); + if (value != null) { + await setSecureStringWithName(key, value); + setEncryptedStringWithName(key, null); + } + } + encryptedMigratedToSecureStorage = true; + } catch (e) { + debugPrint('error migrating encrypted storage: $e'); + } + } + + // String get encryptionKeyId => _encryptionKeyId; + // String? get encryptionKey => _encryptionKey; + // + // String get encryptionIVId => _encryptionIVId; + // String? get encryptionIV => _encryptionIV; String? encrypt(String? value) { return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? @@ -111,6 +146,23 @@ class Storage with Service { NotificationService().notify(notifySettingChanged, name); } + Future getSecureStringWithName(String name, {String? defaultValue}) async { + return await _secureStorage?.read(key: name) ?? defaultValue; + } + + Future setSecureStringWithName(String name, String? value, {bool removeFirst = false}) async { + if (value != null) { + if (removeFirst) { + await _secureStorage?.delete(key: name); + } + await _secureStorage?.write(key: name, value: value); + } else { + await _secureStorage?.delete(key: name); + } + NotificationService().notify(notifySettingChanged, name); + } + + @protected String? getEncryptedStringWithName(String name, {String? defaultValue}) { String? value = _sharedPreferences?.getString(name); if (value != null) { @@ -124,6 +176,7 @@ class Storage with Service { return value ?? defaultValue; } + @protected void setEncryptedStringWithName(String name, String? value) { if (value != null) { if ((_encryptionKey != null) && (_encryptionIV != null)) { @@ -222,11 +275,9 @@ class Storage with Service { } } - // Config - - String get configEnvKey => 'edu.illinois.rokwire.config_environment'; - String? get configEnvironment => getStringWithName(configEnvKey); - set configEnvironment(String? value) => setStringWithName(configEnvKey, value); + String get encryptedMigratedToSecureStorageKey => 'edu.illinois.rokwire.storage.encrypted.migrated'; + bool? get encryptedMigratedToSecureStorage => getBoolWithName(encryptedMigratedToSecureStorageKey); + set encryptedMigratedToSecureStorage(bool? value) => setBoolWithName(encryptedMigratedToSecureStorageKey, value); // Upgrade @@ -249,27 +300,31 @@ class Storage with Service { String get auth2AnonymousIdKey => 'edu.illinois.rokwire.auth2.anonymous.id'; String? get auth2AnonymousId => getStringWithName(auth2AnonymousIdKey); - set auth2AnonymousId(String? value) => setStringWithName(auth2AnonymousIdKey, value); + Future setAuth2AnonymousId(String? value) async => setStringWithName(auth2AnonymousIdKey, value); String get auth2AnonymousTokenKey => 'edu.illinois.rokwire.auth2.anonymous.token'; - Auth2Token? get auth2AnonymousToken => Auth2Token.fromJson(JsonUtils.decodeMap(getEncryptedStringWithName(auth2AnonymousTokenKey))); - set auth2AnonymousToken(Auth2Token? value) => setEncryptedStringWithName(auth2AnonymousTokenKey, JsonUtils.encode(value?.toJson())); + Future getAuth2AnonymousToken() async => Auth2Token.fromJson(JsonUtils.decodeMap(await getSecureStringWithName(auth2AnonymousTokenKey))); + Future setAuth2AnonymousToken(Auth2Token? value) async => setSecureStringWithName(auth2AnonymousTokenKey, JsonUtils.encode(value?.toJson())); String get auth2AnonymousPrefsKey => 'edu.illinois.rokwire.auth2.anonymous.prefs'; - Auth2UserPrefs? get auth2AnonymousPrefs => Auth2UserPrefs.fromJson(JsonUtils.decodeMap(getEncryptedStringWithName(auth2AnonymousPrefsKey))); - set auth2AnonymousPrefs(Auth2UserPrefs? value) => setEncryptedStringWithName(auth2AnonymousPrefsKey, JsonUtils.encode(value?.toJson())); + Future getAuth2AnonymousPrefs() async => Auth2UserPrefs.fromJson(JsonUtils.decodeMap(await getSecureStringWithName(auth2AnonymousPrefsKey))); + Future setAuth2AnonymousPrefs(Auth2UserPrefs? value) async => setSecureStringWithName(auth2AnonymousPrefsKey, JsonUtils.encode(value?.toJson())); String get auth2AnonymousProfileKey => 'edu.illinois.rokwire.auth2.anonymous.profile'; - Auth2UserProfile? get auth2AnonymousProfile => Auth2UserProfile.fromJson(JsonUtils.decodeMap(getEncryptedStringWithName(auth2AnonymousProfileKey))); - set auth2AnonymousProfile(Auth2UserProfile? value) => setEncryptedStringWithName(auth2AnonymousProfileKey, JsonUtils.encode(value?.toJson())); + Future getAuth2AnonymousProfile() async => Auth2UserProfile.fromJson(JsonUtils.decodeMap(await getSecureStringWithName(auth2AnonymousProfileKey))); + Future setAuth2AnonymousProfile(Auth2UserProfile? value) async => await setSecureStringWithName(auth2AnonymousProfileKey, JsonUtils.encode(value?.toJson())); String get auth2TokenKey => 'edu.illinois.rokwire.auth2.token'; - Auth2Token? get auth2Token => Auth2Token.fromJson(JsonUtils.decodeMap(getEncryptedStringWithName(auth2TokenKey))); - set auth2Token(Auth2Token? value) => setEncryptedStringWithName(auth2TokenKey, JsonUtils.encode(value?.toJson())); + Future getAuth2Token() async => Auth2Token.fromJson(JsonUtils.decodeMap(await getSecureStringWithName(auth2TokenKey))); + Future setAuth2Token(Auth2Token? value) async => await setSecureStringWithName(auth2TokenKey, JsonUtils.encode(value?.toJson())); + + String get auth2OidcTokenKey => 'edu.illinois.rokwire.auth2.oidc.token'; + Future getAuth2OidcToken() async => Auth2Token.fromJson(JsonUtils.decodeMap(await getSecureStringWithName(auth2OidcTokenKey))); + Future setAuth2OidcToken(Auth2Token? value) async => await setSecureStringWithName(auth2OidcTokenKey, JsonUtils.encode(value?.toJson())); String get auth2AccountKey => 'edu.illinois.rokwire.auth2.account'; - Auth2Account? get auth2Account => Auth2Account.fromJson(JsonUtils.decodeMap(getEncryptedStringWithName(auth2AccountKey))); - set auth2Account(Auth2Account? value) => setEncryptedStringWithName(auth2AccountKey, JsonUtils.encode(value?.toJson())); + Future getAuth2Account() async => Auth2Account.fromJson(JsonUtils.decodeMap(await getSecureStringWithName(auth2AccountKey))); + Future setAuth2Account(Auth2Account? value) async => await setSecureStringWithName(auth2AccountKey, JsonUtils.encode(value?.toJson())); // Http Proxy String get httpProxyEnabledKey => 'edu.illinois.rokwire.http_proxy.enabled'; @@ -293,6 +348,11 @@ class Storage with Service { String? get selectedLanguage => getStringWithName(selectedLanguageKey); set selectedLanguage(String? value) => setStringWithName(selectedLanguageKey, value); + // Theme + static const String selectedThemeKey = 'edu.illinois.rokwire.theme.selected'; + String? get selectedTheme => getStringWithName(selectedThemeKey); + set selectedTheme(String? value) => setStringWithName(selectedThemeKey, value); + // Inbox String get inboxFirebaseMessagingTokenKey => 'edu.illinois.rokwire.inbox.firebase_messaging.token'; String? get inboxFirebaseMessagingToken => getStringWithName(inboxFirebaseMessagingTokenKey); diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 70a225538..3c74ddb89 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -20,19 +20,22 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/network.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; +import 'package:rokwire_plugin/service/storage.dart'; +import 'package:rokwire_plugin/ui/widgets/ui_image.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:path/path.dart'; import 'package:http/http.dart' as http; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; - class Styles extends Service implements NotificationsListener{ static const String notifyChanged = "edu.illinois.rokwire.styles.changed"; @@ -55,21 +58,14 @@ class Styles extends Service implements NotificationsListener{ Map? _netAssetsStyles; Map? _debugAssetsStyles; - UiColors? _colors; - UiColors get colors => _colors ?? _emptyColors; - static UiColors get appColors => _instance?._colors ?? _emptyColors; - - UiFontFamilies? _fontFamilies; - UiFontFamilies get fontFamilies => _fontFamilies ?? _emptyFontFamilies; - static UiFontFamilies get appFontFamilies => _instance?._fontFamilies ?? _emptyFontFamilies; - - UiTextStyles? _textStyles; - UiTextStyles get textStyles => _textStyles ?? _emptyTextStyles; - static UiTextStyles get appTextStyles => _instance?._textStyles ?? _emptyTextStyles; + Map _themes = {}; + List get themes => _themes.keys.toList(); - UiImages? _images; - UiImages get images => _images ?? _emptyImages; - static UiImages get appImages => _instance?._images ?? _emptyImages; + UiTheme? _theme; + UiColors get colors => _theme?.colors ?? _emptyColors; + UiFontFamilies get fontFamilies => _theme?.fontFamilies ?? _emptyFontFamilies; + UiTextStyles get textStyles => _theme?.textStyles ?? _emptyTextStyles; + UiImages get images => _theme?.images ?? _emptyImages; // Singletone Factory @@ -89,7 +85,10 @@ class Styles extends Service implements NotificationsListener{ @override void createService() { - NotificationService().subscribe(this, AppLivecycle.notifyStateChanged); + NotificationService().subscribe(this, [ + Service.notifyInitialized, + AppLifecycle.notifyStateChanged + ]); } @override @@ -99,13 +98,23 @@ class Styles extends Service implements NotificationsListener{ @override Future initService() async { - - _assetsDir = await getAssetsDir(); - _assetsManifest = await loadAssetsManifest(); - _assetsStyles = await loadFromAssets(assetsKey); - _appAssetsStyles = await loadFromAssets(appAssetsKey); - _netAssetsStyles = await loadFromCache(netCacheFileName); - _debugAssetsStyles = await loadFromCache(debugCacheFileName); + List> futures = [ + loadAssetsManifest(), + loadFromAssets(assetsKey), + if (!kIsWeb) + loadFromAssets(appAssetsKey), + if (!kIsWeb) + getAssetsDir(), + ]; + List results = await Future.wait(futures); + _assetsManifest = (0 < results.length) ? results[0] : null; + _assetsStyles = (1 < results.length) ? results[1] : null; + _appAssetsStyles = (2 < results.length) ? results[2] : null; + _assetsDir = (3 < results.length) ? results[3] : null; + + if (_assetsDir != null) { + await initCaches(); + } if ((_assetsStyles != null) || (_appAssetsStyles != null) || (_netAssetsStyles != null) || (_debugAssetsStyles != null)) { await build(); @@ -122,21 +131,36 @@ class Styles extends Service implements NotificationsListener{ } } - @override - Set get serviceDependsOn { - return { Config() }; - } - // NotificationsListener @override void onNotification(String name, dynamic param) { - if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + + if (name == Service.notifyInitialized) { + onServiceInitialized(param is Service ? param : null); + } + if (name == AppLifecycle.notifyStateChanged) { + onAppLifecycleStateChanged((param is AppLifecycleState) ? param : null); + } + } + + @protected + Future onServiceInitialized(Service? service) async { + if (isInitialized) { + if ((service == Config()) && !kIsWeb) { + _assetsDir = await getAssetsDir(); + if (_assetsDir != null) { + await initCaches(); + } + if ((_netAssetsStyles != null) || (_debugAssetsStyles != null)) { + await build(); + } + } } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + @protected + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } @@ -170,7 +194,9 @@ class Styles extends Service implements NotificationsListener{ @protected Future?> loadFromAssets(String assetsKey) async { try { return JsonUtils.decodeMap(await rootBundle.loadString(assetsKey)); } - catch(e) { debugPrint(e.toString()); } + catch(e) { + debugPrint(e.toString()); + } return null; } @@ -217,7 +243,7 @@ class Styles extends Service implements NotificationsListener{ @protected Future loadContentStringFromNet() async { - if (Config().assetsUrl != null) { + if (StringUtils.isNotEmpty(Config().assetsUrl)) { http.Response? response = await Network().get("${Config().assetsUrl}/$netAssetFileName"); return (response?.statusCode == 200) ? response?.body : null; } @@ -238,13 +264,89 @@ class Styles extends Service implements NotificationsListener{ } } + @protected + Future initCaches() async { + List> futures = [ + loadFromCache(netCacheFileName), + loadFromCache(debugCacheFileName), + ]; + + List results = await Future.wait(futures); + _netAssetsStyles = results[0]; + _debugAssetsStyles = results[1]; + } + @protected Future build() async { Map styles = contentMap; - _colors = await compute(UiColors.fromJson, JsonUtils.mapValue(styles['color'])); - _fontFamilies = await compute(UiFontFamilies.fromJson, JsonUtils.mapValue(styles['font_family'])); - _textStyles = await compute(UiTextStyles.fromCreationParam, _UiTextStylesCreationParam(JsonUtils.mapValue(styles['text_style']), colors: _colors, fontFamilies: _fontFamilies)); - _images = await compute(UiImages.fromCreationParam, _UiImagesCreationParam(JsonUtils.mapValue(styles['image']), colors: _colors, assetPathResolver: resolveImageAssetPath)); + Map> themes = JsonUtils.mapOfStringToMapOfStringsValue(styles['themes']) ?? {}; + + List> futures = [ + UiTheme.fromJson(styles, resolveImageAssetPath), + ]; + + List themeKeys = []; + List defaultThemeKeys = []; + for (MapEntry> theme in themes.entries) { + if (theme.value.isNotEmpty) { + Map? themeData = MapUtils.mergeToNew(styles, theme.value, level: 1); + futures.add(UiTheme.fromJson(themeData, resolveImageAssetPath)); + themeKeys.add(theme.key); + } else { + defaultThemeKeys.add(theme.key); + } + } + + List results = await Future.wait(futures); + UiTheme defaultTheme = results[0]; + _themes['default'] = defaultTheme; + + for (int i = 0; i < themeKeys.length; i++) { + String key = themeKeys[i]; + _themes[key] = results[i + 1]; + } + for (String key in defaultThemeKeys) { + _themes[key] = defaultTheme; + } + + _applySelectedTheme(); + } + + void _applySelectedTheme() { + String? selectedTheme = _getSelectedTheme(); + UiTheme? themeData = _themes[selectedTheme ?? 'default']; + if (themeData != null) { + _theme = themeData; + } + } + + Future applyTheme(String? theme) async { + Storage().selectedTheme = theme; + _applySelectedTheme(); + NotificationService().notify(notifyChanged, null); + } + + Future updateSystemTheme() async { + if (Storage().selectedTheme == AppThemes.system) { + Styles().applyTheme(AppThemes.system); + } + } + + String? _getSelectedTheme() { + String? selectedTheme = Storage().selectedTheme; + if (selectedTheme == null) { + Storage().selectedTheme = AppThemes.system; + } + + if (selectedTheme == AppThemes.system) { + var brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; + if (brightness == Brightness.light) { + selectedTheme = AppThemes.light; + } else if (brightness == Brightness.dark) { + selectedTheme = AppThemes.dark; + } + } + return selectedTheme; } Map get contentMap { @@ -294,6 +396,33 @@ class Styles extends Service implements NotificationsListener{ } } +class UiTheme { + UiColors? colors; + UiFontFamilies? fontFamilies; + UiTextStyles? textStyles; + UiImages? images; + + UiTheme({this.colors, this.fontFamilies, this.textStyles, this.images}); + + static Future fromJson(Map styles, String Function(Uri)? assetPathResolver) async { + List> futures = [ + compute(UiColors.fromJson, JsonUtils.mapValue(styles['color'])), + compute(UiFontFamilies.fromJson, JsonUtils.mapValue(styles['font_family'])), + ]; + List results = await Future.wait(futures); + UiColors? colors = results[0]; + UiFontFamilies? fontFamilies = results[1]; + + futures = [ + compute(UiTextStyles.fromCreationParam, _UiTextStylesCreationParam(JsonUtils.mapValue(styles['text_style']), colors: colors, fontFamilies: fontFamilies)), + compute(UiImages.fromCreationParam, _UiImagesCreationParam(JsonUtils.mapValue(styles['image']), colors: colors, assetPathResolver: assetPathResolver)), + ]; + results = await Future.wait(futures); + + return UiTheme(colors: colors, fontFamilies: fontFamilies, textStyles: results[0], images: results[1]); + } +} + class UiColors { final Map colorMap; @@ -315,69 +444,125 @@ class UiColors { return UiColors(colors); } + @Deprecated("Use AppColors instead") Color get fillColorPrimary => colorMap['fillColorPrimary'] ?? const Color(0xFF002855); + @Deprecated("Use AppColors instead") Color get fillColorPrimaryTransparent03 => colorMap['fillColorPrimaryTransparent03'] ?? const Color(0x4D002855); + @Deprecated("Use AppColors instead") Color get fillColorPrimaryTransparent05 => colorMap['fillColorPrimaryTransparent05'] ?? const Color(0x80002855); + @Deprecated("Use AppColors instead") Color get fillColorPrimaryTransparent09 => colorMap['fillColorPrimaryTransparent09'] ?? const Color(0xE6002855); + @Deprecated("Use AppColors instead") Color get fillColorPrimaryTransparent015 => colorMap['fillColorPrimaryTransparent015'] ?? const Color(0x26002855); + @Deprecated("Use AppColors instead") Color get textColorPrimary => colorMap['textColorPrimary'] ?? const Color(0xFFFFFFFF); + @Deprecated("Use AppColors instead") Color get textColorPrimaryVariant => colorMap['textColorPrimaryVariant'] ?? const Color(0xFFFFFFFF); + @Deprecated("Use AppColors instead") Color get textColorDisabled => colorMap['textColorDisabled'] ?? const Color(0xFF5C5C5C); + @Deprecated("Use AppColors instead") Color get fillColorPrimaryVariant => colorMap['fillColorPrimaryVariant'] ?? const Color(0xFF0F2040); + @Deprecated("Use AppColors instead") Color get fillColorSecondary => colorMap['fillColorSecondary'] ?? const Color(0xFFE84A27); + @Deprecated("Use AppColors instead") Color get fillColorSecondaryTransparent05 => colorMap['fillColorSecondaryTransparent05'] ?? const Color(0x80E84A27); + @Deprecated("Use AppColors instead") Color get fillColorSecondaryVariant => colorMap['fillColorSecondaryVariant'] ?? const Color(0xFFCF3C1B); + @Deprecated("Use AppColors instead") Color get textColorSecondary => colorMap['textColorSecondary'] ?? const Color(0xFFFFFFFF); + @Deprecated("Use AppColors instead") Color get textColorSecondaryVariant => colorMap['textColorSecondaryVariant'] ?? const Color(0xFFFFFF); + @Deprecated("Use AppColors instead") Color get gradientColorPrimary => colorMap['gradientColorPrimary'] ?? const Color(0xFF244372); + @Deprecated("Use AppColors instead") Color get surface => colorMap['surface'] ?? const Color(0xFFFFFFFF); + @Deprecated("Use AppColors instead") Color get textSurface => colorMap['textSurface'] ?? const Color(0xFF404040); + @Deprecated("Use AppColors instead") Color get textSurfaceTransparent15 => colorMap['textSurfaceTransparent15'] ?? const Color(0x26404040); + @Deprecated("Use AppColors instead") Color get surfaceAccent => colorMap['surfaceAccent'] ?? const Color(0xFFDADDE1); + @Deprecated("Use AppColors instead") Color get surfaceAccentTransparent15 => colorMap['surfaceAccentTransparent15'] ?? const Color(0x26DADDE1); + @Deprecated("Use AppColors instead") Color get textSurfaceAccent => colorMap['textSurfaceAccent'] ?? const Color(0xFF404040); + @Deprecated("Use AppColors instead") Color get background => colorMap['background'] ?? const Color(0xFFF5F5F5); + @Deprecated("Use AppColors instead") Color get textBackground => colorMap['textBackground'] ?? const Color(0xFF404040); + @Deprecated("Use AppColors instead") Color get backgroundVariant => colorMap['backgroundVariant'] ?? const Color(0xFFE8E9EA); + @Deprecated("Use AppColors instead") Color get textBackgroundVariant => colorMap['textBackgroundVariant'] ?? const Color(0xFF404040); + @Deprecated("Use AppColors instead") Color get textBackgroundVariant2 => colorMap['textBackgroundVariant2'] ?? const Color(0xFFE7E7E7); + @Deprecated("Use AppColors instead") Color get accentColor1 => colorMap['accentColor1'] ?? const Color(0xFFE84A27); + @Deprecated("Use AppColors instead") Color get accentColor2 => colorMap['accentColor2'] ?? const Color(0xFF5FA7A3); + @Deprecated("Use AppColors instead") Color get accentColor3 => colorMap['accentColor3'] ?? const Color(0xFF5182CF); + @Deprecated("Use AppColors instead") Color get accentColor4 => colorMap['accentColor4'] ?? const Color(0xFF9318BB); + @Deprecated("Use AppColors instead") Color get iconColor => colorMap['iconColor'] ?? const Color(0xFFE84A27); + @Deprecated("Use AppColors instead") Color get eventColor => colorMap['eventColor'] ?? const Color(0xFFE54B30); + @Deprecated("Use AppColors instead") Color get diningColor => colorMap['diningColor'] ?? const Color(0xFFF09842); + @Deprecated("Use AppColors instead") Color get placeColor => colorMap['placeColor'] ?? const Color(0xFF62A7A3); + @Deprecated("Use AppColors instead") Color get mtdColor => colorMap['mtdColor'] ?? const Color(0xFF2376E5); + @Deprecated("Use AppColors instead") Color get white => colorMap['white'] ?? const Color(0xFFFFFFFF); + @Deprecated("Use AppColors instead") Color get whiteTransparent01 => colorMap['whiteTransparent01'] ?? const Color(0x1AFFFFFF); + @Deprecated("Use AppColors instead") Color get whiteTransparent06 => colorMap['whiteTransparent06'] ?? const Color(0x99FFFFFF); + @Deprecated("Use AppColors instead") Color get black => colorMap['black'] ?? const Color(0xFF000000); + @Deprecated("Use AppColors instead") Color get blackTransparent06 => colorMap['blackTransparent06'] ?? const Color(0x99000000); + @Deprecated("Use AppColors instead") Color get blackTransparent018 => colorMap['blackTransparent018'] ?? const Color(0x30000000); + @Deprecated("Use AppColors instead") Color get blackTransparent038 => colorMap['blackTransparent038'] ?? const Color(0x61000000); + @Deprecated("Use AppColors instead") Color get mediumGray => colorMap['mediumGray'] ?? const Color(0xFF717372); + @Deprecated("Use AppColors instead") Color get mediumGray1 => colorMap['mediumGray1'] ?? const Color(0xFF535353); + @Deprecated("Use AppColors instead") Color get mediumGray2 => colorMap['mediumGray2'] ?? const Color(0xFF979797); + @Deprecated("Use AppColors instead") Color get lightGray => colorMap['lightGray'] ?? const Color(0xFFEDEDED); + @Deprecated("Use AppColors instead") Color get disabledTextColor => colorMap['disabledTextColor'] ?? const Color(0xFFBDBDBD); + @Deprecated("Use AppColors instead") Color get disabledTextColorTwo => colorMap['disabledTextColorTwo'] ?? const Color(0xFF868F9D); + @Deprecated("Use AppColors instead") Color get dividerLine => colorMap['dividerLine'] ?? const Color(0xFF535353); + @Deprecated("Use AppColors instead") Color get dividerLineAccent => colorMap['dividerLineAccent'] ?? const Color(0xFFDADADA); + @Deprecated("Use AppColors instead") Color get mango => colorMap['mango'] ?? const Color(0xFFf29835); + @Deprecated("Use AppColors instead") Color get greenAccent => colorMap['greenAccent'] ?? const Color(0xFF69F0AE); + @Deprecated("Use AppColors instead") Color get saferLocationWaitTimeColorRed => colorMap['saferLocationWaitTimeColorRed'] ?? const Color(0xFFFF0000); + @Deprecated("Use AppColors instead") Color get saferLocationWaitTimeColorYellow => colorMap['saferLocationWaitTimeColorYellow'] ?? const Color(0xFFFFFF00); + @Deprecated("Use AppColors instead") Color get saferLocationWaitTimeColorGreen => colorMap['saferLocationWaitTimeColorGreen'] ?? const Color(0xFF00FF00); + @Deprecated("Use AppColors instead") Color get saferLocationWaitTimeColorGrey => colorMap['saferLocationWaitTimeColorGrey'] ?? const Color(0xFF808080); Color? getColor(String key) => colorMap[key]; @@ -421,21 +606,37 @@ class UiFontFamilies { return UiFontFamilies(familyMap ?? {}); } + @Deprecated("Use AppFontFamilies instead") String get black => familyMap["black"] ?? 'ProximaNovaBlack'; + @Deprecated("Use AppFontFamilies instead") String get blackIt => familyMap["black_italic"] ?? 'ProximaNovaBlackIt'; + @Deprecated("Use AppFontFamilies instead") String get bold => familyMap["bold"] ?? 'ProximaNovaBold'; + @Deprecated("Use AppFontFamilies instead") String get boldIt => familyMap["bold_italic"] ?? 'ProximaNovaBoldIt'; + @Deprecated("Use AppFontFamilies instead") String get extraBold => familyMap["extra_bold"] ?? 'ProximaNovaExtraBold'; + @Deprecated("Use AppFontFamilies instead") String get extraBoldIt => familyMap["extra_bold_italic"] ?? 'ProximaNovaExtraBoldIt'; + @Deprecated("Use AppFontFamilies instead") String get light => familyMap["light"] ?? 'ProximaNovaLight'; + @Deprecated("Use AppFontFamilies instead") String get lightIt => familyMap["light_italic"] ?? 'ProximaNovaLightIt'; + @Deprecated("Use AppFontFamilies instead") String get medium => familyMap["medium"] ?? 'ProximaNovaMedium'; + @Deprecated("Use AppFontFamilies instead") String get mediumIt => familyMap["medium_italic"] ?? 'ProximaNovaMediumIt'; + @Deprecated("Use AppFontFamilies instead") String get regular => familyMap["regular"] ?? 'ProximaNovaRegular'; + @Deprecated("Use AppFontFamilies instead") String get regularIt => familyMap["regular_italic"] ?? 'ProximaNovaRegularIt'; + @Deprecated("Use AppFontFamilies instead") String get semiBold => familyMap["semi_bold"] ?? 'ProximaNovaSemiBold'; + @Deprecated("Use AppFontFamilies instead") String get semiBoldIt => familyMap["semi_bold_italic"] ?? 'ProximaNovaSemiBoldIt'; + @Deprecated("Use AppFontFamilies instead") String get thin => familyMap["thin"] ?? 'ProximaNovaThin'; + @Deprecated("Use AppFontFamilies instead") String get thinIt => familyMap["thin_italic"] ?? 'ProximaNovaThinIt'; String? fromCode(String? code) => familyMap[code]; @@ -479,6 +680,7 @@ class UiTextStyles { double? fontHeight = JsonUtils.doubleValue(style['height']); String? fontFamily = JsonUtils.stringValue(style['font_family']); String? fontFamilyRef = fontFamilies?.fromCode(fontFamily); + FontStyle? fontStyle = _TextStyleUtils.textFontStyleFromString(JsonUtils.stringValue(style["font_style"])); TextDecoration? textDecoration = _TextStyleUtils.textDecorationFromString(JsonUtils.stringValue(style["decoration"])); TextOverflow? textOverflow = _TextStyleUtils.textOverflowFromString(JsonUtils.stringValue(style["overflow"])); TextDecorationStyle? decorationStyle = _TextStyleUtils.textDecorationStyleFromString(JsonUtils.stringValue(style["decoration_style"])); @@ -488,16 +690,21 @@ class UiTextStyles { double? decorationThickness = JsonUtils.doubleValue(style['decoration_thickness']); bool inherit = JsonUtils.boolValue(style["inherit"]) ?? true; - TextStyle textStyle = TextStyle(fontFamily: fontFamilyRef ?? fontFamily, fontSize: fontSize, color: color, letterSpacing: letterSpacing, wordSpacing: wordSpacing, decoration: textDecoration, - overflow: textOverflow, height: fontHeight, fontWeight: fontWeight, decorationThickness: decorationThickness, decorationStyle: decorationStyle, decorationColor: decorationColor, inherit: inherit); + TextStyle textStyle = TextStyle(fontFamily: fontFamilyRef ?? fontFamily, + fontSize: fontSize, color: color, letterSpacing: letterSpacing, + wordSpacing: wordSpacing, decoration: textDecoration, fontStyle: fontStyle, + overflow: textOverflow, height: fontHeight, fontWeight: fontWeight, + decorationThickness: decorationThickness, + decorationStyle: decorationStyle, decorationColor: decorationColor, + inherit: inherit); //Extending capabilities String? extendsKey = JsonUtils.stringValue(style['extends']); - Map? ancestorStyleMap = (StringUtils.isNotEmpty(extendsKey) && stylesJson!=null ? JsonUtils.mapValue(stylesJson[extendsKey]) : null); + Map? ancestorStyleMap = (StringUtils.isNotEmpty(extendsKey) && stylesJson!=null ? JsonUtils.mapValue(stylesJson[extendsKey]) : null); TextStyle? ancestorTextStyle = constructTextStyle(ancestorStyleMap, stylesJson: stylesJson, colors: colors, fontFamilies: fontFamilies); - bool overrides = JsonUtils.boolValue(style["override"]) ?? true; + bool overrides = JsonUtils.boolValue(style["override"]) ?? true; - if(ancestorTextStyle != null ){ + if (ancestorTextStyle != null) { return overrides ? ancestorTextStyle.merge(textStyle) : ancestorTextStyle; } @@ -524,55 +731,45 @@ class UiImages { static UiImages fromCreationParam(_UiImagesCreationParam param) => UiImages(param.imageMap, colors: param.colors, assetPathResolver: param.assetPathResolver); - Widget? getImage(String? imageKey, {ImageSpec? defaultSpec, Key? key, String? type, dynamic source, double? scale, double? size, - double? fill, dynamic weight, double? grade, double? opticalSize, - double? width, double? height, Color? color, String? semanticLabel, bool excludeFromSemantics = false, + UiImage? getImage(String? imageKey, {ImageSpec? defaultSpec, Widget? defaultWidget, Key? key, dynamic source, double? scale, double? size, + double? width, double? height, dynamic weight, Color? color, String? semanticLabel, bool excludeFromSemantics = false, + double? fill, double? grade, double? opticalSize, String? fontFamily, String? fontPackage, bool isAntiAlias = false, bool matchTextDirection = false, bool gaplessPlayback = false, AlignmentGeometry? alignment, Animation? opacity, BlendMode? colorBlendMode, BoxFit? fit, FilterQuality? filterQuality, ImageRepeat? repeat, - Rect? centerSlice, String? fontFamily, String? fontPackage, TextDirection? textDirection, Map? networkHeaders, + Rect? centerSlice, TextDirection? textDirection, Map? networkHeaders, Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Widget, ImageChunkEvent?)? loadingBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder} ) { Map imageJson = (imageMap != null && imageKey != null) ? JsonUtils.mapValue(imageMap![imageKey]) ?? {} : {}; - ImageSpec? imageSpec = ImageSpec.fromJson(imageJson) ?? defaultSpec; + ImageSpec? imageSpec = ImageSpec.fromJson(imageJson, colors: colors) ?? defaultSpec; if (imageSpec != null) { - if (imageSpec is FlutterImageSpec) { - return _getFlutterImage(imageSpec, type: type, source: source, key: key, - scale: scale, size: size, width: width, height: height, color: color, - semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics, - isAntiAlias: isAntiAlias, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, - alignment: alignment, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, filterQuality: filterQuality, - repeat: repeat, centerSlice: centerSlice, networkHeaders: networkHeaders, - frameBuilder: frameBuilder, loadingBuilder: loadingBuilder, errorBuilder: errorBuilder); - } else if (imageSpec is FontAwesomeImageSpec && (weight is String || weight == null)) { - return _getFaIcon(imageSpec, type: type, source: source, key: key, size: size ?? height ?? width, weight: weight, - color: color, textDirection: textDirection, semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics); - } else if (imageSpec is MaterialIconImageSpec && (weight is double || weight == null)) { - return _getMaterialIcon(imageSpec, type: type, source: source, key: key, size: size, fill: fill, weight: weight, - grade: grade, opticalSize: opticalSize, color: color, semanticLabel: semanticLabel, textDirection: textDirection, - excludeFromSemantics: excludeFromSemantics, fontFamily: fontFamily, fontPackage: fontPackage, matchTextDirection: matchTextDirection); - } else { - return null; - } - } - - // If no image definition for that key - try with asset name / network source - if (imageKey != null) { + imageSpec = ImageSpec.fromOther(imageSpec, source: source, scale: scale, size: size, + width: width, height: height, weight: weight, + fill: fill, grade: grade, opticalSize: opticalSize, + fontFamily: fontFamily, fontPackage: fontPackage, + color: color, semanticLabel: semanticLabel, isAntiAlias: isAntiAlias, + matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, + alignment: alignment, colorBlendMode: colorBlendMode, fit: fit, + filterQuality: filterQuality, repeat: repeat, textDirection: textDirection, + ); + } else if (imageKey != null) { + // If no image definition for that key - try with asset name / network source Uri? uri = Uri.tryParse(imageKey); if (uri != null) { - return _getDefaultFlutterImage(uri, key: key, - scale: scale, width: width ?? size, height: height ?? size, color: color, - semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics, - isAntiAlias: isAntiAlias, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, - alignment: alignment, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, filterQuality: filterQuality, - repeat: repeat, centerSlice: centerSlice, networkHeaders: networkHeaders, - frameBuilder: frameBuilder, loadingBuilder: loadingBuilder, errorBuilder: errorBuilder); + imageSpec = _getDefaultFlutterImageSpec(uri, + scale: scale, width: width ?? size, height: height ?? size, color: color, + semanticLabel: semanticLabel, isAntiAlias: isAntiAlias, matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, alignment: alignment,colorBlendMode: colorBlendMode, + fit: fit, filterQuality: filterQuality, repeat: repeat + ); } } - - return null; + + return UiImage(key: key, spec: imageSpec, defaultWidget: defaultWidget, excludeFromSemantics: excludeFromSemantics, + opacity: opacity, repeat: repeat, centerSlice: centerSlice, networkHeaders: networkHeaders, + frameBuilder: frameBuilder, loadingBuilder: loadingBuilder, errorBuilder: errorBuilder); } /* Example: @@ -590,14 +787,13 @@ class UiImages { "repeat":"noRepeat" } */ - - Image? _getFlutterImage(FlutterImageSpec imageSpec, { String? type, dynamic source, Key? key, + static Image? getFlutterImage(FlutterImageSpec imageSpec, { String? type, dynamic source, Key? key, double? scale, double? size, double? width, double? height, Color? color, String? semanticLabel, bool excludeFromSemantics = false, bool isAntiAlias = false, bool matchTextDirection = false, bool gaplessPlayback = false, - AlignmentGeometry? alignment, Animation? opacity, BlendMode? colorBlendMode, BoxFit? fit, FilterQuality? filterQuality, - ImageRepeat? repeat, Rect? centerSlice, Map? networkHeaders, Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, + AlignmentGeometry? alignment, Animation? opacity, BlendMode? colorBlendMode, BoxFit? fit, FilterQuality? filterQuality, + ImageRepeat? repeat, Rect? centerSlice, Map? networkHeaders, Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Widget, ImageChunkEvent?)? loadingBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder } - ) { + ) { type ??= imageSpec.type; source ??= imageSpec.source; @@ -613,7 +809,7 @@ class UiImages { fit ??= imageSpec.fit; filterQuality ??= imageSpec.filterQuality ?? FilterQuality.low; repeat ??= imageSpec.repeat ?? ImageRepeat.noRepeat; - + try { switch (type) { case 'flutter.asset': String? assetString = JsonUtils.stringValue(source); @@ -622,7 +818,7 @@ class UiImages { scale: scale, width: width, height: height, color: color, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, alignment: alignment, repeat: repeat, centerSlice: centerSlice, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, isAntiAlias: isAntiAlias, filterQuality: filterQuality, ) : null; - + case 'flutter.file': File? sourceFile = _ImageUtils.fileValue(source); return (sourceFile != null) ? Image.file(sourceFile, @@ -630,7 +826,7 @@ class UiImages { scale: scale ?? 1.0, width: width, height: height, color: color, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, alignment: alignment, repeat: repeat, centerSlice: centerSlice, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, isAntiAlias: isAntiAlias, filterQuality: filterQuality, ) : null; - + case 'flutter.network': String? urlString = JsonUtils.stringValue(source); return (urlString != null) ? Image.network(urlString, @@ -639,7 +835,7 @@ class UiImages { centerSlice: centerSlice, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, isAntiAlias: isAntiAlias, filterQuality: filterQuality, headers: networkHeaders ) : null; - + case 'flutter.memory': Uint8List? bytes = _ImageUtils.bytesValue(source); return (bytes != null) ? Image.memory(bytes, key: key, frameBuilder: frameBuilder, errorBuilder: errorBuilder, semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics, @@ -664,7 +860,7 @@ class UiImages { } */ - Widget? _getFaIcon(FontAwesomeImageSpec imageSpec, {String? type, dynamic source, + static Widget? getFaIcon(FontAwesomeImageSpec imageSpec, {String? type, dynamic source, Key? key, double? size, String? weight, Color? color, TextDirection? textDirection, String? semanticLabel, bool excludeFromSemantics = false}) { type ??= imageSpec.type; @@ -678,10 +874,9 @@ class UiImages { try { switch (type) { case 'fa.icon': - IconData? iconData = _ImageUtils.faIconDataValue(weight, codePoint: _ImageUtils.faCodePointValue(source)); return (iconData != null) ? ExcludeSemantics(excluding: excludeFromSemantics, child: - FaIcon(iconData, key: key, size: size, color: color, semanticLabel: semanticLabel, textDirection: textDirection,) + FaIcon(iconData, key: key, size: size, color: color, semanticLabel: semanticLabel, textDirection: textDirection,) ) : null; }} catch (e) { @@ -690,35 +885,35 @@ class UiImages { return null; } - Image? _getDefaultFlutterImage(Uri uri, { Key? key, double? scale, double? width, double? height, Color? color, String? semanticLabel, - bool excludeFromSemantics = false, bool isAntiAlias = false, bool matchTextDirection = false, bool gaplessPlayback = false, - AlignmentGeometry? alignment, Animation? opacity, BlendMode? colorBlendMode, BoxFit? fit, FilterQuality? filterQuality, - ImageRepeat? repeat, Rect? centerSlice, Map? networkHeaders, Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, - Widget Function(BuildContext, Widget, ImageChunkEvent?)? loadingBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder } + FlutterImageSpec? _getDefaultFlutterImageSpec(Uri uri, { double? scale, double? width, double? height, Color? color, String? semanticLabel, + bool isAntiAlias = false, bool matchTextDirection = false, bool gaplessPlayback = false, + AlignmentGeometry? alignment, BlendMode? colorBlendMode, BoxFit? fit, FilterQuality? filterQuality, + ImageRepeat? repeat } ) { try { scale ??= 1.0; alignment ??= Alignment.center; repeat ??= ImageRepeat.noRepeat; filterQuality ??= FilterQuality.low; + String? type; + String? source; if (uri.scheme.isNotEmpty) { - return Image.network(uri.toString(), - key: key, frameBuilder: frameBuilder, loadingBuilder: loadingBuilder, errorBuilder: errorBuilder, semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics, - scale: scale, width: width, height: height, color: color, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, alignment: alignment, repeat: repeat, - centerSlice: centerSlice, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, isAntiAlias: isAntiAlias, filterQuality: filterQuality, - headers: networkHeaders - ); + type = 'flutter.network'; + source = uri.toString(); } else if (uri.path.isNotEmpty) { - return Image.asset((assetPathResolver != null) ? assetPathResolver!(uri) : uri.path, - key: key, frameBuilder: frameBuilder, errorBuilder: errorBuilder, semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics, - scale: scale, width: width, height: height, color: color, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, alignment: alignment, repeat: repeat, - centerSlice: centerSlice, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, isAntiAlias: isAntiAlias, filterQuality: filterQuality, - ); + type = 'flutter.asset'; + source = assetPathResolver?.call(uri) ?? uri.path; } - else { - return null; + + if (type != null && source != null) { + return FlutterImageSpec(type: 'flutter.network', source: uri.toString(), + semanticLabel: semanticLabel, scale: scale, width: width, height: height, + color: color, colorBlendMode: colorBlendMode, fit: fit, alignment: alignment, + repeat: repeat, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, filterQuality: filterQuality, + ); } } catch(e) { @@ -727,7 +922,7 @@ class UiImages { return null; } - Widget? _getMaterialIcon(MaterialIconImageSpec imageSpec, { String? type, dynamic source, Key? key, + static Widget? getMaterialIcon(MaterialIconImageSpec imageSpec, { String? type, dynamic source, Key? key, double? size, double? fill, double? weight, double? grade, double? opticalSize, Color? color, String? fontFamily, String? fontPackage, TextDirection? textDirection, bool? matchTextDirection, String? semanticLabel, bool excludeFromSemantics = false, @@ -735,13 +930,13 @@ class UiImages { // TODO: Did not include Icon shadows type ??= imageSpec.type; source ??= imageSpec.source; - + size ??= imageSpec.size; fill ??= imageSpec.fill; weight ??= imageSpec.weight; grade ??= imageSpec.grade; opticalSize ??= imageSpec.opticalSize; - + color ??= imageSpec.color; fontFamily ??= imageSpec.fontFamily; fontPackage ??= imageSpec.fontPackage; @@ -780,22 +975,22 @@ abstract class ImageSpec { const ImageSpec({required this.type, this.source, this.size, this.color, this.semanticLabel}); - static ImageSpec? fromJson(Map json) { + static ImageSpec? fromJson(Map json, {UiColors? colors}) { String? type = JsonUtils.stringValue(json['type']); if (type == null) { return null; } else if (type.startsWith('flutter.')) { - return FlutterImageSpec.fromJson(json); + return FlutterImageSpec.fromJson(json, colors: colors); } else if (type.startsWith('fa.')) { - return FontAwesomeImageSpec.fromJson(json); + return FontAwesomeImageSpec.fromJson(json, colors: colors); } else if (type.startsWith('material.')) { - return MaterialIconImageSpec.fromJson(json); + return MaterialIconImageSpec.fromJson(json, colors: colors); } return null; } - factory ImageSpec.baseFromJson(Map json) { - Color? color = _ImageUtils.colorValue(JsonUtils.stringValue(json['color']), colors: Styles().colors); + factory ImageSpec.baseFromJson(Map json, {UiColors? colors}) { + Color? color = _ImageUtils.colorValue(JsonUtils.stringValue(json['color']), colors: colors ?? Styles().colors); return _BaseImageSpec( type: JsonUtils.stringValue(json['type']) ?? '', source: json['src'], @@ -804,6 +999,58 @@ abstract class ImageSpec { semanticLabel: JsonUtils.stringValue(json['semantic_label']), ); } + + factory ImageSpec.fromOther(ImageSpec spec, {dynamic source, double? scale, double? size, + double? width, double? height, dynamic weight, Color? color, String? semanticLabel, + double? fill, double? grade, double? opticalSize, String? fontFamily, String? fontPackage, + bool? isAntiAlias, bool? matchTextDirection, bool? gaplessPlayback, AlignmentGeometry? alignment, + BlendMode? colorBlendMode, BoxFit? fit, FilterQuality? filterQuality, ImageRepeat? repeat, + TextDirection? textDirection}) { + ImageSpec imageSpec = spec; + String type = imageSpec.type; + source ??= imageSpec.source; + size ??= imageSpec.size; + color ??= imageSpec.color; + + if (imageSpec is FlutterImageSpec) { + scale ??= imageSpec.scale; + width ??= imageSpec.width; + height ??= imageSpec.height; + alignment ??= imageSpec.alignment; + colorBlendMode ??= imageSpec.colorBlendMode; + fit ??= imageSpec.fit; + filterQuality ??= imageSpec.filterQuality; + repeat ??= imageSpec.repeat; + matchTextDirection ?? imageSpec.matchTextDirection; + + imageSpec = FlutterImageSpec(type: type, source: source, size: size, color: color, + scale: scale, width: width, height: height, isAntiAlias: isAntiAlias, + matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, + alignment: alignment, colorBlendMode: colorBlendMode, fit: fit, + filterQuality: filterQuality, repeat: repeat); + } else if (imageSpec is FontAwesomeImageSpec) { + weight ??= imageSpec.weight; + textDirection ??= imageSpec.textDirection; + semanticLabel ??= imageSpec.semanticLabel; + + imageSpec = FontAwesomeImageSpec(type: type, source: source, size: size, color: color, + semanticLabel: semanticLabel, weight: weight, textDirection: textDirection); + } else if (imageSpec is MaterialIconImageSpec) { + weight ??= imageSpec.weight; + textDirection ??= imageSpec.textDirection; + matchTextDirection ?? imageSpec.matchTextDirection; + fill ??= imageSpec.fill; + grade ??= imageSpec.grade; + opticalSize ??= imageSpec.opticalSize; + fontFamily ??= imageSpec.fontFamily; + fontPackage ??= imageSpec.fontPackage; + + imageSpec = MaterialIconImageSpec(type: type, source: source, size: size, color: color, + semanticLabel: semanticLabel, fill: fill, weight: weight, grade: grade, opticalSize: opticalSize, + fontFamily: fontFamily, fontPackage: fontPackage, textDirection: textDirection, matchTextDirection: matchTextDirection); + } + return imageSpec; + } } class _BaseImageSpec extends ImageSpec { @@ -834,7 +1081,7 @@ class FlutterImageSpec extends ImageSpec { this.alignment, this.colorBlendMode, this.fit, this.filterQuality, this.repeat}) : super(type: base.type, source: base.source, size: base.size, color: base.color, semanticLabel: base.semanticLabel); - factory FlutterImageSpec.fromJson(Map json) { + factory FlutterImageSpec.fromJson(Map json, {UiColors? colors}) { ImageSpec base = ImageSpec.baseFromJson(json); return FlutterImageSpec.fromBase(base, scale: JsonUtils.doubleValue(json['scale']), @@ -863,8 +1110,8 @@ class FontAwesomeImageSpec extends ImageSpec { FontAwesomeImageSpec.fromBase(ImageSpec base, {this.weight, this.textDirection}) : super(type: base.type, source: base.source, size: base.size, color: base.color, semanticLabel: base.semanticLabel); - factory FontAwesomeImageSpec.fromJson(Map json) { - ImageSpec base = ImageSpec.baseFromJson(json); + factory FontAwesomeImageSpec.fromJson(Map json, {UiColors? colors}) { + ImageSpec base = ImageSpec.baseFromJson(json, colors: colors); TextDirection? textDirection = _ImageUtils.lookup(TextDirection.values, JsonUtils.stringValue(json['text_direction'])); return FontAwesomeImageSpec.fromBase(base, weight: JsonUtils.stringValue(json['weight']), @@ -893,8 +1140,8 @@ class MaterialIconImageSpec extends ImageSpec { this.fontFamily, this.fontPackage, this.textDirection, this.matchTextDirection}) : super(type: base.type, source: base.source, size: base.size, color: base.color, semanticLabel: base.semanticLabel); - factory MaterialIconImageSpec.fromJson(Map json) { - ImageSpec base = ImageSpec.baseFromJson(json); + factory MaterialIconImageSpec.fromJson(Map json, {UiColors? colors}) { + ImageSpec base = ImageSpec.baseFromJson(json, colors: colors); return MaterialIconImageSpec.fromBase(base, fill: JsonUtils.doubleValue(json['fill']), weight: JsonUtils.doubleValue(json['weight']), @@ -909,7 +1156,7 @@ class MaterialIconImageSpec extends ImageSpec { } class _ImageUtils { - + static File? fileValue(dynamic value) { if (value is File) { return value; @@ -1013,6 +1260,14 @@ class _ImageUtils { class _TextStyleUtils { + static FontStyle? textFontStyleFromString(String? value) { + switch (value) { + case "normal" : return FontStyle.normal; + case "italic" : return FontStyle.italic; + default : return null; + } + } + static TextDecoration? textDecorationFromString(String? decoration){ switch(decoration){ case "lineThrough" : return TextDecoration.lineThrough; diff --git a/lib/ui/panels/modal_image_panel.dart b/lib/ui/panels/modal_image_panel.dart index 73cdee29d..5372e1c67 100644 --- a/lib/ui/panels/modal_image_panel.dart +++ b/lib/ui/panels/modal_image_panel.dart @@ -15,6 +15,7 @@ */ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -120,7 +121,7 @@ class ModalImagePanel extends StatelessWidget { return closeWidget ?? Semantics(label: closeLabel ?? "Close Button", hint: closeHint, button: true, focusable: true, focused: true, child: GestureDetector(onTap: () => _onClose(context), child: Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: - Text('\u00D7', style: TextStyle(color: Styles().colors.white, fontFamily: Styles().fontFamilies.medium, fontSize: 50),), + Text('\u00D7', style: TextStyle(color: AppColors.textLight, fontFamily: AppFontFamilies.medium, fontSize: 50),), ), ) ); @@ -128,7 +129,7 @@ class ModalImagePanel extends StatelessWidget { Widget _buildProgressWidget(BuildContext context, ImageChunkEvent progress) { return progressWidget ?? SizedBox(height: progressSize.width, width: 24, child: - CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? Styles().colors.white), + CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? AppColors.surface), value: progress.expectedTotalBytes != null ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null), ); } @@ -136,7 +137,6 @@ class ModalImagePanel extends StatelessWidget { Widget _buildPinchZoomControl({required Widget child}) => PinchZoom( child: child, - resetDuration: const Duration(milliseconds: 100), maxScale: 4, onZoomStart: (){print('Start zooming');}, onZoomEnd: (){print('Stop zooming');}, diff --git a/lib/ui/panels/rule_element_creation_panel.dart b/lib/ui/panels/rule_element_creation_panel.dart index 50751f8f0..cdfa6da1e 100644 --- a/lib/ui/panels/rule_element_creation_panel.dart +++ b/lib/ui/panels/rule_element_creation_panel.dart @@ -16,6 +16,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/actions.dart'; import 'package:rokwire_plugin/model/alert.dart'; @@ -23,7 +24,6 @@ import 'package:rokwire_plugin/model/rules.dart'; import 'package:rokwire_plugin/model/survey.dart'; import 'package:rokwire_plugin/service/localization.dart'; import 'package:rokwire_plugin/service/surveys.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/panels/survey_data_options_panel.dart'; import 'package:rokwire_plugin/ui/widgets/form_field.dart'; import 'package:rokwire_plugin/ui/widgets/header_bar.dart'; @@ -237,7 +237,7 @@ class _RuleElementCreationPanelState extends State { return Scaffold( appBar: const HeaderBar(title: "Edit Rule Element"), bottomNavigationBar: widget.tabBar, - backgroundColor: Styles().colors.background, + backgroundColor: AppColors.background, body: SurveyElementCreationWidget(body: _buildRuleElement(), completionOptions: _buildDone(), scrollController: _scrollController,), ); } @@ -427,9 +427,9 @@ class _RuleElementCreationPanelState extends State { Widget _buildDone() { return Padding(padding: const EdgeInsets.all(8.0), child: RoundedButton( label: 'Done', - borderColor: Styles().colors.fillColorPrimaryVariant, - backgroundColor: Styles().colors.surface, - textStyle: Styles().textStyles.getTextStyle('widget.detail.large.fat'), + borderColor: AppColors.fillColorPrimaryVariant, + backgroundColor: AppColors.surface, + textStyle: AppTextStyles.widgetDetailLargeBold, onTap: _onTapDone, )); } diff --git a/lib/ui/panels/survey_creation_panel.dart b/lib/ui/panels/survey_creation_panel.dart index 6fc7342b6..6b7820a7a 100644 --- a/lib/ui/panels/survey_creation_panel.dart +++ b/lib/ui/panels/survey_creation_panel.dart @@ -16,12 +16,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/rules.dart'; import 'package:rokwire_plugin/model/survey.dart'; import 'package:rokwire_plugin/service/localization.dart'; import 'package:rokwire_plugin/service/rules.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/service/surveys.dart'; import 'package:rokwire_plugin/ui/panels/rule_element_creation_panel.dart'; import 'package:rokwire_plugin/ui/panels/survey_panel.dart'; @@ -182,7 +182,7 @@ class _SurveyCreationPanelState extends State { return Scaffold( appBar: HeaderBar(title: widget.survey != null ? "Update Survey" : "Create Survey"), bottomNavigationBar: widget.tabBar, - backgroundColor: Styles().colors.background, + backgroundColor: AppColors.background, body: SurveyElementCreationWidget(body: _buildSurveyCreationTools(), completionOptions: _buildPreviewAndSave(), scrollController: _scrollController,), ); } @@ -271,24 +271,24 @@ class _SurveyCreationPanelState extends State { )), ],)); } - return Text("Maximum question branch depth ($_maxBranchDepth) exceeded. Your survey may not be shown correctly.", style: Styles().textStyles.getTextStyle('widget.error.regular.fat')); + return Text("Maximum question branch depth ($_maxBranchDepth) exceeded. Your survey may not be shown correctly.", style: AppTextStyles.widgetErrorRegularBold); } Widget _buildPreviewAndSave() { return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Flexible(flex: 1, child: Padding(padding: const EdgeInsets.all(8.0), child: RoundedButton( label: 'Preview', - borderColor: Styles().colors.getColor("fillColorPrimaryVariant"), - backgroundColor: Styles().colors.surface, - textStyle: Styles().textStyles.getTextStyle('widget.detail.large.fat'), + borderColor: AppColors.fillColorPrimaryVariant, + backgroundColor: AppColors.surface, + textStyle: AppTextStyles.widgetDetailLargeBold, onTap: _onTapPreview, ))), Flexible(flex: 1, child: Padding(padding: const EdgeInsets.all(8.0), child: Stack(children: [ RoundedButton( label: 'Save', - borderColor: Styles().colors.getColor("fillColorPrimaryVariant"), - backgroundColor: Styles().colors.surface, - textStyle: Styles().textStyles.getTextStyle('widget.detail.large.fat'), + borderColor: AppColors.fillColorPrimaryVariant, + backgroundColor: AppColors.surface, + textStyle: AppTextStyles.widgetDetailLargeBold, onTap: _onTapSave, enabled: !_loading ), diff --git a/lib/ui/panels/survey_data_creation_panel.dart b/lib/ui/panels/survey_data_creation_panel.dart index 388ec8fc3..1768522c2 100644 --- a/lib/ui/panels/survey_data_creation_panel.dart +++ b/lib/ui/panels/survey_data_creation_panel.dart @@ -16,12 +16,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/actions.dart'; import 'package:rokwire_plugin/model/options.dart'; import 'package:rokwire_plugin/model/rules.dart'; import 'package:rokwire_plugin/model/survey.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/panels/rule_element_creation_panel.dart'; import 'package:rokwire_plugin/ui/panels/survey_data_options_panel.dart'; import 'package:rokwire_plugin/ui/popups/popup_message.dart'; @@ -110,7 +110,7 @@ class _SurveyDataCreationPanelState extends State { return Scaffold( appBar: const HeaderBar(title: "Edit Survey Data"), bottomNavigationBar: widget.tabBar, - backgroundColor: Styles().colors.background, + backgroundColor: AppColors.background, body: SurveyElementCreationWidget(body: _buildSurveyDataComponents(), completionOptions: _buildDone(), scrollController: _scrollController,), ); } @@ -254,15 +254,15 @@ class _SurveyDataCreationPanelState extends State { // defaultResponseRule Visibility(visible: _data is! SurveyDataResult, child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: Styles().colors.getColor('surface')), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: AppColors.surface), padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(top: 16), child: Column(children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: Text("Default Response Rule", style: Styles().textStyles.getTextStyle('widget.message.regular'), maxLines: 2, overflow: TextOverflow.ellipsis,)), + Expanded(child: Text("Default Response Rule", style: AppTextStyles.widgetMessageRegular, maxLines: 2, overflow: TextOverflow.ellipsis,)), GestureDetector( onTap: _onTapManageDefaultResponseRule, - child: Text(_data.defaultResponseRule == null ? "Create" : "Remove", style: Styles().textStyles.getTextStyle('widget.button.title.medium.underline')) + child: Text(_data.defaultResponseRule == null ? "Create" : "Remove", style: AppTextStyles.widgetButtonTitleMediumUnderline) ), ],), Visibility(visible: _data.defaultResponseRule != null, child: Padding(padding: const EdgeInsets.only(top: 16), child: @@ -282,15 +282,15 @@ class _SurveyDataCreationPanelState extends State { // scoreRule (show entry if survey is scored) Visibility(visible: _data is! SurveyDataResult && widget.scoredSurvey, child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: Styles().colors.getColor('surface')), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: AppColors.surface), padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(top: 16), child: Column(children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("Score Rule", style: Styles().textStyles.getTextStyle('widget.message.regular')), + Text("Score Rule", style: AppTextStyles.widgetMessageRegular), GestureDetector( onTap: _onTapManageScoreRule, - child: Text(_data.scoreRule == null ? "Create" : "Remove", style: Styles().textStyles.getTextStyle('widget.button.title.medium.underline')) + child: Text(_data.scoreRule == null ? "Create" : "Remove", style: AppTextStyles.widgetButtonTitleMediumUnderline) ), ],), Visibility(visible: _data.scoreRule != null, child: Padding(padding: const EdgeInsets.only(top: 16), child: @@ -318,9 +318,9 @@ class _SurveyDataCreationPanelState extends State { Widget _buildDone() { return Padding(padding: const EdgeInsets.all(8.0), child: RoundedButton( label: 'Done', - borderColor: Styles().colors.fillColorPrimaryVariant, - backgroundColor: Styles().colors.surface, - textStyle: Styles().textStyles.getTextStyle('widget.detail.large.fat'), + borderColor: AppColors.fillColorPrimaryVariant, + backgroundColor: AppColors.surface, + textStyle: AppTextStyles.widgetDetailLargeBold, onTap: _onTapDone, )); } diff --git a/lib/ui/panels/survey_data_default_response_panel.dart b/lib/ui/panels/survey_data_default_response_panel.dart index de7604ac7..1e1858be7 100644 --- a/lib/ui/panels/survey_data_default_response_panel.dart +++ b/lib/ui/panels/survey_data_default_response_panel.dart @@ -15,8 +15,8 @@ */ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/widgets/form_field.dart'; import 'package:rokwire_plugin/ui/widgets/header_bar.dart'; import 'package:rokwire_plugin/ui/widgets/rounded_button.dart'; @@ -74,7 +74,7 @@ class _SurveyDataDefaultResponsePanelState extends State { return Scaffold( appBar: HeaderBar(title: _headerText), bottomNavigationBar: widget.tabBar, - backgroundColor: Styles().colors.background, + backgroundColor: AppColors.background, body: SurveyElementCreationWidget(body: _buildSurveyDataOptions(), completionOptions: _buildDone(), scrollController: _scrollController,), ); } @@ -181,9 +181,9 @@ class _SurveyDataOptionsPanelState extends State { Widget _buildDone() { return Padding(padding: const EdgeInsets.all(8.0), child: RoundedButton( label: 'Done', - borderColor: Styles().colors.fillColorPrimaryVariant, - backgroundColor: Styles().colors.surface, - textStyle: Styles().textStyles.getTextStyle('widget.detail.large.fat'), + borderColor: AppColors.fillColorPrimaryVariant, + backgroundColor: AppColors.surface, + textStyle: AppTextStyles.widgetDetailLargeBold, onTap: _onTapDone, )); } diff --git a/lib/ui/panels/survey_panel.dart b/lib/ui/panels/survey_panel.dart index bbf340b25..b5b4759a5 100644 --- a/lib/ui/panels/survey_panel.dart +++ b/lib/ui/panels/survey_panel.dart @@ -15,9 +15,9 @@ */ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/survey.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/service/surveys.dart'; import 'package:rokwire_plugin/ui/widgets/survey.dart'; @@ -74,10 +74,10 @@ class _SurveyPanelState extends State { return Scaffold( appBar: widget.headerBar ?? HeaderBar(title: _survey?.title), bottomNavigationBar: widget.tabBar, - backgroundColor: Styles().colors.background, + backgroundColor: AppColors.background, body: Column( children: [ - Visibility(visible: _loading, child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary))), + Visibility(visible: _loading, child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(AppColors.fillColorPrimary))), Expanded(child: Scrollbar( radius: const Radius.circular(2), thumbVisibility: true, diff --git a/lib/ui/panels/web_panel.dart b/lib/ui/panels/web_panel.dart index 20e7dcace..f0c3f8f5a 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -15,17 +15,16 @@ */ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/deep_link.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/tracking_services.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/ui/widgets/header_bar.dart'; import 'package:rokwire_plugin/utils/utils.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:flutter_html/flutter_html.dart' as flutter_html; import 'package:sprintf/sprintf.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -60,7 +59,7 @@ class WebPanel extends StatefulWidget { @protected Widget buildInitializing(BuildContext context) { return Center(child: - CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary),), + CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation(AppColors.fillColorPrimary)), ); } @@ -88,8 +87,8 @@ class WebPanel extends StatefulWidget { if (title != null) { contentList.add(flutter_html.Html(data: title, onLinkTap: (url, context, element) => onTapStatusLink(url), - style: { "body": flutter_html.Style(color: Styles().colors.fillColorPrimary, - fontFamily: Styles().fontFamilies.bold, fontSize: flutter_html.FontSize(32), + style: { "body": flutter_html.Style(color: AppColors.fillColorPrimary, + fontFamily: AppFontFamilies.bold, fontSize: flutter_html.FontSize(32), textAlign: TextAlign.center, padding: null /* EdgeInsets.zero, const flutter_html.HtmlPaddings() */, margin: flutter_html.Margins.zero), },), ); } @@ -101,8 +100,8 @@ class WebPanel extends StatefulWidget { if ((message != null)) { contentList.add(flutter_html.Html(data: message, onLinkTap: (url, context, element) => onTapStatusLink(url), - style: { "body": flutter_html.Style(color: Styles().colors.fillColorPrimary, - fontFamily: Styles().fontFamilies.regular, fontSize: flutter_html.FontSize(20), + style: { "body": flutter_html.Style(color: AppColors.fillColorPrimary, + fontFamily: AppFontFamilies.regular, fontSize: flutter_html.FontSize(20), textAlign: TextAlign.left, padding: null /* EdgeInsets.zero, const flutter_html.HtmlPaddings() */, margin: flutter_html.Margins.zero), },), ); } @@ -139,7 +138,7 @@ class WebPanel extends StatefulWidget { } class WebPanelState extends State implements NotificationsListener { - + late flutter_webview.WebViewController _controller; bool? _isOnline; bool? _isTrackingEnabled; bool _isPageLoading = true; @@ -149,12 +148,42 @@ class WebPanelState extends State implements NotificationsListener { void initState() { super.initState(); NotificationService().subscribe(this, [ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, DeepLink.notifyUri, ]); - if (Platform.isAndroid) { - flutter_webview.WebView.platform = flutter_webview.SurfaceAndroidWebView(); + + Uri? uri; + if (widget.url != null) { + uri = Uri.tryParse(widget.url!); + } + _controller = flutter_webview.WebViewController() + ..setJavaScriptMode(flutter_webview.JavaScriptMode.unrestricted) + // ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + flutter_webview.NavigationDelegate( + onProgress: (int progress) { + // Update loading bar. + }, + onPageStarted: (String url) {}, + onPageFinished: (String url) { + setState(() { + _isPageLoading = false; + }); + }, + onWebResourceError: (flutter_webview.WebResourceError error) {}, + onNavigationRequest: widget.processNavigation, + ), + ); + + if (uri != null) { + _controller.loadRequest(uri); } + + //TODO: See if we need to replace this + // if (Platform.isAndroid) { + // flutter_webview.WebView.platform = flutter_webview.SurfaceAndroidWebView(); + // } + widget.getOnline().then((bool isOnline) { setState(() { _isOnline = isOnline; @@ -193,7 +222,7 @@ class WebPanelState extends State implements NotificationsListener { return Scaffold( appBar: widget.headerBar ?? HeaderBar(title: widget.title), - backgroundColor: Styles().colors.background, + backgroundColor: AppColors.background, body: Column(children: [ Expanded(child: contentWidget), widget.tabBar ?? Container() @@ -203,15 +232,8 @@ class WebPanelState extends State implements NotificationsListener { Widget _buildWebView() { return Stack(children: [ Visibility(visible: _isForeground, - child: flutter_webview.WebView( - initialUrl: widget.url, - javascriptMode: flutter_webview.JavascriptMode.unrestricted, - navigationDelegate: widget.processNavigation, - onPageFinished: (url) { - setState(() { - _isPageLoading = false; - }); - },),), + child: flutter_webview.WebViewWidget(controller: _controller), + ), Visibility(visible: _isPageLoading, child: const Center( child: CircularProgressIndicator(), @@ -221,7 +243,7 @@ class WebPanelState extends State implements NotificationsListener { @override void onNotification(String name, dynamic param){ - if (name == AppLivecycle.notifyStateChanged) { + if (name == AppLifecycle.notifyStateChanged) { setState(() { _isForeground = (param == AppLifecycleState.resumed); }); diff --git a/lib/ui/popups/alerts.dart b/lib/ui/popups/alerts.dart index 8a9774e15..5d9d53b61 100644 --- a/lib/ui/popups/alerts.dart +++ b/lib/ui/popups/alerts.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/actions.dart'; import 'package:rokwire_plugin/model/alert.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/popups/popup_message.dart'; import 'package:rokwire_plugin/ui/widget_builders/actions.dart'; @@ -31,7 +31,7 @@ class Alerts { return Container( margin: margin, height: height, - color: Styles().colors.dividerLine, + color: AppColors.dividerLine, ); } } \ No newline at end of file diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index 6236a1cfe..299b921b4 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -15,6 +15,7 @@ */ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/widgets/rounded_button.dart'; @@ -36,7 +37,7 @@ class PopupMessage extends StatelessWidget { final TextAlign? messageTextAlign; final EdgeInsetsGeometry messagePadding; final Widget? messageWidget; - + final Widget? button; final String? buttonTitle; final EdgeInsetsGeometry buttonPadding; @@ -63,7 +64,7 @@ class PopupMessage extends StatelessWidget { this.messageTextAlign, this.messagePadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 24), this.messageWidget, - + this.button, this.buttonTitle, this.buttonPadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 16), @@ -73,22 +74,22 @@ class PopupMessage extends StatelessWidget { this.borderRadius, }) : super(key: key); - @protected Color? get defaultTitleBarColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultTitleBarColor => AppColors.fillColorPrimary; @protected Color? get displayTitleBarColor => titleBarColor ?? defaultTitleBarColor; - @protected Color? get defaultTitleTextColor => Styles().colors.white; + @protected Color? get defaultTitleTextColor => AppColors.textLight; @protected Color? get displayTitleTextColor => titleTextColor ?? defaultTitleTextColor; - @protected String? get defaultTitleFontFamily => Styles().fontFamilies.bold; + @protected String? get defaultTitleFontFamily => AppFontFamilies.bold; @protected String? get displayTitleFontFamily => titleFontFamily ?? defaultTitleFontFamily; @protected TextStyle get defaultTitleTextStyle => TextStyle(fontFamily: displayTitleFontFamily, fontSize: titleFontSize, color: displayTitleTextColor); @protected TextStyle get displayTitleTextStyle => titleTextStyle ?? defaultTitleTextStyle; - @protected Color? get defaultMessageTextColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultMessageTextColor => AppColors.fillColorPrimary; @protected Color? get displayMessageTextColor => messageTextColor ?? defaultMessageTextColor; - @protected String? get defaultMessageFontFamily => Styles().fontFamilies.bold; + @protected String? get defaultMessageFontFamily => AppFontFamilies.bold; @protected String? get displayMessageFontFamily => messageFontFamily ?? defaultMessageFontFamily; @protected TextStyle get defaultMessageTextStyle => TextStyle(fontFamily: displayMessageFontFamily, fontSize: messageFontSize, color: displayMessageTextColor); @@ -121,7 +122,7 @@ class PopupMessage extends StatelessWidget { TextAlign? messageTextAlign, EdgeInsetsGeometry messagePadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 24), Widget? messageWidget, - + Widget? button, String? buttonTitle, EdgeInsetsGeometry buttonPadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 16), @@ -153,7 +154,7 @@ class PopupMessage extends StatelessWidget { messageTextAlign: messageTextAlign, messagePadding: messagePadding, messageWidget: messageWidget, - + button: button, buttonTitle: buttonTitle, buttonPadding: buttonPadding, @@ -209,6 +210,8 @@ class ActionsMessage extends StatelessWidget { final EdgeInsetsGeometry titlePadding; final Color? titleBarColor; final Widget? closeButtonIcon; + + final Color? backgroundColor; final String? message; final TextStyle? messageTextStyle; @@ -237,6 +240,8 @@ class ActionsMessage extends StatelessWidget { this.titlePadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8), this.titleBarColor, this.closeButtonIcon, + + this.backgroundColor, this.message, this.messageTextStyle, @@ -256,22 +261,25 @@ class ActionsMessage extends StatelessWidget { this.borderRadius, }) : super(key: key); - @protected Color? get defaultTitleBarColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultBackgroundColor => AppColors.surface; + @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; + + @protected Color? get defaultTitleBarColor => AppColors.fillColorPrimary; @protected Color? get displayTitleBarColor => titleBarColor ?? defaultTitleBarColor; - @protected Color? get defaultTitleTextColor => Styles().colors.white; + @protected Color? get defaultTitleTextColor => AppColors.textLight; @protected Color? get displayTitleTextColor => titleTextColor ?? defaultTitleTextColor; - @protected String? get defaultTitleFontFamily => Styles().fontFamilies.bold; + @protected String? get defaultTitleFontFamily => AppFontFamilies.bold; @protected String? get displayTitleFontFamily => titleFontFamily ?? defaultTitleFontFamily; @protected TextStyle get defaultTitleTextStyle => TextStyle(fontFamily: displayTitleFontFamily, fontSize: titleFontSize, color: displayTitleTextColor); @protected TextStyle get displayTitleTextStyle => titleTextStyle ?? defaultTitleTextStyle; - @protected Color? get defaultMessageTextColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultMessageTextColor => AppColors.textPrimary; @protected Color? get displayMessageTextColor => messageTextColor ?? defaultMessageTextColor; - @protected String? get defaultMessageFontFamily => Styles().fontFamilies.bold; + @protected String? get defaultMessageFontFamily => AppFontFamilies.bold; @protected String? get displayMessageFontFamily => messageFontFamily ?? defaultMessageFontFamily; @protected TextStyle get defaultMessageTextStyle => TextStyle(fontFamily: displayMessageFontFamily, fontSize: messageFontSize, color: displayMessageTextColor); @@ -283,12 +291,13 @@ class ActionsMessage extends StatelessWidget { @protected ShapeBorder get defaultBorder => RoundedRectangleBorder(borderRadius: displayBorderRadius,); @protected ShapeBorder get displayBorder => border ?? defaultBorder; - @protected Widget? get defaultCloseButtonIcon => Styles().images.getImage('close-circle-white', defaultSpec: FontAwesomeImageSpec(type: 'fa.icon', source: '0xf057', size: 18.0, color: Styles().colors.surface)); + @protected Widget? get defaultCloseButtonIcon => Styles().images.getImage('close-circle-light', defaultSpec: FontAwesomeImageSpec(type: 'fa.icon', source: '0xf057', size: 18.0, color: AppColors.surface)); @protected Widget? get displayCloseButtonIcon => closeButtonIcon ?? defaultCloseButtonIcon; - static Future show({ + static Future show({ String? title, TextStyle? titleTextStyle, + Color? backgroundColor, Color? titleTextColor, String? titleFontFamily, double titleFontSize = 20, @@ -327,6 +336,8 @@ class ActionsMessage extends StatelessWidget { titlePadding: titlePadding, titleBarColor: titleBarColor, closeButtonIcon: closeButtonIcon, + + backgroundColor: backgroundColor, message: message, messageTextStyle: messageTextStyle, @@ -349,10 +360,12 @@ class ActionsMessage extends StatelessWidget { Widget build(BuildContext context) { List flexibleButtons = []; for (Widget button in buttons) { - flexibleButtons.add(Flexible(flex: 1, child: button)); + flexibleButtons.add(button); } Widget? closeButton = displayCloseButtonIcon; - return Dialog(shape: displayBorder, clipBehavior: Clip.antiAlias, child: + return Dialog( + backgroundColor: displayBackgroundColor, + shape: displayBorder, clipBehavior: Clip.antiAlias, child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Material( color: displayTitleBarColor, @@ -372,7 +385,8 @@ class ActionsMessage extends StatelessWidget { buttonAxis == Axis.vertical ? Padding(padding: buttonsPadding, child: Column(children: buttons),) : Padding(padding: buttonsPadding, - child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: flexibleButtons,), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, children: flexibleButtons,), ), ]) ],), diff --git a/lib/ui/widget_builders/buttons.dart b/lib/ui/widget_builders/buttons.dart index 25bf07b4d..56ed5b364 100644 --- a/lib/ui/widget_builders/buttons.dart +++ b/lib/ui/widget_builders/buttons.dart @@ -1,14 +1,14 @@ import 'package:flutter/widgets.dart'; -import 'package:rokwire_plugin/service/styles.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/ui/widgets/rounded_button.dart'; class ButtonBuilder { static Widget standardRoundedButton({String label = '', void Function()? onTap}) { return RoundedButton( label: label, - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.surface, - textStyle: Styles().textStyles.getTextStyle('widget.detail.regular.fat'), + borderColor: AppColors.fillColorSecondary, + backgroundColor: AppColors.surface, + textStyle: AppTextStyles.widgetDetailRegularBold, onTap: onTap, ); } diff --git a/lib/ui/widget_builders/dropdowns.dart b/lib/ui/widget_builders/dropdowns.dart new file mode 100644 index 000000000..8b4cea937 --- /dev/null +++ b/lib/ui/widget_builders/dropdowns.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; + +class DropdownBuilder { + static List> getItems(List options, {String? nullOption, TextStyle? style}) { + List> dropDownItems = >[]; + if (nullOption != null) { + dropDownItems.add(DropdownMenuItem(value: null, child: Text(nullOption, style: style ?? AppTextStyles.widgetDetailRegular))); + } + for (T option in options) { + dropDownItems.add(DropdownMenuItem(value: option, child: Text(option.toString(), style: style ?? AppTextStyles.widgetDetailRegular))); + } + return dropDownItems; + } +} \ No newline at end of file diff --git a/lib/ui/widget_builders/scroll_pager.dart b/lib/ui/widget_builders/scroll_pager.dart index 1851564d4..e97a6be20 100644 --- a/lib/ui/widget_builders/scroll_pager.dart +++ b/lib/ui/widget_builders/scroll_pager.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/localization.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/widget_builders/loading.dart'; import 'package:rokwire_plugin/ui/widgets/scroll_pager.dart'; @@ -30,10 +30,10 @@ class ScrollPagerBuilder { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Styles().images.getImage('retry-gray', defaultSpec: FontAwesomeImageSpec(type: 'fa.icon', source: '0xf2f9', weight: 'solid', size: 18.0, color: Styles().colors.mediumGray)) ?? Container(), + AppImages.retryMedium, const SizedBox(width: 8.0), Text(Localization().getStringEx('widget.scroll_pager.error.title', 'Something went wrong'), - style: Styles().textStyles.getTextStyle('widget.message.light.regular')), + style: AppTextStyles.widgetMessageLightRegular), ], ), ), diff --git a/lib/ui/widget_builders/survey.dart b/lib/ui/widget_builders/survey.dart index 9a1c0580a..4a00f2dcc 100644 --- a/lib/ui/widget_builders/survey.dart +++ b/lib/ui/widget_builders/survey.dart @@ -1,9 +1,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/survey.dart'; import 'package:rokwire_plugin/service/app_datetime.dart'; import 'package:rokwire_plugin/service/localization.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/panels/survey_panel.dart'; import 'package:rokwire_plugin/ui/widget_builders/actions.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -15,10 +15,10 @@ class SurveyBuilder { List buttonActions = resultSurveyButtons(context, survey); List content = []; if (StringUtils.isNotEmpty(survey.text)) { - content.add(Text(survey.text, textAlign: TextAlign.start, style: Styles().textStyles.getTextStyle('widget.title.large.fat'))); + content.add(Text(survey.text, textAlign: TextAlign.start, style: AppTextStyles.widgetTitleLargeBold)); } if (StringUtils.isNotEmpty(survey.moreInfo)) { - content.add(Padding(padding: const EdgeInsets.only(top: 8), child: Text(survey.moreInfo!, style: Styles().textStyles.getTextStyle('widget.detail.regular')))); + content.add(Padding(padding: const EdgeInsets.only(top: 8), child: Text(survey.moreInfo!, style: AppTextStyles.widgetDetailRegular))); } if (CollectionUtils.isNotEmpty(buttonActions)) { content.add(Padding( @@ -49,14 +49,14 @@ class SurveyBuilder { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible(child: Text(title ?? response.survey.title.toUpperCase(), style: Styles().textStyles.getTextStyle('widget.title.small.fat'))), + Flexible(child: Text(title ?? response.survey.title.toUpperCase(), style: AppTextStyles.widgetTitleSmallBold)), Row( mainAxisSize: MainAxisSize.min, children: [ - Text(date ?? '', style: Styles().textStyles.getTextStyle('widget.detail.small')), + Text(date ?? '', style: AppTextStyles.widgetDetailSmall), Container(width: 8.0), - Styles().images.getImage('chevron-right-bold', excludeFromSemantics: true) ?? Container() - // UIIcon(IconAssets.chevronRight, size: 14.0, color: Styles().colors.headlineText), + AppImages.chevronRight + // UIIcon(IconAssets.chevronRight, size: 14.0, color: AppColors.headlineText), ], ), ], @@ -75,9 +75,9 @@ class SurveyBuilder { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(key.replaceAll('_', ' ').toUpperCase() + ':', - style: Styles().textStyles.getTextStyle('widget.description.regular.fat')), + style: AppTextStyles.widgetDescriptionRegularBold), const SizedBox(width: 8.0), - Flexible(child: Text(responseData ?? '', style: Styles().textStyles.getTextStyle('widget.detail.regular'))), + Flexible(child: Text(responseData ?? '', style: AppTextStyles.widgetDetailRegular)), ], )); } @@ -91,7 +91,7 @@ class SurveyBuilder { return Material( borderRadius: BorderRadius.circular(30), - color: Styles().colors.surface, + color: AppColors.surface, child: InkWell( borderRadius: BorderRadius.circular(30), onTap: () => Navigator.push(context, CupertinoPageRoute(builder: (context) => @@ -119,7 +119,7 @@ class SurveyBuilder { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(Localization().getStringEx("widget.survey.response_card.result.title", "Result:"), - style: Styles().textStyles.getTextStyle('widget.detail.regular.fat')), + style: AppTextStyles.widgetDetailRegularBold), const SizedBox(height: 8.0), SurveyBuilder.surveyDataResult(context, dataResult) ?? Container(), ], diff --git a/lib/ui/widgets/expandable_section.dart b/lib/ui/widgets/expandable_section.dart new file mode 100644 index 000000000..05f39216e --- /dev/null +++ b/lib/ui/widgets/expandable_section.dart @@ -0,0 +1,91 @@ +/* + * 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/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; +import 'package:rokwire_plugin/service/styles.dart'; + +class ExpandableSection extends StatefulWidget { + final String? title; + final TextStyle? titleStyle; + final Widget? titleWidget; + final TextStyle? subtitleStyle; + final String? subtitle; + final Widget? subtitleWidget; + final String? iconKey; + final Widget? contents; + final bool initiallyExpanded; + + ExpandableSection({this.title, this.titleStyle, this.titleWidget, this.subtitle, this.subtitleStyle, + this.subtitleWidget, this.iconKey, this.contents, this.initiallyExpanded = false, Key? key}) : super(key: key); + + @override + ExpandableSectionState createState() => ExpandableSectionState(); + + @protected Color? get defaultTitleColor => AppColors.textPrimary; + @protected String? get defaultTitleFontFamily => AppFontFamilies.bold; + @protected double? get defaultTitleSize => 18; + @protected TextStyle get defaultTitleStyle => TextStyle(fontFamily: defaultTitleFontFamily, + fontSize: defaultTitleSize, color: defaultTitleColor); + @protected TextStyle get displayTitleStyle => titleStyle ?? defaultTitleStyle; + @protected Widget get defaultTitleWidget => Text(title ?? '', style: displayTitleStyle); + @protected Widget get displayTitleWidget => titleWidget ?? defaultTitleWidget; + + @protected Color? get defaultSubtitleColor => AppColors.textDark; + @protected String? get defaultSubtitleFontFamily => AppFontFamilies.bold; + @protected double? get defaultSubtitleSize => 16; + @protected TextStyle get defaultSubtitleStyle => TextStyle(fontFamily: defaultSubtitleFontFamily, + fontSize: defaultSubtitleSize, color: defaultSubtitleColor); + @protected TextStyle get displaySubtitleStyle => subtitleStyle ?? defaultSubtitleStyle; + @protected Widget? get defaultSubtitleWidget => subtitle != null ? Text(subtitle ?? '', style: displaySubtitleStyle) : null; + @protected Widget? get displaySubtitleWidget => subtitleWidget ?? defaultSubtitleWidget; +} + +class ExpandableSectionState extends State { + bool _expanded = false; + + @override + void initState() { + _expanded = widget.initiallyExpanded; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ExpansionTile( + initiallyExpanded: widget.initiallyExpanded, + title: widget.displayTitleWidget, + subtitle: widget.displaySubtitleWidget, + trailing: Styles().images.getImage(_expanded ? 'chevron-up' : 'chevron-down', + defaultSpec: FontAwesomeImageSpec( + type: 'fa.icon', + source: _expanded ? '0xf077' : '0xf078', + weight: 'solid', + size: 18, + color: AppColors.fillColorSecondary + ) + ), + children: [widget.contents ?? Container(),], + onExpansionChanged: (bool expanded) { + setState(() => _expanded = expanded); + }, + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/expandable_text.dart b/lib/ui/widgets/expandable_text.dart index 2af4d86a0..0dda1e567 100644 --- a/lib/ui/widgets/expandable_text.dart +++ b/lib/ui/widgets/expandable_text.dart @@ -15,6 +15,7 @@ */ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/styles.dart'; class ExpandableText extends StatefulWidget { @@ -54,15 +55,15 @@ class ExpandableText extends StatefulWidget { this.footerWidget, }) : super(key: key); - TextStyle get _textStyle => textStyle ?? TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Styles().colors.textBackground,); + TextStyle get _textStyle => textStyle ?? TextStyle(fontFamily: AppFontFamilies.regular, fontSize: 16, color: AppColors.textDark); String get trimSuffix => '...'; - Color? get _splitterColor => splitterColor ?? Styles().colors.fillColorSecondary; + Color? get _splitterColor => splitterColor ?? AppColors.fillColorSecondary; String get readMoreText => 'Read more'; String? get readMoreHint => null; - TextStyle get _readMoreStyle => readMoreStyle ?? TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorPrimary); + TextStyle get _readMoreStyle => readMoreStyle ?? TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16, color: AppColors.fillColorPrimary); Widget? get _readMoreIcon => readMoreIcon ?? (readMoreIconKey != null ? Styles().images.getImage(readMoreIconKey!, excludeFromSemantics: true) : null); diff --git a/lib/ui/widgets/filters.dart b/lib/ui/widgets/filters.dart index 173f4cb0e..ce86d2243 100644 --- a/lib/ui/widgets/filters.dart +++ b/lib/ui/widgets/filters.dart @@ -15,6 +15,7 @@ */ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:rokwire_plugin/service/styles.dart'; @@ -61,8 +62,8 @@ class FilterListItem extends StatelessWidget { this.selectedIconPadding = const EdgeInsets.only(left: 10), }) : super(key: key); - @protected TextStyle? get defaultTitleTextStyle => TextStyle(fontFamily: Styles().fontFamilies.medium, fontSize: 16, color: Styles().colors.fillColorPrimary); - @protected TextStyle? get defaultSelectedTitleTextStyle => TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorPrimary); + @protected TextStyle? get defaultTitleTextStyle => TextStyle(fontFamily: AppFontFamilies.medium, fontSize: 16, color: AppColors.fillColorPrimary); + @protected TextStyle? get defaultSelectedTitleTextStyle => TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16, color: AppColors.fillColorPrimary); TextStyle? get _titleTextStyle => selected ? (selectedTitleTextStyle ?? defaultSelectedTitleTextStyle) : (titleTextStyle ?? defaultTitleTextStyle); @protected TextStyle? get defaultDescriptionTextStyle => defaultTitleTextStyle; @@ -95,7 +96,7 @@ class FilterListItem extends StatelessWidget { return Semantics(label: title, button: true, selected: selected, excludeSemantics: true, child: InkWell(onTap: onTap, child: - Container(color: (selected ? Styles().colors.background : Colors.white), child: + Container(color: (selected ? AppColors.background : Colors.white), child: Padding(padding: padding, child: Row(mainAxisSize: MainAxisSize.max, children: contentList), ), @@ -145,8 +146,8 @@ class FilterSelector extends StatelessWidget { }) : super(key: key); - @protected TextStyle? get defaultTitleTextStyle => TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorPrimary); - @protected TextStyle? get defaultActiveTitleTextStyle => TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorSecondary); + @protected TextStyle? get defaultTitleTextStyle => TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16, color: AppColors.fillColorPrimary); + @protected TextStyle? get defaultActiveTitleTextStyle => TextStyle(fontFamily: AppFontFamilies.bold, fontSize: 16, color: AppColors.fillColorSecondary); TextStyle? get _titleTextStyle => active ? (activeTitleTextStyle ?? defaultActiveTitleTextStyle) : (titleTextStyle ?? defaultTitleTextStyle); Widget? get _iconImage => (iconKey != null) ? Styles().images.getImage(iconKey, excludeFromSemantics: true) : null; diff --git a/lib/ui/widgets/flex_content.dart b/lib/ui/widgets/flex_content.dart index 40c1a3b15..b7987dfdc 100644 --- a/lib/ui/widgets/flex_content.dart +++ b/lib/ui/widgets/flex_content.dart @@ -14,11 +14,11 @@ * limitations under the License. */ -import 'dart:io'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/content.dart'; +import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/ui/panels/web_panel.dart'; import 'package:rokwire_plugin/ui/widgets/rounded_button.dart'; @@ -51,10 +51,10 @@ class FlexContent extends StatefulWidget { FlexContentWidgetState createState() => FlexContentWidgetState(); @protected - Color? get backgroundColor => Styles().colors.lightGray; + Color? get backgroundColor => AppColors.background; @protected - Color? get topSplitterColor => Styles().colors.fillColorPrimaryVariant; + Color? get topSplitterColor => AppColors.fillColorPrimaryVariant; @protected double? get topSplitterHeight => 1; @@ -93,7 +93,7 @@ class FlexContent extends StatefulWidget { EdgeInsetsGeometry get titlePadding => const EdgeInsets.only(top: 0); @protected - TextStyle get titleTextStyle => TextStyle(color: Styles().colors.fillColorPrimary, fontFamily: Styles().fontFamilies.extraBold, fontSize: 20, ); + TextStyle get titleTextStyle => TextStyle(color: AppColors.fillColorPrimary, fontFamily: AppFontFamilies.extraBold, fontSize: 20, ); @protected Widget buildText(String? text) => Visibility(visible: StringUtils.isNotEmpty(text), child: @@ -106,7 +106,7 @@ class FlexContent extends StatefulWidget { EdgeInsetsGeometry get textPadding => const EdgeInsets.only(top: 10); @protected - TextStyle get textTextStyle => TextStyle(color: Styles().colors.textSurface, fontFamily: Styles().fontFamilies.medium, fontSize: 16, ); + TextStyle get textTextStyle => TextStyle(color: AppColors.textDark, fontFamily: AppFontFamilies.medium, fontSize: 16, ); @protected Widget buildButtons(BuildContext context, List? buttonsJson) { @@ -141,9 +141,9 @@ class FlexContent extends StatefulWidget { @protected Widget buildButton(BuildContext context, Map button) => RoundedButton( label: StringUtils.ensureNotEmpty(JsonUtils.stringValue(button['title'])), - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, + textColor: AppColors.fillColorPrimary, + borderColor: AppColors.fillColorSecondary, + backgroundColor: AppColors.surface, contentWeight: 0.0, onTap: () => onTapButton(context, button), ); @@ -187,7 +187,7 @@ class FlexContent extends StatefulWidget { Map? options = JsonUtils.mapValue(linkJson['options']); dynamic target = (options != null) ? options['target'] : 'internal'; if (target is Map) { - target = target[Platform.operatingSystem.toLowerCase()]; + target = target[Config().operatingSystem.toLowerCase()]; } if (target == 'external') { diff --git a/lib/ui/widgets/form_field.dart b/lib/ui/widgets/form_field.dart index 6130bd7f9..091346fe4 100644 --- a/lib/ui/widgets/form_field.dart +++ b/lib/ui/widgets/form_field.dart @@ -14,8 +14,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/localization.dart'; -import 'package:rokwire_plugin/service/styles.dart'; class FormFieldText extends StatefulWidget { final String label; @@ -23,7 +23,7 @@ class FormFieldText extends StatefulWidget { final bool readOnly; final bool multipleLines; final bool required; - + final String? initialValue; final String? hint; final int? maxLength; @@ -54,7 +54,7 @@ class _FormFieldTextState extends State { label: widget.label, child: TextFormField( readOnly: widget.readOnly, - style: Styles().textStyles.getTextStyle('body'), + style: AppTextStyles.widgetDetailRegular, maxLines: widget.multipleLines ? null : 1, minLines: widget.multipleLines ? 2 : null, maxLength: widget.maxLength, @@ -68,7 +68,7 @@ class _FormFieldTextState extends State { contentPadding: const EdgeInsets.all(24.0), labelText: widget.label, hintText: widget.hint, - prefix: widget.required ? Text("* ", semanticsLabel: Localization().getStringEx("widget.form_field_text.required.hint", "Required"), style: Styles().textStyles.getTextStyle('widget.error.regular.fat')) : null, + prefix: widget.required ? Text("* ", semanticsLabel: Localization().getStringEx("widget.form_field_text.required.hint", "Required"), style: AppTextStyles.widgetErrorRegularBold) : null, filled: true, fillColor: Colors.white, enabledBorder: OutlineInputBorder( @@ -77,7 +77,7 @@ class _FormFieldTextState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(width: 2, color: Styles().colors.fillColorPrimary) + borderSide: BorderSide(width: 2, color: AppColors.fillColorPrimary) ) ), controller: widget.controller, diff --git a/lib/ui/widgets/popup_toast.dart b/lib/ui/widgets/popup_toast.dart index 48ba94f86..f75cb11dc 100644 --- a/lib/ui/widgets/popup_toast.dart +++ b/lib/ui/widgets/popup_toast.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rokwire_plugin/service/styles.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; class PopupToast extends StatelessWidget { @@ -9,8 +9,8 @@ class PopupToast extends StatelessWidget { static const EdgeInsetsGeometry defaultPadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8); static BoxDecoration defaultDecoration = BoxDecoration( - color: Styles.appColors.surface, - border: Border.all(color: Styles.appColors.surfaceAccent, width: 1), + color: AppColors.surface, + border: Border.all(color: AppColors.surfaceAccent, width: 1), borderRadius: const BorderRadius.all(const Radius.circular(8)) ); diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index 9e6346087..53ce1af64 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -52,20 +53,20 @@ class RibbonButton extends StatefulWidget { this.label, this.description, this.onTap, - this.backgroundColor, //= Styles().colors.white + this.backgroundColor, //= AppColors.white this.padding, this.textWidget, this.textStyle, - this.textColor, //= Styles().colors.fillColorPrimary - this.fontFamily, //= Styles().fontFamilies.bold + this.textColor, //= AppColors.fillColorPrimary + this.fontFamily, //= AppFontFamilies.bold this.fontSize = 16.0, this.textAlign = TextAlign.left, this.descriptionWidget, this.descriptionTextStyle, - this.descriptionTextColor, //= Styles().colors.textSurface - this.descriptionFontFamily, //= Styles().fontFamilies.regular + this.descriptionTextColor, //= Styles().colors.getColor('textSurface') + this.descriptionFontFamily, //= AppFontFamilies.regular this.descriptionFontSize = 14.0, this.descriptionTextAlign = TextAlign.left, this.descriptionPadding = const EdgeInsets.only(top: 2), @@ -94,32 +95,35 @@ class RibbonButton extends StatefulWidget { this.semanticsValue, }) : super(key: key); - @protected Color? get defaultBackgroundColor => Styles().colors.white; + @protected Color? get defaultBackgroundColor => AppColors.background; @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; @protected EdgeInsetsGeometry get displayPadding => padding ?? (hasDescription ? complexPadding : simplePadding); @protected EdgeInsetsGeometry get simplePadding => const EdgeInsets.symmetric(horizontal: 16, vertical: 16); @protected EdgeInsetsGeometry get complexPadding => const EdgeInsets.symmetric(horizontal: 16, vertical: 8); - @protected Color? get defaultTextColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultTextColor => AppColors.textPrimary; @protected Color? get displayTextColor => textColor ?? defaultTextColor; - @protected String? get defaultFontFamily => Styles().fontFamilies.bold; + @protected String? get defaultFontFamily => AppFontFamilies.bold; @protected String? get displayFontFamily => fontFamily ?? defaultFontFamily; @protected TextStyle get displayTextStyle => textStyle ?? TextStyle(fontFamily: displayFontFamily, fontSize: fontSize, color: displayTextColor); @protected Widget get displayTextWidget => textWidget ?? Text(label ?? '', style: displayTextStyle, textAlign: textAlign,); @protected bool get hasDescription => StringUtils.isNotEmpty(description) || (descriptionWidget != null); - @protected Color? get defaultDescriptionTextColor => Styles().colors.textSurface; + @protected Color? get defaultDescriptionTextColor => Styles().colors.getColor('textSurface'); @protected Color? get displayDescriptionTextColor => descriptionTextColor ?? defaultDescriptionTextColor; - @protected String? get defaultDescriptionFontFamily => Styles().fontFamilies.regular; + @protected String? get defaultDescriptionFontFamily => AppFontFamilies.regular; @protected String? get displayDescriptionFontFamily => descriptionFontFamily ?? defaultDescriptionFontFamily; @protected TextStyle get displayDescriptionTextStyle => descriptionTextStyle ?? TextStyle(fontFamily: displayDescriptionFontFamily, fontSize: fontSize, color: displayDescriptionTextColor); - @protected Widget get displayDescriptionWidget => descriptionWidget ?? Text(description ?? '', style: displayDescriptionTextStyle, textAlign: descriptionTextAlign,); + @protected Widget get displayDescriptionWidget => descriptionWidget ?? Padding( + padding: descriptionPadding, + child: Text(description ?? '', style: displayDescriptionTextStyle, textAlign: descriptionTextAlign,), + ); @protected Widget? get leftIconImage => (leftIconKey != null) ? Styles().images.getImage(leftIconKey, excludeFromSemantics: true) : null; @protected Widget? get rightIconImage => (rightIconKey != null) ? Styles().images.getImage(rightIconKey, excludeFromSemantics: true) : null; - @protected Color? get defaultProgressColor => Styles().colors.fillColorSecondary; + @protected Color? get defaultProgressColor => AppColors.fillColorSecondary; @protected Color? get displayProgressColor => progressColor ?? defaultProgressColor; @protected double get defaultStrokeWidth => 2.0; @protected double get displayProgressStrokeWidth => progressStrokeWidth ?? defaultStrokeWidth; @@ -165,27 +169,28 @@ class _RibbonButtonState extends State { Widget? leftIconWidget = !widget.progressHidesLeftIcon ? (widget.leftIcon ?? widget.leftIconImage) : null; Widget? rightIconWidget = !widget.progressHidesRightIcon ? (widget.rightIcon ?? widget.rightIconImage) : null; Widget textContentWidget = widget.hasDescription ? - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - widget.displayTextWidget, - widget.displayDescriptionWidget, - ],) : widget.displayTextWidget; - return Semantics(label: widget.label, hint: widget.hint, value : widget.semanticsValue, button: true, excludeSemantics: true, child: - GestureDetector(onTap: () => widget.onTapWidget(context), child: - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: - Container(key: _contentKey, decoration: BoxDecoration(color: widget.displayBackgroundColor, border: widget.border, borderRadius: widget.borderRadius, boxShadow: widget.borderShadow), child: - Padding(padding: widget.displayPadding, child: - Row(children: [ - (leftIconWidget != null) ? Padding(padding: widget.leftIconPadding, child: leftIconWidget) : Container(), - Expanded(child: - textContentWidget - ), - (rightIconWidget != null) ? Padding(padding: widget.rightIconPadding, child: rightIconWidget) : Container(), - ],), - ), - ) + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + widget.displayTextWidget, + widget.displayDescriptionWidget, + ],) : widget.displayTextWidget; + return Container(key: _contentKey, decoration: BoxDecoration(border: widget.border, borderRadius: widget.borderRadius, boxShadow: widget.borderShadow), + child: Material(color: widget.displayBackgroundColor ?? Colors.transparent, + borderRadius: widget.borderRadius, + child: Semantics(label: widget.label, hint: widget.hint, value : widget.semanticsValue, button: true, excludeSemantics: true, child: + InkWell( + borderRadius: widget.borderRadius, + onTap: widget.onTap != null ? () => widget.onTapWidget(context) : null, + child: Padding(padding: widget.displayPadding, child: + Row(children: [ + (leftIconWidget != null) ? Padding(padding: widget.leftIconPadding, child: leftIconWidget) : Container(), + Expanded(child: + textContentWidget + ), + (rightIconWidget != null) ? Padding(padding: widget.rightIconPadding, child: rightIconWidget) : Container(), + ],), + ), ), - ],), + ), ), ); } diff --git a/lib/ui/widgets/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index 42bfeac9b..1af27fe11 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -17,7 +17,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:rokwire_plugin/service/styles.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; class RoundedButton extends StatefulWidget { final String label; @@ -65,15 +65,15 @@ class RoundedButton extends StatefulWidget { Key? key, required this.label, this.onTap, - this.backgroundColor, //= Styles().colors.white + this.backgroundColor, //= AppColors.white this.padding = const EdgeInsets.symmetric(horizontal: 20, vertical: 10), this.contentWeight = 1.0, this.conentAlignment = MainAxisAlignment.center, this.textWidget, this.textStyle, - this.textColor, //= Styles().colors.fillColorPrimary - this.fontFamily, //= Styles().fontFamilies.bold + this.textColor, //= AppColors.fillColorPrimary + this.fontFamily, //= AppFontFamilies.bold this.fontSize = 20.0, this.textAlign = TextAlign.center, @@ -87,10 +87,10 @@ class RoundedButton extends StatefulWidget { this.enabled = true, this.border, - this.borderColor, //= Styles().colors.fillColorSecondary + this.borderColor, //= AppColors.fillColorSecondary this.borderWidth = 2.0, this.borderShadow, - this.maxBorderRadius = 24.0, + this.maxBorderRadius = 36.0, this.secondaryBorder, this.secondaryBorderColor, @@ -102,19 +102,19 @@ class RoundedButton extends StatefulWidget { this.progressStrokeWidth, }) : super(key: key); - @protected Color? get defaultBackgroundColor => Styles().colors.white; + @protected Color? get defaultBackgroundColor => AppColors.surface; @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; - @protected Color? get defautTextColor => Styles().colors.fillColorPrimary; - @protected Color? get displayTextColor => textColor ?? defautTextColor; - @protected String? get defaultFontFamily => Styles().fontFamilies.bold; + @protected Color? get defaultTextColor => AppColors.textPrimary; + @protected Color? get displayTextColor => textColor ?? defaultTextColor; + @protected String? get defaultFontFamily => AppFontFamilies.bold; @protected String? get displayFontFamily => fontFamily ?? defaultFontFamily; @protected TextStyle get defaultTextStyle => TextStyle(fontFamily: displayFontFamily, fontSize: fontSize, color: displayTextColor); @protected TextStyle get displayTextStyle => textStyle ?? defaultTextStyle; @protected Widget get defaultTextWidget => Text(label, style: displayTextStyle, textAlign: textAlign,); @protected Widget get displayTextWidget => textWidget ?? defaultTextWidget; - @protected Color get defaultBorderColor => Styles().colors.fillColorSecondary; + @protected Color get defaultBorderColor => AppColors.fillColorSecondary; @protected Color get displayBorderColor => borderColor ?? defaultBorderColor; @protected Border get defaultBorder => Border.all(color: displayBorderColor, width: borderWidth); @protected Border get displayBorder => border ?? defaultBorder; @@ -163,8 +163,9 @@ class _RoundedButtonState extends State { Widget get _outerContent { //TODO: Fix ripple effect from InkWell (behind button content) - return Semantics(label: widget.label, hint: widget.hint, button: true, enabled: widget.enabled, child: - InkWell(onTap: widget.onTap, borderRadius: borderRadius, child: _wrapperContent), + return Material(color: Colors.transparent, + child: Semantics(label: widget.label, hint: widget.hint, button: true, + enabled: widget.enabled, child: _wrapperContent,), ); } @@ -207,9 +208,13 @@ class _RoundedButtonState extends State { Border? secondaryBorder = widget.displaySecondaryBorder; // BorderRadiusGeometry? borderRadius = - return Container(key: _contentKey, decoration: BoxDecoration(color: widget.displayBackgroundColor, border: widget.displayBorder, borderRadius: borderRadius, boxShadow: widget.borderShadow), child: (secondaryBorder != null) - ? Container(decoration: BoxDecoration(color: widget.displayBackgroundColor, border: secondaryBorder, borderRadius: borderRadius), child: _innerContent) - : _innerContent + return InkWell( + onTap: widget.onTap, + borderRadius: borderRadius, + child: Ink(key: _contentKey, decoration: BoxDecoration(color: widget.displayBackgroundColor, border: widget.displayBorder, borderRadius: borderRadius, boxShadow: widget.borderShadow), child: (secondaryBorder != null) + ? Ink(decoration: BoxDecoration(color: widget.displayBackgroundColor, border: secondaryBorder, borderRadius: borderRadius), child: _innerContent) + : _innerContent + ), ); } diff --git a/lib/ui/widgets/rounded_tab.dart b/lib/ui/widgets/rounded_tab.dart index b297a360f..f9acb6605 100644 --- a/lib/ui/widgets/rounded_tab.dart +++ b/lib/ui/widgets/rounded_tab.dart @@ -15,7 +15,7 @@ */ import 'package:flutter/material.dart'; -import 'package:rokwire_plugin/service/styles.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; class RoundedTab extends StatefulWidget { final String? title; @@ -68,14 +68,14 @@ class RoundedTab extends StatefulWidget { this.onTap, }): super(key: key); - @protected Color? get defaultTextColor => selected ? Styles().colors.white : Styles().colors.fillColorPrimary; + @protected Color? get defaultTextColor => selected ? AppColors.textLight : AppColors.fillColorPrimary; @protected Color? get displayTextColor => (selected ? selectedTextColor : textColor) ?? defaultTextColor; - @protected String? get defaultFontFamily => Styles().fontFamilies.bold; + @protected String? get defaultFontFamily => AppFontFamilies.bold; @protected String? get displayFontFamily => fontFamily ?? defaultFontFamily; @protected TextStyle get defaultTextStyle => TextStyle(fontFamily: displayFontFamily, fontSize: fontSize, color: displayTextColor); @protected TextStyle get displayTextStyle => (selected ? selectedTextStyle : textStyle) ?? defaultTextStyle; - @protected Color? get defaultBackgroundColor => selected ? Styles().colors.fillColorPrimary : Styles().colors.surfaceAccent; + @protected Color? get defaultBackgroundColor => selected ? AppColors.fillColorPrimary : AppColors.surfaceAccent; @protected Color? get displayBackgroundColor => (selected ? selectedBackgroundColor : backgroundColor) ?? defaultBackgroundColor; @protected Color get defaultBorderColor => const Color(0xffdadde1); @protected Color get displayBorderColor => (selected ? selectedBorderColor : borderColor) ?? defaultBorderColor; diff --git a/lib/ui/widgets/scroll_pager.dart b/lib/ui/widgets/scroll_pager.dart index ba7553046..7fef39727 100644 --- a/lib/ui/widgets/scroll_pager.dart +++ b/lib/ui/widgets/scroll_pager.dart @@ -67,8 +67,11 @@ class ScrollPagerController { ScrollController? get scrollController => _scrollController; ScrollPagerController({required this.limit, required this.onPage, this.onStateChanged, this.onReset, ScrollController? controller}) { - controller ??= ScrollController(); - registerScrollController(controller); + registerScrollController(controller ?? ScrollController()); + } + + void dispose() { + deregisterScrollController(); } Future reset() async { diff --git a/lib/ui/widgets/section.dart b/lib/ui/widgets/section.dart index bdc6f1649..6027d2605 100644 --- a/lib/ui/widgets/section.dart +++ b/lib/ui/widgets/section.dart @@ -15,7 +15,7 @@ */ import 'package:flutter/material.dart'; -import 'package:rokwire_plugin/service/styles.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; class VerticalTitleValueSection extends StatelessWidget { final String? title; @@ -70,34 +70,34 @@ class VerticalTitleValueSection extends StatelessWidget { this.padding = const EdgeInsets.only(left: 10), }) : super(key: key); - @protected Color? get defaultTitleTextColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultTitleTextColor => AppColors.fillColorPrimary; @protected Color? get displayTitleTextColor => titleTextColor ?? defaultTitleTextColor; - @protected String? get defaultTitleFontFamily => Styles().fontFamilies.regular; + @protected String? get defaultTitleFontFamily => AppFontFamilies.regular; @protected String? get displayTitleFontFamily => titleFontFamily ?? defaultTitleFontFamily; @protected TextStyle get defaultTitleTextStyle => TextStyle(fontFamily: displayTitleFontFamily, fontSize: titleFontSize, color: displayTitleTextColor); @protected TextStyle get displayTitleTextStyle => titleTextStyle ?? defaultTitleTextStyle; - @protected Color? get defaultValueTextColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultValueTextColor => AppColors.fillColorPrimary; @protected Color? get displayValueTextColor => valueTextColor ?? defaultValueTextColor; - @protected String? get defaultValueFontFamily => Styles().fontFamilies.extraBold; + @protected String? get defaultValueFontFamily => AppFontFamilies.extraBold; @protected String? get displayValueFontFamily => valueFontFamily ?? defaultValueFontFamily; @protected TextStyle get defaultValueTextStyle => TextStyle(fontFamily: displayValueFontFamily, fontSize: valueFontSize, color: displayValueTextColor); @protected TextStyle get displayValueTextStyle => valueTextStyle ?? defaultValueTextStyle; - @protected Color? get defaultHintTextColor => Styles().colors.textBackground; + @protected Color? get defaultHintTextColor => AppColors.textDark; @protected Color? get displayHintTextColor => hintTextColor ?? defaultHintTextColor; - @protected String? get defaultHintFontFamily => Styles().fontFamilies.regular; + @protected String? get defaultHintFontFamily => AppFontFamilies.regular; @protected String? get displayHintFontFamily => hintFontFamily ?? defaultHintFontFamily; @protected TextStyle get defaultHintTextStyle => TextStyle(fontFamily: displayHintFontFamily, fontSize: hintFontSize, color: displayHintTextColor); @protected TextStyle get displayHintTextStyle => hintTextStyle ?? defaultHintTextStyle; - @protected Color get defaultBorderColor => Styles().colors.fillColorSecondary; + @protected Color get defaultBorderColor => AppColors.fillColorSecondary; @protected Color get displayBorderColor => borderColor ?? defaultBorderColor; @protected BoxBorder get defaultBorder => Border(left: BorderSide(color: displayBorderColor, width: borderWidth)); diff --git a/lib/ui/widgets/section_header.dart b/lib/ui/widgets/section_header.dart index d47928fd9..e12bc1dba 100644 --- a/lib/ui/widgets/section_header.dart +++ b/lib/ui/widgets/section_header.dart @@ -15,6 +15,7 @@ */ import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/ui/panels/modal_image_holder.dart'; import 'package:rokwire_plugin/ui/widgets/triangle_painter.dart'; @@ -57,6 +58,7 @@ class SectionSlantHeader extends StatelessWidget { final EdgeInsetsGeometry rightIconPadding; final Widget? headerWidget; + final Widget? headerChild; final Widget? progressWidget; final List? children; final EdgeInsetsGeometry childrenPadding; @@ -102,6 +104,7 @@ class SectionSlantHeader extends StatelessWidget { this.rightIconPadding = const EdgeInsets.only(left: 16, right: 16), this.headerWidget, + this.headerChild, this.progressWidget, this.children, this.childrenPadding = const EdgeInsets.all(16), @@ -122,6 +125,16 @@ class SectionSlantHeader extends StatelessWidget { contentList.add(_buildSubTitle()); } + if (headerChild != null) { + contentList.add(Container( + color: slantColor, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: headerChild ?? Container(), + ), + )); + } + // Slant List slantList = []; if (StringUtils.isNotEmpty(slantImageKey)) { @@ -140,7 +153,7 @@ class SectionSlantHeader extends StatelessWidget { } else { Widget slantContentWidget = Container(color: _slantColor, child: - CustomPaint(painter: TrianglePainter(painterColor: backgroundColor ?? Styles().colors.background, horzDir: TriangleHorzDirection.rightToLeft), child: + CustomPaint(painter: TrianglePainter(painterColor: backgroundColor ?? AppColors.background, horzDir: TriangleHorzDirection.rightToLeft), child: Container(height: slantPainterHeight,), ), ); @@ -216,17 +229,17 @@ class SectionSlantHeader extends StatelessWidget { ); } - Color? get _slantColor => slantColor ?? Styles().colors.fillColorPrimary; + Color? get _slantColor => slantColor ?? AppColors.fillColorPrimary; TextStyle get _titleTextStyle => titleTextStyle ?? TextStyle( - color: titleTextColor ?? Styles().colors.textColorPrimary, - fontFamily: titleFontFamilly ?? Styles().fontFamilies.extraBold, + color: titleTextColor ?? AppColors.textPrimary, + fontFamily: titleFontFamilly ?? AppFontFamilies.extraBold, fontSize: titleFontSize ); TextStyle get _subTitleTextStyle => subTitleTextStyle ?? TextStyle( - color: subTitleTextColor ?? Styles().colors.textColorPrimary, - fontFamily: subTitleFontFamilly ?? Styles().fontFamilies.regular, + color: subTitleTextColor ?? AppColors.textPrimary, + fontFamily: subTitleFontFamilly ?? AppFontFamilies.regular, fontSize: subTitleFontSize ); } @@ -362,17 +375,17 @@ class SectionRibbonHeader extends StatelessWidget { contentWidget; } - Color? get _backgroundColor => backgroundColor ?? Styles().colors.fillColorPrimary; + Color? get _backgroundColor => backgroundColor ?? AppColors.fillColorPrimary; TextStyle get _titleTextStyle => titleTextStyle ?? TextStyle( - color: titleTextColor ?? Styles().colors.white, - fontFamily: titleFontFamilly ?? Styles().fontFamilies.extraBold, + color: titleTextColor ?? AppColors.textLight, + fontFamily: titleFontFamilly ?? AppFontFamilies.extraBold, fontSize: titleFontSize ); TextStyle get _subTitleTextStyle => subTitleTextStyle ?? TextStyle( - color: subTitleTextColor ?? Styles().colors.white, - fontFamily: subTitleFontFamilly ?? Styles().fontFamilies.regular, + color: subTitleTextColor ?? AppColors.textLight, + fontFamily: subTitleFontFamilly ?? AppFontFamilies.regular, fontSize: subTitleFontSize ); } @@ -447,10 +460,10 @@ class ImageSlantHeader extends StatelessWidget { Widget _buildProgressWidget(BuildContext context, ImageChunkEvent progress) { return progressWidget ?? SizedBox(height: progressSize.width, width: 24, child: - CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? Styles().colors.white), + CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? AppColors.surface), value: progress.expectedTotalBytes != null ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null), ); } - Color? get _slantImageColor => slantImageColor ?? Styles().colors.fillColorSecondary; + Color? get _slantImageColor => slantImageColor ?? AppColors.fillColorSecondary; } \ No newline at end of file diff --git a/lib/ui/widgets/survey.dart b/lib/ui/widgets/survey.dart index 41efcca98..43599ba34 100644 --- a/lib/ui/widgets/survey.dart +++ b/lib/ui/widgets/survey.dart @@ -16,6 +16,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/options.dart'; import 'package:rokwire_plugin/model/rules.dart'; import 'package:rokwire_plugin/model/survey.dart'; @@ -84,8 +85,8 @@ class SurveyWidget extends StatefulWidget { return Column(mainAxisAlignment: MainAxisAlignment.end, children: [ RoundedButton( label: Localization().getStringEx("widget.survey.button.action.continue.title", "Continue") + questionProgress, - textColor: canContinue ? null : Styles().colors.disabledTextColor, - borderColor: canContinue ? null : Styles().colors.disabledTextColor, + textColor: canContinue ? null : AppColors.textDisabled, + borderColor: canContinue ? null : AppColors.textDisabled, enabled: canContinue && !controller.saving, onTap: controller.continueSurvey, progress: controller.saving), @@ -149,7 +150,7 @@ class _SurveyWidgetState extends State { alignment: Alignment.center, child: Padding( padding: const EdgeInsets.all(32.0), - child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorPrimary)), + child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(AppColors.fillColorPrimary)), ), ); } @@ -175,14 +176,14 @@ class _SurveyWidgetState extends State { } return Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: Text(AppDateTime().getDisplayDateTime(dateTaken), style: Styles().textStyles.getTextStyle('widget.detail.regular'),), + child: Text(AppDateTime().getDisplayDateTime(dateTaken), style: AppTextStyles.widgetDetailRegular,), ); } Widget _buildMoreInfo() { return Padding( padding: const EdgeInsets.only(bottom: 32.0), - child: Text(_survey!.moreInfo ?? '', style: Styles().textStyles.getTextStyle('widget.message.large.fat'),), + child: Text(_survey!.moreInfo ?? '', style: AppTextStyles.widgetMessageLargeBold,), ); } @@ -256,14 +257,14 @@ class _SurveyWidgetState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ Visibility(visible: surveyWidget!.orientation == WidgetOrientation.left, child: surveyWidget.widget!), - Visibility(visible: !survey.allowSkip, child: Text("* ", semanticsLabel: Localization().getStringEx("widget.survey.label.required.hint", "Required"), style: textStyle ?? Styles().textStyles.getTextStyle('widget.error.regular.fat'))), + Visibility(visible: !survey.allowSkip, child: Text("* ", semanticsLabel: Localization().getStringEx("widget.survey.label.required.hint", "Required"), style: textStyle ?? AppTextStyles.widgetErrorRegularBold)), Visibility( visible: !surveyWidget.containsText, child: Flexible( child: Text( survey.text, textAlign: TextAlign.start, - style: textStyle ?? Styles().textStyles.getTextStyle('widget.message.medium'), + style: textStyle ?? AppTextStyles.widgetMessageMedium, ), ), ), @@ -278,7 +279,7 @@ class _SurveyWidgetState extends State { child: Text( survey.moreInfo ?? '', textAlign: TextAlign.start, - style: Styles().textStyles.getTextStyle('widget.detail.regular'), + style: AppTextStyles.widgetDetailRegular, ), ), ), @@ -412,11 +413,11 @@ class _SurveyWidgetState extends State { _onChangeResponse(false); }, enabled: enabled, - textWidget: Text(option.title, style: Styles().textStyles.getTextStyle('widget.detail.small'), textAlign: TextAlign.center), - backgroundDecoration: BoxDecoration(shape: BoxShape.circle, color: Styles().colors.surface), - borderDecoration: BoxDecoration(shape: BoxShape.circle, color: Styles().colors.fillColorPrimaryVariant), - selectedWidget: Container(alignment: Alignment.center, decoration: BoxDecoration(shape: BoxShape.circle, color: Styles().colors.fillColorSecondary)), - disabledWidget: Container(alignment: Alignment.center, decoration: BoxDecoration(shape: BoxShape.circle, color: Styles().colors.mediumGray)), + textWidget: Text(option.title, style: AppTextStyles.widgetDetailSmall, textAlign: TextAlign.center), + backgroundDecoration: BoxDecoration(shape: BoxShape.circle, color: AppColors.surface), + borderDecoration: BoxDecoration(shape: BoxShape.circle, color: AppColors.fillColorPrimaryVariant), + selectedWidget: Container(alignment: Alignment.center, decoration: BoxDecoration(shape: BoxShape.circle, color: AppColors.fillColorSecondary)), + disabledWidget: Container(alignment: Alignment.center, decoration: BoxDecoration(shape: BoxShape.circle, color: AppColors.textDisabled)), ), ))); } @@ -434,8 +435,8 @@ class _SurveyWidgetState extends State { survey.response = false; } return SurveyDataWidget(Checkbox( - checkColor: Styles().colors.surface, - activeColor: Styles().colors.fillColorPrimary, + checkColor: AppColors.surface, + activeColor: AppColors.fillColorPrimary, value: survey.response, onChanged: enabled ? (bool? value) { // if (survey.scored && survey.response != null) { @@ -453,7 +454,7 @@ class _SurveyWidgetState extends State { } return SurveyDataWidget(Switch( value: survey.response, - activeColor: Styles().colors.fillColorPrimary, + activeColor: AppColors.fillColorPrimary, onChanged: enabled ? (bool value) { // if (survey.scored && survey.response != null) { // return; @@ -632,12 +633,12 @@ class _SurveyWidgetState extends State { return SurveyDataWidget(Row( mainAxisSize: MainAxisSize.max, children: [ - Container(decoration: BoxDecoration(color: Styles().colors.surface, borderRadius: BorderRadius.circular(8)),child: Padding( + Container(decoration: BoxDecoration(color: AppColors.surface, borderRadius: BorderRadius.circular(8)),child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 4.0), - child: Text(label, style: Styles().textStyles.getTextStyle('headline3')), + child: Text(label, style: AppTextStyles.widgetHeadingRegular), )), Expanded( - child: Slider(value: value, min: min, max: max, label: label, activeColor: Styles().colors.fillColorPrimary, onChanged: enabled ? (value) { + child: Slider(value: value, min: min, max: max, label: label, activeColor: AppColors.fillColorPrimary, onChanged: enabled ? (value) { survey.response = value; _onChangeResponse(false); } : null) @@ -661,8 +662,8 @@ class _SurveyWidgetState extends State { List buttons = []; for (int i = min; i <= max; i++) { buttons.add(Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(i.toString(), style: Styles().textStyles.getTextStyle('label')), - Radio(value: i, groupValue: value, activeColor: Styles().colors.fillColorPrimary, + Text(i.toString(), style: AppTextStyles.widgetDetailRegular), + Radio(value: i, groupValue: value, activeColor: AppColors.fillColorPrimary, onChanged: enabled ? (Object? value) { survey.response = value; _onChangeResponse(false); @@ -676,7 +677,7 @@ class _SurveyWidgetState extends State { Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: buttons), Padding( padding: const EdgeInsets.only(top: 24.0), - child: Container(height: 1, color: Styles().colors.dividerLine), + child: Container(height: 1, color: AppColors.dividerLine), ) ], ); @@ -885,7 +886,7 @@ class CustomIconSelectionList extends StatelessWidget { child: InkWell( onTap: onChanged != null ? () => onChanged!(index) : null, child: ListTile( - title: Transform.translate(offset: const Offset(-15, 0), child: Text(optionList[index].title, style: selected ? Styles().textStyles.getTextStyle('labelSelected') : Styles().textStyles.getTextStyle('label'))), + title: Transform.translate(offset: const Offset(-15, 0), child: Text(optionList[index].title, style: selected ? AppTextStyles.widgetDetailRegularBold : AppTextStyles.widgetDetailRegular)), leading: Row( mainAxisSize: MainAxisSize.min, @@ -908,11 +909,11 @@ class CustomIconSelectionList extends StatelessWidget { Text( "Correct Answer: ", textAlign: TextAlign.start, - style: Styles().textStyles.getTextStyle('headline2')), + style: AppTextStyles.widgetHeadingLarge), Text( correctAnswer ?? "", textAlign: TextAlign.start, - style: Styles().textStyles.getTextStyle('body')) + style: AppTextStyles.widgetDescriptionRegular) ], ), )), @@ -962,8 +963,9 @@ class SingleSelectionList extends StatelessWidget { child: Card( clipBehavior: Clip.hardEdge, child: RadioListTile( - title: Transform.translate(offset: const Offset(-15, 0), child: Text(title, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16 /*, color: Styles().colors.headlineText */))), - activeColor: Styles().colors.fillColorSecondary, + title: Transform.translate(offset: const Offset(-15, 0), + child: Text(title, style: AppTextStyles.widgetTitleRegular)), + activeColor: AppColors.fillColorSecondary, value: title, groupValue: selectedValue?.title, onChanged: onChanged != null ? (_) => onChanged!(index) : null, @@ -998,9 +1000,9 @@ class MultiSelectionList extends StatelessWidget { child: InkWell( onTap: onChanged != null ? () => onChanged!(index) : null, child: CheckboxListTile( - title: Transform.translate(offset: const Offset(-15, 0), child: Text(selectionList[index].title, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16 /* , color: Styles().colors.headlineText */))), + title: Transform.translate(offset: const Offset(-15, 0), child: Text(selectionList[index].title, style: AppTextStyles.widgetDetailRegular)), checkColor: Colors.white, - activeColor: Styles().colors.fillColorSecondary, + activeColor: AppColors.fillColorSecondary, value: isChecked?[index], onChanged: onChanged != null ? (_) => onChanged!(index) : null, contentPadding: const EdgeInsets.all(8), diff --git a/lib/ui/widgets/survey_creation.dart b/lib/ui/widgets/survey_creation.dart index d120507c0..b5e563f42 100644 --- a/lib/ui/widgets/survey_creation.dart +++ b/lib/ui/widgets/survey_creation.dart @@ -14,6 +14,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/model/actions.dart'; import 'package:rokwire_plugin/model/options.dart'; @@ -135,7 +136,7 @@ class _SurveyElementListState extends State { maxLines: 3, ) : Text( label, - style: Styles().textStyles.getTextStyle('widget.detail.medium'), + style: AppTextStyles.widgetDetailMedium, overflow: TextOverflow.ellipsis, maxLines: 3, )), @@ -152,9 +153,9 @@ class _SurveyElementListState extends State { child: ListTileTheme(horizontalTitleGap: 8, child: rokwire.ExpansionTile( key: grandParentElement == null && (parentIndex ?? 0) > 0 && _handleScrolling ? (widget.targetWidgetKeys?[parentIndex! - 1]) : null, controller: parentElement == null ? widget.controller : null, - iconColor: Styles().colors.getColor('fillColorSecondary'), - backgroundColor: Styles().colors.getColor('background'), - collapsedBackgroundColor: Styles().colors.getColor('surface'), + iconColor: AppColors.fillColorSecondary, + backgroundColor: AppColors.background, + collapsedBackgroundColor: AppColors.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)), collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)), title: useSubtitle ? GestureDetector( @@ -162,17 +163,17 @@ class _SurveyElementListState extends State { child: Text.rich(TextSpan(children: [ TextSpan( text: 'From ', - style: Styles().textStyles.getTextStyle('widget.detail.medium'), + style: AppTextStyles.widgetDetailMedium, ), TextSpan( text: widget.dataSubtitles![parentIndex]!, - style: Styles().textStyles.getTextStyle('widget.button.title.medium.fat.underline'), + style: AppTextStyles.widgetButtonTitleMediumBoldUnderline, ), ],),), ) : title, subtitle: useSubtitle ? Padding(padding: const EdgeInsets.only(bottom: 4), child: title) : null, children: [ - Container(height: 2, color: Styles().colors.getColor('fillColorSecondary'),), + Container(height: 2, color: AppColors.fillColorSecondary,), dataList.isNotEmpty ? ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, @@ -203,14 +204,14 @@ class _SurveyElementListState extends State { entryText = data; } - Widget dataKeyText = Text('${index + 1}. $entryText', style: Styles().textStyles.getTextStyle('widget.detail.medium'), overflow: TextOverflow.ellipsis, maxLines: 2,); + Widget dataKeyText = Text('${index + 1}. $entryText', style: AppTextStyles.widgetDetailMedium, overflow: TextOverflow.ellipsis, maxLines: 2,); List textWidgets = [dataKeyText]; if (_handleScrolling && widget.dataSubtitles?[index] != null) { textWidgets.add(GestureDetector( onTap: widget.onScroll != null ? () => widget.onScroll!(widget.widgetKeys![index]) : null, child: Padding( padding: const EdgeInsets.only(top: 4), - child: Text(widget.dataSubtitles![index]!, style: Styles().textStyles.getTextStyle('widget.button.title.medium.fat.underline')) + child: Text(widget.dataSubtitles![index]!, style: AppTextStyles.widgetButtonTitleMediumBoldUnderline) ) )); } @@ -219,7 +220,7 @@ class _SurveyElementListState extends State { Widget displayEntry = Card( key: _handleScrolling ? (widget.targetWidgetKeys?[index]) : null, margin: const EdgeInsets.symmetric(vertical: 4), - color: Styles().colors.getColor('surface'), + color: AppColors.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)), child: InkWell( onTap: widget.onEdit != null ? () => widget.onEdit!(index, surveyElement, null, null) : null, @@ -235,7 +236,7 @@ class _SurveyElementListState extends State { maxSimultaneousDrags: 1, feedback: Card(child: Container( padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: Styles().colors.getColor('surface')), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: AppColors.surface), child: Align(alignment: Alignment.centerLeft, child: dataKeyText), )), child: DragTarget>( @@ -252,7 +253,7 @@ class _SurveyElementListState extends State { Widget _buildTextEntryWidget(int index, dynamic data, SurveyElement surveyElement, RuleElement? parentElement, int depth) { Widget sectionTextEntry = TextField( controller: data as TextEditingController, - style: Styles().textStyles.getTextStyle('widget.detail.medium'), + style: AppTextStyles.widgetDetailMedium, decoration: InputDecoration.collapsed( hintText: surveyElement == SurveyElement.sections ? "Section Name" : "Value", border: InputBorder.none, @@ -260,7 +261,7 @@ class _SurveyElementListState extends State { ); return Card( margin: const EdgeInsets.symmetric(vertical: 4), - color: Styles().colors.getColor('surface'), + color: AppColors.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)), child: Padding(padding: const EdgeInsets.all(8), child: Row(children: [ Expanded(flex: 3, child: Padding(padding: const EdgeInsets.only(left: 8), child: sectionTextEntry)), @@ -273,7 +274,7 @@ class _SurveyElementListState extends State { if (data is Pair) { return Card( margin: const EdgeInsets.symmetric(vertical: 4), - color: Styles().colors.getColor('surface'), + color: AppColors.surface, child: SurveyElementCreationWidget.buildCheckboxWidget(data.left, data.right, widget.onChanged != null ? (value) => widget.onChanged!(index, value) : null, padding: EdgeInsets.zero) ); } @@ -312,11 +313,11 @@ class _SurveyElementListState extends State { child: Text.rich(TextSpan(children: [ TextSpan( text: 'From ', - style: Styles().textStyles.getTextStyle('widget.detail.medium'), + style: AppTextStyles.widgetDetailMedium, ), TextSpan( text: widget.dataSubtitles![index]!, - style: Styles().textStyles.getTextStyle('widget.button.title.medium.fat.underline'), + style: AppTextStyles.widgetButtonTitleMediumBoldUnderline, recognizer: TapGestureRecognizer()..onTap = widget.onScroll != null ? () => widget.onScroll!(widget.widgetKeys![index - 1]) : null ), ],), overflow: TextOverflow.ellipsis, maxLines: 2,) @@ -324,7 +325,7 @@ class _SurveyElementListState extends State { } textWidgets.add(Text.rich(TextSpan(children: _buildTextSpansForLink(summary, surveyElement)), overflow: TextOverflow.ellipsis, maxLines: 2,)); } else { - textWidgets.add(Text(summary, style: Styles().textStyles.getTextStyle('widget.detail.medium'), overflow: TextOverflow.ellipsis, maxLines: 2,)); + textWidgets.add(Text(summary, style: AppTextStyles.widgetDetailMedium, overflow: TextOverflow.ellipsis, maxLines: 2,)); } Widget ruleText = Column(crossAxisAlignment: CrossAxisAlignment.start, children: textWidgets); int numButtons = _numEntryManagementButtons(index, element: data, parentElement: parentElement, addRemove: addRemove); @@ -367,7 +368,7 @@ class _SurveyElementListState extends State { // maxSimultaneousDrags: 1, // feedback: Card(child: Container( // padding: const EdgeInsets.all(16), - // decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: Styles().colors.getColor('surface')), + // decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: AppColors.surface), // child: Align(alignment: Alignment.centerLeft, child: ruleText) // )), // child: DragTarget( @@ -395,7 +396,7 @@ class _SurveyElementListState extends State { } Widget optionDataText = Text( '${index + 1}. $entryText', - style: Styles().textStyles.getTextStyle(data.isCorrect ? 'widget.detail.medium.fat' : 'widget.detail.medium'), + style: data.isCorrect ? AppTextStyles.widgetDetailRegularBold : AppTextStyles.widgetDetailMedium, overflow: TextOverflow.ellipsis, maxLines: 2, ); @@ -416,7 +417,7 @@ class _SurveyElementListState extends State { maxSimultaneousDrags: 1, feedback: Card(child: Container( padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: Styles().colors.getColor('surface')), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: AppColors.surface), child: optionDataText, )), child: DragTarget( @@ -434,14 +435,14 @@ class _SurveyElementListState extends State { Widget _buildActionsWidget(int index, dynamic data, SurveyElement surveyElement, RuleElement? parentElement, int depth) { if (data is ActionData) { - Widget actionDataText = Text('${index + 1}. ${data.label ?? 'New Action'}', style: Styles().textStyles.getTextStyle('widget.detail.medium'), overflow: TextOverflow.ellipsis, maxLines: 2,); + Widget actionDataText = Text('${index + 1}. ${data.label ?? 'New Action'}', style: AppTextStyles.widgetDetailMedium, overflow: TextOverflow.ellipsis, maxLines: 2,); List textWidgets = [actionDataText]; if (data.data != null) { String dataString = data.data.toString(); if (dataString.isNotEmpty) { textWidgets.add(Padding( padding: const EdgeInsets.only(top: 4), - child: Text(dataString, style: Styles().textStyles.getTextStyle('widget.detail.medium')) + child: Text(dataString, style: AppTextStyles.widgetDetailMedium) )); } } @@ -463,7 +464,7 @@ class _SurveyElementListState extends State { maxSimultaneousDrags: 1, feedback: Card(child: Container( padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: Styles().colors.getColor('surface')), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: AppColors.surface), child: actionDataText, )), child: DragTarget( @@ -494,13 +495,13 @@ class _SurveyElementListState extends State { double buttonSize = _entryManagementButtonSize / 2; return Align(alignment: Alignment.centerRight, child: Row(mainAxisSize: MainAxisSize.min, children: [ Visibility(visible: addRemove && belowLimit, child: SizedBox(width: _entryManagementButtonSize, height: _entryManagementButtonSize, child: IconButton( - icon: Styles().images.getImage('plus-circle', color: Styles().colors.getColor('fillColorPrimary'), size: buttonSize) ?? const Icon(Icons.add), + icon: Styles().images.getImage('plus-circle', color: AppColors.fillColorPrimary, size: buttonSize) ?? const Icon(Icons.add), onPressed: widget.onAdd != null ? () => widget.onAdd!(index + 1, surveyElement, parentElement) : null, padding: EdgeInsets.zero, splashRadius: buttonSize, ))), Visibility(visible: editable, child: SizedBox(width: _entryManagementButtonSize, height: _entryManagementButtonSize, child: IconButton( - icon: Styles().images.getImage('edit-white', color: Styles().colors.getColor('fillColorPrimary'), size: buttonSize) ?? const Icon(Icons.edit), + icon: Styles().images.getImage('edit-white', color: AppColors.fillColorPrimary, size: buttonSize) ?? const Icon(Icons.edit), onPressed: widget.onEdit != null ? () => widget.onEdit!(index, surveyElement, element, parentElement) : null, padding: EdgeInsets.zero, splashRadius: buttonSize, @@ -552,7 +553,7 @@ class _SurveyElementListState extends State { if (dataKeyIndex > 0) { textSpans.add(TextSpan( text: partialLink, - style: Styles().textStyles.getTextStyle('widget.button.title.medium.fat.underline'), + style: AppTextStyles.widgetButtonTitleMediumBoldUnderline, recognizer: TapGestureRecognizer()..onTap = widget.onScroll != null ? () => widget.onScroll!(widget.widgetKeys![dataKeyIndex + widgetKeyOffset]) : null, )); previousLink = true; @@ -566,7 +567,7 @@ class _SurveyElementListState extends State { } textSpans.add(TextSpan( text: text, - style: Styles().textStyles.getTextStyle('widget.detail.medium'), + style: AppTextStyles.widgetDetailMedium, )); previousLink = false; } @@ -660,20 +661,20 @@ class SurveyElementCreationWidget extends StatefulWidget { static Widget buildDropdownWidget(Map supportedItems, String label, T? value, Function(T?)? onChanged, {EdgeInsetsGeometry padding = const EdgeInsets.symmetric(horizontal: 16), EdgeInsetsGeometry margin = const EdgeInsets.only(top: 16)}) { return Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: Styles().colors.getColor('surface')), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0), color: AppColors.surface), padding: padding, margin: margin, child: Row(children: [ - Text(label, style: Styles().textStyles.getTextStyle('widget.message.regular')), + Text(label, style: AppTextStyles.widgetMessageRegular), Expanded(child: Align(alignment: Alignment.centerRight, child: DropdownButtonHideUnderline(child: DropdownButton( icon: Styles().images.getImage('chevron-down', excludeFromSemantics: true), isExpanded: true, - style: Styles().textStyles.getTextStyle('widget.detail.regular'), + style: AppTextStyles.widgetDetailRegular, items: buildDropdownItems(supportedItems), value: value, onChanged: onChanged, - dropdownColor: Styles().colors.getColor('surface'), + dropdownColor: AppColors.surface, ), ),))], ) @@ -682,11 +683,11 @@ class SurveyElementCreationWidget extends StatefulWidget { static Widget buildCheckboxWidget(String label, bool value, Function(bool?)? onChanged, {EdgeInsetsGeometry padding = const EdgeInsets.only(top: 16.0)}) { return Padding(padding: padding, child: CheckboxListTile( - title: Padding(padding: const EdgeInsets.only(left: 8), child: Text(label, style: Styles().textStyles.getTextStyle('widget.message.regular'))), + title: Padding(padding: const EdgeInsets.only(left: 8), child: Text(label, style: AppTextStyles.widgetMessageRegular)), contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), - tileColor: Styles().colors.getColor('surface'), - checkColor: Styles().colors.getColor('surface'), - activeColor: Styles().colors.getColor('fillColorPrimary'), + tileColor: AppColors.surface, + checkColor: AppColors.surface, + activeColor: AppColors.fillColorPrimary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)), value: value, onChanged: onChanged, @@ -701,7 +702,7 @@ class SurveyElementCreationWidget extends StatefulWidget { value: item.key, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text(item.value, style: Styles().textStyles.getTextStyle('widget.detail.regular'), textAlign: TextAlign.center, maxLines: 2,) + child: Text(item.value, style: AppTextStyles.widgetDetailRegular, textAlign: TextAlign.center, maxLines: 2,) ), alignment: Alignment.centerRight, )); @@ -731,7 +732,7 @@ class _SurveyElementCreationWidgetState extends State _TabBarState(); @protected - Color? get backgroundColor { + Color? get backgroundColor => AppColors.surface; + + @protected + Color? get environmentColor { switch(Config().configEnvironment) { case ConfigEnvironment.dev: return Colors.yellowAccent; case ConfigEnvironment.test: return Colors.lightGreenAccent; - case ConfigEnvironment.production: return Styles().colors.surface; - default: return Colors.white; + case ConfigEnvironment.production: return AppColors.surface; + default: return null; } } @protected - BoxBorder? get border => Border(top: BorderSide(color: Styles().colors.surfaceAccent, width: 1, style: BorderStyle.solid)); + BoxBorder? get border => Border(top: BorderSide(color: AppColors.surfaceAccent, width: 1, style: BorderStyle.solid)); @protected Decoration? get decoration => BoxDecoration(color: backgroundColor, border: border); @@ -71,13 +75,11 @@ class _TabBarState extends State implements NotificationsListener { Widget build(BuildContext context) { return Column(mainAxisSize: MainAxisSize.min, children: [ Container(decoration: widget.decoration, child: - SafeArea(child: - Row(children: - buildTabs(), - ), - ), + Row(children: buildTabs()), ), - ],); + Visibility(visible: widget.environmentColor != null, + child: Container(height: 4, color: widget.environmentColor)) + ]); } @protected @@ -87,9 +89,7 @@ class _TabBarState extends State implements NotificationsListener { for (int tabIndex = 0; tabIndex < tabsCount; tabIndex++) { Widget? tab = widget.buildTab(context, _contentListCodes![tabIndex], tabIndex); if (tab != null) { - tabs.add(Expanded( - child: tab, - )); + tabs.add(Expanded(child: tab)); } } @@ -132,6 +132,7 @@ class TabWidget extends StatelessWidget { final String? iconKey; final String? selectedIconKey; final bool selected; + final bool showAnimation; final void Function(TabWidget tabWidget) onTap; const TabWidget({ @@ -141,16 +142,22 @@ class TabWidget extends StatelessWidget { this.selectedIconKey, this.hint, this.selected = false, + this.showAnimation = true, required this.onTap }) : super(key: key); @override Widget build(BuildContext context) { - return GestureDetector(onTap: () => onTap(this), behavior: HitTestBehavior.translucent, child: - Stack(children: [ - buildTab(context), - selected ? buildSelectedIndicator(context) : Container(), - ], + return Material(color: Colors.transparent, + child: InkWell( + onTap: () => onTap(this), + splashFactory: showAnimation ? null : NoSplash.splashFactory, + splashColor: showAnimation ? null : Colors.transparent, + highlightColor: showAnimation ? null : Colors.transparent, + child: Column(children: [ + selected ? buildSelectedIndicator(context) : Container(), + buildTab(context), + ]), ), ); } @@ -186,7 +193,7 @@ class TabWidget extends StatelessWidget { TextAlign get tabTextAlign => TextAlign.center; @protected - TextStyle get tabTextStyle => TextStyle(fontFamily: Styles().fontFamilies.bold, color: selected ? Styles().colors.fillColorSecondary : Styles().colors.mediumGray, fontSize: 12); + TextStyle get tabTextStyle => TextStyle(fontFamily: AppFontFamilies.bold, color: selected ? AppColors.fillColorSecondary : AppColors.textMedium, fontSize: 12); @protected double getTextScaleFactor(BuildContext context) => min(MediaQuery.of(context).textScaler.scale(1), 2); @@ -205,7 +212,8 @@ class TabWidget extends StatelessWidget { Widget getTabIcon(BuildContext context) { String? key = selected ? (selectedIconKey ?? iconKey) : iconKey; Widget defaultIcon = SizedBox(width: tabIconSize.width, height: tabIconSize.height); - return (key != null) ? Styles().images.getImage(key, width: tabIconSize.width, height: tabIconSize.height) ?? defaultIcon : defaultIcon; + return (key != null) ? Styles().images.getImage(key, width: tabIconSize.width, height: tabIconSize.height, + color: selected ? AppColors.fillColorSecondary : AppColors.textDisabled) ?? defaultIcon : defaultIcon; } @protected @@ -214,17 +222,13 @@ class TabWidget extends StatelessWidget { // Selected Indicator @protected - Widget buildSelectedIndicator(BuildContext context) => Positioned.fill(child: - Column(mainAxisAlignment: MainAxisAlignment.start, children: [ - Container(height: selectedIndicatorHeight, color: selectedIndicatorColor) - ],), - ); + Widget buildSelectedIndicator(BuildContext context) => Container(height: selectedIndicatorHeight, color: selectedIndicatorColor); @protected double get selectedIndicatorHeight => 4; @protected - Color? get selectedIndicatorColor => Styles().colors.fillColorSecondary; + Color? get selectedIndicatorColor => AppColors.fillColorSecondary; } diff --git a/lib/ui/widgets/tile_button.dart b/lib/ui/widgets/tile_button.dart index 4166c9cbd..3cad75a40 100644 --- a/lib/ui/widgets/tile_button.dart +++ b/lib/ui/widgets/tile_button.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/styles.dart'; class TileButton extends StatelessWidget { @@ -25,7 +26,8 @@ class TileButton extends StatelessWidget { final double titleFontSize; final TextStyle? titleTextStyle; - final String? iconAsset; + final String? iconKey; + final Widget? icon; final Border? border; final BorderRadiusGeometry? borderRadius; @@ -48,7 +50,8 @@ class TileButton extends StatelessWidget { this.titleFontSize = 20, this.titleTextStyle, - this.iconAsset, + this.icon, + this.iconKey, this.border, this.borderRadius, @@ -66,13 +69,16 @@ class TileButton extends StatelessWidget { @override Widget build(BuildContext context) { List contentList = []; - if (iconAsset != null) { - Widget? icon = Styles().images.getImage(iconAsset!); + if (icon != null) { + contentList.add(icon!); + } else if (iconKey != null) { + Widget? icon = Styles().images.getImage(iconKey!); if (icon != null) { contentList.add(icon); } } - if ((title != null) && (iconAsset != null)) { + + if ((title != null) && ((icon != null) || (iconKey != null))) { contentList.add(Container(height: contentSpacing)); } if (title != null) { @@ -92,16 +98,16 @@ class TileButton extends StatelessWidget { ); } - @protected Color? get defaultTitleTextColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultTitleTextColor => AppColors.fillColorPrimary; @protected Color? get displayTitleTextColor => titleTextColor ?? defaultTitleTextColor; - @protected String? get defaultTitleFontFamilly => Styles().fontFamilies.bold; + @protected String? get defaultTitleFontFamilly => AppFontFamilies.bold; @protected String? get displayTitleFontFamilly => titleFontFamilly ?? defaultTitleFontFamilly; @protected TextStyle get defaultTitleTextStyle => TextStyle(color: displayTitleTextColor, fontFamily: displayTitleFontFamilly, fontSize: titleFontSize); @protected TextStyle get displayTitleTextStyle => titleTextStyle ?? defaultTitleTextStyle; - @protected Color get defaultBorderColor => Styles().colors.white; + @protected Color get defaultBorderColor => AppColors.surface; @protected Color get displayBorderColor => borderColor ?? defaultBorderColor; @protected BorderRadiusGeometry get defaultBorderRadius => BorderRadius.circular(4); @@ -123,7 +129,8 @@ class TileWideButton extends StatelessWidget { final String? titleFontFamilly; final double titleFontSize; final TextStyle? titleTextStyle; - + + final Widget? icon; final String? iconAsset; final Border? border; @@ -146,6 +153,7 @@ class TileWideButton extends StatelessWidget { this.titleFontSize = 20, this.titleTextStyle, + this.icon, this.iconAsset, this.border, @@ -160,16 +168,16 @@ class TileWideButton extends StatelessWidget { this.onTap }) : super(key: key); - @protected Color? get defaultTitleTextColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultTitleTextColor => AppColors.fillColorPrimary; @protected Color? get displayTitleTextColor => titleTextColor ?? defaultTitleTextColor; - @protected String? get defaultTitleFontFamilly => Styles().fontFamilies.bold; + @protected String? get defaultTitleFontFamilly => AppFontFamilies.bold; @protected String? get displayTitleFontFamilly => titleFontFamilly ?? defaultTitleFontFamilly; @protected TextStyle get defaultTitleTextStyle => TextStyle(color: displayTitleTextColor, fontFamily: displayTitleFontFamilly, fontSize: titleFontSize); @protected TextStyle get displayTitleTextStyle => titleTextStyle ?? defaultTitleTextStyle; - @protected Color get defaultBorderColor => Styles().colors.white; + @protected Color get defaultBorderColor => AppColors.surface; @protected Color get displayBorderColor => borderColor ?? defaultBorderColor; @protected BorderRadiusGeometry get defaultBorderRadius => BorderRadius.circular(4); @@ -189,8 +197,10 @@ class TileWideButton extends StatelessWidget { List contentList = []; if (title != null) { contentList.add(Expanded(child: Text(title!, textAlign: TextAlign.center, style: displayTitleTextStyle))); - } - if (iconAsset != null) { + } + if (icon != null) { + contentList.add(Expanded(child: Column(mainAxisSize: MainAxisSize.min, children: [icon!]))); + } else if (iconAsset != null) { Widget? icon = Styles().images.getImage(iconAsset!); if (icon != null) { contentList.add(Expanded(child: Column(mainAxisSize: MainAxisSize.min, children: [icon]))); @@ -293,7 +303,7 @@ class TileToggleButton extends StatelessWidget { @protected Color? get displayBackgroundColor => selected ? selectedBackgroundColor : backgroundColor; - @protected Color get defaultSelectedBorderColor => Styles().colors.fillColorPrimary; + @protected Color get defaultSelectedBorderColor => AppColors.fillColorPrimary; @protected Color get displaySelectedBorderColor => selectedBorderColor ?? defaultSelectedBorderColor; @protected Color get displayBorderColor => selected ? displaySelectedBorderColor : borderColor; @@ -301,18 +311,18 @@ class TileToggleButton extends StatelessWidget { @protected BorderRadiusGeometry get displayBorderRadius => borderRadius ?? defaultBorderRadius; @protected BoxBorder get defaultBorder => Border.all(color: displayBorderColor, width: borderWidth); - @protected BoxBorder get dislayBorder => defaultBorder; + @protected BoxBorder get displayBorder => defaultBorder; - @protected List get defaultBorderShadow => [BoxShadow(color: Styles().colors.blackTransparent018, offset: const Offset(2, 2), blurRadius: 6)]; + @protected List get defaultBorderShadow => [BoxShadow(color: AppColors.shadow, offset: const Offset(2, 2), blurRadius: 6)]; @protected List get displayBorderShadow => borderShadow ?? defaultBorderShadow; - @protected Decoration get defaultDecoration => BoxDecoration(color: displayBackgroundColor, borderRadius: displayBorderRadius, border: dislayBorder, boxShadow: displayBorderShadow); + @protected Decoration get defaultDecoration => BoxDecoration(color: displayBackgroundColor, borderRadius: displayBorderRadius, border: displayBorder, boxShadow: displayBorderShadow); @protected Decoration get displayDecoration => defaultDecoration; - @protected Color? get defaultTitleColor => Styles().colors.fillColorPrimary; + @protected Color? get defaultTitleColor => AppColors.fillColorPrimary; @protected Color? get displayTitleColor => (selected ? selectedTitleColor : titleColor) ?? defaultTitleColor; - @protected String? get defaultTitleFontFamily => Styles().fontFamilies.bold; + @protected String? get defaultTitleFontFamily => AppFontFamilies.bold; @protected String? get displayTitleFontFamily => titleFontFamily ?? defaultTitleFontFamily; @protected TextStyle get defaultTitleStyle => TextStyle(fontFamily: displayTitleFontFamily, fontSize: titleFontSize, color: displayTitleColor); diff --git a/lib/ui/widgets/ui_image.dart b/lib/ui/widgets/ui_image.dart new file mode 100644 index 000000000..519c0b6b4 --- /dev/null +++ b/lib/ui/widgets/ui_image.dart @@ -0,0 +1,72 @@ +import 'package:flutter/widgets.dart'; +import 'package:rokwire_plugin/service/styles.dart'; + +class UiImage extends StatelessWidget { + final ImageSpec? spec; + final Widget? defaultWidget; + final bool excludeFromSemantics; + final Animation? opacity; + final ImageRepeat? repeat; + final Rect? centerSlice; + final Map? networkHeaders; + final Widget Function(BuildContext, Widget, int?, bool)? frameBuilder; + final Widget Function(BuildContext, Widget, ImageChunkEvent?)? loadingBuilder; + final Widget Function(BuildContext, Object, StackTrace?)? errorBuilder; + const UiImage({super.key, this.spec, this.defaultWidget, this.excludeFromSemantics = false, + this.opacity, this.repeat, this.centerSlice, this.networkHeaders, + this.frameBuilder, this.loadingBuilder, this.errorBuilder}); + + UiImage apply({Key? key, Widget? defaultWidget, dynamic source, double? scale, double? size, + double? width, double? height, String? weight, Color? color, String? semanticLabel, bool? excludeFromSemantics, + double? fill, double? grade, double? opticalSize, String? fontFamily, String? fontPackage, + bool? isAntiAlias, bool? matchTextDirection, bool? gaplessPlayback, AlignmentGeometry? alignment, + Animation? opacity, BlendMode? colorBlendMode, BoxFit? fit, FilterQuality? filterQuality, ImageRepeat? repeat, + Rect? centerSlice, TextDirection? textDirection, Map? networkHeaders, + Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, + Widget Function(BuildContext, Widget, ImageChunkEvent?)? loadingBuilder, + Widget Function(BuildContext, Object, StackTrace?)? errorBuilder}) { + + ImageSpec? imageSpec = spec; + if (imageSpec == null) { + return const UiImage(); + } + + imageSpec = ImageSpec.fromOther(imageSpec, source: source, scale: scale, size: size, + width: width, height: height, weight: weight, + fill: fill, grade: grade, opticalSize: opticalSize, + fontFamily: fontFamily, fontPackage: fontPackage, + color: color, semanticLabel: semanticLabel, isAntiAlias: isAntiAlias, + matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, + alignment: alignment, colorBlendMode: colorBlendMode, fit: fit, + filterQuality: filterQuality, repeat: repeat, textDirection: textDirection, + ); + + return UiImage(key: key ?? this.key, spec: imageSpec, + defaultWidget: defaultWidget ?? this.defaultWidget, + excludeFromSemantics: excludeFromSemantics ?? this.excludeFromSemantics, + opacity: opacity ?? this.opacity, repeat: repeat ?? this.repeat, + centerSlice: centerSlice ?? this.centerSlice, networkHeaders: networkHeaders ?? this.networkHeaders, + frameBuilder: frameBuilder ?? this.frameBuilder, loadingBuilder: loadingBuilder ?? this.loadingBuilder, + errorBuilder: errorBuilder ?? this.errorBuilder); + } + + @override + Widget build(BuildContext context) { + Widget? image; + ImageSpec? imageSpec = spec; + if (imageSpec != null) { + try { + if (imageSpec is FlutterImageSpec) { + image = UiImages.getFlutterImage(imageSpec); + } else if (imageSpec is FontAwesomeImageSpec) { + image = UiImages.getFaIcon(imageSpec); + } else if (imageSpec is MaterialIconImageSpec) { + image = UiImages.getMaterialIcon(imageSpec); + } + } catch(e) { + debugPrint(e.toString()); + } + } + return image ?? defaultWidget ?? Container(); + } +} diff --git a/lib/utils/image_utils.dart b/lib/utils/image_utils.dart index 039fecdc6..4e5b75739 100644 --- a/lib/utils/image_utils.dart +++ b/lib/utils/image_utils.dart @@ -167,7 +167,10 @@ class ImageUtils { } } - ui.ParagraphStyle paragraphStyle = textStyle.getParagraphStyle(textScaler: (0 < textScaleFactor) ? TextScaler.linear(textScaleFactor) : TextScaler.noScaling, textDirection: textDirection, textAlign: textAlign, maxLines: maxLines); + ui.ParagraphStyle paragraphStyle = textStyle.getParagraphStyle( + textScaler: (0 < textScaleFactor) ? TextScaler.linear(textScaleFactor) + : TextScaler.noScaling, textDirection: textDirection, + textAlign: textAlign, maxLines: maxLines); ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(paragraphStyle)..addText(text); return paragraphBuilder.build()..layout(ui.ParagraphConstraints(width: size.width)); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index aab5028f7..5b0c09d2a 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -29,20 +29,39 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:rokwire_plugin/service/network.dart'; import 'package:timezone/timezone.dart' as timezone; import 'package:url_launcher/url_launcher.dart'; +import 'package:universal_html/html.dart' as html; class StringUtils { static bool isNotEmpty(String? stringToCheck) => (stringToCheck != null && stringToCheck.isNotEmpty); + static bool isEmpty(String? stringToCheck) => + !isNotEmpty(stringToCheck); + static bool isNotEmptyString(dynamic value) => (value is String) && value.isNotEmpty; static String ensureNotEmpty(String? value, {String defaultValue = ''}) => - ((value != null) && value.isNotEmpty) ? value : defaultValue; + ((value != null) && value.isNotEmpty) ? value : defaultValue; - static bool isEmpty(String? stringToCheck) => - !isNotEmpty(stringToCheck); + static String? notEmptyString(String? value, [String? value1, String? value2, String? value3]) { + if (isNotEmpty(value)) { + return value; + } + else if (isNotEmpty(value1)) { + return value1; + } + else if (isNotEmpty(value2)) { + return value2; + } + else if (isNotEmpty(value3)) { + return value3; + } + else { + return null; + } + } static String wrapRange(String s, String firstValue, String secondValue, int startPosition, int endPosition) { String word = s.substring(startPosition, endPosition); @@ -164,6 +183,29 @@ class StringUtils { static bool isUinValid(String? uin) { return isNotEmpty(uin) && RegExp(_uinPattern).hasMatch(uin!); } + + static String base64UrlEncode(String value) => utf8.fuse(base64Url).encode(value); + + static String base64UrlDecode(String value) => utf8.fuse(base64Url).decode(value); + + static String generatePassword({bool letter = true, bool isNumber = true, bool isSpecial = true}) { + final length = 20; + final letterLowerCase = "abcdefghijklmnopqrstuvwxyz"; + final letterUpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final number = '0123456789'; + final special = '@#%^*>\$@?/[]=+'; + + String chars = ""; + if (letter) chars += '$letterLowerCase$letterUpperCase'; + if (isNumber) chars += '$number'; + if (isSpecial) chars += '$special'; + return List.generate(length, (index) { + final indexRandom = math.Random.secure().nextInt(chars.length); + return chars [indexRandom]; + }).join(''); + } + + static T? invokeIfExists(String? val, T Function(String val) inv) => (val != null) ? inv(val) : null; } class CollectionUtils { @@ -291,6 +333,19 @@ class SetUtils { } } +extension SetExt on Set { + bool containsAny(Iterable other) { + if (isNotEmpty && other.isNotEmpty) { + for (Object? entry in other) { + if (contains(entry)) { + return true; + } + } + } + return false; + } +} + class LinkedHashSetUtils { static LinkedHashSet? from(Iterable? elements) { return (elements != null) ? LinkedHashSet.from(elements) : null; @@ -349,6 +404,13 @@ class MapUtils { return null; } + static Map mergeToNew(Map dest, Map? src, { int? level }) { + Map out = {}; + out.addAll(dest); + merge(dest, src, level: level); + return out; + } + static void merge(Map dest, Map? src, { int? level }) { src?.forEach((String key, dynamic srcV) { dynamic destV = dest[key]; @@ -578,7 +640,7 @@ class UrlUtils { } static Uri? buildUri(Uri uri, { String? scheme, String? userInfo, String? host, int? port, String? path, String? query, String? fragment}) { - + String sourceHost = uri.host; String sourcePath = uri.path; if (sourceHost.isEmpty && sourcePath.isNotEmpty) { @@ -867,6 +929,29 @@ class JsonUtils { return null; } + static Map? mapStringsValue(dynamic value) { + try { + return (value is Map) ? value : null; + } + catch(e) { + debugPrint(e.toString()); + } + return null; + } + + static Map>? mapOfStringToMapOfStringsValue(dynamic value) { + Map>? result; + if (value is Map) { + result = >{}; + for (dynamic key in value.keys) { + if (key is String) { + MapUtils.set(result, key, mapStringsValue(value[key])); + } + } + } + return result; + } + static Map>? mapOfStringToLinkedHashSetOfStringsValue(dynamic value) { Map>? result; if (value is Map) { @@ -1227,7 +1312,9 @@ DayPart? dayPartFromString(String? value) { } class DateTimeUtils { - + + static const String defaultDateTimeFormat = 'yyyy-MM-ddTHH:mm:ss.SSS'; + static DateTime? dateTimeFromString(String? dateTimeString, {String? format, bool isUtc = false}) { if (StringUtils.isEmpty(dateTimeString)) { return null; @@ -1244,17 +1331,17 @@ class DateTimeUtils { return dateTime; } - static String? utcDateTimeToString(DateTime? dateTime, { String format = 'yyyy-MM-ddTHH:mm:ss.SSS' }) { + static String? utcDateTimeToString(DateTime? dateTime, { String format = defaultDateTimeFormat }) { return (dateTime != null) ? (DateFormat(format).format(dateTime.isUtc ? dateTime : dateTime.toUtc()) + 'Z') : null; } - static String? localDateTimeToString(DateTime? dateTime, { String format = 'yyyy-MM-ddTHH:mm:ss.SSS' }) { + static String? localDateTimeToString(DateTime? dateTime, { String format = defaultDateTimeFormat }) { return (dateTime != null) ? (DateFormat(format).format(dateTime.toLocal())) : null; } static DateTime? dateTimeFromSecondsSinceEpoch(int? seconds) => (seconds != null) ? DateTime.fromMillisecondsSinceEpoch(seconds * 1000) : null; - + static int? dateTimeToSecondsSinceEpoch(DateTime? dateTime) => (dateTime != null) ? (dateTime.millisecondsSinceEpoch ~/ 1000) : null; @@ -1295,6 +1382,14 @@ class DateTimeUtils { return (date != null) ? DateTime(date.year, date.month, date.day) : null; } + static DateTime dayStart(DateTime date) { + return DateTime(date.year, date.month, date.day); + } + + static DateTime dayEnd(DateTime date) { + return dayStart(date).add(const Duration(days: 1)).subtract(const Duration(microseconds: 1)); + } + static DateTime nowTimezone(timezone.Location? location) { DateTime now = DateTime.now(); if (location != null) { @@ -1303,48 +1398,58 @@ class DateTimeUtils { return now; } - static bool isToday(DateTime? date, {timezone.Location? location}) { + static bool isToday(DateTime? date, {DateTime? now, timezone.Location? location}) { if (date == null) { return false; } - DateTime now = nowTimezone(location); + now ??= nowTimezone(location); return now.day == date.day && now.month == date.month && now.year == date.year; } - static bool isYesterday(DateTime? date, {timezone.Location? location}) { + static bool isYesterday(DateTime? date, {DateTime? now, timezone.Location? location}) { if (date == null) { return false; } - DateTime yesterday = nowTimezone(location).subtract(const Duration(days: 1)); + now ??= nowTimezone(location); + DateTime yesterday = now.subtract(const Duration(days: 1)); return yesterday.day == date.day && yesterday.month == date.month && yesterday.year == date.year; } - static bool isTomorrow(DateTime? date, {timezone.Location? location}) { + static bool isTomorrow(DateTime? date, {DateTime? now, timezone.Location? location}) { if (date == null) { return false; } - DateTime tomorrow = nowTimezone(location).add(const Duration(days: 1)); + now ??= nowTimezone(location); + DateTime tomorrow = now.add(const Duration(days: 1)); return tomorrow.day == date.day && tomorrow.month == date.month && tomorrow.year == date.year; } - static bool isThisWeek(DateTime? date, {timezone.Location? location}) { + static bool isThisWeek(DateTime? date, {DateTime? now, timezone.Location? location}) { + return isInRange(date, start: weekStart(now: now, location: location), end: weekEnd(now: now, location: location)); + } + + static DateTime weekStart({DateTime? now, timezone.Location? location}) { + now ??= nowTimezone(location); + DateTime today = dayStart(now); + return today.subtract(Duration(days: today.weekday - 1)); + } + + static DateTime weekEnd({DateTime? now, timezone.Location? location}) { + return weekStart(now: now, location: location).add(const Duration(days: 7)).subtract(const Duration(microseconds: 1)); + } + + static bool isInRange(DateTime? date, {DateTimeRange? range, DateTime? start, DateTime? end}) { if (date == null) { return false; } - if (date.isAfter(weekStart(location: location)) && date.isBefore(weekEnd(location: location))) { + if (range != null && date.isAfter(range.start) && date.isBefore(range.end)) { + return true; + } + if (start != null && end != null && date.isAfter(start) && date.isBefore(end)) { return true; } return false; } - - static DateTime weekStart({timezone.Location? location}) { - DateTime now = nowTimezone(location); - return now.subtract(Duration(days: now.weekday - 1)); - } - - static DateTime weekEnd({timezone.Location? location}) { - return weekStart(location: location).add(const Duration(days: 7)).subtract(const Duration(microseconds: 1)); - } static timezone.TZDateTime? changeTimeZoneToDate(DateTime time, timezone.Location location) { try{ @@ -1448,6 +1553,21 @@ extension TZDateTimeExt on timezone.TZDateTime { } } +class WebUtils { + static String getCookie(String name) { + String? cookie = html.document.cookie; + if (StringUtils.isNotEmpty(cookie)) { + for (String item in cookie!.split(";")) { + final split = item.split("="); + if (split[0].trim() == name) { + return split[1]; + } + } + } + return ""; + } +} + class Pair { final L left; final R right; diff --git a/pubspec.yaml b/pubspec.yaml index 977af983f..6fb3e4d9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.7.0 homepage: environment: - sdk: ">=2.15.1 <3.0.0" + sdk: '>=3.0.0 <4.0.0' flutter: ">=2.5.0" dependencies: @@ -13,37 +13,45 @@ dependencies: logger: ^1.1.0 connectivity_plus: ^3.0.0 - uni_links: ^0.5.1 - fluttertoast: ^8.0.8 - path: ^1.8.0 + app_links: ^3.5.0 + fluttertoast: ^8.2.2 + path: ^1.8.2 gallery_saver: ^2.3.2 - path_provider: ^2.0.4 + path_provider: ^2.0.12 asn1lib: ^1.0.3 pointycastle: ^3.5.0 encrypt: ^5.0.1 intl: ^0.18.0 - http: ^0.13.3 + http: ^0.13.5 timezone: ^0.9.2 flutter_native_timezone: ^2.0.0 - geolocator: ^8.0.0 + geolocator: ^9.0.2 cookie_jar: ^3.0.1 - shared_preferences: ^2.0.7 - package_info: ^2.0.2 + shared_preferences: ^2.2.0 + package_info_plus: ^4.0.2 device_info: ^2.0.3 - url_launcher: ^6.0.10 + url_launcher: ^6.1.8 sprintf: ^7.0.0 flutter_local_notifications: ^16.1.0 sqflite: ^2.1.0 - device_calendar: ^4.0.1 - image_picker: ^0.8.5+3 + device_calendar: ^4.3.1 + image_picker: ^1.0.4 mime_type: ^1.0.0 - uuid: ^3.0.5 - flutter_html: ^3.0.0-alpha.6 - webview_flutter: ^2.0.13 + uuid: ^3.0.7 + flutter_html: ^3.0.0-beta.2 + webview_flutter: ^4.0.5 flutter_exif_rotation: ^0.5.1 + flutter_secure_storage: ^9.0.0 + universal_html: ^2.2.3 + flutter_web_auth_2: ^2.1.4 + pinch_zoom: ^2.0.0 + graphql_flutter: ^5.1.2 + native_flutter_proxy: ^0.1.15 # font_awesome_flutter: ^10.6.0 # published plugin - uncomment if used font_awesome_flutter: '>= 4.7.0' # comment if published plugin is used - pinch_zoom: ^1.0.0 + + flutter_passkey: + git: https://github.com/rokmetro/flutter_passkey.git #Firebase firebase_core: ^2.13.0 @@ -79,9 +87,9 @@ flutter: pluginClass: RokwirePlugin # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/ + # # For details regarding assets in packages, see # https://flutter.dev/assets-and-images/#from-packages diff --git a/tools/encrypt_util.dart b/tools/encrypt_util.dart new file mode 100644 index 000000000..0c58e0c06 --- /dev/null +++ b/tools/encrypt_util.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:encrypt/encrypt.dart' as encrypt_package; + +void main() async { + //String encryptedFilepath = "assets/configs.json.enc"; + String keysFilepath = "assets/config.keys.json"; + //String decryptedFile = "assets/configs.json"; + + Map? encryptionKeys = await loadEncryptionKeysFromAssets(keysFilepath); + String? encryptionKey = encryptionKeys != null ? encryptionKeys['key'] : null; + String? encryptionIV = encryptionKeys != null ? encryptionKeys['iv'] : null; + if (encryptionKey == null || encryptionIV == null) { + return; + } + + // decryptAssetFile(encryptedFilepath, decryptedFile, encryptionKey, encryptionIV); + // encryptAssetFile(decryptedFile, encryptedFilepath, encryptionKey, encryptionIV); + + //String tempFile = "plugin/tools/temp.enc"; + //String contents = File(tempFile).readAsStringSync(); + // String contents = """"""; + // print(decrypt(contents, key: encryptionKey, iv: encryptionIV)); + // print(encrypt(contents, key: encryptionKey, iv: encryptionIV)); + + // print(json.encode(generateKey())); +} + +void encryptAssetFile(String decryptedFile, String encryptedFile, String encryptionKey, String encryptionIV) { + String decrypted = File(decryptedFile).readAsStringSync(); + String? encrypted = encrypt(decrypted, key: encryptionKey, iv: encryptionIV); + if (encrypted != null) { + File(encryptedFile).writeAsString(encrypted); + print("saved encrypted output"); + } +} + +void decryptAssetFile(String encryptedFile, String decryptedFile, String encryptionKey, String encryptionIV) { + String encrypted = File(encryptedFile).readAsStringSync(); + String? decrypted = decrypt(encrypted, key: encryptionKey, iv: encryptionIV); + if (decrypted != null) { + File(decryptedFile).writeAsString(decrypted); + print("saved decrypted output"); + } +} + +Future?> loadEncryptionKeysFromAssets(String filepath) async { + try { + String keysContents = File(filepath).readAsStringSync(); + return json.decode(keysContents); + } catch (e) { + print(e); + } + return null; +} + +String? encrypt(String plainText, {String? key, String? iv, encrypt_package.AESMode mode = encrypt_package.AESMode.cbc, String padding = 'PKCS7' }) { + if (key != null) { + try { + final encrypterKey = encrypt_package.Key.fromBase64(key); + final encrypterIV = (iv != null) ? encrypt_package.IV.fromBase64(iv) : encrypt_package.IV.fromLength(base64Decode(key).length); + final encrypter = encrypt_package.Encrypter(encrypt_package.AES(encrypterKey, mode: mode, padding: padding)); + return encrypter.encrypt(plainText, iv: encrypterIV).base64; + } + catch(e) { + print(e.toString()); + } + } + return null; +} + +String? decrypt(String cipherBase64, { String? key, String? iv, encrypt_package.AESMode mode = encrypt_package.AESMode.cbc, String padding = 'PKCS7' }) { + if (key != null) { + try { + final encrypterKey = encrypt_package.Key.fromBase64(key); + final encrypterIV = (iv != null) ? encrypt_package.IV.fromBase64(iv) : encrypt_package.IV.fromLength(base64Decode(key).length); + final encrypter = encrypt_package.Encrypter(encrypt_package.AES(encrypterKey, mode: mode, padding: padding)); + return encrypter.decrypt(encrypt_package.Encrypted.fromBase64(cipherBase64), iv: encrypterIV); + } + catch(e) { + print(e.toString()); + } + } + return null; +} + +Map generateKey() { + final key = encrypt_package.Key.fromSecureRandom(32); + final iv = encrypt_package.IV.fromSecureRandom(16); + Map keys = { + 'key': key.base64, + 'iv': iv.base64, + }; + return keys; +} \ No newline at end of file diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart new file mode 100644 index 000000000..86abbe6e6 --- /dev/null +++ b/tools/gen_styles.dart @@ -0,0 +1,391 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'package:args/args.dart'; + +const flagUpdateCode = 'update-code'; +const flagSkipPlugin = 'skip-plugin'; + +Map classMap = { + 'color': 'AppColors', + 'text_style': 'AppTextStyles', + 'font_family': 'AppFontFamilies', + 'image': 'AppImages', + 'themes': 'AppThemes', +}; + +Map typesMap = { + 'color': 'Color', + 'text_style': 'TextStyle', + 'font_family': 'String', + 'image': 'UiImage', + 'themes': 'String', +}; + +Map refsMap = { + 'color': 'Styles().colors.getColor(%key)', + 'text_style': 'Styles().textStyles.getTextStyle(%key)', + 'font_family': 'Styles().fontFamilies.fromCode(%key)', + 'image': 'Styles().images.getImage(%key)', + 'themes': '%key', +}; + +Map, {Map? data})> defaultFuncs = { + 'color': _buildDefaultColor, + 'text_style': (name, entry, {data}) => _buildDefaultClass(name, entry, data: data, classFields: textStyleFields), + 'font_family': _buildDefaultString, + 'image': _buildDefaultImage, +}; + +Map textStyleFields = { + 'color': 'color', + 'decoration_color': 'decorationColor:AppColors', + 'size': 'fontSize', + 'height': 'height', + 'font_family': 'fontFamily', + 'font_style': 'fontStyle:FontStyle', + 'letter_spacing': 'letterSpacing', + 'word_spacing': 'wordSpacing', + 'decoration_thickness': 'decorationThickness', + 'decoration': 'decoration:TextDecoration', + 'overflow': 'overflow:TextOverflow', + 'decoration_style': 'decorationStyle:TextDecorationStyle', + 'weight': 'fontWeight:FontWeight', +}; + +String capitalize(String s) => s[0].toUpperCase() + s.substring(1); + +String camelCase(String s, {bool startUpper = false}) { + List parts = s.split(RegExp(r'[_\-.]')); + String out = ''; + for (String part in parts) { + if (out.isEmpty && !startUpper) { + out += part; + } else { + out += capitalize(part); + } + } + return out; +} + +Map replacements = {}; + +void main(List arguments) async { + final parser = ArgParser()..addFlag(flagUpdateCode, negatable: false, abbr: 'u') + ..addFlag(flagSkipPlugin, negatable: false, abbr: 'p'); + ArgResults argResults = parser.parse(arguments); + + bool updateCode = argResults[flagUpdateCode]; + bool skipPlugin = argResults[flagSkipPlugin]; + + String assetFilepath = 'assets/styles.json'; + String pluginAssetFilepath = 'plugin/assets/styles.json'; + String libPath = 'lib/'; + String genFilepath = '${libPath}gen/styles.dart'; + + LinkedHashMap? asset = await _loadFileJson(assetFilepath); + if (asset == null) { + print('asset was not loaded'); + return; + } + if (!skipPlugin) { + print("merging plugin asset..."); + LinkedHashMap? pluginAsset = await _loadFileJson(pluginAssetFilepath); + if (pluginAsset != null) { + asset = _mergeJson(pluginAsset, asset); + File(assetFilepath).writeAsString(_prettyJsonEncode(asset)); + print('saved merged plugin asset to $assetFilepath'); + } else { + print('plugin asset was not loaded'); + return; + } + } + String fileString = _parseAsset(asset); + if (fileString.isNotEmpty) { + File(genFilepath).writeAsString(fileString); + print("saved generated code to $genFilepath"); + if (updateCode) { + _updateCodeRefs(libPath, genFilepath); + } + } +} + +String _prettyJsonEncode(LinkedHashMap jsonObject, {bool deepFormat = false}){ + String out = '{'; + bool first = true; + for (dynamic entry in jsonObject.entries) { + if (!(entry is MapEntry)) { + print('invalid entry type: ${entry.key} - ${entry.runtimeType}'); + continue; + } + if (!first) { + out += ','; + } + out += '\n'; + out += ' "${entry.key}": {'; + if (entry.value is LinkedHashMap || entry.key == 'themes') { + bool firstSub = true; + for (dynamic subentry in entry.value.entries) { + if (!firstSub) { + out += ','; + } + if (subentry.key.startsWith('_blank')) { + out += '\n'; + firstSub = true; + continue; + } + LinkedHashMap subMap = LinkedHashMap(); + subMap.addEntries([subentry]); + String valJson; + if (entry.key == 'themes' || deepFormat) { + valJson = _prettyJsonEncode(subMap, deepFormat: entry.key == 'themes'); + valJson = valJson.substring(1, valJson.length - 2); + valJson = valJson.replaceAll('\n', '\n '); + } else { + valJson = json.encode(subMap).replaceAll(':', ': '); + valJson = valJson.substring(1, valJson.length - 1); + out += '\n'; + } + out += ' ${valJson}'; + firstSub = false; + } + } else { + out += ' ${json.encode(entry.value)}\n'; + } + out += '\n'; + out += ' }'; + first = false; + } + out += '\n'; + out += '}'; + return out; +} + +LinkedHashMap _mergeJson(LinkedHashMap? from, LinkedHashMap? to) { + to ??= LinkedHashMap(); + LinkedHashMap out = LinkedHashMap(); + out.addEntries(to.entries); + for (MapEntry section in from?.entries ?? {}) { + if (!out.containsKey(section.key)) { + out[section.key] = {}; + } + + if (section.value is Map) { + bool addedToSection = false; + String? lastBlank; + for (MapEntry entry in section.value.entries ?? {}) { + dynamic outSection = out[section.key]; + if (!outSection.containsKey(entry.key)) { + if (section.key != 'themes') { + if (entry.key.startsWith('_blank')) { + if (!addedToSection || lastBlank != null) { + continue; + } + lastBlank = entry.key; + } else { + lastBlank = null; + } + if (!addedToSection) { + if (outSection.isNotEmpty){ + out[section.key]['_blank_plugin_start'] = ''; + } + out[section.key]['_MERGED FROM PLUGIN_'] = 'The following styles were merged from the plugin. They can be overridden here as needed.'; + } + } + print("added ${section.key}: ${entry.key} = ${entry.value}"); + out[section.key][entry.key] = entry.value; + addedToSection = true; + } + } + if (lastBlank != null) { + dynamic outSection = out[section.key]; + if (outSection is Map) { + outSection.remove(lastBlank); + } + } + } else { + print("unexpected section type: ${section.value}"); + } + } + + return out; +} + +String _parseAsset(LinkedHashMap asset) { + List classStrings = []; + for (MapEntry entry in asset.entries) { + if (entry.value is Map) { + String? classString = _buildClass(entry.key, entry.value); + if (classString != null) { + classStrings.add(classString); + } + } else { + print("unexpected structure type: ${entry.value}"); + } + } + return _buildFile(classStrings); +} + +String? _buildClass(String name, Map json) { + String? className = classMap[name]; + String? type = typesMap[name]; + String? ref = refsMap[name]; + if (className == null || type == null || ref == null) { + return null; + } + + bool addedToSection = false; + bool lastBlank = false; + String classString = "class $className {\n"; + for (MapEntry entry in json.entries) { + if (entry.key.startsWith('_blank')) { + if (!lastBlank && addedToSection) { + classString += '\n'; + } + lastBlank = true; + continue; + } + + if (entry.key.startsWith('_')) { + classString += " // ${entry.key.replaceAll('_', ' ').trim()}: ${entry.value}\n"; + continue; + } + + addedToSection = true; + lastBlank = false; + + String varName = camelCase(entry.key); + String varRef = ref.replaceAll("%key", "'${entry.key}'"); + String? defaultObj = defaultFuncs[name]?.call(name, entry, data: json); + String defaultObjString = defaultObj != null ? ' ?? $defaultObj' : ''; + classString += " static $type get $varName => $varRef$defaultObjString;\n"; + replacements[varRef] = '$className.$varName'; + } + if (classString.endsWith('\n\n')) { + classString = classString.substring(0, classString.length - 1); + } + classString += "}\n"; + return classString; +} + +String? _buildDefaultClass(String name, MapEntry entry, {Map? classFields, Map? data}) { + String? type = typesMap[name]; + if (type == null) { + return null; + } + + dynamic value = entry.value; + if (value is Map) { + String? extendsKey = value['extends']; + if (extendsKey != null) { + dynamic extendsMap = data?[extendsKey]; + if (extendsMap is Map) { + _mergeMaps(extendsMap, value); + value = extendsMap; + } + } + String params = ''; + for (MapEntry entry in value.entries) { + String enumType = ''; + if (classFields != null) { + String? field = classFields[entry.key]; + if (field != null) { + if (params.isNotEmpty) { + params += ', '; + } + List fields = field.split(':'); + if (fields.length == 2) { + params += fields[0]; + enumType = '${fields[1]}.'; + } else { + params += field; + } + } else { + continue; + } + } else { + params += camelCase(entry.key); + } + String? styleClass = classMap[entry.key]; + if (styleClass != null) { + params += ': $styleClass.${camelCase(entry.value)}'; + } else { + params += ': $enumType${entry.value}'; + } + } + return "$type($params)"; + } + return null; +} + +String? _buildDefaultColor(String name, MapEntry entry, {Map? data}) { + dynamic value = entry.value; + if (value is String ) { + value = value.replaceFirst('#', ''); + if (value.length == 6) { + value = 'FF' + value; + } + return 'const Color(0x$value)'; + } + return null; +} + +String? _buildDefaultString(String name, MapEntry entry, {Map? data}) { + if (entry.value is String) { + return "'${entry.value}'"; + } + return null; +} + +String? _buildDefaultImage(String name, MapEntry entry, {Map? data}) { + return 'UiImage(spec: ImageSpec.fromJson(${json.encode(entry.value)}))'; +} + +String _buildFile(List classStrings) { + String fileString = "// Code generated by plugin/utils/gen_styles.dart DO NOT EDIT.\n\n"; + fileString += "import 'package:rokwire_plugin/service/styles.dart';\n"; + fileString += "import 'package:rokwire_plugin/ui/widgets/ui_image.dart';\n"; + fileString += "import 'package:flutter/material.dart';\n"; + fileString += "\n"; + fileString += classStrings.join("\n"); + return fileString; +} + +Future?> _loadFileJson(String filepath) async { + try { + String content = (await File(filepath).readAsString()).trim(); + List lines = content.split('\n'); + for (final (int index, String line) in lines.indexed) { + if (line.trim().isEmpty) { + lines[index] = '"_blank_${filepath}#$index": "",'; + } + } + content = lines.join('\n'); + return json.decode(content); + } catch (e) { + print(e); + } + return null; +} + +void _updateCodeRefs(String libPath, String genFilepath) async { + print('updating code references...'); + final dir = Directory(libPath); + List allContents = dir.listSync(recursive: true); + for (FileSystemEntity entity in allContents) { + if (entity is File && entity.path != genFilepath) { + print('processing file ${entity.path}'); + String data = entity.readAsStringSync(); + for (MapEntry replacement in replacements.entries) { + data = data.replaceAll(replacement.key, replacement.value); + } + entity.writeAsStringSync(data); + } + } +} + +void _mergeMaps(Map dest, Map? src) { + src?.forEach((String key, dynamic val) { + dest[key] = val; + }); +} \ No newline at end of file