Files
App.K12.Student.Miniapp_Flu…/lib/features/auth/privacy_consent_page.dart
T
2026-09-04 15:22:20 +08:00

268 lines
9.5 KiB
Dart

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<PrivacyConsentPage> createState() => _PrivacyConsentPageState();
}
class _PrivacyConsentPageState extends State<PrivacyConsentPage> {
bool _submitting = false;
_LegalDocumentData? _activeDocument;
Future<void> _openLegalDocument({
required String title,
required String content,
}) async {
if (!mounted) return;
setState(() {
_activeDocument = _LegalDocumentData(
title: title,
content: content,
);
});
}
Future<void> _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<void> _decline() async {
final result = await showDialog<bool>(
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;
}