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 e70772d4..87d698cb 100644 --- a/lib/state_notifiers/deeplink_notifier.dart +++ b/lib/state_notifiers/deeplink_notifier.dart @@ -8,21 +8,22 @@ import 'package:flutter_riverpod/legacy.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 f712c874..5ec9ab64 100644 --- a/lib/state_notifiers/push_request_notifier.dart +++ b/lib/state_notifiers/push_request_notifier.dart @@ -23,7 +23,8 @@ import 'dart:async'; -import 'package:flutter_riverpod/legacy.dart'; +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'; import 'package:edumfa_authenticator/model/tokens/push_token.dart'; @@ -31,36 +32,76 @@ 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, + this._initState, PushProvider? pushProvider, EduMFAIOClient? ioClient, RsaUtils? rsaUtils, FirebaseUtils? firebaseUtils, - }) : _ioClient = ioClient ?? const EduMFAIOClient(), - _pushProvider = pushProvider ?? PushProvider(), - _rsaUtils = rsaUtils ?? const RsaUtils(), - super(initState) { - _pushProvider.initialize(pushSubscriber: this, firebaseUtils: firebaseUtils ?? FirebaseUtils()); + this._attachProviderListeners = false, + }) : _ioClient = ioClient ?? const EduMFAIOClient(), + _pushProvider = pushProvider ?? PushProvider(), + _rsaUtils = rsaUtils ?? const RsaUtils(), + _firebaseUtils = firebaseUtils ?? FirebaseUtils(); + + @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 Future acceptPop(PushToken pushToken) async { final pushRequest = pushToken.pushRequests.tryPop(); if (pushRequest == null) return false; - Logger.info('Approving push request.', name: 'push_request_notifier.dart#approve'); + Logger.info( + 'Approving push request.', + name: 'push_request_notifier.dart#approve', + ); final updatedPushRequest = pushRequest.copyWith(accepted: true); - final successfullyApproved = await _handleReaction(pushRequest: updatedPushRequest, token: pushToken); + final successfullyApproved = await _handleReaction( + pushRequest: updatedPushRequest, + token: pushToken, + ); if (!successfullyApproved) { pushToken.pushRequests.add(pushRequest); return false; @@ -72,9 +113,15 @@ class PushRequestNotifier extends StateNotifier { Future declinePop(PushToken pushToken) async { final pushRequest = pushToken.pushRequests.tryPop(); if (pushRequest == null) return false; - Logger.info('Decline push request.', name: 'push_request_notifier.dart#decline'); + Logger.info( + 'Decline push request.', + name: 'push_request_notifier.dart#decline', + ); final updatedPushRequest = pushRequest.copyWith(accepted: false); - final successfullyDeclined = await _handleReaction(pushRequest: updatedPushRequest, token: pushToken); + final successfullyDeclined = await _handleReaction( + pushRequest: updatedPushRequest, + token: pushToken, + ); if (!successfullyDeclined) { pushToken.pushRequests.add(pushRequest); return false; @@ -85,10 +132,16 @@ class PushRequestNotifier extends StateNotifier { void newRequest(PushRequest pushRequest) => state = pushRequest; - Future _handleReaction({required PushRequest pushRequest, required PushToken token}) async { + Future _handleReaction({ + required PushRequest pushRequest, + required PushToken token, + }) async { if (pushRequest.accepted == null) return false; - Logger.info('Push auth request accepted=${pushRequest.accepted}, sending response to edumfa', name: 'token_widgets.dart#handleReaction'); + Logger.info( + 'Push auth request accepted=${pushRequest.accepted}, sending response to edumfa', + name: 'token_widgets.dart#handleReaction', + ); // signature ::= {nonce}|{serial}[|decline] String msg = '${pushRequest.nonce}|${token.serial}'; @@ -114,9 +167,16 @@ class PushRequestNotifier extends StateNotifier { body["decline"] = "1"; } - Response response = await _ioClient.doPost(sslVerify: pushRequest.sslVerify, url: pushRequest.uri, body: body); + Response response = await _ioClient.doPost( + sslVerify: pushRequest.sslVerify, + url: pushRequest.uri, + body: body, + ); if (response.statusCode != 200) { - Logger.warning('Sending push request response failed.', name: 'token_widgets.dart#handleReaction'); + Logger.warning( + 'Sending push request response failed.', + name: 'token_widgets.dart#handleReaction', + ); return false; } diff --git a/lib/state_notifiers/settings_notifier.dart b/lib/state_notifiers/settings_notifier.dart index 062d709b..bfd71048 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 39e2c358..e213e805 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_riverpod/legacy.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,27 +43,100 @@ class TokenNotifier extends StateNotifier { final RsaUtils _rsaUtils; final EduMFAIOClient _ioClient; final FirebaseUtils _firebaseUtils; + final TokenState? _initialState; + final bool _attachProviderListeners; TokenNotifier({ - TokenState? initialState, + this._initialState, TokenRepository? repository, RsaUtils? rsaUtils, EduMFAIOClient? ioClient, FirebaseUtils? firebaseUtils, - }) : _rsaUtils = rsaUtils ?? const RsaUtils(), - _repo = repository ?? const SecureTokenRepository(), - _ioClient = ioClient ?? const EduMFAIOClient(), - _firebaseUtils = firebaseUtils ?? FirebaseUtils(), - super( - initialState ?? TokenState(), - ) { + this._attachProviderListeners = false, + }) : _rsaUtils = rsaUtils ?? const RsaUtils(), + _repo = repository ?? const SecureTokenRepository(), + _ioClient = ioClient ?? const EduMFAIOClient(), + _firebaseUtils = firebaseUtils ?? FirebaseUtils(); + + @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 { await _loadFromRepo(); await loadingRepo; - Logger.info('TokenNotifier initialized.', name: 'token_notifier.dart#_init'); + Logger.info( + 'TokenNotifier initialized.', + name: 'token_notifier.dart#_init', + ); } ///////////////////////////////////////////////////////////////////////////// @@ -73,7 +148,9 @@ class TokenNotifier extends StateNotifier { state = state.addOrReplaceTokens(tokens); await loadingRepo; loadingRepo = Future(() async { - final failedTokens = await _repo.saveOrReplaceTokens(state.lastlyUpdatedTokens); + final failedTokens = await _repo.saveOrReplaceTokens( + state.lastlyUpdatedTokens, + ); if (failedTokens.isNotEmpty) { Logger.warning( 'Saving tokens failed. Failed Tokens: ${failedTokens.length}', @@ -92,7 +169,9 @@ class TokenNotifier extends StateNotifier { state = state.replaceTokens(tokens); await loadingRepo; loadingRepo = Future(() async { - final failedTokens = await _repo.saveOrReplaceTokens(state.lastlyUpdatedTokens); + final failedTokens = await _repo.saveOrReplaceTokens( + state.lastlyUpdatedTokens, + ); if (failedTokens.isNotEmpty) { Logger.warning( 'Saving tokens failed. Failed Tokens: ${failedTokens.length}', @@ -131,18 +210,16 @@ class TokenNotifier extends StateNotifier { Future _loadFromRepo() async { log('_loadFromRepo'); List tokens; - loadingRepo = Future( - () async { - try { - tokens = await _repo.loadTokens(); - TokenState newState = TokenState(tokens: tokens); - state = newState; - return newState; - } catch (_) { - return Future(() => state); - } - }, - ); + loadingRepo = Future(() async { + try { + tokens = await _repo.loadTokens(); + TokenState newState = TokenState(tokens: tokens); + state = newState; + return newState; + } catch (_) { + return Future(() => state); + } + }); final newState = await loadingRepo; await _handlePushTokensIfExist(); return newState; @@ -153,13 +230,19 @@ class TokenNotifier extends StateNotifier { ////////////////////////////////////////////////////////////////////////////// /// Always waits for repo and other updating methods - Future updateToken(T token, T Function(T) updater) async { + Future updateToken( + T token, + T Function(T) updater, + ) async { await updatingTokens; updatingTokens = Future(() async { await loadingRepo; final current = state.currentOf(token); if (current == null) { - Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#updateToken'); + Logger.warning( + 'Tried to update a token that does not exist.', + name: 'token_notifier.dart#updateToken', + ); return null; } final updated = updater(current); @@ -168,7 +251,10 @@ class TokenNotifier extends StateNotifier { return (await updatingTokens)?.whereType().firstOrNull; } - Future> updateTokens(List tokens, T Function(T) updater) async { + Future> updateTokens( + List tokens, + T Function(T) updater, + ) async { await updatingTokens; updatingTokens = Future(() async { await loadingRepo; @@ -205,7 +291,10 @@ class TokenNotifier extends StateNotifier { try { return await _loadFromRepo(); } catch (_) { - Logger.warning('Loading tokens from storage failed.', name: 'token_notifier.dart#loadStateFromRepo'); + Logger.warning( + 'Loading tokens from storage failed.', + name: 'token_notifier.dart#loadStateFromRepo', + ); return null; } } @@ -215,10 +304,16 @@ class TokenNotifier extends StateNotifier { _cancelTimers(); try { await _repo.saveOrReplaceTokens(state.tokens); - Logger.info('Saved ${state.tokens.length} Tokens to storage.', name: 'token_notifier.dart#saveStateToRepo'); + Logger.info( + 'Saved ${state.tokens.length} Tokens to storage.', + name: 'token_notifier.dart#saveStateToRepo', + ); return true; } catch (_) { - Logger.warning('Saving tokens to storage failed.', name: 'token_notifier.dart#saveStateToRepo'); + Logger.warning( + 'Saving tokens to storage failed.', + name: 'token_notifier.dart#saveStateToRepo', + ); return false; } } @@ -229,16 +324,25 @@ class TokenNotifier extends StateNotifier { Future addPushRequestToToken(PushRequest pr) async { await updatingTokens; - PushToken? token = state.tokens.whereType().firstWhereOrNull((t) => t.serial == pr.serial && t.isRolledOut); - Logger.info('Adding push request to token', name: 'token_notifier.dart#addPushRequestToToken'); + PushToken? token = state.tokens.whereType().firstWhereOrNull( + (t) => t.serial == pr.serial && t.isRolledOut, + ); + Logger.info( + 'Adding push request to token', + name: 'token_notifier.dart#addPushRequestToToken', + ); if (token == null) { - Logger.warning('The requested token does not exist or is not rolled out.', name: 'token_notifier.dart#addPushRequestToToken'); + Logger.warning( + 'The requested token does not exist or is not rolled out.', + name: 'token_notifier.dart#addPushRequestToToken', + ); return false; } String signature = pr.signature; // Must match the server's sign_string in eduMFA pushtoken.py: // "{nonce}|{url}|{serial}|{question}|{title}|{sslverify}" - String signedData = '${pr.nonce}|' + String signedData = + '${pr.nonce}|' '${pr.uri}|' '${pr.serial}|' '${pr.question}|' @@ -247,14 +351,24 @@ class TokenNotifier extends StateNotifier { // Re-add url and sslverify to android legacy tokens: if (token.url == null) { - token = await updateToken(token, (p0) => p0.copyWith(url: pr.uri, sslVerify: pr.sslVerify)); + token = await updateToken( + token, + (p0) => p0.copyWith(url: pr.uri, sslVerify: pr.sslVerify), + ); } if (token == null) { - Logger.warning('The requested token does not exist anymore', name: 'token_notifier.dart#addPushRequestToToken'); + Logger.warning( + 'The requested token does not exist anymore', + name: 'token_notifier.dart#addPushRequestToToken', + ); return false; } - bool isVerified = _rsaUtils.verifyRSASignature(token.rsaPublicServerKey!, utf8.encode(signedData), base32.decode(signature)); + bool isVerified = _rsaUtils.verifyRSASignature( + token.rsaPublicServerKey!, + utf8.encode(signedData), + base32.decode(signature), + ); if (!isVerified) { Logger.warning( @@ -264,7 +378,10 @@ class TokenNotifier extends StateNotifier { ); return false; } - Logger.info('Validating incoming message was successful.', name: 'token_notifier.dart#addPushRequestToToken'); + Logger.info( + 'Validating incoming message was successful.', + name: 'token_notifier.dart#addPushRequestToToken', + ); if (token.knowsRequestWithId(pr.id)) { Logger.info( @@ -276,7 +393,10 @@ class TokenNotifier extends StateNotifier { // Save the pending request. token = await updateToken(token, (p0) => p0.withPushRequest(pr)) ?? token; - Logger.info('Added push request ${pr.id} to token ${token.id}', name: 'token_notifier.dart#addPushRequestToToken'); + Logger.info( + 'Added push request ${pr.id} to token ${token.id}', + name: 'token_notifier.dart#addPushRequestToToken', + ); return true; } @@ -284,82 +404,171 @@ class TokenNotifier extends StateNotifier { Future removePushRequest(PushRequest pushRequest) async { await updatingTokens; Logger.info('Removing push request ${pushRequest.id}'); - PushToken? token = state.tokens.whereType().firstWhereOrNull((t) => t.serial == pushRequest.serial); + PushToken? token = state.tokens.whereType().firstWhereOrNull( + (t) => t.serial == pushRequest.serial, + ); if (token == null) { - Logger.warning('The requested token with serial "${pushRequest.serial}" does not exist.', name: 'token_notifier.dart#removePushRequest'); + Logger.warning( + 'The requested token with serial "${pushRequest.serial}" does not exist.', + name: 'token_notifier.dart#removePushRequest', + ); return false; } - token = await updateToken(token, (p0) => p0.withoutPushRequest(pushRequest)) ?? token; - - Logger.info('Removed push request from token ${token.id}', name: 'token_notifier.dart#removePushRequest'); + token = + await updateToken( + token, + (p0) => p0.withoutPushRequest(pushRequest), + ) ?? + token; + + Logger.info( + 'Removed push request from token ${token.id}', + name: 'token_notifier.dart#removePushRequest', + ); return true; } Future rolloutPushToken(PushToken token) async { await updatingTokens; token = (getTokenFromId(token.id)) as PushToken? ?? token; - assert(token.url != null, 'Token url is null. Cannot rollout token without url.'); - Logger.info('Rolling out token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); + assert( + token.url != null, + 'Token url is null. Cannot rollout token without url.', + ); + Logger.info( + 'Rolling out token "${token.id}"', + name: 'token_notifier.dart#rolloutPushToken', + ); if (token.isRolledOut) { - Logger.info('Ignoring rollout request: Token "${token.id}" already rolled out.', name: 'token_notifier.dart#rolloutPushToken'); + Logger.info( + 'Ignoring rollout request: Token "${token.id}" already rolled out.', + name: 'token_notifier.dart#rolloutPushToken', + ); return true; } if (token.rolloutState.rollOutInProgress) { - Logger.info('Ignoring rollout request: Rollout of token "${token.id}" already started. Tokenstate: ${token.rolloutState} ', - name: 'token_notifier.dart#rolloutPushToken'); + Logger.info( + 'Ignoring rollout request: Rollout of token "${token.id}" already started. Tokenstate: ${token.rolloutState} ', + name: 'token_notifier.dart#rolloutPushToken', + ); return false; } if (token.expirationDate?.isBefore(DateTime.now()) == true) { - Logger.info('Ignoring rollout request: Token "${token.id}" is expired. ', name: 'token_notifier.dart#rolloutPushToken'); + Logger.info( + 'Ignoring rollout request: Token "${token.id}" is expired. ', + name: 'token_notifier.dart#rolloutPushToken', + ); if (globalNavigatorKey.currentContext != null) { - globalRef?.read(statusMessageProvider.notifier).state = ( - S.of(globalNavigatorKey.currentContext!).errorRollOutNotPossibleAnymore, - S.of(globalNavigatorKey.currentContext!).errorTokenExpired(token.label), - ); + globalRef + ?.read(statusMessageProvider.notifier) + .setMessage( + S + .of(globalNavigatorKey.currentContext!) + .errorRollOutNotPossibleAnymore, + S + .of(globalNavigatorKey.currentContext!) + .errorTokenExpired(token.label), + ); } _removeToken(token); return false; } if (token.privateTokenKey == null) { - Logger.info('Updating rollout state of token "${token.id}" to generatingRSAKeyPair', name: 'token_notifier.dart#rolloutPushToken'); - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.generatingRSAKeyPair)) ?? token; - Logger.info('Updated token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); + Logger.info( + 'Updating rollout state of token "${token.id}" to generatingRSAKeyPair', + name: 'token_notifier.dart#rolloutPushToken', + ); + token = + await updateToken( + token, + (p0) => p0.copyWith( + rolloutState: PushTokenRollOutState.generatingRSAKeyPair, + ), + ) ?? + token; + Logger.info( + 'Updated token "${token.id}"', + name: 'token_notifier.dart#rolloutPushToken', + ); try { final keyPair = await _rsaUtils.generateRSAKeyPair(); token = token.withPrivateTokenKey(keyPair.privateKey); token = token.withPublicTokenKey(keyPair.publicKey); - token = await updateToken(token, (p0) { + token = + await updateToken(token, (p0) { p0 = p0.withPrivateTokenKey(keyPair.privateKey); return p0.withPublicTokenKey(keyPair.publicKey); }) ?? token; - Logger.info('Updated token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); + Logger.info( + 'Updated token "${token.id}"', + name: 'token_notifier.dart#rolloutPushToken', + ); } catch (e, s) { - Logger.error('Error while generating RSA key pair.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.generatingRSAKeyPairFailed)) ?? token; + Logger.error( + 'Error while generating RSA key pair.', + name: 'token_notifier.dart#rolloutPushToken', + error: e, + stackTrace: s, + ); + token = + await updateToken( + token, + (p0) => p0.copyWith( + rolloutState: PushTokenRollOutState.generatingRSAKeyPairFailed, + ), + ) ?? + token; await Haptics.vibrate(HapticsType.error); return false; } } - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKey)) ?? token; + token = + await updateToken( + token, + (p0) => + p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKey), + ) ?? + token; if (!kIsWeb && Platform.isIOS) { - Logger.warning('Triggering network access permission for token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); - if (await _ioClient.triggerNetworkAccessPermission(url: token.url!, sslVerify: token.sslVerify) == false) { - Logger.warning('Network access permission for token "${token.id}" failed.', name: 'token_notifier.dart#rolloutPushToken'); - updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)); + Logger.warning( + 'Triggering network access permission for token "${token.id}"', + name: 'token_notifier.dart#rolloutPushToken', + ); + if (await _ioClient.triggerNetworkAccessPermission( + url: token.url!, + sslVerify: token.sslVerify, + ) == + false) { + Logger.warning( + 'Network access permission for token "${token.id}" failed.', + name: 'token_notifier.dart#rolloutPushToken', + ); + updateToken( + token, + (p0) => p0.copyWith( + rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed, + ), + ); await Haptics.vibrate(HapticsType.error); return false; } - Logger.warning('Network access permission for token "${token.id}" successful.', name: 'token_notifier.dart#rolloutPushToken'); + Logger.warning( + 'Network access permission for token "${token.id}" successful.', + name: 'token_notifier.dart#rolloutPushToken', + ); } try { // TODO What to do with poll only tokens if google-services is used? - Logger.warning('SSLVerify: ${token.sslVerify}', name: 'token_notifier.dart#rolloutPushToken'); + Logger.warning( + 'SSLVerify: ${token.sslVerify}', + name: 'token_notifier.dart#rolloutPushToken', + ); Response response = await _ioClient.doPost( sslVerify: token.sslVerify, url: token.url!, @@ -367,25 +576,65 @@ class TokenNotifier extends StateNotifier { 'enrollment_credential': token.enrollmentCredentials, 'serial': token.serial, 'fbtoken': await _firebaseUtils.getFBToken(), - 'pubkey': _rsaUtils.serializeRSAPublicKeyPKCS8(token.rsaPublicTokenKey!), + 'pubkey': _rsaUtils.serializeRSAPublicKeyPKCS8( + token.rsaPublicTokenKey!, + ), }, ); if (response.statusCode == 200) { - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.parsingResponse)) ?? token; + token = + await updateToken( + token, + (p0) => p0.copyWith( + rolloutState: PushTokenRollOutState.parsingResponse, + ), + ) ?? + token; try { RSAPublicKey publicServerKey = await _parseRollOutResponse(response); - token = await updateToken(token, (p0) => p0.withPublicServerKey(publicServerKey)) ?? token; + token = + await updateToken( + token, + (p0) => p0.withPublicServerKey(publicServerKey), + ) ?? + token; } on FormatException catch (e, s) { - showMessage(message: "Couldn't parsing RSA public key: ${e.message}", duration: const Duration(seconds: 3)); + showMessage( + message: "Couldn't parsing RSA public key: ${e.message}", + duration: const Duration(seconds: 3), + ); - Logger.warning('Error while parsing RSA public key.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.parsingResponseFailed)) ?? token; + Logger.warning( + 'Error while parsing RSA public key.', + name: 'token_notifier.dart#rolloutPushToken', + error: e, + stackTrace: s, + ); + token = + await updateToken( + token, + (p0) => p0.copyWith( + rolloutState: PushTokenRollOutState.parsingResponseFailed, + ), + ) ?? + token; await Haptics.vibrate(HapticsType.error); return false; } - Logger.info('Roll out successful', name: 'token_notifier.dart#rolloutPushToken'); - token = await updateToken(token, (p0) => p0.copyWith(isRolledOut: true, rolloutState: PushTokenRollOutState.rolloutComplete)) ?? token; + Logger.info( + 'Roll out successful', + name: 'token_notifier.dart#rolloutPushToken', + ); + token = + await updateToken( + token, + (p0) => p0.copyWith( + isRolledOut: true, + rolloutState: PushTokenRollOutState.rolloutComplete, + ), + ) ?? + token; if (!isRunningTests()) { await Haptics.vibrate(HapticsType.success); } @@ -393,52 +642,105 @@ class TokenNotifier extends StateNotifier { return true; } else { - Logger.warning('Post request on roll out failed.', - name: 'token_notifier.dart#rolloutPushToken', - error: 'Token: ${token.serial}\nStatus code: ${response.statusCode},\nURL:${response.request?.url}\nBody: ${response.body}'); + Logger.warning( + 'Post request on roll out failed.', + name: 'token_notifier.dart#rolloutPushToken', + error: + 'Token: ${token.serial}\nStatus code: ${response.statusCode},\nURL:${response.request?.url}\nBody: ${response.body}', + ); try { - final message = response.body.isNotEmpty ? (json.decode(response.body)['result']?['error']?['message']) : ''; - globalRef?.read(statusMessageProvider.notifier).state = ( - S.of(globalNavigatorKey.currentContext!).errorRollOutFailed(token.label), - message, - ); + final message = response.body.isNotEmpty + ? (json.decode(response.body)['result']?['error']?['message']) + : ''; + 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 = ( - S.of(globalNavigatorKey.currentContext!).errorRollOutFailed(token.label), - S.of(globalNavigatorKey.currentContext!).statusCode(response.statusCode) - ); + globalRef + ?.read(statusMessageProvider.notifier) + .setMessage( + S + .of(globalNavigatorKey.currentContext!) + .errorRollOutFailed(token.label), + S + .of(globalNavigatorKey.currentContext!) + .statusCode(response.statusCode), + ); } - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)) ?? token; + token = + await updateToken( + token, + (p0) => p0.copyWith( + rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed, + ), + ) ?? + token; await Haptics.vibrate(HapticsType.error); return false; } } catch (e, s) { - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)) ?? token; + token = + await updateToken( + token, + (p0) => p0.copyWith( + rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed, + ), + ) ?? + token; await Haptics.vibrate(HapticsType.error); - if (e is PlatformException && e.code == FIREBASE_TOKEN_ERROR_CODE || e is SocketException || e is TimeoutException || e is FirebaseException) { - Logger.warning('Connection error: Roll out push token failed.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); + if (e is PlatformException && e.code == FIREBASE_TOKEN_ERROR_CODE || + e is SocketException || + e is TimeoutException || + e is FirebaseException) { + Logger.warning( + 'Connection error: Roll out push token failed.', + name: 'token_notifier.dart#rolloutPushToken', + error: e, + stackTrace: s, + ); showMessage( - message: S.of(globalNavigatorKey.currentContext!).errorRollOutNoConnectionToServer(token.label), + message: S + .of(globalNavigatorKey.currentContext!) + .errorRollOutNoConnectionToServer(token.label), duration: const Duration(seconds: 3), ); } else if (e is HandshakeException) { - Logger.warning('SSL error: Roll out push token failed.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); + Logger.warning( + 'SSL error: Roll out push token failed.', + name: 'token_notifier.dart#rolloutPushToken', + error: e, + stackTrace: s, + ); showMessage( - message: S.of(globalNavigatorKey.currentContext!).errorRollOutSSLHandshakeFailed, + message: S + .of(globalNavigatorKey.currentContext!) + .errorRollOutSSLHandshakeFailed, duration: const Duration(seconds: 3), ); } else { if (globalNavigatorKey.currentContext != null) { showMessage( - message: S.of(globalNavigatorKey.currentContext!).errorRollOutUnknownError(e), + message: S + .of(globalNavigatorKey.currentContext!) + .errorRollOutUnknownError(e), duration: const Duration(seconds: 3), ); } - Logger.error('Roll out push token failed.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); + Logger.error( + 'Roll out push token failed.', + name: 'token_notifier.dart#rolloutPushToken', + error: e, + stackTrace: s, + ); } return false; } @@ -454,7 +756,10 @@ class TokenNotifier extends StateNotifier { // TODO: Translate messages Future handleQrCodeUri(String? qrCodeUri) async { if (qrCodeUri == null) { - showMessage(message: 'The provided image doesn\'t contain a QR code.', duration: const Duration(seconds: 3)); + showMessage( + message: 'The provided image doesn\'t contain a QR code.', + duration: const Duration(seconds: 3), + ); await Haptics.vibrate(HapticsType.error); return; } @@ -463,19 +768,36 @@ class TokenNotifier extends StateNotifier { try { uri = Uri.parse(qrCodeUri); } catch (_) { - showMessage(message: 'The scanned QR code is not a valid URI.', duration: const Duration(seconds: 3)); + showMessage( + message: 'The scanned QR code is not a valid URI.', + duration: const Duration(seconds: 3), + ); await Haptics.vibrate(HapticsType.error); return; } List tokens = await _tokensFromUri(uri); - tokens = tokens.map((e) => TokenOriginSourceType.qrScan.addOriginToToken(token: e, data: qrCodeUri)).toList(); + tokens = tokens + .map( + (e) => TokenOriginSourceType.qrScan.addOriginToToken( + token: e, + data: qrCodeUri, + ), + ) + .toList(); await addOrReplaceTokens(tokens); await _handlePushTokensIfExist(); } Future handleLink(Uri uri) async { List tokens = await _tokensFromUri(uri); - tokens = tokens.map((e) => TokenOriginSourceType.link.addOriginToToken(token: e, data: uri.toString())).toList(); + tokens = tokens + .map( + (e) => TokenOriginSourceType.link.addOriginToToken( + token: e, + data: uri.toString(), + ), + ) + .toList(); await addOrReplaceTokens(tokens); await _handlePushTokensIfExist(); } @@ -493,16 +815,25 @@ class TokenNotifier extends StateNotifier { ////////////////////////////////////////////////////////////////////////////// Future _parseRollOutResponse(Response response) async { - Logger.info('Parsing rollout response, try to extract public_key.', name: 'token_notifier.dart#_parseRollOutResponse'); + Logger.info( + 'Parsing rollout response, try to extract public_key.', + name: 'token_notifier.dart#_parseRollOutResponse', + ); try { String key = json.decode(response.body)['detail']['public_key']; key = key.replaceAll('\n', ''); - Logger.info('Extracting public key was successful.', name: 'token_notifier.dart#_parseRollOutResponse'); + Logger.info( + 'Extracting public key was successful.', + name: 'token_notifier.dart#_parseRollOutResponse', + ); return _rsaUtils.deserializeRSAPublicKeyPKCS1(key); } on FormatException catch (e) { - throw FormatException('Response body does not contain RSA public key.', e); + throw FormatException( + 'Response body does not contain RSA public key.', + e, + ); } } @@ -513,7 +844,10 @@ class TokenNotifier extends StateNotifier { checkNotificationPermission(); } for (final element in state.pushTokensToRollOut) { - Logger.info('Handling push token "${element.id}"', name: 'token_notifier.dart#_handlePushTokensIfExist'); + Logger.info( + 'Handling push token "${element.id}"', + name: 'token_notifier.dart#_handlePushTokensIfExist', + ); await rolloutPushToken(element); } } 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 9e309859..fa2b4bed 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,7 +17,6 @@ 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'; import 'package:flutter_riverpod/legacy.dart'; @@ -26,114 +24,41 @@ import 'package:flutter_riverpod/legacy.dart'; // 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) { @@ -144,7 +69,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); } }); }, @@ -153,16 +78,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 dbfc9b5b..4f9b6214 100644 --- a/lib/utils/riverpod_state_listener.dart +++ b/lib/utils/riverpod_state_listener.dart @@ -7,18 +7,18 @@ import 'package:edumfa_authenticator/state_notifiers/deeplink_notifier.dart'; import 'package:edumfa_authenticator/state_notifiers/token_notifier.dart'; import 'package:flutter_riverpod/legacy.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); } @@ -40,9 +40,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/test/tests_app_wrapper.dart b/test/tests_app_wrapper.dart index 7c5efa0d..ced47f9e 100644 --- a/test/tests_app_wrapper.dart +++ b/test/tests_app_wrapper.dart @@ -26,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 dd8cde67..fc9bb72c 100644 --- a/test/unit_test/state_notifiers/push_request_notifier_test.dart +++ b/test/unit_test/state_notifiers/push_request_notifier_test.dart @@ -36,17 +36,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'), @@ -59,7 +60,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(); @@ -80,7 +81,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, @@ -95,7 +96,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(); @@ -116,7 +117,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 3d4f7dd6..47961274 100644 --- a/test/unit_test/state_notifiers/settings_notifier_test.dart +++ b/test/unit_test/state_notifiers/settings_notifier_test.dart @@ -25,9 +25,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, @@ -45,13 +45,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, )); @@ -65,13 +65,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, )); @@ -85,13 +85,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, )); @@ -105,13 +105,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, )); @@ -125,13 +125,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 6080fd67..a1ab6de2 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_notifier_test.dart @@ -35,7 +35,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()), @@ -53,8 +53,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); @@ -64,7 +64,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(); @@ -125,8 +125,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', @@ -170,7 +177,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 = [ @@ -200,8 +207,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); @@ -212,7 +219,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', @@ -230,8 +237,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); @@ -242,7 +249,7 @@ void _testTokenNotifier() { }); test('rolloutPushToken', () async { - final container = ProviderContainer(); + final container = ProviderContainer(retry: (_, _) => null); final mockRepo = MockTokenRepository(); final mockIOClient = MockEduMFAIOClient(); final mockFirebaseUtils = MockFirebaseUtils(); @@ -266,8 +273,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,