forked from baron/baron-sso
린트 적용
This commit is contained in:
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user