import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../../core/i18n/locale_utils.dart'; import '../../../core/services/auth_proxy_service.dart'; import '../../../core/ui/toast_service.dart'; import 'package:userfront/i18n.dart'; class ResetPasswordScreen extends StatefulWidget { final String? loginId; // Now receiving loginId const ResetPasswordScreen({super.key, this.loginId}); @override State createState() => _ResetPasswordScreenState(); } class _ResetPasswordScreenState extends State { final TextEditingController _passwordController = TextEditingController(); final TextEditingController _confirmPasswordController = TextEditingController(); final _formKey = GlobalKey(); bool _isLoading = false; String? _loginId; String? _token; bool _isPasswordObscured = true; bool _isConfirmPasswordObscured = true; Map? _policy; bool _isPolicyLoading = false; @override void initState() { super.initState(); // 1. Get loginId from GoRouter state if available _loginId = widget.loginId; // 2. Fallback to URI query parameter if not available via router if (_loginId == null || _loginId!.isEmpty) { final uri = Uri.base; _loginId = uri.queryParameters['loginId']; } // 토큰도 함께 읽어놓는다. final uri = Uri.base; _token = uri.queryParameters['token']; _loadPolicy(); } Future _loadPolicy() async { setState(() { _isPolicyLoading = true; }); try { final policy = await AuthProxyService.fetchPasswordPolicy(); if (mounted) { setState(() { _policy = policy; }); } } catch (_) { // 실패해도 기본 검증 로직 사용 } finally { if (mounted) { setState(() { _isPolicyLoading = false; }); } } } Future _handlePasswordReset() async { if (_formKey.currentState?.validate() != true) return; if ((_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)) { _showError(tr('msg.userfront.reset.invalid_link')); return; } setState(() => _isLoading = true); try { await AuthProxyService.completePasswordReset( loginId: _loginId, token: _token, newPassword: _passwordController.text, ); if (mounted) { ToastService.success(tr('msg.userfront.reset.success')); context.go(buildLocalizedSigninPath(Uri.base)); } } catch (e) { if (mounted) { _showError( tr( 'msg.userfront.reset.error.generic', params: {'error': e.toString()}, ), ); } } finally { if (mounted) { setState(() => _isLoading = false); } } } void _showError(String message) { ToastService.error(message); } String _buildPolicyDescription() { if (_isPolicyLoading) { return tr('msg.userfront.reset.policy_loading'); } final minLength = (_policy?['minLength'] as int?) ?? 12; final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0; final requiresLower = _policy?['lowercase'] ?? true; final requiresUpper = _policy?['uppercase'] ?? false; final requiresNumber = _policy?['number'] ?? true; final requiresSymbol = _policy?['nonAlphanumeric'] ?? true; final parts = [ tr( 'msg.userfront.reset.policy.min_length', params: {'count': '$minLength'}, ), ]; if (minTypes > 0) { parts.add( tr( 'msg.userfront.reset.policy.min_types', params: {'count': '$minTypes'}, ), ); } if (requiresLower) { parts.add(tr('msg.userfront.reset.policy.lowercase')); } if (requiresUpper) { parts.add(tr('msg.userfront.reset.policy.uppercase')); } if (requiresNumber) { parts.add(tr('msg.userfront.reset.policy.number')); } if (requiresSymbol) { parts.add(tr('msg.userfront.reset.policy.symbol')); } return parts.join(", "); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('ui.userfront.reset.title')), centerTitle: true, ), body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 400), padding: const EdgeInsets.all(24), child: (_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty) ? _buildInvalidTokenView() : Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( tr('ui.userfront.reset.subtitle'), style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( _buildPolicyDescription(), textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey), ), const SizedBox(height: 40), TextFormField( key: const ValueKey('reset_password_new_input'), controller: _passwordController, obscureText: _isPasswordObscured, decoration: InputDecoration( labelText: tr('ui.userfront.reset.new_password'), border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon( _isPasswordObscured ? Icons.visibility_off : Icons.visibility, ), onPressed: () { setState(() { _isPasswordObscured = !_isPasswordObscured; }); }, ), ), validator: (value) { final val = value ?? ""; if (val.isEmpty) { return tr( 'msg.userfront.reset.error.empty_password', ); } final minLength = (_policy?['minLength'] as int?) ?? 12; if (val.length < minLength) { return tr( 'msg.userfront.reset.error.min_length', params: {'count': '$minLength'}, ); } final hasLower = RegExp(r'[a-z]').hasMatch(val); final hasUpper = RegExp(r'[A-Z]').hasMatch(val); final hasNumber = RegExp(r'[0-9]').hasMatch(val); final hasSymbol = RegExp(r'[\W_]').hasMatch(val); int typeCount = 0; if (hasLower) typeCount++; if (hasUpper) typeCount++; if (hasNumber) typeCount++; if (hasSymbol) typeCount++; final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0; if (minTypes > 0 && typeCount < minTypes) { return tr( 'msg.userfront.reset.error.min_types', params: {'count': '$minTypes'}, ); } if ((_policy?['lowercase'] ?? true) && !hasLower) { return tr('msg.userfront.reset.error.lowercase'); } if ((_policy?['uppercase'] ?? false) && !hasUpper) { return tr('msg.userfront.reset.error.uppercase'); } if ((_policy?['number'] ?? true) && !hasNumber) { return tr('msg.userfront.reset.error.number'); } if ((_policy?['nonAlphanumeric'] ?? true) && !hasSymbol) { return tr('msg.userfront.reset.error.symbol'); } return null; }, ), const SizedBox(height: 16), TextFormField( key: const ValueKey('reset_password_confirm_input'), controller: _confirmPasswordController, obscureText: _isConfirmPasswordObscured, decoration: InputDecoration( labelText: tr('ui.userfront.reset.confirm_password'), border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon( _isConfirmPasswordObscured ? Icons.visibility_off : Icons.visibility, ), onPressed: () { setState(() { _isConfirmPasswordObscured = !_isConfirmPasswordObscured; }); }, ), ), validator: (value) { if (value != _passwordController.text) { return tr('msg.userfront.reset.error.mismatch'); } return null; }, ), const SizedBox(height: 24), FilledButton( key: const ValueKey('reset_password_submit_button'), onPressed: _isLoading ? null : _handlePasswordReset, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(50), ), child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text(tr('ui.userfront.reset.submit')), ), ], ), ), ), ), ); } Widget _buildInvalidTokenView() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Colors.red, size: 60), const SizedBox(height: 16), Text( tr('msg.userfront.reset.invalid_title'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( tr('msg.userfront.reset.invalid_body'), textAlign: TextAlign.center, ), ], ), ); } }