diff --git a/mobile-app/lib/app_lifecycle_manager.dart b/mobile-app/lib/app_lifecycle_manager.dart index de441ed8..4cc38e4c 100644 --- a/mobile-app/lib/app_lifecycle_manager.dart +++ b/mobile-app/lib/app_lifecycle_manager.dart @@ -97,7 +97,9 @@ class _AppLifecycleManagerState extends ConsumerState with print('App resumed but offline - polling paused'); } - // Always check authentication on resume to enforce inactivity timeout + // Check authentication ONLY on resume from background. + // This prevents flicker from transient backgrounds (FaceID, system overlays) + // that briefly pause/resume the app. localAuthNotifier.checkAuthentication(); // Initialize Taskmaster login if wallet exists @@ -108,17 +110,18 @@ class _AppLifecycleManagerState extends ConsumerState with } } else { // Handle background states (inactive, paused, hidden, detached) - // Only act if we haven't already processed a background transition if (!_isBackgrounded) { - print('AppLifecycleState.$state - pausing and locking'); + print('AppLifecycleState.$state - pausing (update pause time only)'); _isBackgrounded = true; // Pause global polling when app goes to background // Transaction tracking continues for pending transactions pollingManager.pausePolling(); - // When the app goes into the background, lock it. - localAuthNotifier.lockApp(); + // Update last paused time for timeout calculation, but DO NOT lock + // the UI immediately. This avoids flicker on short system pauses. + // The checkAuthentication() on resume will decide if auth is needed. + localAuthNotifier.recordBackgroundTime(); } else { print('AppLifecycleState.$state - already backgrounded, skipping actions'); } diff --git a/mobile-app/lib/generated/version.g.dart b/mobile-app/lib/generated/version.g.dart index 1b9e87e9..ecd72fc0 100644 --- a/mobile-app/lib/generated/version.g.dart +++ b/mobile-app/lib/generated/version.g.dart @@ -1,2 +1,2 @@ -const appVersion = '1.3.0'; -const appBuildNumber = '85'; +const appVersion = '1.3.2'; +const appBuildNumber = '91'; diff --git a/mobile-app/lib/providers/local_auth_provider.dart b/mobile-app/lib/providers/local_auth_provider.dart index 0493b61e..3a2bd668 100644 --- a/mobile-app/lib/providers/local_auth_provider.dart +++ b/mobile-app/lib/providers/local_auth_provider.dart @@ -4,13 +4,15 @@ import 'package:resonance_network_wallet/services/local_auth_service.dart'; class LocalAuthState { final bool isAuthenticated; final bool isAuthenticating; + final bool isVisuallyLocked; - LocalAuthState({this.isAuthenticated = false, this.isAuthenticating = false}); + LocalAuthState({this.isAuthenticated = true, this.isAuthenticating = false, this.isVisuallyLocked = false}); - LocalAuthState copyWith({bool? isAuthenticated, bool? isAuthenticating}) { + LocalAuthState copyWith({bool? isAuthenticated, bool? isAuthenticating, bool? isVisuallyLocked}) { return LocalAuthState( isAuthenticated: isAuthenticated ?? this.isAuthenticated, isAuthenticating: isAuthenticating ?? this.isAuthenticating, + isVisuallyLocked: isVisuallyLocked ?? this.isVisuallyLocked, ); } } @@ -35,20 +37,27 @@ class LocalAuthController extends StateNotifier { localizedReason: 'Please authenticate to access your wallet', ); - state = state.copyWith(isAuthenticated: didAuthenticate, isAuthenticating: false); + state = state.copyWith(isAuthenticated: didAuthenticate, isAuthenticating: false, isVisuallyLocked: false); } void checkAuthentication() { if (_localAuthService.shouldRequireAuthentication()) { + final alreadyAuthenticating = state.isAuthenticating; state = state.copyWith(isAuthenticated: false); - authenticate(); + if (!alreadyAuthenticating) { + authenticate(); + } } else { - state = state.copyWith(isAuthenticated: true); + state = state.copyWith(isAuthenticated: true, isAuthenticating: false, isVisuallyLocked: false); } } - void lockApp() { + void recordBackgroundTime() { _localAuthService.updateLastPausedTime(); - state = state.copyWith(isAuthenticated: false); + state = state.copyWith(isVisuallyLocked: true); + } + + void clearVisualLock() { + state = state.copyWith(isVisuallyLocked: false); } } diff --git a/mobile-app/lib/services/local_auth_service.dart b/mobile-app/lib/services/local_auth_service.dart index e9fb5ee2..cdfee453 100644 --- a/mobile-app/lib/services/local_auth_service.dart +++ b/mobile-app/lib/services/local_auth_service.dart @@ -11,7 +11,7 @@ class LocalAuthService { final LocalAuthentication _localAuth = LocalAuthentication(); final SettingsService _settingsService = SettingsService(); - static const _authTimeout = Duration(seconds: 30); + static const _authTimeout = Duration(seconds: 10); Future isBiometricAvailable() async { try { @@ -45,10 +45,14 @@ class LocalAuthService { final didAuthenticate = await _localAuth.authenticate( localizedReason: localizedReason, - options: const AuthenticationOptions(biometricOnly: false, stickyAuth: true, sensitiveTransaction: true), + options: const AuthenticationOptions( + biometricOnly: false, + stickyAuth: true, + sensitiveTransaction: true, + ), ); - if (didAuthenticate) _cleanLastPausedTime(); + if (didAuthenticate) cleanLastPausedTime(); return didAuthenticate; } on PlatformException catch (e) { debugPrint('Platform exception during authentication: $e'); @@ -74,7 +78,7 @@ class LocalAuthService { _settingsService.setLastPausedTime(DateTime.now()); } - void _cleanLastPausedTime() { + void cleanLastPausedTime() { _settingsService.cleanLastPausedTime(); } diff --git a/mobile-app/lib/v2/screens/auth/auth_wrapper.dart b/mobile-app/lib/v2/screens/auth/auth_wrapper.dart index 71279f03..0985946c 100644 --- a/mobile-app/lib/v2/screens/auth/auth_wrapper.dart +++ b/mobile-app/lib/v2/screens/auth/auth_wrapper.dart @@ -14,41 +14,61 @@ class AuthWrapper extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(localAuthProvider); - if (authState.isAuthenticated) { - // If authenticated, be invisible. - return const SizedBox.shrink(); + if (!authState.isAuthenticated) { + return _buildLockScreen(context, ref, authState.isAuthenticating); } - return _buildLockScreen(context, ref, authState.isAuthenticating); + if (authState.isVisuallyLocked) { + return _buildPrivacyOverlay(context); + } + + return const SizedBox.shrink(); + } + + Widget _buildPrivacyOverlay(BuildContext context) { + return Scaffold( + backgroundColor: context.colors.background, + body: GradientBackground(child: Center(child: Image.asset('assets/v2/auth_wrapper_bracket.png'))), + ); } Widget _buildLockScreen(BuildContext context, WidgetRef ref, bool isAuthenticating) { return Scaffold( backgroundColor: context.colors.background, body: GradientBackground( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - alignment: Alignment.center, - children: [ - Image.asset('assets/v2/auth_wrapper_bracket.png'), - Text('Authorization \n Required', style: context.themeText.lockTitle, textAlign: TextAlign.center), - ], - ), - const SizedBox(height: 120), - Padding( - padding: EdgeInsets.symmetric(horizontal: context.themeSize.screenPadding), - child: GlassButton.simple( - label: 'Unlock Wallet', - onTap: () { - ref.read(localAuthProvider.notifier).authenticate(); - }, - variant: ButtonVariant.secondary, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: Alignment.center, + children: [ + Image.asset('assets/v2/auth_wrapper_bracket.png'), + Text('Authorization \n Required', style: context.themeText.lockTitle, textAlign: TextAlign.center), + ], + ), + const SizedBox(height: 60), + if (isAuthenticating) + const CircularProgressIndicator() + else + Padding( + padding: EdgeInsets.symmetric(horizontal: context.themeSize.screenPadding), + child: GlassButton.simple( + label: 'Unlock Wallet', + onTap: () { + ref.read(localAuthProvider.notifier).authenticate(); + }, + variant: ButtonVariant.secondary, + ), + ), + const SizedBox(height: 40), + Text( + isAuthenticating ? 'Authenticating...' : 'Use device biometrics to unlock', + style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), + textAlign: TextAlign.center, ), - ), - ], + ], + ), ), ), ); diff --git a/mobile-app/lib/v2/screens/send/send_sheet.dart b/mobile-app/lib/v2/screens/send/send_sheet.dart index fb38cf43..44e4f06a 100644 --- a/mobile-app/lib/v2/screens/send/send_sheet.dart +++ b/mobile-app/lib/v2/screens/send/send_sheet.dart @@ -11,6 +11,7 @@ import 'package:resonance_network_wallet/v2/screens/send/send_screen_logic.dart' import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; +import 'package:resonance_network_wallet/services/local_auth_service.dart'; import 'package:resonance_network_wallet/services/transaction_submission_service.dart'; import 'package:resonance_network_wallet/v2/components/success_check.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; @@ -193,6 +194,12 @@ class _SendSheetState extends ConsumerState { void _backToForm() => setState(() => _step = _Step.form); Future _confirmSend() async { + final authed = await LocalAuthService().authenticate(localizedReason: 'Authenticate to confirm transaction'); + if (!authed || !mounted) { + if (mounted) setState(() => _errorMessage = 'Authentication required to send'); + return; + } + setState(() { _step = _Step.sending; _errorMessage = null; diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index f32d4549..c022048e 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -1085,10 +1085,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -1744,10 +1744,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.10" timezone: dependency: "direct main" description: diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 6c3053ae..52d1c2ca 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -2,7 +2,7 @@ name: resonance_network_wallet description: A Flutter wallet for the Quantus blockchain. publish_to: "none" -version: 1.3.1+86 +version: 1.3.2+91 environment: sdk: ">=3.8.0 <4.0.0"