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:
@@ -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)),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user