@umituz/react-native-settings 5.4.17 → 5.4.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/ai-consent/index.ts +53 -0
- package/src/domains/ai-consent/presentation/components/AIConsentModal.tsx +64 -0
- package/src/domains/ai-consent/presentation/components/AIConsentSetting.tsx +106 -0
- package/src/domains/ai-consent/presentation/hooks/useAIConsent.ts +125 -0
- package/src/domains/ai-consent/presentation/screens/AIConsentScreen.tsx +414 -0
- package/src/index.ts +3 -0
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +9 -1
- package/src/presentation/navigation/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.18",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification - expo-store-review and expo-device now lazy loaded",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-settings - AI Consent Domain
|
|
3
|
+
*
|
|
4
|
+
* AI consent management for React Native apps
|
|
5
|
+
* Required by Apple App Store Guidelines 5.1.1(i) & 5.1.2(i)
|
|
6
|
+
*
|
|
7
|
+
* Displays AI technology disclosure and obtains user consent before
|
|
8
|
+
* using AI generation features.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import {
|
|
12
|
+
* AIConsentScreen,
|
|
13
|
+
* AIConsentModal,
|
|
14
|
+
* AIConsentSetting,
|
|
15
|
+
* useAIConsent
|
|
16
|
+
* } from '@umituz/react-native-settings/ai-consent';
|
|
17
|
+
*
|
|
18
|
+
* // Show modal on app launch
|
|
19
|
+
* const { isConsentModalVisible, handleAcceptConsent, handleDeclineConsent } = useAIConsent();
|
|
20
|
+
*
|
|
21
|
+
* <AIConsentModal
|
|
22
|
+
* visible={isConsentModalVisible}
|
|
23
|
+
* onAccept={handleAcceptConsent}
|
|
24
|
+
* onDecline={handleDeclineConsent}
|
|
25
|
+
* />
|
|
26
|
+
*
|
|
27
|
+
* // Add to settings screen
|
|
28
|
+
* <AIConsentSetting />
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// PRESENTATION LAYER - Components
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
export { AIConsentModal } from './presentation/components/AIConsentModal';
|
|
36
|
+
export type { AIConsentModalProps } from './presentation/components/AIConsentModal';
|
|
37
|
+
|
|
38
|
+
export { AIConsentSetting } from './presentation/components/AIConsentSetting';
|
|
39
|
+
export type { AIConsentSettingProps } from './presentation/components/AIConsentSetting';
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// PRESENTATION LAYER - Screens
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
export { AIConsentScreen } from './presentation/screens/AIConsentScreen';
|
|
46
|
+
export type { AIConsentScreenProps, AIConsentScreenParams, AIProvider } from './presentation/screens/AIConsentScreen';
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// PRESENTATION LAYER - Hooks
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export { useAIConsent } from './presentation/hooks/useAIConsent';
|
|
53
|
+
export type { UseAIConsentReturn, AIConsentState } from './presentation/hooks/useAIConsent';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Consent Modal
|
|
3
|
+
*
|
|
4
|
+
* Modal wrapper for AI consent screen.
|
|
5
|
+
* Required by Apple App Store Guidelines 5.1.1(i) & 5.1.2(i)
|
|
6
|
+
*
|
|
7
|
+
* Displays on first app launch before any AI features are used.
|
|
8
|
+
* Can also be shown manually from settings.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* <AIConsentModal
|
|
12
|
+
* visible={isConsentModalVisible}
|
|
13
|
+
* onAccept={handleAccept}
|
|
14
|
+
* onDecline={handleDecline}
|
|
15
|
+
* />
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React, { memo } from 'react';
|
|
19
|
+
import { Modal, View, StyleSheet } from 'react-native';
|
|
20
|
+
import { AIConsentScreen, type AIProvider } from '../screens/AIConsentScreen';
|
|
21
|
+
|
|
22
|
+
export interface AIConsentModalProps {
|
|
23
|
+
visible: boolean;
|
|
24
|
+
onAccept: () => void;
|
|
25
|
+
onDecline: () => void;
|
|
26
|
+
providers?: AIProvider[];
|
|
27
|
+
customMessage?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const AIConsentModal: React.FC<AIConsentModalProps> = memo(({
|
|
31
|
+
visible,
|
|
32
|
+
onAccept,
|
|
33
|
+
onDecline,
|
|
34
|
+
providers,
|
|
35
|
+
customMessage,
|
|
36
|
+
}) => {
|
|
37
|
+
return (
|
|
38
|
+
<Modal
|
|
39
|
+
visible={visible}
|
|
40
|
+
animationType="slide"
|
|
41
|
+
transparent={false}
|
|
42
|
+
onRequestClose={onDecline}
|
|
43
|
+
>
|
|
44
|
+
<View style={styles.container}>
|
|
45
|
+
<AIConsentScreen
|
|
46
|
+
providers={providers}
|
|
47
|
+
customMessage={customMessage}
|
|
48
|
+
onAccept={onAccept}
|
|
49
|
+
onDecline={onDecline}
|
|
50
|
+
standalone={true}
|
|
51
|
+
/>
|
|
52
|
+
</View>
|
|
53
|
+
</Modal>
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
AIConsentModal.displayName = 'AIConsentModal';
|
|
58
|
+
|
|
59
|
+
const styles = StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
backgroundColor: '#FFFFFF',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIConsentSetting Component
|
|
3
|
+
*
|
|
4
|
+
* Settings list item for AI consent management.
|
|
5
|
+
* Shows current consent status and navigates to full consent screen.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Displays consent status (accepted/declined/pending)
|
|
9
|
+
* - Tappable card that opens AI consent screen
|
|
10
|
+
* - Icon with background color
|
|
11
|
+
* - Internationalized
|
|
12
|
+
* - Universal across iOS, Android, Web
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* import { AIConsentSetting } from '@umituz/react-native-settings/ai-consent';
|
|
16
|
+
*
|
|
17
|
+
* <AIConsentSetting />
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React, { useCallback, memo } from 'react';
|
|
21
|
+
import { View, StyleSheet } from 'react-native';
|
|
22
|
+
import { useAppDesignTokens, withAlpha } from '@umituz/react-native-design-system/theme';
|
|
23
|
+
import { useAppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
24
|
+
import { AtomicText } from '@umituz/react-native-design-system/atoms';
|
|
25
|
+
import { useAIConsent } from '../hooks/useAIConsent';
|
|
26
|
+
|
|
27
|
+
export interface AIConsentSettingProps {
|
|
28
|
+
/** Custom title */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Custom description when consented */
|
|
31
|
+
consentedDescription?: string;
|
|
32
|
+
/** Custom description when not consented */
|
|
33
|
+
notConsentedDescription?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const AIConsentSetting: React.FC<AIConsentSettingProps> = memo(({
|
|
37
|
+
title = 'AI Technology Consent',
|
|
38
|
+
consentedDescription = 'You have consented to use AI services',
|
|
39
|
+
notConsentedDescription = 'Review AI service disclosure',
|
|
40
|
+
}) => {
|
|
41
|
+
const tokens = useAppDesignTokens();
|
|
42
|
+
const navigation = useAppNavigation();
|
|
43
|
+
const { hasConsented, isLoading } = useAIConsent();
|
|
44
|
+
|
|
45
|
+
const handlePress = useCallback(() => {
|
|
46
|
+
navigation.push('AIConsent' as never);
|
|
47
|
+
}, [navigation]);
|
|
48
|
+
|
|
49
|
+
if (isLoading) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const iconColor = hasConsented ? tokens.colors.success : tokens.colors.warning;
|
|
54
|
+
const backgroundColor = withAlpha(iconColor, 0.1);
|
|
55
|
+
const description = hasConsented ? consentedDescription : notConsentedDescription;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View style={[styles.container, { backgroundColor }]}>
|
|
59
|
+
<View style={[styles.iconContainer, { backgroundColor: iconColor }]}>
|
|
60
|
+
<AtomicText style={styles.icon}>🤖</AtomicText>
|
|
61
|
+
</View>
|
|
62
|
+
|
|
63
|
+
<View style={styles.content}>
|
|
64
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
65
|
+
<AtomicText style={styles.description}>{description}</AtomicText>
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
AIConsentSetting.displayName = 'AIConsentSetting';
|
|
72
|
+
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
container: {
|
|
75
|
+
flexDirection: 'row',
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
padding: 16,
|
|
78
|
+
borderRadius: 12,
|
|
79
|
+
marginVertical: 8,
|
|
80
|
+
},
|
|
81
|
+
iconContainer: {
|
|
82
|
+
width: 48,
|
|
83
|
+
height: 48,
|
|
84
|
+
borderRadius: 24,
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
justifyContent: 'center',
|
|
87
|
+
marginRight: 16,
|
|
88
|
+
},
|
|
89
|
+
icon: {
|
|
90
|
+
fontSize: 24,
|
|
91
|
+
},
|
|
92
|
+
content: {
|
|
93
|
+
flex: 1,
|
|
94
|
+
},
|
|
95
|
+
title: {
|
|
96
|
+
fontSize: 16,
|
|
97
|
+
fontWeight: '600',
|
|
98
|
+
color: '#111827',
|
|
99
|
+
marginBottom: 4,
|
|
100
|
+
},
|
|
101
|
+
description: {
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
color: '#6B7280',
|
|
104
|
+
lineHeight: 20,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAIConsent Hook
|
|
3
|
+
* Manages AI consent state and display logic
|
|
4
|
+
* Required by Apple App Store Guidelines 5.1.1(i) & 5.1.2(i)
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Checks if user has consented to AI services
|
|
8
|
+
* - Shows modal on first use
|
|
9
|
+
* - Persists consent state
|
|
10
|
+
* - Memoized for performance
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
14
|
+
import { useStorage } from '@umituz/react-native-design-system/storage';
|
|
15
|
+
|
|
16
|
+
export interface AIConsentState {
|
|
17
|
+
hasConsented: boolean;
|
|
18
|
+
consentTimestamp?: number;
|
|
19
|
+
consentVersion?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CONSENT_STORAGE_KEY = '@app:ai_consent_accepted';
|
|
23
|
+
const CURRENT_CONSENT_VERSION = '1.0';
|
|
24
|
+
|
|
25
|
+
export interface UseAIConsentReturn {
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
isConsentModalVisible: boolean;
|
|
28
|
+
hasConsented: boolean;
|
|
29
|
+
consentState: AIConsentState | null;
|
|
30
|
+
handleAcceptConsent: () => Promise<void>;
|
|
31
|
+
handleDeclineConsent: () => void;
|
|
32
|
+
showConsentModal: () => void;
|
|
33
|
+
checkConsent: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useAIConsent = (): UseAIConsentReturn => {
|
|
37
|
+
const { getString, setString } = useStorage();
|
|
38
|
+
const [isConsentModalVisible, setIsConsentModalVisible] = useState(false);
|
|
39
|
+
const [hasConsented, setHasConsented] = useState(false);
|
|
40
|
+
const [consentState, setConsentState] = useState<AIConsentState | null>(null);
|
|
41
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if user has previously consented
|
|
45
|
+
*/
|
|
46
|
+
const checkConsent = useCallback(async () => {
|
|
47
|
+
setIsLoading(true);
|
|
48
|
+
try {
|
|
49
|
+
const consentData = await getString(CONSENT_STORAGE_KEY, '');
|
|
50
|
+
|
|
51
|
+
if (consentData) {
|
|
52
|
+
const parsed: AIConsentState = JSON.parse(consentData);
|
|
53
|
+
setConsentState(parsed);
|
|
54
|
+
setHasConsented(parsed.hasConsented);
|
|
55
|
+
|
|
56
|
+
// Show modal if user hasn't consented
|
|
57
|
+
if (!parsed.hasConsented) {
|
|
58
|
+
setIsConsentModalVisible(true);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// No consent found - show modal
|
|
62
|
+
setIsConsentModalVisible(true);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('[useAIConsent] Failed to check consent:', error);
|
|
66
|
+
// On error, show modal to be safe
|
|
67
|
+
setIsConsentModalVisible(true);
|
|
68
|
+
} finally {
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
}
|
|
71
|
+
}, [getString]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle user accepting AI consent
|
|
75
|
+
*/
|
|
76
|
+
const handleAcceptConsent = useCallback(async () => {
|
|
77
|
+
try {
|
|
78
|
+
const newState: AIConsentState = {
|
|
79
|
+
hasConsented: true,
|
|
80
|
+
consentTimestamp: Date.now(),
|
|
81
|
+
consentVersion: CURRENT_CONSENT_VERSION,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await setString(CONSENT_STORAGE_KEY, JSON.stringify(newState));
|
|
85
|
+
setConsentState(newState);
|
|
86
|
+
setHasConsented(true);
|
|
87
|
+
setIsConsentModalVisible(false);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('[useAIConsent] Failed to save consent:', error);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}, [setString]);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle user declining AI consent
|
|
96
|
+
*/
|
|
97
|
+
const handleDeclineConsent = useCallback(() => {
|
|
98
|
+
setIsConsentModalVisible(false);
|
|
99
|
+
// User declined - they can still use the app but AI features will be blocked
|
|
100
|
+
// This is handled by the hasConsented flag
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Manually show consent modal (e.g., from settings)
|
|
105
|
+
*/
|
|
106
|
+
const showConsentModal = useCallback(() => {
|
|
107
|
+
setIsConsentModalVisible(true);
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
// Check consent on mount
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
void checkConsent();
|
|
113
|
+
}, [checkConsent]);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
isLoading,
|
|
117
|
+
isConsentModalVisible,
|
|
118
|
+
hasConsented,
|
|
119
|
+
consentState,
|
|
120
|
+
handleAcceptConsent,
|
|
121
|
+
handleDeclineConsent,
|
|
122
|
+
showConsentModal,
|
|
123
|
+
checkConsent,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Consent Screen
|
|
3
|
+
*
|
|
4
|
+
* Full screen for displaying AI technology disclosure and consent.
|
|
5
|
+
* Required by Apple App Store Guidelines 5.1.1(i) & 5.1.2(i)
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Lists all AI providers and their purposes
|
|
9
|
+
* - Explains data sharing practices
|
|
10
|
+
* - Provides privacy policy links
|
|
11
|
+
* - Accept/Decline buttons
|
|
12
|
+
* - Scrollable content
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { memo } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
View,
|
|
18
|
+
StyleSheet,
|
|
19
|
+
ScrollView,
|
|
20
|
+
TouchableOpacity,
|
|
21
|
+
} from 'react-native';
|
|
22
|
+
import {
|
|
23
|
+
ScreenLayout,
|
|
24
|
+
} from '@umituz/react-native-design-system/layouts';
|
|
25
|
+
import {
|
|
26
|
+
AtomicText,
|
|
27
|
+
AtomicButton,
|
|
28
|
+
AtomicSpinner,
|
|
29
|
+
} from '@umituz/react-native-design-system/atoms';
|
|
30
|
+
import {
|
|
31
|
+
NavigationHeader,
|
|
32
|
+
useAppNavigation,
|
|
33
|
+
} from '@umituz/react-native-design-system/molecules';
|
|
34
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
|
|
35
|
+
|
|
36
|
+
export interface AIProvider {
|
|
37
|
+
name: string;
|
|
38
|
+
purpose: string;
|
|
39
|
+
privacyUrl: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AIConsentScreenParams {
|
|
43
|
+
providers?: AIProvider[];
|
|
44
|
+
customMessage?: string;
|
|
45
|
+
onAccept?: () => void;
|
|
46
|
+
onDecline?: () => void;
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AIConsentScreenProps {
|
|
51
|
+
route?: {
|
|
52
|
+
params?: AIConsentScreenParams;
|
|
53
|
+
};
|
|
54
|
+
providers?: AIProvider[];
|
|
55
|
+
customMessage?: string;
|
|
56
|
+
onAccept?: () => void;
|
|
57
|
+
onDecline?: () => void;
|
|
58
|
+
standalone?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DEFAULT_PROVIDERS: AIProvider[] = [
|
|
62
|
+
{
|
|
63
|
+
name: 'Pruna AI',
|
|
64
|
+
purpose: 'Image generation & editing',
|
|
65
|
+
privacyUrl: 'https://pruna.ai/privacy',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'FAL AI',
|
|
69
|
+
purpose: 'Video generation infrastructure',
|
|
70
|
+
privacyUrl: 'https://fal.ai/privacy',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Groq AI',
|
|
74
|
+
purpose: 'Text processing & prompts',
|
|
75
|
+
privacyUrl: 'https://groq.com/privacy',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export const AIConsentScreen: React.FC<AIConsentScreenProps> = memo(({
|
|
80
|
+
route,
|
|
81
|
+
providers = DEFAULT_PROVIDERS,
|
|
82
|
+
customMessage,
|
|
83
|
+
onAccept,
|
|
84
|
+
onDecline,
|
|
85
|
+
standalone = false,
|
|
86
|
+
}) => {
|
|
87
|
+
const navigation = useAppNavigation();
|
|
88
|
+
const tokens = useAppDesignTokens();
|
|
89
|
+
const [loading, setLoading] = React.useState(false);
|
|
90
|
+
|
|
91
|
+
// Get params from route or use props
|
|
92
|
+
const params = route?.params || {};
|
|
93
|
+
const finalProviders = params.providers || providers;
|
|
94
|
+
const finalMessage = params.customMessage || customMessage;
|
|
95
|
+
const finalOnAccept = params.onAccept || onAccept;
|
|
96
|
+
const finalOnDecline = params.onDecline || onDecline;
|
|
97
|
+
|
|
98
|
+
const handleAccept = async () => {
|
|
99
|
+
if (finalOnAccept) {
|
|
100
|
+
setLoading(true);
|
|
101
|
+
try {
|
|
102
|
+
await finalOnAccept();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('[AIConsentScreen] Accept error:', error);
|
|
105
|
+
} finally {
|
|
106
|
+
setLoading(false);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!standalone) {
|
|
111
|
+
navigation.goBack();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleDecline = () => {
|
|
116
|
+
if (finalOnDecline) {
|
|
117
|
+
finalOnDecline();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!standalone) {
|
|
121
|
+
navigation.goBack();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleLinkPress = (url: string) => {
|
|
126
|
+
// TODO: Implement Linking.openURL
|
|
127
|
+
console.log('[AIConsentScreen] Open URL:', url);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ScreenLayout
|
|
132
|
+
scrollable={true}
|
|
133
|
+
edges={['top', 'bottom', 'left', 'right']}
|
|
134
|
+
hideScrollIndicator={false}
|
|
135
|
+
>
|
|
136
|
+
{!standalone && (
|
|
137
|
+
<NavigationHeader
|
|
138
|
+
title="AI Technology Disclosure"
|
|
139
|
+
onBackPress={() => navigation.goBack()}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<ScrollView
|
|
144
|
+
style={styles.scrollView}
|
|
145
|
+
contentContainerStyle={styles.scrollContent}
|
|
146
|
+
showsVerticalScrollIndicator={false}
|
|
147
|
+
>
|
|
148
|
+
{finalMessage && (
|
|
149
|
+
<AtomicText style={styles.introText}>
|
|
150
|
+
{finalMessage}
|
|
151
|
+
</AtomicText>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
<AtomicText style={styles.introText}>
|
|
155
|
+
Vivoim uses multiple AI services to generate content. Before using
|
|
156
|
+
our AI features, please review how your data is processed.
|
|
157
|
+
</AtomicText>
|
|
158
|
+
|
|
159
|
+
<View style={styles.section}>
|
|
160
|
+
<AtomicText style={styles.sectionTitle}>
|
|
161
|
+
🤖 AI Services We Use
|
|
162
|
+
</AtomicText>
|
|
163
|
+
<View style={styles.providerList}>
|
|
164
|
+
{finalProviders.map((provider, index) => (
|
|
165
|
+
<View key={index} style={styles.providerItem}>
|
|
166
|
+
<View style={styles.providerHeader}>
|
|
167
|
+
<AtomicText style={styles.providerName}>
|
|
168
|
+
{provider.name}
|
|
169
|
+
</AtomicText>
|
|
170
|
+
<TouchableOpacity
|
|
171
|
+
onPress={() => handleLinkPress(provider.privacyUrl)}
|
|
172
|
+
>
|
|
173
|
+
<AtomicText style={styles.privacyLink}>
|
|
174
|
+
Privacy Policy
|
|
175
|
+
</AtomicText>
|
|
176
|
+
</TouchableOpacity>
|
|
177
|
+
</View>
|
|
178
|
+
<AtomicText style={styles.providerPurpose}>
|
|
179
|
+
{provider.purpose}
|
|
180
|
+
</AtomicText>
|
|
181
|
+
</View>
|
|
182
|
+
))}
|
|
183
|
+
</View>
|
|
184
|
+
</View>
|
|
185
|
+
|
|
186
|
+
<View style={styles.section}>
|
|
187
|
+
<AtomicText style={styles.sectionTitle}>
|
|
188
|
+
📸 What We Send to AI Services
|
|
189
|
+
</AtomicText>
|
|
190
|
+
<AtomicText style={styles.bullet}>
|
|
191
|
+
• Photos you select from your gallery
|
|
192
|
+
</AtomicText>
|
|
193
|
+
<AtomicText style={styles.bullet}>
|
|
194
|
+
• Text prompts you enter
|
|
195
|
+
</AtomicText>
|
|
196
|
+
<AtomicText style={styles.bullet}>
|
|
197
|
+
• Generation instructions and settings
|
|
198
|
+
</AtomicText>
|
|
199
|
+
<AtomicText style={styles.note}>
|
|
200
|
+
Note: Your photos are processed to generate content and are NOT
|
|
201
|
+
stored permanently by us or AI providers after processing
|
|
202
|
+
completes.
|
|
203
|
+
</AtomicText>
|
|
204
|
+
</View>
|
|
205
|
+
|
|
206
|
+
<View style={styles.section}>
|
|
207
|
+
<AtomicText style={styles.sectionTitle}>
|
|
208
|
+
🔒 Data Privacy
|
|
209
|
+
</AtomicText>
|
|
210
|
+
<AtomicText style={styles.bullet}>
|
|
211
|
+
• Original photos are deleted after processing
|
|
212
|
+
</AtomicText>
|
|
213
|
+
<AtomicText style={styles.bullet}>
|
|
214
|
+
• Generated content is stored in your account only
|
|
215
|
+
</AtomicText>
|
|
216
|
+
<AtomicText style={styles.bullet}>
|
|
217
|
+
• You can delete any content at any time
|
|
218
|
+
</AtomicText>
|
|
219
|
+
<AtomicText style={styles.bullet}>
|
|
220
|
+
• Face data is used for generation only
|
|
221
|
+
</AtomicText>
|
|
222
|
+
<AtomicText style={styles.bullet}>
|
|
223
|
+
• No biometric authentication or tracking
|
|
224
|
+
</AtomicText>
|
|
225
|
+
</View>
|
|
226
|
+
|
|
227
|
+
<View style={styles.section}>
|
|
228
|
+
<AtomicText style={styles.sectionTitle}>
|
|
229
|
+
📄 Legal Documents
|
|
230
|
+
</AtomicText>
|
|
231
|
+
<TouchableOpacity
|
|
232
|
+
style={styles.linkButton}
|
|
233
|
+
onPress={() => handleLinkPress('https://umituz.com/projects/ai-technology/vivoim/privacy')}
|
|
234
|
+
>
|
|
235
|
+
<AtomicText style={styles.linkText}>
|
|
236
|
+
Privacy Policy →
|
|
237
|
+
</AtomicText>
|
|
238
|
+
</TouchableOpacity>
|
|
239
|
+
<TouchableOpacity
|
|
240
|
+
style={styles.linkButton}
|
|
241
|
+
onPress={() => handleLinkPress('https://umituz.com/projects/ai-technology/vivoim/terms')}
|
|
242
|
+
>
|
|
243
|
+
<AtomicText style={styles.linkText}>
|
|
244
|
+
Terms of Use →
|
|
245
|
+
</AtomicText>
|
|
246
|
+
</TouchableOpacity>
|
|
247
|
+
</View>
|
|
248
|
+
|
|
249
|
+
<View style={styles.declarationSection}>
|
|
250
|
+
<View style={styles.checkbox}>
|
|
251
|
+
<View style={styles.checkboxInner}>
|
|
252
|
+
<AtomicText style={styles.checkmark}>✓</AtomicText>
|
|
253
|
+
</View>
|
|
254
|
+
</View>
|
|
255
|
+
<AtomicText style={styles.declarationText}>
|
|
256
|
+
I have read and agree to the Privacy Policy and Terms of Use. I
|
|
257
|
+
understand that my photos will be processed by third-party AI
|
|
258
|
+
services to generate content.
|
|
259
|
+
</AtomicText>
|
|
260
|
+
</View>
|
|
261
|
+
</ScrollView>
|
|
262
|
+
|
|
263
|
+
<View style={styles.footer}>
|
|
264
|
+
<AtomicButton
|
|
265
|
+
variant="secondary"
|
|
266
|
+
onPress={handleDecline}
|
|
267
|
+
style={styles.declineButton}
|
|
268
|
+
fullWidth
|
|
269
|
+
>
|
|
270
|
+
Decline
|
|
271
|
+
</AtomicButton>
|
|
272
|
+
<AtomicButton
|
|
273
|
+
variant="primary"
|
|
274
|
+
onPress={handleAccept}
|
|
275
|
+
disabled={loading}
|
|
276
|
+
style={styles.acceptButton}
|
|
277
|
+
fullWidth
|
|
278
|
+
>
|
|
279
|
+
{loading ? (
|
|
280
|
+
<AtomicSpinner size="sm" color="background" />
|
|
281
|
+
) : (
|
|
282
|
+
'I Accept'
|
|
283
|
+
)}
|
|
284
|
+
</AtomicButton>
|
|
285
|
+
</View>
|
|
286
|
+
</ScreenLayout>
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
AIConsentScreen.displayName = 'AIConsentScreen';
|
|
291
|
+
|
|
292
|
+
const styles = StyleSheet.create({
|
|
293
|
+
scrollView: {
|
|
294
|
+
flex: 1,
|
|
295
|
+
},
|
|
296
|
+
scrollContent: {
|
|
297
|
+
paddingBottom: 100,
|
|
298
|
+
},
|
|
299
|
+
introText: {
|
|
300
|
+
fontSize: 15,
|
|
301
|
+
lineHeight: 22,
|
|
302
|
+
marginBottom: 24,
|
|
303
|
+
color: '#374151',
|
|
304
|
+
},
|
|
305
|
+
section: {
|
|
306
|
+
marginBottom: 24,
|
|
307
|
+
padding: 16,
|
|
308
|
+
backgroundColor: '#F9FAFB',
|
|
309
|
+
borderRadius: 12,
|
|
310
|
+
},
|
|
311
|
+
sectionTitle: {
|
|
312
|
+
fontSize: 17,
|
|
313
|
+
fontWeight: '600',
|
|
314
|
+
color: '#111827',
|
|
315
|
+
marginBottom: 12,
|
|
316
|
+
},
|
|
317
|
+
providerList: {
|
|
318
|
+
gap: 12,
|
|
319
|
+
},
|
|
320
|
+
providerItem: {
|
|
321
|
+
backgroundColor: '#FFFFFF',
|
|
322
|
+
padding: 12,
|
|
323
|
+
borderRadius: 8,
|
|
324
|
+
borderWidth: 1,
|
|
325
|
+
borderColor: '#E5E7EB',
|
|
326
|
+
},
|
|
327
|
+
providerHeader: {
|
|
328
|
+
flexDirection: 'row',
|
|
329
|
+
justifyContent: 'space-between',
|
|
330
|
+
alignItems: 'center',
|
|
331
|
+
marginBottom: 4,
|
|
332
|
+
},
|
|
333
|
+
providerName: {
|
|
334
|
+
fontSize: 15,
|
|
335
|
+
fontWeight: '600',
|
|
336
|
+
color: '#111827',
|
|
337
|
+
},
|
|
338
|
+
privacyLink: {
|
|
339
|
+
fontSize: 13,
|
|
340
|
+
color: '#3B82F6',
|
|
341
|
+
},
|
|
342
|
+
providerPurpose: {
|
|
343
|
+
fontSize: 14,
|
|
344
|
+
color: '#6B7280',
|
|
345
|
+
},
|
|
346
|
+
bullet: {
|
|
347
|
+
fontSize: 15,
|
|
348
|
+
color: '#374151',
|
|
349
|
+
lineHeight: 22,
|
|
350
|
+
marginLeft: 8,
|
|
351
|
+
},
|
|
352
|
+
note: {
|
|
353
|
+
fontSize: 13,
|
|
354
|
+
color: '#6B7280',
|
|
355
|
+
fontStyle: 'italic',
|
|
356
|
+
marginLeft: 8,
|
|
357
|
+
marginTop: 4,
|
|
358
|
+
},
|
|
359
|
+
linkButton: {
|
|
360
|
+
paddingVertical: 8,
|
|
361
|
+
marginTop: 8,
|
|
362
|
+
},
|
|
363
|
+
linkText: {
|
|
364
|
+
fontSize: 15,
|
|
365
|
+
color: '#3B82F6',
|
|
366
|
+
fontWeight: '500',
|
|
367
|
+
},
|
|
368
|
+
declarationSection: {
|
|
369
|
+
flexDirection: 'row',
|
|
370
|
+
alignItems: 'flex-start',
|
|
371
|
+
marginTop: 8,
|
|
372
|
+
padding: 12,
|
|
373
|
+
backgroundColor: '#EFF6FF',
|
|
374
|
+
borderRadius: 8,
|
|
375
|
+
},
|
|
376
|
+
checkbox: {
|
|
377
|
+
width: 24,
|
|
378
|
+
height: 24,
|
|
379
|
+
marginRight: 12,
|
|
380
|
+
marginTop: 2,
|
|
381
|
+
},
|
|
382
|
+
checkboxInner: {
|
|
383
|
+
width: 24,
|
|
384
|
+
height: 24,
|
|
385
|
+
borderRadius: 4,
|
|
386
|
+
backgroundColor: '#3B82F6',
|
|
387
|
+
alignItems: 'center',
|
|
388
|
+
justifyContent: 'center',
|
|
389
|
+
},
|
|
390
|
+
checkmark: {
|
|
391
|
+
color: '#FFFFFF',
|
|
392
|
+
fontSize: 16,
|
|
393
|
+
fontWeight: '700',
|
|
394
|
+
},
|
|
395
|
+
declarationText: {
|
|
396
|
+
flex: 1,
|
|
397
|
+
fontSize: 14,
|
|
398
|
+
color: '#1E3A8A',
|
|
399
|
+
lineHeight: 20,
|
|
400
|
+
},
|
|
401
|
+
footer: {
|
|
402
|
+
flexDirection: 'row',
|
|
403
|
+
padding: 16,
|
|
404
|
+
borderTopWidth: 1,
|
|
405
|
+
borderTopColor: '#E5E7EB',
|
|
406
|
+
gap: 12,
|
|
407
|
+
},
|
|
408
|
+
declineButton: {
|
|
409
|
+
flex: 1,
|
|
410
|
+
},
|
|
411
|
+
acceptButton: {
|
|
412
|
+
flex: 1,
|
|
413
|
+
},
|
|
414
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -141,6 +141,9 @@ export * from "./domains/gamification";
|
|
|
141
141
|
// Localization Domain - i18n, language selection, translations
|
|
142
142
|
export * from "./domains/localization";
|
|
143
143
|
|
|
144
|
+
// AI Consent Domain - AI technology disclosure and consent
|
|
145
|
+
export * from "./domains/ai-consent";
|
|
146
|
+
|
|
144
147
|
// =============================================================================
|
|
145
148
|
// PRESENTATION LAYER - Config Creator Utilities
|
|
146
149
|
// =============================================================================
|
|
@@ -6,6 +6,7 @@ import { SettingsScreen } from "../../screens/SettingsScreen";
|
|
|
6
6
|
import { DisclaimerScreen } from "../../../domains/disclaimer/presentation/screens/DisclaimerScreen";
|
|
7
7
|
import { FeedbackScreen } from "../../../domains/feedback/presentation/screens/FeedbackScreen";
|
|
8
8
|
import { RatingPromptScreen } from "../../../domains/rating/presentation/screens/RatingPromptScreen";
|
|
9
|
+
import { AIConsentScreen } from "../../../domains/ai-consent/presentation/screens/AIConsentScreen";
|
|
9
10
|
|
|
10
11
|
// AccountScreen is an optional peer — lazy require so the package works without @umituz/react-native-auth
|
|
11
12
|
// Returns null if @umituz/react-native-auth is not installed
|
|
@@ -198,6 +199,12 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
|
|
|
198
199
|
options: { headerShown: false },
|
|
199
200
|
};
|
|
200
201
|
|
|
202
|
+
const aiConsentScreen = {
|
|
203
|
+
name: "AIConsent" as const,
|
|
204
|
+
component: AIConsentScreen,
|
|
205
|
+
options: { headerShown: false },
|
|
206
|
+
};
|
|
207
|
+
|
|
201
208
|
return combineScreens(
|
|
202
209
|
baseScreens,
|
|
203
210
|
faqScreen,
|
|
@@ -209,7 +216,8 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
|
|
|
209
216
|
featureRequestScreen,
|
|
210
217
|
disclaimerScreen,
|
|
211
218
|
feedbackScreen,
|
|
212
|
-
ratingPromptScreen
|
|
219
|
+
ratingPromptScreen,
|
|
220
|
+
aiConsentScreen
|
|
213
221
|
);
|
|
214
222
|
}, [
|
|
215
223
|
translations,
|
|
@@ -9,6 +9,7 @@ import type { FAQCategory } from "../../domains/faqs";
|
|
|
9
9
|
import type { DisclaimerScreenParams } from "../../domains/disclaimer/presentation/screens/DisclaimerScreen";
|
|
10
10
|
import type { FeedbackScreenParams } from "../../domains/feedback/presentation/screens/FeedbackScreen";
|
|
11
11
|
import type { RatingPromptScreenParams } from "../../domains/rating/presentation/screens/RatingPromptScreen";
|
|
12
|
+
import type { AIConsentScreenParams } from "../../domains/ai-consent/presentation/screens/AIConsentScreen";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* App Info passed from main app (APP_INFO constant)
|
|
@@ -54,6 +55,7 @@ export type SettingsStackParamList = {
|
|
|
54
55
|
Disclaimer: DisclaimerScreenParams;
|
|
55
56
|
Feedback: FeedbackScreenParams;
|
|
56
57
|
RatingPrompt: RatingPromptScreenParams;
|
|
58
|
+
AIConsent: AIConsentScreenParams;
|
|
57
59
|
PasswordPrompt: {
|
|
58
60
|
onComplete: (password: string | null) => void;
|
|
59
61
|
title?: string;
|