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;
|
return failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function makeWindowCloseNavigateToRoot(page: Page): Promise<void> {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
window.close = () => {
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test.describe('UserFront WASM auth routing', () => {
|
test.describe('UserFront WASM auth routing', () => {
|
||||||
test('비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다', async ({ page }) => {
|
test('비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다', async ({ page }) => {
|
||||||
await mockUserfrontApis(page, { sessionStatus: 401 });
|
await mockUserfrontApis(page, { sessionStatus: 401 });
|
||||||
@@ -239,6 +247,7 @@ test.describe('UserFront WASM auth routing', () => {
|
|||||||
verifyRequests.push({ path, body });
|
verifyRequests.push({ path, body });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await makeWindowCloseNavigateToRoot(page);
|
||||||
|
|
||||||
await page.goto('/ko/l/AB123456');
|
await page.goto('/ko/l/AB123456');
|
||||||
|
|
||||||
@@ -281,6 +290,7 @@ test.describe('UserFront WASM auth routing', () => {
|
|||||||
verifyCalls += 1;
|
verifyCalls += 1;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await makeWindowCloseNavigateToRoot(page);
|
||||||
|
|
||||||
await page.goto('/ko/l/AB123456');
|
await page.goto('/ko/l/AB123456');
|
||||||
|
|
||||||
@@ -375,6 +385,7 @@ test.describe('UserFront WASM auth routing', () => {
|
|||||||
verifyRequests.push({ path, body });
|
verifyRequests.push({ path, body });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await makeWindowCloseNavigateToRoot(page);
|
||||||
|
|
||||||
await page.goto('/ko/verify/e2e-email-token');
|
await page.goto('/ko/verify/e2e-email-token');
|
||||||
|
|
||||||
@@ -423,6 +434,7 @@ test.describe('UserFront WASM auth routing', () => {
|
|||||||
verifyRequests.push({ path, body });
|
verifyRequests.push({ path, body });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await makeWindowCloseNavigateToRoot(page);
|
||||||
|
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email',
|
'/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 = "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_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."
|
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."
|
success = "Sign-in approval completed."
|
||||||
|
|
||||||
[msg.userfront.login_success]
|
[msg.userfront.login_success]
|
||||||
@@ -584,6 +585,7 @@ action_label = "Done"
|
|||||||
action_label_close = "Close Window"
|
action_label_close = "Close Window"
|
||||||
page_title = "Sign-in approval"
|
page_title = "Sign-in approval"
|
||||||
title = "Approval complete"
|
title = "Approval complete"
|
||||||
|
title_pending = "Checking approval"
|
||||||
title_remote = "Sign-in approved"
|
title_remote = "Sign-in approved"
|
||||||
|
|
||||||
[ui.userfront.login_success]
|
[ui.userfront.login_success]
|
||||||
@@ -702,4 +704,3 @@ toggle_label = "Show active sessions only"
|
|||||||
|
|
||||||
[msg.userfront.audit.filter]
|
[msg.userfront.audit.filter]
|
||||||
description = "Toggle to view only active sessions."
|
description = "Toggle to view only active sessions."
|
||||||
|
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주
|
|||||||
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
|
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
|
||||||
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
|
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
|
||||||
approved_remote = "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요."
|
approved_remote = "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요."
|
||||||
|
pending_remote = "승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요."
|
||||||
success = "로그인 승인에 성공했습니다."
|
success = "로그인 승인에 성공했습니다."
|
||||||
|
|
||||||
[msg.userfront.login_success]
|
[msg.userfront.login_success]
|
||||||
@@ -807,6 +808,7 @@ page_title = "로그인 승인"
|
|||||||
title = "승인 완료"
|
title = "승인 완료"
|
||||||
action_label_close = "창 닫기"
|
action_label_close = "창 닫기"
|
||||||
title_remote = "로그인 승인 완료"
|
title_remote = "로그인 승인 완료"
|
||||||
|
title_pending = "로그인 승인 확인 중"
|
||||||
|
|
||||||
[ui.userfront.login_success]
|
[ui.userfront.login_success]
|
||||||
later = "나중에 하기 (대시보드로 이동)"
|
later = "나중에 하기 (대시보드로 이동)"
|
||||||
@@ -923,4 +925,3 @@ toggle_label = "활성 세션만 보기"
|
|||||||
|
|
||||||
[msg.userfront.audit.filter]
|
[msg.userfront.audit.filter]
|
||||||
description = "활성화된 세션만 보려면 토글을 켜주세요."
|
description = "활성화된 세션만 보려면 토글을 켜주세요."
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
_redirectUrl = widget.redirectUrl;
|
_redirectUrl = widget.redirectUrl;
|
||||||
_passwordFocusNode.addListener(_handlePasswordFocusChange);
|
_passwordFocusNode.addListener(_handlePasswordFocusChange);
|
||||||
HardwareKeyboard.instance.addHandler(_handleHardwareKeyEvent);
|
HardwareKeyboard.instance.addHandler(_handleHardwareKeyEvent);
|
||||||
|
_verificationOnly = _isVerificationOnlyUri(Uri.base);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final uri = Uri.base;
|
final uri = Uri.base;
|
||||||
@@ -127,8 +128,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
final hasVerificationToken =
|
final hasVerificationToken =
|
||||||
widget.verificationToken != null || hasTokenParam;
|
widget.verificationToken != null || hasTokenParam;
|
||||||
final hasLoginCode = loginIdParam != null && codeParam != null;
|
final hasLoginCode = loginIdParam != null && codeParam != null;
|
||||||
_verificationOnly =
|
|
||||||
hasVerificationToken || hasLoginCode || hasShortCodePath;
|
|
||||||
final notice = uri.queryParameters['notice'];
|
final notice = uri.queryParameters['notice'];
|
||||||
|
|
||||||
if (hasShortCodePath) {
|
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() {
|
void _handlePasswordFocusChange() {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -765,6 +773,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
_onVerificationAction?.call();
|
_onVerificationAction?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _closeVerificationWindowIfPossible() {
|
||||||
|
if (webWindow.hasOpener()) {
|
||||||
|
webWindow.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildVerificationResultView() {
|
Widget _buildVerificationResultView() {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -802,7 +816,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_verificationOnly) {
|
if (_verificationOnly) {
|
||||||
webWindow.close();
|
_closeVerificationWindowIfPossible();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final hasLocalSession =
|
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 {
|
Future<void> _verifyToken(String token) async {
|
||||||
debugPrint("[Auth] Starting verification for token: $token");
|
debugPrint("[Auth] Starting verification for token: $token");
|
||||||
final approvedMessage = tr('msg.userfront.login.verification.approved');
|
final approvedMessage = tr('msg.userfront.login.verification.approved');
|
||||||
@@ -858,7 +921,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -894,7 +957,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -951,7 +1014,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -972,7 +1035,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -985,7 +1048,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
remoteApprovedMessage,
|
remoteApprovedMessage,
|
||||||
title: tr('ui.userfront.login.verification.title_remote'),
|
title: tr('ui.userfront.login.verification.title_remote'),
|
||||||
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
|
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1005,7 +1068,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1053,7 +1116,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1074,7 +1137,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1087,7 +1150,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
remoteApprovedMessage,
|
remoteApprovedMessage,
|
||||||
title: tr('ui.userfront.login.verification.title_remote'),
|
title: tr('ui.userfront.login.verification.title_remote'),
|
||||||
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
|
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1105,7 +1168,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
actionLabel: tr(
|
actionLabel: tr(
|
||||||
'ui.userfront.login.verification.action_label_close',
|
'ui.userfront.login.verification.action_label_close',
|
||||||
),
|
),
|
||||||
onAction: () => webWindow.close(),
|
onAction: _closeVerificationWindowIfPossible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1609,21 +1672,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_verificationOnly && _verificationApproved) {
|
if (_verificationOnly) {
|
||||||
return Scaffold(
|
return _buildVerificationOnlyScaffold();
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -649,6 +649,8 @@ const Map<String, String> koStrings = {
|
|||||||
"승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요.",
|
"승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요.",
|
||||||
"msg.userfront.login.verification.approved_local":
|
"msg.userfront.login.verification.approved_local":
|
||||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
||||||
|
"msg.userfront.login.verification.pending_remote":
|
||||||
|
"승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요.",
|
||||||
"msg.userfront.login.verification.success": "로그인 승인에 성공했습니다.",
|
"msg.userfront.login.verification.success": "로그인 승인에 성공했습니다.",
|
||||||
"msg.userfront.login.verification_failed": "승인 처리에 실패했습니다: {{error}}",
|
"msg.userfront.login.verification_failed": "승인 처리에 실패했습니다: {{error}}",
|
||||||
"msg.userfront.login_success.subtitle": "성공적으로 로그인되었습니다.",
|
"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.action_label_close": "창 닫기",
|
||||||
"ui.userfront.login.verification.page_title": "로그인 승인",
|
"ui.userfront.login.verification.page_title": "로그인 승인",
|
||||||
"ui.userfront.login.verification.title": "승인 완료",
|
"ui.userfront.login.verification.title": "승인 완료",
|
||||||
|
"ui.userfront.login.verification.title_pending": "로그인 승인 확인 중",
|
||||||
"ui.userfront.login.verification.title_remote": "로그인 승인 완료",
|
"ui.userfront.login.verification.title_remote": "로그인 승인 완료",
|
||||||
"ui.userfront.login_success.later": "나중에 하기 (대시보드로 이동)",
|
"ui.userfront.login_success.later": "나중에 하기 (대시보드로 이동)",
|
||||||
"ui.userfront.login_success.qr": "QR 인증 (카메라 켜기)",
|
"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.",
|
"Approved. Please return to the original browser or PC screen.",
|
||||||
"msg.userfront.login.verification.approved_local":
|
"msg.userfront.login.verification.approved_local":
|
||||||
"Approved. This device is already signed in, and the remote window will be signed in shortly.",
|
"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.success": "Sign-in approval completed.",
|
||||||
"msg.userfront.login.verification_failed":
|
"msg.userfront.login.verification_failed":
|
||||||
"Failed to approve the sign-in request: {{error}}",
|
"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.action_label_close": "Close Window",
|
||||||
"ui.userfront.login.verification.page_title": "Sign-in approval",
|
"ui.userfront.login.verification.page_title": "Sign-in approval",
|
||||||
"ui.userfront.login.verification.title": "Approval complete",
|
"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.verification.title_remote": "Sign-in approved",
|
||||||
"ui.userfront.login_success.later": "Do this later (go to dashboard)",
|
"ui.userfront.login_success.later": "Do this later (go to dashboard)",
|
||||||
"ui.userfront.login_success.qr": "Use QR approval",
|
"ui.userfront.login_success.qr": "Use QR approval",
|
||||||
|
|||||||
Reference in New Issue
Block a user