| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- 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<GoogleAuthBindScreen> createState() =>
- _GoogleAuthBindScreenState();
- }
- class _GoogleAuthBindScreenState extends ConsumerState<GoogleAuthBindScreen> {
- 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<void> _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<GoogleAuthState>(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<String> 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,
- ),
- ),
- ),
- );
- }
- }
|