Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions mobile-app/lib/app_lifecycle_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ class _AppLifecycleManagerState extends ConsumerState<AppLifecycleManager> 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
Expand All @@ -108,17 +110,18 @@ class _AppLifecycleManagerState extends ConsumerState<AppLifecycleManager> 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');
}
Expand Down
4 changes: 2 additions & 2 deletions mobile-app/lib/generated/version.g.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
const appVersion = '1.3.0';
const appBuildNumber = '85';
const appVersion = '1.3.2';
const appBuildNumber = '91';
23 changes: 16 additions & 7 deletions mobile-app/lib/providers/local_auth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
Expand All @@ -35,20 +37,27 @@ class LocalAuthController extends StateNotifier<LocalAuthState> {
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);
}
}
12 changes: 8 additions & 4 deletions mobile-app/lib/services/local_auth_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> isBiometricAvailable() async {
try {
Expand Down Expand Up @@ -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');
Expand All @@ -74,7 +78,7 @@ class LocalAuthService {
_settingsService.setLastPausedTime(DateTime.now());
}

void _cleanLastPausedTime() {
void cleanLastPausedTime() {
_settingsService.cleanLastPausedTime();
}

Expand Down
72 changes: 46 additions & 26 deletions mobile-app/lib/v2/screens/auth/auth_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
),
],
],
),
),
),
);
Expand Down
7 changes: 7 additions & 0 deletions mobile-app/lib/v2/screens/send/send_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -193,6 +194,12 @@ class _SendSheetState extends ConsumerState<SendSheet> {
void _backToForm() => setState(() => _step = _Step.form);

Future<void> _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;
Expand Down
8 changes: 4 additions & 4 deletions mobile-app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion mobile-app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading