From 990837667e15c30c863db1c9ffac56ca1ebd07f0 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 23 Dec 2022 17:18:00 -0600 Subject: [PATCH 001/177] initial work for template app (not compiling) --- android/build.gradle | 2 +- example/pubspec.lock | 47 ++++---- ...{app_livecycle.dart => app_lifecycle.dart} | 28 ++--- lib/service/app_navigation.dart | 2 +- lib/service/assets.dart | 10 +- lib/service/auth2.dart | 10 +- lib/service/config.dart | 10 +- lib/service/firebase_messaging.dart | 38 +++++- lib/service/flex_ui.dart | 10 +- lib/service/geo_fence.dart | 10 +- lib/service/groups.dart | 10 +- lib/service/inbox.dart | 10 +- lib/service/localization.dart | 10 +- lib/service/location_services.dart | 10 +- lib/service/polls.dart | 4 +- lib/service/styles.dart | 108 ++++++++++++------ lib/ui/panels/web_panel.dart | 6 +- lib/ui/popups/popup_message.dart | 2 +- lib/ui/widget_builders/buttons.dart | 2 +- lib/ui/widget_builders/survey.dart | 8 +- lib/ui/widgets/section_header.dart | 10 +- lib/ui/widgets/survey.dart | 23 ++-- pubspec.yaml | 20 ++-- 23 files changed, 240 insertions(+), 150 deletions(-) rename lib/service/{app_livecycle.dart => app_lifecycle.dart} (71%) diff --git a/android/build.gradle b/android/build.gradle index 03cc91c9b..b9c167f97 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -30,7 +30,7 @@ android { } defaultConfig { - minSdkVersion 16 + minSdkVersion 19 } } diff --git a/example/pubspec.lock b/example/pubspec.lock index a81b08358..c5640ea29 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: _flutterfire_internals url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.11" args: dependency: transitive description: @@ -77,14 +77,14 @@ packages: name: cloud_firestore_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "5.7.7" + version: "5.10.0" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web url: "https://pub.dartlang.org" source: hosted - version: "2.8.10" + version: "3.2.0" collection: dependency: transitive description: @@ -175,7 +175,7 @@ packages: name: device_calendar url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.3.0-3635229635" device_info: dependency: transitive description: @@ -224,56 +224,56 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "2.4.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.5.1" + version: "4.5.2" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.7.3" + version: "2.0.2" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.4.5" + version: "3.0.8" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.1.12" + version: "3.3.9" firebase_messaging: dependency: transitive description: name: firebase_messaging url: "https://pub.dartlang.org" source: hosted - version: "13.1.0" + version: "14.2.0" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "4.2.9" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.10" flutter: dependency: "direct main" description: flutter @@ -313,7 +313,7 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "10.0.0" + version: "12.0.4" flutter_local_notifications_linux: dependency: transitive description: @@ -393,28 +393,28 @@ packages: name: geolocator url: "https://pub.dartlang.org" source: hosted - version: "8.0.3" + version: "9.0.2" geolocator_android: dependency: transitive description: name: geolocator_android url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "4.1.4" geolocator_apple: dependency: transitive description: name: geolocator_apple url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.2.3" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.7" geolocator_web: dependency: transitive description: @@ -422,6 +422,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.4" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" html: dependency: transitive description: @@ -652,7 +659,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pointycastle: dependency: transitive description: @@ -762,7 +769,7 @@ packages: name: sprintf url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "7.0.0" sqflite: dependency: transitive description: @@ -825,7 +832,7 @@ packages: name: timezone url: "https://pub.dartlang.org" source: hosted - version: "0.8.0" + version: "0.9.0" tuple: dependency: transitive description: 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..2ec818985 100644 --- a/lib/service/app_navigation.dart +++ b/lib/service/app_navigation.dart @@ -28,7 +28,7 @@ class AppNavigation extends NavigatorObserver { static const String notifyParamRoute = 'route'; static const String notifyParamPreviousRoute = 'previous_route'; - // Singletone Factory + // Singleton Factory static AppNavigation ? _instance; diff --git a/lib/service/assets.dart b/lib/service/assets.dart index f92d7ae40..c79e4322a 100644 --- a/lib/service/assets.dart +++ b/lib/service/assets.dart @@ -25,7 +25,7 @@ import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/network.dart'; -import 'package:rokwire_plugin/service/app_livecycle.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:path/path.dart'; import 'package:http/http.dart' as http; @@ -62,7 +62,7 @@ class Assets with Service implements NotificationsListener { @override void createService() { - NotificationService().subscribe(this, AppLivecycle.notifyStateChanged); + NotificationService().subscribe(this, AppLifecycle.notifyStateChanged); } @override @@ -101,12 +101,12 @@ class Assets 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.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 9c1df4799..7c750b88e 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -6,7 +6,7 @@ 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'; @@ -81,7 +81,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { void createService() { NotificationService().subscribe(this, [ DeepLink.notifyUri, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, Auth2UserProfile.notifyChanged, Auth2UserPrefs.notifyChanged, ]); @@ -146,13 +146,13 @@ 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); } } @protected - void onAppLivecycleStateChanged(AppLifecycleState? state) { + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/config.dart b/lib/service/config.dart index a0dfe880d..d91997672 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -20,7 +20,7 @@ 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/app_lifecycle.dart'; import 'package:rokwire_plugin/service/connectivity.dart'; import 'package:rokwire_plugin/service/log.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; @@ -80,7 +80,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @override void createService() { NotificationService().subscribe(this, [ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, //TBD: FirebaseMessaging.notifyConfigUpdate ]); } @@ -112,15 +112,15 @@ class Config with Service, NetworkAuthProvider, 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(); diff --git a/lib/service/firebase_messaging.dart b/lib/service/firebase_messaging.dart index 1255d4125..fd7dc24c5 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -15,10 +15,11 @@ */ import 'dart:async'; +import 'dart:io'; 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/inbox.dart'; import 'package:rokwire_plugin/service/firebase_core.dart'; import 'package:rokwire_plugin/service/log.dart'; @@ -95,6 +96,37 @@ class FirebaseMessaging with Service { return { FirebaseCore(), Storage(), }; } + 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 (Platform.isAndroid) { + 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 @protected @@ -122,7 +154,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 +175,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 b67d0e3d2..c1e4cb401 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'; @@ -76,7 +76,7 @@ class FlexUI with Service implements NotificationsListener { Auth2UserPrefs.notifyPrivacyLevelChanged, Auth2.notifyLoginChanged, Auth2.notifyLinkChanged, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, Groups.notifyUserGroupsUpdated, GeoFence.notifyCurrentRegionsUpdated, Config.notifyConfigChanged, @@ -130,13 +130,13 @@ class FlexUI with Service implements NotificationsListener { { updateContent(); } - else if (name == AppLivecycle.notifyStateChanged) { - onAppLivecycleStateChanged(param); + else if (name == AppLifecycle.notifyStateChanged) { + onAppLifecycleStateChanged(param); } } @protected - void onAppLivecycleStateChanged(AppLifecycleState? state) { + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/geo_fence.dart b/lib/service/geo_fence.dart index 89822236b..601c317c0 100644 --- a/lib/service/geo_fence.dart +++ b/lib/service/geo_fence.dart @@ -25,7 +25,7 @@ import 'package:collection/collection.dart'; import 'package:rokwire_plugin/model/geo_fence.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/network.dart'; @@ -77,7 +77,7 @@ class GeoFence with Service implements NotificationsListener { @override void createService() { NotificationService().subscribe(this, [ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, ]); } @@ -119,12 +119,12 @@ class GeoFence 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.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/groups.dart b/lib/service/groups.dart index eca0dd19c..02c787208 100644 --- a/lib/service/groups.dart +++ b/lib/service/groups.dart @@ -26,7 +26,7 @@ import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rokwire_plugin/model/group.dart'; import 'package:rokwire_plugin/model/event.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'; @@ -106,7 +106,7 @@ class Groups with Service implements NotificationsListener { NotificationService().subscribe(this,[ DeepLink.notifyUri, Auth2.notifyLoginChanged, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, FirebaseMessaging.notifyGroupsNotification, Connectivity.notifyStatusChanged ]); @@ -158,8 +158,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(); } @@ -181,7 +181,7 @@ class Groups with Service implements NotificationsListener { } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + void _onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/inbox.dart b/lib/service/inbox.dart index 50443fbee..da9045de1 100644 --- a/lib/service/inbox.dart +++ b/lib/service/inbox.dart @@ -4,7 +4,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 +51,7 @@ class Inbox with Service implements NotificationsListener { FirebaseMessaging.notifyToken, Auth2.notifyLoginChanged, Auth2.notifyPrepareUserDelete, - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, ]); } @@ -90,14 +90,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(); } diff --git a/lib/service/localization.dart b/lib/service/localization.dart index 24f3a169f..c0376093f 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'; @@ -73,7 +73,7 @@ class Localization with Service implements NotificationsListener { @override void createService() { - NotificationService().subscribe(this, AppLivecycle.notifyStateChanged); + NotificationService().subscribe(this, AppLifecycle.notifyStateChanged); } @override @@ -261,12 +261,12 @@ class Localization 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.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/location_services.dart b/lib/service/location_services.dart index b9faf2f5a..0be53f7fc 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, ]); } @@ -175,12 +175,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/polls.dart b/lib/service/polls.dart index 9cd2a437f..7bde48763 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'; @@ -450,7 +450,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/styles.dart b/lib/service/styles.dart index 43af3e77b..efc0a0c16 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -21,7 +21,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/network.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; @@ -79,7 +79,7 @@ class Styles extends Service implements NotificationsListener{ @override void createService() { - NotificationService().subscribe(this, AppLivecycle.notifyStateChanged); + NotificationService().subscribe(this, AppLifecycle.notifyStateChanged); } @override @@ -121,12 +121,12 @@ class Styles extends 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.paused) { _pausedDateTime = DateTime.now(); } @@ -303,65 +303,107 @@ class UiColors { return UiColors(colors); } - Color? get fillColorPrimary => colorMap['fillColorPrimary']; - Color? get fillColorPrimaryTransparent03 => colorMap['fillColorPrimaryTransparent03']; - Color? get fillColorPrimaryTransparent05 => colorMap['fillColorPrimaryTransparent05']; - Color? get fillColorPrimaryTransparent09 => colorMap['fillColorPrimaryTransparent09']; - Color? get fillColorPrimaryTransparent015 => colorMap['fillColorPrimaryTransparent015']; - Color? get textColorPrimary => colorMap['textColorPrimary']; - Color? get fillColorPrimaryVariant => colorMap['fillColorPrimaryVariant']; - Color? get textColorPrimaryVariant => colorMap['textColorPrimaryVariant']; - Color? get fillColorSecondary => colorMap['fillColorSecondary']; - Color? get fillColorSecondaryTransparent05 => colorMap['fillColorSecondaryTransparent05']; - Color? get textColorSecondary => colorMap['textColorSecondary']; - Color? get fillColorSecondaryVariant => colorMap['fillColorSecondaryVariant']; - Color? get textColorSecondaryVariant => colorMap['textColorSecondaryVariant']; - Color? get gradientColorPrimary => colorMap['gradientColorPrimary']; + Color? get fillColorPrimary => colorMap['fillColorPrimary']; + Color? get fillColorPrimaryVariant => colorMap['fillColorPrimaryVariant']; + Color? get fillColorSecondary => colorMap['fillColorSecondary']; + Color? get fillColorSecondaryVariant => colorMap['fillColorSecondaryVariant']; + Color? get gradientColorPrimary => colorMap['gradientColorPrimary']; Color? get surface => colorMap['surface']; - Color? get textSurface => colorMap['textSurface']; - Color? get textSurfaceTransparent15 => colorMap['textSurfaceTransparent15']; Color? get surfaceAccent => colorMap['surfaceAccent']; - Color? get surfaceAccentTransparent15 => colorMap['surfaceAccentTransparent15']; - Color? get textSurfaceAccent => colorMap['textSurfaceAccent']; Color? get background => colorMap['background']; - Color? get textBackground => colorMap['textBackground']; Color? get backgroundVariant => colorMap['backgroundVariant']; - Color? get textBackgroundVariant => colorMap['textBackgroundVariant']; - Color? get headlineText => colorMap['headlineText']; + + Color? get textPrimary => colorMap['textPrimary']; + Color? get textAccent => colorMap['textAccent']; + Color? get textLight => colorMap['textLight']; + Color? get textMedium => colorMap['textMedium']; + Color? get textDark => colorMap['textDark']; + Color? get textDisabled => colorMap['textDisabled']; Color? get accentColor1 => colorMap['accentColor1']; Color? get accentColor2 => colorMap['accentColor2']; Color? get accentColor3 => colorMap['accentColor3']; Color? get accentColor4 => colorMap['accentColor4']; + Color? get dividerLine => colorMap['dividerLine']; + + // DEPRECATED + + @Deprecated("Transparency should be handled directly by widgets") + Color? get fillColorPrimaryTransparent03 => colorMap['fillColorPrimaryTransparent03']; + @Deprecated("Transparency should be handled directly by widgets") + Color? get fillColorPrimaryTransparent05 => colorMap['fillColorPrimaryTransparent05']; + @Deprecated("Transparency should be handled directly by widgets") + Color? get fillColorPrimaryTransparent09 => colorMap['fillColorPrimaryTransparent09']; + @Deprecated("Transparency should be handled directly by widgets") + Color? get fillColorPrimaryTransparent015 => colorMap['fillColorPrimaryTransparent015']; + @Deprecated("Transparency should be handled directly by widgets") + Color? get fillColorSecondaryTransparent05 => colorMap['fillColorSecondaryTransparent05']; + + @Deprecated("Set icon colors in styles asset file") Color? get iconColor => colorMap['iconColor']; - Color? get eventColor => colorMap['eventColor']; - Color? get diningColor => colorMap['diningColor']; - Color? get placeColor => colorMap['placeColor']; - Color? get mtdColor => colorMap['mtdColor']; + @Deprecated("Use 'textDark' instead") + Color? get textSurface => colorMap['textSurface']; + @Deprecated("Use 'textDark' instead") + Color? get textSurfaceTransparent15 => colorMap['textSurfaceTransparent15']; + @Deprecated("Use 'textDark' instead") + Color? get textSurfaceAccent => colorMap['textSurfaceAccent']; + @Deprecated("Use 'textDark' instead") + Color? get surfaceAccentTransparent15 => colorMap['surfaceAccentTransparent15']; + @Deprecated("Use 'textDark' instead") + Color? get textBackground => colorMap['textBackground']; + @Deprecated("Use 'textDark' instead") + Color? get textBackgroundVariant => colorMap['textBackgroundVariant']; + @Deprecated("Use 'textPrimary' instead") + Color? get headlineText => colorMap['headlineText']; + @Deprecated("Use 'textDisabled' instead") + Color? get disabledTextColor => colorMap['disabledTextColor']; + @Deprecated("Use 'textDisabled' instead") + Color? get disabledTextColorTwo => colorMap['disabledTextColorTwo']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get white => colorMap['white']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get whiteTransparent01 => colorMap['whiteTransparent01']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get whiteTransparent06 => colorMap['whiteTransparent06']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get blackTransparent06 => colorMap['blackTransparent06']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get blackTransparent018 => colorMap['blackTransparent018']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get mediumGray => colorMap['mediumGray']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get mediumGray1 => colorMap['mediumGray1']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get mediumGray2 => colorMap['mediumGray2']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get lightGray => colorMap['lightGray']; - Color? get disabledTextColor => colorMap['disabledTextColor']; - Color? get disabledTextColorTwo => colorMap['disabledTextColorTwo']; - Color? get dividerLine => colorMap['dividerLine']; + @Deprecated("Color style names should meaningfully reflect intended usage") Color? get mango => colorMap['mango']; + @Deprecated("Application specific colors should be defined in the application") + Color? get eventColor => colorMap['eventColor']; + @Deprecated("Application specific colors should be defined in the application") + Color? get diningColor => colorMap['diningColor']; + @Deprecated("Application specific colors should be defined in the application") + Color? get placeColor => colorMap['placeColor']; + @Deprecated("Rename to 'transitColo', Application specific colors should be defined in the application") + Color? get mtdColor => colorMap['mtdColor']; + + @Deprecated("Application specific colors should be defined in the application") Color? get saferLocationWaitTimeColorRed => colorMap['saferLocationWaitTimeColorRed']; + @Deprecated("Application specific colors should be defined in the application") Color? get saferLocationWaitTimeColorYellow => colorMap['saferLocationWaitTimeColorYellow']; + @Deprecated("Application specific colors should be defined in the application") Color? get saferLocationWaitTimeColorGreen => colorMap['saferLocationWaitTimeColorGreen']; + @Deprecated("Application specific colors should be defined in the application") Color? get saferLocationWaitTimeColorGrey => colorMap['saferLocationWaitTimeColorGrey']; + /// Color? getColor(String key) => colorMap[key]; diff --git a/lib/ui/panels/web_panel.dart b/lib/ui/panels/web_panel.dart index dd74176bb..f6ae71cf1 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -19,7 +19,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:rokwire_plugin/rokwire_plugin.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'; @@ -150,7 +150,7 @@ class WebPanelState extends State implements NotificationsListener { void initState() { super.initState(); NotificationService().subscribe(this, [ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, DeepLink.notifyUri, ]); if (Platform.isAndroid) { @@ -222,7 +222,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/popup_message.dart b/lib/ui/popups/popup_message.dart index 92f680647..f387bb2eb 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -282,7 +282,7 @@ class ActionsMessage extends StatelessWidget { @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 displayCloseButtonIcon => closeButtonIcon ?? defaultCloseButtonIcon; - static Future show({ + static Future show({ String? title, TextStyle? titleTextStyle, Color? titleTextColor, diff --git a/lib/ui/widget_builders/buttons.dart b/lib/ui/widget_builders/buttons.dart index a7d9ff3b7..959f2c441 100644 --- a/lib/ui/widget_builders/buttons.dart +++ b/lib/ui/widget_builders/buttons.dart @@ -8,7 +8,7 @@ class ButtonBuilder { label: label, borderColor: Styles().colors?.fillColorSecondary, backgroundColor: Styles().colors?.surface, - textStyle: Styles().textStyles?.getTextStyle('widget.detail.regular.fat'), + textStyle: Styles().textStyles?.getTextStyle('widget.detail.regular.bold'), onTap: onTap, ); } diff --git a/lib/ui/widget_builders/survey.dart b/lib/ui/widget_builders/survey.dart index 1751ff539..1deb8454c 100644 --- a/lib/ui/widget_builders/survey.dart +++ b/lib/ui/widget_builders/survey.dart @@ -15,7 +15,7 @@ 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: Styles().textStyles?.getTextStyle('widget.title.large.bold'))); } 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')))); @@ -49,7 +49,7 @@ class SurveyBuilder { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible(child: Text(response.survey.title.toUpperCase(), style: Styles().textStyles?.getTextStyle('widget.title.small.fat'))), + Flexible(child: Text(response.survey.title.toUpperCase(), style: Styles().textStyles?.getTextStyle('widget.title.small.bold'))), Row( mainAxisSize: MainAxisSize.min, children: [ @@ -75,7 +75,7 @@ class SurveyBuilder { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(key.replaceAll('_', ' ').toUpperCase() + ':', - style: Styles().textStyles?.getTextStyle('widget.description.regular.fat')), + style: Styles().textStyles?.getTextStyle('widget.description.regular.bold')), const SizedBox(width: 8.0), Flexible(child: Text(responseData ?? '', style: Styles().textStyles?.getTextStyle('widget.detail.regular'))), ], @@ -118,7 +118,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: Styles().textStyles?.getTextStyle('widget.detail.regular.bold')), const SizedBox(height: 8.0), SurveyBuilder.surveyDataResult(context, dataResult) ?? Container(), ], diff --git a/lib/ui/widgets/section_header.dart b/lib/ui/widgets/section_header.dart index 6d05b4e3a..6cb169497 100644 --- a/lib/ui/widgets/section_header.dart +++ b/lib/ui/widgets/section_header.dart @@ -209,13 +209,13 @@ class SectionSlantHeader extends StatelessWidget { Color? get _slantColor => slantColor ?? Styles().colors?.fillColorPrimary; TextStyle get _titleTextStyle => titleTextStyle ?? TextStyle( - color: titleTextColor ?? Styles().colors?.textColorPrimary, + color: titleTextColor ?? Styles().colors?.textPrimary, fontFamily: titleFontFamilly ?? Styles().fontFamilies?.extraBold, fontSize: titleFontSize ); TextStyle get _subTitleTextStyle => subTitleTextStyle ?? TextStyle( - color: subTitleTextColor ?? Styles().colors?.textColorPrimary, + color: subTitleTextColor ?? Styles().colors?.textPrimary, fontFamily: subTitleFontFamilly ?? Styles().fontFamilies?.regular, fontSize: subTitleFontSize ); @@ -355,13 +355,13 @@ class SectionRibbonHeader extends StatelessWidget { Color? get _backgroundColor => backgroundColor ?? Styles().colors?.fillColorPrimary; TextStyle get _titleTextStyle => titleTextStyle ?? TextStyle( - color: titleTextColor ?? Styles().colors?.white, + color: titleTextColor ?? Styles().colors?.textLight, fontFamily: titleFontFamilly ?? Styles().fontFamilies?.extraBold, fontSize: titleFontSize ); TextStyle get _subTitleTextStyle => subTitleTextStyle ?? TextStyle( - color: subTitleTextColor ?? Styles().colors?.white, + color: subTitleTextColor ?? Styles().colors?.textLight, fontFamily: subTitleFontFamilly ?? Styles().fontFamilies?.regular, fontSize: subTitleFontSize ); @@ -437,7 +437,7 @@ 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 ?? Colors.white), + CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? Styles().colors?.surface ?? Colors.white), value: progress.expectedTotalBytes != null ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null), ); } diff --git a/lib/ui/widgets/survey.dart b/lib/ui/widgets/survey.dart index fa82f7389..bec0e7104 100644 --- a/lib/ui/widgets/survey.dart +++ b/lib/ui/widgets/survey.dart @@ -77,8 +77,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 : Styles().colors?.textDisabled, + borderColor: canContinue ? null : Styles().colors?.textDisabled, enabled: canContinue && !controller.saving, onTap: controller.continueSurvey, progress: controller.saving), @@ -159,7 +159,7 @@ class _SurveyWidgetState extends State { 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: Styles().textStyles?.getTextStyle('widget.message.large.bold'),), ); } @@ -229,7 +229,7 @@ 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 ?? Styles().textStyles?.getTextStyle('widget.error.regular.bold'))), Visibility( visible: !surveyWidget.containsText, child: Flexible( @@ -388,7 +388,7 @@ class _SurveyWidgetState extends State { 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)), + disabledWidget: Container(alignment: Alignment.center, decoration: BoxDecoration(shape: BoxShape.circle, color: Styles().colors?.textDisabled)), ), ))); } @@ -498,7 +498,7 @@ class _SurveyWidgetState extends State { labelText: title, hintText: "MM-dd-yyyy", filled: true, - fillColor: !enabled ? Styles().colors?.disabledTextColor : Colors.white, + fillColor: !enabled ? Styles().colors?.textDisabled : Styles().colors?.surface, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: const BorderSide(color: Colors.white)), @@ -910,7 +910,10 @@ 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))), + title: Transform.translate(offset: const Offset(-15, 0), + child: Text(title, style: Styles().textStyles?.getTextStyle('widget.title.regular') ?? + TextStyle(fontFamily: Styles().fontFamilies?.regular, + fontSize: 16, color: Styles().colors?.textPrimary))), activeColor: Styles().colors?.fillColorSecondary, value: title, groupValue: selectedValue?.title, @@ -946,7 +949,11 @@ class MultiSelectionList extends StatelessWidget { child: InkWell( onTap: onChanged != null ? () => onChanged!(index) : null, child: ListTile( - 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: Styles().textStyles?.getTextStyle('widget.title.regular') ?? + TextStyle(fontFamily: Styles().fontFamilies?.regular, + fontSize: 16, color: Styles().colors?.textPrimary))), leading: Checkbox( checkColor: Colors.white, activeColor: Styles().colors?.fillColorSecondary, diff --git a/pubspec.yaml b/pubspec.yaml index 146f68741..bff263123 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: connectivity: ^3.0.6 uni_links: ^0.5.1 fluttertoast: ^8.0.8 - path: ^1.8.0 + path: ^1.8.2 gallery_saver: ^2.3.2 path_provider: ^2.0.4 asn1lib: ^1.0.3 @@ -23,30 +23,30 @@ dependencies: encrypt: ^5.0.1 intl: ^0.17.0 http: ^0.13.3 - timezone: ^0.8.0 + timezone: ^0.9.0 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 device_info: ^2.0.3 url_launcher: ^6.0.10 - sprintf: ^6.0.0 - flutter_local_notifications: ^10.0.0 + sprintf: ^7.0.0 + flutter_local_notifications: ^12.0.0 sqflite: ^2.1.0 - device_calendar: ^4.0.1 + device_calendar: ^4.2.0 image_picker: ^0.8.5+3 mime_type: ^1.0.0 uuid: ^3.0.5 flutter_html: ^2.2.0 - webview_flutter: ^2.0.13 + webview_flutter: ^2.0.4 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.2.1 #Firebase - firebase_core: ^1.10.0 - firebase_crashlytics: ^2.3.0 - firebase_messaging: ^13.1.0 + firebase_core: ^2.4.0 + firebase_crashlytics: ^3.0.8 + firebase_messaging: ^14.2.0 #End Firebase dev_dependencies: From 2e5b730bbb3e9e74f67858267c027a7f824da603 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 3 Jan 2023 16:56:58 -0600 Subject: [PATCH 002/177] save progress --- lib/service/auth2.dart | 12 +++++----- lib/service/config.dart | 44 ++++++++++++++++++++++++++++++---- lib/service/firebase_core.dart | 9 ++++++- lib/service/styles.dart | 1 - pubspec.yaml | 2 +- 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 7c750b88e..dff386792 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -114,12 +114,12 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { if (!await authenticateAnonymously()) { - throw ServiceError( - source: this, - severity: ServiceErrorSeverity.fatal, - title: 'Authentication Initialization Failed', - description: 'Failed to initialize anonymous authentication token.', - ); + // throw ServiceError( + // source: this, + // severity: ServiceErrorSeverity.fatal, + // title: 'Authentication Initialization Failed', + // description: 'Failed to initialize anonymous authentication token.', + // ); } } diff --git a/lib/service/config.dart b/lib/service/config.dart index d91997672..d0070c32a 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -190,6 +190,10 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected Future loadAsStringFromNet() async { + return loadAsStringFromCore(); + } + + Future loadAsStringFromAppConfig() async { try { http.Response? response = await Network().get(appConfigUrl, auth: this); return ((response != null) && (response.statusCode == 200)) ? response.body : null; @@ -199,12 +203,43 @@ 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); + try { + http.Response? response = await Network().post(appConfigUrl, body: bodyString, headers: {'content-type': 'application/json'}); + return ((response != null) && (response.statusCode == 200)) ? response.body : null; + } catch (e) { + debugPrint(e.toString()); + return null; + } + } + @protected Map? configFromJsonString(String? configJsonString) { + return configFromJsonObjectString(configJsonString); + } + + @protected + Map? configFromJsonObjectString(String? configJsonString) { + Map? configJson = JsonUtils.decode(configJsonString); + if (configJson != null) { + decryptSecretKeys(configJson); + return configJson; + } + return null; + } + + @protected + Map? configFromJsonListString(String? configJsonString) { dynamic configJson = JsonUtils.decode(configJsonString); List? jsonList = (configJson is List) ? configJson : null; if (jsonList != null) { - + jsonList.sort((dynamic cfg1, dynamic cfg2) { return ((cfg1 is Map) && (cfg2 is Map)) ? AppVersion.compareVersions(cfg1['mobileAppVersion'], cfg2['mobileAppVersion']) : 0; }); @@ -223,9 +258,10 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected void decryptSecretKeys(Map? config) { - dynamic secretKeys = (config != null) ? config['secretKeys'] : null; + dynamic secretKeys = (config != null) ? config['data']['secretKeys'] : null; if (secretKeys is String) { - config!['secretKeys'] = JsonUtils.decodeMap(AESCrypt.decrypt(secretKeys, key: encryptionKey, iv: encryptionIV)); + String? secrets = AESCrypt.decrypt(secretKeys, key: encryptionKey, iv: encryptionIV); + config!['secretKeys'] = JsonUtils.decodeMap(secrets); } } @@ -264,7 +300,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { _configAsset = null; _config = (configString != null) ? configFromJsonString(configString) : null; - if (_config != null) { + if (_config != null && secretKeys.isNotEmpty) { configFile.writeAsStringSync(configString!, flush: true); checkUpgrade(); } 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/styles.dart b/lib/service/styles.dart index efc0a0c16..ddf0bad45 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -671,7 +671,6 @@ 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,) diff --git a/pubspec.yaml b/pubspec.yaml index bff263123..548344c8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: sprintf: ^7.0.0 flutter_local_notifications: ^12.0.0 sqflite: ^2.1.0 - device_calendar: ^4.2.0 + device_calendar: 4.2.0 image_picker: ^0.8.5+3 mime_type: ^1.0.0 uuid: ^3.0.5 From 7fe2b264fb7ed1a3a544c8cd3ec1ad6b5e3dabc4 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 6 Jan 2023 17:06:47 -0600 Subject: [PATCH 003/177] finish onboarding (needs some testing) --- example/pubspec.lock | 2 +- lib/service/config.dart | 9 +++++---- lib/service/onboarding.dart | 6 ++++-- lib/service/styles.dart | 3 +++ lib/ui/popups/popup_message.dart | 5 +++-- lib/ui/widgets/rounded_button.dart | 17 ++++++++++------- pubspec.yaml | 2 +- 7 files changed, 27 insertions(+), 17 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index c5640ea29..18af151d6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -832,7 +832,7 @@ packages: name: timezone url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.9.1" tuple: dependency: transitive description: diff --git a/lib/service/config.dart b/lib/service/config.dart index d0070c32a..58626a2f4 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -227,9 +227,10 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected Map? configFromJsonObjectString(String? configJsonString) { Map? configJson = JsonUtils.decode(configJsonString); - if (configJson != null) { - decryptSecretKeys(configJson); - return configJson; + Map? configData = configJson?["data"]; + if (configData != null) { + decryptSecretKeys(configData); + return configData; } return null; } @@ -258,7 +259,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected void decryptSecretKeys(Map? config) { - dynamic secretKeys = (config != null) ? config['data']['secretKeys'] : null; + dynamic secretKeys = (config != null) ? config['secretKeys'] : null; if (secretKeys is String) { String? secrets = AESCrypt.decrypt(secretKeys, key: encryptionKey, iv: encryptionIV); config!['secretKeys'] = JsonUtils.decodeMap(secrets); diff --git a/lib/service/onboarding.dart b/lib/service/onboarding.dart index 52dfb2e3c..47c358b68 100644 --- a/lib/service/onboarding.dart +++ b/lib/service/onboarding.dart @@ -99,10 +99,12 @@ class Onboarding with Service implements NotificationsListener { nextPanel(panel).then((dynamic nextPanel) { if (nextPanel is Widget) { if (replace) { - Navigator.pushReplacement(context, CupertinoPageRoute(builder: (context) => nextPanel)); + Navigator.pushReplacement( + context, CupertinoPageRoute(builder: (context) => nextPanel)); } else { - Navigator.push(context, CupertinoPageRoute(builder: (context) => nextPanel)); + Navigator.push( + context, CupertinoPageRoute(builder: (context) => nextPanel)); } } else if ((nextPanel is bool) && !nextPanel) { diff --git a/lib/service/styles.dart b/lib/service/styles.dart index ddf0bad45..2e750d68a 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -328,6 +328,9 @@ class UiColors { Color? get dividerLine => colorMap['dividerLine']; + Color? get success => colorMap['success']; + Color? get alert => colorMap['alert']; + // DEPRECATED @Deprecated("Transparency should be handled directly by widgets") diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index f387bb2eb..06eb053ea 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -255,7 +255,7 @@ class ActionsMessage extends StatelessWidget { @protected Color? get defautTitleBarColor => Styles().colors?.fillColorPrimary; @protected Color? get displayTitleBarColor => titleBarColor ?? defautTitleBarColor; - @protected Color? get defautTitleTextColor => Styles().colors?.white; + @protected Color? get defautTitleTextColor => Styles().colors?.textLight; @protected Color? get displayTitleTextColor => titleTextColor ?? defautTitleTextColor; @protected String? get defaultTitleFontFamily => Styles().fontFamilies?.bold; @@ -368,7 +368,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/widgets/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index f21279375..24addcfef 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -102,7 +102,7 @@ class RoundedButton extends StatefulWidget { this.progressStrokeWidth, }) : super(key: key); - @protected Color? get defaultBackgroundColor => Styles().colors?.white; + @protected Color? get defaultBackgroundColor => Styles().colors?.surface; @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; @protected Color? get defautTextColor => Styles().colors?.fillColorPrimary; @@ -163,9 +163,8 @@ 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 Semantics(label: widget.label, hint: widget.hint, button: true, + enabled: widget.enabled, child: _wrapperContent,); } Widget get _wrapperContent { @@ -207,9 +206,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/pubspec.yaml b/pubspec.yaml index 548344c8f..bff263123 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: sprintf: ^7.0.0 flutter_local_notifications: ^12.0.0 sqflite: ^2.1.0 - device_calendar: 4.2.0 + device_calendar: ^4.2.0 image_picker: ^0.8.5+3 mime_type: ^1.0.0 uuid: ^3.0.5 From 2ffaf9374a124a0fb2acafa7a9726afc23f01220 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 10 Jan 2023 10:09:28 -0600 Subject: [PATCH 004/177] save progress on settings --- lib/service/firebase_messaging.dart | 5 ++ lib/ui/widgets/expandable_section.dart | 90 ++++++++++++++++++++++++++ lib/ui/widgets/ribbon_button.dart | 36 ++++++----- lib/ui/widgets/rounded_button.dart | 8 ++- 4 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 lib/ui/widgets/expandable_section.dart diff --git a/lib/service/firebase_messaging.dart b/lib/service/firebase_messaging.dart index fd7dc24c5..a579de1ad 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -96,6 +96,11 @@ class FirebaseMessaging with Service { return { FirebaseCore(), Storage(), }; } + 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; diff --git a/lib/ui/widgets/expandable_section.dart b/lib/ui/widgets/expandable_section.dart new file mode 100644 index 000000000..d72945b72 --- /dev/null +++ b/lib/ui/widgets/expandable_section.dart @@ -0,0 +1,90 @@ +/* + * 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/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 => Styles().colors?.textPrimary; + @protected String? get defaultTitleFontFamily => Styles().fontFamilies?.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 => Styles().colors?.textDark; + @protected String? get defaultSubtitleFontFamily => Styles().fontFamilies?.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: Styles().colors?.fillColorSecondary + ) + ), + children: [widget.contents ?? Container(),], + onExpansionChanged: (bool expanded) { + setState(() => _expanded = expanded); + }, + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index d6cc18c14..39a9f9bc2 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -133,23 +133,25 @@ class _RibbonButtonState extends State { Widget get _contentWidget { Widget? leftIconWidget = !widget.progressHidesLeftIcon ? (widget.leftIcon ?? widget.leftIconImage) : null; Widget? rightIconWidget = !widget.progressHidesRightIcon ? (widget.rightIcon ?? widget.rightIconImage) : null; - 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.padding, child: - Row(children: [ - (leftIconWidget != null) ? Padding(padding: widget.leftIconPadding, child: leftIconWidget) : Container(), - Expanded(child: - widget.displayTextWidget - ), - (rightIconWidget != null) ? Padding(padding: widget.rightIconPadding, child: rightIconWidget) : Container(), - ],), - ), - ) - ), - ],), + return Material(color: Colors.transparent, + child: Semantics(label: widget.label, hint: widget.hint, value : widget.semanticsValue, button: true, excludeSemantics: true, child: + InkWell(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.padding, child: + Row(children: [ + (leftIconWidget != null) ? Padding(padding: widget.leftIconPadding, child: leftIconWidget) : Container(), + Expanded(child: + widget.displayTextWidget + ), + (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 24addcfef..bd2a4e203 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -90,7 +90,7 @@ class RoundedButton extends StatefulWidget { this.borderColor, //= Styles().colors.fillColorSecondary this.borderWidth = 2.0, this.borderShadow, - this.maxBorderRadius = 24.0, + this.maxBorderRadius = 36.0, this.secondaryBorder, this.secondaryBorderColor, @@ -163,8 +163,10 @@ 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: _wrapperContent,); + return Material(color: Colors.transparent, + child: Semantics(label: widget.label, hint: widget.hint, button: true, + enabled: widget.enabled, child: _wrapperContent,), + ); } Widget get _wrapperContent { From e61fa3bb374249eec34ade29dffac6ce2237344a Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 10 Jan 2023 13:11:35 -0600 Subject: [PATCH 005/177] setup initial panels --- example/pubspec.lock | 2 +- lib/ui/widgets/tab_bar.dart | 25 +++++++++++-------------- pubspec.yaml | 2 +- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 18af151d6..5641cc667 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -379,7 +379,7 @@ packages: name: font_awesome_flutter url: "https://pub.dartlang.org" source: hosted - version: "10.2.1" + version: "10.3.0" gallery_saver: dependency: transitive description: diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index 923130fc1..92c984625 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -27,7 +27,7 @@ class TabBar extends StatefulWidget { } @protected - BoxBorder? get border => Border(top: BorderSide(color: Styles().colors!.surfaceAccent!, width: 1, style: BorderStyle.solid)); + BoxBorder? get border => null; @protected Decoration? get decoration => BoxDecoration(color: backgroundColor, border: border); @@ -71,13 +71,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(), ), ), - ],); + ]); } @protected @@ -146,11 +144,12 @@ class TabWidget extends StatelessWidget { @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), child: + Column(children: [ + selected ? buildSelectedIndicator(context) : Container(), + buildTab(context), + ]), ), ); } @@ -215,9 +214,7 @@ class TabWidget extends StatelessWidget { @protected Widget buildSelectedIndicator(BuildContext context) => Positioned.fill(child: - Column(mainAxisAlignment: MainAxisAlignment.start, children: [ - Container(height: selectedIndicatorHeight, color: selectedIndicatorColor) - ],), + Container(height: selectedIndicatorHeight, color: selectedIndicatorColor), ); @protected diff --git a/pubspec.yaml b/pubspec.yaml index bff263123..97cdfb3a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: flutter_html: ^2.2.0 webview_flutter: ^2.0.4 flutter_exif_rotation: ^0.5.1 - font_awesome_flutter: ^10.2.1 + font_awesome_flutter: ^10.3.0 #Firebase firebase_core: ^2.4.0 From 509be3a702f4b779f499f884a04bb392ff006824 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 16 Jan 2023 16:41:36 -0600 Subject: [PATCH 006/177] fix style issues with dark theme --- lib/service/styles.dart | 6 +++++ lib/ui/popups/popup_message.dart | 2 +- lib/ui/widget_builders/scroll_pager.dart | 2 +- lib/ui/widgets/tab_bar.dart | 29 ++++++++++++------------ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 2e750d68a..fb6c4edeb 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -321,6 +321,12 @@ class UiColors { Color? get textDark => colorMap['textDark']; Color? get textDisabled => colorMap['textDisabled']; + Color? get iconPrimary => colorMap['iconPrimary']; + Color? get iconLight => colorMap['iconLight']; + Color? get iconMedium => colorMap['iconMedium']; + Color? get iconDark => colorMap['iconDark']; + Color? get iconDisabled => colorMap['iconDisabled']; + Color? get accentColor1 => colorMap['accentColor1']; Color? get accentColor2 => colorMap['accentColor2']; Color? get accentColor3 => colorMap['accentColor3']; diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index 06eb053ea..eb56d1053 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -279,7 +279,7 @@ class ActionsMessage extends StatelessWidget { @protected ShapeBorder get defautBorder => RoundedRectangleBorder(borderRadius: displayBorderRadius,); @protected ShapeBorder get displayBorder => border ?? defautBorder; - @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: Styles().colors?.surface)); @protected Widget? get displayCloseButtonIcon => closeButtonIcon ?? defaultCloseButtonIcon; static Future show({ diff --git a/lib/ui/widget_builders/scroll_pager.dart b/lib/ui/widget_builders/scroll_pager.dart index eb97fb4dc..47dd7ffbc 100644 --- a/lib/ui/widget_builders/scroll_pager.dart +++ b/lib/ui/widget_builders/scroll_pager.dart @@ -30,7 +30,7 @@ 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(), + Styles().images?.getImage('retry-gray', defaultSpec: FontAwesomeImageSpec(type: 'fa.icon', source: '0xf2f9', weight: 'solid', size: 18.0, color: Styles().colors?.iconMedium)) ?? Container(), const SizedBox(width: 8.0), Text(Localization().getStringEx('widget.scroll_pager.error.title', 'Something went wrong'), style: Styles().textStyles?.getTextStyle('widget.message.light.regular')), diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index 92c984625..ce361f8cb 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -17,18 +17,20 @@ class TabBar extends StatefulWidget { _TabBarState createState() => _TabBarState(); @protected - Color? get backgroundColor { + Color? get backgroundColor => Styles().colors?.surface ?? Colors.white; + + @protected + BoxBorder? get border => null; + + @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 ?? Colors.white; - default: return Colors.white; + default: return null; } } - @protected - BoxBorder? get border => null; - @protected Decoration? get decoration => BoxDecoration(color: backgroundColor, border: border); @@ -71,10 +73,10 @@ class _TabBarState extends State implements NotificationsListener { Widget build(BuildContext context) { return Column(mainAxisSize: MainAxisSize.min, children: [ Container(decoration: widget.decoration, child: - Row(children: - buildTabs(), - ), + Row(children: buildTabs()), ), + Visibility(visible: widget.environmentColor != null, + child: Container(height: 4, color: widget.environmentColor)) ]); } @@ -85,9 +87,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)); } } @@ -185,7 +185,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: Styles().fontFamilies?.bold, color: selected ? Styles().colors?.fillColorSecondary : Styles().colors?.textMedium, fontSize: 12); @protected double getTextScaleFactor(BuildContext context) => min(MediaQuery.of(context).textScaleFactor, 2); @@ -204,7 +204,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 ? Styles().colors?.fillColorSecondary : Styles().colors?.textMedium) ?? defaultIcon : defaultIcon; } @protected From 570088e929de676bfcb48419a47e8463a87fdddd Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 23 Jan 2023 11:33:51 -0600 Subject: [PATCH 007/177] fix runtime ui errors --- lib/ui/popups/popup_message.dart | 2 +- lib/ui/widgets/tab_bar.dart | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index eb56d1053..334053988 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -345,7 +345,7 @@ 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: diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index ce361f8cb..88efecd8d 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -214,9 +214,7 @@ class TabWidget extends StatelessWidget { // Selected Indicator @protected - Widget buildSelectedIndicator(BuildContext context) => Positioned.fill(child: - Container(height: selectedIndicatorHeight, color: selectedIndicatorColor), - ); + Widget buildSelectedIndicator(BuildContext context) => Container(height: selectedIndicatorHeight, color: selectedIndicatorColor); @protected double get selectedIndicatorHeight => 4; From ffb7622124a35bffdb17f2a9908b5de296c5fc87 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 22 Feb 2023 09:33:43 -0600 Subject: [PATCH 008/177] fix some styling issues --- example/pubspec.lock | 467 ++++++++++++++++++++++++++++--------------- 1 file changed, 307 insertions(+), 160 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 7cb44553c..87df804a3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,273 +5,312 @@ packages: dependency: transitive description: name: _flutterfire_internals - url: "https://pub.dartlang.org" + sha256: d58dc7eda77a05866ad4fac26a4953e82bac29167aa571224d499da5d2027bc6 + url: "https://pub.dev" source: hosted version: "1.0.11" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + url: "https://pub.dev" source: hosted version: "2.3.0" asn1lib: dependency: transitive description: name: asn1lib - url: "https://pub.dartlang.org" + sha256: "35e6681078cf198c5d5e83fdc5fcf4ad39a5f29564c181b370b5c29361f28660" + url: "https://pub.dev" source: hosted version: "1.0.3" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" chewie: dependency: transitive description: name: chewie - url: "https://pub.dartlang.org" + sha256: "7343cffa40ce9d42b37f2869f17b5e5fa681fb329d8385869ce279e03193e2b7" + url: "https://pub.dev" source: hosted version: "1.1.0" chewie_audio: dependency: transitive description: name: chewie_audio - url: "https://pub.dartlang.org" + sha256: f92bb4364ced21318e1a7c0eddaf249a7554e5cf27f869d58c44d926abd292a7 + url: "https://pub.dev" source: hosted version: "1.3.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - url: "https://pub.dartlang.org" + sha256: "86a21d4fd3afe89268e72f62691547e440eff3bcb2781d77c428df706e958149" + url: "https://pub.dev" source: hosted version: "5.10.0" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - url: "https://pub.dartlang.org" + sha256: "2d7339bf3f79adcdb123a88e908474478b060317332189bdd5dcb2d498952127" + url: "https://pub.dev" source: hosted version: "3.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" connectivity: dependency: transitive description: name: connectivity - url: "https://pub.dartlang.org" + sha256: a8e91263cf3e25fb5cc95e19dfde4999e32a648ac3b9e8a558a28165731678f8 + url: "https://pub.dev" source: hosted version: "3.0.6" connectivity_for_web: dependency: transitive description: name: connectivity_for_web - url: "https://pub.dartlang.org" + sha256: "01a390c1d5adc2ed1fa1f52d120c07fe9fd01166a93f965a832fd6cfc0ea6482" + url: "https://pub.dev" source: hosted version: "0.4.0+1" connectivity_macos: dependency: transitive description: name: connectivity_macos - url: "https://pub.dartlang.org" + sha256: "51ae08d5162eca9669b9d8951ed83ce19c5355a81149f94e4dee2740beb93628" + url: "https://pub.dev" source: hosted version: "0.2.1+2" connectivity_platform_interface: dependency: transitive description: name: connectivity_platform_interface - url: "https://pub.dartlang.org" + sha256: "2d82e942df9d49f29a24bb07fb5ce085d4a53e47818c62364d2b6deb9e0d7a8e" + url: "https://pub.dev" source: hosted version: "2.0.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + url: "https://pub.dev" source: hosted version: "3.0.1" cookie_jar: dependency: transitive description: name: cookie_jar - url: "https://pub.dartlang.org" + sha256: d1cc6516a190ba667941f722b6365d202caff3dacb38de24268b8d6ff1ec8a1d + url: "https://pub.dev" source: hosted version: "3.0.1" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: "552ffd2f851d4314958e6265452af1891959e00cd32b6d17452c5b836e27a0fa" + url: "https://pub.dev" source: hosted version: "0.3.2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + url: "https://pub.dev" source: hosted version: "3.0.1" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: d1cd6d6e4b39a4ad295204722b8608f19981677b223f3e942c0b5a33dcf57ec0 + url: "https://pub.dev" source: hosted version: "0.17.1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: "1989d917fbe8e6b39806207df5a3fdd3d816cbd090fac2ce26fb45e9a71476e5" + url: "https://pub.dev" source: hosted version: "1.0.4" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "4f814fc7e73057f78f307a6c4714fe2ffb4bdb994ab1970540a068ec4d5a45be" + url: "https://pub.dev" source: hosted version: "0.7.3" device_calendar: dependency: transitive description: name: device_calendar - url: "https://pub.dartlang.org" + sha256: "3b22c89e1edb6ad296452df5a7ed3ce45e9f4abff74e838d0b73357cb39a4326" + url: "https://pub.dev" source: hosted version: "4.3.0-3635229635" device_info: dependency: transitive description: name: device_info - url: "https://pub.dartlang.org" + sha256: f4a8156cb7b7480d969cb734907d18b333c8f0bc0b1ad0b342cdcecf30d62c48 + url: "https://pub.dev" source: hosted version: "2.0.3" device_info_platform_interface: dependency: transitive description: name: device_info_platform_interface - url: "https://pub.dartlang.org" + sha256: b148e0bf9640145d09a4f8dea96614076f889e7f7f8b5ecab1c7e5c2dbc73c1b + url: "https://pub.dev" source: hosted version: "2.0.1" encrypt: dependency: transitive description: name: encrypt - url: "https://pub.dartlang.org" + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + url: "https://pub.dev" source: hosted version: "5.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: "35d0f481d939de0d640b3db9a7aa36a52cd22054a798a73b4f50bdad5ce12678" + url: "https://pub.dev" source: hosted version: "1.1.2" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + url: "https://pub.dev" source: hosted version: "6.1.2" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + sha256: "01962872df08437d9be593caeab8c700624c93b629f3d3d60f061612d6263666" + url: "https://pub.dev" source: hosted version: "2.4.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + sha256: "5fab93f5b354648efa62e7cc829c90efb68c8796eecf87e0888cae2d5f3accd4" + url: "https://pub.dev" source: hosted version: "4.5.2" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + sha256: b2917618cbe75196261621d676a992e05215ce6a70daca5f655794d5a008d369 + url: "https://pub.dev" source: hosted version: "2.0.2" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - url: "https://pub.dartlang.org" + sha256: "53832e866345fdb0216c549c459313ad1ba554050fe7e170de0ff30d98365656" + url: "https://pub.dev" source: hosted version: "3.0.8" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - url: "https://pub.dartlang.org" + sha256: "13972389743a9b96cfa7909508c4dfc5d6770870264fdc8bb69d0ac5821d8574" + url: "https://pub.dev" source: hosted version: "3.3.9" firebase_messaging: dependency: transitive description: name: firebase_messaging - url: "https://pub.dartlang.org" + sha256: "1ee95810148e5298c0fb6cb00ee4f099218f3d56d358e8047a9bdf4e68b504cb" + url: "https://pub.dev" source: hosted version: "14.2.0" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - url: "https://pub.dartlang.org" + sha256: "4fad3604e98815c5c8df88273f714cab27078a1df5cd848a8ec984b609b86ddd" + url: "https://pub.dev" source: hosted version: "4.2.9" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - url: "https://pub.dartlang.org" + sha256: cbd454c9551e6671747b526cbe643b814df020d630e7502058b9026902003a46 + url: "https://pub.dev" source: hosted version: "3.2.10" flutter: @@ -283,77 +322,88 @@ packages: dependency: transitive description: name: flutter_exif_rotation - url: "https://pub.dartlang.org" + sha256: "642841fa8dbedb0ac7b7f005dd24a606ce237ab2de0fedef7b3419d58b97f2a7" + url: "https://pub.dev" source: hosted version: "0.5.1" flutter_html: dependency: transitive description: name: flutter_html - url: "https://pub.dartlang.org" + sha256: ccb810fcabfce3a7ffaca46e458323915ac7e7fc59082c7357ff848972c02230 + url: "https://pub.dev" source: hosted version: "2.2.1" flutter_layout_grid: dependency: transitive description: name: flutter_layout_grid - url: "https://pub.dartlang.org" + sha256: ab8848620feddfea46f821132eaa75f62301350888af03da00ba4b09cafa5244 + url: "https://pub.dev" source: hosted version: "1.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" source: hosted version: "1.0.4" flutter_local_notifications: dependency: transitive description: name: flutter_local_notifications - url: "https://pub.dartlang.org" + sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 + url: "https://pub.dev" source: hosted version: "12.0.4" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - url: "https://pub.dartlang.org" + sha256: "6af440e3962eeab8459602c309d7d4ab9e62f05d5cfe58195a28f846a0b5d523" + url: "https://pub.dev" source: hosted version: "1.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - url: "https://pub.dartlang.org" + sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + url: "https://pub.dev" source: hosted version: "6.0.0" flutter_math_fork: dependency: transitive description: name: flutter_math_fork - url: "https://pub.dartlang.org" + sha256: a34205227e1a1d040a56e74a5e2e56e9397ea930540a9373bd0b3e2e4f122017 + url: "https://pub.dev" source: hosted version: "0.5.0" flutter_native_timezone: dependency: transitive description: name: flutter_native_timezone - url: "https://pub.dartlang.org" + sha256: ed7bfb982f036243de1c068e269182a877100c994f05143c8b26a325e28c1b02 + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "5c574d21b98ec92adab05ead10afd2b13ff5856c7ca79696edb338a9dd8ed387" + url: "https://pub.dev" source: hosted version: "2.0.5" flutter_svg: dependency: transitive description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: cbf529d563dd910a249041bde2a0dfc3cf62280fcc459b85f3d614b2ab73abb3 + url: "https://pub.dev" source: hosted version: "0.23.0+1" flutter_test: @@ -370,322 +420,368 @@ packages: dependency: transitive description: name: fluttertoast - url: "https://pub.dartlang.org" + sha256: b3ae793108ad2a7e8f2fea91ca5bb2ba7ef03621c4d9ed7a92fe3391da64c000 + url: "https://pub.dev" source: hosted version: "8.0.8" font_awesome_flutter: dependency: transitive description: name: font_awesome_flutter - url: "https://pub.dartlang.org" + sha256: "875dbb9ec1ad30d68102019ceb682760d06c72747c1c5b7885781b95f88569cc" + url: "https://pub.dev" source: hosted version: "10.3.0" gallery_saver: dependency: transitive description: name: gallery_saver - url: "https://pub.dartlang.org" + sha256: df8b7e207ca12d64c71e0710a7ee3bc48aa7206d51cc720716fedb1543a66712 + url: "https://pub.dev" source: hosted version: "2.3.2" geolocator: dependency: transitive description: name: geolocator - url: "https://pub.dartlang.org" + sha256: "5c23f3613f50586c0bbb2b8f970240ae66b3bd992088cf60dd5ee2e6f7dde3a8" + url: "https://pub.dev" source: hosted version: "9.0.2" geolocator_android: dependency: transitive description: name: geolocator_android - url: "https://pub.dartlang.org" + sha256: fe90565c3a8789dc3b433d8f95cdb18343f9bde298a419d978337ba1ae1037d3 + url: "https://pub.dev" source: hosted version: "4.1.4" geolocator_apple: dependency: transitive description: name: geolocator_apple - url: "https://pub.dartlang.org" + sha256: "567cf6d9879b4c33b2bd8bbfff0995e75c62a14f87b1480c8522c5dca5d3b166" + url: "https://pub.dev" source: hosted version: "2.2.3" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - url: "https://pub.dartlang.org" + sha256: af4d69231452f9620718588f41acc4cb58312368716bfff2e92e770b46ce6386 + url: "https://pub.dev" source: hosted version: "4.0.7" geolocator_web: dependency: transitive description: name: geolocator_web - url: "https://pub.dartlang.org" + sha256: c1e068aaa483ec4464376c81038d8c811a732da8c0b89e8014c69e40715a961d + url: "https://pub.dev" source: hosted version: "2.1.4" geolocator_windows: dependency: transitive description: name: geolocator_windows - url: "https://pub.dartlang.org" + sha256: f5911c88e23f48b598dd506c7c19eff0e001645bdc03bb6fecb9f4549208354d + url: "https://pub.dev" source: hosted version: "0.1.1" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: bfef906cbd4e78ef49ae511d9074aebd1d2251482ef601a280973e8b58b51bbf + url: "https://pub.dev" source: hosted version: "0.15.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "2ed163531e071c2c6b7c659635112f24cb64ecbebf6af46b550d536c0b1aa112" + url: "https://pub.dev" source: hosted version: "0.13.4" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + url: "https://pub.dev" source: hosted version: "4.0.0" image_picker: dependency: transitive description: name: image_picker - url: "https://pub.dartlang.org" + sha256: f3712cd190227fb92e0960cb0ce22928ba042c7183b16864ade83b259adf8ea6 + url: "https://pub.dev" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + sha256: "2d4c2634731ced2debc2a36273351dd1cb68080d505c12c472b0dc0657cd853c" + url: "https://pub.dev" source: hosted version: "0.8.5+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + sha256: "1f21a49e1dc82908ac44c2b988ee96b7383d47dc528151c75bac9c67a3bfb9f5" + url: "https://pub.dev" source: hosted version: "2.1.5" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + sha256: aee8b90c2fb018e18cbb437da661d0531a5346d49cab2d5c4a65ebaf026bbd33 + url: "https://pub.dev" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + sha256: dd0d3344bd1ef891e1ceedbe4d4374a246570e065b0fd272ba7125938a29ee54 + url: "https://pub.dev" source: hosted version: "2.4.3" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" source: hosted version: "1.0.1" logger: dependency: transitive description: name: logger - url: "https://pub.dartlang.org" + sha256: "5076f09225f91dc49289a4ccb92df2eeea9ea01cf7c26d49b3a1f04c6a49eec1" + url: "https://pub.dev" source: hosted version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mime_type: dependency: transitive description: name: mime_type - url: "https://pub.dartlang.org" + sha256: "2ad6e67d3d2de9ac0f8ef5352d998fd103cb21351ae8c02fb0c78b079b37d275" + url: "https://pub.dev" source: hosted version: "1.0.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" numerus: dependency: transitive description: name: numerus - url: "https://pub.dartlang.org" + sha256: "0087ef729d63b96cb347a9c44b9c592f21cecb3605b415bbd18710aef80ce5cb" + url: "https://pub.dev" source: hosted version: "1.1.1" package_info: dependency: transitive description: name: package_info - url: "https://pub.dartlang.org" + sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" + url: "https://pub.dev" source: hosted version: "2.0.2" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + sha256: "3bdd251dae9ffaef944450b73f168610db7e968e7b20daf0c3907f8b4aafc8a2" + url: "https://pub.dev" source: hosted version: "0.5.1+1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: ee5c47c1058ad66b4a41746ec3996af9593d0858872807bcd64ac118f0700337 + url: "https://pub.dev" source: hosted version: "0.2.1" path_provider: dependency: transitive description: name: path_provider - url: "https://pub.dartlang.org" + sha256: db1cf6c3f906f50d52c18400be575f75ed620f8717f9b6d11263edd95080dfc6 + url: "https://pub.dev" source: hosted version: "2.0.8" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: c69109bae02c6116bd8ac81319b13eb73dfae02ef74690d2a1a98c1ddd3aaefc + url: "https://pub.dev" source: hosted version: "2.0.11" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + sha256: "038d0141ff5d08c60ed071eee2758b68c50c42a1c10066a1fb6c28ab32fac84c" + url: "https://pub.dev" source: hosted version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: "1e109f4df28bd95eab71e323008b53d19c4d633bc1ab05b577518773474e9621" + url: "https://pub.dev" source: hosted version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + sha256: "0adeb313e1f2c3fc52baeeee59b0fe9c2d1f7da56fd96a9234e1702ec653a453" + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: "3dc0d51b07f85fec3746d9f4e8d31c73bb173cafa2e763f03f8df2e8d1878882" + url: "https://pub.dev" source: hosted version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: "366ad4e3541ea707f859e7148d4d5aba67d589d7936cee04a05c464a277eeb27" + url: "https://pub.dev" source: hosted version: "2.0.5" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "1a914995d4ef10c94ff183528c120d35ed43b5eaa8713fc6766a9be4570782e2" + url: "https://pub.dev" source: hosted version: "4.4.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: "21cc8c830d228dd84c84a9139394702d766622bacd3ea7a2bdcb1f00b280e9a4" + url: "https://pub.dev" source: hosted version: "3.5.0" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" provider: dependency: transitive description: name: provider - url: "https://pub.dartlang.org" + sha256: "7896193cf752c40ba7f7732a95264319a787871e5d628225357f5c909182bc06" + url: "https://pub.dev" source: hosted version: "6.0.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: "616b691d1c8f5c53b7b39ce3542f6a25308d7900bf689d0210e72a644a10387e" + url: "https://pub.dev" source: hosted version: "3.0.1+1" rokwire_plugin: @@ -699,56 +795,64 @@ packages: dependency: transitive description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: eb7cfb572af1ccc8b81f66bfd525ae0e8d196574874105b5c27db64bef9f155a + url: "https://pub.dev" source: hosted version: "2.0.12" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + sha256: eb5f8ca2adb3303e0949971b8bc6408c67b5000502b838ee6cb0ef96cd29e708 + url: "https://pub.dev" source: hosted version: "2.0.10" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + sha256: "2b7319dbccef93d2bc70d03f810a9edb95550f4dcb3fb27e64cae18291b80b28" + url: "https://pub.dev" source: hosted version: "2.0.9" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + sha256: "65e86adba03ba1ecd4a2f3e5d986eac27dbcf5804e0d5e03e029dbf63b5ed3a7" + url: "https://pub.dev" source: hosted version: "2.0.4" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + sha256: d8c36fedba519cba905d30833b818cde3d52d4e9c7438da4664bd523468bb93f + url: "https://pub.dev" source: hosted version: "2.0.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: "992f0fdc46d0a3c0ac2e5859f2de0e577bbe51f78a77ee8f357cbe626a2ad32d" + url: "https://pub.dev" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: "09da0185028a227d51721cade7a3cbd5cc5f163a19593266f2acba87f729bf9c" + url: "https://pub.dev" source: hosted version: "2.0.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: a16f85cbad4e23daf83cd188fc53ca19a9b025743e8e2983d9d89ec4c383d6e6 + url: "https://pub.dev" source: hosted version: "2.0.4" sky_engine: @@ -760,303 +864,346 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sprintf: dependency: transitive description: name: sprintf - url: "https://pub.dartlang.org" + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" source: hosted version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "0128af224bade0990f4ce55f2ef93a7054aa14d28768f17d4d2b21f6b1185523" + url: "https://pub.dev" source: hosted version: "2.1.0" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: "0c7785befac2b5c40fc66b485be2f35396246a6fd6ccb89bb22614ddfb3d54a7" + url: "https://pub.dev" source: hosted version: "2.3.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "271977ff1e9e82ceefb4f08424b8839f577c1852e0726b5ce855311b46d3ef83" + url: "https://pub.dev" source: hosted version: "3.0.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964" + url: "https://pub.dev" source: hosted version: "0.9.1" tuple: dependency: transitive description: name: tuple - url: "https://pub.dartlang.org" + sha256: fe3ae4f0dca3f9aac0888e2e0d117b642ce283a82d7017b54136290c0a3b0dd3 + url: "https://pub.dev" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" uni_links: dependency: transitive description: name: uni_links - url: "https://pub.dartlang.org" + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" source: hosted version: "0.5.1" uni_links_platform_interface: dependency: transitive description: name: uni_links_platform_interface - url: "https://pub.dartlang.org" + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" source: hosted version: "1.0.0" uni_links_web: dependency: transitive description: name: uni_links_web - url: "https://pub.dartlang.org" + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" source: hosted version: "0.1.0" url_launcher: dependency: transitive description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "236e5ad4df2dabb79ce5d0f1aafb4cb70c0ad6dc93f88e610ad31c8fef74f477" + url: "https://pub.dev" source: hosted version: "6.0.18" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "2ec66333093cf5dd5103f089c3a7842fcd5581023a571839d3d176ff10244582" + url: "https://pub.dev" source: hosted version: "6.0.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "0d6b0b9dce05986462e8ba08e51f1909ac63e2e59ebde829c46ad66423f03dee" + url: "https://pub.dev" source: hosted version: "6.0.14" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: ae3c5ce30a1ba0a69c3f8803b23450703cf915575ad591857df98d9c15c11018 + url: "https://pub.dev" source: hosted version: "2.0.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "8fd9ae3ab5e0f96cea7dd66c4ea65e39e3477067f4997c1ec8225d553e8bb8ea" + url: "https://pub.dev" source: hosted version: "2.0.3" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "1b9c4dab07794498b83b5f938e26b20f68c3b460a3015b0307f9541cb34ef93d" + url: "https://pub.dev" source: hosted version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "88de707cba8e0c1c678ed79274298c4737f7f24addd12e786fbf0597de085850" + url: "https://pub.dev" source: hosted version: "2.0.6" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: "4e24aac2a2960fb9a70a07992e1ba69cb99fbcee48fdf17abe280ce867bfcea2" + url: "https://pub.dev" source: hosted version: "2.0.2" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + sha256: "00ba1241ff12e77d8059eeb1f102b35235df01661a6110afd165ab52a0fc7714" + url: "https://pub.dev" source: hosted version: "3.0.5" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + sha256: "9550ef42803203cd729e34313142b7a1ee2a90b395105a7e38e32a9139fa28ad" + url: "https://pub.dev" source: hosted version: "2.2.18" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + sha256: "3be61312f2c72f1f72234a37c7fc7cb6e75d8f9be8f52f3cf8e03ea53f50e148" + url: "https://pub.dev" source: hosted version: "2.3.0" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + sha256: e49d39992fe2c38ec5e7f420b74d0d4c288bee195af61ca9d75ecc19b5025c41 + url: "https://pub.dev" source: hosted version: "2.3.0" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + sha256: ac0b3515d47244cf59aea6cdb009b745d5299b12d248ae123608ec533d483cb6 + url: "https://pub.dev" source: hosted version: "5.1.0" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + sha256: "48de7d5ff19fb474082182c33704c0210c395521ff81e54fa255c7719056fc81" + url: "https://pub.dev" source: hosted version: "2.0.7" wakelock: dependency: transitive description: name: wakelock - url: "https://pub.dartlang.org" + sha256: da22c0789e1f849bec43688a52f2290f4e66268056f7cd77cb71245aef4917a0 + url: "https://pub.dev" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" + url: "https://pub.dev" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + sha256: "108b1b73711f1664ee462e73af34a9286ff496e27d4d8371e2fb4da8fde4cdac" + url: "https://pub.dev" source: hosted version: "0.2.0" webview_flutter: dependency: transitive description: name: webview_flutter - url: "https://pub.dartlang.org" + sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" + url: "https://pub.dev" source: hosted version: "2.8.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - url: "https://pub.dartlang.org" + sha256: "6d796e709d14da8496ee7c750765fad0e860c57a60844bb50f5b93a9ee5ceeab" + url: "https://pub.dev" source: hosted version: "2.8.3" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - url: "https://pub.dartlang.org" + sha256: "6144d750f56ae63fdaad10ff09e0f762142beabde4fefdc2d32564f75572d905" + url: "https://pub.dev" source: hosted version: "1.8.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - url: "https://pub.dartlang.org" + sha256: "87c56a34f69867f3aaf0b66df5399a4fc18898b1df8c738b835e62b73059d66f" + url: "https://pub.dev" source: hosted version: "2.7.1" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: "0b2350f19ef62ccc5d594ea92d9107230210ae0b37459686687b70f41b911e12" + url: "https://pub.dev" source: hosted version: "2.3.6" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: "11541eedefbcaec9de35aa82650b695297ce668662bbd6e3911a7fabdbde589f" + url: "https://pub.dev" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: baa23bcba1ba4ce4b22c0c7a1d9c861e7015cb5169512676da0b85138e72840c + url: "https://pub.dev" source: hosted version: "5.3.1" sdks: - dart: ">=2.18.0-0 <3.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.3.0-0" From 012a5d9b2e4d82bbf6aec6012d475c8b1385df48 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 9 Mar 2023 11:48:27 -0600 Subject: [PATCH 009/177] update dependencies --- example/pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 87df804a3..95e1d6336 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: device_calendar - sha256: "3b22c89e1edb6ad296452df5a7ed3ce45e9f4abff74e838d0b73357cb39a4326" + sha256: d0cad5428c8f5afb9b774b9a263922a16a2c01a05fb0ca50f8ea69ea294fb275 url: "https://pub.dev" source: hosted - version: "4.3.0-3635229635" + version: "4.3.1-4217521123" device_info: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7efb8ac46..47c50c92f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: sprintf: ^7.0.0 flutter_local_notifications: ^12.0.0 sqflite: ^2.1.0 - device_calendar: ^4.2.0 + device_calendar: ^4.3.1-4217521123 image_picker: ^0.8.5+3 mime_type: ^1.0.0 uuid: ^3.0.5 From 966930581293a38fb48ecb419566b1edc1c8f286 Mon Sep 17 00:00:00 2001 From: Ben Dworkin Date: Tue, 21 Mar 2023 11:36:26 -0500 Subject: [PATCH 010/177] add timezone database --- assets/timezone.tzf | Bin 0 -> 382496 bytes lib/service/app_datetime.dart | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 assets/timezone.tzf diff --git a/assets/timezone.tzf b/assets/timezone.tzf new file mode 100644 index 0000000000000000000000000000000000000000..3e73678ecf6533a3a3837a75b76cf3c8085680d4 GIT binary patch literal 382496 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 <&NCdRIF`Maoac_>`xf2EPD}T2 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 IFGJh^11L804gdfE literal 0 HcmV?d00001 diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index f0ff4a2f0..b8a942385 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -70,7 +70,11 @@ 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; From ef722ecc6ad7d79e4289343aaf52a9850b330f8a Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 21 Mar 2023 12:07:19 -0500 Subject: [PATCH 011/177] add assets to pubspec --- pubspec.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 47c50c92f..a61493541 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,7 +72,8 @@ flutter: pluginClass: RokwirePlugin # To add assets to your plugin package, add an assets section, like this: - # assets: + assets: + - assets/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # From 226b582be020bbcf51feb0703c303f9d4384095e Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 21 Mar 2023 15:37:59 -0500 Subject: [PATCH 012/177] fix style issues --- lib/service/app_datetime.dart | 7 ++++--- lib/service/config.dart | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index b8a942385..fed8043e6 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'; @@ -76,14 +77,14 @@ class AppDateTime with Service { 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) { @@ -165,7 +166,7 @@ class AppDateTime with Service { String? timePrefix = getDisplayDay(dateTimeUtc: dateTimeUtc, allDay: allDay, considerSettingsDisplayTime: considerSettingsDisplayTime, includeAtSuffix: includeAtSuffix); String? timeSuffix = getDisplayTime(dateTimeUtc: dateTimeUtc, allDay: allDay, considerSettingsDisplayTime: considerSettingsDisplayTime); - return '$timePrefix $timeSuffix'; + return '$timePrefix, $timeSuffix'; } String? getDisplayDay({DateTime? dateTimeUtc, bool allDay = false, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false}) { diff --git a/lib/service/config.dart b/lib/service/config.dart index 58626a2f4..5c93ad86d 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -540,7 +540,8 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { int get refreshTimeout => JsonUtils.intValue(settings['refreshTimeout']) ?? 0; int? get analyticsDeliveryTimeout => JsonUtils.intValue(settings['analyticsDeliveryTimeout']); int get refreshTokenRetriesCount => JsonUtils.intValue(settings['refreshTokenRetriesCount']) ?? 3; - + String? get timezoneLocation => JsonUtils.stringValue(settings['timezoneLocation']) ?? 'America/Chicago'; + // Getters: other String? get deepLinkRedirectUrl { Uri? assetsUri = StringUtils.isNotEmpty(assetsUrl) ? Uri.tryParse(assetsUrl!) : null; From 5c2af8c06fc69ff973d646bb3a24639c16bbdfc8 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 23 Mar 2023 11:04:47 -0500 Subject: [PATCH 013/177] add gen styles --- utils/gen_styles.dart | 104 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 utils/gen_styles.dart diff --git a/utils/gen_styles.dart b/utils/gen_styles.dart new file mode 100644 index 000000000..dbd9fc928 --- /dev/null +++ b/utils/gen_styles.dart @@ -0,0 +1,104 @@ +import 'dart:convert'; +import 'dart:io'; + +Map classMap = { + 'color': 'AppColors', + 'text_style': 'AppTextStyles', + 'font_family': 'AppFontFamilies', + 'image': 'AppImages', +}; + +Map typesMap = { + 'color': 'Color?', + 'text_style': 'TextStyle?', + 'font_family': 'String?', + 'image': 'Widget?', +}; + +Map refsMap = { + 'color': 'Styles().colors?.getColor(%key)', + 'text_style': 'Styles().textStyles?.getTextStyle(%key)', + 'font_family': 'Styles().fontFamilies?.fromCode(%key)', + 'image': 'Styles().images?.getImage(%key)', +}; + +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; +} + +void main() async { + String assetFilepath = 'assets/styles.json'; + String genFilepath = 'lib/gen/styles.dart'; + + Map? asset = await _loadFileJson(assetFilepath); + if (asset != null) { + String fileString = _parseAsset(asset); + if (fileString.isNotEmpty) { + File(genFilepath).writeAsString(fileString); + print("saved generated file to $genFilepath"); + } + } else { + print('asset was not loaded'); + } +} + +String _parseAsset(Map 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; + } + + String classString = "class $className {\n"; + for (MapEntry entry in json.entries) { + classString += " static $type ${camelCase(entry.key)} = ${ref.replaceAll("%key", "'${entry.key}'")};\n"; + } + classString += "}\n"; + return classString; +} + +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:flutter/material.dart';\n"; + fileString += "\n"; + fileString += classStrings.join("\n"); + return fileString; +} + +Future?> _loadFileJson(String filepath) async { + try { + String content = File(filepath).readAsStringSync(); + return json.decode(content); + } catch (e) { + print(e); + } + return null; +} \ No newline at end of file From c44aa81ae7c56496691d4e80dbf64bbea18f8588 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 23 Mar 2023 11:31:55 -0500 Subject: [PATCH 014/177] add auto-update code refs to gen styles --- utils/gen_styles.dart | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/utils/gen_styles.dart b/utils/gen_styles.dart index dbd9fc928..ae23c4989 100644 --- a/utils/gen_styles.dart +++ b/utils/gen_styles.dart @@ -1,5 +1,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:args/args.dart'; + +const flagUpdateCode = 'update-code'; Map classMap = { 'color': 'AppColors', @@ -37,9 +40,17 @@ String camelCase(String s, {bool startUpper = false}) { return out; } -void main() async { +Map replacements = {}; + +void main(List arguments) async { + final parser = ArgParser()..addFlag(flagUpdateCode, negatable: false, abbr: 'u'); + ArgResults argResults = parser.parse(arguments); + + bool updateCode = argResults[flagUpdateCode]; + String assetFilepath = 'assets/styles.json'; - String genFilepath = 'lib/gen/styles.dart'; + String libPath = 'lib/'; + String genFilepath = '${libPath}gen/styles.dart'; Map? asset = await _loadFileJson(assetFilepath); if (asset != null) { @@ -47,6 +58,9 @@ void main() async { if (fileString.isNotEmpty) { File(genFilepath).writeAsString(fileString); print("saved generated file to $genFilepath"); + if (updateCode) { + _updateCodeRefs(libPath, genFilepath); + } } } else { print('asset was not loaded'); @@ -78,7 +92,10 @@ String? _buildClass(String name, Map json) { String classString = "class $className {\n"; for (MapEntry entry in json.entries) { - classString += " static $type ${camelCase(entry.key)} = ${ref.replaceAll("%key", "'${entry.key}'")};\n"; + String varName = camelCase(entry.key); + String varRef = ref.replaceAll("%key", "'${entry.key}'"); + classString += " static $type $varName = $varRef;\n"; + replacements[varRef] = '$className.$varName'; } classString += "}\n"; return classString; @@ -101,4 +118,20 @@ Future?> _loadFileJson(String filepath) async { 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); + } + } } \ No newline at end of file From 07fa9f1b9a960647e98171b326bc12c584a53bc2 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 23 Mar 2023 12:14:55 -0500 Subject: [PATCH 015/177] add instructions for gen styles tool to readme --- README.md | 27 +++++++++++++++++++++++++++ {utils => tools}/gen_styles.dart | 0 2 files changed, 27 insertions(+) rename {utils => tools}/gen_styles.dart (100%) diff --git a/README.md b/README.md index d745e5a2b..20cd4971d 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,30 @@ 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. + +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, +run the following command: + +``` +dart plugin/tools/gen_styles.dart -u +``` + + diff --git a/utils/gen_styles.dart b/tools/gen_styles.dart similarity index 100% rename from utils/gen_styles.dart rename to tools/gen_styles.dart From 8a5a79bb3dc3cd8823a76d2d9a351a8608e66b9a Mon Sep 17 00:00:00 2001 From: Ben Dworkin Date: Wed, 29 Mar 2023 15:00:48 -0500 Subject: [PATCH 016/177] fixed scroll pager --- lib/ui/widgets/scroll_pager.dart | 63 +++++++++++++++++--------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/lib/ui/widgets/scroll_pager.dart b/lib/ui/widgets/scroll_pager.dart index 8c497a456..8f05324a9 100644 --- a/lib/ui/widgets/scroll_pager.dart +++ b/lib/ui/widgets/scroll_pager.dart @@ -6,46 +6,43 @@ import 'package:rokwire_plugin/ui/widget_builders/scroll_pager.dart'; class ScrollPager extends StatelessWidget { final Widget? child; final Axis scrollDirection; - final bool? reverse; + final bool reverse; final EdgeInsets? padding; final bool? primary; final ScrollPhysics? physics; - late final ScrollController controller; - late final ScrollPagerController pagerController; + final ScrollPagerController controller; final DragStartBehavior dragStartBehavior; final Clip clipBehavior; final String? restorationId; final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - ScrollPager({Key? key, + const ScrollPager({Key? key, this.scrollDirection = Axis.vertical, this.reverse = false, this.padding, this.primary, this.physics, - ScrollController? controller, - required this.pagerController, + required this.controller, this.child, this.dragStartBehavior = DragStartBehavior.start, this.clipBehavior = Clip.hardEdge, this.restorationId, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, - }) : super(key: key) { - this.controller = controller ?? ScrollController(); - pagerController.registerScrollController(controller!); - } + }) : super(key: key); @override Widget build(BuildContext context) { - return SingleChildScrollView(key: key, scrollDirection: scrollDirection, reverse: true, padding: padding, - primary: primary, physics: physics, controller: controller, child: child, dragStartBehavior: dragStartBehavior, - clipBehavior: clipBehavior, restorationId: restorationId, keyboardDismissBehavior: keyboardDismissBehavior); + return RefreshIndicator(onRefresh: controller.reset, + child: SingleChildScrollView(key: key, scrollDirection: scrollDirection, reverse: reverse, padding: padding, + primary: primary, physics: physics, controller: controller.scrollController, child: buildChild(), dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, restorationId: restorationId, keyboardDismissBehavior: keyboardDismissBehavior), + ); } Widget buildChild() { return Column(children: [ child ?? Container(), - ScrollPagerBuilder.buildScrollPagerFooter(pagerController) ?? Container(), + ScrollPagerBuilder.buildScrollPagerFooter(controller) ?? Container(), ]); } } @@ -56,6 +53,7 @@ class ScrollPagerController { Future Function({required int offset, required int limit}) onPage; void Function()? onStateChanged; + void Function()? onReset; bool _isLoading = false; bool _end = false; @@ -67,13 +65,19 @@ class ScrollPagerController { ScrollController? _scrollController; - ScrollPagerController({required this.limit, required this.onPage, this.onStateChanged}); + ScrollController? get scrollController => _scrollController; + + ScrollPagerController({required this.limit, required this.onPage, this.onStateChanged, this.onReset, ScrollController? controller}) { + controller ??= ScrollController(); + registerScrollController(controller); + } - void reset() { + Future reset() async { _offset = 0; _end = false; _error = false; - loadPage(); + onReset?.call(); + await loadPage(); } void registerScrollController(ScrollController controller) { @@ -91,23 +95,22 @@ class ScrollPagerController { } } - void loadPage({bool retry = false}) { + Future loadPage({bool retry = false}) async { if (!_isLoading && !_end && (!_error || retry)) { _isLoading = true; onStateChanged?.call(); - onPage(offset: _offset, limit: limit).then((value) { - if (value >= 0) { - _error = false; - _offset += value; - if (value < limit) { - _end = true; - } - } else { - _error = true; + int value = await onPage(offset: _offset, limit: limit); + if (value >= 0) { + _error = false; + _offset += value; + if (value < limit) { + _end = true; } - _isLoading = false; - onStateChanged?.call(); - }); + } else { + _error = true; + } + _isLoading = false; + onStateChanged?.call(); } } From fcd45e40caaad9443f5dea038f8cbe5a32947e3c Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 29 Mar 2023 16:51:30 -0500 Subject: [PATCH 017/177] handle plugin style asset merging --- README.md | 10 +- assets/styles.json | 210 +++++++++++++++++++++++ lib/ui/widget_builders/buttons.dart | 3 +- lib/ui/widget_builders/scroll_pager.dart | 3 +- lib/ui/widget_builders/survey.dart | 15 +- lib/ui/widgets/survey.dart | 17 +- tools/gen_styles.dart | 80 ++++++++- 7 files changed, 317 insertions(+), 21 deletions(-) create mode 100644 assets/styles.json diff --git a/README.md b/README.md index 20cd4971d..dccfd097a 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,17 @@ 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, -run the following command: +provide the `-u` flag: ``` dart plugin/tools/gen_styles.dart -u diff --git a/assets/styles.json b/assets/styles.json new file mode 100644 index 000000000..1b78cfdaa --- /dev/null +++ b/assets/styles.json @@ -0,0 +1,210 @@ +{ + "color": { + "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", + + "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.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": "textSecondary"}, + "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"} + } +} \ No newline at end of file diff --git a/lib/ui/widget_builders/buttons.dart b/lib/ui/widget_builders/buttons.dart index 959f2c441..b5397965a 100644 --- a/lib/ui/widget_builders/buttons.dart +++ b/lib/ui/widget_builders/buttons.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/widgets/rounded_button.dart'; @@ -8,7 +9,7 @@ class ButtonBuilder { label: label, borderColor: Styles().colors?.fillColorSecondary, backgroundColor: Styles().colors?.surface, - textStyle: Styles().textStyles?.getTextStyle('widget.detail.regular.bold'), + textStyle: AppTextStyles.widgetDetailRegularBold, onTap: onTap, ); } diff --git a/lib/ui/widget_builders/scroll_pager.dart b/lib/ui/widget_builders/scroll_pager.dart index 47dd7ffbc..c4bef4893 100644 --- a/lib/ui/widget_builders/scroll_pager.dart +++ b/lib/ui/widget_builders/scroll_pager.dart @@ -1,4 +1,5 @@ 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'; @@ -33,7 +34,7 @@ class ScrollPagerBuilder { Styles().images?.getImage('retry-gray', defaultSpec: FontAwesomeImageSpec(type: 'fa.icon', source: '0xf2f9', weight: 'solid', size: 18.0, color: Styles().colors?.iconMedium)) ?? Container(), 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 1deb8454c..220e6b145 100644 --- a/lib/ui/widget_builders/survey.dart +++ b/lib/ui/widget_builders/survey.dart @@ -1,5 +1,6 @@ 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'; @@ -15,10 +16,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.bold'))); + 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,11 +50,11 @@ class SurveyBuilder { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible(child: Text(response.survey.title.toUpperCase(), style: Styles().textStyles?.getTextStyle('widget.title.small.bold'))), + Flexible(child: Text(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', excludeFromSemantics: true) ?? Container() // UIIcon(IconAssets.chevronRight, size: 14.0, color: Styles().colors.headlineText), @@ -75,9 +76,9 @@ class SurveyBuilder { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(key.replaceAll('_', ' ').toUpperCase() + ':', - style: Styles().textStyles?.getTextStyle('widget.description.regular.bold')), + 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)), ], )); } @@ -118,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.bold')), + style: AppTextStyles.widgetDetailRegularBold), const SizedBox(height: 8.0), SurveyBuilder.surveyDataResult(context, dataResult) ?? Container(), ], diff --git a/lib/ui/widgets/survey.dart b/lib/ui/widgets/survey.dart index bec0e7104..c67566350 100644 --- a/lib/ui/widgets/survey.dart +++ b/lib/ui/widgets/survey.dart @@ -15,6 +15,7 @@ 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/survey.dart'; import 'package:rokwire_plugin/service/app_datetime.dart'; @@ -152,14 +153,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.bold'),), + child: Text(_survey!.moreInfo ?? '', style: AppTextStyles.widgetMessageLargeBold,), ); } @@ -229,14 +230,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.bold'))), + 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, ), ), ), @@ -251,7 +252,7 @@ class _SurveyWidgetState extends State { child: Text( survey.moreInfo ?? '', textAlign: TextAlign.start, - style: Styles().textStyles?.getTextStyle('widget.detail.regular'), + style: AppTextStyles.widgetDetailRegular, ), ), ), @@ -384,7 +385,7 @@ class _SurveyWidgetState extends State { _onChangeResponse(false); }, enabled: enabled, - textWidget: Text(option.title, style: Styles().textStyles?.getTextStyle('widget.detail.small'), textAlign: TextAlign.center), + textWidget: Text(option.title, style: AppTextStyles.widgetDetailSmall, 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)), @@ -911,7 +912,7 @@ class SingleSelectionList extends StatelessWidget { clipBehavior: Clip.hardEdge, child: RadioListTile( title: Transform.translate(offset: const Offset(-15, 0), - child: Text(title, style: Styles().textStyles?.getTextStyle('widget.title.regular') ?? + child: Text(title, style: AppTextStyles.widgetTitleRegular ?? TextStyle(fontFamily: Styles().fontFamilies?.regular, fontSize: 16, color: Styles().colors?.textPrimary))), activeColor: Styles().colors?.fillColorSecondary, @@ -951,7 +952,7 @@ class MultiSelectionList extends StatelessWidget { child: ListTile( title: Transform.translate(offset: const Offset(-15, 0), child: Text(selectionList[index].title, - style: Styles().textStyles?.getTextStyle('widget.title.regular') ?? + style: AppTextStyles.widgetTitleRegular ?? TextStyle(fontFamily: Styles().fontFamilies?.regular, fontSize: 16, color: Styles().colors?.textPrimary))), leading: Checkbox( diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index ae23c4989..485414b5d 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:args/args.dart'; const flagUpdateCode = 'update-code'; +const flagSkipPlugin = 'skip-plugin'; Map classMap = { 'color': 'AppColors', @@ -43,21 +44,35 @@ String camelCase(String s, {bool startUpper = false}) { Map replacements = {}; void main(List arguments) async { - final parser = ArgParser()..addFlag(flagUpdateCode, negatable: false, abbr: 'u'); + 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'; Map? asset = await _loadFileJson(assetFilepath); + if (!skipPlugin) { + print("merging plugin asset..."); + Map? 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'); + } + } if (asset != null) { String fileString = _parseAsset(asset); if (fileString.isNotEmpty) { File(genFilepath).writeAsString(fileString); - print("saved generated file to $genFilepath"); + print("saved generated code to $genFilepath"); if (updateCode) { _updateCodeRefs(libPath, genFilepath); } @@ -67,6 +82,65 @@ void main(List arguments) async { } } +String _prettyJsonEncode(Map jsonObject){ + String out = '{'; + bool first = true; + for (MapEntry entry in jsonObject.entries) { + if (!first) { + out += ','; + } + out += '\n'; + out += ' "${entry.key}": {'; + if (entry.value is Map) { + bool firstSub = true; + for (MapEntry subentry in entry.value.entries) { + if (!firstSub) { + out += ','; + } + out += '\n'; + Map subMap = {}; + subMap.addEntries([subentry]); + String valJson = json.encode(subMap).replaceAll(':', ': '); + out += ' ${valJson.substring(1, valJson.length - 1)}'; + firstSub = false; + } + } else { + out += ' ${json.encode(entry.value)}\n'; + } + out += '\n'; + out += ' }'; + first = false; + } + out += '\n'; + out += '}'; + return out; +} + +Map _mergeJson(Map? from, Map? to) { + to ??= {}; + Map out = {}; + out.addAll(to); + for (MapEntry section in from?.entries ?? {}) { + if (!out.containsKey(section.key)) { + out[section.key] = {}; + } + + if (section.value is Map) { + for (MapEntry entry in section.value.entries ?? {}) { + dynamic outSection = out[section.key]; + if (!outSection.containsKey(entry.key)) { + print("added ${section.key}: ${entry.key} = ${entry.value}"); + out[section.key][entry.key] = entry.value; + } + } + } else { + print("unexpected section type: ${section.value}"); + } + } + + return out; +} + String _parseAsset(Map asset) { List classStrings = []; for (MapEntry entry in asset.entries) { @@ -94,7 +168,7 @@ String? _buildClass(String name, Map json) { for (MapEntry entry in json.entries) { String varName = camelCase(entry.key); String varRef = ref.replaceAll("%key", "'${entry.key}'"); - classString += " static $type $varName = $varRef;\n"; + classString += " static $type get $varName => $varRef;\n"; replacements[varRef] = '$className.$varName'; } classString += "}\n"; From 2b31d9bb43a61bd45a72dbae76c928f90fa4b093 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 29 Mar 2023 16:57:21 -0500 Subject: [PATCH 018/177] add generated styles --- lib/gen/styles.dart | 189 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 lib/gen/styles.dart diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart new file mode 100644 index 000000000..1116cabbe --- /dev/null +++ b/lib/gen/styles.dart @@ -0,0 +1,189 @@ +// Code generated by plugin/utils/gen_styles.dart DO NOT EDIT. + +import 'package:rokwire_plugin/service/styles.dart'; +import 'package:flutter/material.dart'; + +class AppColors { + static Color? get fillColorPrimary => Styles().colors?.getColor('fillColorPrimary'); + static Color? get fillColorPrimaryVariant => Styles().colors?.getColor('fillColorPrimaryVariant'); + static Color? get fillColorSecondary => Styles().colors?.getColor('fillColorSecondary'); + static Color? get fillColorSecondaryVariant => Styles().colors?.getColor('fillColorSecondaryVariant'); + static Color? get textPrimary => Styles().colors?.getColor('textPrimary'); + static Color? get textAccent => Styles().colors?.getColor('textAccent'); + static Color? get textDark => Styles().colors?.getColor('textDark'); + static Color? get textMedium => Styles().colors?.getColor('textMedium'); + static Color? get textLight => Styles().colors?.getColor('textLight'); + static Color? get textDisabled => Styles().colors?.getColor('textDisabled'); + static Color? get iconPrimary => Styles().colors?.getColor('iconPrimary'); + static Color? get iconLight => Styles().colors?.getColor('iconLight'); + static Color? get iconDark => Styles().colors?.getColor('iconDark'); + static Color? get iconMedium => Styles().colors?.getColor('iconMedium'); + static Color? get iconDisabled => Styles().colors?.getColor('iconDisabled'); + static Color? get surface => Styles().colors?.getColor('surface'); + static Color? get surfaceAccent => Styles().colors?.getColor('surfaceAccent'); + static Color? get background => Styles().colors?.getColor('background'); + static Color? get backgroundVariant => Styles().colors?.getColor('backgroundVariant'); + static Color? get gradientColorPrimary => Styles().colors?.getColor('gradientColorPrimary'); + static Color? get accentColor1 => Styles().colors?.getColor('accentColor1'); + static Color? get accentColor2 => Styles().colors?.getColor('accentColor2'); + static Color? get accentColor3 => Styles().colors?.getColor('accentColor3'); + static Color? get accentColor4 => Styles().colors?.getColor('accentColor4'); + static Color? get success => Styles().colors?.getColor('success'); + static Color? get alert => Styles().colors?.getColor('alert'); + static Color? get dividerLine => Styles().colors?.getColor('dividerLine'); +} + +class AppFontFamilies { + static String? get black => Styles().fontFamilies?.fromCode('black'); + static String? get blackItalic => Styles().fontFamilies?.fromCode('black_italic'); + static String? get bold => Styles().fontFamilies?.fromCode('bold'); + static String? get boldItalic => Styles().fontFamilies?.fromCode('bold_italic'); + static String? get extraBold => Styles().fontFamilies?.fromCode('extra_bold'); + static String? get extraBoldItalic => Styles().fontFamilies?.fromCode('extra_bold_italic'); + static String? get light => Styles().fontFamilies?.fromCode('light'); + static String? get lightItalic => Styles().fontFamilies?.fromCode('light_italic'); + static String? get medium => Styles().fontFamilies?.fromCode('medium'); + static String? get mediumItalic => Styles().fontFamilies?.fromCode('medium_italic'); + static String? get regular => Styles().fontFamilies?.fromCode('regular'); + static String? get regularItalic => Styles().fontFamilies?.fromCode('regular_italic'); + static String? get semiBold => Styles().fontFamilies?.fromCode('semi_bold'); + static String? get semiBoldItalic => Styles().fontFamilies?.fromCode('semi_bold_italic'); + static String? get thin => Styles().fontFamilies?.fromCode('thin'); + static String? get thinItalic => Styles().fontFamilies?.fromCode('thin_italic'); +} + +class AppTextStyles { + static TextStyle? get appTitle => Styles().textStyles?.getTextStyle('app_title'); + static TextStyle? get headerBar => Styles().textStyles?.getTextStyle('header_bar'); + static TextStyle? get headerBarAccent => Styles().textStyles?.getTextStyle('header_bar.accent'); + static TextStyle? get widgetHeadingExtraLarge => Styles().textStyles?.getTextStyle('widget.heading.extra_large'); + static TextStyle? get widgetHeadingExtraLargeBold => Styles().textStyles?.getTextStyle('widget.heading.extra_large.bold'); + static TextStyle? get widgetHeadingLarge => Styles().textStyles?.getTextStyle('widget.heading.large'); + static TextStyle? get widgetHeadingLargeBold => Styles().textStyles?.getTextStyle('widget.heading.large.bold'); + static TextStyle? get widgetHeadingRegular => Styles().textStyles?.getTextStyle('widget.heading.regular'); + static TextStyle? get widgetHeadingRegularBold => Styles().textStyles?.getTextStyle('widget.heading.regular.bold'); + static TextStyle? get widgetHeadingMedium => Styles().textStyles?.getTextStyle('widget.heading.medium'); + static TextStyle? get widgetHeadingSmall => Styles().textStyles?.getTextStyle('widget.heading.small'); + static TextStyle? get widgetMessageDarkExtraLarge => Styles().textStyles?.getTextStyle('widget.message.dark.extra_large'); + static TextStyle? get widgetMessageDarkMedium => Styles().textStyles?.getTextStyle('widget.message.dark.medium'); + static TextStyle? get widgetMessageExtraLargeBold => Styles().textStyles?.getTextStyle('widget.message.extra_large.bold'); + static TextStyle? get widgetMessageLargeBold => Styles().textStyles?.getTextStyle('widget.message.large.bold'); + static TextStyle? get widgetMessageLarge => Styles().textStyles?.getTextStyle('widget.message.large'); + static TextStyle? get widgetMessageLargeDarkBold => Styles().textStyles?.getTextStyle('widget.message.large.dark.bold'); + static TextStyle? get widgetMessageRegularPrimaryBold => Styles().textStyles?.getTextStyle('widget.message.regular.primary.bold'); + static TextStyle? get widgetMessageRegularPrimary => Styles().textStyles?.getTextStyle('widget.message.regular.primary'); + static TextStyle? get widgetMessageLightBoldPrimary => Styles().textStyles?.getTextStyle('widget.message.light.bold.primary'); + static TextStyle? get widgetMessageMedium => Styles().textStyles?.getTextStyle('widget.message.medium'); + static TextStyle? get widgetMessageRegular => Styles().textStyles?.getTextStyle('widget.message.regular'); + static TextStyle? get widgetMessageRegularBold => Styles().textStyles?.getTextStyle('widget.message.regular.bold'); + static TextStyle? get widgetMessageRegularBoldAccent => Styles().textStyles?.getTextStyle('widget.message.regular.bold.accent'); + static TextStyle? get widgetMessageSmall => Styles().textStyles?.getTextStyle('widget.message.small'); + static TextStyle? get widgetMessageSmallPrimaryBold => Styles().textStyles?.getTextStyle('widget.message.small.primary.bold'); + static TextStyle? get widgetMessageLightRegular => Styles().textStyles?.getTextStyle('widget.message.light.regular'); + static TextStyle? get widgetTitleExtraLarge => Styles().textStyles?.getTextStyle('widget.title.extra_large'); + static TextStyle? get widgetTitleLarge => Styles().textStyles?.getTextStyle('widget.title.large'); + static TextStyle? get widgetTitleLargeBold => Styles().textStyles?.getTextStyle('widget.title.large.bold'); + static TextStyle? get widgetTitleMedium => Styles().textStyles?.getTextStyle('widget.title.medium'); + static TextStyle? get widgetTitleMediumBold => Styles().textStyles?.getTextStyle('widget.title.medium.bold'); + static TextStyle? get widgetTitleRegular => Styles().textStyles?.getTextStyle('widget.title.regular'); + static TextStyle? get widgetTitleSmallBold => Styles().textStyles?.getTextStyle('widget.title.small.bold'); + static TextStyle? get widgetTitleTiny => Styles().textStyles?.getTextStyle('widget.title.tiny'); + static TextStyle? get widgetTitleAccentExtraLarge => Styles().textStyles?.getTextStyle('widget.title.accent.extra_large'); + static TextStyle? get widgetTitleAccentLarge => Styles().textStyles?.getTextStyle('widget.title.accent.large'); + static TextStyle? get widgetTitleAccentLargeBold => Styles().textStyles?.getTextStyle('widget.title.accent.large.bold'); + static TextStyle? get widgetTitleAccentMedium => Styles().textStyles?.getTextStyle('widget.title.accent.medium'); + static TextStyle? get widgetTitleAccentMediumBold => Styles().textStyles?.getTextStyle('widget.title.accent.medium.bold'); + static TextStyle? get widgetTitleAccentRegular => Styles().textStyles?.getTextStyle('widget.title.accent.regular'); + static TextStyle? get widgetTitleAccentSmallBold => Styles().textStyles?.getTextStyle('widget.title.accent.small.bold'); + static TextStyle? get widgetTitleAccentTiny => Styles().textStyles?.getTextStyle('widget.title.accent.tiny'); + static TextStyle? get widgetDetailLarge => Styles().textStyles?.getTextStyle('widget.detail.large'); + static TextStyle? get widgetDetailLargeBold => Styles().textStyles?.getTextStyle('widget.detail.large.bold'); + static TextStyle? get widgetDetailRegularBold => Styles().textStyles?.getTextStyle('widget.detail.regular.bold'); + static TextStyle? get widgetDetailRegular => Styles().textStyles?.getTextStyle('widget.detail.regular'); + static TextStyle? get widgetDetailMedium => Styles().textStyles?.getTextStyle('widget.detail.medium'); + static TextStyle? get widgetDetailSmall => Styles().textStyles?.getTextStyle('widget.detail.small'); + static TextStyle? get widgetDetailLightRegular => Styles().textStyles?.getTextStyle('widget.detail.light.regular'); + static TextStyle? get widgetDescriptionLarge => Styles().textStyles?.getTextStyle('widget.description.large'); + static TextStyle? get widgetDescriptionMedium => Styles().textStyles?.getTextStyle('widget.description.medium'); + static TextStyle? get widgetDescriptionRegularThin => Styles().textStyles?.getTextStyle('widget.description.regular.thin'); + static TextStyle? get widgetDescriptionRegular => Styles().textStyles?.getTextStyle('widget.description.regular'); + static TextStyle? get widgetDescriptionRegularBold => Styles().textStyles?.getTextStyle('widget.description.regular.bold'); + static TextStyle? get widgetDescriptionSmall => Styles().textStyles?.getTextStyle('widget.description.small'); + static TextStyle? get widgetDescriptionSmallUnderline => Styles().textStyles?.getTextStyle('widget.description.small_underline'); + static TextStyle? get widgetDescriptionSmallBold => Styles().textStyles?.getTextStyle('widget.description.small.bold'); + static TextStyle? get widgetDescriptionSmallBoldSemiExpanded => Styles().textStyles?.getTextStyle('widget.description.small.bold.semi_expanded'); + static TextStyle? get widgetSuccessRegular => Styles().textStyles?.getTextStyle('widget.success.regular'); + static TextStyle? get widgetSuccessRegularBold => Styles().textStyles?.getTextStyle('widget.success.regular.bold'); + static TextStyle? get widgetErrorRegular => Styles().textStyles?.getTextStyle('widget.error.regular'); + static TextStyle? get widgetErrorRegularBold => Styles().textStyles?.getTextStyle('widget.error.regular.bold'); + static TextStyle? get widgetItemMediumBold => Styles().textStyles?.getTextStyle('widget.item.medium.bold'); + static TextStyle? get widgetItemMedium => Styles().textStyles?.getTextStyle('widget.item.medium'); + static TextStyle? get widgetItemRegularBold => Styles().textStyles?.getTextStyle('widget.item.regular.bold'); + static TextStyle? get widgetItemRegularThin => Styles().textStyles?.getTextStyle('widget.item.regular.thin'); + static TextStyle? get widgetItemRegular => Styles().textStyles?.getTextStyle('widget.item.regular'); + static TextStyle? get widgetItemSmallBold => Styles().textStyles?.getTextStyle('widget.item.small.bold'); + static TextStyle? get widgetItemSmall => Styles().textStyles?.getTextStyle('widget.item.small'); + static TextStyle? get widgetItemSmallThin => Styles().textStyles?.getTextStyle('widget.item.small.thin'); + static TextStyle? get widgetItemTinyBold => Styles().textStyles?.getTextStyle('widget.item.tiny.bold'); + static TextStyle? get widgetItemTiny => Styles().textStyles?.getTextStyle('widget.item.tiny'); + static TextStyle? get widgetItemTinyThin => Styles().textStyles?.getTextStyle('widget.item.tiny.thin'); + static TextStyle? get widgetInfoRegular => Styles().textStyles?.getTextStyle('widget.info.regular'); + static TextStyle? get widgetInfoRegularBold => Styles().textStyles?.getTextStyle('widget.info.regular.bold'); + static TextStyle? get widgetInfoSmall => Styles().textStyles?.getTextStyle('widget.info.small'); + static TextStyle? get widgetInfoSmallBold => Styles().textStyles?.getTextStyle('widget.info.small.bold'); + static TextStyle? get widgetTabSelected => Styles().textStyles?.getTextStyle('widget.tab.selected'); + static TextStyle? get widgetTabNotSelected => Styles().textStyles?.getTextStyle('widget.tab.not_selected'); + static TextStyle? get widgetButtonTitleRegular => Styles().textStyles?.getTextStyle('widget.button.title.regular'); + static TextStyle? get widgetButtonTitleMedium => Styles().textStyles?.getTextStyle('widget.button.title.medium'); + static TextStyle? get widgetButtonTitleMediumBold => Styles().textStyles?.getTextStyle('widget.button.title.medium.bold'); + static TextStyle? get widgetButtonTitleMediumThin => Styles().textStyles?.getTextStyle('widget.button.title.medium.thin'); + static TextStyle? get widgetButtonTitleMediumUnderline => Styles().textStyles?.getTextStyle('widget.button.title.medium.underline'); + static TextStyle? get widgetButtonTitleMediumLightUnderline => Styles().textStyles?.getTextStyle('widget.button.title.medium.light.underline'); + static TextStyle? get widgetButtonTitleEnabled => Styles().textStyles?.getTextStyle('widget.button.title.enabled'); + static TextStyle? get widgetButtonTitleDisabled => Styles().textStyles?.getTextStyle('widget.button.title.disabled'); + static TextStyle? get widgetButtonTitleSmallUnderline => Styles().textStyles?.getTextStyle('widget.button.title.small.underline'); + static TextStyle? get widgetButtonDescriptionSmall => Styles().textStyles?.getTextStyle('widget.button.description.small'); + static TextStyle? get widgetButtonDescriptionTiny => Styles().textStyles?.getTextStyle('widget.button.description.tiny'); + static TextStyle? get widgetColourfulButtonTitleTitleRegular => Styles().textStyles?.getTextStyle('widget.colourful_button.title.title.regular'); + static TextStyle? get widgetColourfulButtonTitleTitleAccent => Styles().textStyles?.getTextStyle('widget.colourful_button.title.title.accent'); + static TextStyle? get widgetInputFieldTextMedium => Styles().textStyles?.getTextStyle('widget.input_field.text.medium'); + static TextStyle? get widgetInputFieldTextRegular => Styles().textStyles?.getTextStyle('widget.input_field.text.regular'); + static TextStyle? get widgetDialogButtonClose => Styles().textStyles?.getTextStyle('widget.dialog.button.close'); + static TextStyle? get widgetDialogMessageMedium => Styles().textStyles?.getTextStyle('widget.dialog.message.medium'); + static TextStyle? get widgetDialogMessageMediumThin => Styles().textStyles?.getTextStyle('widget.dialog.message.medium.thin'); + static TextStyle? get widgetDialogMessageRegularBold => Styles().textStyles?.getTextStyle('widget.dialog.message.regular.bold'); + static TextStyle? get widgetDialogMessageLarge => Styles().textStyles?.getTextStyle('widget.dialog.message.large'); + static TextStyle? get widgetDialogMessageLargeBold => Styles().textStyles?.getTextStyle('widget.dialog.message.large.bold'); + static TextStyle? get widgetDialogMessageDarkLargeBold => Styles().textStyles?.getTextStyle('widget.dialog.message.dark.large.bold'); + static TextStyle? get widgetDialogMessageDarkLarge => Styles().textStyles?.getTextStyle('widget.dialog.message.dark.large'); + static TextStyle? get widgetDialogMessageDarkMedium => Styles().textStyles?.getTextStyle('widget.dialog.message.dark.medium'); + static TextStyle? get widgetCardTitleLarge => Styles().textStyles?.getTextStyle('widget.card.title.large'); + static TextStyle? get widgetCardTitleMedium => Styles().textStyles?.getTextStyle('widget.card.title.medium'); + static TextStyle? get widgetCardTitleRegularBold => Styles().textStyles?.getTextStyle('widget.card.title.regular.bold'); + static TextStyle? get widgetCardTitleSmall => Styles().textStyles?.getTextStyle('widget.card.title.small'); + static TextStyle? get widgetCardTitleSmallBold => Styles().textStyles?.getTextStyle('widget.card.title.small.bold'); + static TextStyle? get widgetCardTitleTiny => Styles().textStyles?.getTextStyle('widget.card.title.tiny'); + static TextStyle? get widgetCardTitleTinyBold => Styles().textStyles?.getTextStyle('widget.card.title.tiny.bold'); + static TextStyle? get widgetCardDetailRegularVariant => Styles().textStyles?.getTextStyle('widget.card.detail.regular_variant'); + static TextStyle? get widgetCardDetailRegular => Styles().textStyles?.getTextStyle('widget.card.detail.regular'); + static TextStyle? get widgetCardDetailRegularBold => Styles().textStyles?.getTextStyle('widget.card.detail.regular.bold'); + static TextStyle? get widgetCardDetailMedium => Styles().textStyles?.getTextStyle('widget.card.detail.medium'); + static TextStyle? get widgetCardDetailSmallVariant => Styles().textStyles?.getTextStyle('widget.card.detail.small_variant'); + static TextStyle? get widgetCardDetailSmallVariant2 => Styles().textStyles?.getTextStyle('widget.card.detail.small_variant2'); + static TextStyle? get widgetCardDetailSmall => Styles().textStyles?.getTextStyle('widget.card.detail.small'); + static TextStyle? get widgetCardDetailTiny => Styles().textStyles?.getTextStyle('widget.card.detail.tiny'); + static TextStyle? get widgetCardDetailTinyBold => Styles().textStyles?.getTextStyle('widget.card.detail.tiny.bold'); + static TextStyle? get widgetCardDetailTinyVariant2 => Styles().textStyles?.getTextStyle('widget.card.detail.tiny_variant2'); +} + +class AppImages { + static Widget? get home => Styles().images?.getImage('home'); + static Widget? get bug => Styles().images?.getImage('bug'); + static Widget? get notification => Styles().images?.getImage('notification'); + static Widget? get profile => Styles().images?.getImage('profile'); + static Widget? get chevronUp => Styles().images?.getImage('chevron-up'); + static Widget? get chevronDown => Styles().images?.getImage('chevron-down'); + static Widget? get chevronLeft => Styles().images?.getImage('chevron-left'); + static Widget? get chevronRight => Styles().images?.getImage('chevron-right'); + static Widget? get close => Styles().images?.getImage('close'); +} From 2a588b8226a803dbd34f34a615bb299d64b04a24 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 31 Mar 2023 17:13:58 -0500 Subject: [PATCH 019/177] initial passkey credential manager implementation --- android/build.gradle | 14 +- android/src/main/AndroidManifest.xml | 2 + .../rokwire_plugin/GeofenceMonitor.java | 17 +- .../rokwire/rokwire_plugin/Passkey.kt | 118 +++++ .../rokwire/rokwire_plugin/RokwirePlugin.java | 121 +++-- example/pubspec.lock | 472 ++++++++++++------ lib/rokwire_plugin.dart | 22 + lib/service/auth2.dart | 50 +- pubspec.yaml | 10 +- 9 files changed, 589 insertions(+), 237 deletions(-) create mode 100644 android/src/main/java/edu/illinois/rokwire/rokwire_plugin/Passkey.kt 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/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 971a5481a..86fd58e6c 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,5 @@ + + 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/Passkey.kt b/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/Passkey.kt new file mode 100644 index 000000000..eb82c8cd6 --- /dev/null +++ b/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/Passkey.kt @@ -0,0 +1,118 @@ +package edu.illinois.rokwire.rokwire_plugin; + +import android.app.Activity +import android.util.Log +import androidx.credentials.* +import androidx.credentials.exceptions.* +import io.flutter.plugins.firebase.messaging.ContextHolder.getApplicationContext +import kotlinx.coroutines.* + +class PasskeyManager(private val activity: Activity?) { + private val tag = "PasskeyManager" + private val credentialManager: CredentialManager = CredentialManager.create(getApplicationContext()) + private val scope = CoroutineScope(Dispatchers.Default) + + fun login(requestJson: String?, preferImmediatelyAvailableCredentials: Boolean?) { + if (requestJson == null) { + notifyGetPasskeyFailed("MISSING_REQUEST") + return + } + if (activity == null) { + notifyGetPasskeyFailed("NULL_ACTIVITY") + return + } + + // Retrieves the user's saved password for your app from their + // password provider. + val getPasswordOption = GetPasswordOption() + + // Get passkeys from the user's public key credential provider. + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = requestJson, + preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials ?: true + ) + + val getCredRequest = GetCredentialRequest( + listOf(getPasswordOption, getPublicKeyCredentialOption) + ) + + scope.launch { + try { + val result = credentialManager.getCredential( + request = getCredRequest, + activity = activity, + ) + handleSignIn(result) + } catch (e : GetCredentialException) { + Log.e(tag, e.toString()) + notifyGetPasskeyFailed(e.type) + } + } + } + + private fun handleSignIn(result: GetCredentialResponse) { + // Handle the successfully returned credential. + when (val credential = result.credential) { + is PublicKeyCredential -> { + Log.e(tag, "Credential found: " + credential.authenticationResponseJson) + RokwirePlugin.getInstance().notifyPasskeyResult("onGetPasskeySuccess", credential.authenticationResponseJson) + } +// is PasswordCredential -> { +// val username = credential.id +// val password = credential.password +// passwordAuthenticateWithServer(username, password) +// } + else -> { + // Catch any unrecognized credential type here. + notifyGetPasskeyFailed("INVALID_CREDENTIAL_TYPE") + } + } + } + + private fun notifyGetPasskeyFailed(error: String) { + Log.e(tag, error) + RokwirePlugin.getInstance().notifyPasskeyResult("onGetPasskeyFailed", error) + } + + fun createPasskey(requestJson: String?, preferImmediatelyAvailableCredentials: Boolean?) { + if (requestJson == null) { + notifyCreatePasskeyFailed("MISSING_REQUEST") + return + } + if (activity == null) { + notifyGetPasskeyFailed("NULL_ACTIVITY") + return + } + + val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( + // Contains the request in JSON format. Uses the standard WebAuthn + // web JSON spec. + requestJson = requestJson, + // Defines whether you prefer to use only immediately available credentials, + // not hybrid credentials, to fulfill this request. This value is false + // by default. + preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials ?: true, + ) + + // Execute CreateCredentialRequest asynchronously to register credentials + // for a user account. Handle success and failure cases with the result and + // exceptions, respectively. + scope.launch { + try { + val result = credentialManager.createCredential( + request = createPublicKeyCredentialRequest, + activity = activity, + ) + RokwirePlugin.getInstance().notifyPasskeyResult("onCreatePasskeySuccess", result.data.toString()) + } catch (e : CreateCredentialException) { + Log.e(tag, e.toString()) + notifyCreatePasskeyFailed(e.type) + } + } + } + + private fun notifyCreatePasskeyFailed(error: String) { + Log.e(tag, error) + RokwirePlugin.getInstance().notifyPasskeyResult("onCreatePasskeyFailed", error) + } +} 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..5373abb5e 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,51 +132,75 @@ 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; + case "getPasskey": + String requestJson = call.argument("requestJson"); + Boolean preferImmediatelyAvailableCredentials = call.argument("preferImmediatelyAvailableCredentials"); + PasskeyManager manager = new PasskeyManager(getActivity()); + manager.login(requestJson, preferImmediatelyAvailableCredentials); + result.success(null); + break; + case "createPasskey": + requestJson = call.argument("requestJson"); + preferImmediatelyAvailableCredentials = call.argument("preferImmediatelyAvailableCredentials"); + manager = new PasskeyManager(getActivity()); + manager.createPasskey(requestJson, preferImmediatelyAvailableCredentials); + result.success(null); + 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)); } } + public void notifyPasskeyResult(String event, Object arguments) { + Activity activity = getActivity(); + if ((activity != null) && (_channel != null)) { + activity.runOnUiThread(() -> _channel.invokeMethod(String.format("passkey.%s", event), arguments)); + } + } + // PluginRegistry.ActivityResultListener @Override @@ -185,7 +211,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 +238,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 +252,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 +365,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/example/pubspec.lock b/example/pubspec.lock index 7d8a2dc61..5cf27d468 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,273 +5,312 @@ packages: dependency: transitive description: name: _flutterfire_internals - url: "https://pub.dartlang.org" + sha256: "2f428053492f92303e42c9afa8e3a78ad1886760e7b594e2b5a6b6ee47376360" + url: "https://pub.dev" source: hosted version: "1.0.2" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + url: "https://pub.dev" source: hosted version: "2.3.0" asn1lib: dependency: transitive description: name: asn1lib - url: "https://pub.dartlang.org" + sha256: "35e6681078cf198c5d5e83fdc5fcf4ad39a5f29564c181b370b5c29361f28660" + url: "https://pub.dev" source: hosted version: "1.0.3" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" chewie: dependency: transitive description: name: chewie - url: "https://pub.dartlang.org" + sha256: "7343cffa40ce9d42b37f2869f17b5e5fa681fb329d8385869ce279e03193e2b7" + url: "https://pub.dev" source: hosted version: "1.1.0" chewie_audio: dependency: transitive description: name: chewie_audio - url: "https://pub.dartlang.org" + sha256: f92bb4364ced21318e1a7c0eddaf249a7554e5cf27f869d58c44d926abd292a7 + url: "https://pub.dev" source: hosted version: "1.3.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - url: "https://pub.dartlang.org" + sha256: d023142c18c28b2610c23c196e829c96976569cc2aa2f8e45328ae8a64c428d1 + url: "https://pub.dev" source: hosted version: "5.7.7" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - url: "https://pub.dartlang.org" + sha256: "3d7d4fa8c1dc5a1f7cb33985ae0ab9924d33d76d4959fe26aed84b7d282887e3" + url: "https://pub.dev" source: hosted version: "2.8.10" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" connectivity: dependency: transitive description: name: connectivity - url: "https://pub.dartlang.org" + sha256: a8e91263cf3e25fb5cc95e19dfde4999e32a648ac3b9e8a558a28165731678f8 + url: "https://pub.dev" source: hosted version: "3.0.6" connectivity_for_web: dependency: transitive description: name: connectivity_for_web - url: "https://pub.dartlang.org" + sha256: "01a390c1d5adc2ed1fa1f52d120c07fe9fd01166a93f965a832fd6cfc0ea6482" + url: "https://pub.dev" source: hosted version: "0.4.0+1" connectivity_macos: dependency: transitive description: name: connectivity_macos - url: "https://pub.dartlang.org" + sha256: "51ae08d5162eca9669b9d8951ed83ce19c5355a81149f94e4dee2740beb93628" + url: "https://pub.dev" source: hosted version: "0.2.1+2" connectivity_platform_interface: dependency: transitive description: name: connectivity_platform_interface - url: "https://pub.dartlang.org" + sha256: "2d82e942df9d49f29a24bb07fb5ce085d4a53e47818c62364d2b6deb9e0d7a8e" + url: "https://pub.dev" source: hosted version: "2.0.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + url: "https://pub.dev" source: hosted version: "3.0.1" cookie_jar: dependency: transitive description: name: cookie_jar - url: "https://pub.dartlang.org" + sha256: d1cc6516a190ba667941f722b6365d202caff3dacb38de24268b8d6ff1ec8a1d + url: "https://pub.dev" source: hosted version: "3.0.1" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: "552ffd2f851d4314958e6265452af1891959e00cd32b6d17452c5b836e27a0fa" + url: "https://pub.dev" source: hosted version: "0.3.2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + url: "https://pub.dev" source: hosted version: "3.0.1" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: d1cd6d6e4b39a4ad295204722b8608f19981677b223f3e942c0b5a33dcf57ec0 + url: "https://pub.dev" source: hosted version: "0.17.1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: "1989d917fbe8e6b39806207df5a3fdd3d816cbd090fac2ce26fb45e9a71476e5" + url: "https://pub.dev" source: hosted version: "1.0.4" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "4f814fc7e73057f78f307a6c4714fe2ffb4bdb994ab1970540a068ec4d5a45be" + url: "https://pub.dev" source: hosted version: "0.7.3" device_calendar: dependency: transitive description: name: device_calendar - url: "https://pub.dartlang.org" + sha256: "5a1ce7887b4ffbaf3743078c8314dede5e694cddd69bab43f35ce815c5d82a7d" + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.3.1" device_info: dependency: transitive description: name: device_info - url: "https://pub.dartlang.org" + sha256: f4a8156cb7b7480d969cb734907d18b333c8f0bc0b1ad0b342cdcecf30d62c48 + url: "https://pub.dev" source: hosted version: "2.0.3" device_info_platform_interface: dependency: transitive description: name: device_info_platform_interface - url: "https://pub.dartlang.org" + sha256: b148e0bf9640145d09a4f8dea96614076f889e7f7f8b5ecab1c7e5c2dbc73c1b + url: "https://pub.dev" source: hosted version: "2.0.1" encrypt: dependency: transitive description: name: encrypt - url: "https://pub.dartlang.org" + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + url: "https://pub.dev" source: hosted version: "5.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: "35d0f481d939de0d640b3db9a7aa36a52cd22054a798a73b4f50bdad5ce12678" + url: "https://pub.dev" source: hosted version: "1.1.2" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + url: "https://pub.dev" source: hosted version: "6.1.2" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + sha256: "9089e055dbd574b2f75e01506f06c9741b28247ea303bc15c2a8f969931f8553" + url: "https://pub.dev" source: hosted version: "1.11.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + sha256: b51257a8b4388565cd66062d727d3e60067b5f5cc3390eb0ecd20b8f97741bdb + url: "https://pub.dev" source: hosted version: "4.5.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + sha256: "839f1b48032a61962792cea1225fae030d4f27163867f181d6d2072dd40acbee" + url: "https://pub.dev" source: hosted version: "1.7.3" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - url: "https://pub.dartlang.org" + sha256: "636ba46935a9199c503f318dc0f270ccaeac519c9585c242d376d530e38a0fcb" + url: "https://pub.dev" source: hosted version: "2.4.5" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - url: "https://pub.dartlang.org" + sha256: b5aa9a2c3967019b3570fe9e12480033b2951c270b66edfd351a64a6f41b2456 + url: "https://pub.dev" source: hosted version: "3.1.12" firebase_messaging: dependency: transitive description: name: firebase_messaging - url: "https://pub.dartlang.org" + sha256: "6126f8f6f3bba6e3ffff05300711615c388eb31e0a7431cebe5514f6572cbdff" + url: "https://pub.dev" source: hosted version: "13.1.0" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - url: "https://pub.dartlang.org" + sha256: "71f120e8b405a172cb0088a2cd62fd8b77be0f0c942a5bda411b80268c1702f8" + url: "https://pub.dev" source: hosted version: "4.2.0" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - url: "https://pub.dartlang.org" + sha256: b7c5e019c4ecf3a512c4a0f00a4a9c1f260eb11872380a91f35983387488ce94 + url: "https://pub.dev" source: hosted version: "3.2.0" flutter: @@ -283,77 +322,88 @@ packages: dependency: transitive description: name: flutter_exif_rotation - url: "https://pub.dartlang.org" + sha256: "642841fa8dbedb0ac7b7f005dd24a606ce237ab2de0fedef7b3419d58b97f2a7" + url: "https://pub.dev" source: hosted version: "0.5.1" flutter_html: dependency: transitive description: name: flutter_html - url: "https://pub.dartlang.org" + sha256: ccb810fcabfce3a7ffaca46e458323915ac7e7fc59082c7357ff848972c02230 + url: "https://pub.dev" source: hosted version: "2.2.1" flutter_layout_grid: dependency: transitive description: name: flutter_layout_grid - url: "https://pub.dartlang.org" + sha256: ab8848620feddfea46f821132eaa75f62301350888af03da00ba4b09cafa5244 + url: "https://pub.dev" source: hosted version: "1.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" source: hosted version: "1.0.4" flutter_local_notifications: dependency: transitive description: name: flutter_local_notifications - url: "https://pub.dartlang.org" + sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 + url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "12.0.4" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - url: "https://pub.dartlang.org" + sha256: "6af440e3962eeab8459602c309d7d4ab9e62f05d5cfe58195a28f846a0b5d523" + url: "https://pub.dev" source: hosted version: "1.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - url: "https://pub.dartlang.org" + sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + url: "https://pub.dev" source: hosted version: "6.0.0" flutter_math_fork: dependency: transitive description: name: flutter_math_fork - url: "https://pub.dartlang.org" + sha256: a34205227e1a1d040a56e74a5e2e56e9397ea930540a9373bd0b3e2e4f122017 + url: "https://pub.dev" source: hosted version: "0.5.0" flutter_native_timezone: dependency: transitive description: name: flutter_native_timezone - url: "https://pub.dartlang.org" + sha256: ed7bfb982f036243de1c068e269182a877100c994f05143c8b26a325e28c1b02 + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "5c574d21b98ec92adab05ead10afd2b13ff5856c7ca79696edb338a9dd8ed387" + url: "https://pub.dev" source: hosted version: "2.0.5" flutter_svg: dependency: transitive description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: cbf529d563dd910a249041bde2a0dfc3cf62280fcc459b85f3d614b2ab73abb3 + url: "https://pub.dev" source: hosted version: "0.23.0+1" flutter_test: @@ -370,315 +420,360 @@ packages: dependency: transitive description: name: fluttertoast - url: "https://pub.dartlang.org" + sha256: b3ae793108ad2a7e8f2fea91ca5bb2ba7ef03621c4d9ed7a92fe3391da64c000 + url: "https://pub.dev" source: hosted version: "8.0.8" font_awesome_flutter: dependency: transitive description: name: font_awesome_flutter - url: "https://pub.dartlang.org" + sha256: "8f0ce0204bd0cafa8631536a6f3b7d05d9c16cdc6e8bd807843f917027c5cefd" + url: "https://pub.dev" source: hosted version: "10.2.1" gallery_saver: dependency: transitive description: name: gallery_saver - url: "https://pub.dartlang.org" + sha256: df8b7e207ca12d64c71e0710a7ee3bc48aa7206d51cc720716fedb1543a66712 + url: "https://pub.dev" source: hosted version: "2.3.2" geolocator: dependency: transitive description: name: geolocator - url: "https://pub.dartlang.org" + sha256: "85bd80050ced6fb1aa2e76aaed43e01d799e3633aa49be9f28d1df7438d2c145" + url: "https://pub.dev" source: hosted version: "8.0.3" geolocator_android: dependency: transitive description: name: geolocator_android - url: "https://pub.dartlang.org" + sha256: "1902d8f1309e45a4572c8485149852ce3b313c5135953c05a6a85e3a2e001089" + url: "https://pub.dev" source: hosted version: "3.0.2" geolocator_apple: dependency: transitive description: name: geolocator_apple - url: "https://pub.dartlang.org" + sha256: e89df2697214f7da67d6b97a41dfc46cec7ff1588766d22d72de1acdc15929bc + url: "https://pub.dev" source: hosted version: "2.0.1" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - url: "https://pub.dartlang.org" + sha256: aea90b1194295e89e221f5d4a75df682ed2326cbef72f63c81ac5b5bb9d72e6a + url: "https://pub.dev" source: hosted version: "4.0.1" geolocator_web: dependency: transitive description: name: geolocator_web - url: "https://pub.dartlang.org" + sha256: c1e068aaa483ec4464376c81038d8c811a732da8c0b89e8014c69e40715a961d + url: "https://pub.dev" source: hosted version: "2.1.4" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: bfef906cbd4e78ef49ae511d9074aebd1d2251482ef601a280973e8b58b51bbf + url: "https://pub.dev" source: hosted version: "0.15.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "2ed163531e071c2c6b7c659635112f24cb64ecbebf6af46b550d536c0b1aa112" + url: "https://pub.dev" source: hosted version: "0.13.4" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + url: "https://pub.dev" source: hosted version: "4.0.0" image_picker: dependency: transitive description: name: image_picker - url: "https://pub.dartlang.org" + sha256: f3712cd190227fb92e0960cb0ce22928ba042c7183b16864ade83b259adf8ea6 + url: "https://pub.dev" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + sha256: "2d4c2634731ced2debc2a36273351dd1cb68080d505c12c472b0dc0657cd853c" + url: "https://pub.dev" source: hosted version: "0.8.5+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + sha256: "1f21a49e1dc82908ac44c2b988ee96b7383d47dc528151c75bac9c67a3bfb9f5" + url: "https://pub.dev" source: hosted version: "2.1.5" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + sha256: aee8b90c2fb018e18cbb437da661d0531a5346d49cab2d5c4a65ebaf026bbd33 + url: "https://pub.dev" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + sha256: dd0d3344bd1ef891e1ceedbe4d4374a246570e065b0fd272ba7125938a29ee54 + url: "https://pub.dev" source: hosted version: "2.4.3" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" source: hosted version: "1.0.1" logger: dependency: transitive description: name: logger - url: "https://pub.dartlang.org" + sha256: "5076f09225f91dc49289a4ccb92df2eeea9ea01cf7c26d49b3a1f04c6a49eec1" + url: "https://pub.dev" source: hosted version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mime_type: dependency: transitive description: name: mime_type - url: "https://pub.dartlang.org" + sha256: "2ad6e67d3d2de9ac0f8ef5352d998fd103cb21351ae8c02fb0c78b079b37d275" + url: "https://pub.dev" source: hosted version: "1.0.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" numerus: dependency: transitive description: name: numerus - url: "https://pub.dartlang.org" + sha256: "0087ef729d63b96cb347a9c44b9c592f21cecb3605b415bbd18710aef80ce5cb" + url: "https://pub.dev" source: hosted version: "1.1.1" package_info: dependency: transitive description: name: package_info - url: "https://pub.dartlang.org" + sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" + url: "https://pub.dev" source: hosted version: "2.0.2" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + sha256: "3bdd251dae9ffaef944450b73f168610db7e968e7b20daf0c3907f8b4aafc8a2" + url: "https://pub.dev" source: hosted version: "0.5.1+1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: ee5c47c1058ad66b4a41746ec3996af9593d0858872807bcd64ac118f0700337 + url: "https://pub.dev" source: hosted version: "0.2.1" path_provider: dependency: transitive description: name: path_provider - url: "https://pub.dartlang.org" + sha256: db1cf6c3f906f50d52c18400be575f75ed620f8717f9b6d11263edd95080dfc6 + url: "https://pub.dev" source: hosted version: "2.0.8" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: c69109bae02c6116bd8ac81319b13eb73dfae02ef74690d2a1a98c1ddd3aaefc + url: "https://pub.dev" source: hosted version: "2.0.11" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + sha256: "038d0141ff5d08c60ed071eee2758b68c50c42a1c10066a1fb6c28ab32fac84c" + url: "https://pub.dev" source: hosted version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: "1e109f4df28bd95eab71e323008b53d19c4d633bc1ab05b577518773474e9621" + url: "https://pub.dev" source: hosted version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + sha256: "0adeb313e1f2c3fc52baeeee59b0fe9c2d1f7da56fd96a9234e1702ec653a453" + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: "3dc0d51b07f85fec3746d9f4e8d31c73bb173cafa2e763f03f8df2e8d1878882" + url: "https://pub.dev" source: hosted version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: "366ad4e3541ea707f859e7148d4d5aba67d589d7936cee04a05c464a277eeb27" + url: "https://pub.dev" source: hosted version: "2.0.5" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "1a914995d4ef10c94ff183528c120d35ed43b5eaa8713fc6766a9be4570782e2" + url: "https://pub.dev" source: hosted version: "4.4.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "075f927ebbab4262ace8d0b283929ac5410c0ac4e7fc123c76429564facfb757" + url: "https://pub.dev" source: hosted version: "2.1.2" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: "21cc8c830d228dd84c84a9139394702d766622bacd3ea7a2bdcb1f00b280e9a4" + url: "https://pub.dev" source: hosted version: "3.5.0" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" provider: dependency: transitive description: name: provider - url: "https://pub.dartlang.org" + sha256: "7896193cf752c40ba7f7732a95264319a787871e5d628225357f5c909182bc06" + url: "https://pub.dev" source: hosted version: "6.0.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: "616b691d1c8f5c53b7b39ce3542f6a25308d7900bf689d0210e72a644a10387e" + url: "https://pub.dev" source: hosted version: "3.0.1+1" rokwire_plugin: @@ -692,56 +787,64 @@ packages: dependency: transitive description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: eb7cfb572af1ccc8b81f66bfd525ae0e8d196574874105b5c27db64bef9f155a + url: "https://pub.dev" source: hosted version: "2.0.12" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + sha256: eb5f8ca2adb3303e0949971b8bc6408c67b5000502b838ee6cb0ef96cd29e708 + url: "https://pub.dev" source: hosted version: "2.0.10" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + sha256: "2b7319dbccef93d2bc70d03f810a9edb95550f4dcb3fb27e64cae18291b80b28" + url: "https://pub.dev" source: hosted version: "2.0.9" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + sha256: "65e86adba03ba1ecd4a2f3e5d986eac27dbcf5804e0d5e03e029dbf63b5ed3a7" + url: "https://pub.dev" source: hosted version: "2.0.4" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + sha256: d8c36fedba519cba905d30833b818cde3d52d4e9c7438da4664bd523468bb93f + url: "https://pub.dev" source: hosted version: "2.0.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: "992f0fdc46d0a3c0ac2e5859f2de0e577bbe51f78a77ee8f357cbe626a2ad32d" + url: "https://pub.dev" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: "09da0185028a227d51721cade7a3cbd5cc5f163a19593266f2acba87f729bf9c" + url: "https://pub.dev" source: hosted version: "2.0.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: a16f85cbad4e23daf83cd188fc53ca19a9b025743e8e2983d9d89ec4c383d6e6 + url: "https://pub.dev" source: hosted version: "2.0.4" sky_engine: @@ -753,303 +856,346 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sprintf: dependency: transitive description: name: sprintf - url: "https://pub.dartlang.org" + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "0128af224bade0990f4ce55f2ef93a7054aa14d28768f17d4d2b21f6b1185523" + url: "https://pub.dev" source: hosted version: "2.1.0" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: "0c7785befac2b5c40fc66b485be2f35396246a6fd6ccb89bb22614ddfb3d54a7" + url: "https://pub.dev" source: hosted version: "2.3.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "271977ff1e9e82ceefb4f08424b8839f577c1852e0726b5ce855311b46d3ef83" + url: "https://pub.dev" source: hosted version: "3.0.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964" + url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.1" tuple: dependency: transitive description: name: tuple - url: "https://pub.dartlang.org" + sha256: fe3ae4f0dca3f9aac0888e2e0d117b642ce283a82d7017b54136290c0a3b0dd3 + url: "https://pub.dev" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" uni_links: dependency: transitive description: name: uni_links - url: "https://pub.dartlang.org" + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" source: hosted version: "0.5.1" uni_links_platform_interface: dependency: transitive description: name: uni_links_platform_interface - url: "https://pub.dartlang.org" + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" source: hosted version: "1.0.0" uni_links_web: dependency: transitive description: name: uni_links_web - url: "https://pub.dartlang.org" + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" source: hosted version: "0.1.0" url_launcher: dependency: transitive description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "236e5ad4df2dabb79ce5d0f1aafb4cb70c0ad6dc93f88e610ad31c8fef74f477" + url: "https://pub.dev" source: hosted version: "6.0.18" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "2ec66333093cf5dd5103f089c3a7842fcd5581023a571839d3d176ff10244582" + url: "https://pub.dev" source: hosted version: "6.0.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "0d6b0b9dce05986462e8ba08e51f1909ac63e2e59ebde829c46ad66423f03dee" + url: "https://pub.dev" source: hosted version: "6.0.14" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: ae3c5ce30a1ba0a69c3f8803b23450703cf915575ad591857df98d9c15c11018 + url: "https://pub.dev" source: hosted version: "2.0.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "8fd9ae3ab5e0f96cea7dd66c4ea65e39e3477067f4997c1ec8225d553e8bb8ea" + url: "https://pub.dev" source: hosted version: "2.0.3" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "1b9c4dab07794498b83b5f938e26b20f68c3b460a3015b0307f9541cb34ef93d" + url: "https://pub.dev" source: hosted version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "88de707cba8e0c1c678ed79274298c4737f7f24addd12e786fbf0597de085850" + url: "https://pub.dev" source: hosted version: "2.0.6" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: "4e24aac2a2960fb9a70a07992e1ba69cb99fbcee48fdf17abe280ce867bfcea2" + url: "https://pub.dev" source: hosted version: "2.0.2" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + sha256: "00ba1241ff12e77d8059eeb1f102b35235df01661a6110afd165ab52a0fc7714" + url: "https://pub.dev" source: hosted version: "3.0.5" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + sha256: "9550ef42803203cd729e34313142b7a1ee2a90b395105a7e38e32a9139fa28ad" + url: "https://pub.dev" source: hosted version: "2.2.18" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + sha256: "3be61312f2c72f1f72234a37c7fc7cb6e75d8f9be8f52f3cf8e03ea53f50e148" + url: "https://pub.dev" source: hosted version: "2.3.0" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + sha256: e49d39992fe2c38ec5e7f420b74d0d4c288bee195af61ca9d75ecc19b5025c41 + url: "https://pub.dev" source: hosted version: "2.3.0" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + sha256: ac0b3515d47244cf59aea6cdb009b745d5299b12d248ae123608ec533d483cb6 + url: "https://pub.dev" source: hosted version: "5.1.0" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + sha256: "48de7d5ff19fb474082182c33704c0210c395521ff81e54fa255c7719056fc81" + url: "https://pub.dev" source: hosted version: "2.0.7" wakelock: dependency: transitive description: name: wakelock - url: "https://pub.dartlang.org" + sha256: da22c0789e1f849bec43688a52f2290f4e66268056f7cd77cb71245aef4917a0 + url: "https://pub.dev" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" + url: "https://pub.dev" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + sha256: "108b1b73711f1664ee462e73af34a9286ff496e27d4d8371e2fb4da8fde4cdac" + url: "https://pub.dev" source: hosted version: "0.2.0" webview_flutter: dependency: transitive description: name: webview_flutter - url: "https://pub.dartlang.org" + sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" + url: "https://pub.dev" source: hosted version: "2.8.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - url: "https://pub.dartlang.org" + sha256: "6d796e709d14da8496ee7c750765fad0e860c57a60844bb50f5b93a9ee5ceeab" + url: "https://pub.dev" source: hosted version: "2.8.3" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - url: "https://pub.dartlang.org" + sha256: "6144d750f56ae63fdaad10ff09e0f762142beabde4fefdc2d32564f75572d905" + url: "https://pub.dev" source: hosted version: "1.8.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - url: "https://pub.dartlang.org" + sha256: "87c56a34f69867f3aaf0b66df5399a4fc18898b1df8c738b835e62b73059d66f" + url: "https://pub.dev" source: hosted version: "2.7.1" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: "0b2350f19ef62ccc5d594ea92d9107230210ae0b37459686687b70f41b911e12" + url: "https://pub.dev" source: hosted version: "2.3.6" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: "11541eedefbcaec9de35aa82650b695297ce668662bbd6e3911a7fabdbde589f" + url: "https://pub.dev" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: baa23bcba1ba4ce4b22c0c7a1d9c861e7015cb5169512676da0b85138e72840c + url: "https://pub.dev" source: hosted version: "5.3.1" sdks: - dart: ">=2.18.0-0 <3.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.3.0-0" diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index 573469d77..a4beb2f96 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -4,6 +4,7 @@ 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'; class RokwirePlugin { @@ -80,6 +81,24 @@ class RokwirePlugin { return false; } + static Future getPasskey(String requestJson, {bool preferImmediatelyAvailableCredentials = true}) async { + try { + return await _channel.invokeMethod('getPasskey', { + 'requestJson': requestJson, + 'preferImmediatelyAvailableCredentials': preferImmediatelyAvailableCredentials + }); + } catch(e) { debugPrint(e.toString()); } + } + + static Future createPasskey(String requestJson, {bool preferImmediatelyAvailableCredentials = true}) async { + try { + return await _channel.invokeMethod('createPasskey', { + 'requestJson': requestJson, + 'preferImmediatelyAvailableCredentials': preferImmediatelyAvailableCredentials + }); + } catch(e) { debugPrint(e.toString()); } + } + // Compound APIs static Future locationServices(String method, [dynamic arguments]) async { @@ -114,5 +133,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/auth2.dart b/lib/service/auth2.dart index 9c1df4799..f51f70ae1 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -19,18 +19,22 @@ import 'package:url_launcher/url_launcher_string.dart'; 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 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 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'; @@ -1207,6 +1211,28 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Log.d(message, lineLength: 512); // max line length of VS Code Debug Console } + // 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 { diff --git a/pubspec.yaml b/pubspec.yaml index e9e6562bb..4042f0c0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: encrypt: ^5.0.1 intl: ^0.17.0 http: ^0.13.3 - timezone: ^0.8.0 + timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 geolocator: ^8.0.0 cookie_jar: ^3.0.1 @@ -31,14 +31,14 @@ dependencies: package_info: ^2.0.2 device_info: ^2.0.3 url_launcher: ^6.0.10 - sprintf: ^6.0.0 - flutter_local_notifications: ^10.0.0 + sprintf: ^7.0.0 + flutter_local_notifications: ^12.0.4 sqflite: ^2.1.0 - device_calendar: ^4.0.1 + device_calendar: ^4.3.1 image_picker: ^0.8.5+3 mime_type: ^1.0.0 uuid: ^3.0.5 - flutter_html: ^2.2.0 + flutter_html: ^2.2.1 webview_flutter: ^2.0.13 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.2.1 From 9cc2f9448e3c81153f62f76417400895b2f9e821 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 4 Apr 2023 15:32:34 -0500 Subject: [PATCH 020/177] fix models, cleanup duplicate code --- lib/gen/styles.dart | 2 +- lib/ui/widgets/section_header.dart | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index 1116cabbe..505f79227 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -12,7 +12,7 @@ class AppColors { static Color? get textAccent => Styles().colors?.getColor('textAccent'); static Color? get textDark => Styles().colors?.getColor('textDark'); static Color? get textMedium => Styles().colors?.getColor('textMedium'); - static Color? get textLight => Styles().colors?.getColor('textLight'); + static Color? get textLight => Styles().colors?.getColor('textLight'); static Color? get textDisabled => Styles().colors?.getColor('textDisabled'); static Color? get iconPrimary => Styles().colors?.getColor('iconPrimary'); static Color? get iconLight => Styles().colors?.getColor('iconLight'); diff --git a/lib/ui/widgets/section_header.dart b/lib/ui/widgets/section_header.dart index 6cb169497..8a00d25ea 100644 --- a/lib/ui/widgets/section_header.dart +++ b/lib/ui/widgets/section_header.dart @@ -57,6 +57,7 @@ class SectionSlantHeader extends StatelessWidget { final EdgeInsetsGeometry rightIconPadding; final Widget? headerWidget; + final Widget? headerChild; final List? children; final EdgeInsetsGeometry childrenPadding; final CrossAxisAlignment childrenAlignment; @@ -101,6 +102,7 @@ class SectionSlantHeader extends StatelessWidget { this.rightIconPadding = const EdgeInsets.only(left: 16, right: 16), this.headerWidget, + this.headerChild, this.children, this.childrenPadding = const EdgeInsets.all(16), this.childrenAlignment = CrossAxisAlignment.center, @@ -120,6 +122,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)) { From f7149fad7b51bda5f428ac9ea911b30434bec705 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 11 Apr 2023 17:10:04 -0500 Subject: [PATCH 021/177] initial chart implementation (in progress) --- lib/service/app_datetime.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index fed8043e6..86f49d69c 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -158,7 +158,7 @@ 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 considerSettingsDisplayTime = true, bool includeAtSuffix = false, bool multiLine = false}) { if (format != null) { DateTime dateTimeToCompare = _getDateTimeToCompare(dateTimeUtc: dateTimeUtc, considerSettingsDisplayTime: considerSettingsDisplayTime)!; return formatDateTime(dateTimeToCompare, format: format, ignoreTimeZone: false, showTzSuffix: true) ?? ''; @@ -166,7 +166,7 @@ class AppDateTime with Service { String? timePrefix = getDisplayDay(dateTimeUtc: dateTimeUtc, allDay: allDay, considerSettingsDisplayTime: considerSettingsDisplayTime, includeAtSuffix: includeAtSuffix); String? timeSuffix = getDisplayTime(dateTimeUtc: dateTimeUtc, allDay: allDay, considerSettingsDisplayTime: considerSettingsDisplayTime); - return '$timePrefix, $timeSuffix'; + return '$timePrefix,${multiLine ? '\n' : ' '}$timeSuffix'; } String? getDisplayDay({DateTime? dateTimeUtc, bool allDay = false, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false}) { From 8a1a6b3f1e36e83ca395d6310cc3c1fa040bb775 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 12 Apr 2023 16:53:36 -0500 Subject: [PATCH 022/177] progress on score chart --- lib/service/app_datetime.dart | 4 +-- lib/utils/utils.dart | 51 +++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index 86f49d69c..22d351a52 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -193,7 +193,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; @@ -203,7 +203,7 @@ 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; diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 06609d7ba..40d68fda9 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1012,6 +1012,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) { @@ -1020,48 +1028,57 @@ 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); + return now.subtract(Duration(days: now.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{ From 2640721f772e9f1b8ef4d36a50d796d3fb641415 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 14 Apr 2023 15:35:17 -0500 Subject: [PATCH 023/177] rework chart computation for fixed ranges, add high score chart --- lib/utils/utils.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 40d68fda9..07dea7be5 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1060,7 +1060,8 @@ class DateTimeUtils { static DateTime weekStart({DateTime? now, timezone.Location? location}) { now ??= nowTimezone(location); - return now.subtract(Duration(days: now.weekday - 1)); + DateTime today = dayStart(now); + return today.subtract(Duration(days: today.weekday - 1)); } static DateTime weekEnd({DateTime? now, timezone.Location? location}) { From 695fadd4b83eb95d09fdd5458d720f4bdb124c6a Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 14 Apr 2023 23:53:57 -0500 Subject: [PATCH 024/177] cleanup remaining ui --- lib/service/auth2.dart | 2 +- lib/service/config.dart | 2 ++ lib/ui/widgets/ribbon_button.dart | 5 +++-- lib/ui/widgets/rounded_button.dart | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index dff386792..b23bc61c1 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -285,7 +285,7 @@ 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)) { + if (Config().supportsAnonymousAuth && (Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (Config().rokwireApiKey != null)) { String url = "${Config().coreUrl}/services/auth/login"; Map headers = { 'Content-Type': 'application/json' diff --git a/lib/service/config.dart b/lib/service/config.dart index 5c93ad86d..32b70af71 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -505,6 +505,8 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { return (assetsCacheDir != null) ? Directory(assetsCacheDir) : null; } + bool get supportsAnonymousAuth => false; + // Getters: compound entries Map get content => _config ?? {}; diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index 39a9f9bc2..fc7dc5d6d 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'; @@ -76,9 +77,9 @@ 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 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 displayFontFamily => fontFamily ?? defaultFontFamily; diff --git a/lib/ui/widgets/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index bd2a4e203..3d2cec118 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -105,7 +105,7 @@ class RoundedButton extends StatefulWidget { @protected Color? get defaultBackgroundColor => Styles().colors?.surface; @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; - @protected Color? get defautTextColor => Styles().colors?.fillColorPrimary; + @protected Color? get defautTextColor => Styles().colors?.textPrimary; @protected Color? get displayTextColor => textColor ?? defautTextColor; @protected String? get defaultFontFamily => Styles().fontFamilies?.bold; @protected String? get displayFontFamily => fontFamily ?? defaultFontFamily; From 168f0a41541333308b1f3bce261ab7ff5648bf62 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Sat, 15 Apr 2023 17:54:52 -0500 Subject: [PATCH 025/177] rework trial board into widget --- lib/service/app_datetime.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index 22d351a52..bf1bd1560 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -158,7 +158,10 @@ class AppDateTime with Service { return DateTimeUtils.utcDateTimeToString(getUtcTimeFromDeviceTime(dateTime)); } - String getDisplayDateTime(DateTime dateTimeUtc, {String? format, bool allDay = false, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false, bool multiLine = false}) { + String getDisplayDateTime(DateTime? dateTimeUtc, {String? format, bool allDay = false, 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) ?? ''; From f242c6c5ade9349752fa9b2fcf0c96a68e205de9 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 18 Apr 2023 10:05:01 -0500 Subject: [PATCH 026/177] use flutter secure storage, fix onboarding and logout issues --- example/pubspec.lock | 48 ++++++++++++++++++ lib/service/auth2.dart | 106 +++++++++++++++++++-------------------- lib/service/storage.dart | 98 ++++++++++++++++++++---------------- pubspec.yaml | 1 + 4 files changed, 155 insertions(+), 98 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 95e1d6336..b0a3dfb7d 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -398,6 +398,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + url: "https://pub.dev" + source: hosted + version: "1.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + url: "https://pub.dev" + source: hosted + version: "2.0.0" flutter_svg: dependency: transitive description: diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index b23bc61c1..a4f53a821 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -41,7 +41,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Timer? _oidcAuthenticationTimer; final Map> _refreshTokenFutures = {}; - final Map _refreshTonenFailCounts = {}; + final Map _refreshTokenFailCounts = {}; Client? _updateUserPrefsClient; Timer? _updateUserPrefsTimer; @@ -94,22 +94,22 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @override Future initService() async { - _token = Storage().auth2Token; - _account = Storage().auth2Account; + _token = await Storage().getAuth2Token(); + _account = await Storage().getAuth2Account(); _anonymousId = Storage().auth2AnonymousId; - _anonymousToken = Storage().auth2AnonymousToken; - _anonymousPrefs = Storage().auth2AnonymousPrefs; - _anonymousProfile = Storage().auth2AnonymousProfile; + _anonymousToken = await Storage().getAuth2AnonymousToken(); + _anonymousPrefs = await Storage().getAuth2AnonymousPrefs(); + _anonymousProfile = await Storage().getAuth2AnonymousProfile(); _deviceId = await RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2); if ((_account == null) && (_anonymousPrefs == null)) { - Storage().auth2AnonymousPrefs = _anonymousPrefs = defaultAnonimousPrefs; + await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonimousPrefs); } if ((_account == null) && (_anonymousProfile == null)) { - Storage().auth2AnonymousProfile = _anonymousProfile = defaultAnonimousProfile; + await Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonimousProfile); } if ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { @@ -305,9 +305,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 Storage().setAuth2AnonymousId(_anonymousId = anonymousId); + await Storage().setAuth2AnonymousToken(_anonymousToken = anonymousToken); _log("Auth2: anonymous auth succeeded: ${response?.statusCode}\n${response?.body}"); return true; } @@ -417,14 +417,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Future applyLogin(Auth2Account account, Auth2Token token, { Map? params }) async { - _refreshTonenFailCounts.remove(_token?.refreshToken); + _refreshTokenFailCounts.remove(_token?.refreshToken); bool? prefsUpdated = account.prefs?.apply(_anonymousPrefs); bool? profileUpdated = account.profile?.apply(_anonymousProfile); - Storage().auth2Token = _token = token; - Storage().auth2Account = _account = account; - Storage().auth2AnonymousPrefs = _anonymousPrefs = null; - Storage().auth2AnonymousProfile = _anonymousProfile = null; + await Storage().setAuth2Token(_token = token); + await Storage().setAuth2Account(_account = account); + await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = null); + await Storage().setAuth2AnonymousProfile(_anonymousProfile = null); if (prefsUpdated == true) { _saveAccountUserPrefs(); @@ -805,7 +805,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Map? responseJson = JsonUtils.decodeMap(response?.body); 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, authTypes: authTypes)); NotificationService().notify(notifyLinkChanged); return Auth2LinkResult.succeeded; } @@ -845,7 +845,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Map? responseJson = (response?.statusCode == 200) ? JsonUtils.decodeMap(response?.body) : 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, authTypes: authTypes)); NotificationService().notify(notifyLinkChanged); return true; } @@ -866,33 +866,31 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Logout - void logout({ Auth2UserPrefs? prefs }) { - if (_token != null) { - _log("Auth2: logout"); - _refreshTonenFailCounts.remove(_token?.refreshToken); + Future logout({ Auth2UserPrefs? prefs }) async { + _log("Auth2: logout"); + _refreshTokenFailCounts.remove(_token?.refreshToken); - Storage().auth2AnonymousPrefs = _anonymousPrefs = prefs ?? _account?.prefs ?? Auth2UserPrefs.empty(); - Storage().auth2AnonymousProfile = _anonymousProfile = Auth2UserProfile.empty(); - Storage().auth2Token = _token = null; - Storage().auth2Account = _account = null; + await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = prefs ?? _account?.prefs ?? Auth2UserPrefs.empty()); + await Storage().setAuth2AnonymousProfile(_anonymousProfile = Auth2UserProfile.empty()); + await Storage().setAuth2Token(_token = null); + await 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 @@ -943,22 +941,22 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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); + _refreshTokenFailCounts.remove(token.refreshToken); if (token == _token) { applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); return responseToken; } else if (token == _anonymousToken) { - Storage().auth2AnonymousToken = _anonymousToken = responseToken; + await Storage().setAuth2AnonymousToken(_anonymousToken = responseToken); 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)) { + int refreshTokenFailCount = (_refreshTokenFailCounts[token.refreshToken] ?? 0) + 1; + if (((response?.statusCode == 400) || (response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTokenFailCount)) { if (token == _token) { logout(); } @@ -967,7 +965,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } else { - _refreshTonenFailCounts[token.refreshToken!] = refreshTonenFailCount; + _refreshTokenFailCounts[token.refreshToken!] = refreshTokenFailCount; } } } @@ -980,8 +978,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } @protected - void applyToken(Auth2Token token, { Map? params }) { - Storage().auth2Token = _token = token; + Future applyToken(Auth2Token token, { Map? params }) async { + await Storage().setAuth2Token(_token = token); } static Future _refreshToken(String? refreshToken) async { @@ -1006,11 +1004,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(); } @@ -1070,13 +1068,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(); } @@ -1089,7 +1087,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; @@ -1177,8 +1175,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) { diff --git a/lib/service/storage.dart b/lib/service/storage.dart index c5dbd2b60..084d40d3a 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'; @@ -32,6 +33,8 @@ class Storage with Service { static const String _encryptionIVId = 'edu.illinois.rokwire.encryption.storage.iv'; SharedPreferences? _sharedPreferences; + FlutterSecureStorage? _secureStorage; + String? _encryptionKey; String? _encryptionIV; @@ -54,6 +57,13 @@ class Storage with Service { @override Future initService() async { _sharedPreferences = await SharedPreferences.getInstance(); + + 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); @@ -86,15 +96,15 @@ class Storage with Service { String get encryptionIVId => _encryptionIVId; String? get encryptionIV => _encryptionIV; - String? encrypt(String? value) { - return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? - AESCrypt.encrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; - } - - String? decrypt(String? value) { - return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? - AESCrypt.decrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; - } + // String? encrypt(String? value) { + // return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? + // AESCrypt.encrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; + // } + // + // String? decrypt(String? value) { + // return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? + // AESCrypt.decrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; + // } // Utilities @@ -111,30 +121,30 @@ class Storage with Service { NotificationService().notify(notifySettingChanged, name); } - String? getEncryptedStringWithName(String name, {String? defaultValue}) { - String? value = _sharedPreferences?.getString(name); - if (value != null) { - if ((_encryptionKey != null) && (_encryptionIV != null)) { - value = decrypt(value); - } - else { - value = null; - } - } - return value ?? defaultValue; - } - - void setEncryptedStringWithName(String name, String? value) { - if (value != null) { - if ((_encryptionKey != null) && (_encryptionIV != null)) { - value = encrypt(value); - _sharedPreferences?.setString(name, value!); - } - } else { - _sharedPreferences?.remove(name); - } - NotificationService().notify(notifySettingChanged, name); - } + // String? getEncryptedStringWithName(String name, {String? defaultValue}) { + // String? value = _sharedPreferences?.getString(name); + // if (value != null) { + // if ((_encryptionKey != null) && (_encryptionIV != null)) { + // value = decrypt(value); + // } + // else { + // value = null; + // } + // } + // return value ?? defaultValue; + // } + // + // void setEncryptedStringWithName(String name, String? value) { + // if (value != null) { + // if ((_encryptionKey != null) && (_encryptionIV != null)) { + // value = encrypt(value); + // _sharedPreferences?.setString(name, value!); + // } + // } else { + // _sharedPreferences?.remove(name); + // } + // NotificationService().notify(notifySettingChanged, name); + // } List? getStringListWithName(String name, {List? defaultValue}) { return _sharedPreferences?.getStringList(name) ?? defaultValue; @@ -249,27 +259,27 @@ 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 _secureStorage?.read(key: auth2AnonymousTokenKey))); + Future setAuth2AnonymousToken(Auth2Token? value) async => await _secureStorage?.write(key: auth2AnonymousTokenKey, value: 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 _secureStorage?.read(key: auth2AnonymousPrefsKey))); + Future setAuth2AnonymousPrefs(Auth2UserPrefs? value) async => await _secureStorage?.write(key: auth2AnonymousPrefsKey, value: 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 _secureStorage?.read(key: auth2AnonymousProfileKey))); + Future setAuth2AnonymousProfile(Auth2UserProfile? value) async => await _secureStorage?.write(key: auth2AnonymousProfileKey, value: 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 _secureStorage?.read(key: auth2TokenKey))); + Future setAuth2Token(Auth2Token? value) async => await _secureStorage?.write(key: auth2TokenKey, value: 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 _secureStorage?.read(key: auth2AccountKey))); + Future setAuth2Account(Auth2Account? value) async => await _secureStorage?.write(key: auth2AccountKey, value: JsonUtils.encode(value?.toJson())); // Http Proxy String get httpProxyEnabledKey => 'edu.illinois.rokwire.http_proxy.enabled'; diff --git a/pubspec.yaml b/pubspec.yaml index a61493541..08d8b12d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: webview_flutter: ^2.0.4 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.3.0 + flutter_secure_storage: ^8.0.0 #Firebase firebase_core: ^2.4.0 From 11c2286f0a53f609aee605bd0128a81edb3d3c0c Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 25 Apr 2023 13:24:41 -0500 Subject: [PATCH 027/177] add scoring algorithm selection --- lib/ui/widget_builders/dropdowns.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 lib/ui/widget_builders/dropdowns.dart 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 From 53a2fcf882abadf19cd837d7d49b1a7f622e50f5 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 26 Apr 2023 16:57:40 -0500 Subject: [PATCH 028/177] add encrypt utils --- tools/encrypt_util.dart | 93 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tools/encrypt_util.dart diff --git a/tools/encrypt_util.dart b/tools/encrypt_util.dart new file mode 100644 index 000000000..6ac6ec94d --- /dev/null +++ b/tools/encrypt_util.dart @@ -0,0 +1,93 @@ +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/secrets/rokmetro/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 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 From 23591c8c580b1dc6f48c6287a497cdae8f81a99c Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 26 Apr 2023 17:08:17 -0500 Subject: [PATCH 029/177] fix encrypt utils --- tools/encrypt_util.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/encrypt_util.dart b/tools/encrypt_util.dart index 6ac6ec94d..c7a3e3f56 100644 --- a/tools/encrypt_util.dart +++ b/tools/encrypt_util.dart @@ -4,7 +4,7 @@ import 'package:encrypt/encrypt.dart' as encrypt_package; void main() async { String encryptedFilepath = "assets/configs.json.enc"; - String keysFilepath = "assets/secrets/rokmetro/config.keys.json"; + String keysFilepath = "assets/config.keys.json"; String decryptedFile = "assets/configs.json"; Map? encryptionKeys = await loadEncryptionKeysFromAssets(keysFilepath); From 48cc96f8cf6b4cd7d066592086107df6079a647a Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 1 May 2023 11:55:13 -0500 Subject: [PATCH 030/177] fix typos --- lib/service/content.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/service/content.dart b/lib/service/content.dart index 13525f2b5..389335f23 100644 --- a/lib/service/content.dart +++ b/lib/service/content.dart @@ -21,7 +21,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'; @@ -66,7 +66,7 @@ class Content with Service implements NotificationsListener { @override void createService() { NotificationService().subscribe(this,[ - AppLivecycle.notifyStateChanged, + AppLifecycle.notifyStateChanged, ]); super.createService(); } @@ -105,7 +105,7 @@ class Content with Service implements NotificationsListener { @override void onNotification(String name, dynamic param) { - if (name == AppLivecycle.notifyStateChanged) { + if (name == AppLifecycle.notifyStateChanged) { _onAppLivecycleStateChanged(param); } } From 07e605f1562db7305e2ac35a306fcc7d99f5181f Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 4 May 2023 13:44:02 -0500 Subject: [PATCH 031/177] use plugin, begin implementation of passkey auth --- example/pubspec.lock | 10 ++- lib/model/auth2.dart | 6 +- lib/service/auth2.dart | 169 +++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 184 insertions(+), 2 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 5cf27d468..fd8704b8c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -390,6 +390,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_passkey: + dependency: transitive + description: + name: flutter_passkey + sha256: "8166c502f43dab8175f00878a63c6eb5e731479a9326952fc79daf2467a77bcc" + url: "https://pub.dev" + source: hosted + version: "1.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1197,5 +1205,5 @@ packages: source: hosted version: "5.3.1" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=3.3.0-0" diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 78f17607b..f674131bf 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -71,7 +71,7 @@ class Auth2Token { //////////////////////////////// // Auth2LoginType -enum Auth2LoginType { anonymous, apiKey, email, phone, phoneTwilio, oidc, oidcIllinois } +enum Auth2LoginType { anonymous, apiKey, email, phone, phoneTwilio, oidc, oidcIllinois, passkey } String? auth2LoginTypeToString(Auth2LoginType value) { switch (value) { @@ -82,6 +82,7 @@ String? auth2LoginTypeToString(Auth2LoginType value) { case Auth2LoginType.phoneTwilio: return 'twilio_phone'; case Auth2LoginType.oidc: return 'oidc'; case Auth2LoginType.oidcIllinois: return 'illinois_oidc'; + case Auth2LoginType.passkey: return 'passkey'; } } @@ -107,6 +108,9 @@ Auth2LoginType? auth2LoginTypeFromString(String? value) { else if (value == 'illinois_oidc') { return Auth2LoginType.oidcIllinois; } + else if (value == 'passkey') { + return Auth2LoginType.passkey; + } return null; } diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index f51f70ae1..fb9476b1b 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_passkey/flutter_passkey.dart'; import 'package:http/http.dart'; import 'package:rokwire_plugin/model/auth2.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; @@ -38,6 +39,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { static const String _deviceIdIdentifier = 'edu.illinois.rokwire.device_id'; + final flutterPasskeyPlugin = FlutterPasskey(); + _OidcLogin? _oidcLogin; bool? _oidcLink; List>? _oidcAuthenticationCompleters; @@ -218,6 +221,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2LoginType get oidcLoginType => Auth2LoginType.oidcIllinois; Auth2LoginType get phoneLoginType => Auth2LoginType.phoneTwilio; Auth2LoginType get emailLoginType => Auth2LoginType.email; + Auth2LoginType get passkeyLoginType => Auth2LoginType.passkey; Auth2Token? get token => _token ?? _anonymousToken; Auth2Token? get userToken => _token; @@ -321,6 +325,155 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return false; } + // Passkey authentication + // Future authenticateWithPasskey(String? username) async { + // if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { + // + // NotificationService().notify(notifyLoginStarted, passkeyLoginType); + // + // final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); + // if (!isPasskeySupported) { + // return Auth2PasskeySignInResult.failedNotSupported; + // } + // + // // Obtain requestOptions from the server + // final requestOptions = getCredentialRequestOptions(); + // flutterPasskeyPlugin.getCredential(requestOptions).then((response) { + // // Send response to the server + // }).catchError((error) { + // // Handle error + // }); + // + // String url = "${Config().coreUrl}/services/auth/login"; + // 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, + // 'creds': { + // "email": email, + // "password": password + // }, + // 'profile': _anonymousProfile?.toJson(), + // 'preferences': _anonymousPrefs?.toJson(), + // 'device': deviceInfo, + // }); + // + // Response? response = await Network().post(url, headers: headers, body: post); + // if (response?.statusCode == 200) { + // bool result = await processLoginResponse(JsonUtils.decodeMap(response?.body)); + // _notifyLogin(emailLoginType, result); + // return result ? Auth2EmailSignInResult.succeeded : Auth2EmailSignInResult.failed; + // } + // else { + // _notifyLogin(emailLoginType, false); + // Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); + // if (error?.status == 'unverified') { + // return Auth2EmailSignInResult.failedNotActivated; + // } + // else if (error?.status == 'verification-expired') { + // return Auth2EmailSignInResult.failedActivationExpired; + // } + // else if (error?.status == 'invalid') { + // return Auth2EmailSignInResult.failedInvalid; + // } + // } + // } + // return Auth2EmailSignInResult.failed; + // } + + Future signUpWithPasskey(String? username, String? displayName) async { + if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { + final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); + if (!isPasskeySupported) { + return Auth2PasskeySignUpResult.failedNotSupported; + } + + String url = "${Config().coreUrl}/services/auth/login"; + 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, + 'params': { + "sign_up": true, + "name": username, + "display_name": displayName, + }, + 'profile': _anonymousProfile?.toJson(), + 'preferences': _anonymousPrefs?.toJson(), + 'device': deviceInfo, + }); + + Response? response = await Network().post(url, headers: headers, body: post); + if (response != null && response.statusCode == 200) { + // Obtain creationOptions from the server + final creationOptions = response.body; + try { + String response = await flutterPasskeyPlugin.createCredential(creationOptions); + // _completeSignUpWithPasskey(response); + } catch(error) { + Log.e(error.toString()); + } + return Auth2PasskeySignUpResult.succeeded; + } + // else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { + // return Auth2PasskeySignUpResult.failedAccountExist; + // } + } + return Auth2PasskeySignUpResult.failed; + } + + // Future _completeSignUpWithPasskey(String response) async { + // if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { + // final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); + // if (!isPasskeySupported) { + // return Auth2PasskeySignUpResult.failedNotSupported; + // } + // + // String url = "${Config().coreUrl}/services/auth/login"; + // 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, + // 'creds': { + // "sign_up": true, + // "name": username, + // "display_name": displayName, + // }, + // 'profile': _anonymousProfile?.toJson(), + // 'preferences': _anonymousPrefs?.toJson(), + // 'device': deviceInfo, + // }); + // + // Response? response = await Network().post(url, headers: headers, body: post); + // if (response != null && response.statusCode == 200) { + // // Obtain creationOptions from the server + // final creationOptions = response.body; + // flutterPasskeyPlugin.createCredential(creationOptions).then((response) { + // // Send response to the server + // }).catchError((error) { + // // Handle error + // }); + // return Auth2PasskeySignUpResult.succeeded; + // } + // // else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { + // // return Auth2PasskeySignUpResult.failedAccountExist; + // // } + // } + // return Auth2PasskeySignUpResult.failed; + // } + // OIDC Authentication Future authenticateWithOidc({bool? link}) async { @@ -1257,6 +1410,22 @@ class _OidcLogin { } +// Auth2PasskeySignUpResult + +enum Auth2PasskeySignUpResult { + succeeded, + failed, + failedNotSupported, +} + +// Auth2PasskeySignInResult + +enum Auth2PasskeySignInResult { + succeeded, + failed, + failedNotSupported, +} + // Auth2PhoneRequestCodeResult enum Auth2PhoneRequestCodeResult { diff --git a/pubspec.yaml b/pubspec.yaml index 4042f0c0d..e42309ace 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: webview_flutter: ^2.0.13 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.2.1 + flutter_passkey: ^1.0.1 #Firebase firebase_core: ^1.10.0 From 624b3092243e5fb1111e4ae250e70291574664ab Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 5 May 2023 17:06:00 -0500 Subject: [PATCH 032/177] add passkey auth type ui --- example/pubspec.lock | 4 ++-- lib/model/auth2.dart | 41 +++++++++++++++++++++++++++++++++++++++-- lib/rokwire_plugin.dart | 4 +++- lib/service/auth2.dart | 11 +++++++---- pubspec.yaml | 2 +- 5 files changed, 52 insertions(+), 10 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index fd8704b8c..a61f2e574 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -394,10 +394,10 @@ packages: dependency: transitive description: name: flutter_passkey - sha256: "8166c502f43dab8175f00878a63c6eb5e731479a9326952fc79daf2467a77bcc" + sha256: ba37994842e9f60dce7a9691b3a787c2180587dbcc107fa9ad7dbe20d47b6812 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index f674131bf..30be02255 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -82,7 +82,7 @@ String? auth2LoginTypeToString(Auth2LoginType value) { case Auth2LoginType.phoneTwilio: return 'twilio_phone'; case Auth2LoginType.oidc: return 'oidc'; case Auth2LoginType.oidcIllinois: return 'illinois_oidc'; - case Auth2LoginType.passkey: return 'passkey'; + case Auth2LoginType.passkey: return 'webauthn'; } } @@ -108,7 +108,7 @@ Auth2LoginType? auth2LoginTypeFromString(String? value) { else if (value == 'illinois_oidc') { return Auth2LoginType.oidcIllinois; } - else if (value == 'passkey') { + else if (value == 'webauthn') { return Auth2LoginType.passkey; } return null; @@ -685,6 +685,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 diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index a4beb2f96..bc3bdde22 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -96,7 +96,9 @@ class RokwirePlugin { 'requestJson': requestJson, 'preferImmediatelyAvailableCredentials': preferImmediatelyAvailableCredentials }); - } catch(e) { debugPrint(e.toString()); } + } catch(e) { + debugPrint(e.toString()); + } } // Compound APIs diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index fb9476b1b..d013537d6 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -397,7 +397,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'Content-Type': 'application/json' }; String? post = JsonUtils.encode({ - 'auth_type': auth2LoginTypeToString(emailLoginType), + 'auth_type': auth2LoginTypeToString(passkeyLoginType), 'app_type_identifier': Config().appPlatformId, 'api_key': Config().rokwireApiKey, 'org_id': Config().coreOrgId, @@ -414,14 +414,17 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Response? response = await Network().post(url, headers: headers, body: post); if (response != null && response.statusCode == 200) { // Obtain creationOptions from the server - final creationOptions = response.body; + Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(response.body)); + Map? requestJson = JsonUtils.decode(message?.message ?? ''); + Map? pubKeyRequest = requestJson?['publicKey']; try { - String response = await flutterPasskeyPlugin.createCredential(creationOptions); + // await RokwirePlugin.createPasskey(JsonUtils.encode(pubKeyRequest) ?? ''); + String response = await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); // _completeSignUpWithPasskey(response); + return Auth2PasskeySignUpResult.succeeded; } catch(error) { Log.e(error.toString()); } - return Auth2PasskeySignUpResult.succeeded; } // else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { // return Auth2PasskeySignUpResult.failedAccountExist; diff --git a/pubspec.yaml b/pubspec.yaml index e42309ace..be17ac36e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: webview_flutter: ^2.0.13 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.2.1 - flutter_passkey: ^1.0.1 + flutter_passkey: ^1.0.2 #Firebase firebase_core: ^1.10.0 From 8247aa61da4f6eaa679c36cf00fd8135359e541e Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 9 May 2023 14:55:06 -0500 Subject: [PATCH 033/177] fix styling and logging issues --- lib/service/auth2.dart | 10 +++++----- lib/ui/popups/popup_message.dart | 2 +- lib/ui/widgets/rounded_button.dart | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 2c66f7e07..5b0bb147b 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1034,15 +1034,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future? refreshTokenFuture = _refreshTokenFutures[token.refreshToken]; if (refreshTokenFuture != null) { - _log("Auth2: will await refresh token:\nSource Token: ${token.refreshToken}"); + _log("Auth2: will await refresh token:"); 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}"); + _log("Auth2: did await refresh token: ${responseToken?.isValid}\n"); return ((responseToken != null) && responseToken.isValid) ? responseToken : null; } else { - _log("Auth2: will refresh token:\nSource Token: ${token.refreshToken}"); + _log("Auth2: will refresh token:\n"); _refreshTokenFutures[token.refreshToken!] = refreshTokenFuture = _refreshToken(token.refreshToken); Response? response = await refreshTokenFuture; @@ -1052,7 +1052,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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}"); + _log("Auth2: did refresh token:\n"); _refreshTokenFailCounts.remove(token.refreshToken); if (token == _token) { @@ -1066,7 +1066,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token.refreshToken}"); + _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\n"); int refreshTokenFailCount = (_refreshTokenFailCounts[token.refreshToken] ?? 0) + 1; if (((response?.statusCode == 400) || (response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTokenFailCount)) { if (token == _token) { diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index 334053988..f103d9409 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -74,7 +74,7 @@ class PopupMessage extends StatelessWidget { @protected Color? get defautTitleBarColor => Styles().colors?.fillColorPrimary; @protected Color? get displayTitleBarColor => titleBarColor ?? defautTitleBarColor; - @protected Color? get defautTitleTextColor => Styles().colors?.white; + @protected Color? get defautTitleTextColor => Styles().colors?.textLight; @protected Color? get displayTitleTextColor => titleTextColor ?? defautTitleTextColor; @protected String? get defaultTitleFontFamily => Styles().fontFamilies?.bold; diff --git a/lib/ui/widgets/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index 3d2cec118..b171dc458 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -105,8 +105,8 @@ class RoundedButton extends StatefulWidget { @protected Color? get defaultBackgroundColor => Styles().colors?.surface; @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; - @protected Color? get defautTextColor => Styles().colors?.textPrimary; - @protected Color? get displayTextColor => textColor ?? defautTextColor; + @protected Color? get defaultTextColor => Styles().colors?.textPrimary; + @protected Color? get displayTextColor => textColor ?? defaultTextColor; @protected String? get defaultFontFamily => Styles().fontFamilies?.bold; @protected String? get displayFontFamily => fontFamily ?? defaultFontFamily; @protected TextStyle get defaultTextStyle => TextStyle(fontFamily: displayFontFamily, fontSize: fontSize, color: displayTextColor); From a0df1cd0ffba7ddbd3ef742486e221d8e9a15053 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 18 May 2023 13:41:29 -0500 Subject: [PATCH 034/177] add passkey login flow (not working) --- lib/service/auth2.dart | 257 +++++++++++++++++++++++----------------- lib/service/config.dart | 27 ++++- 2 files changed, 172 insertions(+), 112 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index d013537d6..ad3300cc5 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -326,64 +326,92 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } // Passkey authentication - // Future authenticateWithPasskey(String? username) async { - // if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { - // - // NotificationService().notify(notifyLoginStarted, passkeyLoginType); - // - // final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); - // if (!isPasskeySupported) { - // return Auth2PasskeySignInResult.failedNotSupported; - // } - // - // // Obtain requestOptions from the server - // final requestOptions = getCredentialRequestOptions(); - // flutterPasskeyPlugin.getCredential(requestOptions).then((response) { - // // Send response to the server - // }).catchError((error) { - // // Handle error - // }); - // - // String url = "${Config().coreUrl}/services/auth/login"; - // 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, - // 'creds': { - // "email": email, - // "password": password - // }, - // 'profile': _anonymousProfile?.toJson(), - // 'preferences': _anonymousPrefs?.toJson(), - // 'device': deviceInfo, - // }); - // - // Response? response = await Network().post(url, headers: headers, body: post); - // if (response?.statusCode == 200) { - // bool result = await processLoginResponse(JsonUtils.decodeMap(response?.body)); - // _notifyLogin(emailLoginType, result); - // return result ? Auth2EmailSignInResult.succeeded : Auth2EmailSignInResult.failed; - // } - // else { - // _notifyLogin(emailLoginType, false); - // Auth2Error? error = Auth2Error.fromJson(JsonUtils.decodeMap(response?.body)); - // if (error?.status == 'unverified') { - // return Auth2EmailSignInResult.failedNotActivated; - // } - // else if (error?.status == 'verification-expired') { - // return Auth2EmailSignInResult.failedActivationExpired; - // } - // else if (error?.status == 'invalid') { - // return Auth2EmailSignInResult.failedInvalid; - // } - // } - // } - // return Auth2EmailSignInResult.failed; - // } + Future authenticateWithPasskey(String? username) async { + if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { + final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); + if (!isPasskeySupported) { + return Auth2PasskeySignInResult.failedNotSupported; + } + + String url = "${Config().coreUrl}/services/auth/login"; + Map headers = { + 'Content-Type': 'application/json' + }; + String? post = JsonUtils.encode({ + 'auth_type': auth2LoginTypeToString(passkeyLoginType), + 'app_type_identifier': Config().appPlatformId, + 'api_key': Config().rokwireApiKey, + 'org_id': Config().coreOrgId, + 'creds': { + 'username': username, + }, + 'params': { + 'sign_up': false, + }, + 'profile': profile?.toJson(), + 'preferences': _anonymousPrefs?.toJson(), + 'device': deviceInfo, + }); + + Response? response = await Network().post(url, headers: headers, body: post); + if (response != null && response.statusCode == 200) { + // Obtain creationOptions from the server + Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(response.body)); + Map? requestJson = JsonUtils.decode(message?.message ?? ''); + Map? pubKeyRequest = requestJson?['publicKey']; + pubKeyRequest?.remove('allowCredentials'); + pubKeyRequest?['userVerification'] = 'required'; + pubKeyRequest = { + "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo", + // "allowCredentials": [], + "timeout": 1800000, + "userVerification": "required", + "rpId": "university.app.services.rokmetro.com" + }; + try { + // await RokwirePlugin.getPasskey(JsonUtils.encode(pubKeyRequest) ?? ''); + String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + return _completeSignInWithPasskey(username, responseData); + } catch(error) { + Log.e(error.toString()); + } + } + else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'not-found') { + return Auth2PasskeySignInResult.failedNotFound; + } + } + return Auth2PasskeySignInResult.failed; + } + + Future _completeSignInWithPasskey(String username, String responseData) async { + if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { + String url = "${Config().coreUrl}/services/auth/login"; + Map headers = { + 'Content-Type': 'application/json' + }; + String? post = JsonUtils.encode({ + 'auth_type': auth2LoginTypeToString(passkeyLoginType), + 'app_type_identifier': Config().appPlatformId, + 'api_key': Config().rokwireApiKey, + 'org_id': Config().coreOrgId, + 'creds': { + "username": username, + "response": responseData, + }, + 'device': deviceInfo, + }); + + Response? response = await Network().post(url, headers: headers, body: post); + if (response != null && response.statusCode == 200) { + Map? responseJson = JsonUtils.decode(response.body); + bool success = await processLoginResponse(responseJson); + if (success) { + return Auth2PasskeySignInResult.succeeded; + } + } + } + return Auth2PasskeySignInResult.failed; + } Future signUpWithPasskey(String? username, String? displayName) async { if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { @@ -392,6 +420,17 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2PasskeySignUpResult.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().coreUrl}/services/auth/login"; Map headers = { 'Content-Type': 'application/json' @@ -401,12 +440,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'app_type_identifier': Config().appPlatformId, 'api_key': Config().rokwireApiKey, 'org_id': Config().coreOrgId, + 'creds': { + 'username': username, + }, 'params': { - "sign_up": true, - "name": username, "display_name": displayName, }, - 'profile': _anonymousProfile?.toJson(), + 'profile': profile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, }); @@ -419,11 +459,18 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Map? pubKeyRequest = requestJson?['publicKey']; try { // await RokwirePlugin.createPasskey(JsonUtils.encode(pubKeyRequest) ?? ''); - String response = await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); - // _completeSignUpWithPasskey(response); - return Auth2PasskeySignUpResult.succeeded; + String responseData = await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + return _completeSignUpWithPasskey(username, responseData); } catch(error) { - Log.e(error.toString()); + try { + String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + Auth2PasskeySignInResult result = await _completeSignInWithPasskey(username, responseData); + if (result == Auth2PasskeySignInResult.succeeded) { + return Auth2PasskeySignUpResult.succeeded; + } + } catch(error) { + Log.e(error.toString()); + } } } // else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { @@ -433,49 +480,35 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2PasskeySignUpResult.failed; } - // Future _completeSignUpWithPasskey(String response) async { - // if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { - // final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); - // if (!isPasskeySupported) { - // return Auth2PasskeySignUpResult.failedNotSupported; - // } - // - // String url = "${Config().coreUrl}/services/auth/login"; - // 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, - // 'creds': { - // "sign_up": true, - // "name": username, - // "display_name": displayName, - // }, - // 'profile': _anonymousProfile?.toJson(), - // 'preferences': _anonymousPrefs?.toJson(), - // 'device': deviceInfo, - // }); - // - // Response? response = await Network().post(url, headers: headers, body: post); - // if (response != null && response.statusCode == 200) { - // // Obtain creationOptions from the server - // final creationOptions = response.body; - // flutterPasskeyPlugin.createCredential(creationOptions).then((response) { - // // Send response to the server - // }).catchError((error) { - // // Handle error - // }); - // return Auth2PasskeySignUpResult.succeeded; - // } - // // else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'already-exists') { - // // return Auth2PasskeySignUpResult.failedAccountExist; - // // } - // } - // return Auth2PasskeySignUpResult.failed; - // } + Future _completeSignUpWithPasskey(String username, String responseData) async { + if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { + String url = "${Config().coreUrl}/services/auth/login"; + Map headers = { + 'Content-Type': 'application/json' + }; + String? post = JsonUtils.encode({ + 'auth_type': auth2LoginTypeToString(passkeyLoginType), + 'app_type_identifier': Config().appPlatformId, + 'api_key': Config().rokwireApiKey, + 'org_id': Config().coreOrgId, + 'creds': { + "username": username, + "response": responseData, + }, + 'device': deviceInfo, + }); + + Response? response = await Network().post(url, headers: headers, body: post); + if (response != null && response.statusCode == 200) { + Map? responseJson = JsonUtils.decode(response.body); + bool success = await processLoginResponse(responseJson); + if (success) { + return Auth2PasskeySignUpResult.succeeded; + } + } + } + return Auth2PasskeySignUpResult.failed; + } // OIDC Authentication @@ -1413,6 +1446,13 @@ class _OidcLogin { } +// Auth2EmailAccountState + +enum Auth2PasskeyAccountState { + nonExistent, + exists, +} + // Auth2PasskeySignUpResult enum Auth2PasskeySignUpResult { @@ -1426,6 +1466,7 @@ enum Auth2PasskeySignUpResult { enum Auth2PasskeySignInResult { succeeded, failed, + failedNotFound, failedNotSupported, } diff --git a/lib/service/config.dart b/lib/service/config.dart index af5312f08..513764267 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -190,7 +190,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected Future loadAsStringFromNet() async { - return loadAsStringFromAppConfig(); + return loadAsStringFromCore(); } Future loadAsStringFromAppConfig() async { @@ -211,7 +211,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { }; String? bodyString = JsonUtils.encode(body); try { - http.Response? response = await Network().post(appConfigUrl, body: bodyString); + http.Response? response = await Network().post(appConfigUrl, body: bodyString, headers: {'content-type': 'application/json'}); return ((response != null) && (response.statusCode == 200)) ? response.body : null; } catch (e) { debugPrint(e.toString()); @@ -221,6 +221,22 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @protected Map? configFromJsonString(String? configJsonString) { + 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 + Map? configFromJsonListString(String? configJsonString) { dynamic configJson = JsonUtils.decode(configJsonString); List? jsonList = (configJson is List) ? configJson : null; if (jsonList != null) { @@ -295,7 +311,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { _configAsset = null; _config = (configString != null) ? configFromJsonString(configString) : null; - if (_config != null) { + if (_config != null && secretKeys.isNotEmpty) { configFile.writeAsStringSync(configString!, flush: true); checkUpgrade(); } @@ -500,6 +516,8 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { return (assetsCacheDir != null) ? Directory(assetsCacheDir) : null; } + bool get supportsAnonymousAuth => false; + // Getters: compound entries Map get content => _config ?? {}; @@ -535,7 +553,8 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { int get refreshTimeout => JsonUtils.intValue(settings['refreshTimeout']) ?? 0; int? get analyticsDeliveryTimeout => JsonUtils.intValue(settings['analyticsDeliveryTimeout']); int get refreshTokenRetriesCount => JsonUtils.intValue(settings['refreshTokenRetriesCount']) ?? 3; - + String? get timezoneLocation => JsonUtils.stringValue(settings['timezoneLocation']) ?? 'America/Chicago'; + // Getters: other String? get deepLinkRedirectUrl { Uri? assetsUri = StringUtils.isNotEmpty(assetsUrl) ? Uri.tryParse(assetsUrl!) : null; From 88f98ad3711f56b798ae0035bdd67ebd0ec9deb6 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 22 May 2023 16:14:58 -0500 Subject: [PATCH 035/177] upgrade dependencies --- example/pubspec.lock | 38 +++++++++++++++++++------------------- pubspec.yaml | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index b0a3dfb7d..dd7a61370 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" charcode: dependency: transitive description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" connectivity: dependency: transitive description: @@ -604,18 +604,18 @@ packages: dependency: transitive description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: @@ -636,10 +636,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -652,10 +652,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime_type: dependency: transitive description: @@ -692,10 +692,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_drawing: dependency: transitive description: @@ -984,10 +984,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timezone: dependency: transitive description: @@ -1253,5 +1253,5 @@ packages: source: hosted version: "5.3.1" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=3.0.0-0 <4.0.0" flutter: ">=3.3.0-0" diff --git a/pubspec.yaml b/pubspec.yaml index 08d8b12d6..c3390b25c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: asn1lib: ^1.0.3 pointycastle: ^3.5.0 encrypt: ^5.0.1 - intl: ^0.17.0 + intl: ^0.18.1 http: ^0.13.3 timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 From e7e188817948349f2182c4d77b2932ebeca7d112 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 22 May 2023 16:41:35 -0500 Subject: [PATCH 036/177] upgrade dependencies for Flutter v3.10, fix errors --- example/pubspec.lock | 526 +++++++++++------------------------ lib/ui/panels/web_panel.dart | 8 +- pubspec.yaml | 4 +- 3 files changed, 161 insertions(+), 377 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index dd7a61370..746b388c8 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: d58dc7eda77a05866ad4fac26a4953e82bac29167aa571224d499da5d2027bc6 + sha256: "8eb354cb8ebed8a9fdf63699d15deff533bc133128898afaf754926b57d611b6" url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "1.3.1" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.1" asn1lib: dependency: transitive description: name: asn1lib - sha256: "35e6681078cf198c5d5e83fdc5fcf4ad39a5f29564c181b370b5c29361f28660" + sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.4.0" async: dependency: transitive description: @@ -49,30 +49,6 @@ 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" - chewie: - dependency: transitive - description: - name: chewie - sha256: "7343cffa40ce9d42b37f2869f17b5e5fa681fb329d8385869ce279e03193e2b7" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - chewie_audio: - dependency: transitive - description: - name: chewie_audio - sha256: f92bb4364ced21318e1a7c0eddaf249a7554e5cf27f869d58c44d926abd292a7 - url: "https://pub.dev" - source: hosted - version: "1.3.0" clock: dependency: transitive description: @@ -81,22 +57,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - cloud_firestore_platform_interface: - dependency: transitive - description: - name: cloud_firestore_platform_interface - sha256: "86a21d4fd3afe89268e72f62691547e440eff3bcb2781d77c428df706e958149" - url: "https://pub.dev" - source: hosted - version: "5.10.0" - cloud_firestore_web: - dependency: transitive - description: - name: cloud_firestore_web - sha256: "2d7339bf3f79adcdb123a88e908474478b060317332189bdd5dcb2d498952127" - url: "https://pub.dev" - source: hosted - version: "3.2.0" collection: dependency: transitive description: @@ -141,10 +101,10 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" cookie_jar: dependency: transitive description: @@ -157,34 +117,34 @@ packages: dependency: transitive description: name: cross_file - sha256: "552ffd2f851d4314958e6265452af1891959e00cd32b6d17452c5b836e27a0fa" + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.3.3+4" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" csslib: dependency: transitive description: name: csslib - sha256: d1cd6d6e4b39a4ad295204722b8608f19981677b223f3e942c0b5a33dcf57ec0 + sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" url: "https://pub.dev" source: hosted - version: "0.17.1" + version: "0.17.3" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: "1989d917fbe8e6b39806207df5a3fdd3d816cbd090fac2ce26fb45e9a71476e5" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" dbus: dependency: transitive description: @@ -197,10 +157,10 @@ packages: dependency: transitive description: name: device_calendar - sha256: d0cad5428c8f5afb9b774b9a263922a16a2c01a05fb0ca50f8ea69ea294fb275 + sha256: "5a1ce7887b4ffbaf3743078c8314dede5e694cddd69bab43f35ce815c5d82a7d" url: "https://pub.dev" source: hosted - version: "4.3.1-4217521123" + version: "4.3.1" device_info: dependency: transitive description: @@ -237,82 +197,82 @@ packages: dependency: transitive description: name: ffi - sha256: "35d0f481d939de0d640b3db9a7aa36a52cd22054a798a73b4f50bdad5ce12678" + sha256: "13a6ccf6a459a125b3fcdb6ec73bd5ff90822e071207c663bfd1f70062d51d18" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" file: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" firebase_core: dependency: transitive description: name: firebase_core - sha256: "01962872df08437d9be593caeab8c700624c93b629f3d3d60f061612d6263666" + sha256: "250678b816279b3240c3a33e1f76bf712c00718f1fbeffc85873a5da8c077379" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.13.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5fab93f5b354648efa62e7cc829c90efb68c8796eecf87e0888cae2d5f3accd4" + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: b2917618cbe75196261621d676a992e05215ce6a70daca5f655794d5a008d369 + sha256: "8c0f4c87d20e2d001a5915df238c1f9c88704231f591324205f5a5d2a7740a45" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.5.0" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - sha256: "53832e866345fdb0216c549c459313ad1ba554050fe7e170de0ff30d98365656" + sha256: "0d74cca3085f144f99aa4bd82cc4d33280d4cb72bac0b733cbf97c2d7d126df8" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.3.1" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "13972389743a9b96cfa7909508c4dfc5d6770870264fdc8bb69d0ac5821d8574" + sha256: "13880033d5f2055f53bcda28024e16607b8400445a425f86732c1935da9260db" url: "https://pub.dev" source: hosted - version: "3.3.9" + version: "3.6.1" firebase_messaging: dependency: transitive description: name: firebase_messaging - sha256: "1ee95810148e5298c0fb6cb00ee4f099218f3d56d358e8047a9bdf4e68b504cb" + sha256: "9cfe5c4560fb83393511ca7620f8fb3f22c9a80303052f10290e732fcfb801bd" url: "https://pub.dev" source: hosted - version: "14.2.0" + version: "14.6.1" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "4fad3604e98815c5c8df88273f714cab27078a1df5cd848a8ec984b609b86ddd" + sha256: "7e25cb71019ccef8b1fd7b37969af79f04c467974cce4dfc291fa36974edd7ba" url: "https://pub.dev" source: hosted - version: "4.2.9" + version: "4.5.1" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: cbd454c9551e6671747b526cbe643b814df020d630e7502058b9026902003a46 + sha256: "5d9840cc8126ea723b1bda901389cb542902f664f2653c16d4f8114e95f13cec" url: "https://pub.dev" source: hosted - version: "3.2.10" + version: "3.5.1" flutter: dependency: "direct main" description: flutter @@ -330,18 +290,10 @@ packages: dependency: transitive description: name: flutter_html - sha256: ccb810fcabfce3a7ffaca46e458323915ac7e7fc59082c7357ff848972c02230 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - flutter_layout_grid: - dependency: transitive - description: - name: flutter_layout_grid - sha256: ab8848620feddfea46f821132eaa75f62301350888af03da00ba4b09cafa5244 + sha256: "850c07bc6c1ed060d3eb3e88469a598260a13eb45d8978b197c1348e0a2b101f" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "3.0.0-beta.1" flutter_lints: dependency: "direct dev" description: @@ -374,14 +326,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - flutter_math_fork: - dependency: transitive - description: - name: flutter_math_fork - sha256: a34205227e1a1d040a56e74a5e2e56e9397ea930540a9373bd0b3e2e4f122017 - url: "https://pub.dev" - source: hosted - version: "0.5.0" flutter_native_timezone: dependency: transitive description: @@ -394,10 +338,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5c574d21b98ec92adab05ead10afd2b13ff5856c7ca79696edb338a9dd8ed387" + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.15" flutter_secure_storage: dependency: transitive description: @@ -446,14 +390,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - flutter_svg: - dependency: transitive - description: - name: flutter_svg - sha256: cbf529d563dd910a249041bde2a0dfc3cf62280fcc459b85f3d614b2ab73abb3 - url: "https://pub.dev" - source: hosted - version: "0.23.0+1" flutter_test: dependency: "direct dev" description: flutter @@ -468,18 +404,18 @@ packages: dependency: transitive description: name: fluttertoast - sha256: b3ae793108ad2a7e8f2fea91ca5bb2ba7ef03621c4d9ed7a92fe3391da64c000 + sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25" url: "https://pub.dev" source: hosted - version: "8.0.8" + version: "8.2.1" font_awesome_flutter: dependency: transitive description: name: font_awesome_flutter - sha256: "875dbb9ec1ad30d68102019ceb682760d06c72747c1c5b7885781b95f88569cc" + sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206" url: "https://pub.dev" source: hosted - version: "10.3.0" + version: "10.4.0" gallery_saver: dependency: transitive description: @@ -500,18 +436,18 @@ packages: dependency: transitive description: name: geolocator_android - sha256: fe90565c3a8789dc3b433d8f95cdb18343f9bde298a419d978337ba1ae1037d3 + sha256: "6cd3c622df085a79fd61f5c14fa024c3ba593aa6b1df2ee809ac59f45e6a9861" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.1.8" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "567cf6d9879b4c33b2bd8bbfff0995e75c62a14f87b1480c8522c5dca5d3b166" + sha256: "22b60ca3b8c0f58e6a9688ff855ee39ab813ca3f0c0609a48d282f6631266f2e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.5" geolocator_platform_interface: dependency: transitive description: @@ -524,10 +460,10 @@ packages: dependency: transitive description: name: geolocator_web - sha256: c1e068aaa483ec4464376c81038d8c811a732da8c0b89e8014c69e40715a961d + sha256: f68a122da48fcfff68bbc9846bb0b74ef651afe84a1b1f6ec20939de4d6860e1 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.6" geolocator_windows: dependency: transitive description: @@ -540,66 +476,66 @@ packages: dependency: transitive description: name: html - sha256: bfef906cbd4e78ef49ae511d9074aebd1d2251482ef601a280973e8b58b51bbf + sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.3" http: dependency: transitive description: name: http - sha256: "2ed163531e071c2c6b7c659635112f24cb64ecbebf6af46b550d536c0b1aa112" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.6" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" image_picker: dependency: transitive description: name: image_picker - sha256: f3712cd190227fb92e0960cb0ce22928ba042c7183b16864ade83b259adf8ea6 + sha256: "9978d3510af4e6a902e545ce19229b926e6de6a1828d6134d3aab2e129a4d270" url: "https://pub.dev" source: hosted - version: "0.8.5+3" + version: "0.8.7+5" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "2d4c2634731ced2debc2a36273351dd1cb68080d505c12c472b0dc0657cd853c" + sha256: c2f3c66400649bd132f721c88218945d6406f693092b2f741b79ae9cdb046e59 url: "https://pub.dev" source: hosted - version: "0.8.5+1" + version: "0.8.6+16" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "1f21a49e1dc82908ac44c2b988ee96b7383d47dc528151c75bac9c67a3bfb9f5" + sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.12" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: aee8b90c2fb018e18cbb437da661d0531a5346d49cab2d5c4a65ebaf026bbd33 + sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0 url: "https://pub.dev" source: hosted - version: "0.8.5+6" + version: "0.8.7+4" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: dd0d3344bd1ef891e1ceedbe4d4374a246570e065b0fd272ba7125938a29ee54 + sha256: "1991219d9dbc42a99aff77e663af8ca51ced592cd6685c9485e3458302d3d4f8" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.6.3" intl: dependency: transitive description: @@ -624,14 +560,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" logger: dependency: transitive description: name: logger - sha256: "5076f09225f91dc49289a4ccb92df2eeea9ea01cf7c26d49b3a1f04c6a49eec1" + sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -664,22 +608,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - numerus: - dependency: transitive - description: - name: numerus - sha256: "0087ef729d63b96cb347a9c44b9c592f21cecb3605b415bbd18710aef80ce5cb" - url: "https://pub.dev" - source: hosted - version: "1.1.1" package_info: dependency: transitive description: @@ -696,94 +624,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" - path_drawing: - dependency: transitive - description: - name: path_drawing - sha256: "3bdd251dae9ffaef944450b73f168610db7e968e7b20daf0c3907f8b4aafc8a2" - url: "https://pub.dev" - source: hosted - version: "0.5.1+1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: ee5c47c1058ad66b4a41746ec3996af9593d0858872807bcd64ac118f0700337 - url: "https://pub.dev" - source: hosted - version: "0.2.1" path_provider: dependency: transitive description: name: path_provider - sha256: db1cf6c3f906f50d52c18400be575f75ed620f8717f9b6d11263edd95080dfc6 + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.15" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c69109bae02c6116bd8ac81319b13eb73dfae02ef74690d2a1a98c1ddd3aaefc + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" url: "https://pub.dev" source: hosted - version: "2.0.11" - path_provider_ios: + version: "2.0.27" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - sha256: "038d0141ff5d08c60ed071eee2758b68c50c42a1c10066a1fb6c28ab32fac84c" + name: path_provider_foundation + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.2.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "1e109f4df28bd95eab71e323008b53d19c4d633bc1ab05b577518773474e9621" + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 url: "https://pub.dev" source: hosted - version: "2.1.5" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - sha256: "0adeb313e1f2c3fc52baeeee59b0fe9c2d1f7da56fd96a9234e1702ec653a453" - url: "https://pub.dev" - source: hosted - version: "2.0.5" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "3dc0d51b07f85fec3746d9f4e8d31c73bb173cafa2e763f03f8df2e8d1878882" + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "366ad4e3541ea707f859e7148d4d5aba67d589d7936cee04a05c464a277eeb27" + sha256: a34ecd7fb548f8e57321fd8e50d865d266941b54e6c3b7758cf8f37c24116905 url: "https://pub.dev" source: hosted - version: "2.0.5" - pedantic: - dependency: transitive - description: - name: pedantic - sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.dev" - source: hosted - version: "1.11.1" + version: "2.0.7" petitparser: dependency: transitive description: name: petitparser - sha256: "1a914995d4ef10c94ff183528c120d35ed43b5eaa8713fc6766a9be4570782e2" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "5.4.0" platform: dependency: transitive description: @@ -796,18 +692,18 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pointycastle: dependency: transitive description: name: pointycastle - sha256: "21cc8c830d228dd84c84a9139394702d766622bacd3ea7a2bdcb1f00b280e9a4" + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.7.3" process: dependency: transitive description: @@ -816,22 +712,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" - provider: - dependency: transitive - description: - name: provider - sha256: "7896193cf752c40ba7f7732a95264319a787871e5d628225357f5c909182bc06" - url: "https://pub.dev" - source: hosted - version: "6.0.2" - quiver: - dependency: transitive - description: - name: quiver - sha256: "616b691d1c8f5c53b7b39ce3542f6a25308d7900bf689d0210e72a644a10387e" - url: "https://pub.dev" - source: hosted - version: "3.0.1+1" rokwire_plugin: dependency: "direct main" description: @@ -843,66 +723,58 @@ packages: dependency: transitive description: name: shared_preferences - sha256: eb7cfb572af1ccc8b81f66bfd525ae0e8d196574874105b5c27db64bef9f155a + sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: eb5f8ca2adb3303e0949971b8bc6408c67b5000502b838ee6cb0ef96cd29e708 + sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" url: "https://pub.dev" source: hosted - version: "2.0.10" - shared_preferences_ios: + version: "2.1.4" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - sha256: "2b7319dbccef93d2bc70d03f810a9edb95550f4dcb3fb27e64cae18291b80b28" + name: shared_preferences_foundation + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.2.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "65e86adba03ba1ecd4a2f3e5d986eac27dbcf5804e0d5e03e029dbf63b5ed3a7" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - sha256: d8c36fedba519cba905d30833b818cde3d52d4e9c7438da4664bd523468bb93f + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.2.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "992f0fdc46d0a3c0ac2e5859f2de0e577bbe51f78a77ee8f357cbe626a2ad32d" + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.2.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "09da0185028a227d51721cade7a3cbd5cc5f163a19593266f2acba87f729bf9c" + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: a16f85cbad4e23daf83cd188fc53ca19a9b025743e8e2983d9d89ec4c383d6e6 + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -928,18 +800,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "0128af224bade0990f4ce55f2ef93a7054aa14d28768f17d4d2b21f6b1185523" + sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.8+4" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "0c7785befac2b5c40fc66b485be2f35396246a6fd6ccb89bb22614ddfb3d54a7" + sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.5" stack_trace: dependency: transitive description: @@ -968,10 +840,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "271977ff1e9e82ceefb4f08424b8839f577c1852e0726b5ce855311b46d3ef83" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" term_glyph: dependency: transitive description: @@ -992,26 +864,18 @@ packages: dependency: transitive description: name: timezone - sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964" - url: "https://pub.dev" - source: hosted - version: "0.9.1" - tuple: - dependency: transitive - description: - name: tuple - sha256: fe3ae4f0dca3f9aac0888e2e0d117b642ce283a82d7017b54136290c0a3b0dd3 + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "0.9.2" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" uni_links: dependency: transitive description: @@ -1040,74 +904,74 @@ packages: dependency: transitive description: name: url_launcher - sha256: "236e5ad4df2dabb79ce5d0f1aafb4cb70c0ad6dc93f88e610ad31c8fef74f477" + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 url: "https://pub.dev" source: hosted - version: "6.0.18" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "2ec66333093cf5dd5103f089c3a7842fcd5581023a571839d3d176ff10244582" + sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87" url: "https://pub.dev" source: hosted - version: "6.0.14" + version: "6.0.34" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "0d6b0b9dce05986462e8ba08e51f1909ac63e2e59ebde829c46ad66423f03dee" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.0.14" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ae3c5ce30a1ba0a69c3f8803b23450703cf915575ad591857df98d9c15c11018 + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.5" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "8fd9ae3ab5e0f96cea7dd66c4ea65e39e3477067f4997c1ec8225d553e8bb8ea" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "1b9c4dab07794498b83b5f938e26b20f68c3b460a3015b0307f9541cb34ef93d" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "88de707cba8e0c1c678ed79274298c4737f7f24addd12e786fbf0597de085850" + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.0.17" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "4e24aac2a2960fb9a70a07992e1ba69cb99fbcee48fdf17abe280ce867bfcea2" + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.6" uuid: dependency: transitive description: name: uuid - sha256: "00ba1241ff12e77d8059eeb1f102b35235df01661a6110afd165ab52a0fc7714" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.7" vector_math: dependency: transitive description: @@ -1116,86 +980,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - video_player: - dependency: transitive - description: - name: video_player - sha256: "9550ef42803203cd729e34313142b7a1ee2a90b395105a7e38e32a9139fa28ad" - url: "https://pub.dev" - source: hosted - version: "2.2.18" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: "3be61312f2c72f1f72234a37c7fc7cb6e75d8f9be8f52f3cf8e03ea53f50e148" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: e49d39992fe2c38ec5e7f420b74d0d4c288bee195af61ca9d75ecc19b5025c41 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: ac0b3515d47244cf59aea6cdb009b745d5299b12d248ae123608ec533d483cb6 - url: "https://pub.dev" - source: hosted - version: "5.1.0" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "48de7d5ff19fb474082182c33704c0210c395521ff81e54fa255c7719056fc81" - url: "https://pub.dev" - source: hosted - version: "2.0.7" - wakelock: - dependency: transitive - description: - name: wakelock - sha256: da22c0789e1f849bec43688a52f2290f4e66268056f7cd77cb71245aef4917a0 - url: "https://pub.dev" - source: hosted - version: "0.5.6" - wakelock_macos: - dependency: transitive - description: - name: wakelock_macos - sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" - url: "https://pub.dev" - source: hosted - version: "0.4.0" - wakelock_platform_interface: - dependency: transitive - description: - name: wakelock_platform_interface - sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" - url: "https://pub.dev" - source: hosted - version: "0.3.0" - wakelock_web: - dependency: transitive - description: - name: wakelock_web - sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" - url: "https://pub.dev" - source: hosted - version: "0.4.0" - wakelock_windows: - dependency: transitive - description: - name: wakelock_windows - sha256: "108b1b73711f1664ee462e73af34a9286ff496e27d4d8371e2fb4da8fde4cdac" - url: "https://pub.dev" - source: hosted - version: "0.2.0" webview_flutter: dependency: transitive description: @@ -1208,50 +992,50 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "6d796e709d14da8496ee7c750765fad0e860c57a60844bb50f5b93a9ee5ceeab" + sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" url: "https://pub.dev" source: hosted - version: "2.8.3" + version: "2.10.4" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "6144d750f56ae63fdaad10ff09e0f762142beabde4fefdc2d32564f75572d905" + sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.9.5" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "87c56a34f69867f3aaf0b66df5399a4fc18898b1df8c738b835e62b73059d66f" + sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.9.5" win32: dependency: transitive description: name: win32 - sha256: "0b2350f19ef62ccc5d594ea92d9107230210ae0b37459686687b70f41b911e12" + sha256: c0e3a4f7be7dae51d8f152230b86627e3397c1ba8c3fa58e63d44a9f3edc9cef url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.6.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "11541eedefbcaec9de35aa82650b695297ce668662bbd6e3911a7fabdbde589f" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 url: "https://pub.dev" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: transitive description: name: xml - sha256: baa23bcba1ba4ce4b22c0c7a1d9c861e7015cb5169512676da0b85138e72840c + sha256: "80d494c09849dc3f899d227a78c30c5b949b985ededf884cb3f3bcd39f4b447a" url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.4.1" sdks: dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.3.0-0" + flutter: ">=3.3.0" diff --git a/lib/ui/panels/web_panel.dart b/lib/ui/panels/web_panel.dart index 42f78fd89..fdf4bd629 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -87,8 +87,8 @@ class WebPanel extends StatefulWidget { if (title != null) { contentList.add(flutter_html.Html(data: title, - onLinkTap: (url, context, attributes, element) => onTapStatusLink(url), - style: { "body": flutter_html.Style(color: Styles().colors!.fillColorPrimary, fontFamily: Styles().fontFamilies!.bold, fontSize: const flutter_html.FontSize(32), textAlign: TextAlign.center, padding: EdgeInsets.zero, margin: EdgeInsets.zero), },), + onLinkTap: (url, context, element) => onTapStatusLink(url), + style: { "body": flutter_html.Style(color: Styles().colors!.fillColorPrimary, fontFamily: Styles().fontFamilies!.bold, fontSize: flutter_html.FontSize(32), textAlign: TextAlign.center, padding: EdgeInsets.zero, margin: flutter_html.Margins.zero), },), ); } @@ -98,8 +98,8 @@ class WebPanel extends StatefulWidget { if ((message != null)) { contentList.add(flutter_html.Html(data: message, - onLinkTap: (url, context, attributes, element) => onTapStatusLink(url), - style: { "body": flutter_html.Style(color: Styles().colors!.fillColorPrimary, fontFamily: Styles().fontFamilies!.regular, fontSize: const flutter_html.FontSize(20), textAlign: TextAlign.left, padding: EdgeInsets.zero, margin: EdgeInsets.zero), },), + onLinkTap: (url, context, element) => onTapStatusLink(url), + style: { "body": flutter_html.Style(color: Styles().colors!.fillColorPrimary, fontFamily: Styles().fontFamilies!.regular, fontSize: flutter_html.FontSize(20), textAlign: TextAlign.left, padding: EdgeInsets.zero, margin: flutter_html.Margins.zero), },), ); } diff --git a/pubspec.yaml b/pubspec.yaml index c3390b25c..c5e89e9f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: asn1lib: ^1.0.3 pointycastle: ^3.5.0 encrypt: ^5.0.1 - intl: ^0.18.1 + intl: ^0.18.0 http: ^0.13.3 timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 @@ -38,7 +38,7 @@ dependencies: image_picker: ^0.8.5+3 mime_type: ^1.0.0 uuid: ^3.0.5 - flutter_html: ^2.2.0 + flutter_html: ^3.0.0-alpha.6 webview_flutter: ^2.0.4 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.3.0 From a4ae1ac9a8b3b3867ebdd2c5057a1c4fc0295712 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Thu, 1 Jun 2023 11:20:01 -0500 Subject: [PATCH 037/177] upgrade flutter_html package to 3.0.0-beta.2 --- example/pubspec.lock | 4 ++-- lib/service/localization.dart | 2 +- lib/ui/panels/web_panel.dart | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 746b388c8..3562b7343 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -290,10 +290,10 @@ packages: dependency: transitive description: name: flutter_html - sha256: "850c07bc6c1ed060d3eb3e88469a598260a13eb45d8978b197c1348e0a2b101f" + sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" url: "https://pub.dev" source: hosted - version: "3.0.0-beta.1" + version: "3.0.0-beta.2" flutter_lints: dependency: "direct dev" description: diff --git a/lib/service/localization.dart b/lib/service/localization.dart index c0376093f..d3d04c5d7 100644 --- a/lib/service/localization.dart +++ b/lib/service/localization.dart @@ -285,7 +285,7 @@ class Localization with Service implements NotificationsListener { String? getString(String? key, {String? defaults, String? language}) { dynamic value; - if ((value == null) && (_localeStrings != null) && ((language == null) || (language == _currentLocale?.languageCode))) { + if ((_localeStrings != null) && ((language == null) || (language == _currentLocale?.languageCode))) { value = _localeStrings![key]; } if ((value == null) && (_defaultStrings != null) && ((language == null) || (language == _defaultLocale?.languageCode))) { diff --git a/lib/ui/panels/web_panel.dart b/lib/ui/panels/web_panel.dart index fdf4bd629..7f0e58d32 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -88,7 +88,7 @@ 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), textAlign: TextAlign.center, padding: EdgeInsets.zero, margin: flutter_html.Margins.zero), },), + style: { "body": flutter_html.Style(color: Styles().colors!.fillColorPrimary, fontFamily: Styles().fontFamilies!.bold, fontSize: flutter_html.FontSize(32), textAlign: TextAlign.center, padding: flutter_html.HtmlPaddings.zero, margin: flutter_html.Margins.zero), },), ); } @@ -99,7 +99,7 @@ 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), textAlign: TextAlign.left, padding: EdgeInsets.zero, margin: flutter_html.Margins.zero), },), + style: { "body": flutter_html.Style(color: Styles().colors!.fillColorPrimary, fontFamily: Styles().fontFamilies!.regular, fontSize: flutter_html.FontSize(20), textAlign: TextAlign.left, padding: flutter_html.HtmlPaddings.zero, margin: flutter_html.Margins.zero), },), ); } diff --git a/pubspec.yaml b/pubspec.yaml index c5e89e9f4..061c7ccda 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: image_picker: ^0.8.5+3 mime_type: ^1.0.0 uuid: ^3.0.5 - flutter_html: ^3.0.0-alpha.6 + flutter_html: ^3.0.0-beta.2 webview_flutter: ^2.0.4 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.3.0 From d1356542d4ae7bf391b2ee28cae8b1587a4d8eaa Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 2 Jun 2023 12:21:17 -0500 Subject: [PATCH 038/177] use gen styles, fix/remove app naming references --- assets/styles.json | 6 ++- lib/gen/styles.dart | 4 +- lib/rokwire_plugin.dart | 4 +- lib/ui/panels/modal_image_panel.dart | 5 ++- lib/ui/panels/survey_panel.dart | 5 ++- lib/ui/panels/web_panel.dart | 9 +++-- lib/ui/popups/alerts.dart | 3 +- lib/ui/popups/popup_message.dart | 23 +++++------ lib/ui/widget_builders/buttons.dart | 4 +- lib/ui/widget_builders/scroll_pager.dart | 2 +- lib/ui/widget_builders/survey.dart | 4 +- lib/ui/widgets/expandable_section.dart | 11 +++--- lib/ui/widgets/expandable_text.dart | 7 ++-- lib/ui/widgets/filters.dart | 11 +++--- lib/ui/widgets/flex_content.dart | 15 +++---- lib/ui/widgets/form_field.dart | 5 ++- lib/ui/widgets/ribbon_button.dart | 4 +- lib/ui/widgets/rounded_button.dart | 9 +++-- lib/ui/widgets/rounded_tab.dart | 7 ++-- lib/ui/widgets/section.dart | 15 +++---- lib/ui/widgets/section_header.dart | 27 +++++++------ lib/ui/widgets/survey.dart | 44 ++++++++++----------- lib/ui/widgets/tab_bar.dart | 9 +++-- lib/ui/widgets/tile_button.dart | 50 ++++++++++++++---------- 24 files changed, 158 insertions(+), 125 deletions(-) diff --git a/assets/styles.json b/assets/styles.json index 1b78cfdaa..a1bf2575f 100644 --- a/assets/styles.json +++ b/assets/styles.json @@ -23,6 +23,8 @@ "background" :"#F5F5F5", "backgroundVariant" :"#E8E9EA", + "shadow" :"#30000000", + "gradientColorPrimary" :"#244372", "accentColor1" :"#E84A27", @@ -205,6 +207,8 @@ "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"} + "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/lib/gen/styles.dart b/lib/gen/styles.dart index 505f79227..2b00f977f 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -12,7 +12,7 @@ class AppColors { static Color? get textAccent => Styles().colors?.getColor('textAccent'); static Color? get textDark => Styles().colors?.getColor('textDark'); static Color? get textMedium => Styles().colors?.getColor('textMedium'); - static Color? get textLight => Styles().colors?.getColor('textLight'); + static Color? get textLight => Styles().colors?.getColor('textLight'); static Color? get textDisabled => Styles().colors?.getColor('textDisabled'); static Color? get iconPrimary => Styles().colors?.getColor('iconPrimary'); static Color? get iconLight => Styles().colors?.getColor('iconLight'); @@ -23,6 +23,7 @@ class AppColors { static Color? get surfaceAccent => Styles().colors?.getColor('surfaceAccent'); static Color? get background => Styles().colors?.getColor('background'); static Color? get backgroundVariant => Styles().colors?.getColor('backgroundVariant'); + static Color? get shadow => Styles().colors?.getColor('shadow'); static Color? get gradientColorPrimary => Styles().colors?.getColor('gradientColorPrimary'); static Color? get accentColor1 => Styles().colors?.getColor('accentColor1'); static Color? get accentColor2 => Styles().colors?.getColor('accentColor2'); @@ -186,4 +187,5 @@ class AppImages { static Widget? get chevronLeft => Styles().images?.getImage('chevron-left'); static Widget? get chevronRight => Styles().images?.getImage('chevron-right'); static Widget? get close => Styles().images?.getImage('close'); + static Widget? get retryMedium => Styles().images?.getImage('retry-medium'); } diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index 573469d77..5135221ab 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -49,7 +49,9 @@ class RokwirePlugin { 'identifier': identifier, 'identifier2': identifier2 }); } - catch(e) { debugPrint(e.toString()); } + catch(e) { + debugPrint(e.toString()); + } return null; } diff --git a/lib/ui/panels/modal_image_panel.dart b/lib/ui/panels/modal_image_panel.dart index a5bf20a74..b50b8f557 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'; @@ -117,7 +118,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 ?? Colors.white, fontFamily: Styles().fontFamilies?.medium, fontSize: 50),), + Text('\u00D7', style: TextStyle(color: AppColors.textLight ?? Colors.white, fontFamily: AppFontFamilies.medium, fontSize: 50),), ), ) ); @@ -125,7 +126,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 ?? Colors.white), + CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? AppColors.surface ?? Colors.white), value: progress.expectedTotalBytes != null ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null), ); } diff --git a/lib/ui/panels/survey_panel.dart b/lib/ui/panels/survey_panel.dart index 0067ea805..2d61baf4e 100644 --- a/lib/ui/panels/survey_panel.dart +++ b/lib/ui/panels/survey_panel.dart @@ -15,6 +15,7 @@ */ 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'; @@ -71,10 +72,10 @@ class _SurveyPanelState extends State { return Scaffold( appBar: 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 7f0e58d32..fbbc1ca30 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -17,6 +17,7 @@ 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'; @@ -60,7 +61,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,7 +89,7 @@ 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), textAlign: TextAlign.center, padding: flutter_html.HtmlPaddings.zero, margin: flutter_html.Margins.zero), },), + style: { "body": flutter_html.Style(color: AppColors.fillColorPrimary, fontFamily: AppFontFamilies.bold, fontSize: flutter_html.FontSize(32), textAlign: TextAlign.center, padding: flutter_html.HtmlPaddings.zero, margin: flutter_html.Margins.zero), },), ); } @@ -99,7 +100,7 @@ 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), textAlign: TextAlign.left, padding: flutter_html.HtmlPaddings.zero, margin: flutter_html.Margins.zero), },), + style: { "body": flutter_html.Style(color: AppColors.fillColorPrimary, fontFamily: AppFontFamilies.regular, fontSize: flutter_html.FontSize(20), textAlign: TextAlign.left, padding: flutter_html.HtmlPaddings.zero, margin: flutter_html.Margins.zero), },), ); } @@ -189,7 +190,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() diff --git a/lib/ui/popups/alerts.dart b/lib/ui/popups/alerts.dart index 03ca9f798..589c0da7b 100644 --- a/lib/ui/popups/alerts.dart +++ b/lib/ui/popups/alerts.dart @@ -1,5 +1,6 @@ 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'; @@ -26,7 +27,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 f103d9409..1a6fd878e 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'; @@ -71,22 +72,22 @@ class PopupMessage extends StatelessWidget { this.borderRadius, }) : super(key: key); - @protected Color? get defautTitleBarColor => Styles().colors?.fillColorPrimary; + @protected Color? get defautTitleBarColor => AppColors.fillColorPrimary; @protected Color? get displayTitleBarColor => titleBarColor ?? defautTitleBarColor; - @protected Color? get defautTitleTextColor => Styles().colors?.textLight; + @protected Color? get defautTitleTextColor => AppColors.textLight; @protected Color? get displayTitleTextColor => titleTextColor ?? defautTitleTextColor; - @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 defautMessageTextColor => Styles().colors?.fillColorPrimary; + @protected Color? get defautMessageTextColor => AppColors.fillColorPrimary; @protected Color? get displayMessageTextColor => messageTextColor ?? defautMessageTextColor; - @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); @@ -252,22 +253,22 @@ class ActionsMessage extends StatelessWidget { this.borderRadius, }) : super(key: key); - @protected Color? get defautTitleBarColor => Styles().colors?.fillColorPrimary; + @protected Color? get defautTitleBarColor => AppColors.fillColorPrimary; @protected Color? get displayTitleBarColor => titleBarColor ?? defautTitleBarColor; - @protected Color? get defautTitleTextColor => Styles().colors?.textLight; + @protected Color? get defautTitleTextColor => AppColors.textLight; @protected Color? get displayTitleTextColor => titleTextColor ?? defautTitleTextColor; - @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 defautMessageTextColor => Styles().colors?.fillColorPrimary; + @protected Color? get defautMessageTextColor => AppColors.fillColorPrimary; @protected Color? get displayMessageTextColor => messageTextColor ?? defautMessageTextColor; - @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); @@ -279,7 +280,7 @@ class ActionsMessage extends StatelessWidget { @protected ShapeBorder get defautBorder => RoundedRectangleBorder(borderRadius: displayBorderRadius,); @protected ShapeBorder get displayBorder => border ?? defautBorder; - @protected Widget? get defaultCloseButtonIcon => Styles().images?.getImage('close-circle-light', 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({ diff --git a/lib/ui/widget_builders/buttons.dart b/lib/ui/widget_builders/buttons.dart index b5397965a..34ef951c9 100644 --- a/lib/ui/widget_builders/buttons.dart +++ b/lib/ui/widget_builders/buttons.dart @@ -7,8 +7,8 @@ class ButtonBuilder { static Widget standardRoundedButton({String label = '', void Function()? onTap}) { return RoundedButton( label: label, - borderColor: Styles().colors?.fillColorSecondary, - backgroundColor: Styles().colors?.surface, + borderColor: AppColors.fillColorSecondary, + backgroundColor: AppColors.surface, textStyle: AppTextStyles.widgetDetailRegularBold, onTap: onTap, ); diff --git a/lib/ui/widget_builders/scroll_pager.dart b/lib/ui/widget_builders/scroll_pager.dart index c4bef4893..1dd0cbaa4 100644 --- a/lib/ui/widget_builders/scroll_pager.dart +++ b/lib/ui/widget_builders/scroll_pager.dart @@ -31,7 +31,7 @@ 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?.iconMedium)) ?? Container(), + AppImages.retryMedium ?? Container(), const SizedBox(width: 8.0), Text(Localization().getStringEx('widget.scroll_pager.error.title', 'Something went wrong'), style: AppTextStyles.widgetMessageLightRegular), diff --git a/lib/ui/widget_builders/survey.dart b/lib/ui/widget_builders/survey.dart index 26adefeea..89fc085c8 100644 --- a/lib/ui/widget_builders/survey.dart +++ b/lib/ui/widget_builders/survey.dart @@ -56,7 +56,7 @@ class SurveyBuilder { children: [ Text(date ?? '', style: AppTextStyles.widgetDetailSmall), Container(width: 8.0), - Styles().images?.getImage('chevron-right-bold', excludeFromSemantics: true) ?? Container() + AppImages.chevronRight ?? Container() // UIIcon(IconAssets.chevronRight, size: 14.0, color: Styles().colors.headlineText), ], ), @@ -92,7 +92,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) => diff --git a/lib/ui/widgets/expandable_section.dart b/lib/ui/widgets/expandable_section.dart index d72945b72..e71904bc7 100644 --- a/lib/ui/widgets/expandable_section.dart +++ b/lib/ui/widgets/expandable_section.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 ExpandableSection extends StatefulWidget { @@ -34,8 +35,8 @@ class ExpandableSection extends StatefulWidget { @override ExpandableSectionState createState() => ExpandableSectionState(); - @protected Color? get defaultTitleColor => Styles().colors?.textPrimary; - @protected String? get defaultTitleFontFamily => Styles().fontFamilies?.bold; + @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); @@ -43,8 +44,8 @@ class ExpandableSection extends StatefulWidget { @protected Widget get defaultTitleWidget => Text(title ?? '', style: displayTitleStyle); @protected Widget get displayTitleWidget => titleWidget ?? defaultTitleWidget; - @protected Color? get defaultSubtitleColor => Styles().colors?.textDark; - @protected String? get defaultSubtitleFontFamily => Styles().fontFamilies?.bold; + @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); @@ -76,7 +77,7 @@ class ExpandableSectionState extends State { source: _expanded ? '0xf077' : '0xf078', weight: 'solid', size: 18, - color: Styles().colors?.fillColorSecondary + color: AppColors.fillColorSecondary ) ), children: [widget.contents ?? Container(),], diff --git a/lib/ui/widgets/expandable_text.dart b/lib/ui/widgets/expandable_text.dart index b043b1efc..e5d523e91 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 d4edacb63..f2cb33067 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 c2b5814a4..983a5bdb1 100644 --- a/lib/ui/widgets/flex_content.dart +++ b/lib/ui/widgets/flex_content.dart @@ -18,6 +18,7 @@ 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/assets.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/ui/panels/web_panel.dart'; @@ -56,10 +57,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; @@ -98,7 +99,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: @@ -111,7 +112,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? buttonsJsonContent) { @@ -146,9 +147,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), ); diff --git a/lib/ui/widgets/form_field.dart b/lib/ui/widgets/form_field.dart index 957ba33b6..ec0d85610 100644 --- a/lib/ui/widgets/form_field.dart +++ b/lib/ui/widgets/form_field.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/styles.dart'; class FormFieldText extends StatefulWidget { @@ -51,7 +52,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, keyboardType: widget.inputType, @@ -72,7 +73,7 @@ class _FormFieldTextState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(width: 2, color: Styles().colors?.fillColorPrimary ?? Colors.white) + borderSide: BorderSide(width: 2, color: AppColors.fillColorPrimary ?? Colors.white) ) ), controller: widget.controller, diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index fc7dc5d6d..6593832c0 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -81,7 +81,7 @@ class RibbonButton extends StatefulWidget { @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; @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,); @@ -89,7 +89,7 @@ class RibbonButton extends StatefulWidget { @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; diff --git a/lib/ui/widgets/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index b171dc458..6a45dad83 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -17,6 +17,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/styles.dart'; class RoundedButton extends StatefulWidget { @@ -102,19 +103,19 @@ class RoundedButton extends StatefulWidget { this.progressStrokeWidth, }) : super(key: key); - @protected Color? get defaultBackgroundColor => Styles().colors?.surface; + @protected Color? get defaultBackgroundColor => AppColors.surface; @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; - @protected Color? get defaultTextColor => Styles().colors?.textPrimary; + @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 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 ?? const Color(0xFF000000); + @protected Color get defaultBorderColor => AppColors.fillColorSecondary ?? const Color(0xFF000000); @protected Color get displayBorderColor => borderColor ?? defaultBorderColor; @protected Border get defaultBorder => Border.all(color: displayBorderColor, width: borderWidth); @protected Border get displayBorder => border ?? defaultBorder; diff --git a/lib/ui/widgets/rounded_tab.dart b/lib/ui/widgets/rounded_tab.dart index 4bdc4ddaf..edff54e7b 100644 --- a/lib/ui/widgets/rounded_tab.dart +++ b/lib/ui/widgets/rounded_tab.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 RoundedTab extends StatefulWidget { @@ -68,14 +69,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/section.dart b/lib/ui/widgets/section.dart index a6ae7b3c9..fdbf24776 100644 --- a/lib/ui/widgets/section.dart +++ b/lib/ui/widgets/section.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 VerticalTitleValueSection extends StatelessWidget { @@ -70,34 +71,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 ?? Colors.transparent; + @protected Color get defaultBorderColor => AppColors.fillColorSecondary ?? Colors.transparent; @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 8a00d25ea..40d2d7242 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'; @@ -148,7 +149,7 @@ class SectionSlantHeader extends StatelessWidget { slantList.addAll([ Container(color: _slantColor, height: slantPainterHeadingHeight,), 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,), ), ), @@ -218,17 +219,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?.textPrimary, - fontFamily: titleFontFamilly ?? Styles().fontFamilies?.extraBold, + color: titleTextColor ?? AppColors.textPrimary, + fontFamily: titleFontFamilly ?? AppFontFamilies.extraBold, fontSize: titleFontSize ); TextStyle get _subTitleTextStyle => subTitleTextStyle ?? TextStyle( - color: subTitleTextColor ?? Styles().colors?.textPrimary, - fontFamily: subTitleFontFamilly ?? Styles().fontFamilies?.regular, + color: subTitleTextColor ?? AppColors.textPrimary, + fontFamily: subTitleFontFamilly ?? AppFontFamilies.regular, fontSize: subTitleFontSize ); } @@ -364,17 +365,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?.textLight, - fontFamily: titleFontFamilly ?? Styles().fontFamilies?.extraBold, + color: titleTextColor ?? AppColors.textLight, + fontFamily: titleFontFamilly ?? AppFontFamilies.extraBold, fontSize: titleFontSize ); TextStyle get _subTitleTextStyle => subTitleTextStyle ?? TextStyle( - color: subTitleTextColor ?? Styles().colors?.textLight, - fontFamily: subTitleFontFamilly ?? Styles().fontFamilies?.regular, + color: subTitleTextColor ?? AppColors.textLight, + fontFamily: subTitleFontFamilly ?? AppFontFamilies.regular, fontSize: subTitleFontSize ); } @@ -449,10 +450,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?.surface ?? Colors.white), + CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? AppColors.surface ?? Colors.white), 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 c67566350..96c6964b0 100644 --- a/lib/ui/widgets/survey.dart +++ b/lib/ui/widgets/survey.dart @@ -78,8 +78,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?.textDisabled, - borderColor: canContinue ? null : Styles().colors?.textDisabled, + textColor: canContinue ? null : AppColors.textDisabled, + borderColor: canContinue ? null : AppColors.textDisabled, enabled: canContinue && !controller.saving, onTap: controller.continueSurvey, progress: controller.saving), @@ -127,7 +127,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)), ), ); } @@ -386,10 +386,10 @@ class _SurveyWidgetState extends State { }, enabled: enabled, textWidget: Text(option.title, style: AppTextStyles.widgetDetailSmall, 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?.textDisabled)), + 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)), ), ))); } @@ -407,8 +407,8 @@ class _SurveyWidgetState extends State { survey.response = null; } 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) { @@ -430,7 +430,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; @@ -499,13 +499,13 @@ class _SurveyWidgetState extends State { labelText: title, hintText: "MM-dd-yyyy", filled: true, - fillColor: !enabled ? Styles().colors?.textDisabled : Styles().colors?.surface, + fillColor: !enabled ? AppColors.textDisabled : AppColors.surface, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: const BorderSide(color: Colors.white)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(width: 2, color: Styles().colors?.fillColorPrimary ?? Colors.white)), + borderSide: BorderSide(width: 2, color: AppColors.fillColorPrimary ?? Colors.white)), ), controller: dateTextController, // validator: _validationFunctions[field.key], @@ -626,12 +626,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')), )), 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) @@ -656,7 +656,7 @@ class _SurveyWidgetState extends State { 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, + Radio(value: i, groupValue: value, activeColor: AppColors.fillColorPrimary, onChanged: enabled ? (Object? value) { survey.response = value; _onChangeResponse(false); @@ -670,7 +670,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), ) ], ); @@ -913,9 +913,9 @@ class SingleSelectionList extends StatelessWidget { child: RadioListTile( title: Transform.translate(offset: const Offset(-15, 0), child: Text(title, style: AppTextStyles.widgetTitleRegular ?? - TextStyle(fontFamily: Styles().fontFamilies?.regular, - fontSize: 16, color: Styles().colors?.textPrimary))), - activeColor: Styles().colors?.fillColorSecondary, + TextStyle(fontFamily: AppFontFamilies.regular, + fontSize: 16, color: AppColors.textPrimary))), + activeColor: AppColors.fillColorSecondary, value: title, groupValue: selectedValue?.title, onChanged: onChanged != null ? (_) => onChanged!(index) : null, @@ -953,11 +953,11 @@ class MultiSelectionList extends StatelessWidget { title: Transform.translate(offset: const Offset(-15, 0), child: Text(selectionList[index].title, style: AppTextStyles.widgetTitleRegular ?? - TextStyle(fontFamily: Styles().fontFamilies?.regular, - fontSize: 16, color: Styles().colors?.textPrimary))), + TextStyle(fontFamily: AppFontFamilies.regular, + fontSize: 16, color: AppColors.textPrimary))), leading: Checkbox( checkColor: Colors.white, - activeColor: Styles().colors?.fillColorSecondary, + activeColor: AppColors.fillColorSecondary, value: isChecked?[index], onChanged: onChanged != null ? (_) => onChanged!(index) : null, ), diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index 88efecd8d..a6bb53a06 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; +import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/flex_ui.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; @@ -17,7 +18,7 @@ class TabBar extends StatefulWidget { _TabBarState createState() => _TabBarState(); @protected - Color? get backgroundColor => Styles().colors?.surface ?? Colors.white; + Color? get backgroundColor => AppColors.surface ?? Colors.white; @protected BoxBorder? get border => null; @@ -185,7 +186,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?.textMedium, 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).textScaleFactor, 2); @@ -205,7 +206,7 @@ class TabWidget extends StatelessWidget { 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, - color: selected ? Styles().colors?.fillColorSecondary : Styles().colors?.textMedium) ?? defaultIcon : defaultIcon; + color: selected ? AppColors.fillColorSecondary : AppColors.textMedium) ?? defaultIcon : defaultIcon; } @protected @@ -220,7 +221,7 @@ class TabWidget extends StatelessWidget { 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 67193e58f..29de68ee4 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 ?? const Color(0x00FFFFFF); + @protected Color get defaultBorderColor => AppColors.surface ?? const Color(0x00FFFFFF); @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 ?? const Color(0x00FFFFFF); + @protected Color get defaultBorderColor => AppColors.surface ?? const Color(0x00FFFFFF); @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 ?? borderColor; + @protected Color get defaultSelectedBorderColor => AppColors.fillColorPrimary ?? borderColor; @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 ?? Colors.transparent, offset: const Offset(2, 2), blurRadius: 6)]; + @protected List get defaultBorderShadow => [BoxShadow(color: AppColors.shadow ?? Colors.transparent, 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); From 2c888e6d02e8d58eb24963bcf4dc6ed96de40a0b Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 2 Jun 2023 14:10:44 -0500 Subject: [PATCH 039/177] cleanup --- lib/service/firebase_crashlytics.dart | 10 +++++--- lib/service/storage.dart | 36 +++++++++++++-------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/service/firebase_crashlytics.dart b/lib/service/firebase_crashlytics.dart index eda525455..552dc09ad 100644 --- a/lib/service/firebase_crashlytics.dart +++ b/lib/service/firebase_crashlytics.dart @@ -40,11 +40,13 @@ class FirebaseCrashlytics with Service { @override Future initService() async{ - // Enable automatic data collection - google.FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); + if (!kIsWeb) { + // Enable automatic data collection + google.FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); - // Pass all uncaught errors to Firebase.Crashlytics. - FlutterError.onError = handleFlutterError; + // Pass all uncaught errors to Firebase.Crashlytics. + FlutterError.onError = handleFlutterError; + } await super.initService(); } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 084d40d3a..7a41d0ad2 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -35,8 +35,8 @@ class Storage with Service { SharedPreferences? _sharedPreferences; FlutterSecureStorage? _secureStorage; - String? _encryptionKey; - String? _encryptionIV; + // String? _encryptionKey; + // String? _encryptionIV; // Singletone Factory @@ -64,9 +64,9 @@ class Storage with Service { 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); - + // _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, @@ -75,14 +75,14 @@ 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 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(); } @@ -90,11 +90,11 @@ class Storage with Service { // Encryption - String get encryptionKeyId => _ecryptionKeyId; - String? get encryptionKey => _encryptionKey; - - String get encryptionIVId => _encryptionIVId; - String? get encryptionIV => _encryptionIV; + // String get encryptionKeyId => _ecryptionKeyId; + // String? get encryptionKey => _encryptionKey; + // + // String get encryptionIVId => _encryptionIVId; + // String? get encryptionIV => _encryptionIV; // String? encrypt(String? value) { // return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? From 940e959ff87a2d9593a47820bebf703d3076731e Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 5 Jun 2023 16:32:34 -0500 Subject: [PATCH 040/177] cleanup --- lib/service/firebase_crashlytics.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/service/firebase_crashlytics.dart b/lib/service/firebase_crashlytics.dart index 552dc09ad..eda525455 100644 --- a/lib/service/firebase_crashlytics.dart +++ b/lib/service/firebase_crashlytics.dart @@ -40,13 +40,11 @@ class FirebaseCrashlytics with Service { @override Future initService() async{ - if (!kIsWeb) { - // Enable automatic data collection - google.FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); + // Enable automatic data collection + google.FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); - // Pass all uncaught errors to Firebase.Crashlytics. - FlutterError.onError = handleFlutterError; - } + // Pass all uncaught errors to Firebase.Crashlytics. + FlutterError.onError = handleFlutterError; await super.initService(); } From a2678307bbda2ea785102bc097dec0ce50035265 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 6 Jun 2023 14:32:42 -0500 Subject: [PATCH 041/177] cleanup --- example/pubspec.lock | 24 ++++++++++++++++-------- lib/service/auth2.dart | 26 +++++++++++++++++--------- lib/service/config.dart | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index d71c0e4ca..f0b738335 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: device_calendar - sha256: ff10736e53bffa3f0982668ed2affd61abb5f2b3a3c1bba7797ea29d4f616c1e + sha256: "5a1ce7887b4ffbaf3743078c8314dede5e694cddd69bab43f35ce815c5d82a7d" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.1" device_info: dependency: transitive description: @@ -306,10 +306,10 @@ packages: dependency: transitive description: name: flutter_local_notifications - sha256: e76db45e04af231d3b2b1832bb5a624ad895b64fec3d0ca35a514daa19f16743 + sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "12.0.4" flutter_local_notifications_linux: dependency: transitive description: @@ -334,6 +334,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_passkey: + dependency: transitive + description: + name: flutter_passkey + sha256: "4a7e9085ab9602c7ee472d3650367b2ee4bc5e3ba15142e38dd32a732e763530" + url: "https://pub.dev" + source: hosted + version: "1.0.3" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -744,10 +752,10 @@ packages: dependency: transitive description: name: sprintf - sha256: ec76d38910b6f1c854ce1353c62d37e7ef82b53dc5ab048c25400d35970776d1 + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.0" sqflite: dependency: transitive description: @@ -816,10 +824,10 @@ packages: dependency: transitive description: name: timezone - sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e" + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.2" typed_data: dependency: transitive description: diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index ad3300cc5..2312d3b20 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -359,15 +359,16 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(response.body)); Map? requestJson = JsonUtils.decode(message?.message ?? ''); Map? pubKeyRequest = requestJson?['publicKey']; - pubKeyRequest?.remove('allowCredentials'); - pubKeyRequest?['userVerification'] = 'required'; - pubKeyRequest = { - "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo", - // "allowCredentials": [], - "timeout": 1800000, - "userVerification": "required", - "rpId": "university.app.services.rokmetro.com" - }; + // pubKeyRequest?.remove('allowCredentials'); + // pubKeyRequest?['userVerification'] = 'required'; + // pubKeyRequest?['allowCredentials'] = []; + // pubKeyRequest = { + // "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo", + // // "allowCredentials": [], + // "timeout": 1800000, + // "userVerification": "required", + // "rpId": "university.app.services.rokmetro.com" + // }; try { // await RokwirePlugin.getPasskey(JsonUtils.encode(pubKeyRequest) ?? ''); String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); @@ -459,6 +460,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Map? pubKeyRequest = requestJson?['publicKey']; try { // await RokwirePlugin.createPasskey(JsonUtils.encode(pubKeyRequest) ?? ''); + // pubKeyRequest?['attestation'] = "none"; + // pubKeyRequest?['excludeCredentials'] = []; + // pubKeyRequest?['authenticatorSelection']?['authenticatorAttachment'] = "platform"; + // pubKeyRequest?['authenticatorSelection']?['requireResidentKey'] = true; + // pubKeyRequest?['authenticatorSelection']?['residentKey'] = 'required'; + // pubKeyRequest?['authenticatorSelection']?['userVerification'] = 'required'; + String responseData = await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); return _completeSignUpWithPasskey(username, responseData); } catch(error) { diff --git a/lib/service/config.dart b/lib/service/config.dart index 46a52ac02..1dc58b8ec 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -224,7 +224,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } @protected - Map? configFromJsonObjectString(String? configJsonString) async { + Map? configFromJsonObjectString(String? configJsonString) { Map? configJson = JsonUtils.decode(configJsonString); Map? configData = configJson?["data"]; if (configData != null) { @@ -235,7 +235,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } @protected - Map? configFromJsonListString(String? configJsonString) { + Future?> configFromJsonListString(String? configJsonString) async { List? jsonList = await JsonUtils.decodeListAsync(configJsonString); if (jsonList != null) { diff --git a/pubspec.yaml b/pubspec.yaml index 66e399540..ccd31c761 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: webview_flutter: ^2.0.13 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.2.1 - flutter_passkey: ^1.0.2 + flutter_passkey: ^1.0.3 #Firebase firebase_core: ^2.13.0 From 8c8e8088b2f3e9429ffe05eef5c90e43298d8f95 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 8 Jun 2023 10:14:17 -0500 Subject: [PATCH 042/177] merge plugin develop --- lib/service/config.dart | 2 +- lib/service/geo_fence.dart | 1 + lib/ui/widgets/flex_content.dart | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/service/config.dart b/lib/service/config.dart index 0c0197b00..305aa10b6 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -235,7 +235,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } @protected - Future>? configFromJsonListString(String? configJsonString) async { + Future?> configFromJsonListString(String? configJsonString) async { List? jsonList = await JsonUtils.decodeListAsync(configJsonString); if (jsonList != null) { jsonList.sort((dynamic cfg1, dynamic cfg2) { diff --git a/lib/service/geo_fence.dart b/lib/service/geo_fence.dart index 67cb4fb1b..b1720b089 100644 --- a/lib/service/geo_fence.dart +++ b/lib/service/geo_fence.dart @@ -22,6 +22,7 @@ import 'package:collection/collection.dart'; import 'package:rokwire_plugin/model/geo_fence.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; import 'package:rokwire_plugin/service/app_lifecycle.dart'; +import 'package:rokwire_plugin/service/content.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; import 'package:rokwire_plugin/service/storage.dart'; diff --git a/lib/ui/widgets/flex_content.dart b/lib/ui/widgets/flex_content.dart index b97c2e069..f4a42f8b0 100644 --- a/lib/ui/widgets/flex_content.dart +++ b/lib/ui/widgets/flex_content.dart @@ -20,6 +20,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:rokwire_plugin/gen/styles.dart'; import 'package:rokwire_plugin/service/assets.dart'; +import 'package:rokwire_plugin/service/content.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'; From 0b5aa787e970c5e47ca95a777d44d1253b4a2156 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 9 Jun 2023 09:32:46 -0500 Subject: [PATCH 043/177] fixes --- example/pubspec.lock | 60 ++++++++++++++++++++++++++++++++++++---- lib/model/auth2.dart | 2 +- lib/service/auth2.dart | 9 ++---- lib/service/flex_ui.dart | 6 ++++ 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index f0b738335..8b6746ae2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -290,10 +290,10 @@ packages: dependency: transitive description: name: flutter_html - sha256: "850c07bc6c1ed060d3eb3e88469a598260a13eb45d8978b197c1348e0a2b101f" + sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" url: "https://pub.dev" source: hosted - version: "3.0.0-beta.1" + version: "3.0.0-beta.2" flutter_lints: dependency: "direct dev" description: @@ -350,6 +350,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.15" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + url: "https://pub.dev" + source: hosted + version: "1.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + url: "https://pub.dev" + source: hosted + version: "2.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -388,18 +436,18 @@ 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: "6cd3c622df085a79fd61f5c14fa024c3ba593aa6b1df2ee809ac59f45e6a9861" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "4.1.8" geolocator_apple: dependency: transitive description: diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index ccffc0606..3db0b9cd4 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -71,7 +71,7 @@ class Auth2Token { //////////////////////////////// // Auth2LoginType -enum Auth2LoginType { anonymous, apiKey, email, phone, username, phoneTwilio, oidc, oidcIllinoisoidcIllinois, passkey } +enum Auth2LoginType { anonymous, apiKey, email, phone, username, phoneTwilio, oidc, oidcIllinois, passkey } String? auth2LoginTypeToString(Auth2LoginType value) { switch (value) { diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 9e22c0eed..a89771de9 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -478,7 +478,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // pubKeyRequest?['authenticatorSelection']?['residentKey'] = 'required'; // pubKeyRequest?['authenticatorSelection']?['userVerification'] = 'required'; - String responseData = await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + String? jsonRequest = JsonUtils.encode(pubKeyRequest) ?? ''; + String responseData = await flutterPasskeyPlugin.createCredential(jsonRequest); return _completeSignUpWithPasskey(username, responseData); } catch(error) { try { @@ -519,11 +520,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Response? response = await Network().post(url, headers: headers, body: post); if (response != null && response.statusCode == 200) { - Map? responseJson = JsonUtils.decode(response.body); - bool success = await processLoginResponse(responseJson); - if (success) { - return Auth2PasskeySignUpResult.succeeded; - } + return Auth2PasskeySignUpResult.succeeded; } } return Auth2PasskeySignUpResult.failed; diff --git a/lib/service/flex_ui.dart b/lib/service/flex_ui.dart index b384471df..3a11ae9e6 100644 --- a/lib/service/flex_ui.dart +++ b/lib/service/flex_ui.dart @@ -602,6 +602,9 @@ class FlexUI with Service implements NotificationsListener { else if ((key == 'usernameLoggedIn') && (value is bool)) { result = result && (Auth2().isUsernameLoggedIn == value); } + else if ((key == 'passkeyLoggedIn') && (value is bool)) { + result = result && (Auth2().isPasskeyLoggedIn == value); + } else if ((key == 'phoneOrEmailLoggedIn') && (value is bool)) { result = result && ((Auth2().isPhoneLoggedIn || Auth2().isEmailLoggedIn) == value) ; } @@ -617,6 +620,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); } From 3ebbce8f28691dc58aa833bc7a6ef4cfd541020a Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 9 Jun 2023 12:38:04 -0500 Subject: [PATCH 044/177] change linked domain --- lib/service/auth2.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 6aa44ddff..bfb107a71 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -367,7 +367,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Response? response = await Network().post(url, headers: headers, body: post); if (response != null && response.statusCode == 200) { // Obtain creationOptions from the server - Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(response.body)); + String? responseBody = response.body; + Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(responseBody)); Map? requestJson = JsonUtils.decode(message?.message ?? ''); Map? pubKeyRequest = requestJson?['publicKey']; // pubKeyRequest?.remove('allowCredentials'); @@ -484,10 +485,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } catch(error) { try { String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); - Auth2PasskeySignInResult result = await _completeSignInWithPasskey(username, responseData); - if (result == Auth2PasskeySignInResult.succeeded) { - return Auth2PasskeySignUpResult.succeeded; - } + Auth2PasskeySignInResult result = await _completeSignInWithPasskey(username, responseData); + if (result == Auth2PasskeySignInResult.succeeded) { + return Auth2PasskeySignUpResult.succeeded; + } } catch(error) { Log.e(error.toString()); } From 932cbddd2aa52bb7f75ba6df71b94d9684f577b3 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander <46940735+roberlander2@users.noreply.github.com> Date: Fri, 9 Jun 2023 17:22:47 -0500 Subject: [PATCH 045/177] [#291] Web app authentication support (#295) * first web auth implementation [#291] * upgrade some dependencies, modifications to get web apps working (without auth) * bug fixes, better error handling [#291] * bug fixes, better error handling, remove hardcoded URL for web identifier [#291] * improve Auth2Csrf headers, implement web strategy for app configs [#291] * update changelog * logout bug fix * fix errors * fix merge issues --------- Co-authored-by: Stephen Hurwit --- CHANGELOG.md | 1 + example/pubspec.lock | 144 ++++++- lib/model/auth2.dart | 2 +- lib/service/analytics.dart | 2 +- lib/service/auth2.dart | 483 +++++++++++++++-------- lib/service/config.dart | 84 ++-- lib/service/deep_link.dart | 12 +- lib/service/firebase_crashlytics.dart | 27 +- lib/service/firebase_messaging.dart | 6 +- lib/service/flex_ui.dart | 30 +- lib/service/geo_fence.dart | 1 - lib/service/inbox.dart | 5 +- lib/service/localization.dart | 7 +- lib/service/onboarding.dart | 4 +- lib/service/storage.dart | 9 +- lib/service/styles.dart | 6 +- lib/ui/panels/survey_panel.dart | 1 - lib/ui/panels/web_panel.dart | 1 - lib/ui/popups/alerts.dart | 1 - lib/ui/widget_builders/buttons.dart | 1 - lib/ui/widget_builders/scroll_pager.dart | 1 - lib/ui/widget_builders/survey.dart | 1 - lib/ui/widgets/flex_content.dart | 6 +- lib/ui/widgets/rounded_button.dart | 1 - lib/ui/widgets/rounded_tab.dart | 1 - lib/ui/widgets/section.dart | 1 - lib/utils/utils.dart | 16 + pubspec.yaml | 23 +- 28 files changed, 589 insertions(+), 288 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9acc13b5f..46b1fa97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Cleaned up Explore interface [#289](https://github.com/rokwire/app-flutter-plugin/issues/289). ### Added - Survey creation tool [#263](https://github.com/rokwire/app-flutter-plugin/issues/263). +- Web app authentication support [#291](https://github.com/rokwire/app-flutter-plugin/issues/291) ### Fixed - Upgrade dependencies for Flutter v3.10 [#285](https://github.com/rokwire/app-flutter-plugin/issues/285) diff --git a/example/pubspec.lock b/example/pubspec.lock index d71c0e4ca..65ae26d3f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -49,6 +49,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: @@ -157,10 +165,10 @@ packages: dependency: transitive description: name: device_calendar - sha256: ff10736e53bffa3f0982668ed2affd61abb5f2b3a3c1bba7797ea29d4f616c1e + sha256: "5a1ce7887b4ffbaf3743078c8314dede5e694cddd69bab43f35ce815c5d82a7d" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.1" device_info: dependency: transitive description: @@ -290,10 +298,10 @@ packages: dependency: transitive description: name: flutter_html - sha256: "850c07bc6c1ed060d3eb3e88469a598260a13eb45d8978b197c1348e0a2b101f" + sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" url: "https://pub.dev" source: hosted - version: "3.0.0-beta.1" + version: "3.0.0-beta.2" flutter_lints: dependency: "direct dev" description: @@ -306,10 +314,10 @@ packages: dependency: transitive description: name: flutter_local_notifications - sha256: e76db45e04af231d3b2b1832bb5a624ad895b64fec3d0ca35a514daa19f16743 + sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "12.0.4" flutter_local_notifications_linux: dependency: transitive description: @@ -342,11 +350,75 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.15" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + url: "https://pub.dev" + source: hosted + version: "1.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + url: "https://pub.dev" + source: hosted + version: "2.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: ee636fa43af689df8ace6421ea5f5e14b8b3eda0e8caa3e5a1e3bb71f0833636 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + 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 @@ -380,18 +452,18 @@ 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: "6cd3c622df085a79fd61f5c14fa024c3ba593aa6b1df2ee809ac59f45e6a9861" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "4.1.8" geolocator_apple: dependency: transitive description: @@ -560,14 +632,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - package_info: + package_info_plus: dependency: transitive description: - name: package_info - sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" + name: package_info_plus + sha256: ceb027f6bc6a60674a233b4a90a7658af1aebdea833da0b5b53c1e9821a78c7b url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "4.0.2" + 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: @@ -744,10 +824,10 @@ packages: dependency: transitive description: name: sprintf - sha256: ec76d38910b6f1c854ce1353c62d37e7ef82b53dc5ab048c25400d35970776d1 + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.0" sqflite: dependency: transitive description: @@ -816,10 +896,10 @@ packages: dependency: transitive description: name: timezone - sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e" + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.2" typed_data: dependency: transitive description: @@ -852,6 +932,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: a5cc5a84188e5d3e58f3ed77fe3dd4575dc1f68aa7c89e51b5b4105b9aab3b9d + url: "https://pub.dev" + source: hosted + version: "2.2.3" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: transitive description: @@ -936,10 +1032,10 @@ packages: dependency: transitive description: name: webview_flutter - sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" + sha256: "392c1d83b70fe2495de3ea2c84531268d5b8de2de3f01086a53334d8b6030a88" url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "3.0.4" webview_flutter_android: dependency: transitive description: @@ -972,6 +1068,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.4" + 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: diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 31d00db80..07a5ee9d4 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -60,7 +60,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 { diff --git a/lib/service/analytics.dart b/lib/service/analytics.dart index ffacc6302..c0c31c500 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'; diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 5b0bb147b..1277ed8ac 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -16,6 +16,8 @@ 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 { @@ -105,11 +107,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _deviceId = await RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2); if ((_account == null) && (_anonymousPrefs == null)) { - await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonimousPrefs); + await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonymousPrefs); } if ((_account == null) && (_anonymousProfile == null)) { - await Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonimousProfile); + await Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonymousProfile); } if ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { @@ -123,7 +125,16 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - _refreshAccount(); + if (kIsWeb && _token == null) { + refreshToken(ignoreUnauthorized: true).then((token) { + if (token != null) { + _refreshAccount(); + NotificationService().notify(Auth2.notifyLoginSucceeded, null); + } + }); + } else { + _refreshAccount(); + } await super.initService(); } @@ -137,7 +148,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) { @@ -176,14 +188,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); } } @@ -205,7 +220,7 @@ 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); + return (await refreshToken(token: token) != null); } return false; } @@ -276,10 +291,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Overrides @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; @@ -290,20 +305,23 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Anonymous Authentication Future authenticateAnonymously() async { - if (Config().supportsAnonymousAuth && (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' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(Auth2LoginType.anonymous), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, - 'device': deviceInfo - }); + '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'])); @@ -325,7 +343,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // OIDC Authentication Future authenticateWithOidc({bool? link}) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { + if (Config().authBaseUrl != null) { if (_oidcAuthenticationCompleters == null) { _oidcAuthenticationCompleters = >[]; @@ -375,25 +393,28 @@ 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"; + if (Config().authBaseUrl != null) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(oidcLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, '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 false; + } _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); @@ -426,10 +447,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { bool? prefsUpdated = account.prefs?.apply(_anonymousPrefs); bool? profileUpdated = account.profile?.apply(_anonymousProfile); - await Storage().setAuth2Token(_token = token); - await Storage().setAuth2Account(_account = account); + _token = token; + _account = account; await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = null); await Storage().setAuth2AnonymousProfile(_anonymousProfile = null); + if (!kIsWeb) { + await Storage().setAuth2Token(token); + await Storage().setAuth2Account(account); + } if (prefsUpdated == true) { _saveAccountUserPrefs(); @@ -446,20 +471,23 @@ 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({ + Map postData = { '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); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + postData['redirect_uri'] = oidcRedirectUrl; + } else { + return null; + } + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); return _OidcLogin.fromJson(JsonUtils.decodeMap(response?.body)); } return null; @@ -507,27 +535,30 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Phone Authentication Future authenticateWithPhone(String? phoneNumber) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (phoneNumber != null)) { + if ((Config().authBaseUrl != null) && (phoneNumber != null)) { NotificationService().notify(notifyLoginStarted, phoneLoginType); - String url = "${Config().coreUrl}/services/auth/login"; + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(phoneLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "phone": phoneNumber, }, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PhoneRequestCodeResult.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; } @@ -539,16 +570,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } Future handlePhoneAuthentication(String? phoneNumber, String? code) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (phoneNumber != null) && (code != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + if ((Config().authBaseUrl != null) && (phoneNumber != null) && (code != null)) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(phoneLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "phone": phoneNumber, "code": code, @@ -556,9 +584,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PhoneSendCodeResult.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)); _notifyLogin(phoneLoginType, result); @@ -578,19 +612,16 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Email Authentication Future authenticateWithEmail(String? email, String? password) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (email != null) && (password != null)) { + if ((Config().authBaseUrl != null) && (email != null) && (password != null)) { NotificationService().notify(notifyLoginStarted, emailLoginType); - String url = "${Config().coreUrl}/services/auth/login"; + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "email": email, "password": password @@ -598,9 +629,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2EmailSignInResult.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)); _notifyLogin(emailLoginType, result); @@ -624,16 +661,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } 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"; + if ((Config().authBaseUrl != null) && (email != null) && (password != null)) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "email": email, "password": password @@ -645,9 +679,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2EmailSignUpResult.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; } @@ -659,20 +699,23 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } 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"; + if ((Config().authBaseUrl != null) && (email != null)) { + String url = "${Config().authBaseUrl}/auth/account/exists"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'user_identifier': email, - }); + }; + 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; @@ -682,21 +725,24 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } 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"; + if ((Config().authBaseUrl != null) && (email != null)) { + String url = "${Config().authBaseUrl}/auth/credential/forgot/initiate"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'user_identifier': email, 'identifier': email, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2EmailForgotPasswordResult.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; } @@ -714,21 +760,24 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } 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"; + if ((Config().authBaseUrl != null) && (email != null)) { + String url = "${Config().authBaseUrl}/auth/credential/send-verify"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(emailLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'user_identifier': email, 'identifier': email, - }); + }; + 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; @@ -737,19 +786,16 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Username Authentication Future authenticateWithUsername(String? username, String? password) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null) && (password != null)) { + if ((Config().authBaseUrl != null) && (username != null) && (password != null)) { NotificationService().notify(notifyLoginStarted, usernameLoginType); - String url = "${Config().coreUrl}/services/auth/login"; + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "username": username, "password": password @@ -760,9 +806,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2UsernameSignInResult.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)); _notifyLogin(usernameLoginType, result); @@ -782,16 +834,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } Future signUpWithUsername(String? username, String? password) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null) && (password != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + if ((Config().authBaseUrl != null) && (username != null) && (password != null)) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "username": username, "password": password @@ -803,9 +852,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2UsernameSignUpResult.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)); _notifyLogin(usernameLoginType, result); @@ -819,20 +874,23 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } 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"; + if ((Config().authBaseUrl != null) && (username != null)) { + String url = "${Config().authBaseUrl}/auth/account/exists"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'user_identifier': username, - }); + }; + 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; @@ -853,20 +911,23 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // 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"; + 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({ + Map postData = { 'auth_type': auth2LoginTypeToString(loginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'user_identifier': 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) { return JsonUtils.boolValue(JsonUtils.decode(response?.body))!; } @@ -875,20 +936,23 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } 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"; + if ((Config().authBaseUrl != null) && (identifier != null)) { + String url = "${Config().authBaseUrl}/auth/account/can-link"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(loginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'user_identifier': 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) { return JsonUtils.boolValue(JsonUtils.decode(response?.body))!; } @@ -970,9 +1034,9 @@ 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, }; } @@ -982,6 +1046,16 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _log("Auth2: logout"); _refreshTokenFailCounts.remove(_token?.refreshToken); + if (Config().authBaseUrl != null) { + Map headers = { + 'Content-Type': 'application/json' + }; + String? body = JsonUtils.encode({ + 'all_sessions': false, + }); + Network().post("${Config().authBaseUrl}/auth/logout", headers: headers, body: body, auth: Auth2Csrf(token: token)); + } + await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = prefs ?? _account?.prefs ?? Auth2UserPrefs.empty()); await Storage().setAuth2AnonymousProfile(_anonymousProfile = Auth2UserProfile.empty()); await Storage().setAuth2Token(_token = null); @@ -1028,32 +1102,35 @@ 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 { + if (Config().authBaseUrl != null) { try { - Future? refreshTokenFuture = _refreshTokenFutures[token.refreshToken]; + Future? refreshTokenFuture = token?.refreshToken != null ? _refreshTokenFutures[token!.refreshToken] : null; if (refreshTokenFuture != null) { - _log("Auth2: will await refresh token:"); + _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}\n"); + _log("Auth2: did await refresh token: ${responseToken?.isValid}\nSource Token: ${token?.refreshToken}"); return ((responseToken != null) && responseToken.isValid) ? responseToken : null; } else { - _log("Auth2: will refresh token:\n"); + _log("Auth2: will refresh token:\nSource Token: ${token?.refreshToken}"); - _refreshTokenFutures[token.refreshToken!] = refreshTokenFuture = _refreshToken(token.refreshToken); + refreshTokenFuture = _refreshToken(token?.refreshToken); + if (token?.refreshToken != null) { + _refreshTokenFutures[token!.refreshToken!]; + } Response? response = await refreshTokenFuture; - _refreshTokenFutures.remove(token.refreshToken); + _refreshTokenFutures.remove(token?.refreshToken); 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:\n"); - _refreshTokenFailCounts.remove(token.refreshToken); + _log("Auth2: did refresh token:\nResponse Token: ${responseToken.refreshToken}\nSource Token: ${token?.refreshToken}"); + _refreshTokenFailCounts.remove(token?.refreshToken); if (token == _token) { applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); @@ -1066,9 +1143,12 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\n"); - int refreshTokenFailCount = (_refreshTokenFailCounts[token.refreshToken] ?? 0) + 1; - if (((response?.statusCode == 400) || (response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTokenFailCount)) { + _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token?.refreshToken}"); + int refreshTokenFailCount = 1; + if (token?.refreshToken != null) { + refreshTokenFailCount += _refreshTokenFailCounts[token!.refreshToken!] ?? 0; + } + if (((response?.statusCode == 400) || (!ignoreUnauthorized && response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTokenFailCount)) { if (token == _token) { logout(); } @@ -1076,14 +1156,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { await authenticateAnonymously(); } } - else { - _refreshTokenFailCounts[token.refreshToken!] = refreshTokenFailCount; + else if (token?.refreshToken != null) { + _refreshTokenFailCounts[token!.refreshToken!] = refreshTokenFailCount; } } } catch(e) { debugPrint(e.toString()); - _refreshTokenFutures.remove(token.refreshToken); // make sure to clear this in case something went wrong. + _refreshTokenFutures.remove(token?.refreshToken); // make sure to clear this in case something went wrong. } } return null; @@ -1091,22 +1171,31 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Future applyToken(Auth2Token token, { Map? params }) async { - await Storage().setAuth2Token(_token = token); + _token = token; + if (!kIsWeb) { + await Storage().setAuth2Token(token); + } } static Future _refreshToken(String? refreshToken) async { - if ((Config().coreUrl != null) && (refreshToken != null)) { - String url = "${Config().coreUrl}/services/auth/refresh"; + if (Config().authBaseUrl != null) { + String url = "${Config().authBaseUrl}/auth/refresh"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ - 'api_key': Config().rokwireApiKey, - 'refresh_token': refreshToken - }); + String? post; + if (!kIsWeb) { + if (refreshToken == null) { + return 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; } @@ -1302,10 +1391,16 @@ 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: Uri.tryParse(urlStr)?.host ?? '').then((String url) { + onDeepLinkUri(Uri.tryParse(url)); + }); + } else if (await canLaunchUrlString(urlStr)) { + await launchUrlString(urlStr, mode: Platform.isAndroid ? LaunchMode.externalApplication : LaunchMode.platformDefault); + } } } catch(e) { @@ -1317,6 +1412,17 @@ 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; + } } class _OidcLogin { @@ -1341,6 +1447,45 @@ class _OidcLogin { } +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[HttpHeaders.authorizationHeader] = "$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)) { + return (await Auth2().refreshToken(token: token) != null); + } + return false; + } +} + // Auth2PhoneRequestCodeResult enum Auth2PhoneRequestCodeResult { diff --git a/lib/service/config.dart b/lib/service/config.dart index 305aa10b6..d39b68adb 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -20,18 +20,20 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:flutter/services.dart' show rootBundle; +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/connectivity.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 { @@ -95,8 +97,10 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { _configEnvironment = configEnvFromString(Storage().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}'); + } await init(); await super.initService(); @@ -205,12 +209,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, headers: {'content-type': 'application/json'}); + 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()); @@ -289,25 +298,31 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @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.', - ); + 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.', + ); + } } - _config = await loadFromFile(configFile); + if (!kIsWeb) { + _config = await loadFromFile(configFile); + } if (_config == null) { - _configAsset = await loadFromAssets(); + 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(); @@ -342,6 +357,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; } @@ -350,7 +368,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); } @@ -359,10 +377,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; } @@ -383,17 +403,18 @@ 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 { String? assetUrl = (_configAsset != null) ? JsonUtils.stringValue(_configAsset!['config_url']) : null; - return assetUrl ?? JsonUtils.stringValue(platformBuildingBlocks['appconfig_url']); + return assetUrl ?? JsonUtils.stringValue(platformBuildingBlocks['appconfig_url']) ?? (kIsWeb ? "$authBaseUrl/application/configs" : null); } String? get rokwireApiKey { @@ -456,7 +477,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 { @@ -539,6 +560,17 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { String? get contentUrl => JsonUtils.stringValue(platformBuildingBlocks["content_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/admin'; + } + return '$coreUrl/services'; + } + // Getters: otherUniversityServices String? get assetsUrl => JsonUtils.stringValue(otherUniversityServices['assets_url']); @@ -556,6 +588,10 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { 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 bypassLogin => true; // Bypass login for testing web layouts + bool get isReleaseWeb => kIsWeb && !kDebugMode; } enum ConfigEnvironment { production, test, dev } diff --git a/lib/service/deep_link.dart b/lib/service/deep_link.dart index acdcfd091..34972a32d 100644 --- a/lib/service/deep_link.dart +++ b/lib/service/deep_link.dart @@ -50,11 +50,13 @@ class DeepLink with Service { }); // 2. Updated uri - uriLinkStream.listen((Uri? uri) { - if (uri != null) { - NotificationService().notify(notifyUri, uri); - } - }); + if (!kIsWeb) { + uriLinkStream.listen((Uri? uri) { + if (uri != null) { + NotificationService().notify(notifyUri, uri); + } + }); + } await super.initService(); } 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 a579de1ad..b3082d52d 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -15,11 +15,11 @@ */ import 'dart:async'; -import 'dart:io'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:firebase_messaging/firebase_messaging.dart' as firebase_messaging; 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'; @@ -93,7 +93,7 @@ class FirebaseMessaging with Service { @override Set get serviceDependsOn { - return { FirebaseCore(), Storage(), }; + return { FirebaseCore(), Storage(), Config() }; } Future get authorizationStatus async { @@ -105,7 +105,7 @@ class FirebaseMessaging with Service { 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 (Platform.isAndroid) { + if (Config().operatingSystem == "android") { return (authorizationStatus != firebase_messaging.AuthorizationStatus.denied); } else { return (authorizationStatus == firebase_messaging.AuthorizationStatus.notDetermined); diff --git a/lib/service/flex_ui.dart b/lib/service/flex_ui.dart index 3588e1cd0..7311dc47e 100644 --- a/lib/service/flex_ui.dart +++ b/lib/service/flex_ui.dart @@ -90,10 +90,12 @@ 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); + _appContentSource = kIsWeb ? null : await loadFromAssets(appAssetsKey); + if (!kIsWeb) { + _assetsDir = await getAssetsDir(); + _netContentSource = await loadFromCache(netCacheFileName); + } build(); if (_defaultContent != null) { updateFromNet(); @@ -213,7 +215,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 +225,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); + } } } @@ -643,7 +647,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 b1720b089..453118ceb 100644 --- a/lib/service/geo_fence.dart +++ b/lib/service/geo_fence.dart @@ -21,7 +21,6 @@ import 'package:collection/collection.dart'; import 'package:rokwire_plugin/model/geo_fence.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; -import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/content.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; diff --git a/lib/service/inbox.dart b/lib/service/inbox.dart index cbf5f9313..4c2cc1755 100644 --- a/lib/service/inbox.dart +++ b/lib/service/inbox.dart @@ -1,6 +1,3 @@ - -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:rokwire_plugin/model/inbox.dart'; @@ -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 d3d04c5d7..9db0a3288 100644 --- a/lib/service/localization.dart +++ b/lib/service/localization.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 { @@ -147,7 +148,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(); } @@ -156,7 +157,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(); } @@ -226,7 +227,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 ((jsonData != null) && ((cache == null) || !const DeepCollectionEquality().equals(jsonData, cache))) { diff --git a/lib/service/onboarding.dart b/lib/service/onboarding.dart index 47c358b68..2df7e1c86 100644 --- a/lib/service/onboarding.dart +++ b/lib/service/onboarding.dart @@ -139,9 +139,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; } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 7a41d0ad2..6e1e672fd 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -18,10 +18,10 @@ 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'; +// import 'package:rokwire_plugin/rokwire_plugin.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; -import 'package:rokwire_plugin/utils/crypt.dart'; +// import 'package:rokwire_plugin/utils/crypt.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -29,8 +29,8 @@ 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 _encryptionIVId = 'edu.illinois.rokwire.encryption.storage.iv'; + // static const String _ecryptionKeyId = 'edu.illinois.rokwire.encryption.storage.key'; + // static const String _encryptionIVId = 'edu.illinois.rokwire.encryption.storage.iv'; SharedPreferences? _sharedPreferences; FlutterSecureStorage? _secureStorage; @@ -66,7 +66,6 @@ class Storage with Service { // _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, diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 2cebca5c3..a652ad0a6 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -31,7 +31,7 @@ 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'; - +import 'package:flutter/foundation.dart' show kIsWeb; class Styles extends Service implements NotificationsListener{ @@ -94,7 +94,7 @@ class Styles extends Service implements NotificationsListener{ _assetsDir = await getAssetsDir(); _assetsManifest = await loadAssetsManifest(); _assetsStyles = await loadFromAssets(assetsKey); - _appAssetsStyles = await loadFromAssets(appAssetsKey); + _appAssetsStyles = kIsWeb ? null : await loadFromAssets(appAssetsKey); _netAssetsStyles = await loadFromCache(netCacheFileName); _debugAssetsStyles = await loadFromCache(debugCacheFileName); @@ -208,7 +208,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; } diff --git a/lib/ui/panels/survey_panel.dart b/lib/ui/panels/survey_panel.dart index 7e36ca4e1..53bb88573 100644 --- a/lib/ui/panels/survey_panel.dart +++ b/lib/ui/panels/survey_panel.dart @@ -18,7 +18,6 @@ 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'; diff --git a/lib/ui/panels/web_panel.dart b/lib/ui/panels/web_panel.dart index b8d89d3d2..adaa18d16 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -26,7 +26,6 @@ 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'; diff --git a/lib/ui/popups/alerts.dart b/lib/ui/popups/alerts.dart index f85d75f2e..5d9d53b61 100644 --- a/lib/ui/popups/alerts.dart +++ b/lib/ui/popups/alerts.dart @@ -3,7 +3,6 @@ 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'; diff --git a/lib/ui/widget_builders/buttons.dart b/lib/ui/widget_builders/buttons.dart index 34ef951c9..56ed5b364 100644 --- a/lib/ui/widget_builders/buttons.dart +++ b/lib/ui/widget_builders/buttons.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:rokwire_plugin/gen/styles.dart'; -import 'package:rokwire_plugin/service/styles.dart'; import 'package:rokwire_plugin/ui/widgets/rounded_button.dart'; class ButtonBuilder { diff --git a/lib/ui/widget_builders/scroll_pager.dart b/lib/ui/widget_builders/scroll_pager.dart index 1dd0cbaa4..5a2ef9211 100644 --- a/lib/ui/widget_builders/scroll_pager.dart +++ b/lib/ui/widget_builders/scroll_pager.dart @@ -1,7 +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'; diff --git a/lib/ui/widget_builders/survey.dart b/lib/ui/widget_builders/survey.dart index f3f85a3b2..7be037a25 100644 --- a/lib/ui/widget_builders/survey.dart +++ b/lib/ui/widget_builders/survey.dart @@ -4,7 +4,6 @@ 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'; diff --git a/lib/ui/widgets/flex_content.dart b/lib/ui/widgets/flex_content.dart index f4a42f8b0..f65aed4ee 100644 --- a/lib/ui/widgets/flex_content.dart +++ b/lib/ui/widgets/flex_content.dart @@ -14,13 +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/assets.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'; @@ -189,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/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index 6a45dad83..3014ae50f 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -18,7 +18,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:rokwire_plugin/gen/styles.dart'; -import 'package:rokwire_plugin/service/styles.dart'; class RoundedButton extends StatefulWidget { final String label; diff --git a/lib/ui/widgets/rounded_tab.dart b/lib/ui/widgets/rounded_tab.dart index edff54e7b..f9acb6605 100644 --- a/lib/ui/widgets/rounded_tab.dart +++ b/lib/ui/widgets/rounded_tab.dart @@ -16,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:rokwire_plugin/gen/styles.dart'; -import 'package:rokwire_plugin/service/styles.dart'; class RoundedTab extends StatefulWidget { final String? title; diff --git a/lib/ui/widgets/section.dart b/lib/ui/widgets/section.dart index fdbf24776..da7a79e5c 100644 --- a/lib/ui/widgets/section.dart +++ b/lib/ui/widgets/section.dart @@ -16,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:rokwire_plugin/gen/styles.dart'; -import 'package:rokwire_plugin/service/styles.dart'; class VerticalTitleValueSection extends StatelessWidget { final String? title; diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 6f9420142..be5faa7d2 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -27,6 +27,7 @@ import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.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 { @@ -1268,6 +1269,21 @@ class TZDateTimeUtils { static timezone.TZDateTime max(timezone.TZDateTime v1, timezone.TZDateTime v2) => (v1.isAfter(v2)) ? v1 : v2; } +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 a08f6456f..cb17ced46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,35 +14,37 @@ dependencies: logger: ^1.1.0 connectivity: ^3.0.6 uni_links: ^0.5.1 - fluttertoast: ^8.0.8 + 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.0 flutter_native_timezone: ^2.0.0 geolocator: ^9.0.2 cookie_jar: ^3.0.1 - shared_preferences: ^2.0.7 - package_info: ^2.0.2 + shared_preferences: ^2.0.17 + 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: ^12.0.0 sqflite: ^2.1.0 device_calendar: ^4.3.1-4217521123 - image_picker: ^0.8.5+3 + image_picker: ^0.8.6+1 mime_type: ^1.0.0 - uuid: ^3.0.5 + uuid: ^3.0.7 flutter_html: ^3.0.0-beta.2 - webview_flutter: ^2.0.13 + webview_flutter: ^3.0.4 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.3.0 flutter_secure_storage: ^8.0.0 + universal_html: ^2.2.3 + flutter_web_auth_2: ^2.1.4 #Firebase firebase_core: ^2.13.0 @@ -73,8 +75,7 @@ flutter: pluginClass: RokwirePlugin # To add assets to your plugin package, add an assets section, like this: - assets: - - assets/ + # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # From 6c3eaf0d09b4baa310b3a8161cdcfbcfc81e4f71 Mon Sep 17 00:00:00 2001 From: akshadpai Date: Mon, 12 Jun 2023 12:14:19 -0500 Subject: [PATCH 046/177] Handle IOS passkey login issues --- lib/service/auth2.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index a4b146327..6e8000bc7 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -417,6 +418,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future _completeSignInWithPasskey(String username, String responseData) async { if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { String url = "${Config().coreUrl}/services/auth/login"; + Map? requestJson = JsonUtils.decode(responseData); + // TODO: remove if statement once plugin is fixed + if (Config().operatingSystem == 'ios') { + String? userHandle = requestJson?['response']['userHandle']; + Codec stringToBase64Url = utf8.fuse(base64Url); + requestJson?['response']['userHandle'] = stringToBase64Url.decode(userHandle ?? ''); + } Map headers = { 'Content-Type': 'application/json' }; @@ -427,7 +435,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'org_id': Config().coreOrgId, 'creds': { "username": username, - "response": responseData, + "response": JsonUtils.encode(requestJson), }, 'device': deviceInfo, }); @@ -521,6 +529,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future _completeSignUpWithPasskey(String username, String responseData) async { if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { + // Map? requestJson = JsonUtils.decode(responseData ?? ''); + // String? userHandle = requestJson?['userHandle']; + // requestJson?['userHandle'] = base64Url.decode(userHandle ?? ''); + String url = "${Config().coreUrl}/services/auth/login"; Map headers = { 'Content-Type': 'application/json' From a3bb3773941696f7cd2d4b0ab4ff59214c4c8916 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 16 Jun 2023 08:40:02 -0600 Subject: [PATCH 047/177] expose internal passkey auth errors in ui --- lib/service/auth2.dart | 45 ++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index a4b146327..cc400e92b 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -356,10 +356,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Passkey authentication Future authenticateWithPasskey(String? username) async { + String? errorMessage; if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); if (!isPasskeySupported) { - return Auth2PasskeySignInResult.failedNotSupported; + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotSupported); } String url = "${Config().coreUrl}/services/auth/login"; @@ -404,14 +405,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); return _completeSignInWithPasskey(username, responseData); } catch(error) { - Log.e(error.toString()); + errorMessage = error.toString(); + Log.e(errorMessage); } } else if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'not-found') { - return Auth2PasskeySignInResult.failedNotFound; + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotFound); } } - return Auth2PasskeySignInResult.failed; + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed, error: errorMessage); } Future _completeSignInWithPasskey(String username, String responseData) async { @@ -437,18 +439,19 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Map? responseJson = JsonUtils.decode(response.body); bool success = await processLoginResponse(responseJson); if (success) { - return Auth2PasskeySignInResult.succeeded; + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.succeeded); } } } - return Auth2PasskeySignInResult.failed; + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed); } Future signUpWithPasskey(String? username, String? displayName) async { + String? errorMessage; if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); if (!isPasskeySupported) { - return Auth2PasskeySignUpResult.failedNotSupported; + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNotSupported); } Auth2UserProfile? profile = _anonymousProfile; @@ -504,11 +507,12 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { try { String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); Auth2PasskeySignInResult result = await _completeSignInWithPasskey(username, responseData); - if (result == Auth2PasskeySignInResult.succeeded) { - return Auth2PasskeySignUpResult.succeeded; + if (result.status == Auth2PasskeySignInResultStatus.succeeded) { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); } } catch(error) { - Log.e(error.toString()); + errorMessage = error.toString(); + Log.e(errorMessage); } } } @@ -516,7 +520,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // return Auth2PasskeySignUpResult.failedAccountExist; // } } - return Auth2PasskeySignUpResult.failed; + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed, error: errorMessage); } Future _completeSignUpWithPasskey(String username, String responseData) async { @@ -539,10 +543,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Response? response = await Network().post(url, headers: headers, body: post); if (response != null && response.statusCode == 200) { - return Auth2PasskeySignUpResult.succeeded; + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); } } - return Auth2PasskeySignUpResult.failed; + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed); } // OIDC Authentication @@ -1723,15 +1727,26 @@ enum Auth2PasskeyAccountState { // Auth2PasskeySignUpResult -enum Auth2PasskeySignUpResult { +class Auth2PasskeySignUpResult { + Auth2PasskeySignUpResultStatus status; + String? error; + Auth2PasskeySignUpResult(this.status, {this.error}); +} + +enum Auth2PasskeySignUpResultStatus { succeeded, failed, failedNotSupported, } // Auth2PasskeySignInResult +class Auth2PasskeySignInResult { + Auth2PasskeySignInResultStatus status; + String? error; + Auth2PasskeySignInResult(this.status, {this.error}); +} -enum Auth2PasskeySignInResult { +enum Auth2PasskeySignInResultStatus { succeeded, failed, failedNotFound, From 6bb95c981f47fae60c0fc6bed00046c3c279b433 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander <46940735+roberlander2@users.noreply.github.com> Date: Fri, 16 Jun 2023 18:18:02 -0500 Subject: [PATCH 048/177] [#299] Web app passkey support (#300) * create platform implementations for web passkeys and mobile passkeys * fix errors * bug fixes, challenge not working * dart:html pain * attestation object working, but getting tags error * js interop is nice, passkeys working locally * update changelog --- CHANGELOG.md | 1 + lib/platform_impl/base.dart | 19 ++++++ lib/platform_impl/mobile.dart | 39 +++++++++++ lib/platform_impl/stub.dart | 32 +++++++++ lib/platform_impl/web.dart | 104 ++++++++++++++++++++++++++++ lib/rokwire_plugin.dart | 34 +++++----- lib/service/auth2.dart | 124 ++++++++++++++-------------------- lib/service/config.dart | 2 +- lib/utils/utils.dart | 4 ++ 9 files changed, 270 insertions(+), 89 deletions(-) create mode 100644 lib/platform_impl/base.dart create mode 100644 lib/platform_impl/mobile.dart create mode 100644 lib/platform_impl/stub.dart create mode 100644 lib/platform_impl/web.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b1fa97f..2b6720241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Survey creation tool [#263](https://github.com/rokwire/app-flutter-plugin/issues/263). - 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) ### Fixed - Upgrade dependencies for Flutter v3.10 [#285](https://github.com/rokwire/app-flutter-plugin/issues/285) diff --git a/lib/platform_impl/base.dart b/lib/platform_impl/base.dart new file mode 100644 index 000000000..dc9c2de32 --- /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(Map? options); + Future createPasskey(Map? options); +} \ 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..4bce8fb8a --- /dev/null +++ b/lib/platform_impl/mobile.dart @@ -0,0 +1,39 @@ +// 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(Map? options) async { + Map? pubKeyRequest = options?['publicKey']; + return await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + } + + @override + Future createPasskey(Map? options) async { + Map? pubKeyRequest = options?['publicKey']; + return await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + } +} \ 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..9ec67f377 --- /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(Map? options) { + throw Exception("Unimplemented"); + } + + @override + Future createPasskey(Map? options) { + 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..f59bc2dfb --- /dev/null +++ b/lib/platform_impl/web.dart @@ -0,0 +1,104 @@ +// 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 'dart:html'; +import 'dart:typed_data'; + +import 'package:rokwire_plugin/platform_impl/base.dart'; +import 'package:rokwire_plugin/utils/utils.dart'; + +import 'package:js/js.dart'; + +@JS('atob') +external String atob(String value); + +@JS('btoa') +external String btoa(String value); + +class PasskeyImpl extends BasePasskey { + @override + Future arePasskeysSupported() { + return Future.value(window.navigator.credentials != null); + } + + @override + Future getPasskey(Map? options) async { + if (options?['publicKey']?['challenge'] is String) { + String challenge = options!['publicKey']['challenge']; + options['publicKey']['challenge'] = _encodedStringToBuffer(challenge); + } + if (options?['publicKey']?['allowCredentials'] is Iterable) { + Iterable credentials = options!['publicKey']?['allowCredentials']; + for (int i = 0; i < credentials.length; i++) { + dynamic credential = credentials.elementAt(i); + if (credential is Map && credential['id'] is String) { + credentials.elementAt(i)['id'] = _encodedStringToBuffer(credential['id']); + } + } + } + + PublicKeyCredential credential = await window.navigator.credentials!.get(options); + AuthenticatorResponse? authResponse = credential.response; + if (authResponse is AuthenticatorAssertionResponse) { + Map response = { + 'id': credential.id, + 'rawId': _bufferToEncodedString(credential.rawId), + 'type': credential.type, + 'response': { + 'authenticatorData': _bufferToEncodedString(authResponse.authenticatorData), + 'clientDataJSON': _bufferToEncodedString(authResponse.clientDataJson), + 'signature': _bufferToEncodedString(authResponse.signature), + } + }; + + return JsonUtils.encode(response); + } + + return null; + } + + @override + Future createPasskey(Map? options) async { + if (options?['publicKey']?['challenge'] is String) { + String challenge = options!['publicKey']['challenge']; + options['publicKey']['challenge'] = _encodedStringToBuffer(challenge); + } + if (options?['publicKey']?['user']?['id'] is String) { + String userId = options!['publicKey']['user']['id']; + options['publicKey']['user']['id'] = _encodedStringToBuffer(userId); + } + + PublicKeyCredential credential = await window.navigator.credentials!.create(options); + AuthenticatorResponse? authResponse = credential.response; + if (authResponse is AuthenticatorAttestationResponse) { + Map response = { + 'id': credential.id, + 'rawId': _bufferToEncodedString(credential.rawId), + 'type': credential.type, + 'response': { + 'attestationObject': _bufferToEncodedString(authResponse.attestationObject), + 'clientDataJSON': _bufferToEncodedString(authResponse.clientDataJson), + } + }; + + return JsonUtils.encode(response); + } + + return null; + } + + ByteBuffer _encodedStringToBuffer(String value) => Uint8List.fromList(atob(value.replaceAll('_', '/').replaceAll('-', '+')).codeUnits).buffer; + + String _bufferToEncodedString(ByteBuffer? buffer) => btoa(String.fromCharCodes(buffer?.asUint8List() ?? [])).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); +} \ No newline at end of file diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index 073f82a3e..8736c7f64 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -7,6 +7,10 @@ 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); @@ -83,24 +87,22 @@ class RokwirePlugin { return false; } - static Future getPasskey(String requestJson, {bool preferImmediatelyAvailableCredentials = true}) async { - try { - return await _channel.invokeMethod('getPasskey', { - 'requestJson': requestJson, - 'preferImmediatelyAvailableCredentials': preferImmediatelyAvailableCredentials - }); - } catch(e) { debugPrint(e.toString()); } + static Future arePasskeysSupported() async { + try { return await PasskeyImpl().arePasskeysSupported(); } + catch(e) { debugPrint(e.toString()); } + return false; + } + + static Future getPasskey(Map? options) async { + try { return await PasskeyImpl().getPasskey(options); } + catch(e) { debugPrint(e.toString()); } + return null; } - static Future createPasskey(String requestJson, {bool preferImmediatelyAvailableCredentials = true}) async { - try { - return await _channel.invokeMethod('createPasskey', { - 'requestJson': requestJson, - 'preferImmediatelyAvailableCredentials': preferImmediatelyAvailableCredentials - }); - } catch(e) { - debugPrint(e.toString()); - } + static Future createPasskey(Map? options) async { + try { return await PasskeyImpl().createPasskey(options); } + catch(e) { debugPrint(e.toString()); } + return null; } // Compound APIs diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 5b8de651c..0d1e51731 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1,9 +1,7 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_passkey/flutter_passkey.dart'; import 'package:http/http.dart'; import 'package:rokwire_plugin/model/auth2.dart'; import 'package:rokwire_plugin/rokwire_plugin.dart'; @@ -45,7 +43,6 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { static const String _deviceIdIdentifier = 'edu.illinois.rokwire.device_id'; - final flutterPasskeyPlugin = FlutterPasskey(); _OidcLogin? _oidcLogin; bool? _oidcLink; @@ -358,21 +355,17 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Passkey authentication Future authenticateWithPasskey(String? username) async { String? errorMessage; - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { - final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); - if (!isPasskeySupported) { + if ((Config().authBaseUrl != null) && (username != null)) { + if (!await RokwirePlugin.arePasskeysSupported()) { return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotSupported); } - String url = "${Config().coreUrl}/services/auth/login"; + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(passkeyLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { 'username': username, }, @@ -382,28 +375,22 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'profile': profile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.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 != null && response.statusCode == 200) { // Obtain creationOptions from the server String? responseBody = response.body; Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(responseBody)); Map? requestJson = JsonUtils.decode(message?.message ?? ''); - Map? pubKeyRequest = requestJson?['publicKey']; - // pubKeyRequest?.remove('allowCredentials'); - // pubKeyRequest?['userVerification'] = 'required'; - // pubKeyRequest?['allowCredentials'] = []; - // pubKeyRequest = { - // "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo", - // // "allowCredentials": [], - // "timeout": 1800000, - // "userVerification": "required", - // "rpId": "university.app.services.rokmetro.com" - // }; try { - // await RokwirePlugin.getPasskey(JsonUtils.encode(pubKeyRequest) ?? ''); - String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + String? responseData = await RokwirePlugin.getPasskey(requestJson); return _completeSignInWithPasskey(username, responseData); } catch(error) { errorMessage = error.toString(); @@ -417,32 +404,34 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed, error: errorMessage); } - Future _completeSignInWithPasskey(String username, String responseData) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { - String url = "${Config().coreUrl}/services/auth/login"; + Future _completeSignInWithPasskey(String username, String? responseData) 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']; - Codec stringToBase64Url = utf8.fuse(base64Url); - requestJson?['response']['userHandle'] = stringToBase64Url.decode(userHandle ?? ''); + requestJson?['response']['userHandle'] = StringUtils.base64UrlDecode(userHandle ?? ''); } Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(passkeyLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "username": username, "response": JsonUtils.encode(requestJson), }, 'device': deviceInfo, - }); + }; + Map? additionalParams = _getConfigParams(postData); + if (additionalParams != null) { + postData.addAll(additionalParams); + } else { + return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.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 != null && response.statusCode == 200) { Map? responseJson = JsonUtils.decode(response.body); bool success = await processLoginResponse(responseJson); @@ -456,9 +445,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future signUpWithPasskey(String? username, String? displayName) async { String? errorMessage; - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null) && (username != null)) { - final isPasskeySupported = await flutterPasskeyPlugin.isSupported(); - if (!isPasskeySupported) { + if ((Config().authBaseUrl != null) && (username != null)) { + if (!await RokwirePlugin.arePasskeysSupported()) { return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNotSupported); } @@ -473,15 +461,12 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - String url = "${Config().coreUrl}/services/auth/login"; + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(passkeyLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { 'username': username, }, @@ -491,29 +476,25 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { '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: post); + 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)); Map? requestJson = JsonUtils.decode(message?.message ?? ''); - Map? pubKeyRequest = requestJson?['publicKey']; try { - // await RokwirePlugin.createPasskey(JsonUtils.encode(pubKeyRequest) ?? ''); - // pubKeyRequest?['attestation'] = "none"; - // pubKeyRequest?['excludeCredentials'] = []; - // pubKeyRequest?['authenticatorSelection']?['authenticatorAttachment'] = "platform"; - // pubKeyRequest?['authenticatorSelection']?['requireResidentKey'] = true; - // pubKeyRequest?['authenticatorSelection']?['residentKey'] = 'required'; - // pubKeyRequest?['authenticatorSelection']?['userVerification'] = 'required'; - - String? jsonRequest = JsonUtils.encode(pubKeyRequest) ?? ''; - String responseData = await flutterPasskeyPlugin.createCredential(jsonRequest); + String? responseData = await RokwirePlugin.createPasskey(requestJson); return _completeSignUpWithPasskey(username, responseData); } catch(error) { try { - String responseData = await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + String? responseData = await RokwirePlugin.getPasskey(requestJson); Auth2PasskeySignInResult result = await _completeSignInWithPasskey(username, responseData); if (result.status == Auth2PasskeySignInResultStatus.succeeded) { return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); @@ -531,29 +512,28 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed, error: errorMessage); } - Future _completeSignUpWithPasskey(String username, String responseData) async { - if ((Config().coreUrl != null) && (Config().appPlatformId != null) && (Config().coreOrgId != null)) { - // Map? requestJson = JsonUtils.decode(responseData ?? ''); - // String? userHandle = requestJson?['userHandle']; - // requestJson?['userHandle'] = base64Url.decode(userHandle ?? ''); - - String url = "${Config().coreUrl}/services/auth/login"; + Future _completeSignUpWithPasskey(String username, String? responseData) async { + if ((Config().authBaseUrl != null) && (responseData != null)) { + String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; - String? post = JsonUtils.encode({ + Map postData = { 'auth_type': auth2LoginTypeToString(passkeyLoginType), - 'app_type_identifier': Config().appPlatformId, - 'api_key': Config().rokwireApiKey, - 'org_id': Config().coreOrgId, 'creds': { "username": username, "response": responseData, }, '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: post); + Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response != null && response.statusCode == 200) { return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); } diff --git a/lib/service/config.dart b/lib/service/config.dart index d39b68adb..164a5ba5b 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -323,7 +323,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { _config = (configString != null) ? await configFromJsonString(configString) : null; //TODO: decide how best to handle secret keys - if (_config != null && secretKeys.isNotEmpty) { + if (_config != null) { // && secretKeys.isNotEmpty) { configFile.writeAsStringSync(configString!, flush: true); checkUpgrade(); } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index be5faa7d2..c00cfdeb2 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -166,6 +166,10 @@ 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); } class CollectionUtils { From 6e4202bd4220ab662255bc503f41568f2fe49046 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 16 Jun 2023 17:53:38 -0600 Subject: [PATCH 049/177] fix config loading issue on web --- lib/service/config.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/service/config.dart b/lib/service/config.dart index 164a5ba5b..82a5712af 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -413,8 +413,11 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { // Getters: Config Asset Acknowledgement String? get appConfigUrl { + if (kIsWeb) { + return "$authBaseUrl/application/configs"; + } String? assetUrl = (_configAsset != null) ? JsonUtils.stringValue(_configAsset!['config_url']) : null; - return assetUrl ?? JsonUtils.stringValue(platformBuildingBlocks['appconfig_url']) ?? (kIsWeb ? "$authBaseUrl/application/configs" : null); + return assetUrl ?? JsonUtils.stringValue(platformBuildingBlocks['appconfig_url']); } String? get rokwireApiKey { From a0a12fdc430ac0acca6fbce0ef351c7a092f20f0 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Tue, 20 Jun 2023 12:26:34 -0500 Subject: [PATCH 050/177] fix refresh bug --- lib/service/auth2.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 0d1e51731..2914ad39f 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1321,7 +1321,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { refreshTokenFuture = _refreshToken(token?.refreshToken); if (token?.refreshToken != null) { - _refreshTokenFutures[token!.refreshToken!]; + _refreshTokenFutures[token!.refreshToken!] = refreshTokenFuture; } Response? response = await refreshTokenFuture; _refreshTokenFutures.remove(token?.refreshToken); From 89cf648ff5a5f057f38f40f3fe9a266283485c5f Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Tue, 20 Jun 2023 14:43:57 -0500 Subject: [PATCH 051/177] modify refreshToken to handle futures on web --- lib/service/auth2.dart | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 2914ad39f..dc1a3d4d4 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1304,9 +1304,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Refresh Future refreshToken({Auth2Token? token, bool ignoreUnauthorized = false}) async { + //TODO: validate that using CSRF token as futures and fail counts key works on web + String futureKey = token?.refreshToken ?? WebUtils.getCookie(Auth2Csrf.csrfTokenName); if (Config().authBaseUrl != null) { try { - Future? refreshTokenFuture = token?.refreshToken != null ? _refreshTokenFutures[token!.refreshToken] : null; + Future? refreshTokenFuture = futureKey.isNotEmpty ? _refreshTokenFutures[futureKey] : null; if (refreshTokenFuture != null) { _log("Auth2: will await refresh token:\nSource Token: ${token?.refreshToken}"); @@ -1320,18 +1322,18 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _log("Auth2: will refresh token:\nSource Token: ${token?.refreshToken}"); refreshTokenFuture = _refreshToken(token?.refreshToken); - if (token?.refreshToken != null) { - _refreshTokenFutures[token!.refreshToken!] = refreshTokenFuture; + 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}"); - _refreshTokenFailCounts.remove(token?.refreshToken); + _refreshTokenFailCounts.remove(futureKey); if (token == _token) { applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); @@ -1346,8 +1348,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token?.refreshToken}"); int refreshTokenFailCount = 1; - if (token?.refreshToken != null) { - refreshTokenFailCount += _refreshTokenFailCounts[token!.refreshToken!] ?? 0; + if (futureKey.isNotEmpty) { + refreshTokenFailCount += _refreshTokenFailCounts[futureKey] ?? 0; } if (((response?.statusCode == 400) || (!ignoreUnauthorized && response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTokenFailCount)) { if (token == _token) { @@ -1357,14 +1359,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { await authenticateAnonymously(); } } - else if (token?.refreshToken != null) { - _refreshTokenFailCounts[token!.refreshToken!] = refreshTokenFailCount; + 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. + _refreshTokenFutures.remove(futureKey); // make sure to clear this in case something went wrong. } } return null; @@ -1676,11 +1678,11 @@ class Auth2Csrf with NetworkAuthProvider { Auth2Csrf({this.token}); - static const String _csrfTokenName = 'rokwire-csrf-token'; + static const String csrfTokenName = 'rokwire-csrf-token'; @override Map? get networkAuthHeaders { - String cookieName = _csrfTokenName; + String cookieName = csrfTokenName; if (Config().authBaseUrl?.contains("localhost") == false) { cookieName = '__Host-' + cookieName; } @@ -1688,7 +1690,7 @@ class Auth2Csrf with NetworkAuthProvider { Map headers = {}; String cookieValue = WebUtils.getCookie(cookieName); if (cookieValue.isNotEmpty) { - headers[_csrfTokenName] = cookieValue; + headers[csrfTokenName] = cookieValue; } if (StringUtils.isNotEmpty(token?.accessToken)) { From d95f5739cbcdfc1b6858cc51ea6c33f12deee8af Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Wed, 21 Jun 2023 14:38:27 -0500 Subject: [PATCH 052/177] remove unnecessary android passkey implementation --- .../rokwire/rokwire_plugin/Passkey.kt | 118 ------------------ .../rokwire/rokwire_plugin/RokwirePlugin.java | 21 ---- 2 files changed, 139 deletions(-) delete mode 100644 android/src/main/java/edu/illinois/rokwire/rokwire_plugin/Passkey.kt diff --git a/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/Passkey.kt b/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/Passkey.kt deleted file mode 100644 index eb82c8cd6..000000000 --- a/android/src/main/java/edu/illinois/rokwire/rokwire_plugin/Passkey.kt +++ /dev/null @@ -1,118 +0,0 @@ -package edu.illinois.rokwire.rokwire_plugin; - -import android.app.Activity -import android.util.Log -import androidx.credentials.* -import androidx.credentials.exceptions.* -import io.flutter.plugins.firebase.messaging.ContextHolder.getApplicationContext -import kotlinx.coroutines.* - -class PasskeyManager(private val activity: Activity?) { - private val tag = "PasskeyManager" - private val credentialManager: CredentialManager = CredentialManager.create(getApplicationContext()) - private val scope = CoroutineScope(Dispatchers.Default) - - fun login(requestJson: String?, preferImmediatelyAvailableCredentials: Boolean?) { - if (requestJson == null) { - notifyGetPasskeyFailed("MISSING_REQUEST") - return - } - if (activity == null) { - notifyGetPasskeyFailed("NULL_ACTIVITY") - return - } - - // Retrieves the user's saved password for your app from their - // password provider. - val getPasswordOption = GetPasswordOption() - - // Get passkeys from the user's public key credential provider. - val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( - requestJson = requestJson, - preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials ?: true - ) - - val getCredRequest = GetCredentialRequest( - listOf(getPasswordOption, getPublicKeyCredentialOption) - ) - - scope.launch { - try { - val result = credentialManager.getCredential( - request = getCredRequest, - activity = activity, - ) - handleSignIn(result) - } catch (e : GetCredentialException) { - Log.e(tag, e.toString()) - notifyGetPasskeyFailed(e.type) - } - } - } - - private fun handleSignIn(result: GetCredentialResponse) { - // Handle the successfully returned credential. - when (val credential = result.credential) { - is PublicKeyCredential -> { - Log.e(tag, "Credential found: " + credential.authenticationResponseJson) - RokwirePlugin.getInstance().notifyPasskeyResult("onGetPasskeySuccess", credential.authenticationResponseJson) - } -// is PasswordCredential -> { -// val username = credential.id -// val password = credential.password -// passwordAuthenticateWithServer(username, password) -// } - else -> { - // Catch any unrecognized credential type here. - notifyGetPasskeyFailed("INVALID_CREDENTIAL_TYPE") - } - } - } - - private fun notifyGetPasskeyFailed(error: String) { - Log.e(tag, error) - RokwirePlugin.getInstance().notifyPasskeyResult("onGetPasskeyFailed", error) - } - - fun createPasskey(requestJson: String?, preferImmediatelyAvailableCredentials: Boolean?) { - if (requestJson == null) { - notifyCreatePasskeyFailed("MISSING_REQUEST") - return - } - if (activity == null) { - notifyGetPasskeyFailed("NULL_ACTIVITY") - return - } - - val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( - // Contains the request in JSON format. Uses the standard WebAuthn - // web JSON spec. - requestJson = requestJson, - // Defines whether you prefer to use only immediately available credentials, - // not hybrid credentials, to fulfill this request. This value is false - // by default. - preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials ?: true, - ) - - // Execute CreateCredentialRequest asynchronously to register credentials - // for a user account. Handle success and failure cases with the result and - // exceptions, respectively. - scope.launch { - try { - val result = credentialManager.createCredential( - request = createPublicKeyCredentialRequest, - activity = activity, - ) - RokwirePlugin.getInstance().notifyPasskeyResult("onCreatePasskeySuccess", result.data.toString()) - } catch (e : CreateCredentialException) { - Log.e(tag, e.toString()) - notifyCreatePasskeyFailed(e.type) - } - } - } - - private fun notifyCreatePasskeyFailed(error: String) { - Log.e(tag, error) - RokwirePlugin.getInstance().notifyPasskeyResult("onCreatePasskeyFailed", error) - } -} 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 5373abb5e..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 @@ -167,20 +167,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { case "geoFence": GeofenceMonitor.getInstance().handleMethodCall(nextMethodComponents, call.arguments, result); break; - case "getPasskey": - String requestJson = call.argument("requestJson"); - Boolean preferImmediatelyAvailableCredentials = call.argument("preferImmediatelyAvailableCredentials"); - PasskeyManager manager = new PasskeyManager(getActivity()); - manager.login(requestJson, preferImmediatelyAvailableCredentials); - result.success(null); - break; - case "createPasskey": - requestJson = call.argument("requestJson"); - preferImmediatelyAvailableCredentials = call.argument("preferImmediatelyAvailableCredentials"); - manager = new PasskeyManager(getActivity()); - manager.createPasskey(requestJson, preferImmediatelyAvailableCredentials); - result.success(null); - break; default: result.notImplemented(); break; @@ -194,13 +180,6 @@ public void notifyGeoFence(String event, Object arguments) { } } - public void notifyPasskeyResult(String event, Object arguments) { - Activity activity = getActivity(); - if ((activity != null) && (_channel != null)) { - activity.runOnUiThread(() -> _channel.invokeMethod(String.format("passkey.%s", event), arguments)); - } - } - // PluginRegistry.ActivityResultListener @Override From 6fedb6543f62a1f9586414762fd7f1ab90f98920 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 23 Jun 2023 10:47:49 -0500 Subject: [PATCH 053/177] fix profile styling and update domain links --- lib/rokwire_plugin.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index 8736c7f64..1d1c470fa 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -94,15 +94,11 @@ class RokwirePlugin { } static Future getPasskey(Map? options) async { - try { return await PasskeyImpl().getPasskey(options); } - catch(e) { debugPrint(e.toString()); } - return null; + return await PasskeyImpl().getPasskey(options); } static Future createPasskey(Map? options) async { - try { return await PasskeyImpl().createPasskey(options); } - catch(e) { debugPrint(e.toString()); } - return null; + return await PasskeyImpl().createPasskey(options); } // Compound APIs From 3c99123bcc724bf9bf24aa90b5136217488e2c1d Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 23 Jun 2023 12:27:50 -0500 Subject: [PATCH 054/177] cleanup ui --- lib/ui/widgets/ribbon_button.dart | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index 6593832c0..7456e9d74 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -134,24 +134,23 @@ class _RibbonButtonState extends State { Widget get _contentWidget { Widget? leftIconWidget = !widget.progressHidesLeftIcon ? (widget.leftIcon ?? widget.leftIconImage) : null; Widget? rightIconWidget = !widget.progressHidesRightIcon ? (widget.rightIcon ?? widget.rightIconImage) : null; - return Material(color: Colors.transparent, - child: Semantics(label: widget.label, hint: widget.hint, value : widget.semanticsValue, button: true, excludeSemantics: true, child: - InkWell(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.padding, child: - Row(children: [ - (leftIconWidget != null) ? Padding(padding: widget.leftIconPadding, child: leftIconWidget) : Container(), - Expanded(child: - widget.displayTextWidget - ), - (rightIconWidget != null) ? Padding(padding: widget.rightIconPadding, child: rightIconWidget) : Container(), - ],), + 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.onTapWidget(context), child: + Padding(padding: widget.padding, child: + Row(children: [ + (leftIconWidget != null) ? Padding(padding: widget.leftIconPadding, child: leftIconWidget) : Container(), + Expanded(child: + widget.displayTextWidget ), - ) + (rightIconWidget != null) ? Padding(padding: widget.rightIconPadding, child: rightIconWidget) : Container(), + ],), ), - ],), + ), ), ), ); From 0d6feb6562bb9a41329ec72afdf07b9a3f9ea59d Mon Sep 17 00:00:00 2001 From: Ryan Oberlander <46940735+roberlander2@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:07:10 -0500 Subject: [PATCH 055/177] [#305] Anonymous account association (#306) * add anonymous ids to account preferences * fix preferences jeson encoding bug * update changelog * add flag to enable association of anonymous ids --- CHANGELOG.md | 1 + lib/model/auth2.dart | 35 +++++++++++++++++++++++++++++++++-- lib/service/auth2.dart | 4 ++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6720241..a62419c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Survey creation tool [#263](https://github.com/rokwire/app-flutter-plugin/issues/263). - 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) ### Fixed - Upgrade dependencies for Flutter v3.10 [#285](https://github.com/rokwire/app-flutter-plugin/issues/285) diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 49acd2caf..bf62604e3 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -877,6 +877,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"; @@ -891,8 +892,9 @@ class Auth2UserPrefs { Map? _tags; Map? _settings; Auth2VoterPrefs? _voter; + Set? _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, Set? anonymousIds}) { _privacyLevel = privacyLevel; _roles = roles; _favorites = favorites; @@ -901,6 +903,7 @@ class Auth2UserPrefs { _tags = tags; _settings = settings; _voter = Auth2VoterPrefs.fromOther(voter, onChanged: _onVoterChanged); + _anonymousIds = anonymousIds; } static Auth2UserPrefs? fromJson(Map? json) { @@ -914,6 +917,7 @@ class Auth2UserPrefs { answers: JsonUtils.mapValue(json['answers']), settings: JsonUtils.mapValue(json['settings']), voter: Auth2VoterPrefs.fromJson(JsonUtils.mapValue(json['voter'])), + anonymousIds: JsonUtils.stringListValue(json['anonymous_ids'])?.toSet(), ) : null; } @@ -931,6 +935,7 @@ class Auth2UserPrefs { answers: {}, settings: {}, voter: Auth2VoterPrefs(), + anonymousIds: null, ); } @@ -942,6 +947,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; + List? anonymousIds = (profile != null) ? JsonUtils.stringListValue(profile['anonymous_ids']) : null; return Auth2UserPrefs( privacyLevel: privacyLevel, @@ -956,6 +962,7 @@ class Auth2UserPrefs { answers: answers ?? {}, settings: settings ?? {}, voter: voter ?? Auth2VoterPrefs(), + anonymousIds: anonymousIds?.toSet(), ); } @@ -968,7 +975,8 @@ class Auth2UserPrefs { 'food': JsonUtils.mapOfStringToSetOfStringsJsonValue(_foodFilters), 'tags': _tags, 'settings': _settings, - 'voter': _voter + 'voter': _voter, + 'anonymous_ids': _anonymousIds?.toList(), }; } @@ -982,6 +990,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 @@ -993,6 +1002,7 @@ 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, { bool? notify }) { @@ -1056,6 +1066,16 @@ class Auth2UserPrefs { modified = true; } + if (CollectionUtils.isNotEmpty(prefs._anonymousIds)) { + _anonymousIds ??= {}; + for (String id in prefs._anonymousIds!) { + modified |= _anonymousIds!.add(id); + } + if (notify == true) { + NotificationService().notify(notifyAnonymousIdsChanged); + } + } + if ((prefs._voter != null) && prefs._voter!.isNotEmpty && (prefs._voter != _voter)) { _voter = Auth2VoterPrefs.fromOther(prefs._voter, onChanged: _onVoterChanged); if (notify == true) { @@ -1590,6 +1610,17 @@ class Auth2UserPrefs { NotificationService().notify(notifyChanged, this); } + // Anonymous IDs + + Set? get anonymousIds => _anonymousIds; + + void addAnonymousId(String? id) { + if (id != null) { + _anonymousIds ??= {}; + _anonymousIds!.add(id); + } + } + // Helpers static Map? _tagsFromJson(Map? json) { diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index dc1a3d4d4..0377139ee 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -246,6 +246,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2Account? get account => _account; String? get deviceId => _deviceId; + bool get associateAnonymousIds => false; String? get accountId => _account?.id ?? _anonymousId; Auth2UserPrefs? get prefs => _account?.prefs ?? _anonymousPrefs; Auth2UserProfile? get profile => _account?.profile ?? _anonymousProfile; @@ -646,6 +647,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _refreshTokenFailCounts.remove(_token?.refreshToken); + if (associateAnonymousIds) { + _anonymousPrefs?.addAnonymousId(_anonymousId); + } bool? prefsUpdated = account.prefs?.apply(_anonymousPrefs); bool? profileUpdated = account.profile?.apply(_anonymousProfile); _token = token; From 61ed1f258e4b6e3c0ecd9bda33442269fee3ce1d Mon Sep 17 00:00:00 2001 From: Ryan Oberlander <46940735+roberlander2@users.noreply.github.com> Date: Fri, 30 Jun 2023 12:05:13 -0500 Subject: [PATCH 056/177] [#309] Add timestamps to anonymous IDs (#310) * map anonymous IDs to timestamps [#309] * do not overwrite timestamps for existing anonymous IDs * fix anonymous IDs from json * fix date issues --------- Co-authored-by: Stephen Hurwit --- CHANGELOG.md | 1 + lib/model/auth2.dart | 53 +++++++++++++++++++++++++++++++++++--------- lib/utils/utils.dart | 6 +++-- pubspec.yaml | 5 ++--- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a62419c3f..8eadef4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) ### Fixed - Upgrade dependencies for Flutter v3.10 [#285](https://github.com/rokwire/app-flutter-plugin/issues/285) diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index bf62604e3..b4e876753 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -892,9 +892,9 @@ class Auth2UserPrefs { Map? _tags; Map? _settings; Auth2VoterPrefs? _voter; - Set? _anonymousIds; + Map? _anonymousIds; - Auth2UserPrefs({int? privacyLevel, Set? roles, Map>? favorites, Map>? interests, Map>? foodFilters, Map? tags, Map? answers, Map? settings, Auth2VoterPrefs? voter, Set? anonymousIds}) { + 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; @@ -917,7 +917,7 @@ class Auth2UserPrefs { answers: JsonUtils.mapValue(json['answers']), settings: JsonUtils.mapValue(json['settings']), voter: Auth2VoterPrefs.fromJson(JsonUtils.mapValue(json['voter'])), - anonymousIds: JsonUtils.stringListValue(json['anonymous_ids'])?.toSet(), + anonymousIds: _anonymousIdsFromJson(JsonUtils.mapValue(json['anonymous_ids'])), ) : null; } @@ -947,7 +947,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; - List? anonymousIds = (profile != null) ? JsonUtils.stringListValue(profile['anonymous_ids']) : null; + Map? anonymousIds = (profile != null) ? _anonymousIdsFromJson(JsonUtils.mapValue(profile['anonymous_ids'])) : null; return Auth2UserPrefs( privacyLevel: privacyLevel, @@ -962,7 +962,7 @@ class Auth2UserPrefs { answers: answers ?? {}, settings: settings ?? {}, voter: voter ?? Auth2VoterPrefs(), - anonymousIds: anonymousIds?.toSet(), + anonymousIds: anonymousIds, ); } @@ -976,7 +976,7 @@ class Auth2UserPrefs { 'tags': _tags, 'settings': _settings, 'voter': _voter, - 'anonymous_ids': _anonymousIds?.toList(), + 'anonymous_ids': _anonymousIdsToJson(), }; } @@ -1066,10 +1066,11 @@ class Auth2UserPrefs { modified = true; } - if (CollectionUtils.isNotEmpty(prefs._anonymousIds)) { + if (prefs._anonymousIds?.isNotEmpty ?? false) { _anonymousIds ??= {}; - for (String id in prefs._anonymousIds!) { - modified |= _anonymousIds!.add(id); + for (MapEntry id in prefs._anonymousIds!.entries) { + modified = !_anonymousIds!.containsKey(id.key); + _anonymousIds!.putIfAbsent(id.key, () => id.value); } if (notify == true) { NotificationService().notify(notifyAnonymousIdsChanged); @@ -1612,15 +1613,29 @@ class Auth2UserPrefs { // Anonymous IDs - Set? get anonymousIds => _anonymousIds; + Map? get anonymousIds => _anonymousIds; void addAnonymousId(String? id) { if (id != null) { _anonymousIds ??= {}; - _anonymousIds!.add(id); + _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) { @@ -1666,6 +1681,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; + } } class Auth2VoterPrefs { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index c00cfdeb2..51db324ee 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1083,6 +1083,8 @@ 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)) { @@ -1100,11 +1102,11 @@ 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; } diff --git a/pubspec.yaml b/pubspec.yaml index 91a2e9d52..2bda58624 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,9 +76,8 @@ 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 From ef061b861e25f129b3b40772142cf57f67ac4217 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Sat, 8 Jul 2023 00:34:28 -0500 Subject: [PATCH 057/177] handle public accounts/follows --- lib/service/auth2.dart | 20 ++++++++++++++++---- lib/service/config.dart | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 0377139ee..985519f13 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -444,7 +444,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failed); } - Future signUpWithPasskey(String? username, String? displayName) async { + Future signUpWithPasskey(String? username, String? displayName, {bool? public = false}) async { String? errorMessage; if ((Config().authBaseUrl != null) && (username != null)) { if (!await RokwirePlugin.arePasskeysSupported()) { @@ -474,6 +474,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'params': { "display_name": displayName, }, + 'privacy': { + 'public': public, + }, 'profile': profile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, @@ -739,7 +742,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Phone Authentication - Future authenticateWithPhone(String? phoneNumber) async { + Future authenticateWithPhone(String? phoneNumber, {bool? public = false}) async { if ((Config().authBaseUrl != null) && (phoneNumber != null)) { NotificationService().notify(notifyLoginStarted, phoneLoginType); @@ -752,6 +755,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'creds': { "phone": phoneNumber, }, + 'privacy': { + 'public': public, + }, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, @@ -865,7 +871,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2EmailSignInResult.failed; } - Future signUpWithEmail(String? email, String? password) async { + Future signUpWithEmail(String? email, String? password, {bool? public = false}) async { if ((Config().authBaseUrl != null) && (email != null) && (password != null)) { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { @@ -881,6 +887,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { "sign_up": true, "confirm_password": password }, + 'privacy': { + 'public': public, + }, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, @@ -1038,7 +1047,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2UsernameSignInResult.failed; } - Future signUpWithUsername(String? username, String? password) async { + Future signUpWithUsername(String? username, String? password, {bool? public = false}) async { if ((Config().authBaseUrl != null) && (username != null) && (password != null)) { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { @@ -1054,6 +1063,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { "sign_up": true, "confirm_password": password }, + 'privacy': { + 'public': public, + }, 'profile': _anonymousProfile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, diff --git a/lib/service/config.dart b/lib/service/config.dart index 82a5712af..1fa9b8c22 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -567,6 +567,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { String? get webIdentifierOrigin => html.window.location.origin; String? get authBaseUrl { if (isReleaseWeb) { + print(kDebugMode); return '${html.window.location.origin}/$webServiceId'; } else if (isAdmin) { return '$coreUrl/admin'; @@ -593,7 +594,6 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } bool get isAdmin => false; - bool get bypassLogin => true; // Bypass login for testing web layouts bool get isReleaseWeb => kIsWeb && !kDebugMode; } From d0309207d49545598bad853a4786921de340572d Mon Sep 17 00:00:00 2001 From: akshadpai Date: Tue, 11 Jul 2023 15:01:50 -0500 Subject: [PATCH 058/177] Fix config init failed for web app --- lib/service/config.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/service/config.dart b/lib/service/config.dart index 82a5712af..eea36fa3b 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -413,7 +413,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { // Getters: Config Asset Acknowledgement String? get appConfigUrl { - if (kIsWeb) { + if (isReleaseWeb) { return "$authBaseUrl/application/configs"; } String? assetUrl = (_configAsset != null) ? JsonUtils.stringValue(_configAsset!['config_url']) : null; @@ -566,8 +566,8 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { // Getters: web String? get webIdentifierOrigin => html.window.location.origin; String? get authBaseUrl { - if (isReleaseWeb) { - return '${html.window.location.origin}/$webServiceId'; + if (kIsWeb) { + return 'https://vogue.dev.api.rokmetro.com/$webServiceId'; } else if (isAdmin) { return '$coreUrl/admin'; } From 54060102a9a153b7826f31ac3edefc5a954826f0 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Wed, 12 Jul 2023 15:58:50 -0500 Subject: [PATCH 059/177] fix more bugs running web app locally --- lib/service/auth2.dart | 2 +- lib/service/config.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 985519f13..d6355dca0 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1404,7 +1404,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'Content-Type': 'application/json' }; String? post; - if (!kIsWeb) { + if (!Config().isReleaseWeb) { if (refreshToken == null) { return null; } diff --git a/lib/service/config.dart b/lib/service/config.dart index 0aec6bfac..eb9e70d69 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -567,7 +567,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { // Getters: web String? get webIdentifierOrigin => html.window.location.origin; String? get authBaseUrl { - if (kIsWeb) { + if (isReleaseWeb) { return '${html.window.location.origin}/$webServiceId'; } else if (isAdmin) { return '$coreUrl/admin'; From 164245ce591aeb21282785e7dbfaf37ba0162d32 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Thu, 27 Jul 2023 14:45:20 -0500 Subject: [PATCH 060/177] restore assets.dart --- lib/service/assets.dart | 231 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/lib/service/assets.dart b/lib/service/assets.dart index e69de29bb..f3119be62 100644 --- a/lib/service/assets.dart +++ b/lib/service/assets.dart @@ -0,0 +1,231 @@ +/* + * Copyright 2020 Board of Trustees of the University of Illinois. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:core'; +import 'dart:io'; +import 'dart:math'; +import 'dart:ui'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:rokwire_plugin/service/notification_service.dart'; +import 'package:rokwire_plugin/service/service.dart'; +import 'package:rokwire_plugin/service/config.dart'; +import 'package:rokwire_plugin/service/network.dart'; +import 'package:rokwire_plugin/service/app_lifecycle.dart'; +import 'package:rokwire_plugin/utils/utils.dart'; +import 'package:path/path.dart'; +import 'package:http/http.dart' as http; + +class Assets with Service implements NotificationsListener { + + static const String notifyChanged = "edu.illinois.rokwire.assets.changed"; + + static const String _assetsName = "assets.json"; + + Directory? _assetsDir; + DateTime? _pausedDateTime; + + Map? _defAssets; + Map? _appAssets; + Map? _netAssets; + Map? _assets; + + // Singletone Factory + + static Assets? _instance; + + static Assets? get instance => _instance; + + @protected + static set instance(Assets? value) => _instance = value; + + factory Assets() => _instance ?? (_instance = Assets.internal()); + + @protected + Assets.internal(); + + // Service + + @override + void createService() { + NotificationService().subscribe(this, AppLifecycle.notifyStateChanged); + } + + @override + void destroyService() { + NotificationService().unsubscribe(this); + } + + @override + Future initService() async { + _assetsDir = await getAssetsDir(); + _defAssets = await loadFromAssets(assetsKey); + _appAssets = await loadFromAssets(appAssetsKey); + _netAssets = await loadFromCache(netCacheFileName); + + if ((_defAssets != null) || (_appAssets != null) || (_netAssets != null)) { + build(); + updateFromNet(); + await super.initService(); + } + else { + throw ServiceError( + source: this, + severity: ServiceErrorSeverity.nonFatal, + title: 'Assets Initialization Failed', + description: 'Failed to initialize application assets content.', + ); + } + } + + @override + Set get serviceDependsOn { + return { Config() }; + } + + // NotificationsListener + + @override + void onNotification(String name, dynamic param) { + if (name == AppLifecycle.notifyStateChanged) { + _onAppLifecycleStateChanged(param); + } + } + + void _onAppLifecycleStateChanged(AppLifecycleState? state) { + if (state == AppLifecycleState.paused) { + _pausedDateTime = DateTime.now(); + } + else if (state == AppLifecycleState.resumed) { + if (_pausedDateTime != null) { + Duration pausedDuration = DateTime.now().difference(_pausedDateTime!); + if (Config().refreshTimeout < pausedDuration.inSeconds) { + updateFromNet(); + } + } + } + } + + // Assets + + dynamic operator [](dynamic key) { + return MapPathKey.entry(_assets, key); + } + + String? randomStringFromListWithKey(String key) { + List? list = JsonUtils.listValue(this[key]); + return ((list != null) && list.isNotEmpty) ? JsonUtils.stringValue(list[Random().nextInt(list.length)]) : null; + } + + // Implementation + + Future getAssetsDir() async { + Directory? assetsDir = Config().assetsCacheDir; + if ((assetsDir != null) && !await assetsDir.exists()) { + await assetsDir.create(recursive: true); + } + return assetsDir; + } + + @protected + String get assetsKey => 'assets/$_assetsName'; + + @protected + String get appAssetsKey => 'app/assets/$_assetsName'; + + @protected + Future?> loadFromAssets(String assetsKey) async { + try { return JsonUtils.decodeMap(await rootBundle.loadString(assetsKey)); } + catch(e) { debugPrint(e.toString()); } + return null; + } + + @protected + String get netCacheFileName => _assetsName; + + @protected + Future?> loadFromCache(String cacheFileName) async { + try { + if (_assetsDir != null) { + String cacheFilePath = join(_assetsDir!.path, cacheFileName); + File cacheFile = File(cacheFilePath); + if (await cacheFile.exists()) { + return JsonUtils.decodeMap(await cacheFile.readAsString()); + } + } + } + catch(e) { debugPrint(e.toString()); } + return null; + } + + @protected + Future saveToCache(String cacheFileName, String? content) async { + try { + if (_assetsDir != null) { + String cacheFilePath = join(_assetsDir!.path, cacheFileName); + File cacheFile = File(cacheFilePath); + if (content != null) { + cacheFile.writeAsString(content, flush: true); + } + else if (await cacheFile.exists()) { + await cacheFile.delete(); + } + } + } + catch(e) { debugPrint(e.toString()); } + } + + @protected + String get netAssetFileName => _assetsName; + + @protected + Future loadContentStringFromNet() async { + if (Config().assetsUrl != null) { + http.Response? response = await Network().get("${Config().assetsUrl}/$netAssetFileName"); + return (response?.statusCode == 200) ? response?.body : null; + } + return null; + } + + @protected + Future updateFromNet() async { + String? netAssetsString = await loadContentStringFromNet(); + Map? netAssets = JsonUtils.decodeMap(netAssetsString); + if (((netAssets != null) && !const DeepCollectionEquality().equals(netAssets, _netAssets)) || + ((netAssets == null) && (_netAssets != null))) + { + _netAssets = netAssets; + await saveToCache(netCacheFileName, netAssetsString); + build(); + NotificationService().notify(notifyChanged, null); + } + } + + @protected + void build() { + _assets = {}; + if (_defAssets != null) { + _assets!.addAll(_defAssets!); + } + if (_appAssets != null) { + _assets!.addAll(_appAssets!); + } + if (_netAssets != null) { + _assets!.addAll(_netAssets!); + } + } +} \ No newline at end of file From 43d9574eae53fa3b4a435662d25f6d7f2d5adf8d Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Thu, 27 Jul 2023 14:50:49 -0500 Subject: [PATCH 061/177] restore FlexContent.fromAssets --- lib/ui/widgets/flex_content.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/ui/widgets/flex_content.dart b/lib/ui/widgets/flex_content.dart index f65aed4ee..c81275613 100644 --- a/lib/ui/widgets/flex_content.dart +++ b/lib/ui/widgets/flex_content.dart @@ -17,6 +17,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:rokwire_plugin/gen/styles.dart'; +import 'package:rokwire_plugin/service/assets.dart'; import 'package:rokwire_plugin/service/content.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; @@ -47,6 +48,11 @@ class FlexContent extends StatefulWidget { const FlexContent({Key? key, this.contentKey, this.contentJson, this.onClose}) : super(key: key); + static FlexContent? fromAssets(String contentKey, { void Function(BuildContext context)? onClose }) { + Map? jsonContent = JsonUtils.mapValue(Assets()[contentKey]); + return (jsonContent != null) ? FlexContent(contentKey: contentKey, contentJson: jsonContent, onClose: onClose) : null; + } + @override FlexContentWidgetState createState() => FlexContentWidgetState(); From 654524165829f4b6bd40c9349cfa0a7744b528e2 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 27 Jul 2023 15:52:49 -0500 Subject: [PATCH 062/177] fix missing timezone asset --- pubspec.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index cb17ced46..84f6bde03 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,9 +75,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 From 78345eebab93122cd339d6342497db10bf5c9c2e Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 10 Aug 2023 14:15:30 -0500 Subject: [PATCH 063/177] update dependency, fix typos --- example/pubspec.lock | 260 ++++++++++++++++++++++++--------------- lib/service/content.dart | 4 +- pubspec.yaml | 2 +- 3 files changed, 165 insertions(+), 101 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 65ae26d3f..f7597930b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "8eb354cb8ebed8a9fdf63699d15deff533bc133128898afaf754926b57d611b6" + sha256: "5dce45a06d386358334eb1689108db6455d90ceb0d75848d5f4819283d4ee2b8" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.4" args: dependency: transitive description: name: args - sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" async: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" file: dependency: transitive description: @@ -217,14 +217,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + url: "https://pub.dev" + source: hosted + version: "0.9.3" firebase_core: dependency: transitive description: name: firebase_core - sha256: "250678b816279b3240c3a33e1f76bf712c00718f1fbeffc85873a5da8c077379" + sha256: "2e9324f719e90200dc7d3c4f5d2abc26052f9f2b995d3b6626c47a0dfe1c8192" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.15.0" firebase_core_platform_interface: dependency: transitive description: @@ -237,50 +269,50 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "8c0f4c87d20e2d001a5915df238c1f9c88704231f591324205f5a5d2a7740a45" + sha256: "0fd5c4b228de29b55fac38aed0d9e42514b3d3bd47675de52bf7f8fccaf922fa" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - sha256: "0d74cca3085f144f99aa4bd82cc4d33280d4cb72bac0b733cbf97c2d7d126df8" + sha256: "3607b46342537f98df18b130b6f5ab25cee6981a3a782e1a7b121d04dfea3caa" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.3.4" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "13880033d5f2055f53bcda28024e16607b8400445a425f86732c1935da9260db" + sha256: c63abeb87b18f6e6d4bf6bb3977f15d2d9281a049d93fe098e83e56dcbf7da06 url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "3.6.4" firebase_messaging: dependency: transitive description: name: firebase_messaging - sha256: "9cfe5c4560fb83393511ca7620f8fb3f22c9a80303052f10290e732fcfb801bd" + sha256: "8ac91d83a028eef050de770f1dc98421e215714d245f34de7b154d436676fbd0" url: "https://pub.dev" source: hosted - version: "14.6.1" + version: "14.6.5" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "7e25cb71019ccef8b1fd7b37969af79f04c467974cce4dfc291fa36974edd7ba" + sha256: b2995e3640efb646e9ebf0e2fa50dea84895f0746a31d7e3af0e5e009a533a1a url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.4" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "5d9840cc8126ea723b1bda901389cb542902f664f2653c16d4f8114e95f13cec" + sha256: "5d8446a28339124a2cb4f57a6ca454a3aca7d0c5c0cdfa5707afb192f7c830a7" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.5.4" flutter: dependency: "direct main" description: flutter @@ -322,10 +354,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "6af440e3962eeab8459602c309d7d4ab9e62f05d5cfe58195a28f846a0b5d523" + sha256: "3c6d6db334f609a92be0c0915f40871ec56f5d2adf01e77ae364162c587c0ca8" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: @@ -407,10 +439,10 @@ packages: dependency: transitive description: name: flutter_web_auth_2 - sha256: ee636fa43af689df8ace6421ea5f5e14b8b3eda0e8caa3e5a1e3bb71f0833636 + sha256: "70e4df72940183b8e269c4163f78dd5bf9102ba3329bfe00c0f2373f30fb32d0" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" flutter_web_auth_2_platform_interface: dependency: transitive description: @@ -436,10 +468,10 @@ packages: dependency: transitive description: name: font_awesome_flutter - sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206" + sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3" url: "https://pub.dev" source: hosted - version: "10.4.0" + version: "10.5.0" gallery_saver: dependency: transitive description: @@ -460,18 +492,18 @@ packages: dependency: transitive description: name: geolocator_android - sha256: "6cd3c622df085a79fd61f5c14fa024c3ba593aa6b1df2ee809ac59f45e6a9861" + sha256: "835ff5b4888a2f8eba128996494faf9c5d422785322a81dc0565b99e0f6c379d" url: "https://pub.dev" source: hosted - version: "4.1.8" + version: "4.2.2" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "22b60ca3b8c0f58e6a9688ff855ee39ab813ca3f0c0609a48d282f6631266f2e" + sha256: "36527c555f4c425f7d8fa8c7c07d67b78e3ff7590d40448051959e1860c1cfb4" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.7" geolocator_platform_interface: dependency: transitive description: @@ -492,18 +524,18 @@ packages: dependency: transitive description: name: geolocator_windows - sha256: f5911c88e23f48b598dd506c7c19eff0e001645bdc03bb6fecb9f4549208354d + sha256: "242a35938cbe81dd169c9c72fc53e8183c4447bfa998b5df56b6725796591951" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" html: dependency: transitive description: name: html - sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" url: "https://pub.dev" source: hosted - version: "0.15.3" + version: "0.15.4" http: dependency: transitive description: @@ -524,42 +556,66 @@ packages: dependency: transitive description: name: image_picker - sha256: "9978d3510af4e6a902e545ce19229b926e6de6a1828d6134d3aab2e129a4d270" + sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c url: "https://pub.dev" source: hosted - version: "0.8.7+5" + version: "0.8.9" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: c2f3c66400649bd132f721c88218945d6406f693092b2f741b79ae9cdb046e59 + sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" url: "https://pub.dev" source: hosted - version: "0.8.6+16" + version: "0.8.7+4" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c" + sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" url: "https://pub.dev" source: hosted - version: "2.1.12" + version: "2.2.0" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0 + sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b url: "https://pub.dev" source: hosted - version: "0.8.7+4" + version: "0.8.8" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + url: "https://pub.dev" + source: hosted + version: "0.2.1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "1991219d9dbc42a99aff77e663af8ca51ced592cd6685c9485e3458302d3d4f8" + sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 url: "https://pub.dev" source: hosted - version: "2.6.3" + version: "2.9.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + url: "https://pub.dev" + source: hosted + version: "0.2.1" intl: dependency: transitive description: @@ -596,10 +652,10 @@ packages: dependency: transitive description: name: logger - sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7 + sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" matcher: dependency: transitive description: @@ -624,6 +680,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" mime_type: dependency: transitive description: @@ -636,10 +700,10 @@ packages: dependency: transitive description: name: package_info_plus - sha256: ceb027f6bc6a60674a233b4a90a7658af1aebdea833da0b5b53c1e9821a78c7b + sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -660,50 +724,50 @@ packages: dependency: transitive description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.0" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.1.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.2.0" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.0" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.2.0" petitparser: dependency: transitive description: @@ -724,10 +788,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pointycastle: dependency: transitive description: @@ -755,58 +819,58 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" + sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb + sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" sky_engine: dependency: transitive description: flutter @@ -832,18 +896,18 @@ packages: dependency: transitive description: name: sqflite - sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" url: "https://pub.dev" source: hosted - version: "2.2.8+4" + version: "2.3.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555 + sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.5.0" stack_trace: dependency: transitive description: @@ -952,18 +1016,18 @@ packages: dependency: transitive description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.1.12" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87" + sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" url: "https://pub.dev" source: hosted - version: "6.0.34" + version: "6.0.38" url_launcher_ios: dependency: transitive description: @@ -984,34 +1048,34 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" + sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" uuid: dependency: transitive description: @@ -1064,10 +1128,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.0.6" window_to_front: dependency: transitive description: @@ -1093,5 +1157,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/lib/service/content.dart b/lib/service/content.dart index 071881bc1..6e273f875 100644 --- a/lib/service/content.dart +++ b/lib/service/content.dart @@ -129,11 +129,11 @@ class Content with Service implements NotificationsListener, ContentItemCategory @override void onNotification(String name, dynamic param) { if (name == AppLifecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); + _onAppLifecycleStateChanged(param); } } - void _onAppLivecycleStateChanged(AppLifecycleState? state) { + void _onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } diff --git a/pubspec.yaml b/pubspec.yaml index 84f6bde03..be2ab3422 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: sprintf: ^7.0.0 flutter_local_notifications: ^12.0.0 sqflite: ^2.1.0 - device_calendar: ^4.3.1-4217521123 + device_calendar: ^4.3.1 image_picker: ^0.8.6+1 mime_type: ^1.0.0 uuid: ^3.0.7 From 4d3677a3192d23e1c445ef48f09e856872757116 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Thu, 31 Aug 2023 12:46:05 -0500 Subject: [PATCH 064/177] fixed token refresh bug --- lib/service/auth2.dart | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 1277ed8ac..d5cd9c3bd 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1103,9 +1103,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Refresh Future refreshToken({Auth2Token? token, bool ignoreUnauthorized = false}) async { + //TODO: validate that using CSRF token as futures and fail counts key works on web + String futureKey = token?.refreshToken ?? WebUtils.getCookie(Auth2Csrf.csrfTokenName); if (Config().authBaseUrl != null) { try { - Future? refreshTokenFuture = token?.refreshToken != null ? _refreshTokenFutures[token!.refreshToken] : null; + Future? refreshTokenFuture = futureKey.isNotEmpty ? _refreshTokenFutures[futureKey] : null; if (refreshTokenFuture != null) { _log("Auth2: will await refresh token:\nSource Token: ${token?.refreshToken}"); @@ -1119,18 +1121,18 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _log("Auth2: will refresh token:\nSource Token: ${token?.refreshToken}"); refreshTokenFuture = _refreshToken(token?.refreshToken); - if (token?.refreshToken != null) { - _refreshTokenFutures[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}"); - _refreshTokenFailCounts.remove(token?.refreshToken); + _refreshTokenFailCounts.remove(futureKey); if (token == _token) { applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); @@ -1145,8 +1147,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token?.refreshToken}"); int refreshTokenFailCount = 1; - if (token?.refreshToken != null) { - refreshTokenFailCount += _refreshTokenFailCounts[token!.refreshToken!] ?? 0; + if (futureKey.isNotEmpty) { + refreshTokenFailCount += _refreshTokenFailCounts[futureKey] ?? 0; } if (((response?.statusCode == 400) || (!ignoreUnauthorized && response?.statusCode == 401)) || (Config().refreshTokenRetriesCount <= refreshTokenFailCount)) { if (token == _token) { @@ -1156,14 +1158,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { await authenticateAnonymously(); } } - else if (token?.refreshToken != null) { - _refreshTokenFailCounts[token!.refreshToken!] = refreshTokenFailCount; + 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. + _refreshTokenFutures.remove(futureKey); // make sure to clear this in case something went wrong. } } return null; @@ -1452,19 +1454,19 @@ class Auth2Csrf with NetworkAuthProvider { Auth2Csrf({this.token}); - static const String _csrfTokenName = 'rokwire-csrf-token'; + static const String csrfTokenName = 'rokwire-csrf-token'; @override Map? get networkAuthHeaders { - String cookieName = _csrfTokenName; + 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 (cookieValue.isNotEmpty) { + headers[csrfTokenName] = cookieValue; } if (StringUtils.isNotEmpty(token?.accessToken)) { @@ -1476,7 +1478,7 @@ class Auth2Csrf with NetworkAuthProvider { @override dynamic get networkAuthToken => token; - + @override Future refreshNetworkAuthTokenIfNeeded(BaseResponse? response, dynamic token) async { if ((response?.statusCode == 401) && (token is Auth2Token) && (Auth2().token == token)) { From 225914256d611f8df439dee6e07ca2981ffa4c54 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 31 Aug 2023 16:07:25 -0500 Subject: [PATCH 065/177] add default style gen functionality --- assets/styles.json | 2 +- lib/gen/styles.dart | 350 +++++++++++++++++++++--------------------- tools/gen_styles.dart | 100 +++++++++++- 3 files changed, 271 insertions(+), 181 deletions(-) diff --git a/assets/styles.json b/assets/styles.json index a1bf2575f..5ea7eeaf8 100644 --- a/assets/styles.json +++ b/assets/styles.json @@ -156,7 +156,7 @@ "widget.button.title.medium.thin" : { "font_family": "regular", "size": 16.0, "color": "textPrimary"}, "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": "textSecondary"}, + "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"}, diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index 2b00f977f..5f7024fe6 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -4,188 +4,188 @@ import 'package:rokwire_plugin/service/styles.dart'; import 'package:flutter/material.dart'; class AppColors { - static Color? get fillColorPrimary => Styles().colors?.getColor('fillColorPrimary'); - static Color? get fillColorPrimaryVariant => Styles().colors?.getColor('fillColorPrimaryVariant'); - static Color? get fillColorSecondary => Styles().colors?.getColor('fillColorSecondary'); - static Color? get fillColorSecondaryVariant => Styles().colors?.getColor('fillColorSecondaryVariant'); - static Color? get textPrimary => Styles().colors?.getColor('textPrimary'); - static Color? get textAccent => Styles().colors?.getColor('textAccent'); - static Color? get textDark => Styles().colors?.getColor('textDark'); - static Color? get textMedium => Styles().colors?.getColor('textMedium'); - static Color? get textLight => Styles().colors?.getColor('textLight'); - static Color? get textDisabled => Styles().colors?.getColor('textDisabled'); - static Color? get iconPrimary => Styles().colors?.getColor('iconPrimary'); - static Color? get iconLight => Styles().colors?.getColor('iconLight'); - static Color? get iconDark => Styles().colors?.getColor('iconDark'); - static Color? get iconMedium => Styles().colors?.getColor('iconMedium'); - static Color? get iconDisabled => Styles().colors?.getColor('iconDisabled'); - static Color? get surface => Styles().colors?.getColor('surface'); - static Color? get surfaceAccent => Styles().colors?.getColor('surfaceAccent'); - static Color? get background => Styles().colors?.getColor('background'); - static Color? get backgroundVariant => Styles().colors?.getColor('backgroundVariant'); - static Color? get shadow => Styles().colors?.getColor('shadow'); - static Color? get gradientColorPrimary => Styles().colors?.getColor('gradientColorPrimary'); - static Color? get accentColor1 => Styles().colors?.getColor('accentColor1'); - static Color? get accentColor2 => Styles().colors?.getColor('accentColor2'); - static Color? get accentColor3 => Styles().colors?.getColor('accentColor3'); - static Color? get accentColor4 => Styles().colors?.getColor('accentColor4'); - static Color? get success => Styles().colors?.getColor('success'); - static Color? get alert => Styles().colors?.getColor('alert'); - static Color? get dividerLine => Styles().colors?.getColor('dividerLine'); + 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'); - static String? get blackItalic => Styles().fontFamilies?.fromCode('black_italic'); - static String? get bold => Styles().fontFamilies?.fromCode('bold'); - static String? get boldItalic => Styles().fontFamilies?.fromCode('bold_italic'); - static String? get extraBold => Styles().fontFamilies?.fromCode('extra_bold'); - static String? get extraBoldItalic => Styles().fontFamilies?.fromCode('extra_bold_italic'); - static String? get light => Styles().fontFamilies?.fromCode('light'); - static String? get lightItalic => Styles().fontFamilies?.fromCode('light_italic'); - static String? get medium => Styles().fontFamilies?.fromCode('medium'); - static String? get mediumItalic => Styles().fontFamilies?.fromCode('medium_italic'); - static String? get regular => Styles().fontFamilies?.fromCode('regular'); - static String? get regularItalic => Styles().fontFamilies?.fromCode('regular_italic'); - static String? get semiBold => Styles().fontFamilies?.fromCode('semi_bold'); - static String? get semiBoldItalic => Styles().fontFamilies?.fromCode('semi_bold_italic'); - static String? get thin => Styles().fontFamilies?.fromCode('thin'); - static String? get thinItalic => Styles().fontFamilies?.fromCode('thin_italic'); + 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'); - static TextStyle? get headerBar => Styles().textStyles?.getTextStyle('header_bar'); - static TextStyle? get headerBarAccent => Styles().textStyles?.getTextStyle('header_bar.accent'); - static TextStyle? get widgetHeadingExtraLarge => Styles().textStyles?.getTextStyle('widget.heading.extra_large'); - static TextStyle? get widgetHeadingExtraLargeBold => Styles().textStyles?.getTextStyle('widget.heading.extra_large.bold'); - static TextStyle? get widgetHeadingLarge => Styles().textStyles?.getTextStyle('widget.heading.large'); - static TextStyle? get widgetHeadingLargeBold => Styles().textStyles?.getTextStyle('widget.heading.large.bold'); - static TextStyle? get widgetHeadingRegular => Styles().textStyles?.getTextStyle('widget.heading.regular'); - static TextStyle? get widgetHeadingRegularBold => Styles().textStyles?.getTextStyle('widget.heading.regular.bold'); - static TextStyle? get widgetHeadingMedium => Styles().textStyles?.getTextStyle('widget.heading.medium'); - static TextStyle? get widgetHeadingSmall => Styles().textStyles?.getTextStyle('widget.heading.small'); - static TextStyle? get widgetMessageDarkExtraLarge => Styles().textStyles?.getTextStyle('widget.message.dark.extra_large'); - static TextStyle? get widgetMessageDarkMedium => Styles().textStyles?.getTextStyle('widget.message.dark.medium'); - static TextStyle? get widgetMessageExtraLargeBold => Styles().textStyles?.getTextStyle('widget.message.extra_large.bold'); - static TextStyle? get widgetMessageLargeBold => Styles().textStyles?.getTextStyle('widget.message.large.bold'); - static TextStyle? get widgetMessageLarge => Styles().textStyles?.getTextStyle('widget.message.large'); - static TextStyle? get widgetMessageLargeDarkBold => Styles().textStyles?.getTextStyle('widget.message.large.dark.bold'); - static TextStyle? get widgetMessageRegularPrimaryBold => Styles().textStyles?.getTextStyle('widget.message.regular.primary.bold'); - static TextStyle? get widgetMessageRegularPrimary => Styles().textStyles?.getTextStyle('widget.message.regular.primary'); - static TextStyle? get widgetMessageLightBoldPrimary => Styles().textStyles?.getTextStyle('widget.message.light.bold.primary'); - static TextStyle? get widgetMessageMedium => Styles().textStyles?.getTextStyle('widget.message.medium'); - static TextStyle? get widgetMessageRegular => Styles().textStyles?.getTextStyle('widget.message.regular'); - static TextStyle? get widgetMessageRegularBold => Styles().textStyles?.getTextStyle('widget.message.regular.bold'); - static TextStyle? get widgetMessageRegularBoldAccent => Styles().textStyles?.getTextStyle('widget.message.regular.bold.accent'); - static TextStyle? get widgetMessageSmall => Styles().textStyles?.getTextStyle('widget.message.small'); - static TextStyle? get widgetMessageSmallPrimaryBold => Styles().textStyles?.getTextStyle('widget.message.small.primary.bold'); - static TextStyle? get widgetMessageLightRegular => Styles().textStyles?.getTextStyle('widget.message.light.regular'); - static TextStyle? get widgetTitleExtraLarge => Styles().textStyles?.getTextStyle('widget.title.extra_large'); - static TextStyle? get widgetTitleLarge => Styles().textStyles?.getTextStyle('widget.title.large'); - static TextStyle? get widgetTitleLargeBold => Styles().textStyles?.getTextStyle('widget.title.large.bold'); - static TextStyle? get widgetTitleMedium => Styles().textStyles?.getTextStyle('widget.title.medium'); - static TextStyle? get widgetTitleMediumBold => Styles().textStyles?.getTextStyle('widget.title.medium.bold'); - static TextStyle? get widgetTitleRegular => Styles().textStyles?.getTextStyle('widget.title.regular'); - static TextStyle? get widgetTitleSmallBold => Styles().textStyles?.getTextStyle('widget.title.small.bold'); - static TextStyle? get widgetTitleTiny => Styles().textStyles?.getTextStyle('widget.title.tiny'); - static TextStyle? get widgetTitleAccentExtraLarge => Styles().textStyles?.getTextStyle('widget.title.accent.extra_large'); - static TextStyle? get widgetTitleAccentLarge => Styles().textStyles?.getTextStyle('widget.title.accent.large'); - static TextStyle? get widgetTitleAccentLargeBold => Styles().textStyles?.getTextStyle('widget.title.accent.large.bold'); - static TextStyle? get widgetTitleAccentMedium => Styles().textStyles?.getTextStyle('widget.title.accent.medium'); - static TextStyle? get widgetTitleAccentMediumBold => Styles().textStyles?.getTextStyle('widget.title.accent.medium.bold'); - static TextStyle? get widgetTitleAccentRegular => Styles().textStyles?.getTextStyle('widget.title.accent.regular'); - static TextStyle? get widgetTitleAccentSmallBold => Styles().textStyles?.getTextStyle('widget.title.accent.small.bold'); - static TextStyle? get widgetTitleAccentTiny => Styles().textStyles?.getTextStyle('widget.title.accent.tiny'); - static TextStyle? get widgetDetailLarge => Styles().textStyles?.getTextStyle('widget.detail.large'); - static TextStyle? get widgetDetailLargeBold => Styles().textStyles?.getTextStyle('widget.detail.large.bold'); - static TextStyle? get widgetDetailRegularBold => Styles().textStyles?.getTextStyle('widget.detail.regular.bold'); - static TextStyle? get widgetDetailRegular => Styles().textStyles?.getTextStyle('widget.detail.regular'); - static TextStyle? get widgetDetailMedium => Styles().textStyles?.getTextStyle('widget.detail.medium'); - static TextStyle? get widgetDetailSmall => Styles().textStyles?.getTextStyle('widget.detail.small'); - static TextStyle? get widgetDetailLightRegular => Styles().textStyles?.getTextStyle('widget.detail.light.regular'); - static TextStyle? get widgetDescriptionLarge => Styles().textStyles?.getTextStyle('widget.description.large'); - static TextStyle? get widgetDescriptionMedium => Styles().textStyles?.getTextStyle('widget.description.medium'); - static TextStyle? get widgetDescriptionRegularThin => Styles().textStyles?.getTextStyle('widget.description.regular.thin'); - static TextStyle? get widgetDescriptionRegular => Styles().textStyles?.getTextStyle('widget.description.regular'); - static TextStyle? get widgetDescriptionRegularBold => Styles().textStyles?.getTextStyle('widget.description.regular.bold'); - static TextStyle? get widgetDescriptionSmall => Styles().textStyles?.getTextStyle('widget.description.small'); - static TextStyle? get widgetDescriptionSmallUnderline => Styles().textStyles?.getTextStyle('widget.description.small_underline'); - static TextStyle? get widgetDescriptionSmallBold => Styles().textStyles?.getTextStyle('widget.description.small.bold'); - static TextStyle? get widgetDescriptionSmallBoldSemiExpanded => Styles().textStyles?.getTextStyle('widget.description.small.bold.semi_expanded'); - static TextStyle? get widgetSuccessRegular => Styles().textStyles?.getTextStyle('widget.success.regular'); - static TextStyle? get widgetSuccessRegularBold => Styles().textStyles?.getTextStyle('widget.success.regular.bold'); - static TextStyle? get widgetErrorRegular => Styles().textStyles?.getTextStyle('widget.error.regular'); - static TextStyle? get widgetErrorRegularBold => Styles().textStyles?.getTextStyle('widget.error.regular.bold'); - static TextStyle? get widgetItemMediumBold => Styles().textStyles?.getTextStyle('widget.item.medium.bold'); - static TextStyle? get widgetItemMedium => Styles().textStyles?.getTextStyle('widget.item.medium'); - static TextStyle? get widgetItemRegularBold => Styles().textStyles?.getTextStyle('widget.item.regular.bold'); - static TextStyle? get widgetItemRegularThin => Styles().textStyles?.getTextStyle('widget.item.regular.thin'); - static TextStyle? get widgetItemRegular => Styles().textStyles?.getTextStyle('widget.item.regular'); - static TextStyle? get widgetItemSmallBold => Styles().textStyles?.getTextStyle('widget.item.small.bold'); - static TextStyle? get widgetItemSmall => Styles().textStyles?.getTextStyle('widget.item.small'); - static TextStyle? get widgetItemSmallThin => Styles().textStyles?.getTextStyle('widget.item.small.thin'); - static TextStyle? get widgetItemTinyBold => Styles().textStyles?.getTextStyle('widget.item.tiny.bold'); - static TextStyle? get widgetItemTiny => Styles().textStyles?.getTextStyle('widget.item.tiny'); - static TextStyle? get widgetItemTinyThin => Styles().textStyles?.getTextStyle('widget.item.tiny.thin'); - static TextStyle? get widgetInfoRegular => Styles().textStyles?.getTextStyle('widget.info.regular'); - static TextStyle? get widgetInfoRegularBold => Styles().textStyles?.getTextStyle('widget.info.regular.bold'); - static TextStyle? get widgetInfoSmall => Styles().textStyles?.getTextStyle('widget.info.small'); - static TextStyle? get widgetInfoSmallBold => Styles().textStyles?.getTextStyle('widget.info.small.bold'); - static TextStyle? get widgetTabSelected => Styles().textStyles?.getTextStyle('widget.tab.selected'); - static TextStyle? get widgetTabNotSelected => Styles().textStyles?.getTextStyle('widget.tab.not_selected'); - static TextStyle? get widgetButtonTitleRegular => Styles().textStyles?.getTextStyle('widget.button.title.regular'); - static TextStyle? get widgetButtonTitleMedium => Styles().textStyles?.getTextStyle('widget.button.title.medium'); - static TextStyle? get widgetButtonTitleMediumBold => Styles().textStyles?.getTextStyle('widget.button.title.medium.bold'); - static TextStyle? get widgetButtonTitleMediumThin => Styles().textStyles?.getTextStyle('widget.button.title.medium.thin'); - static TextStyle? get widgetButtonTitleMediumUnderline => Styles().textStyles?.getTextStyle('widget.button.title.medium.underline'); - static TextStyle? get widgetButtonTitleMediumLightUnderline => Styles().textStyles?.getTextStyle('widget.button.title.medium.light.underline'); - static TextStyle? get widgetButtonTitleEnabled => Styles().textStyles?.getTextStyle('widget.button.title.enabled'); - static TextStyle? get widgetButtonTitleDisabled => Styles().textStyles?.getTextStyle('widget.button.title.disabled'); - static TextStyle? get widgetButtonTitleSmallUnderline => Styles().textStyles?.getTextStyle('widget.button.title.small.underline'); - static TextStyle? get widgetButtonDescriptionSmall => Styles().textStyles?.getTextStyle('widget.button.description.small'); - static TextStyle? get widgetButtonDescriptionTiny => Styles().textStyles?.getTextStyle('widget.button.description.tiny'); - static TextStyle? get widgetColourfulButtonTitleTitleRegular => Styles().textStyles?.getTextStyle('widget.colourful_button.title.title.regular'); - static TextStyle? get widgetColourfulButtonTitleTitleAccent => Styles().textStyles?.getTextStyle('widget.colourful_button.title.title.accent'); - static TextStyle? get widgetInputFieldTextMedium => Styles().textStyles?.getTextStyle('widget.input_field.text.medium'); - static TextStyle? get widgetInputFieldTextRegular => Styles().textStyles?.getTextStyle('widget.input_field.text.regular'); - static TextStyle? get widgetDialogButtonClose => Styles().textStyles?.getTextStyle('widget.dialog.button.close'); - static TextStyle? get widgetDialogMessageMedium => Styles().textStyles?.getTextStyle('widget.dialog.message.medium'); - static TextStyle? get widgetDialogMessageMediumThin => Styles().textStyles?.getTextStyle('widget.dialog.message.medium.thin'); - static TextStyle? get widgetDialogMessageRegularBold => Styles().textStyles?.getTextStyle('widget.dialog.message.regular.bold'); - static TextStyle? get widgetDialogMessageLarge => Styles().textStyles?.getTextStyle('widget.dialog.message.large'); - static TextStyle? get widgetDialogMessageLargeBold => Styles().textStyles?.getTextStyle('widget.dialog.message.large.bold'); - static TextStyle? get widgetDialogMessageDarkLargeBold => Styles().textStyles?.getTextStyle('widget.dialog.message.dark.large.bold'); - static TextStyle? get widgetDialogMessageDarkLarge => Styles().textStyles?.getTextStyle('widget.dialog.message.dark.large'); - static TextStyle? get widgetDialogMessageDarkMedium => Styles().textStyles?.getTextStyle('widget.dialog.message.dark.medium'); - static TextStyle? get widgetCardTitleLarge => Styles().textStyles?.getTextStyle('widget.card.title.large'); - static TextStyle? get widgetCardTitleMedium => Styles().textStyles?.getTextStyle('widget.card.title.medium'); - static TextStyle? get widgetCardTitleRegularBold => Styles().textStyles?.getTextStyle('widget.card.title.regular.bold'); - static TextStyle? get widgetCardTitleSmall => Styles().textStyles?.getTextStyle('widget.card.title.small'); - static TextStyle? get widgetCardTitleSmallBold => Styles().textStyles?.getTextStyle('widget.card.title.small.bold'); - static TextStyle? get widgetCardTitleTiny => Styles().textStyles?.getTextStyle('widget.card.title.tiny'); - static TextStyle? get widgetCardTitleTinyBold => Styles().textStyles?.getTextStyle('widget.card.title.tiny.bold'); - static TextStyle? get widgetCardDetailRegularVariant => Styles().textStyles?.getTextStyle('widget.card.detail.regular_variant'); - static TextStyle? get widgetCardDetailRegular => Styles().textStyles?.getTextStyle('widget.card.detail.regular'); - static TextStyle? get widgetCardDetailRegularBold => Styles().textStyles?.getTextStyle('widget.card.detail.regular.bold'); - static TextStyle? get widgetCardDetailMedium => Styles().textStyles?.getTextStyle('widget.card.detail.medium'); - static TextStyle? get widgetCardDetailSmallVariant => Styles().textStyles?.getTextStyle('widget.card.detail.small_variant'); - static TextStyle? get widgetCardDetailSmallVariant2 => Styles().textStyles?.getTextStyle('widget.card.detail.small_variant2'); - static TextStyle? get widgetCardDetailSmall => Styles().textStyles?.getTextStyle('widget.card.detail.small'); - static TextStyle? get widgetCardDetailTiny => Styles().textStyles?.getTextStyle('widget.card.detail.tiny'); - static TextStyle? get widgetCardDetailTinyBold => Styles().textStyles?.getTextStyle('widget.card.detail.tiny.bold'); - static TextStyle? get widgetCardDetailTinyVariant2 => Styles().textStyles?.getTextStyle('widget.card.detail.tiny_variant2'); + 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 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 Widget? get home => Styles().images?.getImage('home'); - static Widget? get bug => Styles().images?.getImage('bug'); - static Widget? get notification => Styles().images?.getImage('notification'); - static Widget? get profile => Styles().images?.getImage('profile'); - static Widget? get chevronUp => Styles().images?.getImage('chevron-up'); - static Widget? get chevronDown => Styles().images?.getImage('chevron-down'); - static Widget? get chevronLeft => Styles().images?.getImage('chevron-left'); - static Widget? get chevronRight => Styles().images?.getImage('chevron-right'); - static Widget? get close => Styles().images?.getImage('close'); - static Widget? get retryMedium => Styles().images?.getImage('retry-medium'); + static Widget get home => Styles().images?.getImage('home') ?? Container(); + static Widget get bug => Styles().images?.getImage('bug') ?? Container(); + static Widget get notification => Styles().images?.getImage('notification') ?? Container(); + static Widget get profile => Styles().images?.getImage('profile') ?? Container(); + static Widget get chevronUp => Styles().images?.getImage('chevron-up') ?? Container(); + static Widget get chevronDown => Styles().images?.getImage('chevron-down') ?? Container(); + static Widget get chevronLeft => Styles().images?.getImage('chevron-left') ?? Container(); + static Widget get chevronRight => Styles().images?.getImage('chevron-right') ?? Container(); + static Widget get close => Styles().images?.getImage('close') ?? Container(); + static Widget get retryMedium => Styles().images?.getImage('retry-medium') ?? Container(); } diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index 485414b5d..be4fffa42 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -13,10 +13,10 @@ Map classMap = { }; Map typesMap = { - 'color': 'Color?', - 'text_style': 'TextStyle?', - 'font_family': 'String?', - 'image': 'Widget?', + 'color': 'Color', + 'text_style': 'TextStyle', + 'font_family': 'String', + 'image': 'Widget', }; Map refsMap = { @@ -26,6 +26,28 @@ Map refsMap = { 'image': 'Styles().images?.getImage(%key)', }; +Map)> defaultFuncs = { + 'color': _buildDefaultColor, + 'text_style': (name, json) => _buildDefaultClass(name, json, classFields: textStyleFields), + 'font_family': _buildDefaultString, + 'image': _buildDefaultContainer, +}; + +Map textStyleFields = { + 'color': 'color', + 'decoration_color': 'decorationColor:AppColors', + 'size': 'fontSize', + 'height': 'height', + 'font_family': 'fontFamily', + 'letter_spacing': 'letterSpacing', + 'word_spacing': 'wordSpacing', + 'decoration_thickness': 'decorationThickness', + 'decoration': 'decoration:TextDecoration', + 'overflow': 'overflow:TextOverflow', + 'decoration_style': 'decorationStyle:TextDecorationStyle', + 'font_weight': 'fontWeight:FontWeight', +}; + String capitalize(String s) => s[0].toUpperCase() + s.substring(1); String camelCase(String s, {bool startUpper = false}) { @@ -168,13 +190,81 @@ String? _buildClass(String name, Map json) { for (MapEntry entry in json.entries) { String varName = camelCase(entry.key); String varRef = ref.replaceAll("%key", "'${entry.key}'"); - classString += " static $type get $varName => $varRef;\n"; + String? defaultObj = defaultFuncs[name]?.call(name, entry); + String defaultObjString = defaultObj != null ? ' ?? $defaultObj' : ''; + classString += " static $type get $varName => $varRef$defaultObjString;\n"; replacements[varRef] = '$className.$varName'; } classString += "}\n"; return classString; } +String? _buildDefaultClass(String name, MapEntry entry, {Map? classFields}) { + String? type = typesMap[name]; + if (type == null) { + return null; + } + + dynamic value = entry.value; + if (value is Map) { + String params = ''; + for (MapEntry entry in value.entries) { + if (params.isNotEmpty) { + params += ', '; + } + + String enumType = ''; + if (classFields != null) { + String? field = classFields[entry.key]; + if (field != null) { + 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.${entry.value}'; + } else { + params += ': $enumType${entry.value}'; + } + } + return "$type($params)"; + } + return null; +} + +String? _buildDefaultColor(String name, MapEntry entry) { + 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) { + if (entry.value is String) { + return "'${entry.value}'"; + } + return null; +} + +String? _buildDefaultContainer(String name, MapEntry entry) { + return 'Container()'; +} + 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"; From 73e17ee11c9e8682fe5d8f822a0552a8adbdad32 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 1 Sep 2023 10:17:03 -0500 Subject: [PATCH 066/177] use gen styles --- assets/styles.json | 5 +- lib/gen/styles.dart | 1 + .../panels/rule_element_creation_panel.dart | 9 ++- lib/ui/panels/survey_creation_panel.dart | 27 +++---- lib/ui/panels/survey_data_creation_panel.dart | 21 +++--- .../survey_data_default_response_panel.dart | 9 ++- lib/ui/panels/survey_data_options_panel.dart | 9 ++- lib/ui/widget_builders/survey.dart | 2 +- lib/ui/widgets/form_field.dart | 2 +- lib/ui/widgets/ribbon_button.dart | 6 +- lib/ui/widgets/rounded_button.dart | 8 +- lib/ui/widgets/survey.dart | 12 +-- lib/ui/widgets/survey_creation.dart | 75 ++++++++++--------- 13 files changed, 97 insertions(+), 89 deletions(-) diff --git a/assets/styles.json b/assets/styles.json index 5ea7eeaf8..67586d0ae 100644 --- a/assets/styles.json +++ b/assets/styles.json @@ -154,8 +154,9 @@ "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.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.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"}, diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index 5f7024fe6..f485a91f8 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -138,6 +138,7 @@ class AppTextStyles { 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); diff --git a/lib/ui/panels/rule_element_creation_panel.dart b/lib/ui/panels/rule_element_creation_panel.dart index 75d9b3230..70ac470d2 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'; @@ -237,7 +238,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 +428,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 60e3fb762..f8ded457e 100644 --- a/lib/ui/panels/survey_creation_panel.dart +++ b/lib/ui/panels/survey_creation_panel.dart @@ -17,6 +17,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.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'; @@ -183,7 +184,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,), ); } @@ -272,24 +273,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 ), @@ -616,27 +617,27 @@ class _SurveyCreationPanelState extends State { if (result is List) { List textSpans = [TextSpan( text: "These are the actions that would have been taken had a user completed this survey as you did\n\n", - style: Styles().textStyles?.getTextStyle('widget.detail.regular.fat'), + style: AppTextStyles.widgetDetailRegularBold, )]; for (RuleAction action in result) { if (RuleAction.supportedPreviews.contains(action.action)) { textSpans.add(TextSpan( text: '\u2022 ${RuleAction.supportedActions[action.action]} ', - style: Styles().textStyles?.getTextStyle('widget.detail.regular.fat'), + style: AppTextStyles.widgetDetailRegularBold, )); textSpans.add(TextSpan( text: action.getSummary().replaceAll('${RuleAction.supportedActions[action.action]!} ', ''), - style: Styles().textStyles?.getTextStyle('widget.button.title.medium.fat.underline'), + style: AppTextStyles.widgetButtonTitleMediumUnderline, recognizer: TapGestureRecognizer()..onTap = () => Rules().evaluateAction(_survey!, action, immediate: true), )); textSpans.add(TextSpan( text: '\n', - style: Styles().textStyles?.getTextStyle('widget.detail.regular.fat'), + style: AppTextStyles.widgetDetailRegularBold, )); } else { textSpans.add(TextSpan( text: '\u2022 ${action.getSummary()}\n', - style: Styles().textStyles?.getTextStyle('widget.detail.regular.fat'), + style: AppTextStyles.widgetDetailRegularBold, )); } } diff --git a/lib/ui/panels/survey_data_creation_panel.dart b/lib/ui/panels/survey_data_creation_panel.dart index 1c3fe7f44..d4e5804c6 100644 --- a/lib/ui/panels/survey_data_creation_panel.dart +++ b/lib/ui/panels/survey_data_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/options.dart'; @@ -110,7 +111,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 +255,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 +283,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 +319,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 51d5e59bc..a60442273 100644 --- a/lib/ui/panels/survey_data_default_response_panel.dart +++ b/lib/ui/panels/survey_data_default_response_panel.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/form_field.dart'; @@ -74,7 +75,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 +182,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/widget_builders/survey.dart b/lib/ui/widget_builders/survey.dart index 7be037a25..e679eaa67 100644 --- a/lib/ui/widget_builders/survey.dart +++ b/lib/ui/widget_builders/survey.dart @@ -56,7 +56,7 @@ class SurveyBuilder { Text(date ?? '', style: AppTextStyles.widgetDetailSmall), Container(width: 8.0), AppImages.chevronRight ?? Container() - // UIIcon(IconAssets.chevronRight, size: 14.0, color: Styles().colors.headlineText), + // UIIcon(IconAssets.chevronRight, size: 14.0, color: AppColors.headlineText), ], ), ], diff --git a/lib/ui/widgets/form_field.dart b/lib/ui/widgets/form_field.dart index c351991ff..8f2018208 100644 --- a/lib/ui/widgets/form_field.dart +++ b/lib/ui/widgets/form_field.dart @@ -69,7 +69,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( diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index 6593832c0..c9ff1c994 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -43,13 +43,13 @@ class RibbonButton extends StatefulWidget { const RibbonButton({Key? key, this.label, this.onTap, - this.backgroundColor, //= Styles().colors.white + this.backgroundColor, //= AppColors.white this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 14), 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, diff --git a/lib/ui/widgets/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index 3014ae50f..473e90b13 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -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,7 +87,7 @@ 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 = 36.0, diff --git a/lib/ui/widgets/survey.dart b/lib/ui/widgets/survey.dart index 1cf1efc3a..69b419f0f 100644 --- a/lib/ui/widgets/survey.dart +++ b/lib/ui/widgets/survey.dart @@ -615,7 +615,7 @@ class _SurveyWidgetState extends State { children: [ 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: AppColors.fillColorPrimary, onChanged: enabled ? (value) { @@ -642,7 +642,7 @@ 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')), + Text(i.toString(), style: AppTextStyles.widgetDetailRegular), Radio(value: i, groupValue: value, activeColor: AppColors.fillColorPrimary, onChanged: enabled ? (Object? value) { survey.response = value; @@ -821,7 +821,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, @@ -844,11 +844,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) ], ), )), @@ -937,7 +937,7 @@ 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: AppColors.fillColorSecondary, value: isChecked?[index], diff --git a/lib/ui/widgets/survey_creation.dart b/lib/ui/widgets/survey_creation.dart index 2ae64a26c..d31b617d7 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 Date: Fri, 1 Sep 2023 17:22:56 -0500 Subject: [PATCH 067/177] refactor image styles --- lib/gen/styles.dart | 21 ++--- lib/service/styles.dart | 160 ++++++++++++++++++++--------------- lib/ui/widgets/ui_image.dart | 67 +++++++++++++++ pubspec.yaml | 2 +- tools/gen_styles.dart | 9 +- 5 files changed, 178 insertions(+), 81 deletions(-) create mode 100644 lib/ui/widgets/ui_image.dart diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index f485a91f8..7ced8dbb4 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -1,6 +1,7 @@ // 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 AppColors { @@ -179,14 +180,14 @@ class AppTextStyles { } class AppImages { - static Widget get home => Styles().images?.getImage('home') ?? Container(); - static Widget get bug => Styles().images?.getImage('bug') ?? Container(); - static Widget get notification => Styles().images?.getImage('notification') ?? Container(); - static Widget get profile => Styles().images?.getImage('profile') ?? Container(); - static Widget get chevronUp => Styles().images?.getImage('chevron-up') ?? Container(); - static Widget get chevronDown => Styles().images?.getImage('chevron-down') ?? Container(); - static Widget get chevronLeft => Styles().images?.getImage('chevron-left') ?? Container(); - static Widget get chevronRight => Styles().images?.getImage('chevron-right') ?? Container(); - static Widget get close => Styles().images?.getImage('close') ?? Container(); - static Widget get retryMedium => Styles().images?.getImage('retry-medium') ?? Container(); + 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/service/styles.dart b/lib/service/styles.dart index a652ad0a6..d41460bbc 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -27,6 +27,7 @@ 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/ui/widgets/ui_image.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:path/path.dart'; import 'package:http/http.dart' as http; @@ -552,13 +553,12 @@ class UiImages { final UiColors? colors; final String Function(Uri uri)? assetPathResolver; - UiImages(this.imageMap, { this.colors, this.assetPathResolver}); 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, + UiImage? getImage(String? imageKey, {ImageSpec? defaultSpec, Widget? defaultWidget, Key? key, dynamic source, double? scale, double? size, double? width, double? height, String? weight, 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, @@ -571,37 +571,29 @@ class UiImages { Map imageJson = (imageMap != null && imageKey != null) ? JsonUtils.mapValue(imageMap![imageKey]) ?? {} : {}; ImageSpec? imageSpec = ImageSpec.fromJson(imageJson) ?? 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) { - return _getFaIcon(imageSpec, type: type, source: source, key: key, size: size ?? height ?? width, weight: weight, - color: color, textDirection: textDirection, semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics); - } 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, + 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: @@ -619,14 +611,13 @@ class UiImages { "repeat":"noRepeat" } */ - - Image? _getFlutterImage(FlutterImageSpec imageSpec, { String? type, dynamic source, Key? key, + 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; @@ -642,7 +633,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); @@ -651,29 +642,29 @@ 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, 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, + 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, - 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 + 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 ) : 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, - 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 + 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; }} catch(e) { @@ -693,7 +684,7 @@ class UiImages { } */ - Widget? _getFaIcon(FontAwesomeImageSpec imageSpec, {String? type, dynamic source, + 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; @@ -709,7 +700,7 @@ class UiImages { 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) { @@ -718,35 +709,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) { @@ -794,6 +785,43 @@ abstract class ImageSpec { semanticLabel: JsonUtils.stringValue(json['semantic_label']), ); } + + factory ImageSpec.fromOther(ImageSpec spec, {dynamic source, double? scale, double? size, + double? width, double? height, String? weight, Color? color, String? semanticLabel, + 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; + + 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); + } + return imageSpec; + } } class _BaseImageSpec extends ImageSpec { @@ -864,7 +892,7 @@ class FontAwesomeImageSpec extends ImageSpec { } class _ImageUtils { - + static File? fileValue(dynamic value) { if (value is File) { return value; diff --git a/lib/ui/widgets/ui_image.dart b/lib/ui/widgets/ui_image.dart new file mode 100644 index 000000000..d7e9a76be --- /dev/null +++ b/lib/ui/widgets/ui_image.dart @@ -0,0 +1,67 @@ +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, + 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, + 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 = Styles().images?.getFlutterImage(imageSpec); + } else if (imageSpec is FontAwesomeImageSpec) { + image = Styles().images?.getFaIcon(imageSpec); + } + } catch(e) { + debugPrint(e.toString()); + } + } + return image ?? defaultWidget ?? Container(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index be2ab3422..d310639fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.5.0 homepage: environment: - sdk: ">=2.15.1 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=2.5.0" dependencies: diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index be4fffa42..e2832d12d 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -16,7 +16,7 @@ Map typesMap = { 'color': 'Color', 'text_style': 'TextStyle', 'font_family': 'String', - 'image': 'Widget', + 'image': 'UiImage', }; Map refsMap = { @@ -30,7 +30,7 @@ Map)> defaultFuncs = { 'color': _buildDefaultColor, 'text_style': (name, json) => _buildDefaultClass(name, json, classFields: textStyleFields), 'font_family': _buildDefaultString, - 'image': _buildDefaultContainer, + 'image': _buildDefaultImage, }; Map textStyleFields = { @@ -261,13 +261,14 @@ String? _buildDefaultString(String name, MapEntry entry) { return null; } -String? _buildDefaultContainer(String name, MapEntry entry) { - return 'Container()'; +String? _buildDefaultImage(String name, MapEntry entry) { + 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"); From 685dc5a3107f5bf4bd2794789ea1023f308864d9 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 5 Sep 2023 10:33:49 -0700 Subject: [PATCH 068/177] fix image color issues --- lib/service/styles.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/service/styles.dart b/lib/service/styles.dart index d41460bbc..768e29d7f 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -569,7 +569,7 @@ class UiImages { ) { 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) { imageSpec = ImageSpec.fromOther(imageSpec, source: source, scale: scale, size: size, width: width, height: height, weight: weight, @@ -763,20 +763,20 @@ 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); } 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'], @@ -852,7 +852,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']), @@ -881,8 +881,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']), From 2ce8049f0b3d183055c981d03e0b4ef138fe4933 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 6 Sep 2023 10:32:02 -0700 Subject: [PATCH 069/177] fix material images --- lib/service/styles.dart | 26 +++++++++++++++++++++----- lib/ui/widgets/ui_image.dart | 9 +++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/service/styles.dart b/lib/service/styles.dart index fe00690f9..0d7ed6212 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -561,10 +561,10 @@ class UiImages { 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, + 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} @@ -573,8 +573,10 @@ class UiImages { Map imageJson = (imageMap != null && imageKey != null) ? JsonUtils.mapValue(imageMap![imageKey]) ?? {} : {}; ImageSpec? imageSpec = ImageSpec.fromJson(imageJson, colors: colors) ?? defaultSpec; if (imageSpec != null) { - imageSpec = ImageSpec.fromOther(imageSpec, source: source, - scale: scale, size: size, width: width, height: height, weight: weight, + 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, @@ -827,7 +829,8 @@ abstract class ImageSpec { } factory ImageSpec.fromOther(ImageSpec spec, {dynamic source, double? scale, double? size, - double? width, double? height, String? weight, Color? color, String? semanticLabel, + 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}) { @@ -846,6 +849,7 @@ abstract class ImageSpec { 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, @@ -857,6 +861,18 @@ abstract class ImageSpec { 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 = FontAwesomeImageSpec(type: type, source: source, size: size, color: color, semanticLabel: semanticLabel, weight: weight, textDirection: textDirection); } diff --git a/lib/ui/widgets/ui_image.dart b/lib/ui/widgets/ui_image.dart index d7e9a76be..82505e3e7 100644 --- a/lib/ui/widgets/ui_image.dart +++ b/lib/ui/widgets/ui_image.dart @@ -18,6 +18,7 @@ class UiImage extends StatelessWidget { 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, @@ -30,8 +31,10 @@ class UiImage extends StatelessWidget { return const UiImage(); } - imageSpec = ImageSpec.fromOther(imageSpec, source: source, - scale: scale, size: size, width: width, height: height, weight: weight, + 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, @@ -57,6 +60,8 @@ class UiImage extends StatelessWidget { image = Styles().images?.getFlutterImage(imageSpec); } else if (imageSpec is FontAwesomeImageSpec) { image = Styles().images?.getFaIcon(imageSpec); + } else if (imageSpec is MaterialIconImageSpec) { + image = Styles().images?.getMaterialIcon(imageSpec); } } catch(e) { debugPrint(e.toString()); From 5d120b934988869d432b6de02c1f64a544a6abf9 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 12 Sep 2023 14:49:03 -0700 Subject: [PATCH 070/177] fix merge issues --- lib/service/config.dart | 1 - lib/service/styles.dart | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/service/config.dart b/lib/service/config.dart index 579f45523..73bd11b47 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -27,7 +27,6 @@ import 'package:rokwire_plugin/service/connectivity.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'; diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 329ab63eb..a556038ea 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -335,6 +335,7 @@ class UiColors { Color? get accentColor4 => colorMap['accentColor4']; Color? get dividerLine => colorMap['dividerLine']; + Color? get dividerLineAccent => colorMap['dividerLineAccent']; Color? get success => colorMap['success']; Color? get alert => colorMap['alert']; @@ -393,10 +394,6 @@ class UiColors { Color? get mediumGray2 => colorMap['mediumGray2']; @Deprecated("Color style names should meaningfully reflect intended usage") Color? get lightGray => colorMap['lightGray']; - Color? get disabledTextColor => colorMap['disabledTextColor']; - Color? get disabledTextColorTwo => colorMap['disabledTextColorTwo']; - Color? get dividerLine => colorMap['dividerLine']; - Color? get dividerLineAccent => colorMap['dividerLineAccent']; @Deprecated("Color style names should meaningfully reflect intended usage") Color? get mango => colorMap['mango']; From c0783492276a372f9f768c715d027f4f978c727d Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 12 Sep 2023 15:02:24 -0700 Subject: [PATCH 071/177] fix merge issues --- lib/model/auth2.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index e57835abe..e5912fa9e 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -1054,7 +1054,7 @@ class Auth2UserPrefs { (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) { From 502701a2d02bc6da234ee82d6aa842b9ee30735c Mon Sep 17 00:00:00 2001 From: Ryan Oberlander <46940735+roberlander2@users.noreply.github.com> Date: Fri, 6 Oct 2023 19:19:01 -0500 Subject: [PATCH 072/177] [#379] Integrate auth refactor (#392) * update linking to handle passkeys * add identifier type enum, allow multiple identifier types for passkey auth * update changelog * undo testing change * return alternative sign in options if passkey signin fails * add refresh change for easier testing * add username to all passkey auth requests * passkey auth type code bug fix * fix phone auth type * upgrade dependencies, add back error on anonymous auth fail * add graphql support * add possibleTypes to graphql client * allow graphql errors * fix typo * testing setup * allow identifier-less sign in for passkeys * identifier-less flow for web working * fix material icon styles * begin adding auth identifier class and refactoring * resolve errors * change passkey to git dependency * more auth service, model refactoring * more auth service refactoring * oidc sso integration * set route name in onboarding, allow login with identifier ID * Upgrade to connectivity_plus [rokmetro/vogue-app#45]. * Fixed Android build: require gradle-7.3.3 for Java 17. * move identifier type, login type enums into auth type, auth identifier classes * add mastodon functionality * add relative time util * update flex ui auth rules * add media to status creator * add update username function * handle mastodon sign up * add poll display and vote * remove username from account and email, phone from account profile * improve encryption util * fix passkey auth bugs, add app_type_identifier field back to link auth type * add more error status codes to passkey auth * change passkey sign up creation options to string * auth type model fix, ribbon button description padding fix * use oidcAuthType getter instead of Auth2Type.typeOidcIllinois * update changelog * upgrade webview * handle account secrets --------- Co-authored-by: Stephen Hurwit Co-authored-by: Mihail Varbanov --- .gitignore | 1 + CHANGELOG.md | 2 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/pubspec.lock | 401 ++++++-- lib/model/auth2.dart | 350 ++++--- lib/platform_impl/base.dart | 4 +- lib/platform_impl/mobile.dart | 22 +- lib/platform_impl/stub.dart | 4 +- lib/platform_impl/web.dart | 87 +- lib/rokwire_plugin.dart | 8 +- lib/service/app_datetime.dart | 60 +- lib/service/auth2.dart | 859 ++++++++++-------- lib/service/config.dart | 3 +- lib/service/flex_ui.dart | 14 +- lib/service/graph_ql.dart | 53 ++ lib/service/groups.dart | 4 +- lib/service/onboarding.dart | 12 +- lib/service/rules.dart | 21 +- lib/service/storage.dart | 35 +- lib/service/styles.dart | 5 +- lib/ui/panels/web_panel.dart | 47 +- lib/ui/popups/popup_message.dart | 2 +- lib/ui/widgets/ribbon_button.dart | 5 +- lib/utils/utils.dart | 18 + pubspec.yaml | 11 +- tools/encrypt_util.dart | 2 + 26 files changed, 1325 insertions(+), 707 deletions(-) create mode 100644 lib/service/graph_ql.dart 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 66446b178..405080bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Load again content attributes JSON from content service [#359](https://github.com/rokwire/app-flutter-plugin/issues/359). - Build event time filters in local timezone [#377](https://github.com/rokwire/app-flutter-plugin/issues/377). - Upgrade to connectivity_plus [#45](https://github.com/rokmetro/vogue-app/issues/45). +- Integrate auth refactor [#379](https://github.com/rokwire/app-flutter-plugin/issues/379) ### Added - Survey creation tool [#263](https://github.com/rokwire/app-flutter-plugin/issues/263). - Web app authentication support [#291](https://github.com/rokwire/app-flutter-plugin/issues/291) @@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Override survey action summary [#373](https://github.com/rokwire/app-flutter-plugin/issues/373) - Added registrationOccupancy to Event2PersonsResult and relevant utility methods [#375](https://github.com/rokwire/app-flutter-plugin/issues/375). - 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) ### Fixed - Upgrade dependencies for Flutter v3.10 [#285](https://github.com/rokwire/app-flutter-plugin/issues/285) 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 d38753995..7bc2b5405 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -49,6 +49,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: @@ -61,10 +69,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" connectivity_plus: dependency: transitive description: @@ -141,10 +149,10 @@ packages: dependency: transitive description: name: device_calendar - sha256: ff10736e53bffa3f0982668ed2affd61abb5f2b3a3c1bba7797ea29d4f616c1e + sha256: "5a1ce7887b4ffbaf3743078c8314dede5e694cddd69bab43f35ce815c5d82a7d" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.1" device_info: dependency: transitive description: @@ -193,6 +201,38 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "182c3f8350cee659f7b115e956047ee3dc672a96665883a545e81581b9a82c72" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" firebase_core: dependency: transitive description: @@ -270,14 +310,22 @@ 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: name: flutter_html - sha256: "850c07bc6c1ed060d3eb3e88469a598260a13eb45d8978b197c1348e0a2b101f" + sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" url: "https://pub.dev" source: hosted - version: "3.0.0-beta.1" + version: "3.0.0-beta.2" flutter_lints: dependency: "direct dev" description: @@ -290,10 +338,10 @@ packages: dependency: transitive description: name: flutter_local_notifications - sha256: e76db45e04af231d3b2b1832bb5a624ad895b64fec3d0ca35a514daa19f16743 + sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "12.0.4" flutter_local_notifications_linux: dependency: transitive description: @@ -321,10 +369,11 @@ packages: flutter_passkey: dependency: transitive description: - name: flutter_passkey - sha256: "4a7e9085ab9602c7ee472d3650367b2ee4bc5e3ba15142e38dd32a732e763530" - url: "https://pub.dev" - source: hosted + 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 @@ -334,11 +383,75 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.15" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" + url: "https://pub.dev" + source: hosted + version: "8.1.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: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" + url: "https://pub.dev" + source: hosted + version: "2.1.1" 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,18 +485,18 @@ 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: @@ -416,6 +529,86 @@ packages: 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: "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" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" html: dependency: transitive description: @@ -444,42 +637,66 @@ packages: dependency: transitive description: name: image_picker - sha256: "9978d3510af4e6a902e545ce19229b926e6de6a1828d6134d3aab2e129a4d270" + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" url: "https://pub.dev" source: hosted - version: "0.8.7+5" + version: "1.0.4" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: c2f3c66400649bd132f721c88218945d6406f693092b2f741b79ae9cdb046e59 + sha256: "47da2161c2e9f8f8a9cbbd89d466d174333fbdd769aeed848912e0b16d9cb369" url: "https://pub.dev" source: hosted - version: "0.8.6+16" + version: "0.8.8" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c" + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" url: "https://pub.dev" source: hosted - version: "2.1.12" + version: "3.0.1" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0 + sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 url: "https://pub.dev" source: hosted - version: "0.8.7+4" + version: "0.8.8+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "1991219d9dbc42a99aff77e663af8ca51ced592cd6685c9485e3458302d3d4f8" + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + url: "https://pub.dev" + source: hosted + version: "2.9.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" url: "https://pub.dev" source: hosted - version: "2.6.3" + version: "0.2.1+1" intl: dependency: transitive description: @@ -524,18 +741,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -544,6 +761,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" mime_type: dependency: transitive description: @@ -560,14 +785,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: @@ -679,14 +920,22 @@ packages: relative: true source: path version: "1.5.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: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.1" shared_preferences_android: dependency: transitive description: @@ -715,10 +964,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" shared_preferences_web: dependency: transitive description: @@ -744,18 +993,18 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" sprintf: dependency: transitive description: name: sprintf - sha256: ec76d38910b6f1c854ce1353c62d37e7ef82b53dc5ab048c25400d35970776d1 + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.0" sqflite: dependency: transitive description: @@ -816,18 +1065,18 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" timezone: dependency: transitive description: name: timezone - sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e" + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.2" typed_data: dependency: transitive description: @@ -860,6 +1109,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: transitive description: @@ -940,38 +1205,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + url: "https://pub.dev" + source: hosted + 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: @@ -980,6 +1261,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.4" + 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: @@ -997,5 +1286,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.7.0" diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index e5912fa9e..9f4e51620 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -3,6 +3,7 @@ import 'dart:collection'; 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'; @@ -68,81 +69,39 @@ class Auth2Token { } } -//////////////////////////////// -// Auth2LoginType - -enum Auth2LoginType { anonymous, apiKey, email, phone, username, phoneTwilio, oidc, oidcIllinois, passkey } - -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'; - case Auth2LoginType.passkey: return 'webauthn'; - } -} - -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; - } - else if (value == 'webauthn') { - return Auth2LoginType.passkey; - } - 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 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, ); @@ -151,12 +110,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 + secrets: JsonUtils.mapValue(json['secrets']) ?? {}, //TBD Auth2 permissions: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['permissions'])), roles: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['roles'])), groups: Auth2StringEntry.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; @@ -165,12 +125,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, }; @@ -180,22 +141,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)); @@ -203,14 +164,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; } } @@ -218,11 +226,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); } } @@ -246,6 +254,22 @@ class Auth2Account { bool hasPermission(String premission) => (Auth2StringEntry.findInList(permissions, name: premission) != null); 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 { @@ -271,9 +295,6 @@ class Auth2UserProfile { int? _birthYear; String? _photoUrl; - String? _email; - String? _phone; - String? _address; String? _state; String? _zip; @@ -282,9 +303,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, @@ -293,8 +313,6 @@ class Auth2UserProfile { _birthYear = birthYear, _photoUrl = photoUrl, - _email = email, - _phone = phone, _address = address, _state = state, @@ -314,8 +332,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']), @@ -345,9 +361,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, @@ -366,8 +379,6 @@ class Auth2UserProfile { 'birth_year': _birthYear, 'photo_url': _photoUrl, - 'email': _email, - 'phone': _phone, 'address': _address, 'state': _state, @@ -388,8 +399,6 @@ class Auth2UserProfile { (other._birthYear == _birthYear) && (other._photoUrl == _photoUrl) && - (other._email == _email) && - (other._phone == _phone) && (other._address == _address) && (other._state == _state) && @@ -407,8 +416,6 @@ class Auth2UserProfile { (_birthYear?.hashCode ?? 0) ^ (_photoUrl?.hashCode ?? 0) ^ - (_email?.hashCode ?? 0) ^ - (_phone?.hashCode ?? 0) ^ (_address?.hashCode ?? 0) ^ (_state?.hashCode ?? 0) ^ @@ -460,20 +467,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) || @@ -522,8 +515,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; @@ -645,46 +636,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), }; } @@ -692,35 +790,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) { diff --git a/lib/platform_impl/base.dart b/lib/platform_impl/base.dart index dc9c2de32..966ae941d 100644 --- a/lib/platform_impl/base.dart +++ b/lib/platform_impl/base.dart @@ -14,6 +14,6 @@ abstract class BasePasskey { Future arePasskeysSupported(); - Future getPasskey(Map? options); - Future createPasskey(Map? options); + 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 index 4bce8fb8a..2c225bcaf 100644 --- a/lib/platform_impl/mobile.dart +++ b/lib/platform_impl/mobile.dart @@ -26,14 +26,24 @@ class PasskeyImpl extends BasePasskey { } @override - Future getPasskey(Map? options) async { - Map? pubKeyRequest = options?['publicKey']; - return await flutterPasskeyPlugin.getCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + 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(Map? options) async { - Map? pubKeyRequest = options?['publicKey']; - return await flutterPasskeyPlugin.createCredential(JsonUtils.encode(pubKeyRequest) ?? ''); + 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 index 9ec67f377..6feba40c3 100644 --- a/lib/platform_impl/stub.dart +++ b/lib/platform_impl/stub.dart @@ -21,12 +21,12 @@ class PasskeyImpl extends BasePasskey { } @override - Future getPasskey(Map? options) { + Future getPasskey(String? optionsJson) { throw Exception("Unimplemented"); } @override - Future createPasskey(Map? options) { + 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 index f59bc2dfb..7c17eff1b 100644 --- a/lib/platform_impl/web.dart +++ b/lib/platform_impl/web.dart @@ -12,93 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:html'; -import 'dart:typed_data'; - +import 'package:js/js.dart'; +import 'package:js/js_util.dart'; import 'package:rokwire_plugin/platform_impl/base.dart'; -import 'package:rokwire_plugin/utils/utils.dart'; -import 'package:js/js.dart'; +@JS('isSupported') +external bool isSupported(); -@JS('atob') -external String atob(String value); +@JS('getPasskey') +external dynamic getPasskeyJS(String? optionsJson); -@JS('btoa') -external String btoa(String value); +@JS('createPasskey') +external dynamic createPasskeyJS(String? optionsJson); class PasskeyImpl extends BasePasskey { @override Future arePasskeysSupported() { - return Future.value(window.navigator.credentials != null); + return Future.value(isSupported()); } @override - Future getPasskey(Map? options) async { - if (options?['publicKey']?['challenge'] is String) { - String challenge = options!['publicKey']['challenge']; - options['publicKey']['challenge'] = _encodedStringToBuffer(challenge); - } - if (options?['publicKey']?['allowCredentials'] is Iterable) { - Iterable credentials = options!['publicKey']?['allowCredentials']; - for (int i = 0; i < credentials.length; i++) { - dynamic credential = credentials.elementAt(i); - if (credential is Map && credential['id'] is String) { - credentials.elementAt(i)['id'] = _encodedStringToBuffer(credential['id']); - } - } - } - - PublicKeyCredential credential = await window.navigator.credentials!.get(options); - AuthenticatorResponse? authResponse = credential.response; - if (authResponse is AuthenticatorAssertionResponse) { - Map response = { - 'id': credential.id, - 'rawId': _bufferToEncodedString(credential.rawId), - 'type': credential.type, - 'response': { - 'authenticatorData': _bufferToEncodedString(authResponse.authenticatorData), - 'clientDataJSON': _bufferToEncodedString(authResponse.clientDataJson), - 'signature': _bufferToEncodedString(authResponse.signature), - } - }; - - return JsonUtils.encode(response); - } - - return null; + Future getPasskey(String? optionsJson) { + return promiseToFuture(getPasskeyJS(optionsJson)); } @override - Future createPasskey(Map? options) async { - if (options?['publicKey']?['challenge'] is String) { - String challenge = options!['publicKey']['challenge']; - options['publicKey']['challenge'] = _encodedStringToBuffer(challenge); - } - if (options?['publicKey']?['user']?['id'] is String) { - String userId = options!['publicKey']['user']['id']; - options['publicKey']['user']['id'] = _encodedStringToBuffer(userId); - } - - PublicKeyCredential credential = await window.navigator.credentials!.create(options); - AuthenticatorResponse? authResponse = credential.response; - if (authResponse is AuthenticatorAttestationResponse) { - Map response = { - 'id': credential.id, - 'rawId': _bufferToEncodedString(credential.rawId), - 'type': credential.type, - 'response': { - 'attestationObject': _bufferToEncodedString(authResponse.attestationObject), - 'clientDataJSON': _bufferToEncodedString(authResponse.clientDataJson), - } - }; - - return JsonUtils.encode(response); - } - - return null; + Future createPasskey(String? optionsJson) { + return promiseToFuture(createPasskeyJS(optionsJson)); } - - ByteBuffer _encodedStringToBuffer(String value) => Uint8List.fromList(atob(value.replaceAll('_', '/').replaceAll('-', '+')).codeUnits).buffer; - - String _bufferToEncodedString(ByteBuffer? buffer) => btoa(String.fromCharCodes(buffer?.asUint8List() ?? [])).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); } \ No newline at end of file diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index 1d1c470fa..d46f0c3d9 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -93,12 +93,12 @@ class RokwirePlugin { return false; } - static Future getPasskey(Map? options) async { - return await PasskeyImpl().getPasskey(options); + static Future getPasskey(String? optionsJson) async { + return await PasskeyImpl().getPasskey(optionsJson); } - static Future createPasskey(Map? options) async { - return await PasskeyImpl().createPasskey(options); + static Future createPasskey(String? optionsJson) async { + return await PasskeyImpl().createPasskey(optionsJson); } // Compound APIs diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index 9440f8da9..d52f41a2a 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -131,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; } formattedDateTime = formattedDateTime?.toLowerCase(); if (showTzSuffix && (formattedDateTime != null)) { @@ -148,7 +148,7 @@ class AppDateTime with Service { catch (e) { debugPrint(e.toString()); } - return formattedDateTime; + return formattedDateTime != null ? StringUtils.capitalize(formattedDateTime) : null; } DateTime? dateTimeLocalFromJson(dynamic json) { @@ -159,7 +159,7 @@ class AppDateTime with Service { return DateTimeUtils.utcDateTimeToString(getUtcTimeFromDeviceTime(dateTime)); } - String getDisplayDateTime(DateTime? dateTimeUtc, {String? format, bool allDay = false, bool considerSettingsDisplayTime = true, bool includeAtSuffix = false, bool multiLine = 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 ''; } @@ -168,18 +168,24 @@ class AppDateTime with Service { 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); + 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')}"; @@ -213,6 +219,44 @@ class AppDateTime with Service { 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/auth2.dart b/lib/service/auth2.dart index 90c2618d6..8a6e366d1 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -2,6 +2,7 @@ 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'; @@ -31,6 +32,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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"; @@ -60,6 +62,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Client? _updateUserProfileClient; Timer? _updateUserProfileTimer; + Client? _updateUserSecretsClient; + Timer? _updateUserSecretsTimer; + Auth2Token? _token; Auth2Account? _account; @@ -95,6 +100,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { AppLifecycle.notifyStateChanged, Auth2UserProfile.notifyChanged, Auth2UserPrefs.notifyChanged, + Auth2Account.notifySecretsChanged, ]); } @@ -125,12 +131,12 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { if (!await authenticateAnonymously()) { - // throw ServiceError( - // source: this, - // severity: ServiceErrorSeverity.fatal, - // title: 'Authentication Initialization Failed', - // description: 'Failed to initialize anonymous authentication token.', - // ); + throw ServiceError( + source: this, + severity: ServiceErrorSeverity.fatal, + title: 'Authentication Initialization Failed', + description: 'Failed to initialize anonymous authentication token.', + ); } } @@ -170,6 +176,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { else if (name == AppLifecycle.notifyStateChanged) { onAppLifecycleStateChanged(param); } + else if (name == Auth2Account.notifySecretsChanged) { + onAccountSecretsChanged(param); + } } @protected @@ -228,19 +237,14 @@ 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)) { + 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; - Auth2LoginType get passkeyLoginType => Auth2LoginType.passkey; - Auth2Token? get token => _token ?? _anonymousToken; Auth2Token? get userToken => _token; Auth2Token? get anonymousToken => _anonymousToken; @@ -251,26 +255,40 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String? get accountId => _account?.id ?? _anonymousId; Auth2UserPrefs? get prefs => _account?.prefs ?? _anonymousPrefs; Auth2UserProfile? get profile => _account?.profile ?? _anonymousProfile; - Auth2LoginType? get loginType => _account?.authType?.loginType; + String? get loginType => _account?.authType?.code; 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 isPasskeyLoggedIn => (_account?.authType?.loginType == passkeyLoginType); - - 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; - bool get isPasskeyLinked => _account?.isAuthTypeLinked(passkeyLoginType) ?? false; - - 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 linkedPasskey => _account?.getLinkedForAuthType(passkeyLoginType) ?? []; + bool get isOidcLoggedIn => (_account?.authType?.code == oidcAuthType || _account?.authType?.code == Auth2Type.typeOidc); + bool get isCodeLoggedIn => (_account?.authType?.code == Auth2Type.typeCode); + bool get isPasswordLoggedIn => (_account?.authType?.code == Auth2Type.typePassword); + bool get isPasskeyLoggedIn => (_account?.authType?.code == Auth2Type.typePasskey); + + 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(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; @@ -278,10 +296,28 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 username => _account?.username; + 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"); bool get isDebugManager => hasRole("debug"); @@ -303,6 +339,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String? get votePlace => prefs?.voter?.votePlace; // Overrides + @protected + String get oidcAuthType => Auth2Type.typeOidcIllinois; @protected Auth2UserPrefs get defaultAnonymousPrefs => Auth2UserPrefs.empty(); @@ -325,7 +363,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'Content-Type': 'application/json' }; Map postData = { - 'auth_type': auth2LoginTypeToString(Auth2LoginType.anonymous), + 'auth_type': Auth2Type.typeAnonymous, 'device': deviceInfo, }; Map? additionalParams = _getConfigParams(postData); @@ -355,9 +393,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } // Passkey authentication - Future authenticateWithPasskey(String? username) async { + + Future authenticateWithPasskey({String? identifier, String identifierType = Auth2Identifier.typeUsername, String? identifierId}) async { String? errorMessage; - if ((Config().authBaseUrl != null) && (username != null)) { + if (Config().authBaseUrl != null) { if (!await RokwirePlugin.arePasskeysSupported()) { return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotSupported); } @@ -366,17 +405,21 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Map headers = { 'Content-Type': 'application/json' }; + Map creds = {}; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } Map postData = { - 'auth_type': auth2LoginTypeToString(passkeyLoginType), - 'creds': { - 'username': username, - }, + '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) { @@ -390,23 +433,42 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Obtain creationOptions from the server String? responseBody = response.body; Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(responseBody)); - Map? requestJson = JsonUtils.decode(message?.message ?? ''); try { - String? responseData = await RokwirePlugin.getPasskey(requestJson); - return _completeSignInWithPasskey(username, responseData); + 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 if (Auth2Error.fromJson(JsonUtils.decodeMap(response?.body))?.status == 'not-found') { - return Auth2PasskeySignInResult(Auth2PasskeySignInResultStatus.failedNotFound); + } 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 username, String? responseData) async { + 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); @@ -418,13 +480,18 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Map headers = { 'Content-Type': 'application/json' }; + Map creds = { + "response": JsonUtils.encode(requestJson), + }; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } Map postData = { - 'auth_type': auth2LoginTypeToString(passkeyLoginType), - 'creds': { - "username": username, - "response": JsonUtils.encode(requestJson), - }, + '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) { @@ -440,14 +507,25 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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? username, String? displayName, {bool? public = false}) async { + Future signUpWithPasskey(String identifier, {String? displayName, String identifierType = Auth2Identifier.typeUsername, bool? public = false, bool verifyIdentifier = false}) async { String? errorMessage; - if ((Config().authBaseUrl != null) && (username != null)) { + if (Config().authBaseUrl != null) { if (!await RokwirePlugin.arePasskeysSupported()) { return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failedNotSupported); } @@ -468,9 +546,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'Content-Type': 'application/json' }; Map postData = { - 'auth_type': auth2LoginTypeToString(passkeyLoginType), + 'auth_type': Auth2Type.typePasskey, 'creds': { - 'username': username, + identifierType: identifier, }, 'params': { "display_name": displayName, @@ -478,6 +556,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'privacy': { 'public': public, }, + 'username': identifierType == Auth2Identifier.typeUsername ? identifier : null, 'profile': profile?.toJson(), 'preferences': _anonymousPrefs?.toJson(), 'device': deviceInfo, @@ -493,20 +572,35 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (response != null && response.statusCode == 200) { // Obtain creationOptions from the server Auth2Message? message = Auth2Message.fromJson(JsonUtils.decode(response.body)); - Map? requestJson = JsonUtils.decode(message?.message ?? ''); - try { - String? responseData = await RokwirePlugin.createPasskey(requestJson); - return _completeSignUpWithPasskey(username, responseData); - } catch(error) { + if (message != null) { + if (verifyIdentifier) { + return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded, creationOptions: message.message); + } try { - String? responseData = await RokwirePlugin.getPasskey(requestJson); - Auth2PasskeySignInResult result = await _completeSignInWithPasskey(username, responseData); - if (result.status == Auth2PasskeySignInResultStatus.succeeded) { - return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); - } + String? responseData = await RokwirePlugin.createPasskey(message.message); + return completeSignUpWithPasskey(identifier, responseData, identifierType: identifierType); } catch(error) { - errorMessage = error.toString(); - Log.e(errorMessage); + 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) { + errorMessage = error.toString(); + Log.e(errorMessage); + } + } + } 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); } } } @@ -517,18 +611,19 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.failed, error: errorMessage); } - Future _completeSignUpWithPasskey(String username, String? responseData) async { + 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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(passkeyLoginType), + 'auth_type': Auth2Type.typePasskey, 'creds': { - "username": username, + identifierType: identifier, "response": responseData, }, + 'username': identifierType == Auth2Identifier.typeUsername ? identifier : null, 'device': deviceInfo, }; Map? additionalParams = _getConfigParams(postData); @@ -540,7 +635,22 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); if (response != null && response.statusCode == 200) { - return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); + 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); @@ -553,7 +663,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (_oidcAuthenticationCompleters == null) { _oidcAuthenticationCompleters = >[]; - NotificationService().notify(notifyLoginStarted, oidcLoginType); + NotificationService().notify(notifyLoginStarted, oidcAuthType); _OidcLogin? oidcLogin = await getOidcData(); if (oidcLogin?.loginUrl != null) { @@ -586,7 +696,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _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 { @@ -607,7 +717,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'Content-Type': 'application/json' }; Map postData = { - 'auth_type': auth2LoginTypeToString(oidcLoginType), + 'auth_type': oidcAuthType, 'creds': uri?.toString(), 'params': _oidcLogin?.params, 'profile': _anonymousProfile?.toJson(), @@ -690,7 +800,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 'Content-Type': 'application/json' }; Map postData = { - 'auth_type': auth2LoginTypeToString(oidcLoginType), + 'auth_type': oidcAuthType, }; Map? additionalParams = _getConfigParams(postData); if (additionalParams != null) { @@ -729,7 +839,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected void completeOidcAuthentication(Auth2OidcAuthenticateResult? result) { - _notifyLogin(oidcLoginType, result == Auth2OidcAuthenticateResult.succeeded); + _notifyLogin(oidcAuthType, result == Auth2OidcAuthenticateResult.succeeded); _oidcLogin = null; _oidcScope = null; @@ -745,147 +855,161 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - // Phone Authentication + // Code Authentication - Future authenticateWithPhone(String? phoneNumber, {bool? public = false}) async { - if ((Config().authBaseUrl != 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().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; + Map creds = {}; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } Map postData = { - 'auth_type': auth2LoginTypeToString(phoneLoginType), - 'creds': { - "phone": phoneNumber, - }, + '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 Auth2PhoneRequestCodeResult.failed; + return Auth2RequestCodeResult.failed; } 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 }) async { - if ((Config().authBaseUrl != null) && (phoneNumber != null) && (code != null)) { + Future handleCodeAuthentication(String? identifier, String? code, {String identifierType = Auth2Identifier.typePhone, String? identifierId, Auth2AccountScope? scope}) async { + if ((Config().authBaseUrl != null) && (identifier != null || identifierId != null) && (code != null)) { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; + Map creds = { + "code": code, + }; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } Map postData = { - 'auth_type': auth2LoginTypeToString(phoneLoginType), - 'creds': { - "phone": phoneNumber, - "code": code, - }, + '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 Auth2PhoneSendCodeResult.failed; + return Auth2SendCodeResult.failed; } 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 }) async { - if ((Config().authBaseUrl != null) && (email != null) && (password != null)) { + Future authenticateWithPassword(String? identifier, String? password, {String identifierType = Auth2Identifier.typeEmail, String? identifierId, Auth2AccountScope? scope}) async { + if ((Config().authBaseUrl != null) && (identifier != null || identifierId != null) && (password != null)) { - NotificationService().notify(notifyLoginStarted, emailLoginType); + NotificationService().notify(notifyLoginStarted, Auth2Type.typePassword); String url = "${Config().authBaseUrl}/auth/login"; Map headers = { 'Content-Type': 'application/json' }; + Map creds = { + "password": password, + }; + if (StringUtils.isNotEmpty(identifier)) { + creds[identifierType] = identifier; + } Map postData = { - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'creds': { - "email": email, - "password": password - }, + '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 Auth2EmailSignInResult.failed; + return Auth2PasswordSignInResult.failed; } 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, {bool? public = false}) async { - if ((Config().authBaseUrl != null) && (email != null) && (password != null)) { + 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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(emailLoginType), + 'auth_type': Auth2Type.typePassword, 'creds': { - "email": email, + identifierType: identifier, "password": password }, 'params': { @@ -903,29 +1027,30 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (additionalParams != null) { postData.addAll(additionalParams); } else { - return Auth2EmailSignUpResult.failed; + return Auth2PasswordSignUpResult.failed; } 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().authBaseUrl != null) && (email != null)) { + 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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'user_identifier': email, + 'identifier': { + identifierType: identifier, + } }; Map? additionalParams = _getConfigParams(postData); if (additionalParams != null) { @@ -936,58 +1061,59 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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().authBaseUrl != null) && (email != null)) { + 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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'user_identifier': email, - 'identifier': email, + 'auth_type': Auth2Type.typePassword, + 'identifier': { + identifierType: identifier, + }, }; Map? additionalParams = _getConfigParams(postData); if (additionalParams != null) { postData.addAll(additionalParams); } else { - return Auth2EmailForgotPasswordResult.failed; + return Auth2ForgotPasswordResult.failed; } 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().authBaseUrl != null) && (email != null)) { - String url = "${Config().authBaseUrl}/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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(emailLoginType), - 'user_identifier': email, - 'identifier': email, + 'identifier': { + identifierType: identifier, + }, }; Map? additionalParams = _getConfigParams(postData); if (additionalParams != null) { @@ -1002,108 +1128,81 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return false; } - // Username Authentication + // Notify Login - Future authenticateWithUsername(String? username, String? password, { Auth2AccountScope? scope }) async { - if ((Config().authBaseUrl != 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().authBaseUrl}/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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'creds': { - "username": username, - "password": password - }, - 'params': { - "sign_up": false, + 'identifier': { + identifierType: identifier, }, - 'profile': _anonymousProfile?.toJson(), - 'preferences': _anonymousPrefs?.toJson(), - 'device': deviceInfo, }; Map? additionalParams = _getConfigParams(postData); if (additionalParams != null) { postData.addAll(additionalParams); } else { - return Auth2UsernameSignInResult.failed; + return null; } 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, {bool? public = false, Auth2AccountScope? scope }) async { - if ((Config().authBaseUrl != null) && (username != null) && (password != null)) { - String url = "${Config().authBaseUrl}/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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'creds': { - "username": username, - "password": password - }, - 'params': { - "sign_up": true, - "confirm_password": password + 'identifier': { + identifierType: identifier, }, - 'privacy': { - 'public': public, - }, - 'profile': _anonymousProfile?.toJson(), - 'preferences': _anonymousPrefs?.toJson(), - 'device': deviceInfo, }; Map? additionalParams = _getConfigParams(postData); if (additionalParams != null) { postData.addAll(additionalParams); } else { - return Auth2UsernameSignUpResult.failed; + return null; } 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().authBaseUrl != null) && (username != null)) { - String url = "${Config().authBaseUrl}/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' }; Map postData = { - 'auth_type': auth2LoginTypeToString(usernameLoginType), - 'user_identifier': username, + 'identifier': { + identifierType: identifier, + }, }; Map? additionalParams = _getConfigParams(postData); if (additionalParams != null) { @@ -1114,84 +1213,91 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 Checks + // Account Identifier Linking - Future canSignIn(String? identifier, Auth2LoginType loginType) async { - if ((Config().authBaseUrl != null) && (identifier != null)) { - String url = "${Config().authBaseUrl}/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' }; - Map postData = { - 'auth_type': auth2LoginTypeToString(loginType), - 'user_identifier': identifier, - }; - Map? additionalParams = _getConfigParams(postData); - if (additionalParams != null) { - postData.addAll(additionalParams); - } else { - return null; - } + String? post = JsonUtils.encode({ + 'identifier': { + identifierType: identifier, + }, + }); - Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); + 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().authBaseUrl != null) && (identifier != null)) { - String url = "${Config().authBaseUrl}/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' }; - Map postData = { - 'auth_type': auth2LoginTypeToString(loginType), - 'user_identifier': identifier, - }; - Map? additionalParams = _getConfigParams(postData); - if (additionalParams != null) { - postData.addAll(additionalParams); - } else { - return null; - } + String? body = JsonUtils.encode({ + 'id': id + }); - Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); - 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, @@ -1201,49 +1307,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) { - await Storage().setAuth2Account(_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) { - await Storage().setAuth2Account(_account = Auth2Account.fromOther(_account, authTypes: authTypes)); + await Storage().setAuth2Account(_account = Auth2Account.fromOther(_account, identifiers: identifiers, authTypes: authTypes)); NotificationService().notify(notifyLinkChanged); return true; } @@ -1471,6 +1579,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"; @@ -1573,6 +1724,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)) { @@ -1726,32 +1896,31 @@ class Auth2Csrf with NetworkAuthProvider { @override Future refreshNetworkAuthTokenIfNeeded(BaseResponse? response, dynamic token) async { - if ((response?.statusCode == 401) && (token is Auth2Token) && (Auth2().token == token)) { + 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; } } -// Auth2EmailAccountState - -enum Auth2PasskeyAccountState { - nonExistent, - exists, -} - // Auth2PasskeySignUpResult class Auth2PasskeySignUpResult { Auth2PasskeySignUpResultStatus status; String? error; - Auth2PasskeySignUpResult(this.status, {this.error}); + String? creationOptions; + Auth2PasskeySignUpResult(this.status, {this.error, this.creationOptions}); } enum Auth2PasskeySignUpResultStatus { succeeded, failed, failedNotSupported, + failedAccountExist, + failedNotFound, + failedActivationExpired, + failedNotActivated, } // Auth2PasskeySignInResult @@ -1765,88 +1934,100 @@ enum Auth2PasskeySignInResultStatus { succeeded, failed, failedNotFound, + failedActivationExpired, + failedNotActivated, failedNotSupported, + failedNoCredentials, + failedCancelled, } -// Auth2PhoneRequestCodeResult +// Auth2RequestCodeResult -enum Auth2PhoneRequestCodeResult { +enum Auth2RequestCodeResult { succeeded, failed, failedAccountExist, } -Auth2PhoneRequestCodeResult auth2PhoneRequestCodeResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2PhoneRequestCodeResult.succeeded; - case Auth2LinkResult.failedAccountExist: return Auth2PhoneRequestCodeResult.failedAccountExist; - default: return Auth2PhoneRequestCodeResult.failed; +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}); +} -enum Auth2EmailSignUpResult { +// Auth2PasswordSignUpResult + +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, @@ -1862,56 +2043,22 @@ enum Auth2OidcAuthenticateResult { } Auth2OidcAuthenticateResult auth2OidcAuthenticateResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2OidcAuthenticateResult.succeeded; - case Auth2LinkResult.failedAccountExist: return Auth2OidcAuthenticateResult.failedAccountExist; + switch (value.status) { + case Auth2LinkResultStatus.succeeded: return Auth2OidcAuthenticateResult.succeeded; + case Auth2LinkResultStatus.failedAccountExist: return Auth2OidcAuthenticateResult.failedAccountExist; default: return Auth2OidcAuthenticateResult.failed; } } -// Auth2UsernameAccountState - -enum Auth2UsernameAccountState { - nonExistent, - exists, -} - -// Auth2UsernameSignUpResult - -enum Auth2UsernameSignUpResult { - 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, -} +// Auth2LinkResult -Auth2UsernameSignInResult auth2UsernameSignInResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value) { - case Auth2LinkResult.succeeded: return Auth2UsernameSignInResult.succeeded; - case Auth2LinkResult.failedInvalid: return Auth2UsernameSignInResult.failedInvalid; - default: return Auth2UsernameSignInResult.failed; - } +class Auth2LinkResult { + Auth2LinkResultStatus status; + String? message; + Auth2LinkResult(this.status, {this.message}); } -// Auth2LinkResult - -enum Auth2LinkResult { +enum Auth2LinkResultStatus { succeeded, failed, failedActivationExpired, diff --git a/lib/service/config.dart b/lib/service/config.dart index b6678b421..9726fd5b4 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -443,9 +443,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; } diff --git a/lib/service/flex_ui.dart b/lib/service/flex_ui.dart index 2986197a5..a74f7df02 100644 --- a/lib/service/flex_ui.dart +++ b/lib/service/flex_ui.dart @@ -597,21 +597,15 @@ 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 == 'usernameLoggedIn') && (value is bool)) { - result = result && (Auth2().isUsernameLoggedIn == value); + else if ((key == 'passwordLoggedIn') && (value is bool)) { + result = result && (Auth2().isPasswordLoggedIn == value); } else if ((key == 'passkeyLoggedIn') && (value is bool)) { result = result && (Auth2().isPasskeyLoggedIn == value); } - else if ((key == 'phoneOrEmailLoggedIn') && (value is bool)) { - result = result && ((Auth2().isPhoneLoggedIn || Auth2().isEmailLoggedIn) == value) ; - } else if ((key == 'shibbolethLinked') && (value is bool)) { result = result && (Auth2().isOidcLinked == value); } diff --git a/lib/service/graph_ql.dart b/lib/service/graph_ql.dart new file mode 100644 index 000000000..9c39be662 --- /dev/null +++ b/lib/service/graph_ql.dart @@ -0,0 +1,53 @@ +/* + * 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(); + } + + ValueNotifier getClient(String url, {Map> possibleTypes = const {}}) { + ValueNotifier client = ValueNotifier(GraphQLClient( + link: HttpLink(url, defaultHeaders: {}), + defaultPolicies: DefaultPolicies(query: Policies(error: ErrorPolicy.all)), + cache: GraphQLCache(store: HiveStore(), possibleTypes: possibleTypes, partialDataPolicy: PartialDataCachePolicy.accept), + )); + return client; + } +} diff --git a/lib/service/groups.dart b/lib/service/groups.dart index 524215e5a..4247afec3 100644 --- a/lib/service/groups.dart +++ b/lib/service/groups.dart @@ -446,7 +446,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); @@ -635,7 +635,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/onboarding.dart b/lib/service/onboarding.dart index 2df7e1c86..bf054c9bb 100644 --- a/lib/service/onboarding.dart +++ b/lib/service/onboarding.dart @@ -99,12 +99,16 @@ class Onboarding with Service implements NotificationsListener { nextPanel(panel).then((dynamic nextPanel) { 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) { 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/storage.dart b/lib/service/storage.dart index 02945f333..0f0ee862a 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -18,10 +18,8 @@ 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'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; -// import 'package:rokwire_plugin/utils/crypt.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -120,6 +118,19 @@ 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) async { + if (value != null) { + await _secureStorage?.write(key: name, value: value); + } else { + await _secureStorage?.delete(key: name); + } + NotificationService().notify(notifySettingChanged, name); + } + // String? getEncryptedStringWithName(String name, {String? defaultValue}) { // String? value = _sharedPreferences?.getString(name); // if (value != null) { @@ -261,24 +272,24 @@ class Storage with Service { Future setAuth2AnonymousId(String? value) async => setStringWithName(auth2AnonymousIdKey, value); String get auth2AnonymousTokenKey => 'edu.illinois.rokwire.auth2.anonymous.token'; - Future getAuth2AnonymousToken() async => Auth2Token.fromJson(JsonUtils.decodeMap(await _secureStorage?.read(key: auth2AnonymousTokenKey))); - Future setAuth2AnonymousToken(Auth2Token? value) async => await _secureStorage?.write(key: auth2AnonymousTokenKey, value: 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'; - Future getAuth2AnonymousPrefs() async => Auth2UserPrefs.fromJson(JsonUtils.decodeMap(await _secureStorage?.read(key: auth2AnonymousPrefsKey))); - Future setAuth2AnonymousPrefs(Auth2UserPrefs? value) async => await _secureStorage?.write(key: auth2AnonymousPrefsKey, value: 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'; - Future getAuth2AnonymousProfile() async => Auth2UserProfile.fromJson(JsonUtils.decodeMap(await _secureStorage?.read(key: auth2AnonymousProfileKey))); - Future setAuth2AnonymousProfile(Auth2UserProfile? value) async => await _secureStorage?.write(key: auth2AnonymousProfileKey, value: 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'; - Future getAuth2Token() async => Auth2Token.fromJson(JsonUtils.decodeMap(await _secureStorage?.read(key: auth2TokenKey))); - Future setAuth2Token(Auth2Token? value) async => await _secureStorage?.write(key: auth2TokenKey, value: 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 auth2AccountKey => 'edu.illinois.rokwire.auth2.account'; - Future getAuth2Account() async => Auth2Account.fromJson(JsonUtils.decodeMap(await _secureStorage?.read(key: auth2AccountKey))); - Future setAuth2Account(Auth2Account? value) async => await _secureStorage?.write(key: auth2AccountKey, value: 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'; diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 0d7ed6212..3e13642b3 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -873,8 +873,9 @@ abstract class ImageSpec { fontFamily ??= imageSpec.fontFamily; fontPackage ??= imageSpec.fontPackage; - imageSpec = FontAwesomeImageSpec(type: type, source: source, size: size, color: color, - semanticLabel: semanticLabel, weight: weight, textDirection: textDirection); + 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; } diff --git a/lib/ui/panels/web_panel.dart b/lib/ui/panels/web_panel.dart index a6bfb06f0..27471c2d9 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -139,7 +139,7 @@ class WebPanel extends StatefulWidget { } class WebPanelState extends State implements NotificationsListener { - + late flutter_webview.WebViewController _controller; bool? _isOnline; bool? _isTrackingEnabled; bool _isPageLoading = true; @@ -152,9 +152,39 @@ class WebPanelState extends State implements NotificationsListener { 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; @@ -203,15 +233,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(), diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index 96b4b150e..0f51b3926 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -269,7 +269,7 @@ class ActionsMessage extends StatelessWidget { @protected TextStyle get defaultTitleTextStyle => TextStyle(fontFamily: displayTitleFontFamily, fontSize: titleFontSize, color: displayTitleTextColor); @protected TextStyle get displayTitleTextStyle => titleTextStyle ?? defaultTitleTextStyle; - @protected Color? get defaultMessageTextColor => AppColors.fillColorPrimary; + @protected Color? get defaultMessageTextColor => AppColors.textPrimary; @protected Color? get displayMessageTextColor => messageTextColor ?? defaultMessageTextColor; @protected String? get defaultMessageFontFamily => AppFontFamilies.bold; diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index 8f3f49645..e91f4f95f 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -115,7 +115,10 @@ class RibbonButton extends StatefulWidget { @protected String? get defaultDescriptionFontFamily => Styles().fontFamilies?.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; diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index f8671c815..874e5a2fd 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -18,6 +18,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; @@ -169,6 +170,23 @@ class StringUtils { 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 = Random.secure().nextInt(chars.length); + return chars [indexRandom]; + }).join(''); + } } class CollectionUtils { diff --git a/pubspec.yaml b/pubspec.yaml index dfc96b3bc..015452c8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: flutter_native_timezone: ^2.0.0 geolocator: ^9.0.2 cookie_jar: ^3.0.1 - shared_preferences: ^2.0.17 + shared_preferences: ^2.2.0 package_info_plus: ^4.0.2 device_info: ^2.0.3 url_launcher: ^6.1.8 @@ -35,18 +35,21 @@ dependencies: flutter_local_notifications: ^12.0.4 sqflite: ^2.1.0 device_calendar: ^4.3.1 - image_picker: ^0.8.6+1 + image_picker: ^1.0.4 mime_type: ^1.0.0 uuid: ^3.0.7 flutter_html: ^3.0.0-beta.2 - webview_flutter: ^3.0.4 + webview_flutter: ^4.0.5 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.3.0 flutter_secure_storage: ^8.0.0 universal_html: ^2.2.3 flutter_web_auth_2: ^2.1.4 pinch_zoom: ^1.0.0 - flutter_passkey: ^1.0.3 + graphql_flutter: ^5.1.2 + + flutter_passkey: + git: https://github.com/rokmetro/flutter_passkey.git #Firebase firebase_core: ^2.13.0 diff --git a/tools/encrypt_util.dart b/tools/encrypt_util.dart index c7a3e3f56..483ede9b2 100644 --- a/tools/encrypt_util.dart +++ b/tools/encrypt_util.dart @@ -17,6 +17,8 @@ void main() async { // 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)); From 88de2d962405db0c083e80ad2e9ab80afd5f97bf Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Wed, 11 Oct 2023 14:09:02 -0500 Subject: [PATCH 073/177] expose convenience functions for config environment --- lib/service/config.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/service/config.dart b/lib/service/config.dart index 9726fd5b4..73f5b5bc7 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -504,6 +504,10 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { } } + bool get isProduction => _configEnvironment == ConfigEnvironment.production; + bool get isTest => _configEnvironment == ConfigEnvironment.test; + bool get isDev => _configEnvironment == ConfigEnvironment.dev; + ConfigEnvironment? get configEnvironment { return _configEnvironment; } From 2eeb77ac8aa39c92fbebba0e960938a4ebbb21cd Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 12 Oct 2023 12:07:10 -0500 Subject: [PATCH 074/177] fix style gen bugs --- tools/gen_styles.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index e2832d12d..37f8e9a1e 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -45,7 +45,7 @@ Map textStyleFields = { 'decoration': 'decoration:TextDecoration', 'overflow': 'overflow:TextOverflow', 'decoration_style': 'decorationStyle:TextDecorationStyle', - 'font_weight': 'fontWeight:FontWeight', + 'weight': 'fontWeight:FontWeight', }; String capitalize(String s) => s[0].toUpperCase() + s.substring(1); @@ -232,7 +232,7 @@ String? _buildDefaultClass(String name, MapEntry entry, {Map Date: Thu, 12 Oct 2023 16:48:17 -0500 Subject: [PATCH 075/177] add theme support to styles --- assets/styles.json | 3 ++ lib/gen/styles.dart | 4 ++ lib/service/storage.dart | 5 +++ lib/service/styles.dart | 83 ++++++++++++++++++++++++++++++++-------- lib/utils/utils.dart | 30 +++++++++++++++ tools/gen_styles.dart | 19 +++++++-- 6 files changed, 124 insertions(+), 20 deletions(-) diff --git a/assets/styles.json b/assets/styles.json index 67586d0ae..479a6318d 100644 --- a/assets/styles.json +++ b/assets/styles.json @@ -1,4 +1,7 @@ { + "themes": { + "system": {} + }, "color": { "fillColorPrimary" :"#002855", "fillColorPrimaryVariant" :"#0F2040", diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index 7ced8dbb4..ae6728a3f 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -4,6 +4,10 @@ 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 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); diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 0f0ee862a..8f24dca80 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -313,6 +313,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 3e13642b3..dbdbcc891 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -20,6 +20,7 @@ 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_lifecycle.dart'; @@ -27,6 +28,7 @@ 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'; @@ -51,19 +53,16 @@ class Styles extends Service implements NotificationsListener{ Map? _netAssetsStyles; Map? _debugAssetsStyles; - UiColors? _colors; - UiColors? get colors => _colors; + Map _themes = {}; + List get themes => _themes.keys.toList(); - UiFontFamilies? _fontFamilies; - UiFontFamilies? get fontFamilies => _fontFamilies; + UiTheme? _theme; + UiColors? get colors => _theme?.colors; + UiFontFamilies? get fontFamilies => _theme?.fontFamilies; + UiTextStyles? get textStyles => _theme?.textStyles; + UiImages? get images => _theme?.images; - UiTextStyles? _textStyles; - UiTextStyles? get textStyles => _textStyles; - - UiImages? _images; - UiImages? get images => _images; - - // Singletone Factory + // Singleton Factory static Styles? _instance; @@ -116,7 +115,7 @@ class Styles extends Service implements NotificationsListener{ @override Set get serviceDependsOn { - return { Config() }; + return { Config(), Storage() }; } // NotificationsListener @@ -233,10 +232,45 @@ class Styles extends Service implements NotificationsListener{ @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']) ?? {}; + UiTheme defaultTheme = await UiTheme.fromJson(styles, resolveImageAssetPath); + _themes['default'] = defaultTheme; + for (MapEntry> theme in themes.entries) { + if (theme.value.isNotEmpty) { + Map? themeData = MapUtils.mergeToNew(styles, theme.value, level: 1); + _themes[theme.key] = await UiTheme.fromJson(themeData, resolveImageAssetPath); + } else { + _themes[theme.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); + } + + String? _getSelectedTheme() { + String? selectedTheme = Storage().selectedTheme; + if (selectedTheme == 'system') { + var brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; + if (brightness == Brightness.light) { + selectedTheme = 'light'; + } else if (brightness == Brightness.dark) { + selectedTheme = 'dark'; + } + } + return selectedTheme; } Map get contentMap { @@ -286,6 +320,23 @@ 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 { + UiColors? colors = await compute(UiColors.fromJson, JsonUtils.mapValue(styles['color'])); + UiFontFamilies? fontFamilies = await compute(UiFontFamilies.fromJson, JsonUtils.mapValue(styles['font_family'])); + UiTextStyles? textStyles = await compute(UiTextStyles.fromCreationParam, _UiTextStylesCreationParam(JsonUtils.mapValue(styles['text_style']), colors: colors, fontFamilies: fontFamilies)); + UiImages? images = await compute(UiImages.fromCreationParam, _UiImagesCreationParam(JsonUtils.mapValue(styles['image']), colors: colors, assetPathResolver: assetPathResolver)); + return UiTheme(colors: colors, fontFamilies: fontFamilies, textStyles: textStyles, images: images); + } +} + class UiColors { final Map colorMap; diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 874e5a2fd..283fa416d 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -372,6 +372,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]; @@ -890,6 +897,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) { diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index 37f8e9a1e..21c587e45 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -10,6 +10,7 @@ Map classMap = { 'text_style': 'AppTextStyles', 'font_family': 'AppFontFamilies', 'image': 'AppImages', + 'themes': 'AppThemes', }; Map typesMap = { @@ -17,6 +18,7 @@ Map typesMap = { 'text_style': 'TextStyle', 'font_family': 'String', 'image': 'UiImage', + 'themes': 'String', }; Map refsMap = { @@ -24,6 +26,7 @@ Map refsMap = { 'text_style': 'Styles().textStyles?.getTextStyle(%key)', 'font_family': 'Styles().fontFamilies?.fromCode(%key)', 'image': 'Styles().images?.getImage(%key)', + 'themes': '%key', }; Map)> defaultFuncs = { @@ -104,7 +107,7 @@ void main(List arguments) async { } } -String _prettyJsonEncode(Map jsonObject){ +String _prettyJsonEncode(Map jsonObject, {bool deepFormat = false}){ String out = '{'; bool first = true; for (MapEntry entry in jsonObject.entries) { @@ -119,11 +122,19 @@ String _prettyJsonEncode(Map jsonObject){ if (!firstSub) { out += ','; } - out += '\n'; Map subMap = {}; subMap.addEntries([subentry]); - String valJson = json.encode(subMap).replaceAll(':', ': '); - out += ' ${valJson.substring(1, valJson.length - 1)}'; + 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 { From a9689ff78910fe84e453a8862c07820a21a94717 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 13 Oct 2023 18:40:23 -0500 Subject: [PATCH 076/177] handle oidc provider token --- lib/service/auth2.dart | 10 +++++++++- lib/service/graph_ql.dart | 4 ++-- lib/service/storage.dart | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 8a6e366d1..6180084d1 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -68,6 +68,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2Token? _token; Auth2Account? _account; + Auth2Token? _oidcToken; + String? _anonymousId; Auth2Token? _anonymousToken; Auth2UserPrefs? _anonymousPrefs; @@ -113,6 +115,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future initService() async { _token = await Storage().getAuth2Token(); _account = await Storage().getAuth2Account(); + _oidcToken = await Storage().getAuth2OidcToken(); _anonymousId = Storage().auth2AnonymousId; _anonymousToken = await Storage().getAuth2AnonymousToken(); @@ -250,7 +253,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2Token? get anonymousToken => _anonymousToken; Auth2Account? get account => _account; String? get deviceId => _deviceId; - + + Auth2Token? get oidcToken => _oidcToken; + bool get associateAnonymousIds => false; String? get accountId => _account?.id ?? _anonymousId; Auth2UserPrefs? get prefs => _account?.prefs ?? _anonymousPrefs; @@ -761,6 +766,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Future applyLogin(Auth2Account account, Auth2Token token, { Auth2AccountScope? scope, Map? params }) async { + Auth2Token? oidcToken = (params != null) ? Auth2Token.fromJson(JsonUtils.mapValue(params['oidc_token'])) : null; _refreshTokenFailCounts.remove(_token?.refreshToken); @@ -770,11 +776,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { bool? prefsUpdated = account.prefs?.apply(_anonymousPrefs, scope: scope?.prefs); bool? profileUpdated = account.profile?.apply(_anonymousProfile, scope: scope?.profile); _token = token; + _oidcToken = oidcToken; _account = account; await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = null); await Storage().setAuth2AnonymousProfile(_anonymousProfile = null); if (!kIsWeb) { await Storage().setAuth2Token(token); + await Storage().setAuth2OidcToken(oidcToken); await Storage().setAuth2Account(account); } diff --git a/lib/service/graph_ql.dart b/lib/service/graph_ql.dart index 9c39be662..47648a314 100644 --- a/lib/service/graph_ql.dart +++ b/lib/service/graph_ql.dart @@ -42,9 +42,9 @@ class GraphQL with Service { await super.initService(); } - ValueNotifier getClient(String url, {Map> possibleTypes = const {}}) { + ValueNotifier getClient(String url, {Map> possibleTypes = const {}, Map defaultHeaders = const {}}) { ValueNotifier client = ValueNotifier(GraphQLClient( - link: HttpLink(url, defaultHeaders: {}), + link: HttpLink(url, defaultHeaders: defaultHeaders), defaultPolicies: DefaultPolicies(query: Policies(error: ErrorPolicy.all)), cache: GraphQLCache(store: HiveStore(), possibleTypes: possibleTypes, partialDataPolicy: PartialDataCachePolicy.accept), )); diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 8f24dca80..c0a57b7b5 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -287,6 +287,10 @@ class Storage with Service { 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'; Future getAuth2Account() async => Auth2Account.fromJson(JsonUtils.decodeMap(await getSecureStringWithName(auth2AccountKey))); Future setAuth2Account(Auth2Account? value) async => await setSecureStringWithName(auth2AccountKey, JsonUtils.encode(value?.toJson())); From fe5090bf6a001728ade2f0c02338b8fa6bec40d3 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Sat, 14 Oct 2023 01:05:41 -0500 Subject: [PATCH 077/177] handle text style extensions in style generation --- lib/service/styles.dart | 6 +++--- tools/gen_styles.dart | 35 ++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/service/styles.dart b/lib/service/styles.dart index dbdbcc891..8e430320a 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -581,11 +581,11 @@ class UiTextStyles { //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; } diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index 21c587e45..925fc1013 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -29,9 +29,9 @@ Map refsMap = { 'themes': '%key', }; -Map)> defaultFuncs = { +Map, {Map? data})> defaultFuncs = { 'color': _buildDefaultColor, - 'text_style': (name, json) => _buildDefaultClass(name, json, classFields: textStyleFields), + 'text_style': (name, entry, {data}) => _buildDefaultClass(name, entry, data: data, classFields: textStyleFields), 'font_family': _buildDefaultString, 'image': _buildDefaultImage, }; @@ -201,7 +201,7 @@ String? _buildClass(String name, Map json) { for (MapEntry entry in json.entries) { String varName = camelCase(entry.key); String varRef = ref.replaceAll("%key", "'${entry.key}'"); - String? defaultObj = defaultFuncs[name]?.call(name, entry); + 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'; @@ -210,7 +210,7 @@ String? _buildClass(String name, Map json) { return classString; } -String? _buildDefaultClass(String name, MapEntry entry, {Map? classFields}) { +String? _buildDefaultClass(String name, MapEntry entry, {Map? classFields, Map? data}) { String? type = typesMap[name]; if (type == null) { return null; @@ -218,16 +218,23 @@ String? _buildDefaultClass(String name, MapEntry entry, {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) { - if (params.isNotEmpty) { - params += ', '; - } - 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]; @@ -253,7 +260,7 @@ String? _buildDefaultClass(String name, MapEntry entry, {Map entry) { +String? _buildDefaultColor(String name, MapEntry entry, {Map? data}) { dynamic value = entry.value; if (value is String ) { value = value.replaceFirst('#', ''); @@ -265,14 +272,14 @@ String? _buildDefaultColor(String name, MapEntry entry) { return null; } -String? _buildDefaultString(String name, MapEntry entry) { +String? _buildDefaultString(String name, MapEntry entry, {Map? data}) { if (entry.value is String) { return "'${entry.value}'"; } return null; } -String? _buildDefaultImage(String name, MapEntry entry) { +String? _buildDefaultImage(String name, MapEntry entry, {Map? data}) { return 'UiImage(spec: ImageSpec.fromJson(${json.encode(entry.value)}))'; } @@ -310,4 +317,10 @@ void _updateCodeRefs(String libPath, String genFilepath) async { entity.writeAsStringSync(data); } } +} + +void _mergeMaps(Map dest, Map? src) { + src?.forEach((String key, dynamic val) { + dest[key] = val; + }); } \ No newline at end of file From f17ca0d81892f66ff0f4643c210bd7c677a00661 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Sat, 14 Oct 2023 01:22:09 -0500 Subject: [PATCH 078/177] update system theme on mode change --- assets/styles.json | 2 ++ lib/gen/styles.dart | 2 ++ lib/service/styles.dart | 17 ++++++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/assets/styles.json b/assets/styles.json index 479a6318d..ea60ee416 100644 --- a/assets/styles.json +++ b/assets/styles.json @@ -1,5 +1,7 @@ { "themes": { + "light": {}, + "dark": {}, "system": {} }, "color": { diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index ae6728a3f..b10bc92f6 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -5,6 +5,8 @@ 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'; } diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 8e430320a..4f7baf952 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -23,6 +23,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.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'; @@ -260,14 +261,24 @@ class Styles extends Service implements NotificationsListener{ NotificationService().notify(notifyChanged, null); } + Future updateSystemTheme() async { + if (Storage().selectedTheme == AppThemes.system) { + Styles().applyTheme(AppThemes.system); + } + } + String? _getSelectedTheme() { String? selectedTheme = Storage().selectedTheme; - if (selectedTheme == 'system') { + if (selectedTheme == null) { + Storage().selectedTheme = AppThemes.system; + } + + if (selectedTheme == AppThemes.system) { var brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; if (brightness == Brightness.light) { - selectedTheme = 'light'; + selectedTheme = AppThemes.light; } else if (brightness == Brightness.dark) { - selectedTheme = 'dark'; + selectedTheme = AppThemes.dark; } } return selectedTheme; From e6d4d582349ffc41c632d5c527bacdd499253669 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander <46940735+roberlander2@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:02:39 -0500 Subject: [PATCH 079/177] [#96] Improve exception handling for passkeys (#397) * add cancelled and no creds values to passkey sign up status * improve exception parsing on passkey sign up * update changelog --- CHANGELOG.md | 1 + lib/service/auth2.dart | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 405080bec..04fb41a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Only return null on unsuccessful survey responses request [#349](https://github.com/rokwire/app-flutter-plugin/issues/349) - Local notifications repeating weekly [#365](https://github.com/rokwire/app-flutter-plugin/issues/365) - Groups load upcoming events [#3645](https://github.com/rokwire/illinois-app/issues/3645). +- Improve exception handling for passkeys [#396](https://github.com/rokwire/app-flutter-plugin/issues/396) ## [1.4.0] - 2023-05-12 ### Fixed diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 6180084d1..29246a76d 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -592,9 +592,23 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Auth2PasskeySignUpResult(Auth2PasskeySignUpResultStatus.succeeded); } } catch(error) { - errorMessage = error.toString(); - Log.e(errorMessage); + 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)); @@ -1929,6 +1943,8 @@ enum Auth2PasskeySignUpResultStatus { failedNotFound, failedActivationExpired, failedNotActivated, + failedNoCredentials, + failedCancelled, } // Auth2PasskeySignInResult From 57451bc6543971135b8d2142911a43f3c2098844 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Wed, 18 Oct 2023 17:25:51 +0300 Subject: [PATCH 080/177] Added StringUtils.invokeIfExists helper function [rokmetro/vogue-app#49]. --- lib/utils/utils.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 283fa416d..262dc4968 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -187,6 +187,8 @@ class StringUtils { return chars [indexRandom]; }).join(''); } + + static T? invokeIfExists(String? val, T Function(String val) inv) => (val != null) ? inv(val) : null; } class CollectionUtils { From 331b6a774c8a13d441fdfb40e1ede8ebf2cebd37 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 19 Oct 2023 12:30:50 -0500 Subject: [PATCH 081/177] fix action message background color --- lib/ui/popups/popup_message.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index 0f51b3926..4f756ca3f 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -210,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; @@ -238,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, @@ -257,6 +261,9 @@ class ActionsMessage extends StatelessWidget { this.borderRadius, }) : super(key: key); + @protected Color? get defaultBackgroundColor => AppColors.surface; + @protected Color? get displayBackgroundColor => backgroundColor ?? defaultBackgroundColor; + @protected Color? get defaultTitleBarColor => AppColors.fillColorPrimary; @protected Color? get displayTitleBarColor => titleBarColor ?? defaultTitleBarColor; @@ -290,6 +297,7 @@ class ActionsMessage extends StatelessWidget { static Future show({ String? title, TextStyle? titleTextStyle, + Color? backgroundColor, Color? titleTextColor, String? titleFontFamily, double titleFontSize = 20, @@ -328,6 +336,8 @@ class ActionsMessage extends StatelessWidget { titlePadding: titlePadding, titleBarColor: titleBarColor, closeButtonIcon: closeButtonIcon, + + backgroundColor: backgroundColor, message: message, messageTextStyle: messageTextStyle, @@ -353,7 +363,9 @@ class ActionsMessage extends StatelessWidget { 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, From 09c689a6d5dbe30818218a4b9cf382650d4a8064 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 19 Oct 2023 14:34:01 -0500 Subject: [PATCH 082/177] standardize styling --- lib/ui/widgets/tab_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index a6bb53a06..35c8b75ff 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -206,7 +206,7 @@ class TabWidget extends StatelessWidget { 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, - color: selected ? AppColors.fillColorSecondary : AppColors.textMedium) ?? defaultIcon : defaultIcon; + color: selected ? AppColors.fillColorSecondary : AppColors.textDisabled) ?? defaultIcon : defaultIcon; } @protected From 932bebe9b9ea8812ad67581f5edbd4b8d7b798f6 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 24 Oct 2023 17:09:40 -0500 Subject: [PATCH 083/177] progress on bookmark service integration --- lib/service/graph_ql.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/service/graph_ql.dart b/lib/service/graph_ql.dart index 47648a314..6364368da 100644 --- a/lib/service/graph_ql.dart +++ b/lib/service/graph_ql.dart @@ -42,12 +42,16 @@ class GraphQL with Service { await super.initService(); } - ValueNotifier getClient(String url, {Map> possibleTypes = const {}, Map defaultHeaders = const {}}) { - ValueNotifier client = ValueNotifier(GraphQLClient( + GraphQLClient getClient(String url, {Map> possibleTypes = const {}, Map defaultHeaders = const {}}) { + GraphQLClient client = GraphQLClient( link: HttpLink(url, defaultHeaders: defaultHeaders), defaultPolicies: DefaultPolicies(query: Policies(error: ErrorPolicy.all)), cache: GraphQLCache(store: HiveStore(), possibleTypes: possibleTypes, partialDataPolicy: PartialDataCachePolicy.accept), - )); + ); return client; } + + ValueNotifier getNotifier(GraphQLClient client) { + return ValueNotifier(client); + } } From c028262b024927c38118f732b7a7ac8714610c43 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Sat, 4 Nov 2023 00:44:28 -0500 Subject: [PATCH 084/177] Release v1.21.0, resolves #103, resolves #110, resolves #108, resolves #129 --- example/pubspec.lock | 4 ++-- lib/ui/panels/modal_image_panel.dart | 1 - pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 7bc2b5405..8a3a1f9f5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -877,10 +877,10 @@ packages: 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: diff --git a/lib/ui/panels/modal_image_panel.dart b/lib/ui/panels/modal_image_panel.dart index e5bcbdb77..4a0a0213c 100644 --- a/lib/ui/panels/modal_image_panel.dart +++ b/lib/ui/panels/modal_image_panel.dart @@ -137,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/pubspec.yaml b/pubspec.yaml index 015452c8c..75e5f9dea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ dependencies: flutter_secure_storage: ^8.0.0 universal_html: ^2.2.3 flutter_web_auth_2: ^2.1.4 - pinch_zoom: ^1.0.0 + pinch_zoom: ^2.0.0 graphql_flutter: ^5.1.2 flutter_passkey: From 6c253849eaa29c34b711d0bff25eedad9e53e85b Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Tue, 7 Nov 2023 14:01:27 +0200 Subject: [PATCH 085/177] Added Connectivity.checkOnline API [rokmetro/vogue-app/#139]. --- lib/service/connectivity.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/service/connectivity.dart b/lib/service/connectivity.dart index cd86b5e85..0cabf66e9 100644 --- a/lib/service/connectivity.dart +++ b/lib/service/connectivity.dart @@ -99,6 +99,12 @@ class Connectivity with Service { // Connectivity + Future checkStatus() async { + ConnectivityStatus? connectivityStatus = _statusFromResult(await connectivity.Connectivity().checkConnectivity()); + _setConnectivityStatus(connectivityStatus); + return connectivityStatus; + } + ConnectivityStatus? get status { return _connectivityStatus; } From 6306bb62e68a999d583bb45299ec74cd9357b1ff Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 10 Nov 2023 14:30:35 -0600 Subject: [PATCH 086/177] apply system proxy settings by default --- example/pubspec.lock | 8 ++++++++ lib/service/http_proxy.dart | 19 +++++++++++++++++++ pubspec.yaml | 1 + 3 files changed, 28 insertions(+) diff --git a/example/pubspec.lock b/example/pubspec.lock index 8a3a1f9f5..59da57a84 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -777,6 +777,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: diff --git a/lib/service/http_proxy.dart b/lib/service/http_proxy.dart index 2e1191632..89e3face8 100644 --- a/lib/service/http_proxy.dart +++ b/lib/service/http_proxy.dart @@ -17,6 +17,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:native_flutter_proxy/native_proxy_reader.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; import 'package:rokwire_plugin/service/storage.dart'; @@ -51,6 +52,7 @@ class HttpProxy extends Service implements NotificationsListener { @override Future initService() async { _handleChanged(); + await applySystemProxy(); await super.initService(); } @@ -72,6 +74,23 @@ class HttpProxy extends Service implements NotificationsListener { } } + Future applySystemProxy() async { + 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; + } + bool? get httpProxyEnabled{ return Storage().httpProxyEnabled; diff --git a/pubspec.yaml b/pubspec.yaml index 75e5f9dea..06d1d9ce4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: flutter_web_auth_2: ^2.1.4 pinch_zoom: ^2.0.0 graphql_flutter: ^5.1.2 + native_flutter_proxy: ^0.1.15 flutter_passkey: git: https://github.com/rokmetro/flutter_passkey.git From 5206e90c1660c678c73ad59c055aa6fc02adb01e Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Sat, 11 Nov 2023 16:37:13 -0600 Subject: [PATCH 087/177] add font style support --- lib/service/styles.dart | 18 ++++++++++++++++-- tools/gen_styles.dart | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 4f7baf952..41b9053f8 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -578,6 +578,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"])); @@ -587,8 +588,13 @@ 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']); @@ -1146,6 +1152,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/tools/gen_styles.dart b/tools/gen_styles.dart index 925fc1013..72b297c39 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -42,6 +42,7 @@ Map textStyleFields = { 'size': 'fontSize', 'height': 'height', 'font_family': 'fontFamily', + 'font_style': 'fontStyle:FontStyle', 'letter_spacing': 'letterSpacing', 'word_spacing': 'wordSpacing', 'decoration_thickness': 'decorationThickness', From 68afa35f25d8eed1eabf8abe6f72d9a642ce6a02 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 16 Nov 2023 12:38:00 -0600 Subject: [PATCH 088/177] support setting default fetch policy for GraphQL client --- lib/service/graph_ql.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/service/graph_ql.dart b/lib/service/graph_ql.dart index 6364368da..0c22f2e46 100644 --- a/lib/service/graph_ql.dart +++ b/lib/service/graph_ql.dart @@ -42,10 +42,15 @@ class GraphQL with Service { await super.initService(); } - GraphQLClient getClient(String url, {Map> possibleTypes = const {}, Map defaultHeaders = const {}}) { + GraphQLClient getClient(String url, {Map> possibleTypes = const {}, + Map defaultHeaders = const {}, + FetchPolicy? defaultFetchPolicy}) { GraphQLClient client = GraphQLClient( link: HttpLink(url, defaultHeaders: defaultHeaders), - defaultPolicies: DefaultPolicies(query: Policies(error: ErrorPolicy.all)), + defaultPolicies: DefaultPolicies(query: Policies( + fetch: defaultFetchPolicy, + error: ErrorPolicy.all + )), cache: GraphQLCache(store: HiveStore(), possibleTypes: possibleTypes, partialDataPolicy: PartialDataCachePolicy.accept), ); return client; From 46fd4765747b7a5b44250e66073ebfef31271df7 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 17 Nov 2023 17:19:06 -0600 Subject: [PATCH 089/177] check access token expiration --- example/pubspec.lock | 34 +++++++++++++++++++++------------- lib/model/auth2.dart | 22 +++++++++++++++++++++- lib/service/auth2.dart | 16 ++++++++++++++++ lib/service/graph_ql.dart | 8 ++++++-- lib/service/network.dart | 15 +++++++++++++++ lib/utils/image_utils.dart | 7 +++++-- pubspec.yaml | 1 + 7 files changed, 85 insertions(+), 18 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 59da57a84..ba4d768d6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" connectivity_plus: dependency: transitive description: @@ -713,6 +713,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + jwt_decoder: + dependency: transitive + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -757,10 +765,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -1033,18 +1041,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -1073,10 +1081,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" timezone: dependency: transitive description: @@ -1217,10 +1225,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -1294,5 +1302,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.7.0" diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 9f4e51620..e64977bce 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:jwt_decoder/jwt_decoder.dart'; import 'package:rokwire_plugin/service/app_datetime.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -15,8 +17,17 @@ class Auth2Token { final String? accessToken; final String? refreshToken; final String? tokenType; + late final Map? _parsedAccessToken; - Auth2Token({this.accessToken, this.refreshToken, this.idToken, this.tokenType}); + Auth2Token({this.accessToken, this.refreshToken, this.idToken, this.tokenType}) { + if (accessToken != null) { + try { + _parsedAccessToken = JwtDecoder.decode(accessToken!); + } catch (e) { + debugPrint(e.toString()); + } + } + } static Auth2Token? fromOther(Auth2Token? value, {String? idToken, String? accessToken, String? refreshToken, String? tokenType }) { return (value != null) ? Auth2Token( @@ -45,6 +56,15 @@ class Auth2Token { }; } + bool? get accessIsExpired { + if (_parsedAccessToken == null) { + return null; + } + DateTime expirationDate = DateTime.fromMillisecondsSinceEpoch(0) + .add(Duration(seconds: _parsedAccessToken?['exp']?.toInt())); + return DateTime.now().isAfter(expirationDate); + } + @override bool operator ==(other) => (other is Auth2Token) && diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 29246a76d..2ac4e2368 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -247,6 +247,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return false; } + @override + Future refreshNetworkAuthTokenIfExpired(dynamic token) async { + if (token is Auth2Token && token.accessIsExpired == true) { + return (await Auth2().refreshToken(token: token) != null); + } + return false; + } + // Getters Auth2Token? get token => _token ?? _anonymousToken; Auth2Token? get userToken => _token; @@ -1924,6 +1932,14 @@ class Auth2Csrf with NetworkAuthProvider { } return false; } + + @override + Future refreshNetworkAuthTokenIfExpired(dynamic token) async { + if (token is Auth2Token && token.accessIsExpired == true) { + return (await Auth2().refreshToken(token: token) != null); + } + return false; + } } // Auth2PasskeySignUpResult diff --git a/lib/service/graph_ql.dart b/lib/service/graph_ql.dart index 0c22f2e46..3bb242fa8 100644 --- a/lib/service/graph_ql.dart +++ b/lib/service/graph_ql.dart @@ -43,10 +43,14 @@ class GraphQL with Service { } GraphQLClient getClient(String url, {Map> possibleTypes = const {}, - Map defaultHeaders = const {}, + Map defaultHeaders = const {}, AuthLink? authLink, FetchPolicy? defaultFetchPolicy}) { + Link link = HttpLink(url, defaultHeaders: defaultHeaders); + if (authLink != null) { + link = authLink.concat(link); + } GraphQLClient client = GraphQLClient( - link: HttpLink(url, defaultHeaders: defaultHeaders), + link: link, defaultPolicies: DefaultPolicies(query: Policies( fetch: defaultFetchPolicy, error: ErrorPolicy.all diff --git a/lib/service/network.dart b/lib/service/network.dart index e558778a6..335de83ca 100644 --- a/lib/service/network.dart +++ b/lib/service/network.dart @@ -32,6 +32,7 @@ abstract class NetworkAuthProvider { Map? get networkAuthHeaders; dynamic get networkAuthToken => null; Future refreshNetworkAuthTokenIfNeeded(http.BaseResponse? response, dynamic token) async => false; + Future refreshNetworkAuthTokenIfExpired(dynamic token) async => false; } class Network { @@ -145,6 +146,8 @@ class Network { try { dynamic token = auth?.networkAuthToken; + await auth?.refreshNetworkAuthTokenIfExpired(token); + response = await _get(url, headers: headers, body: body, encoding: encoding, auth: auth, client: client, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { @@ -187,6 +190,8 @@ class Network { try { dynamic token = auth?.networkAuthToken; + await auth?.refreshNetworkAuthTokenIfExpired(token); + response = await _post(url, body: body, encoding: encoding, headers: headers, client: client, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { @@ -229,6 +234,8 @@ class Network { try { dynamic token = auth?.networkAuthToken; + await auth?.refreshNetworkAuthTokenIfExpired(token); + response = await _put(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout, client: client); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { @@ -267,6 +274,8 @@ class Network { try { dynamic token = auth?.networkAuthToken; + await auth?.refreshNetworkAuthTokenIfExpired(token); + response = await _patch(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { @@ -304,6 +313,8 @@ class Network { http.Response? response; try { dynamic token = auth?.networkAuthToken; + await auth?.refreshNetworkAuthTokenIfExpired(token); + response = await _delete(url, body: body, encoding:encoding, headers: headers, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { @@ -371,6 +382,8 @@ class Network { try { dynamic token = auth?.networkAuthToken; + await auth?.refreshNetworkAuthTokenIfExpired(token); + 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) { @@ -439,6 +452,8 @@ class Network { try { dynamic token = auth?.networkAuthToken; + await auth?.refreshNetworkAuthTokenIfExpired(token); + response = await _head(url, headers: headers, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, token) == true) { diff --git a/lib/utils/image_utils.dart b/lib/utils/image_utils.dart index 9a56441db..4e5b75739 100644 --- a/lib/utils/image_utils.dart +++ b/lib/utils/image_utils.dart @@ -156,7 +156,7 @@ class ImageUtils { TextPainter textPainter = TextPainter( text: TextSpan(text: text, style: textStyle), textDirection: textDirection, - textScaleFactor: textScaleFactor, + textScaler: TextScaler.linear(textScaleFactor), maxLines: maxLines, )..layout(); if ((textPainter.width <= size.width) && (textPainter.height <= size.height)) { @@ -167,7 +167,10 @@ class ImageUtils { } } - ui.ParagraphStyle paragraphStyle = textStyle.getParagraphStyle(textScaleFactor: (0 < textScaleFactor) ? textScaleFactor : 1, 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/pubspec.yaml b/pubspec.yaml index 06d1d9ce4..a88c2808a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: pinch_zoom: ^2.0.0 graphql_flutter: ^5.1.2 native_flutter_proxy: ^0.1.15 + jwt_decoder: ^2.0.1 flutter_passkey: git: https://github.com/rokmetro/flutter_passkey.git From 0e68a9ed2e1f01355e557009d60e407ec47c4ad5 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 17 Nov 2023 21:27:46 -0600 Subject: [PATCH 090/177] fix ui images to handle failed styles init --- lib/service/styles.dart | 6 +++--- lib/ui/widgets/ui_image.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 41b9053f8..dec7a09b9 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -683,7 +683,7 @@ 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, @@ -756,7 +756,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; @@ -818,7 +818,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, diff --git a/lib/ui/widgets/ui_image.dart b/lib/ui/widgets/ui_image.dart index 82505e3e7..519c0b6b4 100644 --- a/lib/ui/widgets/ui_image.dart +++ b/lib/ui/widgets/ui_image.dart @@ -57,11 +57,11 @@ class UiImage extends StatelessWidget { if (imageSpec != null) { try { if (imageSpec is FlutterImageSpec) { - image = Styles().images?.getFlutterImage(imageSpec); + image = UiImages.getFlutterImage(imageSpec); } else if (imageSpec is FontAwesomeImageSpec) { - image = Styles().images?.getFaIcon(imageSpec); + image = UiImages.getFaIcon(imageSpec); } else if (imageSpec is MaterialIconImageSpec) { - image = Styles().images?.getMaterialIcon(imageSpec); + image = UiImages.getMaterialIcon(imageSpec); } } catch(e) { debugPrint(e.toString()); From fd5455fcdf3ff3e80518af1bc6e01418bc275868 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 17 Nov 2023 22:15:46 -0600 Subject: [PATCH 091/177] handle fallback service initialization --- lib/service/service.dart | 24 ++++++++++++++++++++++++ lib/service/styles.dart | 16 +++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 9270ae8eb..66dff17a9 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -31,6 +31,9 @@ abstract class Service { _isInitialized = true; } + Future initServiceFallback() async { + } + void initServiceUI() async { } @@ -85,6 +88,7 @@ class Services { if (service.isInitialized != true) { ServiceError? error = await initService(service); if (error?.severity == ServiceErrorSeverity.fatal) { + await initFallback(); return error; } } @@ -102,6 +106,15 @@ class Services { );*/ } + @protected + Future initFallback() async { + for (Service service in _services!) { + if (service.isInitialized != true) { + await initServiceFallback(service); + } + } + } + @protected Future initService(Service service) async { try { @@ -113,6 +126,17 @@ class Services { return null; } + + @protected + Future initServiceFallback(Service service) async { + try { + await service.initServiceFallback(); + } + on ServiceError catch (error) { + debugPrint(error.toString()); + } + } + void initUI() { if (_services != null) { for (Service service in _services!) { diff --git a/lib/service/styles.dart b/lib/service/styles.dart index dec7a09b9..c4a17ce79 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -114,6 +114,18 @@ class Styles extends Service implements NotificationsListener{ } } + @override + Future initServiceFallback() async { + + _assetsManifest = await loadAssetsManifest(); + _assetsStyles = await loadFromAssets(assetsKey); + _appAssetsStyles = kIsWeb ? null : await loadFromAssets(appAssetsKey); + + if ((_assetsStyles != null) || (_appAssetsStyles != null) || (_netAssetsStyles != null) || (_debugAssetsStyles != null)) { + await build(); + } + } + @override Set get serviceDependsOn { return { Config(), Storage() }; @@ -162,7 +174,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; } From 4d47dc9ce48b7196eff8c009145f40a9f2f9bc1d Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 17 Nov 2023 22:45:59 -0600 Subject: [PATCH 092/177] handle additional fallback service inits --- lib/service/auth2.dart | 45 ++++++++++++++++++++++++---------------- lib/service/flex_ui.dart | 23 +++++++++++++------- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 2ac4e2368..07322300a 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -113,24 +113,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @override Future initService() async { - _token = await Storage().getAuth2Token(); - _account = await Storage().getAuth2Account(); - _oidcToken = await Storage().getAuth2OidcToken(); - - _anonymousId = Storage().auth2AnonymousId; - _anonymousToken = await Storage().getAuth2AnonymousToken(); - _anonymousPrefs = await Storage().getAuth2AnonymousPrefs(); - _anonymousProfile = await Storage().getAuth2AnonymousProfile(); - - _deviceId = await RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2); - - if ((_account == null) && (_anonymousPrefs == null)) { - await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonymousPrefs); - } - - if ((_account == null) && (_anonymousProfile == null)) { - await Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonymousProfile); - } + await _initServiceOffline(); if ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { if (!await authenticateAnonymously()) { @@ -157,6 +140,32 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { await super.initService(); } + @override + Future initServiceFallback() async { + await _initServiceOffline(); + } + + Future _initServiceOffline() async { + _token = await Storage().getAuth2Token(); + _account = await Storage().getAuth2Account(); + _oidcToken = await Storage().getAuth2OidcToken(); + + _anonymousId = Storage().auth2AnonymousId; + _anonymousToken = await Storage().getAuth2AnonymousToken(); + _anonymousPrefs = await Storage().getAuth2AnonymousPrefs(); + _anonymousProfile = await Storage().getAuth2AnonymousProfile(); + + _deviceId = await RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2); + + if ((_account == null) && (_anonymousPrefs == null)) { + await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonymousPrefs); + } + + if ((_account == null) && (_anonymousProfile == null)) { + await Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonymousProfile); + } + } + @override Set get serviceDependsOn { return { Storage(), Config() }; diff --git a/lib/service/flex_ui.dart b/lib/service/flex_ui.dart index a74f7df02..55b4e8b6c 100644 --- a/lib/service/flex_ui.dart +++ b/lib/service/flex_ui.dart @@ -90,13 +90,7 @@ class FlexUI with Service implements NotificationsListener { @override Future initService() async { - _defContentSource = await loadFromAssets(assetsKey); - _appContentSource = kIsWeb ? null : await loadFromAssets(appAssetsKey); - if (!kIsWeb) { - _assetsDir = await getAssetsDir(); - _netContentSource = await loadFromCache(netCacheFileName); - } - build(); + await _initServiceOffline(); if (_defaultContent != null) { updateFromNet(); await super.initService(); @@ -111,6 +105,21 @@ class FlexUI with Service implements NotificationsListener { } } + @override + Future initServiceFallback() async { + await _initServiceOffline(); + } + + Future _initServiceOffline() async { + _defContentSource = await loadFromAssets(assetsKey); + _appContentSource = kIsWeb ? null : await loadFromAssets(appAssetsKey); + if (!kIsWeb) { + _assetsDir = await getAssetsDir(); + _netContentSource = await loadFromCache(netCacheFileName); + } + build(); + } + @override Set get serviceDependsOn { return { Config(), Auth2(), Groups(), GeoFence() }; From 1cf6695fc07e468d85926f341ec478f954269c25 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Sat, 18 Nov 2023 12:30:48 -0600 Subject: [PATCH 093/177] enable async notifications --- lib/service/notification_service.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/service/notification_service.dart b/lib/service/notification_service.dart index 15b2e2c0b..0497e8f28 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 = {}; @@ -108,5 +110,5 @@ class NotificationService { } abstract class NotificationsListener { - void onNotification(String name, dynamic param); + FutureOr onNotification(String name, dynamic param); } From 0b0679aa0fbe2abcbd69e0098ef659e9680e53cf Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 27 Nov 2023 15:31:06 -0600 Subject: [PATCH 094/177] add logout started notification --- lib/service/auth2.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 07322300a..e3a0ca2e7 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -27,6 +27,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 notifyLogoutStarted = "edu.illinois.rokwire.auth2.logout.started"; 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"; @@ -1412,6 +1413,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { // Logout Future logout({ Auth2UserPrefs? prefs }) async { + NotificationService().notify(notifyLogoutStarted); _log("Auth2: logout"); _refreshTokenFailCounts.remove(_token?.refreshToken); From 11cce946cc092c039d1ee0ced03f6667f6208cb5 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander <46940735+roberlander2@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:12:23 -0600 Subject: [PATCH 095/177] [#404] Use CCT for Android OIDC login (#405) * use LaunchMode.platformDefault in _launchUrl for all platforms * update changelog --- CHANGELOG.md | 1 + lib/service/auth2.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04fb41a86..9654d76ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Build event time filters in local timezone [#377](https://github.com/rokwire/app-flutter-plugin/issues/377). - Upgrade to connectivity_plus [#45](https://github.com/rokmetro/vogue-app/issues/45). - 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) ### Added - Survey creation tool [#263](https://github.com/rokwire/app-flutter-plugin/issues/263). - Web app authentication support [#291](https://github.com/rokwire/app-flutter-plugin/issues/291) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index e3a0ca2e7..4712479c0 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1834,7 +1834,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { onDeepLinkUri(Uri.tryParse(url)); }); } else if (await canLaunchUrlString(urlStr)) { - await launchUrlString(urlStr, mode: Platform.isAndroid ? LaunchMode.externalApplication : LaunchMode.platformDefault); + await launchUrlString(urlStr); } } } From 1ca6ab9c69a0359d4aaa18669e07c2340129fce5 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Thu, 30 Nov 2023 21:29:20 -0600 Subject: [PATCH 096/177] refactor services init to be more performant --- example/pubspec.lock | 16 ++--- lib/service/app_datetime.dart | 5 ++ lib/service/auth2.dart | 38 +++++++--- lib/service/service.dart | 130 +++++++++++++++++++++++++--------- lib/service/storage.dart | 5 +- pubspec.yaml | 2 +- 6 files changed, 143 insertions(+), 53 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index ba4d768d6..2dc0ad38c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -387,10 +387,10 @@ packages: dependency: transitive description: name: flutter_secure_storage - sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "9.0.0" flutter_secure_storage_linux: dependency: transitive description: @@ -427,10 +427,10 @@ packages: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -877,10 +877,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" petitparser: dependency: transitive description: @@ -1273,10 +1273,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.0.6" window_to_front: dependency: transitive description: diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index d52f41a2a..dacc4579f 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -65,6 +65,11 @@ class AppDateTime with Service { await super.initService(); } + @override + Set get serviceDependsOn { + return { Config(), Localization() }; + } + // Implementation DateTime get now { diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 4712479c0..02a66030d 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -147,23 +147,41 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } Future _initServiceOffline() async { - _token = await Storage().getAuth2Token(); - _account = await Storage().getAuth2Account(); - _oidcToken = await Storage().getAuth2OidcToken(); - _anonymousId = Storage().auth2AnonymousId; - _anonymousToken = await Storage().getAuth2AnonymousToken(); - _anonymousPrefs = await Storage().getAuth2AnonymousPrefs(); - _anonymousProfile = await Storage().getAuth2AnonymousProfile(); - _deviceId = await RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2); + List> futures = [ + Storage().getAuth2Token(), + Storage().getAuth2Account(), + Storage().getAuth2OidcToken(), + + Storage().getAuth2AnonymousToken(), + Storage().getAuth2AnonymousPrefs(), + Storage().getAuth2AnonymousProfile(), + + RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2), + ]; + + List results = await Future.wait(futures); + _token = results[0]; + _account = results[1]; + _oidcToken = results[2]; + _anonymousToken = results[3]; + _anonymousPrefs = results[4]; + _anonymousProfile = results[5]; + _deviceId = results[6]; + + futures.clear(); if ((_account == null) && (_anonymousPrefs == null)) { - await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonymousPrefs); + futures.add(Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonymousPrefs)); } if ((_account == null) && (_anonymousProfile == null)) { - await Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonymousProfile); + futures.add(Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonymousProfile)); + } + + if (futures.isNotEmpty) { + await Future.wait(futures); } } diff --git a/lib/service/service.dart b/lib/service/service.dart index 66dff17a9..20eb902cb 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -62,11 +62,13 @@ class Services { static set instance(Services? value) => _instance = value; List? _services; + Map?>? _serviceDependents; + Set? _initializedServices; void create(List services) { if (_services == null) { - _services = _sort(services); - for (Service service in _services!) { + _services = services; + for (Service service in services) { service.createService(); } } @@ -78,21 +80,25 @@ class Services { service.destroyService(); } _services = null; + _serviceDependents = null; + _initializedServices = null; } } Future init() async { - if (_services != null) { - for (Service service in _services!) { - if (service.isInitialized != true) { - ServiceError? error = await initService(service); - if (error?.severity == ServiceErrorSeverity.fatal) { - await initFallback(); - return error; - } - } + _initializedServices = {}; + _serviceDependents = _getServiceDependents(); + if (_serviceDependents?[null]?.isEmpty ?? true) { + return ServiceError( + source: null, + severity: ServiceErrorSeverity.fatal, + title: 'Services Initialization Error', + description: 'All services have dependencies. No services may be initialized safely.', + ); } + + return _init(null); } return null; @@ -106,6 +112,34 @@ class Services { );*/ } + Future _init(String? serviceName) async { + List> initErrorFutures = []; + for (Service service in _serviceDependents![serviceName] ?? {}) { + bool initialize = true; + for (Service dependency in service.serviceDependsOn ?? {}) { + if (_services!.contains(dependency) && !dependency.isInitialized) { + initialize = false; + break; + } + } + + if (initialize && !service.isInitialized && !_initializedServices!.contains(service.debugDisplayName)) { + _initializedServices!.add(service.debugDisplayName); + initErrorFutures.add(initService(service).then((ServiceError? error) async { + if (error?.severity == ServiceErrorSeverity.fatal) { + initFallback(); + return error; + } + + return await _init(service.debugDisplayName); + })); + } + } + + List initErrors = await Future.wait(initErrorFutures); + return initErrors.firstWhere((element) => element != null, orElse: () => null); + } + @protected Future initFallback() async { for (Service service in _services!) { @@ -118,7 +152,9 @@ class Services { @protected Future initService(Service service) async { try { + Stopwatch stopwatch = Stopwatch()..start(); await service.initService(); + debugPrint('${service.debugDisplayName} service in ${stopwatch.elapsed.inMilliseconds}ms'); } on ServiceError catch (error) { return error; @@ -153,32 +189,60 @@ class Services { } } - static List _sort(List inputServices) { - - List queue = []; - List services = List.from(inputServices); - while (services.isNotEmpty) { - // start with lowest priority service - Service svc = services.last; - services.removeLast(); - - // Move to TBD anyone from Queue that depends on svc - Set? svcDependents = svc.serviceDependsOn; - if (svcDependents != null) { - for (int index = queue.length - 1; index >= 0; index--) { - Service queuedSvc = queue[index]; - if (svcDependents.contains(queuedSvc)) { - queue.removeAt(index); - services.add(queuedSvc); - } + ServiceError? verifyInitialization() { + Set? serviceNames = _services != null ? Set.of(List.generate(_services!.length, (index) => _services![index].debugDisplayName)) : null; + Set remaining = serviceNames?.difference(_initializedServices ?? {}) ?? {}; + return remaining.isEmpty ? null : ServiceError( + source: null, + severity: ServiceErrorSeverity.fatal, + title: 'Services Initialization Error', + description: 'The following services were not initialized: $remaining', + ); + } + + Map?> _getServiceDependents() { + + Map?> serviceDependents = {}; + for (Service service in _services!) { + if (service.serviceDependsOn?.isEmpty ?? true) { + serviceDependents[null] ??= {}; + serviceDependents[null]!.add(service); + } else { + for (Service dependency in service.serviceDependsOn!) { + if (_services!.contains(dependency)) { + serviceDependents[dependency.debugDisplayName] ??= {}; + serviceDependents[dependency.debugDisplayName]!.add(service); + } } } - - // Move svc from TBD to Queue, mark it as processed - queue.add(svc); } + + return serviceDependents; + + // List queue = []; + // List services = List.from(inputServices); + // while (services.isNotEmpty) { + // // start with lowest priority service + // Service svc = services.last; + // services.removeLast(); + + // // Move to TBD anyone from Queue that depends on svc + // Set? svcDependents = svc.serviceDependsOn; + // if (svcDependents != null) { + // for (int index = queue.length - 1; index >= 0; index--) { + // Service queuedSvc = queue[index]; + // if (svcDependents.contains(queuedSvc)) { + // queue.removeAt(index); + // services.add(queuedSvc); + // } + // } + // } + + // // Move svc from TBD to Queue, mark it as processed + // queue.add(svc); + // } - return queue.reversed.toList(); + // return queue.reversed.toList(); } } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index c0a57b7b5..7fa8cba72 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -119,7 +119,10 @@ class Storage with Service { } Future getSecureStringWithName(String name, {String? defaultValue}) async { - return await _secureStorage?.read(key: name) ?? defaultValue; + Stopwatch stopwatch = Stopwatch()..start(); + String? hehe = await _secureStorage?.read(key: name) ?? defaultValue; + debugPrint('get $name in ${stopwatch.elapsed.inMilliseconds}ms'); + return hehe; } Future setSecureStringWithName(String name, String? value) async { diff --git a/pubspec.yaml b/pubspec.yaml index a88c2808a..2c5484942 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: webview_flutter: ^4.0.5 flutter_exif_rotation: ^0.5.1 font_awesome_flutter: ^10.3.0 - flutter_secure_storage: ^8.0.0 + flutter_secure_storage: ^9.0.0 universal_html: ^2.2.3 flutter_web_auth_2: ^2.1.4 pinch_zoom: ^2.0.0 From 7a0706e89b1b9009116e5f29734c273889e2facc Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Fri, 1 Dec 2023 17:04:38 -0600 Subject: [PATCH 097/177] remove stopwatches --- lib/service/service.dart | 2 -- lib/service/storage.dart | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 20eb902cb..f87554bde 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -152,9 +152,7 @@ class Services { @protected Future initService(Service service) async { try { - Stopwatch stopwatch = Stopwatch()..start(); await service.initService(); - debugPrint('${service.debugDisplayName} service in ${stopwatch.elapsed.inMilliseconds}ms'); } on ServiceError catch (error) { return error; diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 7fa8cba72..d920e4222 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -119,9 +119,7 @@ class Storage with Service { } Future getSecureStringWithName(String name, {String? defaultValue}) async { - Stopwatch stopwatch = Stopwatch()..start(); String? hehe = await _secureStorage?.read(key: name) ?? defaultValue; - debugPrint('get $name in ${stopwatch.elapsed.inMilliseconds}ms'); return hehe; } From ae55a74cbede2ee1cee6367fcd94715c30694d8c Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Mon, 4 Dec 2023 15:30:22 -0600 Subject: [PATCH 098/177] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9654d76ed..4dadee07b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgrade to connectivity_plus [#45](https://github.com/rokmetro/vogue-app/issues/45). - 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) + ### Added - Survey creation tool [#263](https://github.com/rokwire/app-flutter-plugin/issues/263). - Web app authentication support [#291](https://github.com/rokwire/app-flutter-plugin/issues/291) From dfe89a8ec7756cc35cee5ba6a4d30a19662ab0cd Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Tue, 5 Dec 2023 11:53:52 -0600 Subject: [PATCH 099/177] remove excess calls to serviceDependsOn, remove serviceDependsOn from app_datetime, remove storage testing leftovers, fix services init retry --- lib/service/app_datetime.dart | 5 -- lib/service/service.dart | 98 ++++++++++++----------------------- lib/service/storage.dart | 3 +- 3 files changed, 34 insertions(+), 72 deletions(-) diff --git a/lib/service/app_datetime.dart b/lib/service/app_datetime.dart index dacc4579f..d52f41a2a 100644 --- a/lib/service/app_datetime.dart +++ b/lib/service/app_datetime.dart @@ -65,11 +65,6 @@ class AppDateTime with Service { await super.initService(); } - @override - Set get serviceDependsOn { - return { Config(), Localization() }; - } - // Implementation DateTime get now { diff --git a/lib/service/service.dart b/lib/service/service.dart index f87554bde..8b2542c73 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -62,8 +62,9 @@ class Services { static set instance(Services? value) => _instance = value; List? _services; - Map?>? _serviceDependents; - Set? _initializedServices; + Map?>? _serviceDependents; // gives services that depend on the key service (looking "down" dependency tree) + Map?>? _serviceDependencies; // gives services are dependencies of the key service (looking "up" dependency tree) + Set? _attemptedServiceInits; // gives names of services for which initialization has been attempted void create(List services) { if (_services == null) { @@ -81,14 +82,14 @@ class Services { } _services = null; _serviceDependents = null; - _initializedServices = null; + _attemptedServiceInits = null; } } Future init() async { if (_services != null) { - _initializedServices = {}; - _serviceDependents = _getServiceDependents(); + _attemptedServiceInits ??= {}; + _setDependencyTree(); if (_serviceDependents?[null]?.isEmpty ?? true) { return ServiceError( source: null, @@ -112,26 +113,26 @@ class Services { );*/ } - Future _init(String? serviceName) async { + Future _init(Service? service) async { List> initErrorFutures = []; - for (Service service in _serviceDependents![serviceName] ?? {}) { + for (Service dependent in _serviceDependents![service] ?? {}) { bool initialize = true; - for (Service dependency in service.serviceDependsOn ?? {}) { + for (Service dependency in _serviceDependencies![dependent] ?? {}) { if (_services!.contains(dependency) && !dependency.isInitialized) { initialize = false; break; } } - if (initialize && !service.isInitialized && !_initializedServices!.contains(service.debugDisplayName)) { - _initializedServices!.add(service.debugDisplayName); - initErrorFutures.add(initService(service).then((ServiceError? error) async { + if (initialize && !dependent.isInitialized && !_attemptedServiceInits!.contains(dependent.debugDisplayName)) { + _attemptedServiceInits!.add(dependent.debugDisplayName); + initErrorFutures.add(initService(dependent).then((ServiceError? error) async { if (error?.severity == ServiceErrorSeverity.fatal) { - initFallback(); + await initServiceFallback(dependent); return error; } - return await _init(service.debugDisplayName); + return await _init(dependent); })); } } @@ -140,15 +141,6 @@ class Services { return initErrors.firstWhere((element) => element != null, orElse: () => null); } - @protected - Future initFallback() async { - for (Service service in _services!) { - if (service.isInitialized != true) { - await initServiceFallback(service); - } - } - } - @protected Future initService(Service service) async { try { @@ -163,11 +155,13 @@ class Services { @protected Future initServiceFallback(Service service) async { - try { - await service.initServiceFallback(); - } - on ServiceError catch (error) { - debugPrint(error.toString()); + if (service.isInitialized != true) { + try { + await service.initServiceFallback(); + } + on ServiceError catch (error) { + debugPrint(error.toString()); + } } } @@ -189,58 +183,32 @@ class Services { ServiceError? verifyInitialization() { Set? serviceNames = _services != null ? Set.of(List.generate(_services!.length, (index) => _services![index].debugDisplayName)) : null; - Set remaining = serviceNames?.difference(_initializedServices ?? {}) ?? {}; + Set remaining = serviceNames?.difference(_attemptedServiceInits ?? {}) ?? {}; return remaining.isEmpty ? null : ServiceError( source: null, severity: ServiceErrorSeverity.fatal, title: 'Services Initialization Error', - description: 'The following services were not initialized: $remaining', + description: 'The following services were not initialized: ${remaining.join(", ")}', ); } - Map?> _getServiceDependents() { - - Map?> serviceDependents = {}; + void _setDependencyTree() { + _serviceDependencies ??= {}; + _serviceDependents ??= {}; for (Service service in _services!) { - if (service.serviceDependsOn?.isEmpty ?? true) { - serviceDependents[null] ??= {}; - serviceDependents[null]!.add(service); + _serviceDependencies![service] = service.serviceDependsOn; + if (_serviceDependencies![service]?.isEmpty ?? true) { + _serviceDependents![null] ??= {}; + _serviceDependents![null]!.add(service); } else { - for (Service dependency in service.serviceDependsOn!) { + for (Service dependency in _serviceDependencies![service]!) { if (_services!.contains(dependency)) { - serviceDependents[dependency.debugDisplayName] ??= {}; - serviceDependents[dependency.debugDisplayName]!.add(service); + _serviceDependents![dependency] ??= {}; + _serviceDependents![dependency]!.add(service); } } } } - - return serviceDependents; - - // List queue = []; - // List services = List.from(inputServices); - // while (services.isNotEmpty) { - // // start with lowest priority service - // Service svc = services.last; - // services.removeLast(); - - // // Move to TBD anyone from Queue that depends on svc - // Set? svcDependents = svc.serviceDependsOn; - // if (svcDependents != null) { - // for (int index = queue.length - 1; index >= 0; index--) { - // Service queuedSvc = queue[index]; - // if (svcDependents.contains(queuedSvc)) { - // queue.removeAt(index); - // services.add(queuedSvc); - // } - // } - // } - - // // Move svc from TBD to Queue, mark it as processed - // queue.add(svc); - // } - - // return queue.reversed.toList(); } } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index d920e4222..c0a57b7b5 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -119,8 +119,7 @@ class Storage with Service { } Future getSecureStringWithName(String name, {String? defaultValue}) async { - String? hehe = await _secureStorage?.read(key: name) ?? defaultValue; - return hehe; + return await _secureStorage?.read(key: name) ?? defaultValue; } Future setSecureStringWithName(String name, String? value) async { From fc19ec046c4c142065cad4f7cc5ea7ec9a276229 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Tue, 5 Dec 2023 12:15:58 -0600 Subject: [PATCH 100/177] clear service dependencies on destroy services --- lib/service/service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/service/service.dart b/lib/service/service.dart index 8b2542c73..c984e33ab 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -82,6 +82,7 @@ class Services { } _services = null; _serviceDependents = null; + _serviceDependencies = null; _attemptedServiceInits = null; } } From 1b4ca645167022c04aad1effa019ffb61e5a4b0a Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Tue, 5 Dec 2023 16:45:12 -0600 Subject: [PATCH 101/177] further improve auth and styles init --- lib/service/auth2.dart | 41 +++++++++++++++++++++-------------- lib/service/styles.dart | 47 +++++++++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 02a66030d..e20695629 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -116,7 +116,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future initService() async { await _initServiceOffline(); - if ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { + if (isAnonymousAuthenticationSupported && (_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { if (!await authenticateAnonymously()) { throw ServiceError( source: this, @@ -150,33 +150,39 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _anonymousId = Storage().auth2AnonymousId; List> futures = [ + RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2), + Storage().getAuth2Token(), Storage().getAuth2Account(), Storage().getAuth2OidcToken(), - - Storage().getAuth2AnonymousToken(), - Storage().getAuth2AnonymousPrefs(), - Storage().getAuth2AnonymousProfile(), - - RokwirePlugin.getDeviceId(deviceIdIdentifier, deviceIdIdentifier2), ]; + if (isAnonymousAuthenticationSupported) { + futures.addAll([ + Storage().getAuth2AnonymousToken(), + Storage().getAuth2AnonymousPrefs(), + Storage().getAuth2AnonymousProfile(), + ]); + } List results = await Future.wait(futures); - _token = results[0]; - _account = results[1]; - _oidcToken = results[2]; - _anonymousToken = results[3]; - _anonymousPrefs = results[4]; - _anonymousProfile = results[5]; - _deviceId = results[6]; + _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)) { + if ((_account == null) && (_anonymousPrefs == null) && isAnonymousAuthenticationSupported) { futures.add(Storage().setAuth2AnonymousPrefs(_anonymousPrefs = defaultAnonymousPrefs)); } - if ((_account == null) && (_anonymousProfile == null)) { + if ((_account == null) && (_anonymousProfile == null) && isAnonymousAuthenticationSupported) { futures.add(Storage().setAuth2AnonymousProfile(_anonymousProfile = defaultAnonymousProfile)); } @@ -185,6 +191,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } + @protected + bool get isAnonymousAuthenticationSupported => true; + @override Set get serviceDependsOn { return { Storage(), Config() }; diff --git a/lib/service/styles.dart b/lib/service/styles.dart index c4a17ce79..3cdaeb20c 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -248,16 +248,35 @@ class Styles extends Service implements NotificationsListener{ Future build() async { Map styles = contentMap; Map> themes = JsonUtils.mapOfStringToMapOfStringsValue(styles['themes']) ?? {}; - UiTheme defaultTheme = await UiTheme.fromJson(styles, resolveImageAssetPath); - _themes['default'] = defaultTheme; + + 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); - _themes[theme.key] = await UiTheme.fromJson(themeData, resolveImageAssetPath); + futures.add(UiTheme.fromJson(themeData, resolveImageAssetPath)); + themeKeys.add(theme.key); } else { - _themes[theme.key] = defaultTheme; + 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(); } @@ -354,11 +373,21 @@ class UiTheme { UiTheme({this.colors, this.fontFamilies, this.textStyles, this.images}); static Future fromJson(Map styles, String Function(Uri)? assetPathResolver) async { - UiColors? colors = await compute(UiColors.fromJson, JsonUtils.mapValue(styles['color'])); - UiFontFamilies? fontFamilies = await compute(UiFontFamilies.fromJson, JsonUtils.mapValue(styles['font_family'])); - UiTextStyles? textStyles = await compute(UiTextStyles.fromCreationParam, _UiTextStylesCreationParam(JsonUtils.mapValue(styles['text_style']), colors: colors, fontFamilies: fontFamilies)); - UiImages? images = await compute(UiImages.fromCreationParam, _UiImagesCreationParam(JsonUtils.mapValue(styles['image']), colors: colors, assetPathResolver: assetPathResolver)); - return UiTheme(colors: colors, fontFamilies: fontFamilies, textStyles: textStyles, images: images); + 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]); } } From c9c739ce0e792d8ffc89ce3d51603e3dd5f8bd77 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Tue, 5 Dec 2023 19:30:42 -0600 Subject: [PATCH 102/177] move isAnonymousAuthenticationSupported getter --- lib/service/auth2.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index e20695629..e6467247c 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -191,9 +191,6 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } - @protected - bool get isAnonymousAuthenticationSupported => true; - @override Set get serviceDependsOn { return { Storage(), Config() }; @@ -302,6 +299,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2Token? get oidcToken => _oidcToken; bool get associateAnonymousIds => false; + bool get isAnonymousAuthenticationSupported => true; String? get accountId => _account?.id ?? _anonymousId; Auth2UserPrefs? get prefs => _account?.prefs ?? _anonymousPrefs; Auth2UserProfile? get profile => _account?.profile ?? _anonymousProfile; From 9c06003456578934e98ea79c3edf284eaed014a1 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Tue, 5 Dec 2023 19:31:39 -0600 Subject: [PATCH 103/177] add parentheses for clarity" --- lib/service/auth2.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index e6467247c..e2838d551 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -116,7 +116,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Future initService() async { await _initServiceOffline(); - if (isAnonymousAuthenticationSupported && (_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid) { + if (isAnonymousAuthenticationSupported && ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid)) { if (!await authenticateAnonymously()) { throw ServiceError( source: this, From 505f219df8a858b98ddbc3575367a51ca2d61305 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 5 Dec 2023 20:39:31 -0600 Subject: [PATCH 104/177] disable graphql cache --- lib/service/graph_ql.dart | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/lib/service/graph_ql.dart b/lib/service/graph_ql.dart index 3bb242fa8..cf211b6e9 100644 --- a/lib/service/graph_ql.dart +++ b/lib/service/graph_ql.dart @@ -44,18 +44,30 @@ class GraphQL with Service { GraphQLClient getClient(String url, {Map> possibleTypes = const {}, Map defaultHeaders = const {}, AuthLink? authLink, - FetchPolicy? defaultFetchPolicy}) { + 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 - )), - cache: GraphQLCache(store: HiveStore(), possibleTypes: possibleTypes, partialDataPolicy: PartialDataCachePolicy.accept), + 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; } @@ -64,3 +76,17 @@ class GraphQL with Service { 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() => {}; +} From 3e209c2b74a821d754fd27ea87afcdb8eecc46ef Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Wed, 6 Dec 2023 12:10:30 +0200 Subject: [PATCH 105/177] Reset to initial [#408]. --- lib/service/service.dart | 120 ++++++++++++++------------------------- 1 file changed, 42 insertions(+), 78 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index c984e33ab..91da84606 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -42,8 +42,6 @@ abstract class Service { Set? get serviceDependsOn { return null; } - - String get debugDisplayName => runtimeType.toString(); } class Services { @@ -62,14 +60,11 @@ class Services { static set instance(Services? value) => _instance = value; List? _services; - Map?>? _serviceDependents; // gives services that depend on the key service (looking "down" dependency tree) - Map?>? _serviceDependencies; // gives services are dependencies of the key service (looking "up" dependency tree) - Set? _attemptedServiceInits; // gives names of services for which initialization has been attempted void create(List services) { if (_services == null) { - _services = services; - for (Service service in services) { + _services = _sort(services); + for (Service service in _services!) { service.createService(); } } @@ -81,28 +76,21 @@ class Services { service.destroyService(); } _services = null; - _serviceDependents = null; - _serviceDependencies = null; - _attemptedServiceInits = null; } } Future init() async { if (_services != null) { - _attemptedServiceInits ??= {}; - _setDependencyTree(); - if (_serviceDependents?[null]?.isEmpty ?? true) { - return ServiceError( - source: null, - severity: ServiceErrorSeverity.fatal, - title: 'Services Initialization Error', - description: 'All services have dependencies. No services may be initialized safely.', - ); + for (Service service in _services!) { + if (service.isInitialized != true) { + ServiceError? error = await initService(service); + if (error?.severity == ServiceErrorSeverity.fatal) { + await initFallback(); + return error; + } + } } - - return _init(null); } - return null; /*TMP: @@ -114,32 +102,13 @@ class Services { );*/ } - Future _init(Service? service) async { - List> initErrorFutures = []; - for (Service dependent in _serviceDependents![service] ?? {}) { - bool initialize = true; - for (Service dependency in _serviceDependencies![dependent] ?? {}) { - if (_services!.contains(dependency) && !dependency.isInitialized) { - initialize = false; - break; - } - } - - if (initialize && !dependent.isInitialized && !_attemptedServiceInits!.contains(dependent.debugDisplayName)) { - _attemptedServiceInits!.add(dependent.debugDisplayName); - initErrorFutures.add(initService(dependent).then((ServiceError? error) async { - if (error?.severity == ServiceErrorSeverity.fatal) { - await initServiceFallback(dependent); - return error; - } - - return await _init(dependent); - })); + @protected + Future initFallback() async { + for (Service service in _services!) { + if (service.isInitialized != true) { + await initServiceFallback(service); } } - - List initErrors = await Future.wait(initErrorFutures); - return initErrors.firstWhere((element) => element != null, orElse: () => null); } @protected @@ -156,13 +125,11 @@ class Services { @protected Future initServiceFallback(Service service) async { - if (service.isInitialized != true) { - try { - await service.initServiceFallback(); - } - on ServiceError catch (error) { - debugPrint(error.toString()); - } + try { + await service.initServiceFallback(); + } + on ServiceError catch (error) { + debugPrint(error.toString()); } } @@ -182,36 +149,33 @@ class Services { } } - ServiceError? verifyInitialization() { - Set? serviceNames = _services != null ? Set.of(List.generate(_services!.length, (index) => _services![index].debugDisplayName)) : null; - Set remaining = serviceNames?.difference(_attemptedServiceInits ?? {}) ?? {}; - return remaining.isEmpty ? null : ServiceError( - source: null, - severity: ServiceErrorSeverity.fatal, - title: 'Services Initialization Error', - description: 'The following services were not initialized: ${remaining.join(", ")}', - ); - } + static List _sort(List inputServices) { - void _setDependencyTree() { - _serviceDependencies ??= {}; - _serviceDependents ??= {}; - for (Service service in _services!) { - _serviceDependencies![service] = service.serviceDependsOn; - if (_serviceDependencies![service]?.isEmpty ?? true) { - _serviceDependents![null] ??= {}; - _serviceDependents![null]!.add(service); - } else { - for (Service dependency in _serviceDependencies![service]!) { - if (_services!.contains(dependency)) { - _serviceDependents![dependency] ??= {}; - _serviceDependents![dependency]!.add(service); - } + List queue = []; + List services = List.from(inputServices); + while (services.isNotEmpty) { + // start with lowest priority service + Service svc = services.last; + services.removeLast(); + + // Move to TBD anyone from Queue that depends on svc + Set? svcDependents = svc.serviceDependsOn; + if (svcDependents != null) { + for (int index = queue.length - 1; index >= 0; index--) { + Service queuedSvc = queue[index]; + if (svcDependents.contains(queuedSvc)) { + queue.removeAt(index); + services.add(queuedSvc); + } } } + + // Move svc from TBD to Queue, mark it as processed + queue.add(svc); } - } + return queue.reversed.toList(); + } } class ServiceError implements Exception { From ec5952ea740b7cdd18038e07d365e9f9ab8f7597 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Wed, 6 Dec 2023 15:29:17 +0200 Subject: [PATCH 106/177] Build service initialization list using Breadth First Search (BFS) algorithm for graph traversal [#408]. --- lib/service/service.dart | 133 ++++++++++++++++++++++++++++++--------- lib/utils/utils.dart | 13 ++++ 2 files changed, 116 insertions(+), 30 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 91da84606..2a3f3f8dd 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -16,6 +16,7 @@ import 'package:flutter/foundation.dart'; +import 'package:rokwire_plugin/utils/utils.dart'; abstract class Service { @@ -63,7 +64,7 @@ class Services { void create(List services) { if (_services == null) { - _services = _sort(services); + _services = services; for (Service service in _services!) { service.createService(); } @@ -80,16 +81,11 @@ class Services { } Future init() async { - if (_services != null) { - for (Service service in _services!) { - if (service.isInitialized != true) { - ServiceError? error = await initService(service); - if (error?.severity == ServiceErrorSeverity.fatal) { - await initFallback(); - return error; - } - } - } + try { + return (_services != null) ? await _executeInitList(_buildInitList(_services!)) : null; + } + on ServiceError catch (error) { + return error; } return null; @@ -149,33 +145,110 @@ class Services { } } - static List _sort(List inputServices) { + @protected + List> _buildInitList(List servicesList) { + List> initList = >[]; // Ordered list of service pools as they should be initialized. + Set registered = Set.from(servicesList); // Pool of all registered services. Some services refer other services that are not registered so we need to ignore them. + Set processing = Set.from(registered); // Pool of services that are not scheduled for initialization yet. + Set processed = {}; // Pool of services that are scheduled for initialization. + + while (processing.isNotEmpty) { + Set currentListEntry = {}; + for (Service service in processing) { + if (_canProcessService(service, processed: processed, registered: registered)) /* (processed.containsAll(service.serviceDependsOn ?? {})) */ { + currentListEntry.add(service); // All service ancessors are already scheduled for initialization => we can initialize this service on the current step. + } + } - List queue = []; - List services = List.from(inputServices); - while (services.isNotEmpty) { - // start with lowest priority service - Service svc = services.last; - services.removeLast(); + if (currentListEntry.isNotEmpty) { + initList.add(currentListEntry); // Register current initialization step. + processed.addAll(currentListEntry); // Mark current initialization step services as scheduled for initialization. + processing.removeAll(currentListEntry); // Remove current initialization step services from the not scheduled yet pool. + } + else { + // There are services not scheduled for initialozation but we cannot retrieve any that depends only on the scheduled pool. + // This is an indication that processing contains a services that have dependancy cycle. + throw ServiceError( + source: null, + severity: ServiceErrorSeverity.fatal, + title: 'Services Initialization Error', + description: 'Service dependency cycle: ${_servicePoolToString(processing)}', + ); + } + } - // Move to TBD anyone from Queue that depends on svc - Set? svcDependents = svc.serviceDependsOn; - if (svcDependents != null) { - for (int index = queue.length - 1; index >= 0; index--) { - Service queuedSvc = queue[index]; - if (svcDependents.contains(queuedSvc)) { - queue.removeAt(index); - services.add(queuedSvc); - } + return initList; + } + + @protected + bool _canProcessService(Service service, { required Set processed, required Set registered}) { + Set? serviceAncessors = service.serviceDependsOn; + if ((serviceAncessors != null) && serviceAncessors.isNotEmpty) { + for (Service serviceAncessor in serviceAncessors) { + if (registered.contains(serviceAncessor) && !processed.contains(serviceAncessor)) { + return false; } } + } + return true; + } - // Move svc from TBD to Queue, mark it as processed - queue.add(svc); + @protected + Future _executeInitList(List> initList) async { + ServiceError? result = null; + Set skipped = {}; + for (int initStep = 0; initStep < initList.length; initStep++) { + Iterable currentListEntry = initList[initStep]; + ServiceError? currentListError = await initServices(currentListEntry, skipped); + debugPrint("Services.init[${initStep}] => ${_servicePoolToString(currentListEntry, marked: skipped)};"); + result ??= currentListError; } + return result; + } + + @protected + Future initServices(Iterable services, Set skipped) async { + if (services.isNotEmpty) { + + Set skippedServices = {}; + List> initFutures = >[]; + for (Service service in services) { + if (skipped.containsAny(service.serviceDependsOn ?? {})) { + // Service ancessor is skipped, invoke initServiceFallback and do not try to initialize it. + initFutures.add(initServiceFallback(service)); + skippedServices.add(service); + } + else { + // Service should be initialized. + initFutures.add(initService(service)); + } + } - return queue.reversed.toList(); + try { + ServiceError? initError; + List initResults = await Future.wait(initFutures); + for (int index = 0; index < initResults.length; index++) { + dynamic initResult = initResults[index]; + if ((initResult is ServiceError) && (initResult.severity == ServiceErrorSeverity.fatal)) { + skipped.add(services.elementAt(index)); // service initialization failed => do not attempt to initialize its ancessors + initError ??= initResult; + } + } + if (skippedServices.isNotEmpty) { + skipped.addAll(skippedServices); // service initialization is skipped => do not attempt to initialize its ancessors + } + return initError; + } + on ServiceError catch (error) { + return error; + } + } + return null; } + + @protected + String _servicePoolToString(Iterable pool, { Set? marked, String mark = '!' }) => + '[${pool.map((service) => "${service.runtimeType}${(marked?.contains(service) == true) ? mark : ''}").join(', ')}]'; } class ServiceError implements Exception { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 262dc4968..c9826e0b6 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -316,6 +316,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; From e9d4482645cf47e8d3c269d450f5b5dfadd740f5 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Wed, 6 Dec 2023 15:38:26 +0200 Subject: [PATCH 107/177] Make skipped param of initServices API optional [#408]. --- lib/service/service.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 2a3f3f8dd..bfb439d77 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -195,25 +195,25 @@ class Services { @protected Future _executeInitList(List> initList) async { + int initStep = 0; ServiceError? result = null; Set skipped = {}; - for (int initStep = 0; initStep < initList.length; initStep++) { - Iterable currentListEntry = initList[initStep]; - ServiceError? currentListError = await initServices(currentListEntry, skipped); - debugPrint("Services.init[${initStep}] => ${_servicePoolToString(currentListEntry, marked: skipped)};"); + for (Iterable currentListEntry in initList) { + ServiceError? currentListError = await initServices(currentListEntry, skipped: skipped); + debugPrint("Services.init[${initStep++}] => ${_servicePoolToString(currentListEntry, marked: skipped)};"); result ??= currentListError; } return result; } @protected - Future initServices(Iterable services, Set skipped) async { + Future initServices(Iterable services, { Set? skipped }) async { if (services.isNotEmpty) { Set skippedServices = {}; List> initFutures = >[]; for (Service service in services) { - if (skipped.containsAny(service.serviceDependsOn ?? {})) { + if (skipped?.containsAny(service.serviceDependsOn ?? {}) == true) { // Service ancessor is skipped, invoke initServiceFallback and do not try to initialize it. initFutures.add(initServiceFallback(service)); skippedServices.add(service); @@ -230,12 +230,12 @@ class Services { for (int index = 0; index < initResults.length; index++) { dynamic initResult = initResults[index]; if ((initResult is ServiceError) && (initResult.severity == ServiceErrorSeverity.fatal)) { - skipped.add(services.elementAt(index)); // service initialization failed => do not attempt to initialize its ancessors + skipped?.add(services.elementAt(index)); // service initialization failed => do not attempt to initialize its ancessors initError ??= initResult; } } if (skippedServices.isNotEmpty) { - skipped.addAll(skippedServices); // service initialization is skipped => do not attempt to initialize its ancessors + skipped?.addAll(skippedServices); // service initialization is skipped => do not attempt to initialize its ancessors } return initError; } From 4e372c79cea69ef895d87c63bbeaa71d80ef23cd Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Wed, 6 Dec 2023 14:48:58 -0600 Subject: [PATCH 108/177] combine some styles init async loads, call verifyInitialization from Services().init() --- lib/service/service.dart | 5 +++-- lib/service/styles.dart | 33 +++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index c984e33ab..6298f4a91 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -96,11 +96,12 @@ class Services { source: null, severity: ServiceErrorSeverity.fatal, title: 'Services Initialization Error', - description: 'All services have dependencies. No services may be initialized safely.', + description: 'All services have service dependencies. No services may be initialized safely.', ); } - return _init(null); + ServiceError? initError = await _init(null); + return initError ?? verifyInitialization(); } return null; diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 3cdaeb20c..bca3b09ba 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -35,7 +35,6 @@ 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'; -import 'package:flutter/foundation.dart' show kIsWeb; class Styles extends Service implements NotificationsListener{ @@ -91,13 +90,31 @@ class Styles extends Service implements NotificationsListener{ @override Future initService() async { - - _assetsDir = await getAssetsDir(); - _assetsManifest = await loadAssetsManifest(); - _assetsStyles = await loadFromAssets(assetsKey); - _appAssetsStyles = kIsWeb ? null : await loadFromAssets(appAssetsKey); - _netAssetsStyles = await loadFromCache(netCacheFileName); - _debugAssetsStyles = await loadFromCache(debugCacheFileName); + List> futures = [ + getAssetsDir(), + loadAssetsManifest(), + loadFromAssets(assetsKey), + ]; + + if (!kIsWeb) { + futures.add(loadFromAssets(appAssetsKey)); + } + List results = await Future.wait(futures); + _assetsDir = results[0]; + _assetsManifest = results[1]; + _assetsStyles = results[2]; + if (!kIsWeb) { + _appAssetsStyles = results[3]; + } + + futures = [ + loadFromCache(netCacheFileName), + loadFromCache(debugCacheFileName), + ]; + results = await Future.wait(futures); + + _netAssetsStyles = results[0]; + _debugAssetsStyles = results[1]; if ((_assetsStyles != null) || (_appAssetsStyles != null) || (_netAssetsStyles != null) || (_debugAssetsStyles != null)) { await build(); From 6e273fa4d2dc92f132bd9f58a8a1cd65eb295b8d Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Thu, 7 Dec 2023 13:45:59 -0600 Subject: [PATCH 109/177] do not call Services._init if there are no dervice dependents --- lib/service/service.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 6298f4a91..63eab5422 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -65,6 +65,7 @@ class Services { Map?>? _serviceDependents; // gives services that depend on the key service (looking "down" dependency tree) Map?>? _serviceDependencies; // gives services are dependencies of the key service (looking "up" dependency tree) Set? _attemptedServiceInits; // gives names of services for which initialization has been attempted + Stopwatch _sw = Stopwatch(); void create(List services) { if (_services == null) { @@ -100,7 +101,9 @@ class Services { ); } + _sw.start(); ServiceError? initError = await _init(null); + _sw.stop(); return initError ?? verifyInitialization(); } @@ -134,7 +137,10 @@ class Services { return error; } - return await _init(dependent); + if (_serviceDependents![dependent]?.isNotEmpty ?? false) { + return await _init(dependent); + } + return null; })); } } @@ -146,7 +152,9 @@ class Services { @protected Future initService(Service service) async { try { + debugPrint('Services.initService ${service} start: ${_sw.elapsedMilliseconds}ms'); await service.initService(); + debugPrint('Services.initService ${service} end: ${_sw.elapsedMilliseconds}ms'); } on ServiceError catch (error) { return error; From b53da934adffd2e0d4c89f1bd4a6ec475eaf01d8 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Thu, 7 Dec 2023 13:46:32 -0600 Subject: [PATCH 110/177] remove stopwatch --- lib/service/service.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 63eab5422..cfe2b0ed2 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -65,7 +65,6 @@ class Services { Map?>? _serviceDependents; // gives services that depend on the key service (looking "down" dependency tree) Map?>? _serviceDependencies; // gives services are dependencies of the key service (looking "up" dependency tree) Set? _attemptedServiceInits; // gives names of services for which initialization has been attempted - Stopwatch _sw = Stopwatch(); void create(List services) { if (_services == null) { @@ -101,9 +100,7 @@ class Services { ); } - _sw.start(); ServiceError? initError = await _init(null); - _sw.stop(); return initError ?? verifyInitialization(); } @@ -152,9 +149,7 @@ class Services { @protected Future initService(Service service) async { try { - debugPrint('Services.initService ${service} start: ${_sw.elapsedMilliseconds}ms'); await service.initService(); - debugPrint('Services.initService ${service} end: ${_sw.elapsedMilliseconds}ms'); } on ServiceError catch (error) { return error; From 5966fedb0a4701fe89a0fa150d98083d722ee461 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 11 Dec 2023 01:49:35 -0600 Subject: [PATCH 111/177] handle deeplinks --- lib/service/deep_link.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/service/deep_link.dart b/lib/service/deep_link.dart index 34972a32d..daa65e50f 100644 --- a/lib/service/deep_link.dart +++ b/lib/service/deep_link.dart @@ -23,7 +23,7 @@ class DeepLink with Service { static const String notifyUri = "edu.illinois.rokwire.deeplink.uri"; - // Singletone Factory + // Singleton Factory static DeepLink? _instance; @@ -37,6 +37,9 @@ class DeepLink with Service { @protected DeepLink.internal(); + Uri? _initialUri; + Uri? get initialUri => _initialUri; + // Service @override @@ -45,6 +48,8 @@ class DeepLink with Service { // 1. Initial Uri getInitialUri().then((uri) { if (uri != null) { + _initialUri = uri; + debugPrint('Launch URI: $uri'); NotificationService().notify(notifyUri, uri); } }); @@ -52,6 +57,7 @@ class DeepLink with Service { // 2. Updated uri if (!kIsWeb) { uriLinkStream.listen((Uri? uri) { + debugPrint('Received URI: $uri'); if (uri != null) { NotificationService().notify(notifyUri, uri); } From d1cab4b9d6ae0720e7aa008a8dd643e74bad3cb6 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 11 Dec 2023 02:14:52 -0600 Subject: [PATCH 112/177] fix typos, add back service debugDisplayName --- lib/service/service.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 347d564b4..544823149 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -43,6 +43,8 @@ abstract class Service { Set? get serviceDependsOn { return null; } + + String get debugDisplayName => runtimeType.toString(); } class Services { @@ -155,7 +157,7 @@ class Services { Set currentListEntry = {}; for (Service service in processing) { if (_canProcessService(service, processed: processed, registered: registered)) /* (processed.containsAll(service.serviceDependsOn ?? {})) */ { - currentListEntry.add(service); // All service ancessors are already scheduled for initialization => we can initialize this service on the current step. + currentListEntry.add(service); // All service ancestors are already scheduled for initialization => we can initialize this service on the current step. } } @@ -181,10 +183,10 @@ class Services { @protected bool _canProcessService(Service service, { required Set processed, required Set registered}) { - Set? serviceAncessors = service.serviceDependsOn; - if ((serviceAncessors != null) && serviceAncessors.isNotEmpty) { - for (Service serviceAncessor in serviceAncessors) { - if (registered.contains(serviceAncessor) && !processed.contains(serviceAncessor)) { + Set? serviceAncestors = service.serviceDependsOn; + if ((serviceAncestors != null) && serviceAncestors.isNotEmpty) { + for (Service serviceAncestor in serviceAncestors) { + if (registered.contains(serviceAncestor) && !processed.contains(serviceAncestor)) { return false; } } @@ -213,7 +215,7 @@ class Services { List> initFutures = >[]; for (Service service in services) { if (skipped?.containsAny(service.serviceDependsOn ?? {}) == true) { - // Service ancessor is skipped, invoke initServiceFallback and do not try to initialize it. + // Service ancestor is skipped, invoke initServiceFallback and do not try to initialize it. initFutures.add(initServiceFallback(service)); skippedServices.add(service); } @@ -229,12 +231,12 @@ class Services { for (int index = 0; index < initResults.length; index++) { dynamic initResult = initResults[index]; if ((initResult is ServiceError) && (initResult.severity == ServiceErrorSeverity.fatal)) { - skipped?.add(services.elementAt(index)); // service initialization failed => do not attempt to initialize its ancessors + skipped?.add(services.elementAt(index)); // service initialization failed => do not attempt to initialize its ancestors initError ??= initResult; } } if (skippedServices.isNotEmpty) { - skipped?.addAll(skippedServices); // service initialization is skipped => do not attempt to initialize its ancessors + skipped?.addAll(skippedServices); // service initialization is skipped => do not attempt to initialize its ancestors } return initError; } From eda8d8ea49a6877ab1a1d0de9645c81ffe198092 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Mon, 11 Dec 2023 21:38:09 -0600 Subject: [PATCH 113/177] fix anonymous auth --- lib/model/auth2.dart | 4 ++++ lib/service/auth2.dart | 3 ++- lib/service/config.dart | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index e64977bce..33f922c23 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -65,6 +65,10 @@ class Auth2Token { return DateTime.now().isAfter(expirationDate); } + dynamic getClaim(String claim) { + return _parsedAccessToken?[claim]; + } + @override bool operator ==(other) => (other is Auth2Token) && diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index e2838d551..132c60cac 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -299,8 +299,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2Token? get oidcToken => _oidcToken; bool get associateAnonymousIds => false; - bool get isAnonymousAuthenticationSupported => true; + 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; String? get loginType => _account?.authType?.code; diff --git a/lib/service/config.dart b/lib/service/config.dart index 73f5b5bc7..400b8630b 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -541,7 +541,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { return (assetsCacheDir != null) ? Directory(assetsCacheDir) : null; } - bool get supportsAnonymousAuth => false; + bool get supportsAnonymousAuth => true; // Getters: compound entries Map get content => _config ?? {}; From 7b234e374df8aa080b5cb0832fdd7a9c1de56628 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Tue, 12 Dec 2023 22:21:19 -0600 Subject: [PATCH 114/177] disable location permission --- android/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 86fd58e6c..131f2d25b 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + From f016667d06ee060b3efc05703e78a29678dbc8c7 Mon Sep 17 00:00:00 2001 From: Dobromir Dobrev Date: Tue, 19 Dec 2023 09:54:16 +0200 Subject: [PATCH 115/177] Do not try to retrieve device id for web. --- lib/rokwire_plugin.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/rokwire_plugin.dart b/lib/rokwire_plugin.dart index d46f0c3d9..77270517c 100644 --- a/lib/rokwire_plugin.dart +++ b/lib/rokwire_plugin.dart @@ -50,6 +50,10 @@ class RokwirePlugin { } static Future getDeviceId([String? identifier, String? identifier2]) async { + if (kIsWeb) { + return null; + } + try { return await _channel.invokeMethod('getDeviceId', { 'identifier': identifier, 'identifier2': identifier2 From 9fef7a706e52f5d0e2658f3953dc9686527117a7 Mon Sep 17 00:00:00 2001 From: Dobromir Dobrev Date: Tue, 19 Dec 2023 09:54:42 +0200 Subject: [PATCH 116/177] Do not try to apply http proxy settings for web. --- lib/service/http_proxy.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/service/http_proxy.dart b/lib/service/http_proxy.dart index 89e3face8..0cff2be5b 100644 --- a/lib/service/http_proxy.dart +++ b/lib/service/http_proxy.dart @@ -75,6 +75,9 @@ class HttpProxy extends Service implements NotificationsListener { } Future applySystemProxy() async { + if (kIsWeb) { + return; + } bool enabled = false; String? host; int? port; @@ -97,6 +100,9 @@ class HttpProxy extends Service implements NotificationsListener { } set httpProxyEnabled(bool? value){ + if (kIsWeb) { + return; + } if(Storage().httpProxyEnabled != value) { Storage().httpProxyEnabled = value; _handleChanged(); From 529e8e8a5c3800ee039acadfb8c6394b0a148e28 Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 28 Dec 2023 19:11:35 -0600 Subject: [PATCH 117/177] fix ribbon button tap listener --- lib/ui/widgets/ribbon_button.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index e91f4f95f..369972942 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -179,8 +179,8 @@ class _RibbonButtonState extends State { child: Semantics(label: widget.label, hint: widget.hint, value : widget.semanticsValue, button: true, excludeSemantics: true, child: InkWell( borderRadius: widget.borderRadius, - onTap: () => widget.onTapWidget(context), child: - Padding(padding: widget.displayPadding, child: + 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: From 891dcb7a0168faae5d7a3e2421fa90e42b16fcff Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Fri, 29 Dec 2023 17:42:00 -0600 Subject: [PATCH 118/177] clear oidc token on logout --- lib/service/auth2.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 132c60cac..706738f68 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1456,6 +1456,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = prefs ?? _account?.prefs ?? Auth2UserPrefs.empty()); await Storage().setAuth2AnonymousProfile(_anonymousProfile = Auth2UserProfile.empty()); await Storage().setAuth2Token(_token = null); + await Storage().setAuth2OidcToken(_oidcToken = null); await Storage().setAuth2Account(_account = null); _updateUserPrefsTimer?.cancel(); From 1b359989f4ee94cd8712f6447a92d867049729ad Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 4 Jan 2024 14:02:31 +0200 Subject: [PATCH 119/177] "Deregister" renamed to "unregister" [rokmetro/vogue-app#481]. --- lib/ui/widgets/scroll_pager.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ui/widgets/scroll_pager.dart b/lib/ui/widgets/scroll_pager.dart index ba7553046..46fb6185d 100644 --- a/lib/ui/widgets/scroll_pager.dart +++ b/lib/ui/widgets/scroll_pager.dart @@ -81,7 +81,7 @@ class ScrollPagerController { void registerScrollController(ScrollController controller) { if (_scrollController != null && _scrollController != controller) { - deregisterScrollController(); + unregisterScrollController(); } controller.addListener(_scrollListener); _scrollController = controller; @@ -113,7 +113,7 @@ class ScrollPagerController { } } - void deregisterScrollController() { + void unregisterScrollController() { _scrollController?.removeListener(_scrollListener); _scrollController = null; } From e825b2ff1de6729c192d05687a0f408dce00a4d6 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 4 Jan 2024 14:10:45 +0200 Subject: [PATCH 120/177] Added ScrollPagerController.dispose [rokmetro/vogue-app#481]. --- lib/ui/widgets/scroll_pager.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/ui/widgets/scroll_pager.dart b/lib/ui/widgets/scroll_pager.dart index 46fb6185d..1ab26d48b 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() { + unregisterScrollController(); } Future reset() async { From 545fa52b8008832cf9c3295bcb0b42a580607057 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Fri, 5 Jan 2024 16:36:07 -0600 Subject: [PATCH 121/177] fix refresh bugs, make secure storage operations on login and logout simultaneous --- lib/service/auth2.dart | 50 +++++++++++++++++++++++++++------------- lib/service/network.dart | 39 +++++++++++++------------------ 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 706738f68..16eb6b08d 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -430,8 +430,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String? anonymousId = (params != null) ? JsonUtils.stringValue(params['anonymous_id']) : null; if ((anonymousToken != null) && anonymousToken.isValid && (anonymousId != null) && anonymousId.isNotEmpty) { _refreshTokenFailCounts.remove(_anonymousToken?.refreshToken); - await Storage().setAuth2AnonymousId(_anonymousId = anonymousId); - await Storage().setAuth2AnonymousToken(_anonymousToken = anonymousToken); + await Future.wait([ + Storage().setAuth2AnonymousId(_anonymousId = anonymousId), + Storage().setAuth2AnonymousToken(_anonymousToken = anonymousToken), + ]); _log("Auth2: anonymous auth succeeded: ${response?.statusCode}\n${response?.body}"); return true; } @@ -836,13 +838,20 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _token = token; _oidcToken = oidcToken; _account = account; - await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = null); - await Storage().setAuth2AnonymousProfile(_anonymousProfile = null); + + List> futures = [ + Storage().setAuth2AnonymousPrefs(_anonymousPrefs = null), + Storage().setAuth2AnonymousProfile(_anonymousProfile = null), + ]; + if (!kIsWeb) { - await Storage().setAuth2Token(token); - await Storage().setAuth2OidcToken(oidcToken); - await Storage().setAuth2Account(account); + futures.addAll([ + Storage().setAuth2Token(token), + Storage().setAuth2OidcToken(oidcToken), + Storage().setAuth2Account(account), + ]); } + await Future.wait(futures); if (prefsUpdated == true) { _saveAccountUserPrefs(); @@ -1453,11 +1462,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Network().post("${Config().authBaseUrl}/auth/logout", headers: headers, body: body, auth: Auth2Csrf(token: token)); } - await Storage().setAuth2AnonymousPrefs(_anonymousPrefs = prefs ?? _account?.prefs ?? Auth2UserPrefs.empty()); - await Storage().setAuth2AnonymousProfile(_anonymousProfile = Auth2UserProfile.empty()); - await Storage().setAuth2Token(_token = null); - await Storage().setAuth2OidcToken(_oidcToken = null); - await Storage().setAuth2Account(_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; @@ -1533,6 +1544,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _refreshTokenFailCounts.remove(futureKey); if (token == _token) { + // do not need to await applyToken because updated tokens are set in memory immediately applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); return responseToken; } @@ -1571,13 +1583,19 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected 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 Storage().setAuth2Token(token); + await Future.wait([ + Storage().setAuth2Token(token), + Storage().setAuth2OidcToken(oidcToken), + ]); } } - static Future _refreshToken(String? refreshToken) async { + Future _refreshToken(String? refreshToken) { if (Config().authBaseUrl != null) { String url = "${Config().authBaseUrl}/auth/refresh"; @@ -1587,7 +1605,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String? post; if (!Config().isReleaseWeb) { if (refreshToken == null) { - return null; + return Future.value(null); } post = JsonUtils.encode({ 'api_key': Config().rokwireApiKey, @@ -1597,7 +1615,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return Network().post(url, headers: headers, body: post, auth: Auth2Csrf()); } - return null; + return Future.value(null); } // User Prefs diff --git a/lib/service/network.dart b/lib/service/network.dart index 335de83ca..6829ac93d 100644 --- a/lib/service/network.dart +++ b/lib/service/network.dart @@ -145,12 +145,11 @@ class Network { http.Response? response; try { - dynamic token = auth?.networkAuthToken; - await auth?.refreshNetworkAuthTokenIfExpired(token); + await auth?.refreshNetworkAuthTokenIfExpired(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) { @@ -189,12 +188,11 @@ class Network { http.Response? response; try { - dynamic token = auth?.networkAuthToken; - await auth?.refreshNetworkAuthTokenIfExpired(token); + await auth?.refreshNetworkAuthTokenIfExpired(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) { @@ -233,12 +231,11 @@ class Network { http.Response? response; try { - dynamic token = auth?.networkAuthToken; - await auth?.refreshNetworkAuthTokenIfExpired(token); + await auth?.refreshNetworkAuthTokenIfExpired(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) { @@ -272,13 +269,12 @@ 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; - await auth?.refreshNetworkAuthTokenIfExpired(token); + try { + await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); 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) { @@ -312,12 +308,11 @@ 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; - await auth?.refreshNetworkAuthTokenIfExpired(token); + await auth?.refreshNetworkAuthTokenIfExpired(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) { @@ -381,12 +376,11 @@ class Network { http.StreamedResponse? response; try { - dynamic token = auth?.networkAuthToken; - await auth?.refreshNetworkAuthTokenIfExpired(token); + await auth?.refreshNetworkAuthTokenIfExpired(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) { @@ -450,13 +444,12 @@ 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; - await auth?.refreshNetworkAuthTokenIfExpired(token); + try { + await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); 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) { From e300e28be36e8eb2860e6ef7a39ac734f2d2ce9b Mon Sep 17 00:00:00 2001 From: Stephen Hurwit Date: Thu, 11 Jan 2024 14:10:01 -0600 Subject: [PATCH 122/177] make app navigation service non-singleton --- lib/service/app_navigation.dart | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/service/app_navigation.dart b/lib/service/app_navigation.dart index 2ec818985..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'; - // Singleton 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 From 271ec5c2c40a5c6e2352ee6d5f979c01a11f4e7a Mon Sep 17 00:00:00 2001 From: shurwit Date: Wed, 17 Jan 2024 15:05:03 -0600 Subject: [PATCH 123/177] disable firebase messaging --- example/pubspec.lock | 24 ----- lib/service/firebase_messaging.dart | 139 ++++++++++++++-------------- pubspec.yaml | 2 +- 3 files changed, 72 insertions(+), 93 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 2dc0ad38c..06080351b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -273,30 +273,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.1" - firebase_messaging: - dependency: transitive - description: - name: firebase_messaging - sha256: "9cfe5c4560fb83393511ca7620f8fb3f22c9a80303052f10290e732fcfb801bd" - url: "https://pub.dev" - source: hosted - version: "14.6.1" - firebase_messaging_platform_interface: - dependency: transitive - description: - name: firebase_messaging_platform_interface - sha256: "7e25cb71019ccef8b1fd7b37969af79f04c467974cce4dfc291fa36974edd7ba" - url: "https://pub.dev" - source: hosted - version: "4.5.1" - firebase_messaging_web: - dependency: transitive - description: - name: firebase_messaging_web - sha256: "5d9840cc8126ea723b1bda901389cb542902f664f2653c16d4f8114e95f13cec" - url: "https://pub.dev" - source: hosted - version: "3.5.1" flutter: dependency: "direct main" description: flutter diff --git a/lib/service/firebase_messaging.dart b/lib/service/firebase_messaging.dart index b3082d52d..30ca71542 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -17,7 +17,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; -import 'package:firebase_messaging/firebase_messaging.dart' as firebase_messaging; +// import 'package:firebase_messaging/firebase_messaging.dart' as firebase_messaging; import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/inbox.dart'; @@ -61,34 +61,34 @@ class FirebaseMessaging with Service { @override Future initService() async { - await firebase_messaging.FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( - alert: true, - badge: true, - sound: true, - ); - - firebase_messaging.FirebaseMessaging.onMessage.listen((firebase_messaging.RemoteMessage message) { - Log.d('FCM: onMessage'); - onFirebaseMessage(message); - }); - - firebase_messaging.FirebaseMessaging.onMessageOpenedApp.listen((firebase_messaging.RemoteMessage message) { - Log.d('FCM: onMessageOpenedApp'); - onFirebaseMessage(message); - }); - - firebase_messaging.FirebaseMessaging.instance.getToken().then((String? token) => applyToken(token)); + // await firebase_messaging.FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( + // alert: true, + // badge: true, + // sound: true, + // ); + // + // firebase_messaging.FirebaseMessaging.onMessage.listen((firebase_messaging.RemoteMessage message) { + // Log.d('FCM: onMessage'); + // onFirebaseMessage(message); + // }); + // + // firebase_messaging.FirebaseMessaging.onMessageOpenedApp.listen((firebase_messaging.RemoteMessage message) { + // Log.d('FCM: onMessageOpenedApp'); + // onFirebaseMessage(message); + // }); + // + // firebase_messaging.FirebaseMessaging.instance.getToken().then((String? token) => applyToken(token)); await super.initService(); } @override void initServiceUI() { - firebase_messaging.FirebaseMessaging.instance.getInitialMessage().then((message) { - if (message != null) { - processDataMessage(message.data); - } - }); + // firebase_messaging.FirebaseMessaging.instance.getInitialMessage().then((message) { + // if (message != null) { + // processDataMessage(message.data); + // } + // }); } @override @@ -97,40 +97,43 @@ class FirebaseMessaging with Service { } Future get authorizationStatus async { - firebase_messaging.NotificationSettings settings = await firebase_messaging.FirebaseMessaging.instance.getNotificationSettings(); - return _convertStatus(settings.authorizationStatus); + // firebase_messaging.NotificationSettings settings = await firebase_messaging.FirebaseMessaging.instance.getNotificationSettings(); + // return _covertStatus(settings.authorizationStatus); + return NotificationsAuthorizationStatus.notDetermined; } 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); - } + // 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); + // } + return false; } 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); + // 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); + return NotificationsAuthorizationStatus.notDetermined; } - 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; - } - } + // 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 @@ -155,25 +158,25 @@ class FirebaseMessaging with Service { // Message Processing - @protected - Future onFirebaseMessage(firebase_messaging.RemoteMessage message) async { - Log.d("FCM: onFirebaseMessage: $message"); - try { - if ((AppLifecycle.instance?.state == AppLifecycleState.resumed) && StringUtils.isNotEmpty(message.notification?.body)) { - NotificationService().notify(notifyForegroundMessage, { - "body": message.notification?.body, - "onComplete": () { - processDataMessage(message.data); - } - }); - } else { - processDataMessage(message.data); - } - } - catch(e) { - debugPrint(e.toString()); - } - } + // @protected + // Future onFirebaseMessage(firebase_messaging.RemoteMessage message) async { + // Log.d("FCM: onFirebaseMessage: $message"); + // try { + // if ((AppLifecycle.instance?.state == AppLifecycleState.resumed) && StringUtils.isNotEmpty(message.notification?.body)) { + // NotificationService().notify(notifyForegroundMessage, { + // "body": message.notification?.body, + // "onComplete": () { + // processDataMessage(message.data); + // } + // }); + // } else { + // processDataMessage(message.data); + // } + // } + // catch(e) { + // debugPrint(e.toString()); + // } + // } @protected diff --git a/pubspec.yaml b/pubspec.yaml index 2c5484942..1baefcdf7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: #Firebase firebase_core: ^2.13.0 firebase_crashlytics: ^3.3.1 - firebase_messaging: ^14.6.1 +# firebase_messaging: ^14.6.1 # This is breaking Airship notifications on iOS DO NOT RE-ENABLE #End Firebase dev_dependencies: From 2ca615cb90750e61731e3596471a26802f6c41ec Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 22 Jan 2024 15:02:40 -0600 Subject: [PATCH 124/177] check expiration in utc --- lib/model/auth2.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 33f922c23..75cbe3479 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -60,7 +60,7 @@ class Auth2Token { if (_parsedAccessToken == null) { return null; } - DateTime expirationDate = DateTime.fromMillisecondsSinceEpoch(0) + DateTime expirationDate = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true) .add(Duration(seconds: _parsedAccessToken?['exp']?.toInt())); return DateTime.now().isAfter(expirationDate); } From d11c3b01c5a6d2476edfe500e73d32578d51e0d0 Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 22 Jan 2024 16:48:23 -0600 Subject: [PATCH 125/177] add client version header to auth requests --- lib/service/auth2.dart | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 16eb6b08d..969a96188 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -409,7 +409,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 ?? '', }; Map postData = { 'auth_type': Auth2Type.typeAnonymous, @@ -454,7 +455,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; Map creds = {}; if (StringUtils.isNotEmpty(identifier)) { @@ -529,7 +531,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { requestJson?['response']['userHandle'] = StringUtils.base64UrlDecode(userHandle ?? ''); } Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; Map creds = { "response": JsonUtils.encode(requestJson), @@ -594,7 +597,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; Map postData = { 'auth_type': Auth2Type.typePasskey, @@ -680,7 +684,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if ((Config().authBaseUrl != null) && (responseData != null)) { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; Map postData = { 'auth_type': Auth2Type.typePasskey, @@ -779,7 +784,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (Config().authBaseUrl != null) { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; Map postData = { 'auth_type': oidcAuthType, @@ -938,7 +944,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; Map creds = {}; if (StringUtils.isNotEmpty(identifier)) { @@ -977,7 +984,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 ?? '', }; Map creds = { "code": code, @@ -1026,7 +1034,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; Map creds = { "password": password, @@ -1079,7 +1088,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 ?? '', }; Map postData = { 'auth_type': Auth2Type.typePassword, @@ -1583,6 +1593,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Future applyToken(Auth2Token token, { Map? params }) async { + return; //TODO: REMOVE THIS Auth2Token? oidcToken = (params != null) ? Auth2Token.fromJson(JsonUtils.mapValue(params['oidc_token'])) : null; _token = token; @@ -1600,7 +1611,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String url = "${Config().authBaseUrl}/auth/refresh"; Map headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Client-Version': Config().appVersion ?? '', }; String? post; if (!Config().isReleaseWeb) { From eac60126a360a5a4d1222c166f68c7e2ac6ef256 Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 22 Jan 2024 16:52:51 -0600 Subject: [PATCH 126/177] remove testing return --- lib/service/auth2.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 969a96188..8f8ef9b79 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1593,7 +1593,6 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected Future applyToken(Auth2Token token, { Map? params }) async { - return; //TODO: REMOVE THIS Auth2Token? oidcToken = (params != null) ? Auth2Token.fromJson(JsonUtils.mapValue(params['oidc_token'])) : null; _token = token; From d19ec0854d086d4d59773b435dd311c649f7e033 Mon Sep 17 00:00:00 2001 From: shurwit Date: Tue, 23 Jan 2024 10:10:29 -0600 Subject: [PATCH 127/177] not logged in without token --- lib/service/auth2.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 8f8ef9b79..6d912dcb6 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -306,11 +306,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { Auth2UserProfile? get profile => _account?.profile ?? _anonymousProfile; String? get loginType => _account?.authType?.code; - bool get isLoggedIn => (_account?.id != null); - bool get isOidcLoggedIn => (_account?.authType?.code == oidcAuthType || _account?.authType?.code == Auth2Type.typeOidc); - bool get isCodeLoggedIn => (_account?.authType?.code == Auth2Type.typeCode); - bool get isPasswordLoggedIn => (_account?.authType?.code == Auth2Type.typePassword); - bool get isPasskeyLoggedIn => (_account?.authType?.code == Auth2Type.typePasskey); + 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; From 49399d3fe7b1a4facd6549e33419621cc10976d8 Mon Sep 17 00:00:00 2001 From: shurwit Date: Tue, 23 Jan 2024 13:54:30 -0600 Subject: [PATCH 128/177] await apply token --- lib/service/auth2.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 6d912dcb6..27ea68e75 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1554,8 +1554,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _refreshTokenFailCounts.remove(futureKey); if (token == _token) { - // do not need to await applyToken because updated tokens are set in memory immediately - applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); + await applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); return responseToken; } else if (token == _anonymousToken) { From 5e70c408c0919ca109acbf523261472d2b9b1229 Mon Sep 17 00:00:00 2001 From: shurwit Date: Thu, 1 Feb 2024 13:28:54 -0600 Subject: [PATCH 129/177] check connectivity when resuming from background --- lib/service/connectivity.dart | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/service/connectivity.dart b/lib/service/connectivity.dart index 0cabf66e9..632a1af5a 100644 --- a/lib/service/connectivity.dart +++ b/lib/service/connectivity.dart @@ -18,13 +18,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 +51,9 @@ class Connectivity with Service { @override void createService() { + NotificationService().subscribe(this, [ + AppLifecycle.notifyStateChanged, + ]); _connectivitySubscription = connectivity.Connectivity().onConnectivityChanged.listen(_onConnectivityChanged); } @@ -71,6 +76,7 @@ class Connectivity with Service { @override void destroyService() { + NotificationService().unsubscribe(this); _connectivitySubscription?.cancel(); _connectivitySubscription = null; } @@ -105,6 +111,20 @@ class Connectivity with Service { 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; } From 1943d6e6940d8dd5288405c8f96b738a27e598c1 Mon Sep 17 00:00:00 2001 From: shurwit Date: Wed, 7 Feb 2024 15:20:43 -0600 Subject: [PATCH 130/177] expose errors for oidc authentication --- lib/service/auth2.dart | 63 ++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 27ea68e75..fe5dd2e44 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -743,8 +743,12 @@ 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; } } @@ -753,7 +757,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return completer.future; } - return Auth2OidcAuthenticateResult.failed; + return Auth2OidcAuthenticateResult(Auth2OidcAuthenticateResultStatus.failed, + error: "auth url is null"); } @protected @@ -770,8 +775,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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; @@ -780,7 +788,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } @protected - Future processOidcAuthentication(Uri? uri) async { + Future processOidcAuthentication(Uri? uri) async { if (Config().authBaseUrl != null) { String url = "${Config().authBaseUrl}/auth/login"; Map headers = { @@ -799,7 +807,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (additionalParams != null) { postData.addAll(additionalParams); } else { - return false; + return 'could not get config params'; } _oidcLogin = null; @@ -809,9 +817,15 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 != HttpStatus.ok) { + return '${response?.statusCode} - ${response?.body}'; + } + return 'invalid token or account response'; } - return false; + return 'auth url is null'; } @protected @@ -891,7 +905,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return null; } Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); - return _OidcLogin.fromJson(JsonUtils.decodeMap(response?.body)); + if (response?.statusCode == HttpStatus.ok) { + return _OidcLogin.fromJson(JsonUtils.decodeMap(response?.body)); + } else { + return _OidcLogin(error: '${response?.statusCode} - ${response?.body}'); + } } return null; } @@ -920,7 +938,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @protected void completeOidcAuthentication(Auth2OidcAuthenticateResult? result) { - _notifyLogin(oidcAuthType, result == Auth2OidcAuthenticateResult.succeeded); + _notifyLogin(oidcAuthType, result?.status == Auth2OidcAuthenticateResultStatus.succeeded); _oidcLogin = null; _oidcScope = null; @@ -1941,8 +1959,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { class _OidcLogin { final String? loginUrl; final Map? params; + final String? error; - _OidcLogin({this.loginUrl, this.params}); + _OidcLogin({this.loginUrl, this.params, this.error}); static _OidcLogin? fromJson(Map? json) { return (json != null) ? _OidcLogin( @@ -2142,17 +2161,27 @@ enum Auth2ForgotPasswordResult { // Auth2OidcAuthenticateResult -enum Auth2OidcAuthenticateResult { +class Auth2OidcAuthenticateResult { + Auth2OidcAuthenticateResultStatus status; + String? message; + String? error; + Auth2OidcAuthenticateResult(this.status, {this.message, this.error}); +} + +enum Auth2OidcAuthenticateResultStatus { succeeded, failed, failedAccountExist, } Auth2OidcAuthenticateResult auth2OidcAuthenticateResultFromAuth2LinkResult(Auth2LinkResult value) { - switch (value.status) { - case Auth2LinkResultStatus.succeeded: return Auth2OidcAuthenticateResult.succeeded; - case Auth2LinkResultStatus.failedAccountExist: return Auth2OidcAuthenticateResult.failedAccountExist; - default: return Auth2OidcAuthenticateResult.failed; + return Auth2OidcAuthenticateResult(auth2OidcAuthenticateResultStatusFromAuth2LinkResultStatus(value.status), message: value.message); +} +Auth2OidcAuthenticateResultStatus auth2OidcAuthenticateResultStatusFromAuth2LinkResultStatus(Auth2LinkResultStatus value) { + switch (value) { + case Auth2LinkResultStatus.succeeded: return Auth2OidcAuthenticateResultStatus.succeeded; + case Auth2LinkResultStatus.failedAccountExist: return Auth2OidcAuthenticateResultStatus.failedAccountExist; + default: return Auth2OidcAuthenticateResultStatus.failed; } } From 604b3b16b3f4355a51680feebd668f46e6b0c57f Mon Sep 17 00:00:00 2001 From: manavmodi Date: Thu, 15 Feb 2024 10:53:57 -0500 Subject: [PATCH 131/177] Added functionality to show or not show tap animation --- lib/ui/widgets/tab_bar.dart | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index 35c8b75ff..43fc09baa 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -131,6 +131,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({ @@ -140,19 +141,33 @@ 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 Material(color: Colors.transparent, - child: InkWell(onTap: () => onTap(this), child: + if(showAnimation) { + return Material(color: Colors.transparent, + child: InkWell(onTap: () => onTap(this), child: Column(children: [ selected ? buildSelectedIndicator(context) : Container(), buildTab(context), ]), - ), - ); + ), + ); + } + else { + return Material(color: Colors.transparent, + child: GestureDetector(onTap: () => onTap(this), child: + Column(children: [ + selected ? buildSelectedIndicator(context) : Container(), + buildTab(context), + ]), + ), + ); + } + } // Tab From 0d46899d363d5619e4962afd494d7b9869fbccb7 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 16 Feb 2024 09:47:53 -0600 Subject: [PATCH 132/177] remove dart:io dependency for web support --- lib/service/auth2.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index fe5dd2e44..0bfcd7549 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -264,7 +263,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; } @@ -820,7 +819,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (result) { return null; } - if (response?.statusCode != HttpStatus.ok) { + if (response?.statusCode != 200) { return '${response?.statusCode} - ${response?.body}'; } return 'invalid token or account response'; @@ -905,7 +904,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return null; } Response? response = await Network().post(url, headers: headers, body: JsonUtils.encode(postData), auth: Auth2Csrf()); - if (response?.statusCode == HttpStatus.ok) { + if (response?.statusCode == 200) { return _OidcLogin.fromJson(JsonUtils.decodeMap(response?.body)); } else { return _OidcLogin(error: '${response?.statusCode} - ${response?.body}'); @@ -1903,7 +1902,9 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { try { if ((urlStr != null)) { if (kIsWeb) { - FlutterWebAuth2.authenticate(url: urlStr, callbackUrlScheme: Uri.tryParse(urlStr)?.host ?? '').then((String url) { + FlutterWebAuth2.authenticate(url: urlStr, + callbackUrlScheme: DeepLink().appScheme ?? '' + ).then((String url) { onDeepLinkUri(Uri.tryParse(url)); }); } else if (await canLaunchUrlString(urlStr)) { @@ -2001,7 +2002,7 @@ class Auth2Csrf with NetworkAuthProvider { if (StringUtils.isNotEmpty(token?.accessToken)) { String tokenType = token!.tokenType ?? 'Bearer'; - headers[HttpHeaders.authorizationHeader] = "$tokenType ${token!.accessToken}"; + headers['Authorization'] = "$tokenType ${token!.accessToken}"; } return headers; } From 95a5e034e957a3b1020526b9b2736676887c91d9 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 16 Feb 2024 09:50:33 -0600 Subject: [PATCH 133/177] make oidc timeout configurable --- lib/service/auth2.dart | 2 +- lib/service/config.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 0bfcd7549..5f8ccd557 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -919,7 +919,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (_oidcAuthenticationTimer != null) { _oidcAuthenticationTimer!.cancel(); } - _oidcAuthenticationTimer = Timer(const Duration(milliseconds: 100), () { + _oidcAuthenticationTimer = Timer(Duration(milliseconds: Config().oidcAuthenticationTimeout), () { completeOidcAuthentication(null); _oidcAuthenticationTimer = null; }); diff --git a/lib/service/config.dart b/lib/service/config.dart index 400b8630b..452c54e93 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -590,6 +590,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { int get refreshTimeout => JsonUtils.intValue(settings['refreshTimeout']) ?? 0; int? get analyticsDeliveryTimeout => JsonUtils.intValue(settings['analyticsDeliveryTimeout']); int get refreshTokenRetriesCount => JsonUtils.intValue(settings['refreshTokenRetriesCount']) ?? 3; + int get oidcAuthenticationTimeout => JsonUtils.intValue(settings['oidcAuthenticationTimeout']) ?? 1000; String? get timezoneLocation => JsonUtils.stringValue(settings['timezoneLocation']) ?? 'America/Chicago'; // Getters: other From 1eb6919f02598e9370a64ad03bcfa2eacfcbbe63 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 16 Feb 2024 16:21:05 -0600 Subject: [PATCH 134/177] notify login change on web init refresh --- lib/service/auth2.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 5f8ccd557..62ab291c2 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -130,7 +130,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { refreshToken(ignoreUnauthorized: true).then((token) { if (token != null) { _refreshAccount(); - NotificationService().notify(Auth2.notifyLoginSucceeded, null); + NotificationService().notify(notifyLoginSucceeded, null); + NotificationService().notify(notifyLoginChanged); } }); } else { From 4dd18b5da6df2ff92a5e38f1f878442441d10181 Mon Sep 17 00:00:00 2001 From: shurwit Date: Thu, 22 Feb 2024 10:01:00 -0600 Subject: [PATCH 135/177] improve logging for auth and network, extend default oidc timeout --- example/pubspec.lock | 50 +++++++++++++++++++++++++++++----------- lib/service/auth2.dart | 9 +++++--- lib/service/config.dart | 2 +- lib/service/network.dart | 19 +++++++++++++++ 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 06080351b..ce2408100 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -697,6 +697,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -725,26 +749,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -805,10 +829,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: transitive description: @@ -1197,14 +1221,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - web: + vm_service: dependency: transitive description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "13.0.0" web_socket_channel: dependency: transitive description: @@ -1278,5 +1302,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 62ab291c2..c60e71b80 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -27,6 +27,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { static const String notifyLoginChanged = "edu.illinois.rokwire.auth2.login.changed"; static const String notifyLoginFinished = "edu.illinois.rokwire.auth2.login.finished"; static const String notifyLogoutStarted = "edu.illinois.rokwire.auth2.logout.started"; + static const String notifyRefreshFailed = "edu.illinois.rokwire.auth2.refresh.failed"; 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"; @@ -36,6 +37,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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"; @@ -902,7 +904,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { postData.addAll(additionalParams); postData['redirect_uri'] = oidcRedirectUrl; } else { - return null; + 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) { @@ -911,7 +913,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return _OidcLogin(error: '${response?.statusCode} - ${response?.body}'); } } - return null; + return _OidcLogin(error: 'auth url is null');; } @protected @@ -921,7 +923,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _oidcAuthenticationTimer!.cancel(); } _oidcAuthenticationTimer = Timer(Duration(milliseconds: Config().oidcAuthenticationTimeout), () { - completeOidcAuthentication(null); + completeOidcAuthentication(Auth2OidcAuthenticateResult(Auth2OidcAuthenticateResultStatus.failed, error: 'oidc login timeout')); _oidcAuthenticationTimer = null; }); } @@ -1583,6 +1585,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token?.refreshToken}"); + NotificationService().notify(notifyRefreshFailed, '${response?.statusCode} - ${response?.body}'); int refreshTokenFailCount = 1; if (futureKey.isNotEmpty) { refreshTokenFailCount += _refreshTokenFailCounts[futureKey] ?? 0; diff --git a/lib/service/config.dart b/lib/service/config.dart index 452c54e93..4184d3d23 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -590,7 +590,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { int get refreshTimeout => JsonUtils.intValue(settings['refreshTimeout']) ?? 0; int? get analyticsDeliveryTimeout => JsonUtils.intValue(settings['analyticsDeliveryTimeout']); int get refreshTokenRetriesCount => JsonUtils.intValue(settings['refreshTokenRetriesCount']) ?? 3; - int get oidcAuthenticationTimeout => JsonUtils.intValue(settings['oidcAuthenticationTimeout']) ?? 1000; + int get oidcAuthenticationTimeout => JsonUtils.intValue(settings['oidcAuthenticationTimeout']) ?? 3000; String? get timezoneLocation => JsonUtils.stringValue(settings['timezoneLocation']) ?? 'America/Chicago'; // Getters: other diff --git a/lib/service/network.dart b/lib/service/network.dart index 6829ac93d..728c22118 100644 --- a/lib/service/network.dart +++ b/lib/service/network.dart @@ -41,6 +41,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 @@ -104,6 +105,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } return null; } @@ -136,6 +138,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -155,6 +158,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -179,6 +183,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -198,6 +203,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -222,6 +228,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -241,6 +248,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -261,6 +269,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -280,6 +289,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -300,6 +310,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -318,6 +329,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -338,6 +350,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -350,6 +363,7 @@ class Network { catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } return null; } @@ -363,6 +377,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -386,6 +401,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { @@ -422,6 +438,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -436,6 +453,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } } return null; @@ -455,6 +473,7 @@ class Network { } catch (e) { Log.d(e.toString()); FirebaseCrashlytics().recordError(e, null); + NotificationService().notify(notifyHttpResponseError, e); } if (sendAnalytics) { From e999ef297372d6ea789047bf47ec75d52d7ddcd7 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 23 Feb 2024 14:44:03 -0600 Subject: [PATCH 136/177] add deeplink error logging --- lib/service/deep_link.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/service/deep_link.dart b/lib/service/deep_link.dart index daa65e50f..bd64fdeef 100644 --- a/lib/service/deep_link.dart +++ b/lib/service/deep_link.dart @@ -20,8 +20,9 @@ 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"; // Singleton Factory @@ -60,7 +61,13 @@ class DeepLink with Service { 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'); }); } From 1bbe66efe47a061f63753fa9815746c58f9ffe9c Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 23 Feb 2024 15:28:07 -0600 Subject: [PATCH 137/177] fix async gap context use --- lib/service/onboarding.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/service/onboarding.dart b/lib/service/onboarding.dart index bf054c9bb..9c5e5b59f 100644 --- a/lib/service/onboarding.dart +++ b/lib/service/onboarding.dart @@ -98,6 +98,9 @@ class Onboarding with Service implements NotificationsListener { void next(BuildContext context, OnboardingPanel panel, {bool replace = false}) { nextPanel(panel).then((dynamic nextPanel) { if (nextPanel is Widget) { + if (!context.mounted) { + return; + } if (replace) { Navigator.pushReplacement(context, CupertinoPageRoute( builder: (context) => nextPanel, From 2dd657a22a33a0dd294f7b5435b92feb5af7c57d Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Fri, 23 Feb 2024 16:22:51 -0600 Subject: [PATCH 138/177] add notifyLoginError notification to Auth service, check for oidcLoginInProgress before sending login request --- lib/service/auth2.dart | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index c60e71b80..60e60218f 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -26,6 +26,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 notifyRefreshFailed = "edu.illinois.rokwire.auth2.refresh.failed"; static const String notifyLogout = "edu.illinois.rokwire.auth2.logout"; @@ -51,6 +52,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _OidcLogin? _oidcLogin; Auth2AccountScope? _oidcScope; bool? _oidcLink; + bool _oidcLoginInProgress = false; List>? _oidcAuthenticationCompleters; bool? _processingOidcAuthentication; Timer? _oidcAuthenticationTimer; @@ -754,6 +756,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } } + _oidcLoginInProgress = true; Completer completer = Completer(); _oidcAuthenticationCompleters!.add(completer); return completer.future; @@ -764,10 +767,16 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } @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; @@ -923,7 +932,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _oidcAuthenticationTimer!.cancel(); } _oidcAuthenticationTimer = Timer(Duration(milliseconds: Config().oidcAuthenticationTimeout), () { - completeOidcAuthentication(Auth2OidcAuthenticateResult(Auth2OidcAuthenticateResultStatus.failed, error: 'oidc login timeout')); + NotificationService().notify(notifyLoginError, 'oidc login timeout'); + completeOidcAuthentication(null); _oidcAuthenticationTimer = null; }); } @@ -942,8 +952,8 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _notifyLogin(oidcAuthType, result?.status == Auth2OidcAuthenticateResultStatus.succeeded); - _oidcLogin = null; - _oidcScope = null; + // _oidcLogin = null; + // _oidcScope = null; _oidcLink = null; if (_oidcAuthenticationCompleters != null) { From 49e413d394cce2fde74d93515102bf52c3315f3b Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 23 Feb 2024 16:42:40 -0600 Subject: [PATCH 139/177] fix context usage in onboarding next --- lib/service/onboarding.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/service/onboarding.dart b/lib/service/onboarding.dart index 9c5e5b59f..6276128af 100644 --- a/lib/service/onboarding.dart +++ b/lib/service/onboarding.dart @@ -97,10 +97,10 @@ 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 (!context.mounted) { - return; - } if (replace) { Navigator.pushReplacement(context, CupertinoPageRoute( builder: (context) => nextPanel, From 12e6a36c9b5a80d6925a76089a3b42dc2c934e38 Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 26 Feb 2024 11:14:53 -0600 Subject: [PATCH 140/177] store service initialization state --- lib/service/config.dart | 6 +++--- lib/service/service.dart | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/service/config.dart b/lib/service/config.dart index 4184d3d23..571fd9aa7 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -575,9 +575,9 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { if (isReleaseWeb) { return '${html.window.location.origin}/$webServiceId'; } else if (isAdmin) { - return '$coreUrl/admin'; + return coreUrl != null ? '$coreUrl/admin': null; } - return '$coreUrl/services'; + return coreUrl != null ? '$coreUrl/services' : null; } // Getters: otherUniversityServices @@ -590,7 +590,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { int get refreshTimeout => JsonUtils.intValue(settings['refreshTimeout']) ?? 0; int? get analyticsDeliveryTimeout => JsonUtils.intValue(settings['analyticsDeliveryTimeout']); int get refreshTokenRetriesCount => JsonUtils.intValue(settings['refreshTokenRetriesCount']) ?? 3; - int get oidcAuthenticationTimeout => JsonUtils.intValue(settings['oidcAuthenticationTimeout']) ?? 3000; + int get oidcAuthenticationTimeout => JsonUtils.intValue(settings['oidcAuthenticationTimeout']) ?? 1000; String? get timezoneLocation => JsonUtils.stringValue(settings['timezoneLocation']) ?? 'America/Chicago'; // Getters: other diff --git a/lib/service/service.dart b/lib/service/service.dart index 544823149..432c23389 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -64,6 +64,12 @@ class Services { List? _services; + bool _initialized = false; + bool get initialized => _initialized; + + ServiceError? _initializeError; + ServiceError? get initializeError => _initializeError; + void create(List services) { if (_services == null) { _services = services; @@ -84,7 +90,11 @@ class Services { Future init() async { try { - return (_services != null) ? await _executeInitList(_buildInitList(_services!)) : null; + ServiceError? error = _initializeError = (_services != null) ? await _executeInitList(_buildInitList(_services!)) : null; + if (error == null) { + _initialized = true; + } + return error; } on ServiceError catch (error) { return error; From e62ed748ad42f5db3192db5e9d811a49631a536a Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 26 Feb 2024 12:14:57 -0600 Subject: [PATCH 141/177] replace uni_links with app_links --- example/pubspec.lock | 42 +++++++++++++++----------------------- lib/service/deep_link.dart | 7 ++++--- pubspec.yaml | 2 +- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index ce2408100..9d62b036b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted 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: @@ -577,6 +585,14 @@ packages: 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: @@ -1101,30 +1117,6 @@ 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: - dependency: transitive - description: - name: uni_links_platform_interface - sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - uni_links_web: - dependency: transitive - description: - name: uni_links_web - sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" - url: "https://pub.dev" - source: hosted - version: "0.1.0" universal_html: dependency: transitive description: @@ -1303,4 +1295,4 @@ packages: version: "6.3.0" sdks: dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.7.0" + flutter: ">=3.10.0" diff --git a/lib/service/deep_link.dart b/lib/service/deep_link.dart index bd64fdeef..ae2bd6f2c 100644 --- a/lib/service/deep_link.dart +++ b/lib/service/deep_link.dart @@ -14,10 +14,10 @@ * 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 { @@ -45,9 +45,10 @@ class DeepLink with 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'); @@ -57,7 +58,7 @@ class DeepLink with Service { // 2. Updated uri if (!kIsWeb) { - uriLinkStream.listen((Uri? uri) { + _appLinks.allUriLinkStream.listen((Uri? uri) { debugPrint('Received URI: $uri'); if (uri != null) { NotificationService().notify(notifyUri, uri); diff --git a/pubspec.yaml b/pubspec.yaml index 1baefcdf7..34a39b88d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: logger: ^1.1.0 connectivity_plus: ^3.0.0 - uni_links: ^0.5.1 + app_links: ^3.5.0 fluttertoast: ^8.2.2 path: ^1.8.2 gallery_saver: ^2.3.2 From c39addaaff81b0a7c3dea5161f7e07b647cabced Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 26 Feb 2024 15:17:13 -0600 Subject: [PATCH 142/177] fix app links stream listener --- lib/service/deep_link.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/deep_link.dart b/lib/service/deep_link.dart index ae2bd6f2c..d9f69f453 100644 --- a/lib/service/deep_link.dart +++ b/lib/service/deep_link.dart @@ -58,7 +58,7 @@ class DeepLink with Service { // 2. Updated uri if (!kIsWeb) { - _appLinks.allUriLinkStream.listen((Uri? uri) { + _appLinks.uriLinkStream.listen((Uri? uri) { debugPrint('Received URI: $uri'); if (uri != null) { NotificationService().notify(notifyUri, uri); From cf3adfb5cf334749c64be19dd76afc9790d2ed82 Mon Sep 17 00:00:00 2001 From: shurwit Date: Tue, 27 Feb 2024 11:26:08 -0600 Subject: [PATCH 143/177] improve refresh error notifications --- lib/service/auth2.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 60e60218f..cfb7ea57d 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -28,7 +28,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 notifyRefreshFailed = "edu.illinois.rokwire.auth2.refresh.failed"; + static const String notifyRefreshError = "edu.illinois.rokwire.auth2.refresh.error"; 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"; @@ -1595,7 +1595,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } _log("Auth2: failed to refresh token: ${response?.statusCode}\n${response?.body}\nSource Token: ${token?.refreshToken}"); - NotificationService().notify(notifyRefreshFailed, '${response?.statusCode} - ${response?.body}'); + NotificationService().notify(notifyRefreshError, '${response?.statusCode} - ${response?.body}'); int refreshTokenFailCount = 1; if (futureKey.isNotEmpty) { refreshTokenFailCount += _refreshTokenFailCounts[futureKey] ?? 0; @@ -1615,6 +1615,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { } catch(e) { debugPrint(e.toString()); + NotificationService().notify(notifyRefreshError, e); _refreshTokenFutures.remove(futureKey); // make sure to clear this in case something went wrong. } } From 6744409a3a4898e5f97ad5275cc91722384c45d3 Mon Sep 17 00:00:00 2001 From: shurwit Date: Tue, 27 Feb 2024 12:19:03 -0600 Subject: [PATCH 144/177] add refresh lifecycle notifications --- lib/service/auth2.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index cfb7ea57d..d70bd311e 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -28,7 +28,10 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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"; @@ -1553,6 +1556,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 { @@ -1564,7 +1568,13 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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; + 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}"); @@ -1585,10 +1595,14 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (token == _token) { await applyToken(responseToken, params: JsonUtils.mapValue(responseJson['params'])); + NotificationService().notify(notifyRefreshSucceeded); + NotificationService().notify(notifyRefreshFinished); return responseToken; } else if (token == _anonymousToken) { await Storage().setAuth2AnonymousToken(_anonymousToken = responseToken); + NotificationService().notify(notifyRefreshSucceeded); + NotificationService().notify(notifyRefreshFinished); return responseToken; } } @@ -1619,6 +1633,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { _refreshTokenFutures.remove(futureKey); // make sure to clear this in case something went wrong. } } + NotificationService().notify(notifyRefreshFinished); return null; } From b290b94047d5255d6162754316db6bb5bc2cd2b9 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 1 Mar 2024 14:20:21 -0600 Subject: [PATCH 145/177] remove access token decoding, improve privilege parsing and checks --- lib/model/auth2.dart | 252 ++++++++++++++++++++++++++++++++------- lib/service/auth2.dart | 16 --- lib/service/network.dart | 15 --- lib/service/storage.dart | 5 +- pubspec.yaml | 1 - 5 files changed, 216 insertions(+), 73 deletions(-) diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 75cbe3479..184e257b8 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -4,7 +4,6 @@ import 'dart:core'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; -import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:rokwire_plugin/service/app_datetime.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/utils/utils.dart'; @@ -17,17 +16,8 @@ class Auth2Token { final String? accessToken; final String? refreshToken; final String? tokenType; - late final Map? _parsedAccessToken; - - Auth2Token({this.accessToken, this.refreshToken, this.idToken, this.tokenType}) { - if (accessToken != null) { - try { - _parsedAccessToken = JwtDecoder.decode(accessToken!); - } catch (e) { - debugPrint(e.toString()); - } - } - } + + Auth2Token({this.accessToken, this.refreshToken, this.idToken, this.tokenType}); static Auth2Token? fromOther(Auth2Token? value, {String? idToken, String? accessToken, String? refreshToken, String? tokenType }) { return (value != null) ? Auth2Token( @@ -56,19 +46,6 @@ class Auth2Token { }; } - bool? get accessIsExpired { - if (_parsedAccessToken == null) { - return null; - } - DateTime expirationDate = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true) - .add(Duration(seconds: _parsedAccessToken?['exp']?.toInt())); - return DateTime.now().isAfter(expirationDate); - } - - dynamic getClaim(String claim) { - return _parsedAccessToken?[claim]; - } - @override bool operator ==(other) => (other is Auth2Token) && @@ -103,9 +80,9 @@ class Auth2Account { final Auth2UserProfile? profile; final Auth2UserPrefs? prefs; final Map secrets; - final List? permissions; - final List? roles; - final List? groups; + final List? permissions; + final List? roles; + final List? groups; final List? identifiers; final List? authTypes; final Map? systemConfigs; @@ -115,7 +92,7 @@ class Auth2Account { factory Auth2Account.fromOther(Auth2Account? other, {String? id, String? username, Auth2UserProfile? profile, Auth2UserPrefs? prefs, Map? secrets, - List? permissions, List? roles, List? groups, + List? permissions, List? roles, List? groups, List? identifiers, List? authTypes, Map? systemConfigs}) { return Auth2Account( id: id ?? other?.id, @@ -137,9 +114,9 @@ class Auth2Account { profile: Auth2UserProfile.fromJson(JsonUtils.mapValue(json['profile'])) ?? profile, prefs: Auth2UserPrefs.fromJson(JsonUtils.mapValue(json['preferences'])) ?? prefs, //TBD Auth2 secrets: JsonUtils.mapValue(json['secrets']) ?? {}, //TBD Auth2 - permissions: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['permissions'])), - roles: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['roles'])), - groups: Auth2StringEntry.listFromJson(JsonUtils.listValue(json['groups'])), + 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']), @@ -274,8 +251,33 @@ 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); @@ -590,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 @@ -1244,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) { @@ -1430,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!); diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index d70bd311e..599ef480f 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -288,14 +288,6 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { return false; } - @override - Future refreshNetworkAuthTokenIfExpired(dynamic token) async { - if (token is Auth2Token && token.accessIsExpired == true) { - return (await Auth2().refreshToken(token: token) != null); - } - return false; - } - // Getters Auth2Token? get token => _token ?? _anonymousToken; Auth2Token? get userToken => _token; @@ -2048,14 +2040,6 @@ class Auth2Csrf with NetworkAuthProvider { } return false; } - - @override - Future refreshNetworkAuthTokenIfExpired(dynamic token) async { - if (token is Auth2Token && token.accessIsExpired == true) { - return (await Auth2().refreshToken(token: token) != null); - } - return false; - } } // Auth2PasskeySignUpResult diff --git a/lib/service/network.dart b/lib/service/network.dart index 728c22118..d7dd646fb 100644 --- a/lib/service/network.dart +++ b/lib/service/network.dart @@ -32,7 +32,6 @@ abstract class NetworkAuthProvider { Map? get networkAuthHeaders; dynamic get networkAuthToken => null; Future refreshNetworkAuthTokenIfNeeded(http.BaseResponse? response, dynamic token) async => false; - Future refreshNetworkAuthTokenIfExpired(dynamic token) async => false; } class Network { @@ -148,8 +147,6 @@ class Network { http.Response? response; try { - await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); - response = await _get(url, headers: headers, body: body, encoding: encoding, auth: auth, client: client, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { @@ -193,8 +190,6 @@ class Network { http.Response? response; try { - await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); - response = await _post(url, body: body, encoding: encoding, headers: headers, client: client, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { @@ -238,8 +233,6 @@ class Network { http.Response? response; try { - await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); - response = await _put(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout, client: client); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { @@ -279,8 +272,6 @@ class Network { http.Response? response; try { - await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); - response = await _patch(url, body: body, encoding: encoding, headers: headers, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { @@ -319,8 +310,6 @@ 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 { - await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); - response = await _delete(url, body: body, encoding:encoding, headers: headers, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { @@ -391,8 +380,6 @@ class Network { http.StreamedResponse? response; try { - await auth?.refreshNetworkAuthTokenIfExpired(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, auth.networkAuthToken) == true) { @@ -463,8 +450,6 @@ class Network { http.Response? response; try { - await auth?.refreshNetworkAuthTokenIfExpired(auth.networkAuthToken); - response = await _head(url, headers: headers, auth: auth, timeout: timeout); if (await auth?.refreshNetworkAuthTokenIfNeeded(response, auth.networkAuthToken) == true) { diff --git a/lib/service/storage.dart b/lib/service/storage.dart index c0a57b7b5..5662d17e1 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -122,8 +122,11 @@ class Storage with Service { return await _secureStorage?.read(key: name) ?? defaultValue; } - Future setSecureStringWithName(String name, String? value) async { + 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); diff --git a/pubspec.yaml b/pubspec.yaml index 34a39b88d..ab079ecb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,6 @@ dependencies: pinch_zoom: ^2.0.0 graphql_flutter: ^5.1.2 native_flutter_proxy: ^0.1.15 - jwt_decoder: ^2.0.1 flutter_passkey: git: https://github.com/rokmetro/flutter_passkey.git From 2ed695d9789200295e0cf0ac74a0f8ebfdc8c4b3 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 1 Mar 2024 14:27:25 -0600 Subject: [PATCH 146/177] update pubspec lock --- example/pubspec.lock | 8 -------- 1 file changed, 8 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 9d62b036b..e2dad6d40 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -705,14 +705,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" - jwt_decoder: - dependency: transitive - description: - name: jwt_decoder - sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" - url: "https://pub.dev" - source: hosted - version: "2.0.1" leak_tracker: dependency: transitive description: From 72e09332e9e6dd5e02084435af087445c0568341 Mon Sep 17 00:00:00 2001 From: Ryan Oberlander Date: Mon, 4 Mar 2024 16:07:20 -0600 Subject: [PATCH 147/177] use updated recursive algorithm for Services.init, add initServiceFallback to algorithm, improve ServiceError reporting for missing dependencies and cycles --- lib/service/service.dart | 228 ++++++++++++++++++++++----------------- 1 file changed, 129 insertions(+), 99 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 432c23389..fcb43e60b 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -15,8 +15,9 @@ */ +import 'dart:async'; + import 'package:flutter/foundation.dart'; -import 'package:rokwire_plugin/utils/utils.dart'; abstract class Service { @@ -89,33 +90,25 @@ class Services { } Future init() async { - try { - ServiceError? error = _initializeError = (_services != null) ? await _executeInitList(_buildInitList(_services!)) : null; - if (error == null) { - _initialized = true; - } - return error; - } - on ServiceError catch (error) { - return error; + if (_services != null) { + ServiceError? se = await _ServicesInitializer(initService, initServiceFallback).process(_services!); + return se; } - - /*TMP: - return ServiceError( - source: null, - severity: ServiceErrorSeverity.fatal, - title: 'Text Initialization Error', - description: 'This is a test initialization error.', - );*/ + return null; } @protected - Future initFallback() async { + Future initFallback() async { + ServiceError? error; for (Service service in _services!) { if (service.isInitialized != true) { - await initServiceFallback(service); + error ??= await initServiceFallback(service); + if (error != null) { + return error; + } } } + return null; } @protected @@ -131,13 +124,14 @@ class Services { @protected - Future initServiceFallback(Service service) async { + Future initServiceFallback(Service service) async { try { await service.initServiceFallback(); } on ServiceError catch (error) { - debugPrint(error.toString()); + return error; } + return null; } void initUI() { @@ -155,111 +149,147 @@ class Services { } } } +} - @protected - List> _buildInitList(List servicesList) { - List> initList = >[]; // Ordered list of service pools as they should be initialized. - Set registered = Set.from(servicesList); // Pool of all registered services. Some services refer other services that are not registered so we need to ignore them. - Set processing = Set.from(registered); // Pool of services that are not scheduled for initialization yet. - Set processed = {}; // Pool of services that are scheduled for initialization. - - while (processing.isNotEmpty) { - Set currentListEntry = {}; - for (Service service in processing) { - if (_canProcessService(service, processed: processed, registered: registered)) /* (processed.containsAll(service.serviceDependsOn ?? {})) */ { - currentListEntry.add(service); // All service ancestors are already scheduled for initialization => we can initialize this service on the current step. +class _ServicesInitializer { + final Set _toDo = {}; + final Set _done = {}; + final Set _inProgress = {}; + + Future Function(Service service) initService; + Future Function(Service service) initServiceFallback; + Completer? _completer; + + _ServicesInitializer(this.initService, this.initServiceFallback); + + Future process(List services) async { + + for (Service service in services) { + (service.isInitialized ? _done : _toDo).add(service); + } + + if (_toDo.isNotEmpty) { + _completer = Completer(); + _run(); + return _completer!.future; + } + else { + return null; + } + } + + void _run() { + if (_toDo.isNotEmpty) { + for (Service service in _toDo) { + try { + if (_canStartService(service) && !_inProgress.contains(service)) { + _inProgress.add(service); + initService(service).then((ServiceError? error) async { + _inProgress.remove(service); + await _finishInitService(service, error); + }); + } + } + on ServiceError catch (error) { + _completer?.complete(error); + return; } } - if (currentListEntry.isNotEmpty) { - initList.add(currentListEntry); // Register current initialization step. - processed.addAll(currentListEntry); // Mark current initialization step services as scheduled for initialization. - processing.removeAll(currentListEntry); // Remove current initialization step services from the not scheduled yet pool. - } - else { - // There are services not scheduled for initialozation but we cannot retrieve any that depends only on the scheduled pool. - // This is an indication that processing contains a services that have dependancy cycle. - throw ServiceError( + if (_inProgress.isEmpty) { + _completer?.complete(ServiceError( source: null, severity: ServiceErrorSeverity.fatal, title: 'Services Initialization Error', - description: 'Service dependency cycle: ${_servicePoolToString(processing)}', - ); + description: 'Service dependency cycle detected: ${_findServiceCycle().join(', ')}.', + )); + _completer = null; } } - - return initList; + else { + _completer?.complete(null); + _completer = null; + } } - @protected - bool _canProcessService(Service service, { required Set processed, required Set registered}) { - Set? serviceAncestors = service.serviceDependsOn; - if ((serviceAncestors != null) && serviceAncestors.isNotEmpty) { - for (Service serviceAncestor in serviceAncestors) { - if (registered.contains(serviceAncestor) && !processed.contains(serviceAncestor)) { - return false; + Future _finishInitService(Service service, ServiceError? error, {bool tryFallback = true}) async { + if (_completer != null && !_completer!.isCompleted) { + if (error?.severity == ServiceErrorSeverity.fatal) { + if (tryFallback) { + _inProgress.add(service); + error = await initServiceFallback(service); + _inProgress.remove(service); + + _finishInitService(service, error, tryFallback: false); } + + _completer?.complete(error); + _completer = null; + } + else { + _done.add(service); + _toDo.remove(service); + _run(); } } - return true; } - @protected - Future _executeInitList(List> initList) async { - int initStep = 0; - ServiceError? result = null; - Set skipped = {}; - for (Iterable currentListEntry in initList) { - ServiceError? currentListError = await initServices(currentListEntry, skipped: skipped); - debugPrint("Services.init[${initStep++}] => ${_servicePoolToString(currentListEntry, marked: skipped)};"); - result ??= currentListError; + bool _canStartService(Service service) { + Set? serviceDependsOn = service.serviceDependsOn; + if ((serviceDependsOn == null) || serviceDependsOn.isEmpty) { + return true; } - return result; - } - @protected - Future initServices(Iterable services, { Set? skipped }) async { - if (services.isNotEmpty) { - - Set skippedServices = {}; - List> initFutures = >[]; - for (Service service in services) { - if (skipped?.containsAny(service.serviceDependsOn ?? {}) == true) { - // Service ancestor is skipped, invoke initServiceFallback and do not try to initialize it. - initFutures.add(initServiceFallback(service)); - skippedServices.add(service); - } - else { - // Service should be initialized. - initFutures.add(initService(service)); + for (Service dependency in serviceDependsOn) { + if (!_done.contains(dependency)) { + if (!_toDo.contains(dependency) && !_inProgress.contains(dependency)) { + // return with an error if a service depends on another service that is missing from the main initialization list (missing from _toDo, _inProgress, and _done) + throw ServiceError( + source: service, + severity: ServiceErrorSeverity.fatal, + title: 'Services Initialization Error', + description: 'Service dependency missing from initialization list: ${dependency.debugDisplayName}.', + ); } + return false; } + } + return true; + } + List _findServiceCycle() { + Map _firstUninitializedDependencies = {}; + // find first uninitialized dependency for each uninitialized service + for (Service toDo in _toDo) { try { - ServiceError? initError; - List initResults = await Future.wait(initFutures); - for (int index = 0; index < initResults.length; index++) { - dynamic initResult = initResults[index]; - if ((initResult is ServiceError) && (initResult.severity == ServiceErrorSeverity.fatal)) { - skipped?.add(services.elementAt(index)); // service initialization failed => do not attempt to initialize its ancestors - initError ??= initResult; - } + Service? dependency = toDo.serviceDependsOn?.firstWhere((dependency) => _toDo.contains(dependency) && !_done.contains(dependency)); + if (dependency != null) { + _firstUninitializedDependencies[toDo] = dependency; } - if (skippedServices.isNotEmpty) { - skipped?.addAll(skippedServices); // service initialization is skipped => do not attempt to initialize its ancestors + } + catch (error) { + if (error is! StateError) { + debugPrint(error.toString()); } - return initError; } - on ServiceError catch (error) { - return error; + } + + // traverse the uninitialized service graph to determine the cycle + int cycleLength = 0; + Map serviceVisits = Map.fromIterable(_toDo, value: (service) => 0); + Service next = _toDo.first; + while (cycleLength < 2 * _toDo.length) { // maximum possible cycle length is the number of uninitialized services (allow to visit at most twice) + if (_firstUninitializedDependencies[next] != null) { + next = _firstUninitializedDependencies[next]!; + if (serviceVisits[next] == 2) { + break; // if trying to visit a service that has been visited twice already, then the cycle must be the list of services visited twice + } + serviceVisits[next] = serviceVisits[next]! + 1; } + cycleLength++; } - return null; + return serviceVisits.keys.where((service) => serviceVisits[service] == 2).toList(); // all services visited twice are part of the cycle } - - @protected - String _servicePoolToString(Iterable pool, { Set? marked, String mark = '!' }) => - '[${pool.map((service) => "${service.runtimeType}${(marked?.contains(service) == true) ? mark : ''}").join(', ')}]'; } class ServiceError implements Exception { From 009d5a9c860338a3241309053804034bcbc33fbe Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 4 Mar 2024 17:15:44 -0600 Subject: [PATCH 148/177] prepare for merge --- lib/service/storage.dart | 120 ++++++++++++++-------- lib/service/styles.dart | 166 ++++++++++++++++--------------- lib/ui/widgets/scroll_pager.dart | 6 +- lib/ui/widgets/tab_bar.dart | 25 ++--- lib/utils/utils.dart | 3 +- 5 files changed, 175 insertions(+), 145 deletions(-) diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 5662d17e1..144e12d9c 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -18,8 +18,10 @@ 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'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; +import 'package:rokwire_plugin/utils/crypt.dart'; import 'package:rokwire_plugin/utils/utils.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -27,14 +29,14 @@ 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 _encryptionIVId = 'edu.illinois.rokwire.encryption.storage.iv'; + 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; + String? _encryptionKey; + String? _encryptionIV; // Singletone Factory @@ -62,8 +64,8 @@ class Storage with Service { 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); + _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, @@ -80,28 +82,54 @@ class Storage with Service { // description: 'Failed to initialize encryption keys.', // ); // } - else { - await super.initService(); - } + migrateEncryptedToSecureStorage(); + await super.initService(); } // Encryption - // String get encryptionKeyId => _ecryptionKeyId; + @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)) ? - // AESCrypt.encrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; - // } - // - // String? decrypt(String? value) { - // return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? - // AESCrypt.decrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; - // } + String? _encrypt(String? value) { + return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? + AESCrypt.encrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; + } + + String? _decrypt(String? value) { + return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? + AESCrypt.decrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; + } // Utilities @@ -134,30 +162,32 @@ class Storage with Service { NotificationService().notify(notifySettingChanged, name); } - // String? getEncryptedStringWithName(String name, {String? defaultValue}) { - // String? value = _sharedPreferences?.getString(name); - // if (value != null) { - // if ((_encryptionKey != null) && (_encryptionIV != null)) { - // value = decrypt(value); - // } - // else { - // value = null; - // } - // } - // return value ?? defaultValue; - // } - // - // void setEncryptedStringWithName(String name, String? value) { - // if (value != null) { - // if ((_encryptionKey != null) && (_encryptionIV != null)) { - // value = encrypt(value); - // _sharedPreferences?.setString(name, value!); - // } - // } else { - // _sharedPreferences?.remove(name); - // } - // NotificationService().notify(notifySettingChanged, name); - // } + @protected + String? getEncryptedStringWithName(String name, {String? defaultValue}) { + String? value = _sharedPreferences?.getString(name); + if (value != null) { + if ((_encryptionKey != null) && (_encryptionIV != null)) { + value = _decrypt(value); + } + else { + value = null; + } + } + return value ?? defaultValue; + } + + @protected + void setEncryptedStringWithName(String name, String? value) { + if (value != null) { + if ((_encryptionKey != null) && (_encryptionIV != null)) { + value = _encrypt(value); + _sharedPreferences?.setString(name, value!); + } + } else { + _sharedPreferences?.remove(name); + } + NotificationService().notify(notifySettingChanged, name); + } List? getStringListWithName(String name, {List? defaultValue}) { return _sharedPreferences?.getStringList(name) ?? defaultValue; @@ -245,6 +275,10 @@ class Storage with Service { } } + String get encryptedMigratedToSecureStorageKey => 'edu.illinois.rokwire.storage.encrypted.migrated'; + bool? get encryptedMigratedToSecureStorage => getBoolWithName(encryptedMigratedToSecureStorageKey); + set encryptedMigratedToSecureStorage(bool? value) => setBoolWithName(encryptedMigratedToSecureStorageKey, value); + // Config String get configEnvKey => 'edu.illinois.rokwire.config_environment'; diff --git a/lib/service/styles.dart b/lib/service/styles.dart index bca3b09ba..cebaa6166 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -427,115 +427,123 @@ class UiColors { return UiColors(colors); } - Color? get fillColorPrimary => colorMap['fillColorPrimary']; - Color? get fillColorPrimaryVariant => colorMap['fillColorPrimaryVariant']; - Color? get fillColorSecondary => colorMap['fillColorSecondary']; - Color? get fillColorSecondaryVariant => colorMap['fillColorSecondaryVariant']; - Color? get gradientColorPrimary => colorMap['gradientColorPrimary']; - - Color? get surface => colorMap['surface']; - Color? get surfaceAccent => colorMap['surfaceAccent']; - Color? get background => colorMap['background']; - Color? get backgroundVariant => colorMap['backgroundVariant']; - - Color? get textPrimary => colorMap['textPrimary']; - Color? get textAccent => colorMap['textAccent']; - Color? get textLight => colorMap['textLight']; - Color? get textMedium => colorMap['textMedium']; - Color? get textDark => colorMap['textDark']; - Color? get textDisabled => colorMap['textDisabled']; - - Color? get iconPrimary => colorMap['iconPrimary']; - Color? get iconLight => colorMap['iconLight']; - Color? get iconMedium => colorMap['iconMedium']; - Color? get iconDark => colorMap['iconDark']; - Color? get iconDisabled => colorMap['iconDisabled']; - - Color? get accentColor1 => colorMap['accentColor1']; - Color? get accentColor2 => colorMap['accentColor2']; - Color? get accentColor3 => colorMap['accentColor3']; - Color? get accentColor4 => colorMap['accentColor4']; - - Color? get dividerLine => colorMap['dividerLine']; - Color? get dividerLineAccent => colorMap['dividerLineAccent']; - - Color? get success => colorMap['success']; - Color? get alert => colorMap['alert']; - // DEPRECATED - @Deprecated("Transparency should be handled directly by widgets") + @Deprecated("Use AppColors instead") + Color? get fillColorPrimary => colorMap['fillColorPrimary']; + @Deprecated("Use AppColors instead") Color? get fillColorPrimaryTransparent03 => colorMap['fillColorPrimaryTransparent03']; - @Deprecated("Transparency should be handled directly by widgets") + @Deprecated("Use AppColors instead") Color? get fillColorPrimaryTransparent05 => colorMap['fillColorPrimaryTransparent05']; - @Deprecated("Transparency should be handled directly by widgets") + @Deprecated("Use AppColors instead") Color? get fillColorPrimaryTransparent09 => colorMap['fillColorPrimaryTransparent09']; - @Deprecated("Transparency should be handled directly by widgets") + @Deprecated("Use AppColors instead") Color? get fillColorPrimaryTransparent015 => colorMap['fillColorPrimaryTransparent015']; - @Deprecated("Transparency should be handled directly by widgets") + @Deprecated("Use AppColors instead") + Color? get textColorPrimary => colorMap['textColorPrimary']; + @Deprecated("Use AppColors instead") + Color? get textColorDisabled => colorMap['textColorDisabled']; + @Deprecated("Use AppColors instead") + Color? get fillColorPrimaryVariant => colorMap['fillColorPrimaryVariant']; + @Deprecated("Use AppColors instead") + Color? get textColorPrimaryVariant => colorMap['textColorPrimaryVariant']; + @Deprecated("Use AppColors instead") + Color? get fillColorSecondary => colorMap['fillColorSecondary']; + @Deprecated("Use AppColors instead") Color? get fillColorSecondaryTransparent05 => colorMap['fillColorSecondaryTransparent05']; - - @Deprecated("Set icon colors in styles asset file") - Color? get iconColor => colorMap['iconColor']; - - @Deprecated("Use 'textDark' instead") + @Deprecated("Use AppColors instead") + Color? get textColorSecondary => colorMap['textColorSecondary']; + @Deprecated("Use AppColors instead") + Color? get fillColorSecondaryVariant => colorMap['fillColorSecondaryVariant']; + @Deprecated("Use AppColors instead") + Color? get textColorSecondaryVariant => colorMap['textColorSecondaryVariant']; + @Deprecated("Use AppColors instead") + Color? get gradientColorPrimary => colorMap['gradientColorPrimary']; + + @Deprecated("Use AppColors instead") + Color? get surface => colorMap['surface']; + @Deprecated("Use AppColors instead") Color? get textSurface => colorMap['textSurface']; - @Deprecated("Use 'textDark' instead") + @Deprecated("Use AppColors instead") Color? get textSurfaceTransparent15 => colorMap['textSurfaceTransparent15']; - @Deprecated("Use 'textDark' instead") - Color? get textSurfaceAccent => colorMap['textSurfaceAccent']; - @Deprecated("Use 'textDark' instead") + @Deprecated("Use AppColors instead") + Color? get surfaceAccent => colorMap['surfaceAccent']; + @Deprecated("Use AppColors instead") Color? get surfaceAccentTransparent15 => colorMap['surfaceAccentTransparent15']; - @Deprecated("Use 'textDark' instead") + @Deprecated("Use AppColors instead") + Color? get textSurfaceAccent => colorMap['textSurfaceAccent']; + @Deprecated("Use AppColors instead") + Color? get background => colorMap['background']; + @Deprecated("Use AppColors instead") Color? get textBackground => colorMap['textBackground']; - @Deprecated("Use 'textDark' instead") + @Deprecated("Use AppColors instead") + Color? get backgroundVariant => colorMap['backgroundVariant']; + @Deprecated("Use AppColors instead") Color? get textBackgroundVariant => colorMap['textBackgroundVariant']; - @Deprecated("Use 'textPrimary' instead") + @Deprecated("Use AppColors instead") + Color? get textBackgroundVariant2 => colorMap['textBackgroundVariant2']; + @Deprecated("Use AppColors instead") Color? get headlineText => colorMap['headlineText']; - @Deprecated("Use 'textDisabled' instead") - Color? get disabledTextColor => colorMap['disabledTextColor']; - @Deprecated("Use 'textDisabled' instead") - Color? get disabledTextColorTwo => colorMap['disabledTextColorTwo']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") + Color? get accentColor1 => colorMap['accentColor1']; + @Deprecated("Use AppColors instead") + Color? get accentColor2 => colorMap['accentColor2']; + @Deprecated("Use AppColors instead") + Color? get accentColor3 => colorMap['accentColor3']; + @Deprecated("Use AppColors instead") + Color? get accentColor4 => colorMap['accentColor4']; + + @Deprecated("Use AppColors instead") + Color? get iconColor => colorMap['iconColor']; + + @Deprecated("Use AppColors instead") + Color? get eventColor => colorMap['eventColor']; + @Deprecated("Use AppColors instead") + Color? get diningColor => colorMap['diningColor']; + @Deprecated("Use AppColors instead") + Color? get placeColor => colorMap['placeColor']; + @Deprecated("Use AppColors instead") + Color? get mtdColor => colorMap['mtdColor']; + + @Deprecated("Use AppColors instead") Color? get white => colorMap['white']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get whiteTransparent01 => colorMap['whiteTransparent01']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get whiteTransparent06 => colorMap['whiteTransparent06']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get blackTransparent06 => colorMap['blackTransparent06']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get blackTransparent018 => colorMap['blackTransparent018']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get mediumGray => colorMap['mediumGray']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get mediumGray1 => colorMap['mediumGray1']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get mediumGray2 => colorMap['mediumGray2']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get lightGray => colorMap['lightGray']; + @Deprecated("Use AppColors instead") + Color? get disabledTextColor => colorMap['disabledTextColor']; + @Deprecated("Use AppColors instead") + Color? get disabledTextColorTwo => colorMap['disabledTextColorTwo']; + @Deprecated("Use AppColors instead") + Color? get dividerLine => colorMap['dividerLine']; + @Deprecated("Use AppColors instead") + Color? get dividerLineAccent => colorMap['dividerLineAccent']; - @Deprecated("Color style names should meaningfully reflect intended usage") + @Deprecated("Use AppColors instead") Color? get mango => colorMap['mango']; - @Deprecated("Application specific colors should be defined in the application") - Color? get eventColor => colorMap['eventColor']; - @Deprecated("Application specific colors should be defined in the application") - Color? get diningColor => colorMap['diningColor']; - @Deprecated("Application specific colors should be defined in the application") - Color? get placeColor => colorMap['placeColor']; - @Deprecated("Rename to 'transitColo', Application specific colors should be defined in the application") - Color? get mtdColor => colorMap['mtdColor']; - - @Deprecated("Application specific colors should be defined in the application") + @Deprecated("Use AppColors instead") Color? get saferLocationWaitTimeColorRed => colorMap['saferLocationWaitTimeColorRed']; - @Deprecated("Application specific colors should be defined in the application") + @Deprecated("Use AppColors instead") Color? get saferLocationWaitTimeColorYellow => colorMap['saferLocationWaitTimeColorYellow']; - @Deprecated("Application specific colors should be defined in the application") + @Deprecated("Use AppColors instead") Color? get saferLocationWaitTimeColorGreen => colorMap['saferLocationWaitTimeColorGreen']; - @Deprecated("Application specific colors should be defined in the application") + @Deprecated("Use AppColors instead") Color? get saferLocationWaitTimeColorGrey => colorMap['saferLocationWaitTimeColorGrey']; /// diff --git a/lib/ui/widgets/scroll_pager.dart b/lib/ui/widgets/scroll_pager.dart index 1ab26d48b..7fef39727 100644 --- a/lib/ui/widgets/scroll_pager.dart +++ b/lib/ui/widgets/scroll_pager.dart @@ -71,7 +71,7 @@ class ScrollPagerController { } void dispose() { - unregisterScrollController(); + deregisterScrollController(); } Future reset() async { @@ -84,7 +84,7 @@ class ScrollPagerController { void registerScrollController(ScrollController controller) { if (_scrollController != null && _scrollController != controller) { - unregisterScrollController(); + deregisterScrollController(); } controller.addListener(_scrollListener); _scrollController = controller; @@ -116,7 +116,7 @@ class ScrollPagerController { } } - void unregisterScrollController() { + void deregisterScrollController() { _scrollController?.removeListener(_scrollListener); _scrollController = null; } diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index 43fc09baa..8672570bb 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -147,27 +147,16 @@ class TabWidget extends StatelessWidget { @override Widget build(BuildContext context) { - if(showAnimation) { - return Material(color: Colors.transparent, - child: InkWell(onTap: () => onTap(this), child: - Column(children: [ + return Material(color: Colors.transparent, + child: InkWell( + onTap: () => onTap(this), + splashFactory: showAnimation ? null : NoSplash.splashFactory, + child: Column(children: [ selected ? buildSelectedIndicator(context) : Container(), buildTab(context), ]), - ), - ); - } - else { - return Material(color: Colors.transparent, - child: GestureDetector(onTap: () => onTap(this), child: - Column(children: [ - selected ? buildSelectedIndicator(context) : Container(), - buildTab(context), - ]), - ), - ); - } - + ), + ); } // Tab diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index c9826e0b6..eaa4dbf2f 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -18,7 +18,6 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; -import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; @@ -183,7 +182,7 @@ class StringUtils { if (isNumber) chars += '$number'; if (isSpecial) chars += '$special'; return List.generate(length, (index) { - final indexRandom = Random.secure().nextInt(chars.length); + final indexRandom = math.Random.secure().nextInt(chars.length); return chars [indexRandom]; }).join(''); } From 4b5303aeadde7199f51d0a176b39d55d70400382 Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 4 Mar 2024 21:44:14 -0600 Subject: [PATCH 149/177] fix merge issues --- example/pubspec.lock | 14 +- lib/gen/styles.dart | 352 ++++++++++++++++++------------------ lib/ui/widgets/tab_bar.dart | 5 +- tools/gen_styles.dart | 8 +- 4 files changed, 188 insertions(+), 191 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index e2dad6d40..e4becf7e2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -322,26 +322,26 @@ packages: dependency: transitive description: name: flutter_local_notifications - sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 + sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 url: "https://pub.dev" source: hosted - version: "12.0.4" + version: "16.3.2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "6af440e3962eeab8459602c309d7d4ab9e62f05d5cfe58195a28f846a0b5d523" + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0+1" flutter_native_timezone: dependency: transitive description: @@ -943,7 +943,7 @@ packages: path: ".." relative: true source: path - version: "1.5.0" + version: "1.7.0" rxdart: dependency: transitive description: diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index b10bc92f6..9cb6920fb 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -11,189 +11,189 @@ class AppThemes { } 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); + 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'; + 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); + 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"})); + 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/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index 7f8a89122..39d54401f 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -18,10 +18,7 @@ class TabBar extends StatefulWidget { _TabBarState createState() => _TabBarState(); @protected - Color? get backgroundColor => AppColors.surface ?? Colors.white; - - @protected - BoxBorder? get border => null; + Color? get backgroundColor => AppColors.surface; @protected Color? get environmentColor { diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index 72b297c39..fb3f219ef 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -22,10 +22,10 @@ Map typesMap = { }; Map refsMap = { - 'color': 'Styles().colors?.getColor(%key)', - 'text_style': 'Styles().textStyles?.getTextStyle(%key)', - 'font_family': 'Styles().fontFamilies?.fromCode(%key)', - 'image': 'Styles().images?.getImage(%key)', + 'color': 'Styles().colors.getColor(%key)', + 'text_style': 'Styles().textStyles.getTextStyle(%key)', + 'font_family': 'Styles().fontFamilies.fromCode(%key)', + 'image': 'Styles().images.getImage(%key)', 'themes': '%key', }; From c7ed508341cce51b9395cfa34e842580760f3aa4 Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 4 Mar 2024 22:04:45 -0600 Subject: [PATCH 150/177] cleanup --- CHANGELOG.md | 1 + android/src/main/AndroidManifest.xml | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a935b77c1..c44d0fd86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 131f2d25b..971a5481a 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,5 +1,3 @@ - - From 706021628d98ff2bf22149205ccdf37b11ff52cc Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 4 Mar 2024 22:07:48 -0600 Subject: [PATCH 151/177] fix styles reference --- lib/ui/widgets/popup_toast.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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)) ); From 20521055bd9d2684d0fdf7a47e987f633fbbdd96 Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 4 Mar 2024 22:13:23 -0600 Subject: [PATCH 152/177] re-enable firebase messaging --- example/pubspec.lock | 24 +++++ lib/service/firebase_messaging.dart | 139 ++++++++++++++-------------- pubspec.yaml | 2 +- 3 files changed, 93 insertions(+), 72 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index e4becf7e2..51c212667 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -281,6 +281,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.1" + firebase_messaging: + dependency: transitive + description: + name: firebase_messaging + sha256: "9cfe5c4560fb83393511ca7620f8fb3f22c9a80303052f10290e732fcfb801bd" + url: "https://pub.dev" + source: hosted + version: "14.6.1" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "7e25cb71019ccef8b1fd7b37969af79f04c467974cce4dfc291fa36974edd7ba" + url: "https://pub.dev" + source: hosted + version: "4.5.1" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "5d9840cc8126ea723b1bda901389cb542902f664f2653c16d4f8114e95f13cec" + url: "https://pub.dev" + source: hosted + version: "3.5.1" flutter: dependency: "direct main" description: flutter diff --git a/lib/service/firebase_messaging.dart b/lib/service/firebase_messaging.dart index 30ca71542..b3082d52d 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -17,7 +17,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; -// import 'package:firebase_messaging/firebase_messaging.dart' as firebase_messaging; +import 'package:firebase_messaging/firebase_messaging.dart' as firebase_messaging; import 'package:rokwire_plugin/service/app_lifecycle.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/inbox.dart'; @@ -61,34 +61,34 @@ class FirebaseMessaging with Service { @override Future initService() async { - // await firebase_messaging.FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( - // alert: true, - // badge: true, - // sound: true, - // ); - // - // firebase_messaging.FirebaseMessaging.onMessage.listen((firebase_messaging.RemoteMessage message) { - // Log.d('FCM: onMessage'); - // onFirebaseMessage(message); - // }); - // - // firebase_messaging.FirebaseMessaging.onMessageOpenedApp.listen((firebase_messaging.RemoteMessage message) { - // Log.d('FCM: onMessageOpenedApp'); - // onFirebaseMessage(message); - // }); - // - // firebase_messaging.FirebaseMessaging.instance.getToken().then((String? token) => applyToken(token)); + await firebase_messaging.FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( + alert: true, + badge: true, + sound: true, + ); + + firebase_messaging.FirebaseMessaging.onMessage.listen((firebase_messaging.RemoteMessage message) { + Log.d('FCM: onMessage'); + onFirebaseMessage(message); + }); + + firebase_messaging.FirebaseMessaging.onMessageOpenedApp.listen((firebase_messaging.RemoteMessage message) { + Log.d('FCM: onMessageOpenedApp'); + onFirebaseMessage(message); + }); + + firebase_messaging.FirebaseMessaging.instance.getToken().then((String? token) => applyToken(token)); await super.initService(); } @override void initServiceUI() { - // firebase_messaging.FirebaseMessaging.instance.getInitialMessage().then((message) { - // if (message != null) { - // processDataMessage(message.data); - // } - // }); + firebase_messaging.FirebaseMessaging.instance.getInitialMessage().then((message) { + if (message != null) { + processDataMessage(message.data); + } + }); } @override @@ -97,43 +97,40 @@ class FirebaseMessaging with Service { } Future get authorizationStatus async { - // firebase_messaging.NotificationSettings settings = await firebase_messaging.FirebaseMessaging.instance.getNotificationSettings(); - // return _covertStatus(settings.authorizationStatus); - return NotificationsAuthorizationStatus.notDetermined; + 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); - // } - return false; + 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); - return NotificationsAuthorizationStatus.notDetermined; + 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; - // } - // } + 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 @@ -158,25 +155,25 @@ class FirebaseMessaging with Service { // Message Processing - // @protected - // Future onFirebaseMessage(firebase_messaging.RemoteMessage message) async { - // Log.d("FCM: onFirebaseMessage: $message"); - // try { - // if ((AppLifecycle.instance?.state == AppLifecycleState.resumed) && StringUtils.isNotEmpty(message.notification?.body)) { - // NotificationService().notify(notifyForegroundMessage, { - // "body": message.notification?.body, - // "onComplete": () { - // processDataMessage(message.data); - // } - // }); - // } else { - // processDataMessage(message.data); - // } - // } - // catch(e) { - // debugPrint(e.toString()); - // } - // } + @protected + Future onFirebaseMessage(firebase_messaging.RemoteMessage message) async { + Log.d("FCM: onFirebaseMessage: $message"); + try { + if ((AppLifecycle.instance?.state == AppLifecycleState.resumed) && StringUtils.isNotEmpty(message.notification?.body)) { + NotificationService().notify(notifyForegroundMessage, { + "body": message.notification?.body, + "onComplete": () { + processDataMessage(message.data); + } + }); + } else { + processDataMessage(message.data); + } + } + catch(e) { + debugPrint(e.toString()); + } + } @protected diff --git a/pubspec.yaml b/pubspec.yaml index 8cadce3ff..4677d5bd7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: #Firebase firebase_core: ^2.13.0 firebase_crashlytics: ^3.3.1 -# firebase_messaging: ^14.6.1 # This is breaking Airship notifications on iOS DO NOT RE-ENABLE + firebase_messaging: ^14.6.1 #End Firebase dev_dependencies: From 25657f1c36a7764d27bb1982b2f3bf42754577d3 Mon Sep 17 00:00:00 2001 From: shurwit Date: Tue, 5 Mar 2024 10:46:09 -0600 Subject: [PATCH 153/177] use published font awesome plugin --- pubspec.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8cadce3ff..2e4190d47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,8 +47,8 @@ dependencies: 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 + font_awesome_flutter: ^10.6.0 # published plugin - uncomment if used +# font_awesome_flutter: '>= 4.7.0' # comment if published plugin is used flutter_passkey: git: https://github.com/rokmetro/flutter_passkey.git @@ -65,9 +65,9 @@ dev_dependencies: flutter_lints: ^1.0.0 # comment the override below if published plugin is used -dependency_overrides: - font_awesome_flutter: - path: plugins/font_awesome_flutter +#dependency_overrides: +# font_awesome_flutter: +# path: plugins/font_awesome_flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From fa029912cb311a4ad299499d24ca1ba0ce29d4fe Mon Sep 17 00:00:00 2001 From: shurwit Date: Tue, 5 Mar 2024 10:46:46 -0600 Subject: [PATCH 154/177] update pubspec lock --- example/pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index e4becf7e2..3240114fa 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -453,10 +453,10 @@ packages: dependency: transitive description: name: font_awesome_flutter - sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206" + sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f" url: "https://pub.dev" source: hosted - version: "10.4.0" + version: "10.7.0" gallery_saver: dependency: transitive description: From b5853ff576d482e9faae3d01fc1353028c510ad6 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 09:48:03 +0200 Subject: [PATCH 155/177] Await logout API call. --- lib/service/auth2.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 480a5e22c..0357fa813 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -1498,7 +1498,7 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { String? body = JsonUtils.encode({ 'all_sessions': false, }); - Network().post("${Config().authBaseUrl}/auth/logout", headers: headers, body: body, auth: Auth2Csrf(token: token)); + await Network().post("${Config().authBaseUrl}/auth/logout", headers: headers, body: body, auth: Auth2Csrf(token: token)); } await Future.wait([ From 2cab5bb201279d1bf1f7b3ea568d888dee141824 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 09:49:01 +0200 Subject: [PATCH 156/177] Make Storage.encrypt/decrypt APIs public again. --- lib/service/storage.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 38234a904..683c7f0d5 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -121,12 +121,12 @@ class Storage with Service { // String get encryptionIVId => _encryptionIVId; // String? get encryptionIV => _encryptionIV; - String? _encrypt(String? value) { + String? encrypt(String? value) { return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? AESCrypt.encrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; } - String? _decrypt(String? value) { + String? decrypt(String? value) { return ((value != null) && (_encryptionKey != null) && (_encryptionIV != null)) ? AESCrypt.decrypt(value, key: _encryptionKey, iv: _encryptionIV) : null; } @@ -167,7 +167,7 @@ class Storage with Service { String? value = _sharedPreferences?.getString(name); if (value != null) { if ((_encryptionKey != null) && (_encryptionIV != null)) { - value = _decrypt(value); + value = decrypt(value); } else { value = null; @@ -180,7 +180,7 @@ class Storage with Service { void setEncryptedStringWithName(String name, String? value) { if (value != null) { if ((_encryptionKey != null) && (_encryptionIV != null)) { - value = _encrypt(value); + value = encrypt(value); _sharedPreferences?.setString(name, value!); } } else { From 0e0dd216a1f3db3454e09b7631b987167f4453f4 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 11:24:51 +0200 Subject: [PATCH 157/177] Added StringUtils.notEmptyString helper. --- lib/utils/utils.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 310600f96..5b0c09d2a 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -36,14 +36,32 @@ 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; - 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); From 805d0835a71e06e5352c6da65d46f75f49a273fb Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 11:25:29 +0200 Subject: [PATCH 158/177] Added again email and phone getters in Auth2. --- lib/service/auth2.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index 0357fa813..acac67e6d 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -346,9 +346,11 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { 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 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 = []; From 0cf0252373a4e6e53813a0e77d2b9dab33e041f7 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 11:51:57 +0200 Subject: [PATCH 159/177] Added convinient status getters in Auth2OidcAuthenticateResult. --- lib/service/auth2.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/service/auth2.dart b/lib/service/auth2.dart index acac67e6d..f8ed93f6a 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -2187,6 +2187,10 @@ class Auth2OidcAuthenticateResult { String? message; String? error; Auth2OidcAuthenticateResult(this.status, {this.message, this.error}); + + bool get succeeded => (status == Auth2OidcAuthenticateResultStatus.succeeded); + bool get failed => (status == Auth2OidcAuthenticateResultStatus.failed); + bool get failedAccountExist => (status == Auth2OidcAuthenticateResultStatus.failedAccountExist); } enum Auth2OidcAuthenticateResultStatus { From 93b334eb9bbb245346e2d6ede2395a93d6a8520c Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 14:00:25 +0200 Subject: [PATCH 160/177] Fixed various warnings. --- lib/ui/panels/modal_image_panel.dart | 4 ++-- lib/ui/panels/rule_element_creation_panel.dart | 3 +-- lib/ui/panels/survey_creation_panel.dart | 3 +-- lib/ui/panels/survey_data_creation_panel.dart | 3 +-- lib/ui/panels/survey_data_default_response_panel.dart | 1 - lib/ui/panels/survey_data_options_panel.dart | 1 - lib/ui/panels/web_panel.dart | 1 - lib/ui/popups/popup_message.dart | 2 +- lib/ui/widget_builders/scroll_pager.dart | 2 +- lib/ui/widget_builders/survey.dart | 2 +- lib/ui/widgets/expandable_section.dart | 2 +- lib/ui/widgets/form_field.dart | 3 +-- lib/ui/widgets/rounded_button.dart | 2 +- lib/ui/widgets/survey.dart | 4 +--- tools/encrypt_util.dart | 10 +++++----- 15 files changed, 17 insertions(+), 26 deletions(-) diff --git a/lib/ui/panels/modal_image_panel.dart b/lib/ui/panels/modal_image_panel.dart index 91cdd1f78..5372e1c67 100644 --- a/lib/ui/panels/modal_image_panel.dart +++ b/lib/ui/panels/modal_image_panel.dart @@ -121,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: AppColors.textLight ?? Colors.white, fontFamily: AppFontFamilies.medium, fontSize: 50),), + Text('\u00D7', style: TextStyle(color: AppColors.textLight, fontFamily: AppFontFamilies.medium, fontSize: 50),), ), ) ); @@ -129,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 ?? AppColors.surface ?? Colors.white), + CircularProgressIndicator(strokeWidth: progressWidth, valueColor: AlwaysStoppedAnimation(progressColor ?? AppColors.surface), value: progress.expectedTotalBytes != null ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null), ); } diff --git a/lib/ui/panels/rule_element_creation_panel.dart b/lib/ui/panels/rule_element_creation_panel.dart index 70ac470d2..cdfa6da1e 100644 --- a/lib/ui/panels/rule_element_creation_panel.dart +++ b/lib/ui/panels/rule_element_creation_panel.dart @@ -24,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'; @@ -238,7 +237,7 @@ class _RuleElementCreationPanelState extends State { return Scaffold( appBar: const HeaderBar(title: "Edit Rule Element"), bottomNavigationBar: widget.tabBar, - backgroundColor: AppColors?.background, + backgroundColor: AppColors.background, body: SurveyElementCreationWidget(body: _buildRuleElement(), completionOptions: _buildDone(), scrollController: _scrollController,), ); } diff --git a/lib/ui/panels/survey_creation_panel.dart b/lib/ui/panels/survey_creation_panel.dart index 70154f1f4..6b7820a7a 100644 --- a/lib/ui/panels/survey_creation_panel.dart +++ b/lib/ui/panels/survey_creation_panel.dart @@ -22,7 +22,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/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'; @@ -183,7 +182,7 @@ class _SurveyCreationPanelState extends State { return Scaffold( appBar: HeaderBar(title: widget.survey != null ? "Update Survey" : "Create Survey"), bottomNavigationBar: widget.tabBar, - backgroundColor: AppColors?.background, + backgroundColor: AppColors.background, body: SurveyElementCreationWidget(body: _buildSurveyCreationTools(), completionOptions: _buildPreviewAndSave(), scrollController: _scrollController,), ); } diff --git a/lib/ui/panels/survey_data_creation_panel.dart b/lib/ui/panels/survey_data_creation_panel.dart index d4e5804c6..1768522c2 100644 --- a/lib/ui/panels/survey_data_creation_panel.dart +++ b/lib/ui/panels/survey_data_creation_panel.dart @@ -22,7 +22,6 @@ 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'; @@ -111,7 +110,7 @@ class _SurveyDataCreationPanelState extends State { return Scaffold( appBar: const HeaderBar(title: "Edit Survey Data"), bottomNavigationBar: widget.tabBar, - backgroundColor: AppColors?.background, + backgroundColor: AppColors.background, body: SurveyElementCreationWidget(body: _buildSurveyDataComponents(), completionOptions: _buildDone(), scrollController: _scrollController,), ); } diff --git a/lib/ui/panels/survey_data_default_response_panel.dart b/lib/ui/panels/survey_data_default_response_panel.dart index a60442273..1e1858be7 100644 --- a/lib/ui/panels/survey_data_default_response_panel.dart +++ b/lib/ui/panels/survey_data_default_response_panel.dart @@ -17,7 +17,6 @@ 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'; diff --git a/lib/ui/panels/survey_data_options_panel.dart b/lib/ui/panels/survey_data_options_panel.dart index 1fb569dd1..3097f1b84 100644 --- a/lib/ui/panels/survey_data_options_panel.dart +++ b/lib/ui/panels/survey_data_options_panel.dart @@ -22,7 +22,6 @@ 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/survey_data_default_response_panel.dart'; import 'package:rokwire_plugin/ui/widgets/form_field.dart'; import 'package:rokwire_plugin/ui/widgets/header_bar.dart'; diff --git a/lib/ui/panels/web_panel.dart b/lib/ui/panels/web_panel.dart index 6e80dc641..f0c3f8f5a 100644 --- a/lib/ui/panels/web_panel.dart +++ b/lib/ui/panels/web_panel.dart @@ -15,7 +15,6 @@ */ 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'; diff --git a/lib/ui/popups/popup_message.dart b/lib/ui/popups/popup_message.dart index 4f756ca3f..299b921b4 100644 --- a/lib/ui/popups/popup_message.dart +++ b/lib/ui/popups/popup_message.dart @@ -291,7 +291,7 @@ 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-light', defaultSpec: FontAwesomeImageSpec(type: 'fa.icon', source: '0xf057', size: 18.0, color: AppColors.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({ diff --git a/lib/ui/widget_builders/scroll_pager.dart b/lib/ui/widget_builders/scroll_pager.dart index 5a2ef9211..e97a6be20 100644 --- a/lib/ui/widget_builders/scroll_pager.dart +++ b/lib/ui/widget_builders/scroll_pager.dart @@ -30,7 +30,7 @@ class ScrollPagerBuilder { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - AppImages.retryMedium ?? Container(), + AppImages.retryMedium, const SizedBox(width: 8.0), Text(Localization().getStringEx('widget.scroll_pager.error.title', 'Something went wrong'), style: AppTextStyles.widgetMessageLightRegular), diff --git a/lib/ui/widget_builders/survey.dart b/lib/ui/widget_builders/survey.dart index 580297d3b..4a00f2dcc 100644 --- a/lib/ui/widget_builders/survey.dart +++ b/lib/ui/widget_builders/survey.dart @@ -55,7 +55,7 @@ class SurveyBuilder { children: [ Text(date ?? '', style: AppTextStyles.widgetDetailSmall), Container(width: 8.0), - AppImages.chevronRight ?? Container() + AppImages.chevronRight // UIIcon(IconAssets.chevronRight, size: 14.0, color: AppColors.headlineText), ], ), diff --git a/lib/ui/widgets/expandable_section.dart b/lib/ui/widgets/expandable_section.dart index e71904bc7..05f39216e 100644 --- a/lib/ui/widgets/expandable_section.dart +++ b/lib/ui/widgets/expandable_section.dart @@ -71,7 +71,7 @@ class ExpandableSectionState extends State { initiallyExpanded: widget.initiallyExpanded, title: widget.displayTitleWidget, subtitle: widget.displaySubtitleWidget, - trailing: Styles().images?.getImage(_expanded ? 'chevron-up' : 'chevron-down', + trailing: Styles().images.getImage(_expanded ? 'chevron-up' : 'chevron-down', defaultSpec: FontAwesomeImageSpec( type: 'fa.icon', source: _expanded ? '0xf077' : '0xf078', diff --git a/lib/ui/widgets/form_field.dart b/lib/ui/widgets/form_field.dart index 8f2018208..091346fe4 100644 --- a/lib/ui/widgets/form_field.dart +++ b/lib/ui/widgets/form_field.dart @@ -16,7 +16,6 @@ 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; @@ -78,7 +77,7 @@ class _FormFieldTextState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(width: 2, color: AppColors.fillColorPrimary ?? Colors.white) + borderSide: BorderSide(width: 2, color: AppColors.fillColorPrimary) ) ), controller: widget.controller, diff --git a/lib/ui/widgets/rounded_button.dart b/lib/ui/widgets/rounded_button.dart index b38801b79..1af27fe11 100644 --- a/lib/ui/widgets/rounded_button.dart +++ b/lib/ui/widgets/rounded_button.dart @@ -114,7 +114,7 @@ class RoundedButton extends StatefulWidget { @protected Widget get defaultTextWidget => Text(label, style: displayTextStyle, textAlign: textAlign,); @protected Widget get displayTextWidget => textWidget ?? defaultTextWidget; - @protected Color get defaultBorderColor => AppColors.fillColorSecondary ?? const Color(0xFF000000); + @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; diff --git a/lib/ui/widgets/survey.dart b/lib/ui/widgets/survey.dart index 9345fb74d..43599ba34 100644 --- a/lib/ui/widgets/survey.dart +++ b/lib/ui/widgets/survey.dart @@ -964,9 +964,7 @@ class SingleSelectionList extends StatelessWidget { clipBehavior: Clip.hardEdge, child: RadioListTile( title: Transform.translate(offset: const Offset(-15, 0), - child: Text(title, style: AppTextStyles.widgetTitleRegular ?? - TextStyle(fontFamily: AppFontFamilies.regular, - fontSize: 16, color: AppColors.textPrimary))), + child: Text(title, style: AppTextStyles.widgetTitleRegular)), activeColor: AppColors.fillColorSecondary, value: title, groupValue: selectedValue?.title, diff --git a/tools/encrypt_util.dart b/tools/encrypt_util.dart index 483ede9b2..ea6a29b8f 100644 --- a/tools/encrypt_util.dart +++ b/tools/encrypt_util.dart @@ -3,9 +3,9 @@ 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"; + //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; @@ -17,8 +17,8 @@ void main() async { // decryptAssetFile(encryptedFilepath, decryptedFile, encryptionKey, encryptionIV); // encryptAssetFile(decryptedFile, encryptedFilepath, encryptionKey, encryptionIV); - String tempFile = "plugin/tools/temp.enc"; - String contents = File(tempFile).readAsStringSync(); + //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)); From 2b5280f8aea2bdc2b2bb45c34cd4fb8aae5e1c8f Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 14:19:00 +0200 Subject: [PATCH 161/177] Fixed build error. --- tools/encrypt_util.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/encrypt_util.dart b/tools/encrypt_util.dart index ea6a29b8f..0c58e0c06 100644 --- a/tools/encrypt_util.dart +++ b/tools/encrypt_util.dart @@ -4,7 +4,7 @@ import 'package:encrypt/encrypt.dart' as encrypt_package; void main() async { //String encryptedFilepath = "assets/configs.json.enc"; - //String keysFilepath = "assets/config.keys.json"; + String keysFilepath = "assets/config.keys.json"; //String decryptedFile = "assets/configs.json"; Map? encryptionKeys = await loadEncryptionKeysFromAssets(keysFilepath); From 798468f39a26add7d080293c1e46c4410b457de7 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 15:19:59 +0200 Subject: [PATCH 162/177] Use AppColors and AppFontFamilies instead of Styles() members. --- lib/ui/widgets/ribbon_button.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index d57c63661..5dd298e24 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -65,8 +65,8 @@ class RibbonButton extends StatefulWidget { this.descriptionWidget, this.descriptionTextStyle, - this.descriptionTextColor, //= Styles().colors.textSurface - this.descriptionFontFamily, //= Styles().fontFamilies.regular + this.descriptionTextColor, //= AppColors.textSurface + this.descriptionFontFamily, //= AppFontFamilies.regular this.descriptionFontSize = 14.0, this.descriptionTextAlign = TextAlign.left, this.descriptionPadding = const EdgeInsets.only(top: 2), @@ -110,9 +110,9 @@ class RibbonButton extends StatefulWidget { @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 => AppColors.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 ?? Padding( From 43a745dd3b69315dcdc7f2cbbca69b64b53b5d84 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 15:24:08 +0200 Subject: [PATCH 163/177] Fixed unnecessary import warning. --- lib/service/connectivity.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/service/connectivity.dart b/lib/service/connectivity.dart index 632a1af5a..08fe97995 100644 --- a/lib/service/connectivity.dart +++ b/lib/service/connectivity.dart @@ -17,7 +17,6 @@ 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'; From e417c4c183047c0b5fdf2a4a43b45da0105225da Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 15:24:57 +0200 Subject: [PATCH 164/177] Added missing entries to AppFontColors. --- lib/gen/styles.dart | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index 9cb6920fb..0b26fd520 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -39,6 +39,40 @@ class AppColors { 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); + static Color get dividerLineAccent => Styles().colors.getColor('dividerLineAccent') ?? const Color(0xFFDADADA); + static Color get diningColor => Styles().colors.getColor('diningColor') ?? const Color(0xFFF09842); + static Color get eventColor => Styles().colors.getColor('diningColor') ?? const Color(0xFFE54B30); + static Color get disabledTextColor => Styles().colors.getColor('disabledTextColor') ?? const Color(0xFFBDBDBD); + static Color get disabledTextColorTwo => Styles().colors.getColor('disabledTextColorTwo') ?? const Color(0xFF868F9D); + static Color get white => Styles().colors.getColor('white') ?? const Color(0xFFFFFFFF); + static Color get whiteTransparent01 => Styles().colors.getColor('whiteTransparent01') ?? const Color(0x1AFFFFFF); + static Color get whiteTransparent06 => Styles().colors.getColor('whiteTransparent06') ?? const Color(0x99FFFFFF); + static Color get mediumGray1 => Styles().colors.getColor('mediumGray1') ?? const Color(0xFF535353); + static Color get mtdColor => Styles().colors.getColor('mtdColor') ?? const Color(0xFF2376E5); + static Color get iconColor => Styles().colors.getColor('iconColor') ?? const Color(0xFFE84A27); + static Color get saferLocationWaitTimeColorRed => Styles().colors.getColor('saferLocationWaitTimeColorRed') ?? const Color(0xFFFF0000); + static Color get saferLocationWaitTimeColorYellow => Styles().colors.getColor('saferLocationWaitTimeColorYellow') ?? const Color(0xFFFFFF00); + static Color get saferLocationWaitTimeColorGreen => Styles().colors.getColor('saferLocationWaitTimeColorGreen') ?? const Color(0xFF00FF00); + static Color get saferLocationWaitTimeColorGrey => Styles().colors.getColor('saferLocationWaitTimeColorGrey') ?? const Color(0xFF808080); + static Color get fillColorPrimaryTransparent03 => Styles().colors.getColor('fillColorPrimaryTransparent03') ?? const Color(0x4D002855); + static Color get fillColorPrimaryTransparent015 => Styles().colors.getColor('fillColorPrimaryTransparent015') ?? const Color(0x26002855); + static Color get fillColorSecondaryTransparent05 => Styles().colors.getColor('fillColorSecondaryTransparent05') ?? const Color(0x80E84A27); + static Color get blackTransparent06 => Styles().colors.getColor('blackTransparent06') ?? const Color(0x99000000); + static Color get blackTransparent018 => Styles().colors.getColor('blackTransparent018') ?? const Color(0x30000000); + static Color get mediumGray => Styles().colors.getColor('mediumGray') ?? const Color(0xFF717372); + static Color get mediumGray2 => Styles().colors.getColor('mediumGray2') ?? const Color(0xFF979797); + static Color get lightGray => Styles().colors.getColor('lightGray') ?? const Color(0xFFEDEDED); + static Color get textSurface => Styles().colors.getColor('textSurface') ?? const Color(0xFF404040); + static Color get textSurfaceAccent => Styles().colors.getColor('textSurfaceAccent') ?? const Color(0xFF404040); + static Color get surfaceAccentTransparent15 => Styles().colors.getColor('surfaceAccentTransparent15') ?? const Color(0x26DADDE1); + static Color get textBackground => Styles().colors.getColor('textBackground') ?? const Color(0xFF404040); + static Color get textBackgroundVariant2 => Styles().colors.getColor('textBackgroundVariant2') ?? const Color(0xFFE7E7E7); + static Color get textColorDisabled => Styles().colors.getColor('textColorDisabled') ?? const Color(0xFF5C5C5C); + static Color get essentialSkillsCoachRed => Styles().colors.getColor('essentialSkillsCoachRed') ?? const Color(0xFFE84A27); + static Color get essentialSkillsCoachOrange => Styles().colors.getColor('essentialSkillsCoachOrange') ?? const Color(0xFFF29936); + static Color get essentialSkillsCoachGreen => Styles().colors.getColor('essentialSkillsCoachGreen') ?? const Color(0xFF247C03); + static Color get essentialSkillsCoachBlue => Styles().colors.getColor('essentialSkillsCoachBlue') ?? const Color(0xFF479CCF); + static Color get essentialSkillsCoachPurple => Styles().colors.getColor('essentialSkillsCoachPurple') ?? const Color(0xFF602f8C); } class AppFontFamilies { From b64a8d21f0188f0ad774985a6f9d39442372d1eb Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 15:32:18 +0200 Subject: [PATCH 165/177] Undo adding missing entries to AppFontColors. --- lib/gen/styles.dart | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/lib/gen/styles.dart b/lib/gen/styles.dart index 0b26fd520..9cb6920fb 100644 --- a/lib/gen/styles.dart +++ b/lib/gen/styles.dart @@ -39,40 +39,6 @@ class AppColors { 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); - static Color get dividerLineAccent => Styles().colors.getColor('dividerLineAccent') ?? const Color(0xFFDADADA); - static Color get diningColor => Styles().colors.getColor('diningColor') ?? const Color(0xFFF09842); - static Color get eventColor => Styles().colors.getColor('diningColor') ?? const Color(0xFFE54B30); - static Color get disabledTextColor => Styles().colors.getColor('disabledTextColor') ?? const Color(0xFFBDBDBD); - static Color get disabledTextColorTwo => Styles().colors.getColor('disabledTextColorTwo') ?? const Color(0xFF868F9D); - static Color get white => Styles().colors.getColor('white') ?? const Color(0xFFFFFFFF); - static Color get whiteTransparent01 => Styles().colors.getColor('whiteTransparent01') ?? const Color(0x1AFFFFFF); - static Color get whiteTransparent06 => Styles().colors.getColor('whiteTransparent06') ?? const Color(0x99FFFFFF); - static Color get mediumGray1 => Styles().colors.getColor('mediumGray1') ?? const Color(0xFF535353); - static Color get mtdColor => Styles().colors.getColor('mtdColor') ?? const Color(0xFF2376E5); - static Color get iconColor => Styles().colors.getColor('iconColor') ?? const Color(0xFFE84A27); - static Color get saferLocationWaitTimeColorRed => Styles().colors.getColor('saferLocationWaitTimeColorRed') ?? const Color(0xFFFF0000); - static Color get saferLocationWaitTimeColorYellow => Styles().colors.getColor('saferLocationWaitTimeColorYellow') ?? const Color(0xFFFFFF00); - static Color get saferLocationWaitTimeColorGreen => Styles().colors.getColor('saferLocationWaitTimeColorGreen') ?? const Color(0xFF00FF00); - static Color get saferLocationWaitTimeColorGrey => Styles().colors.getColor('saferLocationWaitTimeColorGrey') ?? const Color(0xFF808080); - static Color get fillColorPrimaryTransparent03 => Styles().colors.getColor('fillColorPrimaryTransparent03') ?? const Color(0x4D002855); - static Color get fillColorPrimaryTransparent015 => Styles().colors.getColor('fillColorPrimaryTransparent015') ?? const Color(0x26002855); - static Color get fillColorSecondaryTransparent05 => Styles().colors.getColor('fillColorSecondaryTransparent05') ?? const Color(0x80E84A27); - static Color get blackTransparent06 => Styles().colors.getColor('blackTransparent06') ?? const Color(0x99000000); - static Color get blackTransparent018 => Styles().colors.getColor('blackTransparent018') ?? const Color(0x30000000); - static Color get mediumGray => Styles().colors.getColor('mediumGray') ?? const Color(0xFF717372); - static Color get mediumGray2 => Styles().colors.getColor('mediumGray2') ?? const Color(0xFF979797); - static Color get lightGray => Styles().colors.getColor('lightGray') ?? const Color(0xFFEDEDED); - static Color get textSurface => Styles().colors.getColor('textSurface') ?? const Color(0xFF404040); - static Color get textSurfaceAccent => Styles().colors.getColor('textSurfaceAccent') ?? const Color(0xFF404040); - static Color get surfaceAccentTransparent15 => Styles().colors.getColor('surfaceAccentTransparent15') ?? const Color(0x26DADDE1); - static Color get textBackground => Styles().colors.getColor('textBackground') ?? const Color(0xFF404040); - static Color get textBackgroundVariant2 => Styles().colors.getColor('textBackgroundVariant2') ?? const Color(0xFFE7E7E7); - static Color get textColorDisabled => Styles().colors.getColor('textColorDisabled') ?? const Color(0xFF5C5C5C); - static Color get essentialSkillsCoachRed => Styles().colors.getColor('essentialSkillsCoachRed') ?? const Color(0xFFE84A27); - static Color get essentialSkillsCoachOrange => Styles().colors.getColor('essentialSkillsCoachOrange') ?? const Color(0xFFF29936); - static Color get essentialSkillsCoachGreen => Styles().colors.getColor('essentialSkillsCoachGreen') ?? const Color(0xFF247C03); - static Color get essentialSkillsCoachBlue => Styles().colors.getColor('essentialSkillsCoachBlue') ?? const Color(0xFF479CCF); - static Color get essentialSkillsCoachPurple => Styles().colors.getColor('essentialSkillsCoachPurple') ?? const Color(0xFF602f8C); } class AppFontFamilies { From da4e4680ad96b6d57d4766f483ea0c093786ac39 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 7 Mar 2024 16:27:07 +0200 Subject: [PATCH 166/177] Removed unexisting references to AppColors entries. --- lib/ui/widgets/ribbon_button.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ui/widgets/ribbon_button.dart b/lib/ui/widgets/ribbon_button.dart index 5dd298e24..53ce1af64 100644 --- a/lib/ui/widgets/ribbon_button.dart +++ b/lib/ui/widgets/ribbon_button.dart @@ -65,7 +65,7 @@ class RibbonButton extends StatefulWidget { this.descriptionWidget, this.descriptionTextStyle, - this.descriptionTextColor, //= AppColors.textSurface + this.descriptionTextColor, //= Styles().colors.getColor('textSurface') this.descriptionFontFamily, //= AppFontFamilies.regular this.descriptionFontSize = 14.0, this.descriptionTextAlign = TextAlign.left, @@ -110,7 +110,7 @@ class RibbonButton extends StatefulWidget { @protected Widget get displayTextWidget => textWidget ?? Text(label ?? '', style: displayTextStyle, textAlign: textAlign,); @protected bool get hasDescription => StringUtils.isNotEmpty(description) || (descriptionWidget != null); - @protected Color? get defaultDescriptionTextColor => AppColors.textSurface; + @protected Color? get defaultDescriptionTextColor => Styles().colors.getColor('textSurface'); @protected Color? get displayDescriptionTextColor => descriptionTextColor ?? defaultDescriptionTextColor; @protected String? get defaultDescriptionFontFamily => AppFontFamilies.regular; @protected String? get displayDescriptionFontFamily => descriptionFontFamily ?? defaultDescriptionFontFamily; From e1ef8593d60ae75374c120ece7f7c36928f76774 Mon Sep 17 00:00:00 2001 From: shurwit Date: Thu, 7 Mar 2024 10:20:45 -0600 Subject: [PATCH 167/177] remove assets service --- lib/service/assets.dart | 231 ------------------------------- lib/ui/widgets/flex_content.dart | 6 - 2 files changed, 237 deletions(-) delete mode 100644 lib/service/assets.dart diff --git a/lib/service/assets.dart b/lib/service/assets.dart deleted file mode 100644 index f3119be62..000000000 --- a/lib/service/assets.dart +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:core'; -import 'dart:io'; -import 'dart:math'; -import 'dart:ui'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'package:rokwire_plugin/service/notification_service.dart'; -import 'package:rokwire_plugin/service/service.dart'; -import 'package:rokwire_plugin/service/config.dart'; -import 'package:rokwire_plugin/service/network.dart'; -import 'package:rokwire_plugin/service/app_lifecycle.dart'; -import 'package:rokwire_plugin/utils/utils.dart'; -import 'package:path/path.dart'; -import 'package:http/http.dart' as http; - -class Assets with Service implements NotificationsListener { - - static const String notifyChanged = "edu.illinois.rokwire.assets.changed"; - - static const String _assetsName = "assets.json"; - - Directory? _assetsDir; - DateTime? _pausedDateTime; - - Map? _defAssets; - Map? _appAssets; - Map? _netAssets; - Map? _assets; - - // Singletone Factory - - static Assets? _instance; - - static Assets? get instance => _instance; - - @protected - static set instance(Assets? value) => _instance = value; - - factory Assets() => _instance ?? (_instance = Assets.internal()); - - @protected - Assets.internal(); - - // Service - - @override - void createService() { - NotificationService().subscribe(this, AppLifecycle.notifyStateChanged); - } - - @override - void destroyService() { - NotificationService().unsubscribe(this); - } - - @override - Future initService() async { - _assetsDir = await getAssetsDir(); - _defAssets = await loadFromAssets(assetsKey); - _appAssets = await loadFromAssets(appAssetsKey); - _netAssets = await loadFromCache(netCacheFileName); - - if ((_defAssets != null) || (_appAssets != null) || (_netAssets != null)) { - build(); - updateFromNet(); - await super.initService(); - } - else { - throw ServiceError( - source: this, - severity: ServiceErrorSeverity.nonFatal, - title: 'Assets Initialization Failed', - description: 'Failed to initialize application assets content.', - ); - } - } - - @override - Set get serviceDependsOn { - return { Config() }; - } - - // NotificationsListener - - @override - void onNotification(String name, dynamic param) { - if (name == AppLifecycle.notifyStateChanged) { - _onAppLifecycleStateChanged(param); - } - } - - void _onAppLifecycleStateChanged(AppLifecycleState? state) { - if (state == AppLifecycleState.paused) { - _pausedDateTime = DateTime.now(); - } - else if (state == AppLifecycleState.resumed) { - if (_pausedDateTime != null) { - Duration pausedDuration = DateTime.now().difference(_pausedDateTime!); - if (Config().refreshTimeout < pausedDuration.inSeconds) { - updateFromNet(); - } - } - } - } - - // Assets - - dynamic operator [](dynamic key) { - return MapPathKey.entry(_assets, key); - } - - String? randomStringFromListWithKey(String key) { - List? list = JsonUtils.listValue(this[key]); - return ((list != null) && list.isNotEmpty) ? JsonUtils.stringValue(list[Random().nextInt(list.length)]) : null; - } - - // Implementation - - Future getAssetsDir() async { - Directory? assetsDir = Config().assetsCacheDir; - if ((assetsDir != null) && !await assetsDir.exists()) { - await assetsDir.create(recursive: true); - } - return assetsDir; - } - - @protected - String get assetsKey => 'assets/$_assetsName'; - - @protected - String get appAssetsKey => 'app/assets/$_assetsName'; - - @protected - Future?> loadFromAssets(String assetsKey) async { - try { return JsonUtils.decodeMap(await rootBundle.loadString(assetsKey)); } - catch(e) { debugPrint(e.toString()); } - return null; - } - - @protected - String get netCacheFileName => _assetsName; - - @protected - Future?> loadFromCache(String cacheFileName) async { - try { - if (_assetsDir != null) { - String cacheFilePath = join(_assetsDir!.path, cacheFileName); - File cacheFile = File(cacheFilePath); - if (await cacheFile.exists()) { - return JsonUtils.decodeMap(await cacheFile.readAsString()); - } - } - } - catch(e) { debugPrint(e.toString()); } - return null; - } - - @protected - Future saveToCache(String cacheFileName, String? content) async { - try { - if (_assetsDir != null) { - String cacheFilePath = join(_assetsDir!.path, cacheFileName); - File cacheFile = File(cacheFilePath); - if (content != null) { - cacheFile.writeAsString(content, flush: true); - } - else if (await cacheFile.exists()) { - await cacheFile.delete(); - } - } - } - catch(e) { debugPrint(e.toString()); } - } - - @protected - String get netAssetFileName => _assetsName; - - @protected - Future loadContentStringFromNet() async { - if (Config().assetsUrl != null) { - http.Response? response = await Network().get("${Config().assetsUrl}/$netAssetFileName"); - return (response?.statusCode == 200) ? response?.body : null; - } - return null; - } - - @protected - Future updateFromNet() async { - String? netAssetsString = await loadContentStringFromNet(); - Map? netAssets = JsonUtils.decodeMap(netAssetsString); - if (((netAssets != null) && !const DeepCollectionEquality().equals(netAssets, _netAssets)) || - ((netAssets == null) && (_netAssets != null))) - { - _netAssets = netAssets; - await saveToCache(netCacheFileName, netAssetsString); - build(); - NotificationService().notify(notifyChanged, null); - } - } - - @protected - void build() { - _assets = {}; - if (_defAssets != null) { - _assets!.addAll(_defAssets!); - } - if (_appAssets != null) { - _assets!.addAll(_appAssets!); - } - if (_netAssets != null) { - _assets!.addAll(_netAssets!); - } - } -} \ No newline at end of file diff --git a/lib/ui/widgets/flex_content.dart b/lib/ui/widgets/flex_content.dart index 40f8284f8..b7987dfdc 100644 --- a/lib/ui/widgets/flex_content.dart +++ b/lib/ui/widgets/flex_content.dart @@ -17,7 +17,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:rokwire_plugin/gen/styles.dart'; -import 'package:rokwire_plugin/service/assets.dart'; import 'package:rokwire_plugin/service/content.dart'; import 'package:rokwire_plugin/service/config.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; @@ -48,11 +47,6 @@ class FlexContent extends StatefulWidget { const FlexContent({Key? key, this.contentKey, this.contentJson, this.onClose}) : super(key: key); - static FlexContent? fromAssets(String contentKey, { void Function(BuildContext context)? onClose }) { - Map? jsonContent = JsonUtils.mapValue(Assets()[contentKey]); - return (jsonContent != null) ? FlexContent(contentKey: contentKey, contentJson: jsonContent, onClose: onClose) : null; - } - @override FlexContentWidgetState createState() => FlexContentWidgetState(); From a8502a829e6b2459f7357ded57685693847609a3 Mon Sep 17 00:00:00 2001 From: shurwit Date: Thu, 7 Mar 2024 10:24:52 -0600 Subject: [PATCH 168/177] use font awesome override --- pubspec.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index db0f96a20..9b7e2488c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,8 +47,8 @@ dependencies: 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 +# font_awesome_flutter: ^10.6.0 # published plugin - uncomment if used + font_awesome_flutter: '>= 4.7.0' # comment if published plugin is used flutter_passkey: git: https://github.com/rokmetro/flutter_passkey.git @@ -65,9 +65,9 @@ dev_dependencies: flutter_lints: ^1.0.0 # comment the override below if published plugin is used -#dependency_overrides: -# font_awesome_flutter: -# path: plugins/font_awesome_flutter +dependency_overrides: + font_awesome_flutter: + path: plugins/font_awesome_flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From dc73647500bdea9d77be4e766b1241cbf5c91663 Mon Sep 17 00:00:00 2001 From: shurwit Date: Thu, 7 Mar 2024 14:17:24 -0600 Subject: [PATCH 169/177] fix disabled tap animation --- lib/ui/widgets/tab_bar.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ui/widgets/tab_bar.dart b/lib/ui/widgets/tab_bar.dart index 39d54401f..007983f55 100644 --- a/lib/ui/widgets/tab_bar.dart +++ b/lib/ui/widgets/tab_bar.dart @@ -152,6 +152,8 @@ class TabWidget extends StatelessWidget { 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), From ed26c147962dc4ae852fbc55c7448d8e2f4b870c Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 8 Mar 2024 10:36:48 -0600 Subject: [PATCH 170/177] skip style.json comments in gen_styles --- assets/styles.json | 1 + tools/gen_styles.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/assets/styles.json b/assets/styles.json index ea60ee416..13037bda1 100644 --- a/assets/styles.json +++ b/assets/styles.json @@ -5,6 +5,7 @@ "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", diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index fb3f219ef..e48abd95f 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -200,6 +200,9 @@ String? _buildClass(String name, Map json) { String classString = "class $className {\n"; for (MapEntry entry in json.entries) { + if (entry.key.startsWith('_')) { + continue; + } String varName = camelCase(entry.key); String varRef = ref.replaceAll("%key", "'${entry.key}'"); String? defaultObj = defaultFuncs[name]?.call(name, entry, data: json); From 43b2c77fd4d04ba79605621a66510cef93e8b163 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 8 Mar 2024 13:10:16 -0600 Subject: [PATCH 171/177] preserve formatting of styles.json when merging in gen_styles --- pubspec.yaml | 2 +- tools/gen_styles.dart | 71 +++++++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2e4190d47..c5289fcc7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.7.0 homepage: environment: - sdk: ">=2.17.0 <3.0.0" + sdk: '>=3.0.0 <4.0.0' flutter: ">=2.5.0" dependencies: diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index e48abd95f..a37c73f47 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; @@ -82,33 +83,34 @@ void main(List arguments) async { String libPath = 'lib/'; String genFilepath = '${libPath}gen/styles.dart'; - Map? asset = await _loadFileJson(assetFilepath); + LinkedHashMap? asset = await _loadFileJson(assetFilepath); + if (asset == null) { + print('asset was not loaded'); + return; + } if (!skipPlugin) { print("merging plugin asset..."); - Map? pluginAsset = await _loadFileJson(pluginAssetFilepath); + LinkedHashMap? pluginAsset = await _loadFileJson(pluginAssetFilepath, preserveBlanks: false); 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; } } - if (asset != null) { - String fileString = _parseAsset(asset); - if (fileString.isNotEmpty) { - File(genFilepath).writeAsString(fileString); - print("saved generated code to $genFilepath"); - if (updateCode) { - _updateCodeRefs(libPath, genFilepath); - } + String fileString = _parseAsset(asset); + if (fileString.isNotEmpty) { + File(genFilepath).writeAsString(fileString); + print("saved generated code to $genFilepath"); + if (updateCode) { + _updateCodeRefs(libPath, genFilepath); } - } else { - print('asset was not loaded'); } } -String _prettyJsonEncode(Map jsonObject, {bool deepFormat = false}){ +String _prettyJsonEncode(LinkedHashMap jsonObject, {bool deepFormat = false}){ String out = '{'; bool first = true; for (MapEntry entry in jsonObject.entries) { @@ -117,13 +119,18 @@ String _prettyJsonEncode(Map jsonObject, {bool deepFormat = fal } out += '\n'; out += ' "${entry.key}": {'; - if (entry.value is Map) { + if (entry.value is LinkedHashMap) { bool firstSub = true; for (MapEntry subentry in entry.value.entries) { if (!firstSub) { out += ','; } - Map subMap = {}; + if (subentry.key.startsWith('_blank')) { + out += '\n'; + firstSub = true; + continue; + } + LinkedHashMap subMap = LinkedHashMap(); subMap.addEntries([subentry]); String valJson; if (entry.key == 'themes' || deepFormat) { @@ -150,21 +157,27 @@ String _prettyJsonEncode(Map jsonObject, {bool deepFormat = fal return out; } -Map _mergeJson(Map? from, Map? to) { - to ??= {}; - Map out = {}; - out.addAll(to); +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; for (MapEntry entry in section.value.entries ?? {}) { dynamic outSection = out[section.key]; if (!outSection.containsKey(entry.key)) { + if (!addedToSection) { + 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; } } } else { @@ -175,7 +188,7 @@ Map _mergeJson(Map? from, Map return out; } -String _parseAsset(Map asset) { +String _parseAsset(LinkedHashMap asset) { List classStrings = []; for (MapEntry entry in asset.entries) { if (entry.value is Map) { @@ -297,9 +310,13 @@ String _buildFile(List classStrings) { return fileString; } -Future?> _loadFileJson(String filepath) async { +Future?> _loadFileJson(String filepath, + {bool preserveBlanks = true}) async { try { - String content = File(filepath).readAsStringSync(); + String content = await File(filepath).readAsString(); + if (preserveBlanks) { + content = _addBlankPlaceholders(content); + } return json.decode(content); } catch (e) { print(e); @@ -307,6 +324,16 @@ Future?> _loadFileJson(String filepath) async { return null; } +String _addBlankPlaceholders(String content) { + List lines = content.split('\n'); + for (final (int index, String line) in lines.indexed) { + if (line.trim().isEmpty) { + lines[index] = '"_blank$index": "",'; + } + } + return lines.join('\n'); +} + void _updateCodeRefs(String libPath, String genFilepath) async { print('updating code references...'); final dir = Directory(libPath); From c55134b80a70130dfef87363bb049ff156a17bc1 Mon Sep 17 00:00:00 2001 From: shurwit Date: Fri, 8 Mar 2024 16:30:10 -0600 Subject: [PATCH 172/177] fix mixin classes, gen styles handle comments and merge blanks --- lib/model/auth2.dart | 2 +- lib/model/explore.dart | 2 +- lib/service/network.dart | 2 +- lib/service/notification_service.dart | 2 +- lib/service/onboarding.dart | 2 +- lib/service/service.dart | 2 +- tools/gen_styles.dart | 56 ++++++++++++++++++++------- 7 files changed, 47 insertions(+), 21 deletions(-) diff --git a/lib/model/auth2.dart b/lib/model/auth2.dart index 2e5190b18..fe980c5b7 100644 --- a/lib/model/auth2.dart +++ b/lib/model/auth2.dart @@ -2333,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/service/network.dart b/lib/service/network.dart index d7dd646fb..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; diff --git a/lib/service/notification_service.dart b/lib/service/notification_service.dart index 0497e8f28..c5f5db430 100644 --- a/lib/service/notification_service.dart +++ b/lib/service/notification_service.dart @@ -109,6 +109,6 @@ class NotificationService { } -abstract class NotificationsListener { +abstract mixin class NotificationsListener { FutureOr onNotification(String name, dynamic param); } diff --git a/lib/service/onboarding.dart b/lib/service/onboarding.dart index 6276128af..dfa05d1f5 100644 --- a/lib/service/onboarding.dart +++ b/lib/service/onboarding.dart @@ -161,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/service.dart b/lib/service/service.dart index fcb43e60b..022846ad8 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -19,7 +19,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -abstract class Service { +abstract mixin class Service { bool? _isInitialized; diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index a37c73f47..3dc56d5cd 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -90,7 +90,7 @@ void main(List arguments) async { } if (!skipPlugin) { print("merging plugin asset..."); - LinkedHashMap? pluginAsset = await _loadFileJson(pluginAssetFilepath, preserveBlanks: false); + LinkedHashMap? pluginAsset = await _loadFileJson(pluginAssetFilepath); if (pluginAsset != null) { asset = _mergeJson(pluginAsset, asset); File(assetFilepath).writeAsString(_prettyJsonEncode(asset)); @@ -168,9 +168,18 @@ LinkedHashMap _mergeJson(LinkedHashMap? from, 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 (entry.key.startsWith('_blank')) { + if (!addedToSection || lastBlank != null) { + continue; + } + lastBlank = entry.key; + } else { + lastBlank = null; + } if (!addedToSection) { 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.'; @@ -180,6 +189,12 @@ LinkedHashMap _mergeJson(LinkedHashMap? from, addedToSection = true; } } + if (lastBlank != null) { + dynamic outSection = out[section.key]; + if (outSection is Map) { + outSection.remove(lastBlank); + } + } } else { print("unexpected section type: ${section.value}"); } @@ -211,11 +226,26 @@ String? _buildClass(String name, Map json) { 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); @@ -223,6 +253,9 @@ String? _buildClass(String name, Map json) { 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; } @@ -310,13 +343,16 @@ String _buildFile(List classStrings) { return fileString; } -Future?> _loadFileJson(String filepath, - {bool preserveBlanks = true}) async { +Future?> _loadFileJson(String filepath) async { try { String content = await File(filepath).readAsString(); - if (preserveBlanks) { - content = _addBlankPlaceholders(content); + 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); @@ -324,16 +360,6 @@ Future?> _loadFileJson(String filepath, return null; } -String _addBlankPlaceholders(String content) { - List lines = content.split('\n'); - for (final (int index, String line) in lines.indexed) { - if (line.trim().isEmpty) { - lines[index] = '"_blank$index": "",'; - } - } - return lines.join('\n'); -} - void _updateCodeRefs(String libPath, String genFilepath) async { print('updating code references...'); final dir = Directory(libPath); From bd9a5a833f3f78dfdf6f722bf968af89aebb6d44 Mon Sep 17 00:00:00 2001 From: shurwit Date: Mon, 11 Mar 2024 21:32:15 -0500 Subject: [PATCH 173/177] fix style generation merging for missing sections --- tools/gen_styles.dart | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tools/gen_styles.dart b/tools/gen_styles.dart index 3dc56d5cd..86abbe6e6 100644 --- a/tools/gen_styles.dart +++ b/tools/gen_styles.dart @@ -110,18 +110,22 @@ void main(List arguments) async { } } -String _prettyJsonEncode(LinkedHashMap jsonObject, {bool deepFormat = false}){ +String _prettyJsonEncode(LinkedHashMap jsonObject, {bool deepFormat = false}){ String out = '{'; bool first = true; - for (MapEntry entry in jsonObject.entries) { + 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) { + if (entry.value is LinkedHashMap || entry.key == 'themes') { bool firstSub = true; - for (MapEntry subentry in entry.value.entries) { + for (dynamic subentry in entry.value.entries) { if (!firstSub) { out += ','; } @@ -130,7 +134,7 @@ String _prettyJsonEncode(LinkedHashMap jsonObject, {bool deepFo firstSub = true; continue; } - LinkedHashMap subMap = LinkedHashMap(); + LinkedHashMap subMap = LinkedHashMap(); subMap.addEntries([subentry]); String valJson; if (entry.key == 'themes' || deepFormat) { @@ -172,17 +176,21 @@ LinkedHashMap _mergeJson(LinkedHashMap? from, for (MapEntry entry in section.value.entries ?? {}) { dynamic outSection = out[section.key]; if (!outSection.containsKey(entry.key)) { - if (entry.key.startsWith('_blank')) { - if (!addedToSection || lastBlank != null) { - continue; + 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.'; } - lastBlank = entry.key; - } else { - lastBlank = null; - } - if (!addedToSection) { - 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; @@ -345,7 +353,7 @@ String _buildFile(List classStrings) { Future?> _loadFileJson(String filepath) async { try { - String content = await File(filepath).readAsString(); + 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) { From b596e8f38f67977b4b9c9e702e336b0782b785d6 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Tue, 12 Mar 2024 10:37:16 +0200 Subject: [PATCH 174/177] Tuned services initialization, get rid of initServiceFallback [#408]. --- CHANGELOG.md | 1 + lib/service/analytics.dart | 49 +++++--- lib/service/auth2.dart | 59 ++++----- lib/service/config.dart | 114 +++++++----------- lib/service/events.dart | 5 - lib/service/events2.dart | 5 - lib/service/firebase_messaging.dart | 6 +- lib/service/flex_ui.dart | 79 +++++++----- lib/service/geo_fence.dart | 2 +- lib/service/groups.dart | 2 +- lib/service/http_proxy.dart | 15 +-- lib/service/localization.dart | 37 +++++- lib/service/service.dart | 180 ++++++++-------------------- lib/service/storage.dart | 6 - lib/service/styles.dart | 88 ++++++++------ 15 files changed, 281 insertions(+), 367 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c44d0fd86..e0fea7fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) diff --git a/lib/service/analytics.dart b/lib/service/analytics.dart index c0c31c500..4860578b7 100644 --- a/lib/service/analytics.dart +++ b/lib/service/analytics.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/auth2.dart b/lib/service/auth2.dart index 480a5e22c..f350672fa 100644 --- a/lib/service/auth2.dart +++ b/lib/service/auth2.dart @@ -124,40 +124,6 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { @override Future initService() async { - await _initServiceOffline(); - - if (isAnonymousAuthenticationSupported && ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid)) { - if (!await authenticateAnonymously()) { - throw ServiceError( - source: this, - severity: ServiceErrorSeverity.fatal, - title: 'Authentication Initialization Failed', - description: 'Failed to initialize anonymous authentication token.', - ); - } - } - - 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(); - } - - @override - Future initServiceFallback() async { - await _initServiceOffline(); - } - - Future _initServiceOffline() async { _anonymousId = Storage().auth2AnonymousId; List> futures = [ @@ -200,6 +166,31 @@ class Auth2 with Service, NetworkAuthProvider implements NotificationsListener { if (futures.isNotEmpty) { await Future.wait(futures); } + + if (isAnonymousAuthenticationSupported && ((_anonymousId == null) || (_anonymousToken == null) || !_anonymousToken!.isValid)) { + if (!await authenticateAnonymously()) { + throw ServiceError( + source: this, + severity: ServiceErrorSeverity.fatal, + title: 'Authentication Initialization Failed', + description: 'Failed to initialize anonymous authentication token.', + ); + } + } + + 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(); } @override diff --git a/lib/service/config.dart b/lib/service/config.dart index 417f291b4..c059d6364 100644 --- a/lib/service/config.dart +++ b/lib/service/config.dart @@ -23,7 +23,6 @@ import 'package:flutter/services.dart' show rootBundle; 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/connectivity.dart'; import 'package:rokwire_plugin/service/log.dart'; import 'package:rokwire_plugin/service/notification_service.dart'; import 'package:rokwire_plugin/service/service.dart'; @@ -41,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"; @@ -94,7 +92,7 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { @override Future initService() async { - _configEnvironment = configEnvFromString(Storage().configEnvironment) ?? _defaultConfigEnvironment ?? defaultConfigEnvironment; + _configEnvironment = _defaultConfigEnvironment ?? defaultConfigEnvironment; _packageInfo = await PackageInfo.fromPlatform(); if (!kIsWeb) { @@ -102,15 +100,52 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { Log.d('Application Documents Directory: ${_appDocumentsDir!.path}'); } - await init(); + 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 super.initService(); } - @override - Set get serviceDependsOn { - return { Storage(), Connectivity() }; - } - // NotificationsListener @override @@ -296,52 +331,6 @@ class Config with Service, NetworkAuthProvider, NotificationsListener { return null; } - @protected - Future init() async { - 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(); - } - } - @protected Future updateFromNet() async { String? configString = await loadAsStringFromNet(); @@ -489,21 +478,6 @@ 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; 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_messaging.dart b/lib/service/firebase_messaging.dart index 30ca71542..2fc1c55b2 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -15,17 +15,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_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'; @@ -93,7 +89,7 @@ class FirebaseMessaging with Service { @override Set get serviceDependsOn { - return { FirebaseCore(), Storage(), Config() }; + return { FirebaseCore() }; } Future get authorizationStatus async { diff --git a/lib/service/flex_ui.dart b/lib/service/flex_ui.dart index 55b4e8b6c..519d650f9 100644 --- a/lib/service/flex_ui.dart +++ b/lib/service/flex_ui.dart @@ -69,7 +69,8 @@ class FlexUI with Service implements NotificationsListener { @override void createService() { - NotificationService().subscribe(this,[ + NotificationService().subscribe(this, [ + Service.notifyInitialized, Auth2.notifyPrefsChanged, Auth2.notifyUserDeleted, Auth2UserPrefs.notifyRolesChanged, @@ -90,7 +91,25 @@ class FlexUI with Service implements NotificationsListener { @override Future initService() async { - await _initServiceOffline(); + 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(); @@ -105,44 +124,40 @@ class FlexUI with Service implements NotificationsListener { } } - @override - Future initServiceFallback() async { - await _initServiceOffline(); - } - - Future _initServiceOffline() async { - _defContentSource = await loadFromAssets(assetsKey); - _appContentSource = kIsWeb ? null : await loadFromAssets(appAssetsKey); - if (!kIsWeb) { - _assetsDir = await getAssetsDir(); - _netContentSource = await loadFromCache(netCacheFileName); - } - build(); - } - - @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 == AppLifecycle.notifyStateChanged) { - onAppLifecycleStateChanged(param); + 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(); + } } } diff --git a/lib/service/geo_fence.dart b/lib/service/geo_fence.dart index 453118ceb..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 diff --git a/lib/service/groups.dart b/lib/service/groups.dart index ce578a805..6601ae2e0 100644 --- a/lib/service/groups.dart +++ b/lib/service/groups.dart @@ -155,7 +155,7 @@ class Groups with Service implements NotificationsListener { @override Set get serviceDependsOn { - return { DeepLink(), Config(), Auth2() }; + return { Config(), Auth2() }; } // NotificationsListener diff --git a/lib/service/http_proxy.dart b/lib/service/http_proxy.dart index 0cff2be5b..1955c292e 100644 --- a/lib/service/http_proxy.dart +++ b/lib/service/http_proxy.dart @@ -18,13 +18,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:native_flutter_proxy/native_proxy_reader.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/service/config.dart'; import 'package:rokwire_plugin/utils/utils.dart'; -class HttpProxy extends Service implements NotificationsListener { +class HttpProxy extends Service { // Singletone Factory @@ -45,8 +44,6 @@ class HttpProxy extends Service implements NotificationsListener { @override void createService() { super.createService(); - - NotificationService().subscribe(this, [Config.notifyEnvironmentChanged]); } @override @@ -59,19 +56,11 @@ class HttpProxy extends Service implements NotificationsListener { @override void destroyService() { super.destroyService(); - NotificationService().unsubscribe(this); } @override Set get serviceDependsOn { - return {Storage(), Config()}; - } - - @override - void onNotification(String name, dynamic param){ - if(name == Config.notifyEnvironmentChanged){ - _handleChanged(); - } + return { Storage(), Config() }; } Future applySystemProxy() async { diff --git a/lib/service/localization.dart b/lib/service/localization.dart index a323ea83c..99f554810 100644 --- a/lib/service/localization.dart +++ b/lib/service/localization.dart @@ -76,7 +76,10 @@ class Localization with Service implements NotificationsListener { @override void createService() { - NotificationService().subscribe(this, AppLifecycle.notifyStateChanged); + NotificationService().subscribe(this,[ + Service.notifyInitialized, + AppLifecycle.notifyStateChanged, + ]); } @override @@ -110,7 +113,7 @@ class Localization with Service implements NotificationsListener { @override Set get serviceDependsOn { - return { Storage(), Config() }; + return { Storage() }; } // Locale @@ -289,12 +292,36 @@ class Localization with Service implements NotificationsListener { @override void onNotification(String name, dynamic param) { - if (name == AppLifecycle.notifyStateChanged) { - _onAppLifecycleStateChanged(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 _onAppLifecycleStateChanged(AppLifecycleState? state) { + @protected + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } diff --git a/lib/service/service.dart b/lib/service/service.dart index 022846ad8..99fb16979 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -18,9 +18,12 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:rokwire_plugin/service/notification_service.dart'; abstract mixin class Service { + static const String notifyInitialized = "edu.illinois.rokwire.service.initialized"; + bool? _isInitialized; void createService() { @@ -31,9 +34,7 @@ abstract mixin class Service { Future initService() async { _isInitialized = true; - } - - Future initServiceFallback() async { + NotificationService().notify(notifyInitialized, this); } void initServiceUI() async { @@ -64,12 +65,7 @@ class Services { static set instance(Services? value) => _instance = value; List? _services; - - bool _initialized = false; - bool get initialized => _initialized; - - ServiceError? _initializeError; - ServiceError? get initializeError => _initializeError; + bool? _isInitialized; void create(List services) { if (_services == null) { @@ -86,30 +82,18 @@ class Services { service.destroyService(); } _services = null; + ; } } Future init() async { - if (_services != null) { - ServiceError? se = await _ServicesInitializer(initService, initServiceFallback).process(_services!); - return se; - } - return null; + ServiceError? error = await _ServicesInitializer(initService).process(_services); + _isInitialized = (error != null); + return error; } - @protected - Future initFallback() async { - ServiceError? error; - for (Service service in _services!) { - if (service.isInitialized != true) { - error ??= await initServiceFallback(service); - if (error != null) { - return error; - } - } - } - return null; - } + bool get isInitialized => (_isInitialized == true); + bool get isInitializeFailed => (_isInitialized == false); @protected Future initService(Service service) async { @@ -122,18 +106,6 @@ class Services { return null; } - - @protected - Future initServiceFallback(Service service) async { - try { - await service.initServiceFallback(); - } - on ServiceError catch (error) { - return error; - } - return null; - } - void initUI() { if (_services != null) { for (Service service in _services!) { @@ -157,16 +129,13 @@ class _ServicesInitializer { final Set _inProgress = {}; Future Function(Service service) initService; - Future Function(Service service) initServiceFallback; Completer? _completer; - _ServicesInitializer(this.initService, this.initServiceFallback); + _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(); @@ -178,21 +147,40 @@ 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) { - try { - if (_canStartService(service) && !_inProgress.contains(service)) { - _inProgress.add(service); - initService(service).then((ServiceError? error) async { - _inProgress.remove(service); - await _finishInitService(service, error); - }); - } - } - on ServiceError catch (error) { - _completer?.complete(error); - return; + if (_canStartService(service) && !_inProgress.contains(service)) { + _inProgress.add(service); + initService(service).then((ServiceError? error) async { + _inProgress.remove(service); + if ((_completer != null) && (_completer?.isCompleted != true)) { + if (error?.severity == ServiceErrorSeverity.fatal) { + _completer?.complete(error); + _completer = null; + } + else { + _done.add(service); + _toDo.remove(service); + _run(); + } + } + }); } } @@ -201,7 +189,7 @@ class _ServicesInitializer { source: null, severity: ServiceErrorSeverity.fatal, title: 'Services Initialization Error', - description: 'Service dependency cycle detected: ${_findServiceCycle().join(', ')}.', + description: 'Service dependency cycle detected.', )); _completer = null; } @@ -212,83 +200,9 @@ class _ServicesInitializer { } } - Future _finishInitService(Service service, ServiceError? error, {bool tryFallback = true}) async { - if (_completer != null && !_completer!.isCompleted) { - if (error?.severity == ServiceErrorSeverity.fatal) { - if (tryFallback) { - _inProgress.add(service); - error = await initServiceFallback(service); - _inProgress.remove(service); - - _finishInitService(service, error, tryFallback: false); - } - - _completer?.complete(error); - _completer = null; - } - else { - _done.add(service); - _toDo.remove(service); - _run(); - } - } - } - bool _canStartService(Service service) { Set? serviceDependsOn = service.serviceDependsOn; - if ((serviceDependsOn == null) || serviceDependsOn.isEmpty) { - return true; - } - - for (Service dependency in serviceDependsOn) { - if (!_done.contains(dependency)) { - if (!_toDo.contains(dependency) && !_inProgress.contains(dependency)) { - // return with an error if a service depends on another service that is missing from the main initialization list (missing from _toDo, _inProgress, and _done) - throw ServiceError( - source: service, - severity: ServiceErrorSeverity.fatal, - title: 'Services Initialization Error', - description: 'Service dependency missing from initialization list: ${dependency.debugDisplayName}.', - ); - } - return false; - } - } - return true; - } - - List _findServiceCycle() { - Map _firstUninitializedDependencies = {}; - // find first uninitialized dependency for each uninitialized service - for (Service toDo in _toDo) { - try { - Service? dependency = toDo.serviceDependsOn?.firstWhere((dependency) => _toDo.contains(dependency) && !_done.contains(dependency)); - if (dependency != null) { - _firstUninitializedDependencies[toDo] = dependency; - } - } - catch (error) { - if (error is! StateError) { - debugPrint(error.toString()); - } - } - } - - // traverse the uninitialized service graph to determine the cycle - int cycleLength = 0; - Map serviceVisits = Map.fromIterable(_toDo, value: (service) => 0); - Service next = _toDo.first; - while (cycleLength < 2 * _toDo.length) { // maximum possible cycle length is the number of uninitialized services (allow to visit at most twice) - if (_firstUninitializedDependencies[next] != null) { - next = _firstUninitializedDependencies[next]!; - if (serviceVisits[next] == 2) { - break; // if trying to visit a service that has been visited twice already, then the cycle must be the list of services visited twice - } - serviceVisits[next] = serviceVisits[next]! + 1; - } - cycleLength++; - } - return serviceVisits.keys.where((service) => serviceVisits[service] == 2).toList(); // all services visited twice are part of the cycle + return (serviceDependsOn == null) || serviceDependsOn.isEmpty || _done.containsAll(serviceDependsOn); } } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 38234a904..f4aa1c442 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -279,12 +279,6 @@ class Storage with Service { bool? get encryptedMigratedToSecureStorage => getBoolWithName(encryptedMigratedToSecureStorageKey); set encryptedMigratedToSecureStorage(bool? value) => setBoolWithName(encryptedMigratedToSecureStorageKey, value); - // Config - - String get configEnvKey => 'edu.illinois.rokwire.config_environment'; - String? get configEnvironment => getStringWithName(configEnvKey); - set configEnvironment(String? value) => setStringWithName(configEnvKey, value); - // Upgrade String get reportedUpgradeVersionsKey => 'edu.illinois.rokwire.reported_upgrade_versions'; diff --git a/lib/service/styles.dart b/lib/service/styles.dart index 22e3baf06..3c74ddb89 100644 --- a/lib/service/styles.dart +++ b/lib/service/styles.dart @@ -85,7 +85,10 @@ class Styles extends Service implements NotificationsListener{ @override void createService() { - NotificationService().subscribe(this, AppLifecycle.notifyStateChanged); + NotificationService().subscribe(this, [ + Service.notifyInitialized, + AppLifecycle.notifyStateChanged + ]); } @override @@ -96,30 +99,22 @@ class Styles extends Service implements NotificationsListener{ @override Future initService() async { List> futures = [ - getAssetsDir(), loadAssetsManifest(), loadFromAssets(assetsKey), + if (!kIsWeb) + loadFromAssets(appAssetsKey), + if (!kIsWeb) + getAssetsDir(), ]; - - if (!kIsWeb) { - futures.add(loadFromAssets(appAssetsKey)); - } List results = await Future.wait(futures); - _assetsDir = results[0]; - _assetsManifest = results[1]; - _assetsStyles = results[2]; - if (!kIsWeb) { - _appAssetsStyles = results[3]; - } + _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; - futures = [ - loadFromCache(netCacheFileName), - loadFromCache(debugCacheFileName), - ]; - results = await Future.wait(futures); - - _netAssetsStyles = results[0]; - _debugAssetsStyles = results[1]; + if (_assetsDir != null) { + await initCaches(); + } if ((_assetsStyles != null) || (_appAssetsStyles != null) || (_netAssetsStyles != null) || (_debugAssetsStyles != null)) { await build(); @@ -136,33 +131,36 @@ class Styles extends Service implements NotificationsListener{ } } - @override - Future initServiceFallback() async { - - _assetsManifest = await loadAssetsManifest(); - _assetsStyles = await loadFromAssets(assetsKey); - _appAssetsStyles = kIsWeb ? null : await loadFromAssets(appAssetsKey); - - if ((_assetsStyles != null) || (_appAssetsStyles != null) || (_netAssetsStyles != null) || (_debugAssetsStyles != null)) { - await build(); - } - } - - @override - Set get serviceDependsOn { - return { Config(), Storage() }; - } - // NotificationsListener @override void onNotification(String name, dynamic param) { + + if (name == Service.notifyInitialized) { + onServiceInitialized(param is Service ? param : null); + } if (name == AppLifecycle.notifyStateChanged) { - _onAppLifecycleStateChanged(param); + onAppLifecycleStateChanged((param is AppLifecycleState) ? param : null); } } - void _onAppLifecycleStateChanged(AppLifecycleState? state) { + @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(); + } + } + } + } + + @protected + void onAppLifecycleStateChanged(AppLifecycleState? state) { if (state == AppLifecycleState.paused) { _pausedDateTime = DateTime.now(); } @@ -266,6 +264,18 @@ 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; From d85a5d06126b327e573e37248f8c9e5c1921957f Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 14 Mar 2024 11:36:40 +0200 Subject: [PATCH 175/177] Fixed _isInitialized initialization [#408]. --- lib/service/service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 99fb16979..2e5484821 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -88,7 +88,7 @@ class Services { Future init() async { ServiceError? error = await _ServicesInitializer(initService).process(_services); - _isInitialized = (error != null); + _isInitialized = (error == null); return error; } From 76b2cbad02815cbd4705553a75a9e97fe2813134 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Thu, 14 Mar 2024 12:07:43 +0200 Subject: [PATCH 176/177] Make sure not to run multiple instances of Services().init [#408]. --- lib/service/service.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/service/service.dart b/lib/service/service.dart index 2e5484821..0b9624f9e 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -65,6 +65,8 @@ class Services { static set instance(Services? value) => _instance = value; List? _services; + + Future? _initialzeFuture; bool? _isInitialized; void create(List services) { @@ -87,9 +89,16 @@ class Services { } Future init() async { - ServiceError? error = await _ServicesInitializer(initService).process(_services); - _isInitialized = (error == null); - return error; + if (_initialzeFuture != null) { + return await _initialzeFuture; + } + else { + _initialzeFuture = _ServicesInitializer(initService).process(_services); + ServiceError? error = await _initialzeFuture; + _isInitialized = (error == null); + _initialzeFuture = null; + return error; + } } bool get isInitialized => (_isInitialized == true); From dd970f0f9d23451c94916d1733cdc181fd333717 Mon Sep 17 00:00:00 2001 From: Mihail Varbanov Date: Fri, 15 Mar 2024 10:15:33 +0200 Subject: [PATCH 177/177] Fixed dart:ui import. --- lib/service/firebase_messaging.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/service/firebase_messaging.dart b/lib/service/firebase_messaging.dart index f31f828bd..56be799d1 100644 --- a/lib/service/firebase_messaging.dart +++ b/lib/service/firebase_messaging.dart @@ -15,6 +15,7 @@ */ 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_lifecycle.dart';