import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:new_learn/core/providers/privacy_provider.dart'; import 'package:new_learn/core/storage/privacy_storage.dart'; import 'package:new_learn/features/auth/legal_documents.dart'; /// ============================================================ /// 【PrivacyConsentPage】首次启动隐私协议页 /// /// 作用: /// 1. 用户首次打开 App 时,先展示隐私说明; /// 2. 用户点击"同意并继续"后,保存本地状态并放行进入主流程; /// 3. 用户点击"暂不同意"时,维持拦截状态(Android 可直接退出应用)。 /// /// 这里是"应用启动级"协议页,不等同于登录页里的协议勾选: /// - 启动协议:未登录前就要先拦; /// - 登录页协议:属于业务表单校验,发生在点击登录时。 /// ============================================================ class PrivacyConsentPage extends StatefulWidget { const PrivacyConsentPage({super.key}); @override State createState() => _PrivacyConsentPageState(); } class _PrivacyConsentPageState extends State { bool _submitting = false; _LegalDocumentData? _activeDocument; Future _openLegalDocument({ required String title, required String content, }) async { if (!mounted) return; setState(() { _activeDocument = _LegalDocumentData( title: title, content: content, ); }); } Future _accept() async { if (_submitting) return; setState(() => _submitting = true); await PrivacyStorage.instance.saveAccepted(true); // 更新全局门禁状态:启动 Gate 和 go_router 守卫都会同步放行。 privacyAcceptedNotifier.value = true; if (mounted) { setState(() => _submitting = false); } } Future _decline() async { final result = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('提示'), content: const Text('若您不同意隐私政策,将无法使用我们的服务'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('重新考虑'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('退出'), ), ], ), ); // 选"重新考虑"或直接关闭弹窗,都继续停留在当前隐私页。 if (result != false) return; await PrivacyStorage.instance.saveAccepted(false); privacyAcceptedNotifier.value = false; // Android 上按常见做法直接退出应用;其他平台调用也安全,无法退出时只是不生效。 await SystemNavigator.pop(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final activeDocument = _activeDocument; if (activeDocument != null) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (didPop) return; setState(() => _activeDocument = null); }, child: Scaffold( backgroundColor: const Color(0xFFF5F7FA), appBar: AppBar( title: Text(activeDocument.title), leading: IconButton( onPressed: () => setState(() => _activeDocument = null), icon: const Icon(Icons.arrow_back), ), ), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: SelectableText( activeDocument.content, style: const TextStyle( fontSize: 14, height: 1.8, color: Color(0xFF323233), ), ), ), ), ), ), ); } return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '欢迎使用爱学蝶变', style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 16), Text( '为保障你的个人信息安全,在继续使用前,请先阅读并同意《用户协议》和《隐私政策》。同意后我们才会继续进入 App 的登录和业务流程。', style: theme.textTheme.bodyLarge?.copyWith( color: const Color(0xFF4B5563), height: 1.7, ), ), const SizedBox(height: 20), Expanded( child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(16), ), child: SingleChildScrollView( child: Text( '1. 我们会在必要范围内使用账号信息、设备信息,用于登录鉴权、学习记录同步、功能服务提供。\n\n' '2. 未经你的明确同意,我们不会将你的个人信息用于与学习无关的用途。\n\n' '3. 你可以通过《用户协议》和《隐私政策》了解信息收集、使用、存储与保护方式。\n\n' '4. 若你不同意本协议内容,将无法继续进入 App 主流程。', style: theme.textTheme.bodyMedium?.copyWith( color: const Color(0xFF475569), height: 1.8, ), ), ), ), ), const SizedBox(height: 16), Wrap( spacing: 4, runSpacing: 4, children: [ Text( '点击“同意并继续”即表示你已阅读并同意', style: theme.textTheme.bodySmall?.copyWith( color: const Color(0xFF64748B), ), ), InkWell( onTap: () => _openLegalDocument( title: kUserAgreementTitle, content: kUserAgreementContent, ), child: const Text( '《用户协议》', style: TextStyle( color: Color(0xFF2563EB), fontSize: 12, ), ), ), Text( '和', style: theme.textTheme.bodySmall?.copyWith( color: const Color(0xFF64748B), ), ), InkWell( onTap: () => _openLegalDocument( title: kPrivacyPolicyTitle, content: kPrivacyPolicyContent, ), child: const Text( '《隐私政策》', style: TextStyle( color: Color(0xFF2563EB), fontSize: 12, ), ), ), ], ), const SizedBox(height: 20), Row( children: [ Expanded( child: OutlinedButton( onPressed: _submitting ? null : _decline, style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(48), ), child: const Text('暂不同意'), ), ), const SizedBox(width: 12), Expanded( child: FilledButton( onPressed: _submitting ? null : _accept, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), ), child: _submitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('同意并继续'), ), ), ], ), ], ), ), ), ); } } class _LegalDocumentData { const _LegalDocumentData({ required this.title, required this.content, }); final String title; final String content; }