forked from baron/baron-sso
fix userfront mobile approval close flow
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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 = "활성화된 세션만 보려면 토글을 켜주세요."
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user