From a9b4088697e6edc94d43c663bd267afad8914e2d Mon Sep 17 00:00:00 2001 From: Lucas Hardt Date: Mon, 22 Jun 2026 16:09:55 +0200 Subject: [PATCH] chore(deps): migrate flutter_riverpod to v3 --- integration_test/add_tokens_test.dart | 4 +- integration_test/views_test.dart | 4 +- lib/main.dart | 2 +- lib/state_notifiers/deeplink_notifier.dart | 27 +-- .../push_request_notifier.dart | 33 +++- lib/state_notifiers/settings_notifier.dart | 9 +- lib/state_notifiers/token_notifier.dart | 63 ++++++- lib/utils/network_utils.dart | 4 +- lib/utils/push_provider.dart | 12 +- lib/utils/riverpod_providers.dart | 156 +++++++----------- lib/utils/riverpod_state_listener.dart | 14 +- lib/views/main_view/main_view.dart | 5 +- .../connectivity_listener.dart | 2 +- .../tokens_view_widgets/token_search_bar.dart | 14 +- lib/widgets/app_wrapper.dart | 1 + lib/widgets/app_wrappers/state_observer.dart | 2 +- lib/widgets/status_bar.dart | 2 +- pubspec.lock | 8 +- pubspec.yaml | 2 +- test/tests_app_wrapper.dart | 2 + .../push_request_notifier_test.dart | 25 +-- .../settings_notifier_test.dart | 24 +-- .../state_notifiers/token_notifier_test.dart | 37 +++-- 23 files changed, 252 insertions(+), 200 deletions(-) diff --git a/integration_test/add_tokens_test.dart b/integration_test/add_tokens_test.dart index cf24dec8..b4e57581 100644 --- a/integration_test/add_tokens_test.dart +++ b/integration_test/add_tokens_test.dart @@ -29,8 +29,8 @@ void main() { (tester) async { await tester.pumpWidget(TestsAppWrapper( overrides: [ - settingsProvider.overrideWith((ref) => SettingsNotifier(repository: mockSettingsRepository)), - tokenProvider.overrideWith((ref) => TokenNotifier(repository: mockTokenRepository)), + settingsProvider.overrideWith(() => SettingsNotifier(repository: mockSettingsRepository)), + tokenProvider.overrideWith(() => TokenNotifier(repository: mockTokenRepository)), ], child: const EduMFAAuthenticator(), )); diff --git a/integration_test/views_test.dart b/integration_test/views_test.dart index 49bb0f77..6cef4031 100644 --- a/integration_test/views_test.dart +++ b/integration_test/views_test.dart @@ -52,8 +52,8 @@ void main() { testWidgets('Views Test', (tester) async { await tester.pumpWidget(TestsAppWrapper( overrides: [ - settingsProvider.overrideWith((ref) => SettingsNotifier(repository: mockSettingsRepository)), - tokenProvider.overrideWith((ref) => TokenNotifier( + settingsProvider.overrideWith(() => SettingsNotifier(repository: mockSettingsRepository)), + tokenProvider.overrideWith(() => TokenNotifier( repository: mockTokenRepository, rsaUtils: mockRsaUtils, firebaseUtils: mockFirebaseUtils, diff --git a/lib/main.dart b/lib/main.dart index dfd75935..7caa22ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -76,7 +76,7 @@ class EduMFAAuthenticator extends ConsumerWidget { return LayoutBuilder(builder: (context, constraints) { WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(appConstraintsProvider.notifier).state = constraints; + ref.read(appConstraintsProvider.notifier).setConstraints(constraints); }); return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { diff --git a/lib/state_notifiers/deeplink_notifier.dart b/lib/state_notifiers/deeplink_notifier.dart index ecf7e4b3..0408d512 100644 --- a/lib/state_notifiers/deeplink_notifier.dart +++ b/lib/state_notifiers/deeplink_notifier.dart @@ -8,21 +8,22 @@ import 'package:edumfa_authenticator/utils/riverpod_state_listener.dart'; bool _initialUriIsHandled = false; -class DeeplinkNotifier extends StateNotifier { +class DeeplinkNotifier extends Notifier { final List _subs = []; final List _sources; - DeeplinkNotifier({required this._sources}) - : super(null) { - _handleInitialUri(); - _handleIncomingLinks(); - } + DeeplinkNotifier({required List sources}) + : _sources = sources; @override - void dispose() { - for (var sub in _subs) { - sub.cancel(); - } - super.dispose(); + DeepLink? build() { + ref.onDispose(() { + for (var sub in _subs) { + sub.cancel(); + } + }); + _handleInitialUri(); + _handleIncomingLinks(); + return null; } /// Handle incoming links - the ones that the app will recieve from the OS @@ -33,7 +34,7 @@ class DeeplinkNotifier extends StateNotifier { for (var source in _sources) { _subs.add(source.stream.listen((Uri? uri) { Logger.info('Got uri from ${source.name}'); - if (!mounted) return; + if (!ref.mounted) return; if (uri == null) return; state = DeepLink(uri); }, onError: (Object err) { @@ -50,7 +51,7 @@ class DeeplinkNotifier extends StateNotifier { for (var source in _sources) { final initialUri = await source.initialUri; if (initialUri != null) { - if (!mounted) return; + if (!ref.mounted) return; state = DeepLink(initialUri, fromInit: true); Logger.info('Got initial uri from ${source.name}'); return; // There can only be one initial uri diff --git a/lib/state_notifiers/push_request_notifier.dart b/lib/state_notifiers/push_request_notifier.dart index 0707df70..ed7c95f8 100644 --- a/lib/state_notifiers/push_request_notifier.dart +++ b/lib/state_notifiers/push_request_notifier.dart @@ -23,6 +23,7 @@ import 'dart:async'; +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:edumfa_authenticator/model/push_request.dart'; @@ -31,15 +32,19 @@ import 'package:edumfa_authenticator/utils/firebase_utils.dart'; import 'package:edumfa_authenticator/utils/logger.dart'; import 'package:edumfa_authenticator/utils/network_utils.dart'; import 'package:edumfa_authenticator/utils/push_provider.dart'; +import 'package:edumfa_authenticator/utils/riverpod_providers.dart'; import 'package:edumfa_authenticator/utils/rsa_utils.dart'; /// Interface between the [PushProvider] and the UI. -class PushRequestNotifier extends StateNotifier { +class PushRequestNotifier extends Notifier { // Used for periodically polling for push challenges final PushProvider _pushProvider; final EduMFAIOClient _ioClient; final RsaUtils _rsaUtils; + final FirebaseUtils _firebaseUtils; + final PushRequest? _initState; + final bool _attachProviderListeners; PushRequestNotifier({ PushRequest? initState, @@ -47,11 +52,33 @@ class PushRequestNotifier extends StateNotifier { EduMFAIOClient? ioClient, RsaUtils? rsaUtils, FirebaseUtils? firebaseUtils, + bool attachProviderListeners = false, }) : _ioClient = ioClient ?? const EduMFAIOClient(), _pushProvider = pushProvider ?? PushProvider(), _rsaUtils = rsaUtils ?? const RsaUtils(), - super(initState) { - _pushProvider.initialize(pushSubscriber: this, firebaseUtils: firebaseUtils ?? FirebaseUtils()); + _firebaseUtils = firebaseUtils ?? FirebaseUtils(), + _initState = initState, + _attachProviderListeners = attachProviderListeners; + + @override + PushRequest? build() { + Logger.info("New PushRequestNotifier created", name: 'pushRequestProvider'); + if (_attachProviderListeners) { + ref.listen(settingsProvider, (previous, next) { + if (previous?.enablePolling != next.enablePolling) { + Logger.info("Polling enabled changed from ${previous?.enablePolling} to ${next.enablePolling}", name: 'pushRequestProvider#settingsProvider'); + _pushProvider.setPollingEnabled(next.enablePolling); + } + }); + ref.listen(appStateProvider, (previous, next) { + if (previous == AppLifecycleState.paused && next == AppLifecycleState.resumed) { + Logger.info('Polling for challenges on resume', name: 'pushRequestProvider#appStateProvider'); + _pushProvider.pollForChallenges(isManually: false); + } + }); + } + _pushProvider.initialize(pushSubscriber: this, firebaseUtils: _firebaseUtils); + return _initState; } // ACTIONS diff --git a/lib/state_notifiers/settings_notifier.dart b/lib/state_notifiers/settings_notifier.dart index a52ceec4..7de0afc0 100644 --- a/lib/state_notifiers/settings_notifier.dart +++ b/lib/state_notifiers/settings_notifier.dart @@ -9,16 +9,21 @@ import 'package:edumfa_authenticator/utils/push_provider.dart'; /// This class provies access to the device specific settings. /// It also ensures that the settings are saved to the device. /// To Update a state use: ref.read(settingsProvider.notifier).anyMethod(value) -class SettingsNotifier extends StateNotifier { +class SettingsNotifier extends Notifier { late Future loadingRepo; final SettingsRepository _repo; + final SettingsState? _initialState; SettingsNotifier({ required SettingsRepository repository, SettingsState? initialState, }) : _repo = repository, - super(initialState ?? SettingsState()) { + _initialState = initialState; + + @override + SettingsState build() { loadFromRepo(); + return _initialState ?? SettingsState(); } void loadFromRepo() async { loadingRepo = Future(() async { diff --git a/lib/state_notifiers/token_notifier.dart b/lib/state_notifiers/token_notifier.dart index 5802158c..f1ef2c75 100644 --- a/lib/state_notifiers/token_notifier.dart +++ b/lib/state_notifiers/token_notifier.dart @@ -9,7 +9,9 @@ import 'package:edumfa_authenticator/generated/l10n.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:http/http.dart'; import 'package:pointycastle/asymmetric/api.dart'; @@ -33,7 +35,7 @@ import 'package:edumfa_authenticator/utils/rsa_utils.dart'; import 'package:edumfa_authenticator/utils/utils.dart'; import 'package:edumfa_authenticator/utils/view_utils.dart'; -class TokenNotifier extends StateNotifier { +class TokenNotifier extends Notifier { static final Map _timers = {}; late Future loadingRepo; late Future?> updatingTokens = Future(() => null); @@ -41,6 +43,8 @@ class TokenNotifier extends StateNotifier { final RsaUtils _rsaUtils; final EduMFAIOClient _ioClient; final FirebaseUtils _firebaseUtils; + final TokenState? _initialState; + final bool _attachProviderListeners; TokenNotifier({ TokenState? initialState, @@ -48,14 +52,59 @@ class TokenNotifier extends StateNotifier { RsaUtils? rsaUtils, EduMFAIOClient? ioClient, FirebaseUtils? firebaseUtils, + bool attachProviderListeners = false, }) : _rsaUtils = rsaUtils ?? const RsaUtils(), _repo = repository ?? const SecureTokenRepository(), _ioClient = ioClient ?? const EduMFAIOClient(), _firebaseUtils = firebaseUtils ?? FirebaseUtils(), - super( - initialState ?? TokenState(), - ) { + _initialState = initialState, + _attachProviderListeners = attachProviderListeners; + + @override + TokenState build() { + Logger.info("New TokenNotifier created"); + if (_attachProviderListeners) { + ref.listen(deeplinkProvider, (previous, newLink) { + if (newLink == null) { + Logger.info("Received null deeplink", name: 'tokenProvider#deeplinkProvider'); + return; + } + Logger.info("Received new deeplink", name: 'tokenProvider#deeplinkProvider'); + handleLink(newLink.uri); + }); + + ref.listen(pushRequestProvider, (previous, newPushRequest) { + if (newPushRequest == null) { + Logger.info("Received null pushRequest", name: 'tokenProvider#pushRequestProvider'); + return; + } + if (newPushRequest.accepted == null) { + Logger.info("Received new pushRequest", name: 'tokenProvider#pushRequestProvider'); + addPushRequestToToken(newPushRequest); + } + if (newPushRequest.accepted != null) { + Logger.info("Received pushRequest with accepted=${newPushRequest.accepted}... removing it from state.", name: 'tokenProvider#pushRequestProvider'); + removePushRequest(newPushRequest); + FlutterLocalNotificationsPlugin().cancelAll(); + } + }); + + ref.listen(appStateProvider, (previous, next) { + Logger.info('tokenProvider reviced new AppState. Changed from $previous to $next'); + if (previous == AppLifecycleState.paused && next == AppLifecycleState.resumed) { + Logger.info('Refreshing tokens on resume', name: 'tokenProvider#appStateProvider'); + loadStateFromRepo(); + } + if (previous == AppLifecycleState.resumed && next == AppLifecycleState.paused) { + Logger.info('Saving tokens and cancelling all notifications on pause', name: 'tokenProvider#appStateProvider'); + FlutterLocalNotificationsPlugin().cancelAll(); + saveStateToRepo(); + } + }); + } + _init(); + return _initialState ?? TokenState(); } Future _init() async { @@ -311,7 +360,7 @@ class TokenNotifier extends StateNotifier { Logger.info('Ignoring rollout request: Token "${token.id}" is expired. ', name: 'token_notifier.dart#rolloutPushToken'); if (globalNavigatorKey.currentContext != null) { - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).errorRollOutNotPossibleAnymore, S.of(globalNavigatorKey.currentContext!).errorTokenExpired(token.label), ); @@ -396,14 +445,14 @@ class TokenNotifier extends StateNotifier { try { final message = response.body.isNotEmpty ? (json.decode(response.body)['result']?['error']?['message']) : ''; - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).errorRollOutFailed(token.label), message, ); } on FormatException { // Format Exception is thrown if the response body is not a valid json. This happens if the server is not reachable. - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).errorRollOutFailed(token.label), S.of(globalNavigatorKey.currentContext!).statusCode(response.statusCode) ); diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index 873df6ec..7a934743 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -57,7 +57,7 @@ class EduMFAIOClient { if (isRetry) { Logger.warning('SocketException while retrying', name: 'utils.dart#triggerNetworkAccessPermission'); if (globalNavigatorKey.currentState?.context != null) { - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(await globalContext).connectionFailed, S.of(await globalContext).checkYourNetwork, ); @@ -74,7 +74,7 @@ class EduMFAIOClient { Logger.warning('ClientException', name: 'utils.dart#triggerNetworkAccessPermission'); ioClient.close(); if (globalNavigatorKey.currentState?.context == null) return false; - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(await globalContext).connectionFailed, S.of(await globalContext).checkYourNetwork, ); diff --git a/lib/utils/push_provider.dart b/lib/utils/push_provider.dart index e10e052e..7a46ba47 100644 --- a/lib/utils/push_provider.dart +++ b/lib/utils/push_provider.dart @@ -242,7 +242,7 @@ class PushProvider { if (connectivityResult.contains(ConnectivityResult.none)) { if (isManually) { Logger.info('Tried to poll without any internet connection available.', name: 'push_provider.dart#pollForChallenges'); - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).pollingFailed, S.of(globalNavigatorKey.currentContext!).noNetworkConnection, ); @@ -269,7 +269,7 @@ class PushProvider { Logger.info(rsaUtils.runtimeType.toString(), name: 'push_provider.dart#pollForChallenge'); String? signature = await rsaUtils.trySignWithToken(token, message); if (signature == null) { - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).pollingFailedFor(token.serial), S.of(globalNavigatorKey.currentContext!).couldNotSignMessage, ); @@ -289,7 +289,7 @@ class PushProvider { ? await instance!._ioClient.doGet(url: token.url!, parameters: parameters, sslVerify: token.sslVerify) : await const EduMFAIOClient().doGet(url: token.url!, parameters: parameters, sslVerify: token.sslVerify); } catch (e) { - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).errorWhenPullingChallenges(token.serial), null, ); @@ -302,7 +302,7 @@ class PushProvider { challengeList = _getAndValidateDataFromResponse(response); } catch (_) { if (isManually) { - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).errorWhenPullingChallenges(token.serial), S.of(globalNavigatorKey.currentContext!).pushRequestParseError, ); @@ -316,7 +316,7 @@ class PushProvider { case 403: final error = getErrorMessageFromResponse(response); if (isManually) { - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).pollingFailedFor(token.serial), error ?? S.of(globalNavigatorKey.currentContext!).statusCode(response.statusCode), ); @@ -328,7 +328,7 @@ class PushProvider { default: final error = getErrorMessageFromResponse(response); if (isManually) { - globalRef?.read(statusMessageProvider.notifier).state = ( + globalRef?.read(statusMessageProvider.notifier).setMessage( S.of(globalNavigatorKey.currentContext!).pollingFailedFor(token.serial), error ?? S.of(globalNavigatorKey.currentContext!).statusCode(response.statusCode), ); diff --git a/lib/utils/riverpod_providers.dart b/lib/utils/riverpod_providers.dart index 1d102566..70dfba51 100644 --- a/lib/utils/riverpod_providers.dart +++ b/lib/utils/riverpod_providers.dart @@ -4,7 +4,6 @@ import 'package:app_links/app_links.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:edumfa_authenticator/generated/l10n.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:edumfa_authenticator/model/push_request.dart'; @@ -18,121 +17,47 @@ import 'package:edumfa_authenticator/state_notifiers/settings_notifier.dart'; import 'package:edumfa_authenticator/state_notifiers/token_notifier.dart'; import 'package:edumfa_authenticator/utils/globals.dart'; import 'package:edumfa_authenticator/utils/logger.dart'; -import 'package:edumfa_authenticator/utils/push_provider.dart'; import 'package:edumfa_authenticator/utils/riverpod_state_listener.dart'; // Never use globalRef to .watch() a provider. only use it to .read() a provider // Otherwise the whole app will rebuild on every state change of the provider WidgetRef? globalRef; -final tokenProvider = StateNotifierProvider( - (ref) { - Logger.info("New TokenNotifier created"); - final newTokenNotifier = TokenNotifier(); - - ref.listen(deeplinkProvider, (previous, newLink) { - if (newLink == null) { - Logger.info("Received null deeplink", name: 'tokenProvider#deeplinkProvider'); - return; - } - Logger.info("Received new deeplink", name: 'tokenProvider#deeplinkProvider'); - newTokenNotifier.handleLink(newLink.uri); - }); - - ref.listen(pushRequestProvider, (previous, newPushRequest) { - if (newPushRequest == null) { - Logger.info("Received null pushRequest", name: 'tokenProvider#pushRequestProvider'); - return; - } - if (newPushRequest.accepted == null) { - Logger.info("Received new pushRequest", name: 'tokenProvider#pushRequestProvider'); - newTokenNotifier.addPushRequestToToken(newPushRequest); - } - if (newPushRequest.accepted != null) { - Logger.info("Received pushRequest with accepted=${newPushRequest.accepted}... removing it from state.", name: 'tokenProvider#pushRequestProvider'); - newTokenNotifier.removePushRequest(newPushRequest); - FlutterLocalNotificationsPlugin().cancelAll(); - } - }); - - ref.listen( - appStateProvider, - (previous, next) { - Logger.info('tokenProvider reviced new AppState. Changed from $previous to $next'); - if (previous == AppLifecycleState.paused && next == AppLifecycleState.resumed) { - Logger.info('Refreshing tokens on resume', name: 'tokenProvider#appStateProvider'); - newTokenNotifier.loadStateFromRepo(); - } - if (previous == AppLifecycleState.resumed && next == AppLifecycleState.paused) { - Logger.info('Saving tokens and cancelling all notifications on pause', name: 'tokenProvider#appStateProvider'); - FlutterLocalNotificationsPlugin().cancelAll(); - newTokenNotifier.saveStateToRepo(); - } - }, - ); - return newTokenNotifier; - }, +final tokenProvider = NotifierProvider( + () => TokenNotifier(attachProviderListeners: true), name: 'tokenProvider', ); -final settingsProvider = StateNotifierProvider( - (ref) { - // Using Logger here will cause a circular dependency because Logger uses settingsProvider (logging verbosity) - return SettingsNotifier(repository: PreferenceSettingsRepository()); - }, +final settingsProvider = NotifierProvider( + // Using Logger here will cause a circular dependency because Logger uses settingsProvider (logging verbosity) + () => SettingsNotifier(repository: PreferenceSettingsRepository()), name: 'settingsProvider', ); -final pushRequestProvider = StateNotifierProvider( - (ref) { - Logger.info("New PushRequestNotifier created", name: 'pushRequestProvider'); - final pushProvider = PushProvider(); - ref.listen(settingsProvider, (previous, next) { - if (previous?.enablePolling != next.enablePolling) { - Logger.info("Polling enabled changed from ${previous?.enablePolling} to ${next.enablePolling}", name: 'pushRequestProvider#settingsProvider'); - pushProvider.setPollingEnabled(next.enablePolling); - } - }); - - final pushRequestNotifier = PushRequestNotifier( - pushProvider: pushProvider, - ); - - ref.listen(appStateProvider, (previous, next) { - if (previous == AppLifecycleState.paused && next == AppLifecycleState.resumed) { - Logger.info('Polling for challenges on resume', name: 'pushRequestProvider#appStateProvider'); - pushProvider.pollForChallenges(isManually: false); - } - }); - - return pushRequestNotifier; - }, +final pushRequestProvider = NotifierProvider( + () => PushRequestNotifier(attachProviderListeners: true), name: 'pushRequestProvider', ); -final deeplinkProvider = StateNotifierProvider( - (ref) { - Logger.info("New DeeplinkNotifier created", name: 'deeplinkProvider'); - return DeeplinkNotifier(sources: [ +final deeplinkProvider = NotifierProvider( + () => DeeplinkNotifier( + sources: [ DeeplinkSource( name: 'uni_links', stream: AppLinks().uriLinkStream, initialUri: Future.value(null) ), - ]); - }, + ], + ), name: 'deeplinkProvider', ); -final appStateProvider = StateProvider( - (ref) { - Logger.info("New AppStateNotifier created", name: 'appStateProvider'); - return null; - }, +final appStateProvider = NotifierProvider( + AppStateNotifier.new, name: 'appStateProvider', ); -final tokenFilterProvider = StateProvider((ref) => null); +final tokenFilterProvider = NotifierProvider(TokenFilterNotifier.new); final connectivityProvider = StreamProvider>( (ref) { @@ -143,7 +68,7 @@ final connectivityProvider = StreamProvider>( Logger.info("First connectivity check: $connectivity", name: 'connectivityProvider#initialCheck'); final hasNoConnection = connectivity.contains(ConnectivityResult.none); if (hasNoConnection && newState.hasPushTokens && globalNavigatorKey.currentContext != null) { - ref.read(statusMessageProvider.notifier).state = (S.of(globalNavigatorKey.currentContext!).noNetworkConnection, null); + ref.read(statusMessageProvider.notifier).setMessage(S.of(globalNavigatorKey.currentContext!).noNetworkConnection, null); } }); }, @@ -152,16 +77,49 @@ final connectivityProvider = StreamProvider>( }, ); -final statusMessageProvider = StateProvider<(String, String?)?>( - (ref) { +final statusMessageProvider = NotifierProvider( + StatusMessageNotifier.new, +); + +final appConstraintsProvider = NotifierProvider( + AppConstraintsNotifier.new, +); + +class AppStateNotifier extends Notifier { + @override + AppLifecycleState? build() { + Logger.info("New AppStateNotifier created", name: 'appStateProvider'); + return null; + } + + void setState(AppLifecycleState? value) => state = value; +} + +class TokenFilterNotifier extends Notifier { + @override + TokenFilter? build() => null; + + void setFilter(TokenFilter? value) => state = value; +} + +class StatusMessageNotifier extends Notifier<(String, String?)?> { + @override + (String, String?)? build() { Logger.info("New statusMessageProvider created", name: 'statusMessageProvider'); return null; - }, -); + } -final appConstraintsProvider = StateProvider( - (ref) { + void setMessage(String message, String? details) => state = (message, details); + + void clear() => state = null; +} + +class AppConstraintsNotifier extends Notifier { + @override + BoxConstraints? build() { Logger.info("New constraintsProvider created", name: 'appConstraintsProvider'); return null; - }, -); + } + + void setConstraints(BoxConstraints value) => state = value; +} diff --git a/lib/utils/riverpod_state_listener.dart b/lib/utils/riverpod_state_listener.dart index 67610537..dcbf5a2a 100644 --- a/lib/utils/riverpod_state_listener.dart +++ b/lib/utils/riverpod_state_listener.dart @@ -6,18 +6,18 @@ import 'package:edumfa_authenticator/processors/scheme_processors/navigation_sch import 'package:edumfa_authenticator/state_notifiers/deeplink_notifier.dart'; import 'package:edumfa_authenticator/state_notifiers/token_notifier.dart'; -abstract class StateNotifierProviderListener, S> { - final StateNotifierProvider provider; +abstract class NotifierProviderListener, S> { + final NotifierProvider provider; final void Function(S? previous, S next) onNewState; - const StateNotifierProviderListener({required this.provider, required this.onNewState}); + const NotifierProviderListener({required this.provider, required this.onNewState}); void buildListen(WidgetRef ref) { ref.listen(provider, onNewState); } } -abstract class DeepLinkListener extends StateNotifierProviderListener { +abstract class DeepLinkListener extends NotifierProviderListener { const DeepLinkListener({ - required StateNotifierProvider deeplinkProvider, + required NotifierProvider deeplinkProvider, required super.onNewState, }) : super(provider: deeplinkProvider); } @@ -39,9 +39,9 @@ class NavigationDeepLinkListener extends DeepLinkListener { } } -abstract class TokenStateListener extends StateNotifierProviderListener { +abstract class TokenStateListener extends NotifierProviderListener { const TokenStateListener({ - required StateNotifierProvider tokenProvider, + required NotifierProvider tokenProvider, required super.onNewState, }) : super(provider: tokenProvider); } diff --git a/lib/views/main_view/main_view.dart b/lib/views/main_view/main_view.dart index c0235924..1f5471d4 100644 --- a/lib/views/main_view/main_view.dart +++ b/lib/views/main_view/main_view.dart @@ -48,14 +48,13 @@ class _MainViewState extends ConsumerState with LifecycleMixin { @override void onAppResume() { Logger.info('MainView Resume', name: 'main_view.dart#onAppResume'); - globalRef?.read(appStateProvider.notifier).state = - AppLifecycleState.resumed; + globalRef?.read(appStateProvider.notifier).setState(AppLifecycleState.resumed); } @override void onAppPause() { Logger.info('MainView Pause', name: 'main_view.dart#onAppPause'); - globalRef?.read(appStateProvider.notifier).state = AppLifecycleState.paused; + globalRef?.read(appStateProvider.notifier).setState(AppLifecycleState.paused); } @override diff --git a/lib/views/tokens_view/tokens_view_widgets/connectivity_listener.dart b/lib/views/tokens_view/tokens_view_widgets/connectivity_listener.dart index 6d38c225..f182b257 100644 --- a/lib/views/tokens_view/tokens_view_widgets/connectivity_listener.dart +++ b/lib/views/tokens_view/tokens_view_widgets/connectivity_listener.dart @@ -18,7 +18,7 @@ class ConnectivityListener extends ConsumerWidget { if (newState.hasPushTokens) { if (!context.mounted) return; Logger.info("Connectivity changed: $connectivity"); - ref.read(statusMessageProvider.notifier).state = (S.of(context).noNetworkConnection, null); + ref.read(statusMessageProvider.notifier).setMessage(S.of(context).noNetworkConnection, null); } }); } diff --git a/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart b/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart index 00e3167a..2b907d1d 100644 --- a/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart +++ b/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart @@ -30,7 +30,7 @@ class _TokenSearchBarState extends ConsumerState { @override Widget build(BuildContext context) { - var tokenFilter = ref.read(tokenFilterProvider.notifier).state; + var tokenFilter = ref.read(tokenFilterProvider); return SearchBar( key: _searchBarKey, controller: _searchController, @@ -46,9 +46,11 @@ class _TokenSearchBarState extends ConsumerState { }, onTap: () async => await Haptics.vibrate(HapticsType.soft), onChanged: (value) { - ref.read(tokenFilterProvider.notifier).state = _searchController.text.isEmpty - ? null - : TokenFilter(searchQuery: value); + ref.read(tokenFilterProvider.notifier).setFilter( + _searchController.text.isEmpty + ? null + : TokenFilter(searchQuery: value), + ); setState(() {}); }, leading: SizedBox( @@ -75,7 +77,7 @@ class _TokenSearchBarState extends ConsumerState { ? IconButton( onPressed: () { _searchController.clear(); - ref.read(tokenFilterProvider.notifier).state = null; + ref.read(tokenFilterProvider.notifier).setFilter(null); setState(() {}); }, icon: const Icon(Icons.close) @@ -95,4 +97,4 @@ class _TokenSearchBarState extends ConsumerState { super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/widgets/app_wrapper.dart b/lib/widgets/app_wrapper.dart index e7ae490f..89f508dc 100644 --- a/lib/widgets/app_wrapper.dart +++ b/lib/widgets/app_wrapper.dart @@ -16,6 +16,7 @@ class AppWrapper extends StatelessWidget { Widget build(BuildContext context) { return SingleTouchRecognizer( child: ProviderScope( + retry: (_, _) => null, child: StateObserver( listeners: [ NavigationDeepLinkListener(deeplinkProvider: deeplinkProvider), diff --git a/lib/widgets/app_wrappers/state_observer.dart b/lib/widgets/app_wrappers/state_observer.dart index 4025c08a..7c098184 100644 --- a/lib/widgets/app_wrappers/state_observer.dart +++ b/lib/widgets/app_wrappers/state_observer.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:edumfa_authenticator/utils/riverpod_state_listener.dart'; class StateObserver extends ConsumerWidget { - final List listeners; + final List listeners; final Widget child; const StateObserver({super.key, required this.listeners, required this.child}); diff --git a/lib/widgets/status_bar.dart b/lib/widgets/status_bar.dart index 713db111..ea1b3c70 100644 --- a/lib/widgets/status_bar.dart +++ b/lib/widgets/status_bar.dart @@ -38,7 +38,7 @@ class _StatusBarState extends ConsumerState { currentStatusMessage = null; statusbarOverlay!.remove(); statusbarOverlay = null; - ref.read(statusMessageProvider.notifier).state = null; + ref.read(statusMessageProvider.notifier).clear(); _tryPop(); }); }; diff --git a/pubspec.lock b/pubspec.lock index 38385f52..f36e9b7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -500,10 +500,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.3.2" flutter_secure_storage: dependency: "direct main" description: @@ -1224,10 +1224,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.3.2" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 42c55462..b9143c3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: json_annotation: ^4.12.0 # State Management - flutter_riverpod: ^2.6.1 + flutter_riverpod: ^3.3.2 flutterlifecyclehooks: ^5.0.0 # Storage diff --git a/test/tests_app_wrapper.dart b/test/tests_app_wrapper.dart index 02472a7a..ced47f9e 100644 --- a/test/tests_app_wrapper.dart +++ b/test/tests_app_wrapper.dart @@ -1,6 +1,7 @@ import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/misc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:edumfa_authenticator/interfaces/repo/settings_repository.dart'; @@ -25,6 +26,7 @@ class TestsAppWrapper extends StatelessWidget { @override Widget build(BuildContext context) { return ProviderScope( + retry: (_, _) => null, overrides: overrides, child: EasyDynamicThemeWidget(child: child), ); diff --git a/test/unit_test/state_notifiers/push_request_notifier_test.dart b/test/unit_test/state_notifiers/push_request_notifier_test.dart index c3e17c75..2775af6a 100644 --- a/test/unit_test/state_notifiers/push_request_notifier_test.dart +++ b/test/unit_test/state_notifiers/push_request_notifier_test.dart @@ -35,17 +35,18 @@ void main() { void _testPushRequestNotifier() { group('PushRequestNotifier', () { test('newRequest', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockPushProvider = _MockPushProvider(); final mockFirebaseUtils = MockFirebaseUtils(); - final notifier = PushRequestNotifier( - pushProvider: mockPushProvider, - firebaseUtils: mockFirebaseUtils, - ioClient: MockEduMFAIOClient(), - rsaUtils: MockRsaUtils(), + final testProvider = NotifierProvider( + () => PushRequestNotifier( + pushProvider: mockPushProvider, + firebaseUtils: mockFirebaseUtils, + ioClient: MockEduMFAIOClient(), + rsaUtils: MockRsaUtils(), + ), ); - final testProvider = StateNotifierProvider((ref) => notifier); - await mockPushProvider.initialize(pushSubscriber: notifier, firebaseUtils: mockFirebaseUtils); + container.read(testProvider.notifier); final pr = PushRequest( title: 'title', uri: Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100'), @@ -58,7 +59,7 @@ void _testPushRequestNotifier() { expect(container.read(testProvider), pr); }); test('accept', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockPushProvider = _MockPushProvider(); final mockIoClient = MockEduMFAIOClient(); final mockRsaUtils = MockRsaUtils(); @@ -79,7 +80,7 @@ void _testPushRequestNotifier() { body: {'nonce': 'nonce', 'serial': 'serial', 'signature': 'signature'}, sslVerify: false)) .thenAnswer((_) async => Response('', 200)); - final testProvider = StateNotifierProvider((ref) { + final testProvider = NotifierProvider(() { final notifier = PushRequestNotifier( pushProvider: mockPushProvider, ioClient: mockIoClient, @@ -94,7 +95,7 @@ void _testPushRequestNotifier() { expect(container.read(testProvider)!.accepted, isTrue); }); test('decline', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockPushProvider = _MockPushProvider(); final mockIoClient = MockEduMFAIOClient(); final mockRsaUtils = MockRsaUtils(); @@ -115,7 +116,7 @@ void _testPushRequestNotifier() { body: {'nonce': 'nonce', 'serial': 'serial', 'signature': 'signature', 'decline': '1'}, sslVerify: false)) .thenAnswer((_) async => Response('', 200)); - final testProvider = StateNotifierProvider((ref) { + final testProvider = NotifierProvider(() { final notifier = PushRequestNotifier( pushProvider: mockPushProvider, ioClient: mockIoClient, diff --git a/test/unit_test/state_notifiers/settings_notifier_test.dart b/test/unit_test/state_notifiers/settings_notifier_test.dart index d065634e..99d3f9b6 100644 --- a/test/unit_test/state_notifiers/settings_notifier_test.dart +++ b/test/unit_test/state_notifiers/settings_notifier_test.dart @@ -24,9 +24,9 @@ void _testSettingsNotifier() { group('SettingsNotifier', () { final mockRepo = MockSettingsRepository(); test('load state from repo on creation', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); when(mockRepo.loadSettings()).thenAnswer((_) async => _state); - final testProvider = StateNotifierProvider((ref) => SettingsNotifier( + final testProvider = NotifierProvider(() => SettingsNotifier( initialState: SettingsState( isFirstRun: true, enablePolling: false, @@ -44,13 +44,13 @@ void _testSettingsNotifier() { }); test('addCrashReportRecipient', () { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final copyWithSettings = _state.copyWith( crashReportRecipients: {'someone', 'anotherOne'}, ); when(mockRepo.loadSettings()).thenAnswer((_) async => _state); when(mockRepo.saveSettings(copyWithSettings)).thenAnswer((_) async => true); - final testProvider = StateNotifierProvider((ref) => SettingsNotifier( + final testProvider = NotifierProvider(() => SettingsNotifier( initialState: _state, repository: mockRepo, )); @@ -64,13 +64,13 @@ void _testSettingsNotifier() { }); }); test('setPolling', () { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final copyWithSettings = _state.copyWith( enablePolling: !_state.enablePolling, ); when(mockRepo.loadSettings()).thenAnswer((_) async => _state); when(mockRepo.saveSettings(copyWithSettings)).thenAnswer((_) async => true); - final testProvider = StateNotifierProvider((ref) => SettingsNotifier( + final testProvider = NotifierProvider(() => SettingsNotifier( initialState: _state, repository: mockRepo, )); @@ -84,13 +84,13 @@ void _testSettingsNotifier() { }); }); test('setVerboseLogging', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final copyWithSettings = _state.copyWith( verboseLogging: !_state.verboseLogging, ); when(mockRepo.loadSettings()).thenAnswer((_) async => _state); when(mockRepo.saveSettings(copyWithSettings)).thenAnswer((_) async => true); - final testProvider = StateNotifierProvider((ref) => SettingsNotifier( + final testProvider = NotifierProvider(() => SettingsNotifier( initialState: _state, repository: mockRepo, )); @@ -104,13 +104,13 @@ void _testSettingsNotifier() { }); }); test('toggleVerboseLogging', () { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final copyWithSettings = _state.copyWith( verboseLogging: !_state.verboseLogging, ); when(mockRepo.loadSettings()).thenAnswer((_) async => _state); when(mockRepo.saveSettings(copyWithSettings)).thenAnswer((_) async => true); - final testProvider = StateNotifierProvider((ref) => SettingsNotifier( + final testProvider = NotifierProvider(() => SettingsNotifier( initialState: _state, repository: mockRepo, )); @@ -124,13 +124,13 @@ void _testSettingsNotifier() { }); }); test('setFirstRun', () { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final copyWithSettings = _state.copyWith( isFirstRun: !_state.isFirstRun, ); when(mockRepo.loadSettings()).thenAnswer((_) async => _state); when(mockRepo.saveSettings(copyWithSettings)).thenAnswer((_) async => true); - final testProvider = StateNotifierProvider((ref) => SettingsNotifier( + final testProvider = NotifierProvider(() => SettingsNotifier( initialState: _state, repository: mockRepo, )); diff --git a/test/unit_test/state_notifiers/token_notifier_test.dart b/test/unit_test/state_notifiers/token_notifier_test.dart index 8dca2b1c..2febe850 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_notifier_test.dart @@ -34,7 +34,7 @@ void main() { void _testTokenNotifier() { group('TokenNotifier', () { test('refreshRolledOutPushTokens', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockRepo = MockTokenRepository(); final before = [ PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true, pushRequests: PushRequestQueue()), @@ -52,8 +52,8 @@ void _testTokenNotifier() { ]; final responses = [before, after]; when(mockRepo.loadTokens()).thenAnswer((_) async => responses.removeAt(0)); - final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + final testProvider = NotifierProvider( + () => TokenNotifier(repository: mockRepo), ); final notifier = container.read(testProvider.notifier); expect((await notifier.loadStateFromRepo())?.tokens, after); @@ -63,7 +63,7 @@ void _testTokenNotifier() { verify(mockRepo.loadTokens()).called(2); }); test('addTokenFromOtpAuth: rolloutPushToken', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockRepo = MockTokenRepository(); final mockRsaUtils = MockRsaUtils(); final mockIOClient = MockEduMFAIOClient(); @@ -124,8 +124,15 @@ void _testTokenNotifier() { ), ); - final notifier = TokenNotifier(repository: mockRepo, rsaUtils: mockRsaUtils, ioClient: mockIOClient, firebaseUtils: mockFirebaseUtils); - final testProvider = StateNotifierProvider((ref) => notifier); + final testProvider = NotifierProvider( + () => TokenNotifier( + repository: mockRepo, + rsaUtils: mockRsaUtils, + ioClient: mockIOClient, + firebaseUtils: mockFirebaseUtils, + ), + ); + final notifier = container.read(testProvider.notifier); await notifier.handleQrCodeUri( Uri( scheme: 'edumfa-push', @@ -169,7 +176,7 @@ void _testTokenNotifier() { expect(pushToken.sslVerify, pushTokenShouldBe.sslVerify); }); test('addPushRequestToToken', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockRepo = MockTokenRepository(); final mockRsaUtils = MockRsaUtils(); final before = [ @@ -199,8 +206,8 @@ void _testTokenNotifier() { when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceTokens([after.first])).thenAnswer((_) async => []); when(mockRsaUtils.verifyRSASignature(any, any, any)).thenReturn(true); - final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo, rsaUtils: mockRsaUtils), + final testProvider = NotifierProvider( + () => TokenNotifier(repository: mockRepo, rsaUtils: mockRsaUtils), ); final notifier = container.read(testProvider.notifier); expect(await notifier.addPushRequestToToken(pr), true); @@ -211,7 +218,7 @@ void _testTokenNotifier() { verify(mockRsaUtils.verifyRSASignature(any, any, any)).called(1); }); test('removePushRequest', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockRepo = MockTokenRepository(); final pr = PushRequest( title: 'title', @@ -229,8 +236,8 @@ void _testTokenNotifier() { ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceTokens([after.first])).thenAnswer((_) async => []); - final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + final testProvider = NotifierProvider( + () => TokenNotifier(repository: mockRepo), ); final notifier = container.read(testProvider.notifier); await notifier.removePushRequest(pr); @@ -241,7 +248,7 @@ void _testTokenNotifier() { }); test('rolloutPushToken', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockRepo = MockTokenRepository(); final mockIOClient = MockEduMFAIOClient(); final mockFirebaseUtils = MockFirebaseUtils(); @@ -265,8 +272,8 @@ void _testTokenNotifier() { body: anyNamed('body'), sslVerify: anyNamed('sslVerify'), )).thenAnswer((_) => Future.value(Response('{"detail": {"public_key": "publicKey"}}', 200))); - final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( + final testProvider = NotifierProvider( + () => TokenNotifier( repository: mockRepo, rsaUtils: mockRsaUtils, ioClient: mockIOClient,