1
0
forked from baron/baron-sso

린트 적용

This commit is contained in:
2026-02-12 10:39:47 +09:00
parent 21b9594de5
commit 74884f6616
65 changed files with 26389 additions and 1583 deletions

View File

@@ -18,7 +18,12 @@ class LoginScreen extends ConsumerStatefulWidget {
final String? loginChallenge;
final String? redirectUrl;
const LoginScreen({super.key, this.verificationToken, this.loginChallenge, this.redirectUrl});
const LoginScreen({
super.key,
this.verificationToken,
this.loginChallenge,
this.redirectUrl,
});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
@@ -28,7 +33,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final TextEditingController _linkIdController = TextEditingController();
final TextEditingController _passwordLoginIdController = TextEditingController();
final TextEditingController _passwordLoginIdController =
TextEditingController();
final TextEditingController _passwordController = TextEditingController();
String? _redirectUrl;
String? _loginChallenge;
@@ -41,8 +47,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
int _qrRemainingSeconds = 0;
Timer? _qrCountdownTimer;
int _qrPollIntervalMs = 2000;
final TextEditingController _shortCodePrefixController = TextEditingController();
final TextEditingController _shortCodeDigitsController = TextEditingController();
final TextEditingController _shortCodePrefixController =
TextEditingController();
final TextEditingController _shortCodeDigitsController =
TextEditingController();
String? _linkPendingRef;
String? _lastLinkLoginId;
bool _lastLinkIsEmail = true;
@@ -75,12 +83,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
super.initState();
_tabController = TabController(length: 3, vsync: this, initialIndex: 1);
_tabController.addListener(_handleTabSelection);
_drySendEnabled = _parseBoolParam(Uri.base.queryParameters['drySend']) && !AuthProxyService.isProdEnv;
_drySendEnabled =
_parseBoolParam(Uri.base.queryParameters['drySend']) &&
!AuthProxyService.isProdEnv;
_redirectUrl = widget.redirectUrl;
WidgetsBinding.instance.addPostFrameCallback((_) async {
final uri = Uri.base;
if (_redirectUrl == null) {
if (uri.queryParameters.containsKey('redirect_url')) {
_redirectUrl = uri.queryParameters['redirect_url'];
@@ -89,15 +99,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
_loginChallenge = widget.loginChallenge ?? uri.queryParameters['login_challenge'];
_loginChallenge =
widget.loginChallenge ?? uri.queryParameters['login_challenge'];
final loginIdParam = uri.queryParameters['loginId'];
final codeParam = uri.queryParameters['code'];
final pendingRefParam = uri.queryParameters['pendingRef'];
final hasShortCodePath = uri.pathSegments.length >= 2 && uri.pathSegments.first == 'l';
final hasShortCodePath =
uri.pathSegments.length >= 2 && uri.pathSegments.first == 'l';
final hasTokenParam = uri.queryParameters.containsKey('t');
final hasVerificationToken = widget.verificationToken != null || hasTokenParam;
final hasVerificationToken =
widget.verificationToken != null || hasTokenParam;
final hasLoginCode = loginIdParam != null && codeParam != null;
_verificationOnly = hasVerificationToken || hasLoginCode || hasShortCodePath;
_verificationOnly =
hasVerificationToken || hasLoginCode || hasShortCodePath;
final notice = uri.queryParameters['notice'];
if (hasShortCodePath) {
@@ -150,9 +164,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
tr(
'msg.userfront.login.cookie_check_failed',
fallback: '로그인 확인 실패: {{error}}',
params: {
'error': e.toString().replaceFirst('Exception: ', ''),
},
params: {'error': e.toString().replaceFirst('Exception: ', '')},
),
);
}
@@ -171,8 +183,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final token = AuthTokenStore.getToken();
if (token != null && token.isNotEmpty) {
if (WebAuthIntegration.isPopup() || (_redirectUrl != null && _redirectUrl!.isNotEmpty)) {
debugPrint("[Auth] Cookie session with external integration. Notifying...");
if (WebAuthIntegration.isPopup() ||
(_redirectUrl != null && _redirectUrl!.isNotEmpty)) {
debugPrint(
"[Auth] Cookie session with external integration. Notifying...",
);
WebAuthIntegration.sendLoginSuccess(token);
return;
}
@@ -200,7 +215,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
try {
await AuthProxyService.checkCookieSession();
AuthTokenStore.setCookieMode(provider: AuthTokenStore.getProvider() ?? 'ory');
AuthTokenStore.setCookieMode(
provider: AuthTokenStore.getProvider() ?? 'ory',
);
await _acceptOidcLoginAndRedirect();
} catch (e) {
debugPrint("[Auth] OIDC auto-accept cookie check failed: $e");
@@ -289,7 +306,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final parts = jwt.split('.');
if (parts.length != 3) return 'User';
final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
final payload = utf8.decode(
base64Url.decode(base64Url.normalize(parts[1])),
);
final data = json.decode(payload);
return data['name'] ?? data['email'] ?? data['sub'] ?? 'User';
} catch (e) {
@@ -360,65 +379,65 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
void _startQrPolling() {
_qrPollingTimer?.cancel();
_qrPollingTimer = Timer.periodic(Duration(milliseconds: _qrPollIntervalMs), (timer) async {
if (_qrPendingRef == null || !mounted || _qrRemainingSeconds <= 0) {
timer.cancel();
return;
}
_qrPollingTimer = Timer.periodic(
Duration(milliseconds: _qrPollIntervalMs),
(timer) async {
if (_qrPendingRef == null || !mounted || _qrRemainingSeconds <= 0) {
timer.cancel();
return;
}
try {
final res = await AuthProxyService.pollQrStatus(_qrPendingRef!);
if (res['error'] == 'slow_down') {
final interval = res['interval'];
if (interval is int && interval > 0) {
final nextIntervalMs = interval * 1000;
if (nextIntervalMs != _qrPollIntervalMs) {
_qrPollIntervalMs = nextIntervalMs;
try {
final res = await AuthProxyService.pollQrStatus(_qrPendingRef!);
if (res['error'] == 'slow_down') {
final interval = res['interval'];
if (interval is int && interval > 0) {
final nextIntervalMs = interval * 1000;
if (nextIntervalMs != _qrPollIntervalMs) {
_qrPollIntervalMs = nextIntervalMs;
timer.cancel();
_startQrPolling();
return;
}
} else {
_qrPollIntervalMs += 500;
timer.cancel();
_startQrPolling();
return;
}
} else {
_qrPollIntervalMs += 500;
timer.cancel();
_startQrPolling();
}
if (res['error'] == 'authorization_pending') {
return;
}
}
if (res['error'] == 'authorization_pending') {
return;
}
if (res['error'] == 'expired_token') {
timer.cancel();
_qrCountdownTimer?.cancel();
_showError(
tr(
'msg.userfront.login.qr_expired',
fallback: 'QR 세션이 만료되었습니다.',
),
);
return;
}
if (res['status'] == 'ok') {
timer.cancel();
_qrCountdownTimer?.cancel();
final token = res['sessionJwt'] ?? res['token'];
if (token is String && token.isNotEmpty) {
_completeLoginFromToken(token);
} else {
if (res['error'] == 'expired_token') {
timer.cancel();
_qrCountdownTimer?.cancel();
_showError(
tr(
'msg.userfront.login.token_missing',
fallback: '로그인 토큰을 확인할 수 없습니다.',
),
tr('msg.userfront.login.qr_expired', fallback: 'QR 세션이 만료되었습니다.'),
);
return;
}
if (res['status'] == 'ok') {
timer.cancel();
_qrCountdownTimer?.cancel();
final token = res['sessionJwt'] ?? res['token'];
if (token is String && token.isNotEmpty) {
_completeLoginFromToken(token);
} else {
_showError(
tr(
'msg.userfront.login.token_missing',
fallback: '로그인 토큰을 확인할 수 없습니다.',
),
);
}
}
} catch (e) {
debugPrint("[QR] Polling error: $e");
}
} catch (e) {
debugPrint("[QR] Polling error: $e");
}
});
},
);
}
void _stopQrPolling() {
@@ -486,21 +505,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Duration redirectDelay = const Duration(seconds: 2),
}) {
if (!mounted) return;
final resolvedTitle = title ??
tr(
'ui.userfront.login.verification.title',
fallback: '승인 완료',
);
final resolvedPageTitle = pageTitle ??
tr(
'ui.userfront.login.verification.page_title',
fallback: '로그인 승인',
);
final resolvedActionLabel = actionLabel ??
tr(
'ui.userfront.login.verification.action_label',
fallback: '확인',
);
final resolvedTitle =
title ?? tr('ui.userfront.login.verification.title', fallback: '승인 완료');
final resolvedPageTitle =
pageTitle ??
tr('ui.userfront.login.verification.page_title', fallback: '로그인 승인');
final resolvedActionLabel =
actionLabel ??
tr('ui.userfront.login.verification.action_label', fallback: '확인');
setState(() {
_verificationApproved = true;
_verificationMessage = message;
@@ -524,11 +536,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle_outline, color: Colors.green, size: 72),
const Icon(
Icons.check_circle_outline,
color: Colors.green,
size: 72,
),
const SizedBox(height: 16),
Text(
_verificationTitle,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
const SizedBox(height: 12),
Text(
@@ -544,7 +564,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 24),
FilledButton(
onPressed: () {
final hasLocalSession = AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie();
final hasLocalSession =
AuthTokenStore.getToken() != null ||
AuthTokenStore.usesCookie();
final target = hasLocalSession ? '/' : '/signin';
if (mounted) {
setState(() {
@@ -586,10 +608,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (status == 'approved' || (jwt == null && _verificationOnly)) {
if (mounted) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
}
return;
}
@@ -602,18 +621,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
);
return;
}
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
return;
}
if (mounted) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
}
} catch (e) {
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
@@ -629,9 +642,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
Future<void> _verifyLoginCode(String loginId, String code, {String? pendingRef}) async {
Future<void> _verifyLoginCode(
String loginId,
String code, {
String? pendingRef,
}) async {
final sanitizedLoginId = loginId.replaceAll(' ', '+');
debugPrint("[Auth] Starting code verification for loginId: $sanitizedLoginId");
debugPrint(
"[Auth] Starting code verification for loginId: $sanitizedLoginId",
);
final approvedMessage = tr(
'msg.userfront.login.verification.approved',
fallback: '승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.',
@@ -653,16 +672,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
);
final jwt = res['sessionJwt'] ?? res['token'];
final status = res['status']?.toString();
debugPrint("[Auth] Code verification successful for loginId: $sanitizedLoginId");
debugPrint(
"[Auth] Code verification successful for loginId: $sanitizedLoginId",
);
final hasLocalSession = await _hasValidLocalSession();
final actionPath = hasLocalSession ? '/' : '/signin';
if (jwt == null && status == 'approved') {
if (mounted) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
}
return;
}
@@ -676,18 +694,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return;
}
if (_verificationOnly) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
return;
}
_markVerificationApproved(
linkLoginMessage,
title: tr(
'ui.userfront.login.link.title',
fallback: '링크 로그인 완료',
),
title: tr('ui.userfront.login.link.title', fallback: '링크 로그인 완료'),
pageTitle: tr(
'ui.userfront.login.link.page_title',
fallback: '링크 로그인',
@@ -703,13 +715,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
if (_verificationOnly && mounted) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
}
} catch (e) {
debugPrint("[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e");
debugPrint(
"[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e",
);
if (mounted) {
_showError(
tr(
@@ -747,10 +758,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (jwt == null && status == 'approved') {
if (mounted) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
}
return;
}
@@ -764,10 +772,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return;
}
if (_verificationOnly) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
return;
}
_completeLoginFromToken(jwt, provider: res['provider'] as String?);
@@ -775,10 +780,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
if (_verificationOnly && mounted) {
_markVerificationApproved(
approvedMessage,
actionPath: actionPath,
);
_markVerificationApproved(approvedMessage, actionPath: actionPath);
}
} catch (e) {
debugPrint("[Auth] Short code verification FAILED. Error: $e");
@@ -836,7 +838,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
);
try {
final res = await AuthProxyService.loginWithPassword(loginId, password, loginChallenge: _loginChallenge);
final res = await AuthProxyService.loginWithPassword(
loginId,
password,
loginChallenge: _loginChallenge,
);
final jwt = res['sessionJwt'];
final provider = res['provider'] as String?;
final redirectTo = res['redirectTo'] as String?;
@@ -860,9 +866,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
tr(
'msg.userfront.login.password.failed',
fallback: '로그인 실패: {{error}}',
params: {
'error': e.toString().replaceFirst('Exception: ', ''),
},
params: {'error': e.toString().replaceFirst('Exception: ', '')},
),
);
}
@@ -900,13 +904,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
Future<void> _startEnchantedFlow(String loginId, {required bool isEmail, bool codeOnly = false}) async {
Future<void> _startEnchantedFlow(
String loginId, {
required bool isEmail,
bool codeOnly = false,
}) async {
try {
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(child: CircularProgressIndicator()),
builder: (context) =>
const Center(child: CircularProgressIndicator()),
);
}
@@ -921,7 +930,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final interval = initResponse['interval'];
final resendAfter = initResponse['resendAfter'];
final expiresIn = initResponse['expiresIn'];
debugPrint("[Auth] Link Sent. PendingRef: $pendingRef, Mode: $mode, Provider: $provider");
debugPrint(
"[Auth] Link Sent. PendingRef: $pendingRef, Mode: $mode, Provider: $provider",
);
if (mounted) {
setState(() {
@@ -974,7 +985,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
Future<void> _pollForSession(String pendingRef, {Duration? initialInterval}) async {
Future<void> _pollForSession(
String pendingRef, {
Duration? initialInterval,
}) async {
int attempts = 0;
const maxAttempts = 60;
var pollInterval = initialInterval ?? const Duration(seconds: 2);
@@ -1047,10 +1061,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
debugPrint("[Auth] Polling timed out for ref: $pendingRef");
Navigator.of(context).pop();
_showError(
tr(
'msg.userfront.login.link_timeout',
fallback: '로그인 요청 시간이 초과되었습니다.',
),
tr('msg.userfront.login.link_timeout', fallback: '로그인 요청 시간이 초과되었습니다.'),
);
}
}
@@ -1138,10 +1149,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
if (WebAuthIntegration.isPopup() || (_redirectUrl != null && _redirectUrl!.isNotEmpty)) {
debugPrint("[Auth] External integration detected (popup or redirect). Notifying...");
if (WebAuthIntegration.isPopup() ||
(_redirectUrl != null && _redirectUrl!.isNotEmpty)) {
debugPrint(
"[Auth] External integration detected (popup or redirect). Notifying...",
);
WebAuthIntegration.sendLoginSuccess(token);
return;
return;
}
debugPrint("[Auth] Login success. Navigating to root.");
@@ -1224,7 +1238,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (_drySendEnabled) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF3CD),
borderRadius: BorderRadius.circular(8),
@@ -1232,13 +1249,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
child: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
const Icon(
Icons.warning_amber_rounded,
color: Color(0xFF8A6D3B),
),
const SizedBox(width: 8),
Expanded(
child: Text(
tr(
'msg.userfront.login.dry_send',
fallback: 'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
fallback:
'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
),
style: const TextStyle(
color: Color(0xFF8A6D3B),
@@ -1294,7 +1315,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
fallback: '이메일 또는 휴대폰 번호',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.person_outline),
prefixIcon: const Icon(
Icons.person_outline,
),
),
onSubmitted: (_) => _handlePasswordLogin(),
),
@@ -1308,7 +1331,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
fallback: '비밀번호',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline),
prefixIcon: const Icon(
Icons.lock_outline,
),
),
onSubmitted: (_) => _handlePasswordLogin(),
),
@@ -1319,7 +1344,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
minimumSize: const Size.fromHeight(50),
),
child: Text(
tr('ui.userfront.login.action.submit', fallback: '로그인'),
tr(
'ui.userfront.login.action.submit',
fallback: '로그인',
),
),
),
],
@@ -1340,7 +1368,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
hintText: '',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.person_outline),
prefixIcon: const Icon(
Icons.person_outline,
),
),
onSubmitted: (_) => _handleLinkLogin(),
),
@@ -1363,7 +1393,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
'msg.userfront.login.link.helper',
fallback: '입력하신 정보로 로그인 링크를 전송합니다.',
),
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
@@ -1371,9 +1404,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text(
tr(
'msg.userfront.login.link.short_code_help',
fallback: '링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.',
fallback:
'링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.',
),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
style: const TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
@@ -1382,16 +1419,21 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Expanded(
flex: 2,
child: TextField(
controller: _shortCodePrefixController,
textCapitalization: TextCapitalization.characters,
controller:
_shortCodePrefixController,
textCapitalization:
TextCapitalization.characters,
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.short_code.prefix',
fallback: '영문 2자리',
),
border: const OutlineInputBorder(),
border:
const OutlineInputBorder(),
hintText: 'AB',
hintStyle: const TextStyle(color: Colors.grey),
hintStyle: const TextStyle(
color: Colors.grey,
),
),
maxLength: 2,
),
@@ -1400,22 +1442,28 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Expanded(
flex: 4,
child: TextField(
controller: _shortCodeDigitsController,
controller:
_shortCodeDigitsController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.short_code.digits',
fallback: '숫자 6자리',
),
border: const OutlineInputBorder(),
border:
const OutlineInputBorder(),
hintText: '345678',
hintStyle: const TextStyle(color: Colors.grey),
hintStyle: const TextStyle(
color: Colors.grey,
),
suffixText: _linkExpireSeconds > 0
? tr(
'ui.userfront.login.short_code.expire_time',
fallback: '유효시간 {{time}}',
params: {
'time': _formatTime(_linkExpireSeconds),
'time': _formatTime(
_linkExpireSeconds,
),
},
)
: null,
@@ -1428,13 +1476,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 12),
FilledButton(
onPressed: () {
final prefix = _shortCodePrefixController.text.trim().toUpperCase();
final digits = _shortCodeDigitsController.text.trim();
if (prefix.length != 2 || digits.length != 6) {
final prefix =
_shortCodePrefixController.text
.trim()
.toUpperCase();
final digits =
_shortCodeDigitsController.text
.trim();
if (prefix.length != 2 ||
digits.length != 6) {
_showError(
tr(
'msg.userfront.login.short_code.invalid',
fallback: '문자 2개와 숫자 6자리를 입력해 주세요.',
fallback:
'문자 2개와 숫자 6자리를 입력해 주세요.',
),
);
return;
@@ -1458,27 +1513,35 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showInfo(
tr(
'msg.userfront.login.link.resend_wait',
fallback: '재발송은 {{time}} 후 가능합니다.',
fallback:
'재발송은 {{time}} 후 가능합니다.',
params: {
'time': _formatTime(_linkResendSeconds),
'time': _formatTime(
_linkResendSeconds,
),
},
),
);
return;
}
final loginId = _lastLinkLoginId ?? _linkIdController.text.trim();
final loginId =
_lastLinkLoginId ??
_linkIdController.text.trim();
if (loginId.isEmpty) {
_showError(
tr(
'msg.userfront.login.link.missing_login_id',
fallback: '이메일 또는 휴대폰 번호를 입력해 주세요.',
fallback:
'이메일 또는 휴대폰 번호를 입력해 주세요.',
),
);
return;
}
_startEnchantedFlow(
loginId,
isEmail: _lastLinkIsEmail || loginId.contains('@'),
isEmail:
_lastLinkIsEmail ||
loginId.contains('@'),
codeOnly: false,
);
},
@@ -1488,7 +1551,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
'ui.userfront.login.link.resend_with_time',
fallback: '재발송 ({{time}})',
params: {
'time': _formatTime(_linkResendSeconds),
'time': _formatTime(
_linkResendSeconds,
),
},
)
: tr(
@@ -1505,15 +1570,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showInfo(
tr(
'msg.userfront.login.link.resend_wait',
fallback: '재발송은 {{time}} 후 가능합니다.',
fallback:
'재발송은 {{time}} 후 가능합니다.',
params: {
'time': _formatTime(_linkResendSeconds),
'time': _formatTime(
_linkResendSeconds,
),
},
),
);
return;
}
final loginId = _lastLinkLoginId ?? _linkIdController.text.trim();
final loginId =
_lastLinkLoginId ??
_linkIdController.text.trim();
if (loginId.isEmpty) {
_showError(
tr(
@@ -1534,7 +1604,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
'ui.userfront.login.link.code_only',
fallback: '코드만 받기({{time}})',
params: {
'time': _formatTime(_linkResendSeconds),
'time': _formatTime(
_linkResendSeconds,
),
},
),
),
@@ -1553,13 +1625,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const CircularProgressIndicator()
else if (_qrImageBase64 != null)
Column(
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade300,
),
borderRadius: BorderRadius.circular(
12,
),
),
child: QrImageView(
data: _qrImageBase64!,
@@ -1570,20 +1647,24 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 12),
Text(
_qrRemainingSeconds > 0
? tr(
'ui.userfront.login.qr.remaining',
fallback: '남은 시간: {{time}}',
params: {
'time': _formatTime(_qrRemainingSeconds),
},
)
: tr(
'ui.userfront.login.qr.expired',
fallback: 'QR 코드 만료됨',
),
? tr(
'ui.userfront.login.qr.remaining',
fallback: '남은 시간: {{time}}',
params: {
'time': _formatTime(
_qrRemainingSeconds,
),
},
)
: tr(
'ui.userfront.login.qr.expired',
fallback: 'QR 코드 만료됨',
),
textAlign: TextAlign.center,
style: TextStyle(
color: _qrRemainingSeconds > 30 ? Colors.blue : Colors.red,
color: _qrRemainingSeconds > 30
? Colors.blue
: Colors.red,
fontWeight: FontWeight.bold,
),
),
@@ -1594,7 +1675,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
fallback: '모바일 앱으로 스캔하세요',
),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
TextButton(
onPressed: _startQrFlow,
@@ -1640,7 +1724,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
'msg.userfront.login.no_account',
fallback: '계정이 없으신가요?',
),
style: const TextStyle(color: Colors.grey, fontSize: 14),
style: const TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
TextButton(
onPressed: () => context.push('/signup'),