From e54cc121c798f42722d603938d4e0110322c0d8f Mon Sep 17 00:00:00 2001 From: Lectom Date: Thu, 21 May 2026 18:14:31 +0900 Subject: [PATCH] fix userfront mobile approval close flow --- userfront-e2e/tests/auth-routing.spec.ts | 12 ++ userfront/assets/translations/en.toml | 3 +- userfront/assets/translations/ko.toml | 3 +- .../auth/presentation/login_screen.dart | 106 +++++++++++++----- userfront/lib/i18n_data.dart | 6 + 5 files changed, 100 insertions(+), 30 deletions(-) diff --git a/userfront-e2e/tests/auth-routing.spec.ts b/userfront-e2e/tests/auth-routing.spec.ts index aadd4e68..8e33ce05 100644 --- a/userfront-e2e/tests/auth-routing.spec.ts +++ b/userfront-e2e/tests/auth-routing.spec.ts @@ -160,6 +160,14 @@ function collectClientFailures(page: Page): string[] { return failures; } +async function makeWindowCloseNavigateToRoot(page: Page): Promise { + await page.addInitScript(() => { + window.close = () => { + window.location.href = '/'; + }; + }); +} + test.describe('UserFront WASM auth routing', () => { test('비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다', async ({ page }) => { await mockUserfrontApis(page, { sessionStatus: 401 }); @@ -239,6 +247,7 @@ test.describe('UserFront WASM auth routing', () => { verifyRequests.push({ path, body }); }, }); + await makeWindowCloseNavigateToRoot(page); await page.goto('/ko/l/AB123456'); @@ -281,6 +290,7 @@ test.describe('UserFront WASM auth routing', () => { verifyCalls += 1; }, }); + await makeWindowCloseNavigateToRoot(page); await page.goto('/ko/l/AB123456'); @@ -375,6 +385,7 @@ test.describe('UserFront WASM auth routing', () => { verifyRequests.push({ path, body }); }, }); + await makeWindowCloseNavigateToRoot(page); await page.goto('/ko/verify/e2e-email-token'); @@ -423,6 +434,7 @@ test.describe('UserFront WASM auth routing', () => { verifyRequests.push({ path, body }); }, }); + await makeWindowCloseNavigateToRoot(page); await page.goto( '/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email', diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index d2dde8b4..dd073f41 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -232,6 +232,7 @@ body = "We could not find an account for that information.\\\\\\\\\\\\\\\\nPleas approved = "Approved. Complete sign-in in the original window." approved_local = "Approved. This device is already signed in, and the remote window will be signed in shortly." approved_remote = "Approved. Please return to the original browser or PC screen." +pending_remote = "Checking the sign-in approval request. Please wait." success = "Sign-in approval completed." [msg.userfront.login_success] @@ -584,6 +585,7 @@ action_label = "Done" action_label_close = "Close Window" page_title = "Sign-in approval" title = "Approval complete" +title_pending = "Checking approval" title_remote = "Sign-in approved" [ui.userfront.login_success] @@ -702,4 +704,3 @@ toggle_label = "Show active sessions only" [msg.userfront.audit.filter] description = "Toggle to view only active sessions." - diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 2267a364..aa76bb49 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -456,6 +456,7 @@ body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주 approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다." approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다" approved_remote = "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요." +pending_remote = "승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요." success = "로그인 승인에 성공했습니다." [msg.userfront.login_success] @@ -807,6 +808,7 @@ page_title = "로그인 승인" title = "승인 완료" action_label_close = "창 닫기" title_remote = "로그인 승인 완료" +title_pending = "로그인 승인 확인 중" [ui.userfront.login_success] later = "나중에 하기 (대시보드로 이동)" @@ -923,4 +925,3 @@ toggle_label = "활성 세션만 보기" [msg.userfront.audit.filter] description = "활성화된 세션만 보려면 토글을 켜주세요." - diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index bcce7d15..8b1a7866 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -100,6 +100,7 @@ class _LoginScreenState extends ConsumerState _redirectUrl = widget.redirectUrl; _passwordFocusNode.addListener(_handlePasswordFocusChange); HardwareKeyboard.instance.addHandler(_handleHardwareKeyEvent); + _verificationOnly = _isVerificationOnlyUri(Uri.base); WidgetsBinding.instance.addPostFrameCallback((_) async { final uri = Uri.base; @@ -127,8 +128,6 @@ class _LoginScreenState extends ConsumerState final hasVerificationToken = widget.verificationToken != null || hasTokenParam; final hasLoginCode = loginIdParam != null && codeParam != null; - _verificationOnly = - hasVerificationToken || hasLoginCode || hasShortCodePath; final notice = uri.queryParameters['notice']; if (hasShortCodePath) { @@ -174,6 +173,15 @@ class _LoginScreenState extends ConsumerState }); } + bool _isVerificationOnlyUri(Uri uri) { + final loginIdParam = uri.queryParameters['loginId']; + final codeParam = uri.queryParameters['code']; + return widget.verificationToken != null || + uri.queryParameters.containsKey('t') || + (loginIdParam != null && codeParam != null) || + extractLoginShortCode(uri) != null; + } + void _handlePasswordFocusChange() { if (!mounted) { return; @@ -765,6 +773,12 @@ class _LoginScreenState extends ConsumerState _onVerificationAction?.call(); } + void _closeVerificationWindowIfPossible() { + if (webWindow.hasOpener()) { + webWindow.close(); + } + } + Widget _buildVerificationResultView() { return Center( child: Padding( @@ -802,7 +816,7 @@ class _LoginScreenState extends ConsumerState return; } if (_verificationOnly) { - webWindow.close(); + _closeVerificationWindowIfPossible(); return; } final hasLocalSession = @@ -827,6 +841,55 @@ class _LoginScreenState extends ConsumerState ); } + Widget _buildVerificationPendingView() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + width: 48, + height: 48, + child: CircularProgressIndicator(strokeWidth: 4), + ), + const SizedBox(height: 20), + Text( + tr('ui.userfront.login.verification.title_pending'), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + Text( + tr('msg.userfront.login.verification.pending_remote'), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black54), + ), + ], + ), + ), + ); + } + + Widget _buildVerificationOnlyScaffold() { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_verificationPageTitle), + leading: _verificationApproved && _onVerificationAction != null + ? IconButton( + icon: const Icon(Icons.close), + onPressed: _runVerificationExitAction, + ) + : null, + actions: const [ThemeToggleButton(compact: true)], + ), + body: _verificationApproved + ? _buildVerificationResultView() + : _buildVerificationPendingView(), + ); + } + Future _verifyToken(String token) async { debugPrint("[Auth] Starting verification for token: $token"); final approvedMessage = tr('msg.userfront.login.verification.approved'); @@ -858,7 +921,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } return; @@ -894,7 +957,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } return; @@ -951,7 +1014,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } return; @@ -972,7 +1035,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); return; } @@ -985,7 +1048,7 @@ class _LoginScreenState extends ConsumerState remoteApprovedMessage, title: tr('ui.userfront.login.verification.title_remote'), actionLabel: tr('ui.userfront.login.verification.action_label_close'), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } } catch (e) { @@ -1005,7 +1068,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } return; @@ -1053,7 +1116,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } return; @@ -1074,7 +1137,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); return; } @@ -1087,7 +1150,7 @@ class _LoginScreenState extends ConsumerState remoteApprovedMessage, title: tr('ui.userfront.login.verification.title_remote'), actionLabel: tr('ui.userfront.login.verification.action_label_close'), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } } catch (e) { @@ -1105,7 +1168,7 @@ class _LoginScreenState extends ConsumerState actionLabel: tr( 'ui.userfront.login.verification.action_label_close', ), - onAction: () => webWindow.close(), + onAction: _closeVerificationWindowIfPossible, ); } return; @@ -1609,21 +1672,8 @@ class _LoginScreenState extends ConsumerState ), ); - if (_verificationOnly && _verificationApproved) { - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - title: Text(_verificationPageTitle), - leading: _onVerificationAction == null - ? null - : IconButton( - icon: const Icon(Icons.close), - onPressed: _runVerificationExitAction, - ), - actions: const [ThemeToggleButton(compact: true)], - ), - body: _buildVerificationResultView(), - ); + if (_verificationOnly) { + return _buildVerificationOnlyScaffold(); } return Scaffold( diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index 43336b7b..e6ddec23 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -649,6 +649,8 @@ const Map koStrings = { "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요.", "msg.userfront.login.verification.approved_local": "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다", + "msg.userfront.login.verification.pending_remote": + "승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요.", "msg.userfront.login.verification.success": "로그인 승인에 성공했습니다.", "msg.userfront.login.verification_failed": "승인 처리에 실패했습니다: {{error}}", "msg.userfront.login_success.subtitle": "성공적으로 로그인되었습니다.", @@ -1919,6 +1921,7 @@ const Map koStrings = { "ui.userfront.login.verification.action_label_close": "창 닫기", "ui.userfront.login.verification.page_title": "로그인 승인", "ui.userfront.login.verification.title": "승인 완료", + "ui.userfront.login.verification.title_pending": "로그인 승인 확인 중", "ui.userfront.login.verification.title_remote": "로그인 승인 완료", "ui.userfront.login_success.later": "나중에 하기 (대시보드로 이동)", "ui.userfront.login_success.qr": "QR 인증 (카메라 켜기)", @@ -2785,6 +2788,8 @@ const Map enStrings = { "Approved. Please return to the original browser or PC screen.", "msg.userfront.login.verification.approved_local": "Approved. This device is already signed in, and the remote window will be signed in shortly.", + "msg.userfront.login.verification.pending_remote": + "Checking the sign-in approval request. Please wait.", "msg.userfront.login.verification.success": "Sign-in approval completed.", "msg.userfront.login.verification_failed": "Failed to approve the sign-in request: {{error}}", @@ -4119,6 +4124,7 @@ const Map enStrings = { "ui.userfront.login.verification.action_label_close": "Close Window", "ui.userfront.login.verification.page_title": "Sign-in approval", "ui.userfront.login.verification.title": "Approval complete", + "ui.userfront.login.verification.title_pending": "Checking approval", "ui.userfront.login.verification.title_remote": "Sign-in approved", "ui.userfront.login_success.later": "Do this later (go to dashboard)", "ui.userfront.login_success.qr": "Use QR approval",