Skip to content

Commit 0128ea1

Browse files
authored
Merge pull request #40 from openpatch/copilot/fix-losing-context-when-locking
fix: restore navigation context after unlock
2 parents ce69f5a + d9e88e3 commit 0128ea1

2 files changed

Lines changed: 32 additions & 13 deletions

File tree

lib/features/setup/unlock_screen.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,7 @@ class _UnlockScreenState extends ConsumerState<UnlockScreen> {
201201

202202
setState(() => _isUnlocking = true);
203203
try {
204-
final success = await ref.read(appSessionProvider).unlock(passphrase);
205-
if (!mounted || !success) {
206-
return;
207-
}
208-
context.go('/groups');
204+
await ref.read(appSessionProvider).unlock(passphrase);
209205
} finally {
210206
if (mounted) {
211207
setState(() => _isUnlocking = false);
@@ -216,14 +212,11 @@ class _UnlockScreenState extends ConsumerState<UnlockScreen> {
216212
Future<void> _unlockWithBiometrics() async {
217213
setState(() => _isUnlocking = true);
218214
try {
219-
final success = await ref
215+
await ref
220216
.read(appSessionProvider)
221217
.unlockWithBiometrics(
222218
localizedReason: 'biometric_reason'.tr(),
223219
);
224-
if (!mounted) return;
225-
if (!success) return;
226-
context.go('/groups');
227220
} finally {
228221
if (mounted) setState(() => _isUnlocking = false);
229222
}

lib/shared/router/app_router.dart

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,24 @@ final routerProvider = Provider<GoRouter>((ref) {
2828
redirect: (context, state) {
2929
switch (session.status) {
3030
case AppSessionStatus.loading:
31+
// Stay on /unlock during an unlock attempt so the `from` query
32+
// parameter is preserved across the loading → ready transition.
33+
if (state.matchedLocation == '/unlock') return null;
3134
return state.matchedLocation == '/loading' ? null : '/loading';
3235
case AppSessionStatus.needsSetup:
3336
return state.matchedLocation == '/setup' ? null : '/setup';
3437
case AppSessionStatus.locked:
35-
return state.matchedLocation == '/unlock' ||
36-
state.matchedLocation == '/recover'
37-
? null
38-
: '/unlock';
38+
if (state.matchedLocation == '/unlock' ||
39+
state.matchedLocation == '/recover') {
40+
return null;
41+
}
42+
// Capture the current location so it can be restored after unlock.
43+
// Strip fragments (client-side only, not meaningful across auth).
44+
final currentUri = state.uri.removeFragment();
45+
return Uri(
46+
path: '/unlock',
47+
queryParameters: {'from': currentUri.toString()},
48+
).toString();
3949
case AppSessionStatus.ready:
4050
if (session.hasPendingRecoveryKey) {
4151
return state.matchedLocation == '/setup/recovery'
@@ -48,6 +58,22 @@ final routerProvider = Provider<GoRouter>((ref) {
4858
state.matchedLocation == '/unlock' ||
4959
state.matchedLocation == '/recover' ||
5060
state.matchedLocation == '/') {
61+
// Restore the location the user was at before the app locked.
62+
final from = state.uri.queryParameters['from'];
63+
if (from != null && from.isNotEmpty) {
64+
final fromUri = Uri.tryParse(from);
65+
// Only allow relative paths (no scheme or host) to prevent open
66+
// redirect attacks. Explicitly reject protocol-relative URLs
67+
// (e.g. //evil.com) as a defense-in-depth measure before
68+
// parsing, since Uri.parse may handle them unexpectedly.
69+
if (!from.startsWith('//') &&
70+
fromUri != null &&
71+
fromUri.scheme.isEmpty &&
72+
fromUri.host.isEmpty &&
73+
fromUri.path.startsWith('/')) {
74+
return from;
75+
}
76+
}
5177
return '/groups';
5278
}
5379
return null;

0 commit comments

Comments
 (0)