1
0
forked from baron/baron-sso

fix userfront mobile approval close flow

This commit is contained in:
2026-05-21 18:14:31 +09:00
parent 66687a4c73
commit e54cc121c7
5 changed files with 100 additions and 30 deletions

View File

@@ -160,6 +160,14 @@ function collectClientFailures(page: Page): string[] {
return failures;
}
async function makeWindowCloseNavigateToRoot(page: Page): Promise<void> {
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',

View File

@@ -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."

View File

@@ -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 = "활성화된 세션만 보려면 토글을 켜주세요."

View File

@@ -100,6 +100,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_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<LoginScreen>
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<LoginScreen>
});
}
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<LoginScreen>
_onVerificationAction?.call();
}
void _closeVerificationWindowIfPossible() {
if (webWindow.hasOpener()) {
webWindow.close();
}
}
Widget _buildVerificationResultView() {
return Center(
child: Padding(
@@ -802,7 +816,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return;
}
if (_verificationOnly) {
webWindow.close();
_closeVerificationWindowIfPossible();
return;
}
final hasLocalSession =
@@ -827,6 +841,55 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
);
}
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<void> _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<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
}
return;
@@ -894,7 +957,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
}
return;
@@ -951,7 +1014,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
}
return;
@@ -972,7 +1035,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
return;
}
@@ -985,7 +1048,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
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<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
}
return;
@@ -1053,7 +1116,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
}
return;
@@ -1074,7 +1137,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
return;
}
@@ -1087,7 +1150,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
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<LoginScreen>
actionLabel: tr(
'ui.userfront.login.verification.action_label_close',
),
onAction: () => webWindow.close(),
onAction: _closeVerificationWindowIfPossible,
);
}
return;
@@ -1609,21 +1672,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
);
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(

View File

@@ -649,6 +649,8 @@ const Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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",