Files
jiu/client/lib/widgets/privacy_consent_gate.dart
T
wangjia b740cc6edf feat(client): iOS 上架合规——首启隐私同意弹窗 + 注销账号入口
个保法/审核 5.1.1:移动端首启全屏隐私同意(链接隐私政策/用户协议,
同意持久化;不同意 Android 退出、iOS 留提示);我的页 iOS 形态新增
「注销账号」(sheet 说明 + 预填 mailto 至 support@51yanmei.com)。
关于页隐私政策/服务条款链接已有,桌面/Web 零改动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 08:56:26 +08:00

146 lines
5.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 首启个人信息保护同意闸(个保法 + App Store 中国区审核 5.1.1)。
//
// 移动端(iOS/Android)首次启动时全屏遮罩弹出:说明收集范围 + 可点
// 《隐私政策》《用户协议》+ 同意/不同意。同意持久化(SharedPreferences
// 后不再出现;不同意时 Android 退出应用,iOS 留在弹层并提示(苹果 HIG
// 禁止程序性退出)。桌面与 Web 完全不弹。
// 挂载点:MaterialApp.router 的 builder(覆盖全部路由,早于登录页交互)。
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import '../core/config/app_info.dart';
import '../core/config/store_compliance.dart';
import '../core/theme/app_dims.g.dart';
import '../core/theme/context_tokens.dart';
import 'ds/ds_atoms.dart';
const _kConsentKey = 'privacy_consent_v1';
class PrivacyConsentGate extends StatefulWidget {
final Widget child;
const PrivacyConsentGate({super.key, required this.child});
@override
State<PrivacyConsentGate> createState() => _PrivacyConsentGateState();
}
class _PrivacyConsentGateState extends State<PrivacyConsentGate> {
bool _needConsent = false;
bool _declinedHint = false;
@override
void initState() {
super.initState();
if (needsPrivacyConsent) _check();
}
Future<void> _check() async {
final prefs = await SharedPreferences.getInstance();
if (mounted && !(prefs.getBool(_kConsentKey) ?? false)) {
setState(() => _needConsent = true);
}
}
Future<void> _agree() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_kConsentKey, true);
if (mounted) setState(() => _needConsent = false);
}
void _decline() {
// Android 惯例:不同意即退出;iOS 禁止程序性退出,留提示。
if (!kIsWeb && Platform.isAndroid) {
SystemNavigator.pop();
} else {
setState(() => _declinedHint = true);
}
}
void _open(String path) {
final base = AppInfo.website.isNotEmpty
? AppInfo.website
: 'https://jiu.51yanmei.com';
launchUrl(Uri.parse('$base$path'), mode: LaunchMode.externalApplication);
}
@override
Widget build(BuildContext context) {
if (!_needConsent) return widget.child;
final t = context.tokens;
final link = TextStyle(
fontSize: AppDims.fsSm, color: t.primary, fontWeight: FontWeight.w600);
return Stack(children: [
widget.child,
const ModalBarrier(
dismissible: false, color: Colors.black54), // ds-ignore 遮罩固定色
Center(
child: Material(
color: Colors.transparent,
child: Container(
width: 320,
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.fromLTRB(22, 22, 22, 18),
decoration: BoxDecoration(
color: t.surface,
borderRadius: BorderRadius.circular(AppDims.rLg),
border: Border.all(color: t.border),
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text('个人信息保护提示',
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
color: t.heading)),
const SizedBox(height: 12),
Text.rich(
TextSpan(
style: TextStyle(
fontSize: AppDims.fsSm, color: t.text, height: 1.7),
children: [
const TextSpan(
text: '感谢使用岩美酒库。为向您提供门店库存管理服务,'
'我们仅收集必要的账号信息、设备信息与操作日志,'
'用于多端数据同步与账号安全保障。详情请阅读'),
TextSpan(
text: '《隐私政策》',
style: link,
recognizer: TapGestureRecognizer()
..onTap = () => _open('/privacy/')),
const TextSpan(text: ''),
TextSpan(
text: '《用户协议》',
style: link,
recognizer: TapGestureRecognizer()
..onTap = () => _open('/terms/')),
const TextSpan(text: '。点击「同意并继续」即表示您已阅读并同意上述条款。'),
],
),
),
if (_declinedHint) ...[
const SizedBox(height: 10),
Text('需同意后方可使用岩美酒库',
style: TextStyle(fontSize: AppDims.fsXs, color: t.warn)),
],
const SizedBox(height: 16),
Row(children: [
Expanded(
child: DsButton('不同意', onPressed: _decline)),
const SizedBox(width: 10),
Expanded(
child: DsButton('同意并继续',
variant: DsBtnVariant.primary, onPressed: _agree)),
]),
]),
),
),
),
]);
}
}