feat(client): iOS 上架合规——首启隐私同意弹窗 + 注销账号入口

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-05 08:56:26 +08:00
parent dddf402d92
commit b740cc6edf
5 changed files with 298 additions and 2 deletions
@@ -0,0 +1,145 @@
// 首启个人信息保护同意闸(个保法 + 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)),
]),
]),
),
),
),
]);
}
}