1
0
forked from baron/baron-sso

feat(userfront): improve magic link approval UX on mobile

- Fixes issue #852 where 'verify_failed' error was shown on remote approval
- Added specialized success view for remote-originated approval requests
- Added 'Close Window' action for mobile browsers
- Improved error handling for already verified/used tokens
- Added necessary i18n strings in Korean and English
This commit is contained in:
2026-05-19 17:59:10 +09:00
parent 9112c4fb36
commit bb918932f4
2 changed files with 120 additions and 8 deletions

View File

@@ -730,6 +730,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
String actionPath = '/',
bool autoRedirect = false,
Duration redirectDelay = const Duration(seconds: 2),
VoidCallback? onAction,
}) {
if (!mounted) return;
final resolvedTitle = title ?? tr('ui.userfront.login.verification.title');
@@ -743,16 +744,23 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_verificationTitle = resolvedTitle;
_verificationPageTitle = resolvedPageTitle;
_verificationActionLabel = resolvedActionLabel;
_onVerificationAction = onAction;
});
_verificationRedirectTimer?.cancel();
if (autoRedirect) {
_verificationRedirectTimer = Timer(redirectDelay, () {
if (!mounted) return;
context.go(actionPath);
if (onAction != null) {
onAction();
} else {
context.go(actionPath);
}
});
}
}
VoidCallback? _onVerificationAction;
Widget _buildVerificationResultView() {
return Center(
child: Padding(
@@ -785,6 +793,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 24),
FilledButton(
onPressed: () {
if (_onVerificationAction != null) {
_onVerificationAction!();
return;
}
final hasLocalSession =
(AuthTokenStore.getToken()?.isNotEmpty ?? false) ||
AuthTokenStore.usesCookie();
@@ -810,6 +822,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Future<void> _verifyToken(String token) async {
debugPrint("[Auth] Starting verification for token: $token");
final approvedMessage = tr('msg.userfront.login.verification.approved');
final remoteApprovedMessage =
tr('msg.userfront.login.verification.approved_remote');
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
);
@@ -829,7 +843,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (status == 'approved' || (jwt == null && _verificationOnly)) {
if (mounted) {
_markVerificationApproved(approvedMessage, actionPath: actionPath);
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
return;
}
@@ -851,6 +870,23 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
} catch (e) {
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
// Handle the case where the token is already verified/used (common in remote flows)
final errorStr = e.toString();
if (errorStr.contains('already_used') ||
errorStr.contains('already_verified') ||
errorStr.contains('session_active')) {
if (mounted) {
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
return;
}
if (mounted) {
_showError(
tr(
@@ -872,6 +908,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
"[Auth] Starting code verification for loginId: $sanitizedLoginId",
);
final approvedMessage = tr('msg.userfront.login.verification.approved');
final remoteApprovedMessage =
tr('msg.userfront.login.verification.approved_remote');
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
);
@@ -894,7 +932,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (jwt == null && status == 'approved') {
if (mounted) {
_markVerificationApproved(approvedMessage, actionPath: actionPath);
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
return;
}
@@ -908,7 +951,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return;
}
if (_verificationOnly) {
_markVerificationApproved(approvedMessage, actionPath: actionPath);
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
return;
}
_onLoginSuccess(jwt, provider: res['provider'] as String?);
@@ -916,12 +964,34 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
if (_verificationOnly && mounted) {
_markVerificationApproved(approvedMessage, actionPath: actionPath);
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
} catch (e) {
debugPrint(
"[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e",
);
// Handle the case where the code is already verified/used (common in remote flows)
final errorStr = e.toString();
if (errorStr.contains('already_used') ||
errorStr.contains('already_verified') ||
errorStr.contains('session_active')) {
if (mounted) {
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
return;
}
if (mounted) {
_showError(
tr(
@@ -938,6 +1008,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (sanitized.isEmpty) return;
debugPrint("[Auth] Starting short code verification for code: $sanitized");
final approvedMessage = tr('msg.userfront.login.verification.approved');
final remoteApprovedMessage =
tr('msg.userfront.login.verification.approved_remote');
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
);
@@ -956,7 +1028,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (jwt == null && status == 'approved') {
if (mounted) {
_markVerificationApproved(approvedMessage, actionPath: actionPath);
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
return;
}
@@ -970,7 +1047,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return;
}
if (_verificationOnly) {
_markVerificationApproved(approvedMessage, actionPath: actionPath);
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
return;
}
_onLoginSuccess(jwt, provider: res['provider'] as String?);
@@ -978,10 +1060,32 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
if (_verificationOnly && mounted) {
_markVerificationApproved(approvedMessage, actionPath: actionPath);
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
} catch (e) {
debugPrint("[Auth] Short code verification FAILED. Error: $e");
// Handle the case where the code is already verified/used (common in remote flows)
final errorStr = e.toString();
if (errorStr.contains('already_used') ||
errorStr.contains('already_verified') ||
errorStr.contains('session_active')) {
if (mounted) {
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(),
);
}
return;
}
if (mounted) {
_showError(
tr(