import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:qr_flutter/qr_flutter.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/top_toast.dart'; import '../../../providers/google_auth_provider.dart'; /// 谷歌验证器绑定页面 — 单页三步表单 /// /// 1. 显示秘钥 + QR 码(页面加载时自动获取) /// 2. 输入谷歌验证码(6位) /// 3. 发送并输入邮箱验证码(6位) /// → 提交按钮同时提交两个验证码 + 秘钥 class GoogleAuthBindScreen extends ConsumerStatefulWidget { const GoogleAuthBindScreen({super.key}); @override ConsumerState createState() => _GoogleAuthBindScreenState(); } class _GoogleAuthBindScreenState extends ConsumerState { final _googleCodeController = TextEditingController(); final _emailCodeController = TextEditingController(); @override void initState() { super.initState(); Future.microtask(() { ref.invalidate(googleAuthProvider); ref.read(googleAuthProvider.notifier).fetchSecret(); }); } @override void dispose() { _googleCodeController.dispose(); _emailCodeController.dispose(); super.dispose(); } bool get _canSubmit => _googleCodeController.text.length == 6 && _emailCodeController.text.length == 6; Future _handleSubmit() async { final success = await ref.read(googleAuthProvider.notifier).bindGoogleAuth( googleCode: _googleCodeController.text, emailCode: _emailCodeController.text, ); if (success && mounted) { showTopToast(context, message: AppLocalizations.of(context)!.bindSuccess, backgroundColor: AppColors.success); context.pop(); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final state = ref.watch(googleAuthProvider); // 监听错误 ref.listen(googleAuthProvider, (prev, next) { if (next.errorMessage != null && next.errorMessage != prev?.errorMessage) { final l10n = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(next.errorMessage!, l10n) ?? next.errorMessage!); ref.read(googleAuthProvider.notifier).clearError(); } }); return Scaffold( appBar: AppBar( elevation: 0, leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), title: Text( AppLocalizations.of(context)!.authenticator, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), centerTitle: true, ), body: Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ═══ 第一步:秘钥 + QR 码 ═══ _SectionTitle(number: '1.'), const SizedBox(height: 8), Text( AppLocalizations.of(context)!.googleBindStep1Hint, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, height: 1.5, ), ), const SizedBox(height: 16), // 秘钥 + 复制 if (state.isSecretLoading) const Center( child: Padding( padding: EdgeInsets.all(20), child: CircularProgressIndicator(strokeWidth: 2), ), ) else ...[ Row( children: [ Expanded( child: Text( state.secret, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 1, ), ), ), GestureDetector( onTap: () { if (state.secret.isEmpty) return; Clipboard.setData( ClipboardData(text: state.secret)); showTopToast( context, message: AppLocalizations.of(context)!.keyCopied, backgroundColor: AppColors.success, duration: const Duration(seconds: 1), ); }, child: Icon( Icons.copy, size: 18, color: cs.onSurface.withAlpha(153), ), ), ], ), const SizedBox(height: 16), // QR 码 if (state.otpauthUrl.isNotEmpty) Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: QrImageView( data: state.otpauthUrl, version: QrVersions.auto, size: 160, backgroundColor: Colors.white, ), ), ), ], const SizedBox(height: 32), // ═══ 第二步:谷歌验证码 ═══ _SectionTitle(number: '2.'), const SizedBox(height: 8), Text( AppLocalizations.of(context)!.googleBindStep2Hint, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, height: 1.5, ), ), const SizedBox(height: 12), Text( AppLocalizations.of(context)!.googleCode, style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), _CodeTextField( controller: _googleCodeController, hintText: AppLocalizations.of(context)!.googleCodeHint, onChanged: (_) => setState(() {}), ), const SizedBox(height: 32), // ═══ 第三步:邮箱验证码 ═══ _SectionTitle(number: '3.'), const SizedBox(height: 8), Text( AppLocalizations.of(context)!.emailCodeInstruction, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, height: 1.5, ), ), const SizedBox(height: 12), // 脱敏邮箱 Text( AppLocalizations.of(context)!.emailCodeSentTo(state.maskedEmail), style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), // 验证码输入 + 发送按钮 Row( children: [ Expanded( child: _CodeTextField( controller: _emailCodeController, hintText: AppLocalizations.of(context)!.enterCode, onChanged: (_) => setState(() {}), ), ), const SizedBox(width: 12), _SendCodeButton( cooldown: state.codeCooldown, isLoading: state.isSendingCode, onTap: () { ref .read(googleAuthProvider.notifier) .sendEmailCode(); }, ), ], ), const SizedBox(height: 12), Text( AppLocalizations.of(context)!.checkSpamMessage, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 12, ), ), ], ), ), ), // ── 提交按钮 ────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 32), child: SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: (_canSubmit && !state.isSubmitting) ? _handleSubmit : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, disabledBackgroundColor: cs.outline.withAlpha(30), foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: state.isSubmitting ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, ), ) : Text( AppLocalizations.of(context)!.submit, style: TextStyle( color: _canSubmit ? cs.surface : cs.onSurface.withAlpha(153), fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ), ), ], ), ); } } // ── 步骤编号标题 ───────────────────────────────────────────── class _SectionTitle extends StatelessWidget { const _SectionTitle({required this.number}); final String number; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Text( number, style: TextStyle( color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, ), ); } } // ── 验证码输入框 ───────────────────────────────────────────── class _CodeTextField extends StatelessWidget { const _CodeTextField({ required this.controller, required this.hintText, required this.onChanged, }); final TextEditingController controller; final String hintText; final ValueChanged onChanged; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return TextField( controller: controller, keyboardType: TextInputType.number, maxLength: 6, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: onChanged, style: TextStyle( color: cs.onSurface, fontSize: 15, ), decoration: InputDecoration( counterText: '', hintText: hintText, hintStyle: TextStyle( color: cs.onSurface.withAlpha(80), fontSize: 14, ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), ), ); } } // ── 发送验证码按钮 ────────────────────────────────────────── class _SendCodeButton extends StatelessWidget { const _SendCodeButton({ required this.cooldown, required this.isLoading, required this.onTap, }); final int cooldown; final bool isLoading; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final disabled = cooldown > 0 || isLoading; return GestureDetector( onTap: disabled ? null : onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all( color: disabled ? cs.outline.withAlpha(60) : cs.onSurface, ), ), child: isLoading ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: cs.onSurface.withAlpha(153), ), ) : Text( cooldown > 0 ? '${cooldown}s' : AppLocalizations.of(context)!.sendCode, style: TextStyle( color: disabled ? cs.onSurface.withAlpha(100) : cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ); } }