|
| 1 | +/** |
| 2 | + * AI 数据处理同意(满足 App Store 审核 Guideline 2.1:向第三方 AI 服务发送数据前需取得用户明确同意)。 |
| 3 | + * |
| 4 | + * 在「即将把用户内容发给 AI」的入口处(任务会话页、新建任务页)首次弹出,用户明确点「同意并继续」后 |
| 5 | + * 才放行;持久化到 AsyncStorage,之后不再打扰。点「暂不使用」则退出该页(不进行任何 AI 交互)。 |
| 6 | + */ |
| 7 | +import AsyncStorage from '@react-native-async-storage/async-storage'; |
| 8 | +import { useCallback, useEffect, useState } from 'react'; |
| 9 | +import { Linking, Modal, Platform, Pressable, StyleSheet, Text, View } from 'react-native'; |
| 10 | +import { getBaseUrl } from '@/api/client'; |
| 11 | +import { useTheme } from '@/theme'; |
| 12 | + |
| 13 | +const AI_CONSENT_KEY = 'mc.aiConsent.v2'; |
| 14 | + |
| 15 | +export type AiConsentStatus = 'loading' | 'granted' | 'needed'; |
| 16 | + |
| 17 | +/** 读取/写入「AI 数据处理同意」状态。loading 期间不弹窗,避免闪现。仅 iOS(App Store 审核要求),Android 直接视为已同意。 */ |
| 18 | +export function useAiConsent() { |
| 19 | + const [status, setStatus] = useState<AiConsentStatus>(Platform.OS === 'ios' ? 'loading' : 'granted'); |
| 20 | + useEffect(() => { |
| 21 | + if (Platform.OS !== 'ios') return; |
| 22 | + let alive = true; |
| 23 | + AsyncStorage.getItem(AI_CONSENT_KEY) |
| 24 | + .then((v) => { if (alive) setStatus(v === '1' ? 'granted' : 'needed'); }) |
| 25 | + .catch(() => { if (alive) setStatus('needed'); }); |
| 26 | + return () => { alive = false; }; |
| 27 | + }, []); |
| 28 | + const grant = useCallback(() => { |
| 29 | + setStatus('granted'); |
| 30 | + AsyncStorage.setItem(AI_CONSENT_KEY, '1').catch(() => undefined); |
| 31 | + }, []); |
| 32 | + return { status, grant }; |
| 33 | +} |
| 34 | + |
| 35 | +export function AiConsentModal({ visible, onAgree, onDecline }: { visible: boolean; onAgree: () => void; onDecline: () => void }) { |
| 36 | + const t = useTheme(); |
| 37 | + const openPrivacy = () => { Linking.openURL(`${getBaseUrl().replace(/\/+$/, '')}/privacy-policy`).catch(() => undefined); }; |
| 38 | + return ( |
| 39 | + <Modal visible={visible} transparent animationType="fade" onRequestClose={onDecline} statusBarTranslucent> |
| 40 | + {/* 居中确认对话框需要压暗背景(共享 Scrim 是透明的,底部 sheet 惯例不压暗,这里局部加);拦截背景点击 */} |
| 41 | + <Pressable style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.45)' }]} /> |
| 42 | + <View style={{ position: 'absolute', left: 26, right: 26, top: 0, bottom: 0, alignItems: 'center', justifyContent: 'center' }} pointerEvents="box-none"> |
| 43 | + <View style={{ width: '100%', backgroundColor: t.bg2, borderRadius: 22, borderWidth: 1, borderColor: t.line2, padding: 22, ...t.shLift }}> |
| 44 | + <Text style={{ color: t.tx, fontSize: 18, fontWeight: '700', marginBottom: 12 }}>使用 AI 编程助手</Text> |
| 45 | + <Text style={{ color: t.tx2, fontSize: 14, lineHeight: 22 }}> |
| 46 | + 为给你提供 AI 编程协助,你在任务中提交的内容(你的指令、代码、文件、图片等)会被发送至 AI 模型进行处理,其中可能包含第三方 AI 服务商。点击「同意并继续」即表示你已知晓并同意上述数据处理方式。 |
| 47 | + </Text> |
| 48 | + <Pressable onPress={openPrivacy} hitSlop={6} style={{ marginTop: 10 }}> |
| 49 | + <Text style={{ color: t.acTx, fontSize: 13, fontWeight: '600' }}>查看《隐私政策》</Text> |
| 50 | + </Pressable> |
| 51 | + <Pressable onPress={onAgree} style={({ pressed }) => [{ marginTop: 20, backgroundColor: t.ac, borderRadius: 14, paddingVertical: 14, alignItems: 'center' }, pressed && { opacity: 0.85 }]}> |
| 52 | + <Text style={{ color: t.acInk, fontSize: 15, fontWeight: '700' }}>同意并继续</Text> |
| 53 | + </Pressable> |
| 54 | + <Pressable onPress={onDecline} style={{ marginTop: 8, paddingVertical: 12, alignItems: 'center' }}> |
| 55 | + <Text style={{ color: t.tx3, fontSize: 14, fontWeight: '600' }}>暂不使用</Text> |
| 56 | + </Pressable> |
| 57 | + </View> |
| 58 | + </View> |
| 59 | + </Modal> |
| 60 | + ); |
| 61 | +} |
0 commit comments