diff --git a/lib/main.dart b/lib/main.dart index e41e477..384c5b6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,6 @@ import 'package:open_authenticator/app.dart'; import 'package:open_authenticator/firebase_options.dart'; import 'package:open_authenticator/i18n/translations.g.dart'; import 'package:open_authenticator/model/app_links.dart'; -import 'package:open_authenticator/model/app_unlock/state.dart'; import 'package:open_authenticator/model/authentication/providers/email_link.dart'; import 'package:open_authenticator/model/authentication/providers/provider.dart'; import 'package:open_authenticator/model/settings/show_intro.dart'; @@ -269,37 +268,32 @@ class _RouteWidgetState extends ConsumerState<_RouteWidget> { }, fireImmediately: true, ); - ref.listenManual( - appUnlockStateProvider, - (previous, next) async { - if (previous == next || next is! AsyncData || next.value) { - return; - } - WidgetsBinding.instance.addPostFrameCallback((_) => handleAppLocked()); - }, - fireImmediately: true, - ); } } @override - Widget build(BuildContext context) => widget.rateMyApp - ? RateMyAppBuilder( - onInitialized: (context, rateMyApp) { - if (rateMyApp.shouldOpenDialog) { - rateMyApp.showRateDialog(context); - } - }, - rateMyApp: RateMyApp.customConditions( - appStoreIdentifier: Stores.appStoreIdentifier, - googlePlayIdentifier: Stores.googlePlayIdentifier, - conditions: [ - SupportedPlatformsCondition(), - ], - )..populateWithDefaultConditions(), - builder: (context) => widget.child, - ) - : widget.child; + Widget build(BuildContext context) { + Widget child = UnlockChallengeWidget( + child: widget.child, + ); + return widget.rateMyApp + ? RateMyAppBuilder( + onInitialized: (context, rateMyApp) { + if (rateMyApp.shouldOpenDialog) { + rateMyApp.showRateDialog(context); + } + }, + rateMyApp: RateMyApp.customConditions( + appStoreIdentifier: Stores.appStoreIdentifier, + googlePlayIdentifier: Stores.googlePlayIdentifier, + conditions: [ + SupportedPlatformsCondition(), + ], + )..populateWithDefaultConditions(), + builder: (context) => child, + ) + : child; + } /// Handles a login link. Future handleLoginLink(Uri loginLink) async { @@ -344,11 +338,4 @@ class _RouteWidgetState extends ConsumerState<_RouteWidget> { TotpLimitDialog.showAndBlock(context); } } - - /// Handles the app locked state. - Future handleAppLocked() async { - if (mounted) { - UnlockChallengeOverlay.display(context); - } - } } diff --git a/lib/model/app_unlock/method.dart b/lib/model/app_unlock/method.dart index aaa886b..3c0b462 100644 --- a/lib/model/app_unlock/method.dart +++ b/lib/model/app_unlock/method.dart @@ -33,6 +33,9 @@ sealed class AppUnlockMethod { /// [context] is required so that we can interact with the user. Future _tryUnlock(BuildContext context, Ref ref, UnlockReason reason); + /// The default app state. + AppLockState get defaultState => AppLockState.locked; + /// Triggered when this method has been chosen has the app unlock method. /// [unlockResult] is the result of the [tryUnlock] call. Future onMethodChosen(Ref ref, {ResultSuccess? enableResult}) => Future.value(); @@ -41,6 +44,16 @@ sealed class AppUnlockMethod { Future onMethodChanged(Ref ref, {ResultSuccess? disableResult}) => Future.value(); } +/// Represents an app state. +enum AppLockState { + /// If the app is locked, waiting for unlock. + locked, + /// If the app has been unlocked. + unlocked, + /// If an unlock challenge has started. + unlockChallengedStarted; +} + /// Local authentication. class LocalAuthenticationAppUnlockMethod extends AppUnlockMethod { @override @@ -153,6 +166,9 @@ class MasterPasswordAppUnlockMethod extends AppUnlockMethod { class NoneAppUnlockMethod extends AppUnlockMethod { @override Future _tryUnlock(BuildContext context, Ref ref, UnlockReason reason) => Future.value(const ResultSuccess()); + + @override + AppLockState get defaultState => AppLockState.unlocked; } /// Configures the unlock reason for [UnlockChallenge]s. diff --git a/lib/model/app_unlock/state.dart b/lib/model/app_unlock/state.dart index 3950b6a..43a07f8 100644 --- a/lib/model/app_unlock/state.dart +++ b/lib/model/app_unlock/state.dart @@ -7,20 +7,21 @@ import 'package:open_authenticator/model/settings/app_unlock_method.dart'; import 'package:open_authenticator/utils/result.dart'; /// The app unlock state state provider. -final appUnlockStateProvider = AsyncNotifierProvider(AppUnlockState.new); +final appLockStateProvider = AsyncNotifierProvider(AppLockStateNotifier.new); /// Allows to get and set the app unlocked state. -class AppUnlockState extends AsyncNotifier { +class AppLockStateNotifier extends AsyncNotifier { @override - FutureOr build() async { + FutureOr build() async { AppUnlockMethod unlockMethod = await ref.read(appUnlockMethodSettingsEntryProvider.future); - return unlockMethod is NoneAppUnlockMethod; + return unlockMethod.defaultState; } /// Tries to unlock the app. Future unlock(BuildContext context, {UnlockReason unlockReason = UnlockReason.openApp}) async { + state = AsyncData(AppLockState.unlockChallengedStarted); Result result = await ref.read(appUnlockMethodSettingsEntryProvider.notifier).unlockWithCurrentMethod(context, unlockReason); - state = AsyncData(result is ResultSuccess ? true : false); + state = AsyncData(result is ResultSuccess ? AppLockState.unlocked : AppLockState.locked); return result; } } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 3b6331d..8c7162e 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:open_authenticator/i18n/translations.g.dart'; import 'package:open_authenticator/main.dart'; +import 'package:open_authenticator/model/app_unlock/method.dart'; import 'package:open_authenticator/model/app_unlock/state.dart'; import 'package:open_authenticator/model/crypto.dart'; import 'package:open_authenticator/model/settings/display_copy_button.dart'; @@ -159,7 +160,7 @@ class _HomePageBody extends ConsumerWidget { AsyncValue totps = ref.watch(totpRepositoryProvider); switch (totps) { case AsyncData(:final value): - bool isUnlocked = ref.watch(appUnlockStateProvider).valueOrNull ?? false; + bool isUnlocked = ref.watch(appLockStateProvider).valueOrNull == AppLockState.unlocked; bool displayCopyButton = ref.watch(displayCopyButtonSettingsEntryProvider).valueOrNull ?? true; Widget child = value.isEmpty ? CustomScrollView( diff --git a/lib/widgets/unlock_challenge.dart b/lib/widgets/unlock_challenge.dart index c818fe1..bbe55f4 100644 --- a/lib/widgets/unlock_challenge.dart +++ b/lib/widgets/unlock_challenge.dart @@ -4,42 +4,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:open_authenticator/app.dart'; import 'package:open_authenticator/i18n/translations.g.dart'; +import 'package:open_authenticator/model/app_unlock/method.dart'; import 'package:open_authenticator/model/app_unlock/state.dart'; import 'package:open_authenticator/utils/result.dart'; import 'package:open_authenticator/widgets/blur.dart'; import 'package:open_authenticator/widgets/snackbar_icon.dart'; import 'package:open_authenticator/widgets/title.dart'; -/// An overlay that is shown waiting for the user to solve the unlock challenge. -class UnlockChallengeOverlay { - /// The current overlay entry, if inserted. - static OverlayEntry? _currentEntry; - - /// Displays the unlock challenge overlay. - static void display(BuildContext context) { - if (_currentEntry != null) { - return; - } - _currentEntry = OverlayEntry( - builder: (context) => _UnlockChallengeOverlayWidget( - onUnlock: () { - _currentEntry?.remove(); - _currentEntry = null; - }, - ), - ); - Overlay.of(context).insert(_currentEntry!); - } -} - /// The unlock challenge widget. -class _UnlockChallengeOverlayWidget extends ConsumerStatefulWidget { - /// Triggered when unlocked. - final VoidCallback? onUnlock; +class UnlockChallengeWidget extends ConsumerStatefulWidget { + /// The child widget. + final Widget child; - /// Creates a new unlock challenge route widget instance. - const _UnlockChallengeOverlayWidget({ - this.onUnlock, + /// Creates a new unlock challenge widget instance. + const UnlockChallengeWidget({ + super.key, + required this.child, }); @override @@ -47,81 +27,71 @@ class _UnlockChallengeOverlayWidget extends ConsumerStatefulWidget { } /// The master password unlock route widget state. -class _UnlockChallengeWidgetState extends ConsumerState<_UnlockChallengeOverlayWidget> { - /// Whether the unlock challenge has started. - bool unlockChallengedStarted = false; - +class _UnlockChallengeWidgetState extends ConsumerState { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addPostFrameCallback((_) { tryUnlockIfNeeded(); }); } @override - Widget build(BuildContext context) => Scaffold( - backgroundColor: Colors.transparent, - body: BlurWidget( - above: Center( - child: ListView( - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: TitleWidget( - textAlign: TextAlign.center, - textStyle: Theme.of(context).textTheme.headlineLarge, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( - translations.appUnlock.widget.text(app: App.appName), - textAlign: TextAlign.center, - ), - ), - Align( - child: SizedBox( - width: math.min(MediaQuery.of(context).size.width, 300), - child: FilledButton.icon( - onPressed: unlockChallengedStarted ? null : tryUnlockIfNeeded, - label: Text(translations.appUnlock.widget.button), - icon: const Icon(Icons.key), - ), + Widget build(BuildContext context) { + AsyncValue appLockState = ref.watch(appLockStateProvider); + return switch (appLockState) { + AsyncData(:final value) => value == AppLockState.unlocked + ? widget.child + : Scaffold( + backgroundColor: Colors.transparent, + body: BlurWidget( + above: Center( + child: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: TitleWidget( + textAlign: TextAlign.center, + textStyle: Theme.of(context).textTheme.headlineLarge, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text( + translations.appUnlock.widget.text(app: App.appName), + textAlign: TextAlign.center, + ), + ), + Align( + child: SizedBox( + width: math.min(MediaQuery.of(context).size.width, 300), + child: FilledButton.icon( + onPressed: value == AppLockState.unlockChallengedStarted ? null : tryUnlockIfNeeded, + label: Text(translations.appUnlock.widget.button), + icon: const Icon(Icons.key), + ), + ), + ), + ], ), ), - ], + below: widget.child, + ), ), - ), - ), - ); + _ => widget.child, + }; + } /// Tries to unlock the app. Future tryUnlockIfNeeded() async { - bool isUnlocked = await ref.read(appUnlockStateProvider.future); - if (!mounted) { - return; - } - if (isUnlocked) { - widget.onUnlock?.call(); - return; - } - setState(() => unlockChallengedStarted = true); - Result result = await ref.read(appUnlockStateProvider.notifier).unlock(context); - if (!mounted) { + AppLockState lockState = await ref.read(appLockStateProvider.future); + if (!mounted || lockState != AppLockState.locked) { return; } - setState(() => unlockChallengedStarted = false); - switch (result) { - case ResultSuccess(): - widget.onUnlock?.call(); - break; - case ResultError(): - SnackBarIcon.showErrorSnackBar(context, text: translations.error.appUnlock); - break; - default: - break; + Result result = await ref.read(appLockStateProvider.notifier).unlock(context); + if (result is ResultError && mounted) { + SnackBarIcon.showErrorSnackBar(context, text: translations.error.appUnlock); } } }