@umituz/react-native-settings 4.17.14 → 4.17.16
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 +16 -15
- package/src/domains/about/__tests__/integration.test.tsx +328 -0
- package/src/domains/about/__tests__/types.d.ts +5 -0
- package/src/domains/about/domain/entities/AppInfo.ts +74 -0
- package/src/domains/about/domain/entities/__tests__/AppInfo.test.ts +93 -0
- package/src/domains/about/domain/repositories/IAboutRepository.ts +22 -0
- package/src/domains/about/index.ts +10 -0
- package/src/domains/about/infrastructure/repositories/AboutRepository.ts +68 -0
- package/src/domains/about/infrastructure/repositories/__tests__/AboutRepository.test.ts +153 -0
- package/src/domains/about/presentation/components/AboutContent.tsx +104 -0
- package/src/domains/about/presentation/components/AboutHeader.tsx +79 -0
- package/src/domains/about/presentation/components/AboutSection.tsx +134 -0
- package/src/domains/about/presentation/components/AboutSettingItem.tsx +208 -0
- package/src/domains/about/presentation/components/__tests__/AboutContent.simple.test.tsx +178 -0
- package/src/domains/about/presentation/components/__tests__/AboutContent.test.tsx +293 -0
- package/src/domains/about/presentation/components/__tests__/AboutHeader.test.tsx +201 -0
- package/src/domains/about/presentation/components/__tests__/AboutSettingItem.test.tsx +71 -0
- package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.simple.test.tsx +229 -0
- package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.test.tsx +240 -0
- package/src/domains/about/presentation/hooks/useAboutInfo.ts +262 -0
- package/src/domains/about/presentation/screens/AboutScreen.tsx +195 -0
- package/src/domains/about/presentation/screens/__tests__/AboutScreen.simple.test.tsx +199 -0
- package/src/domains/about/presentation/screens/__tests__/AboutScreen.test.tsx +366 -0
- package/src/domains/about/types/global.d.ts +15 -0
- package/src/domains/about/utils/__tests__/index.test.ts +408 -0
- package/src/domains/about/utils/index.ts +160 -0
- package/src/domains/appearance/__tests__/components/AppearanceScreen.test.tsx +195 -0
- package/src/domains/appearance/__tests__/hooks/index.test.tsx +232 -0
- package/src/domains/appearance/__tests__/integration/index.test.tsx +207 -0
- package/src/domains/appearance/__tests__/services/appearanceService.test.ts +299 -0
- package/src/domains/appearance/__tests__/setup.ts +96 -0
- package/src/domains/appearance/__tests__/stores/appearanceStore.test.tsx +175 -0
- package/src/domains/appearance/data/colorPalettes.ts +94 -0
- package/src/domains/appearance/hooks/index.ts +6 -0
- package/src/domains/appearance/hooks/useAppearance.ts +61 -0
- package/src/domains/appearance/hooks/useAppearanceActions.ts +144 -0
- package/src/domains/appearance/index.ts +7 -0
- package/src/domains/appearance/infrastructure/services/appearanceService.ts +301 -0
- package/src/domains/appearance/infrastructure/services/systemThemeDetection.ts +79 -0
- package/src/domains/appearance/infrastructure/services/validation.ts +91 -0
- package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +120 -0
- package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +132 -0
- package/src/domains/appearance/presentation/components/AppearanceHeader.tsx +67 -0
- package/src/domains/appearance/presentation/components/AppearancePreview.tsx +141 -0
- package/src/domains/appearance/presentation/components/AppearanceSection.tsx +139 -0
- package/src/domains/appearance/presentation/components/ColorPicker.tsx +113 -0
- package/src/domains/appearance/presentation/components/CustomColorsSection.tsx +186 -0
- package/src/domains/appearance/presentation/components/ThemeModeSection.tsx +110 -0
- package/src/domains/appearance/presentation/components/ThemeOption.tsx +138 -0
- package/src/domains/appearance/presentation/components/index.ts +6 -0
- package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +226 -0
- package/src/domains/appearance/presentation/screens/index.ts +2 -0
- package/src/domains/appearance/types/index.ts +54 -0
- package/src/domains/faqs/domain/entities/FAQEntity.ts +16 -0
- package/src/domains/faqs/domain/services/FAQSearchService.ts +36 -0
- package/src/domains/faqs/domain/services/index.ts +1 -0
- package/src/domains/faqs/index.ts +7 -0
- package/src/domains/faqs/presentation/components/FAQCategory.tsx +71 -0
- package/src/domains/faqs/presentation/components/FAQEmptyState.tsx +75 -0
- package/src/domains/faqs/presentation/components/FAQItem.tsx +103 -0
- package/src/domains/faqs/presentation/components/FAQSearchBar.tsx +70 -0
- package/src/domains/faqs/presentation/components/FAQSection.tsx +50 -0
- package/src/domains/faqs/presentation/components/index.ts +18 -0
- package/src/domains/faqs/presentation/hooks/index.ts +6 -0
- package/src/domains/faqs/presentation/hooks/useFAQExpansion.ts +51 -0
- package/src/domains/faqs/presentation/hooks/useFAQSearch.ts +33 -0
- package/src/domains/faqs/presentation/screens/FAQScreen.tsx +129 -0
- package/src/domains/faqs/presentation/screens/index.ts +2 -0
- package/src/domains/feedback/domain/entities/FeedbackEntity.ts +92 -0
- package/src/domains/feedback/domain/repositories/IFeedbackRepository.ts +28 -0
- package/src/domains/feedback/index.ts +6 -0
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +189 -0
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +111 -0
- package/src/domains/feedback/presentation/components/SupportSection.tsx +160 -0
- package/src/domains/feedback/presentation/hooks/useDeleteFeedback.ts +25 -0
- package/src/domains/feedback/presentation/hooks/useFeedbackForm.ts +59 -0
- package/src/domains/feedback/presentation/hooks/useSubmitFeedback.ts +55 -0
- package/src/domains/feedback/presentation/hooks/useUserFeedback.ts +29 -0
- package/src/domains/legal/__tests__/ContentValidationService.test.ts +195 -0
- package/src/domains/legal/__tests__/StyleCacheService.test.ts +110 -0
- package/src/domains/legal/__tests__/UrlHandlerService.test.ts +71 -0
- package/src/domains/legal/__tests__/setup.ts +82 -0
- package/src/domains/legal/domain/entities/LegalConfig.ts +26 -0
- package/src/domains/legal/domain/services/ContentValidationService.ts +89 -0
- package/src/domains/legal/domain/services/StyleCacheService.ts +97 -0
- package/src/domains/legal/domain/services/UrlHandlerService.ts +128 -0
- package/src/domains/legal/index.ts +8 -0
- package/src/domains/legal/presentation/components/LegalItem.tsx +177 -0
- package/src/domains/legal/presentation/components/LegalLinks.tsx +154 -0
- package/src/domains/legal/presentation/components/LegalSection.tsx +134 -0
- package/src/domains/legal/presentation/screens/LegalScreen.tsx +237 -0
- package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +214 -0
- package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +214 -0
- package/src/index.ts +19 -0
- package/src/presentation/components/DevSettingsSection.tsx +2 -2
- package/src/presentation/components/SettingItem.tsx +2 -2
- package/src/presentation/components/SettingsErrorBoundary.tsx +2 -2
- package/src/presentation/components/SettingsFooter.tsx +2 -2
- package/src/presentation/components/SettingsSection.tsx +2 -2
- package/src/presentation/navigation/SettingsStackNavigator.tsx +2 -2
- package/src/presentation/screens/SettingsScreen.tsx +2 -2
- package/src/presentation/screens/components/SettingsContent.tsx +2 -2
- package/src/presentation/screens/components/SettingsHeader.tsx +2 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LegalLinks Component
|
|
3
|
+
* Single Responsibility: Display Privacy Policy and Terms of Service links
|
|
4
|
+
* Required for App Store compliance in paywall screens
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
|
+
import { AtomicText } from "@umituz/react-native-design-system";
|
|
10
|
+
import { ContentValidationService } from "../../domain/services/ContentValidationService";
|
|
11
|
+
import { UrlHandlerService } from "../../domain/services/UrlHandlerService";
|
|
12
|
+
|
|
13
|
+
export interface LegalLinksProps {
|
|
14
|
+
/**
|
|
15
|
+
* Privacy Policy URL
|
|
16
|
+
*/
|
|
17
|
+
privacyPolicyUrl?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Terms of Service URL
|
|
20
|
+
*/
|
|
21
|
+
termsOfServiceUrl?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Privacy Policy link text (required when privacyPolicyUrl is provided)
|
|
24
|
+
*/
|
|
25
|
+
privacyText?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Terms of Service link text (required when termsOfServiceUrl is provided)
|
|
28
|
+
*/
|
|
29
|
+
termsText?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Callback when Privacy Policy is pressed
|
|
32
|
+
*/
|
|
33
|
+
onPrivacyPress?: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Callback when Terms of Service is pressed
|
|
36
|
+
*/
|
|
37
|
+
onTermsPress?: () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Additional styles
|
|
40
|
+
*/
|
|
41
|
+
style?: object;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const LegalLinks: React.FC<LegalLinksProps> = React.memo(
|
|
45
|
+
({
|
|
46
|
+
privacyPolicyUrl,
|
|
47
|
+
termsOfServiceUrl,
|
|
48
|
+
privacyText,
|
|
49
|
+
termsText,
|
|
50
|
+
onPrivacyPress,
|
|
51
|
+
onTermsPress,
|
|
52
|
+
style,
|
|
53
|
+
}) => {
|
|
54
|
+
// Validate required props
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
ContentValidationService.validateLegalLinks(
|
|
57
|
+
privacyPolicyUrl,
|
|
58
|
+
termsOfServiceUrl,
|
|
59
|
+
privacyText,
|
|
60
|
+
termsText,
|
|
61
|
+
onPrivacyPress,
|
|
62
|
+
onTermsPress
|
|
63
|
+
);
|
|
64
|
+
}, [privacyPolicyUrl, termsOfServiceUrl, privacyText, termsText, onPrivacyPress, onTermsPress]);
|
|
65
|
+
|
|
66
|
+
// Memoize press handlers to prevent child re-renders
|
|
67
|
+
const handlePrivacyPress = React.useCallback(async () => {
|
|
68
|
+
if (onPrivacyPress) {
|
|
69
|
+
onPrivacyPress();
|
|
70
|
+
} else if (privacyPolicyUrl) {
|
|
71
|
+
try {
|
|
72
|
+
await UrlHandlerService.openUrl(privacyPolicyUrl);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (__DEV__) {
|
|
75
|
+
console.error('LegalLinks: Error opening privacy policy URL', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, [onPrivacyPress, privacyPolicyUrl]);
|
|
80
|
+
|
|
81
|
+
const handleTermsPress = React.useCallback(async () => {
|
|
82
|
+
if (onTermsPress) {
|
|
83
|
+
onTermsPress();
|
|
84
|
+
} else if (termsOfServiceUrl) {
|
|
85
|
+
try {
|
|
86
|
+
await UrlHandlerService.openUrl(termsOfServiceUrl);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (__DEV__) {
|
|
89
|
+
console.error('LegalLinks: Error opening terms of service URL', error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}, [onTermsPress, termsOfServiceUrl]);
|
|
94
|
+
|
|
95
|
+
// Memoize conditional rendering to prevent unnecessary re-renders
|
|
96
|
+
const showPrivacy = React.useMemo(() => !!(onPrivacyPress || privacyPolicyUrl), [onPrivacyPress, privacyPolicyUrl]);
|
|
97
|
+
const showTerms = React.useMemo(() => !!(onTermsPress || termsOfServiceUrl), [onTermsPress, termsOfServiceUrl]);
|
|
98
|
+
const showSeparator = React.useMemo(() => showPrivacy && showTerms, [showPrivacy, showTerms]);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View style={[styles.container, style]}>
|
|
102
|
+
{showPrivacy && (
|
|
103
|
+
<TouchableOpacity onPress={handlePrivacyPress} hitSlop={styles.hitSlop}>
|
|
104
|
+
<AtomicText
|
|
105
|
+
type="labelSmall"
|
|
106
|
+
color="primary"
|
|
107
|
+
style={styles.link}
|
|
108
|
+
>
|
|
109
|
+
{privacyText}
|
|
110
|
+
</AtomicText>
|
|
111
|
+
</TouchableOpacity>
|
|
112
|
+
)}
|
|
113
|
+
{showSeparator && (
|
|
114
|
+
<AtomicText
|
|
115
|
+
type="labelSmall"
|
|
116
|
+
color="textTertiary"
|
|
117
|
+
>
|
|
118
|
+
{" • "}
|
|
119
|
+
</AtomicText>
|
|
120
|
+
)}
|
|
121
|
+
{showTerms && (
|
|
122
|
+
<TouchableOpacity onPress={handleTermsPress} hitSlop={styles.hitSlop}>
|
|
123
|
+
<AtomicText
|
|
124
|
+
type="labelSmall"
|
|
125
|
+
color="primary"
|
|
126
|
+
style={styles.link}
|
|
127
|
+
>
|
|
128
|
+
{termsText}
|
|
129
|
+
</AtomicText>
|
|
130
|
+
</TouchableOpacity>
|
|
131
|
+
)}
|
|
132
|
+
</View>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
LegalLinks.displayName = "LegalLinks";
|
|
138
|
+
|
|
139
|
+
const styles = StyleSheet.create({
|
|
140
|
+
container: {
|
|
141
|
+
flexDirection: "row",
|
|
142
|
+
alignItems: "center",
|
|
143
|
+
justifyContent: "center",
|
|
144
|
+
},
|
|
145
|
+
link: {
|
|
146
|
+
textDecorationLine: "underline",
|
|
147
|
+
},
|
|
148
|
+
hitSlop: {
|
|
149
|
+
top: 10,
|
|
150
|
+
bottom: 10,
|
|
151
|
+
left: 10,
|
|
152
|
+
right: 10,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Pressable, StyleSheet, ViewStyle } from 'react-native';
|
|
3
|
+
import { useNavigation } from '@react-navigation/native';
|
|
4
|
+
import { useResponsiveDesignTokens, AtomicIcon, AtomicText } from '@umituz/react-native-design-system';
|
|
5
|
+
import { LegalConfig } from '../../domain/entities/LegalConfig';
|
|
6
|
+
|
|
7
|
+
export interface LegalSectionProps {
|
|
8
|
+
config?: LegalConfig;
|
|
9
|
+
onPress?: () => void;
|
|
10
|
+
containerStyle?: ViewStyle;
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
sectionTitle?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const LegalSection: React.FC<LegalSectionProps> = ({
|
|
17
|
+
config,
|
|
18
|
+
onPress,
|
|
19
|
+
containerStyle,
|
|
20
|
+
title: propsTitle,
|
|
21
|
+
description: propsDescription,
|
|
22
|
+
sectionTitle: propsSectionTitle,
|
|
23
|
+
}) => {
|
|
24
|
+
const navigation = useNavigation();
|
|
25
|
+
const tokens = useResponsiveDesignTokens();
|
|
26
|
+
const colors = tokens.colors;
|
|
27
|
+
|
|
28
|
+
const route = config?.route || config?.defaultRoute || 'Legal';
|
|
29
|
+
const title = propsTitle || config?.title;
|
|
30
|
+
const description = propsDescription || config?.description;
|
|
31
|
+
const sectionTitle = propsSectionTitle || title;
|
|
32
|
+
|
|
33
|
+
const handlePress = () => {
|
|
34
|
+
if (onPress) {
|
|
35
|
+
onPress();
|
|
36
|
+
} else {
|
|
37
|
+
navigation.navigate(route as never);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (!title) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={[styles.sectionContainer, { backgroundColor: colors.surface }, containerStyle]}>
|
|
45
|
+
{!!sectionTitle && (
|
|
46
|
+
<View style={styles.headerContainer}>
|
|
47
|
+
<AtomicText
|
|
48
|
+
type="titleMedium"
|
|
49
|
+
color="primary"
|
|
50
|
+
>
|
|
51
|
+
{sectionTitle}
|
|
52
|
+
</AtomicText>
|
|
53
|
+
</View>
|
|
54
|
+
)}
|
|
55
|
+
<Pressable
|
|
56
|
+
style={({ pressed }) => [
|
|
57
|
+
styles.itemContainer,
|
|
58
|
+
{
|
|
59
|
+
backgroundColor: pressed ? `${colors.primary}08` : 'transparent',
|
|
60
|
+
},
|
|
61
|
+
]}
|
|
62
|
+
onPress={handlePress}
|
|
63
|
+
>
|
|
64
|
+
<View style={styles.content}>
|
|
65
|
+
<View
|
|
66
|
+
style={[
|
|
67
|
+
styles.iconContainer,
|
|
68
|
+
{ backgroundColor: `${colors.primary}15` },
|
|
69
|
+
]}
|
|
70
|
+
>
|
|
71
|
+
<AtomicIcon name="document-text" size="lg" color="primary" />
|
|
72
|
+
</View>
|
|
73
|
+
<View style={styles.textContainer}>
|
|
74
|
+
<AtomicText
|
|
75
|
+
type="bodyLarge"
|
|
76
|
+
color="primary"
|
|
77
|
+
numberOfLines={1}
|
|
78
|
+
style={{ marginBottom: 4 }}
|
|
79
|
+
>
|
|
80
|
+
{title}
|
|
81
|
+
</AtomicText>
|
|
82
|
+
{!!description && (
|
|
83
|
+
<AtomicText
|
|
84
|
+
type="bodyMedium"
|
|
85
|
+
color="secondary"
|
|
86
|
+
numberOfLines={2}
|
|
87
|
+
>
|
|
88
|
+
{description}
|
|
89
|
+
</AtomicText>
|
|
90
|
+
)}
|
|
91
|
+
</View>
|
|
92
|
+
<AtomicIcon name="chevron-right" size="md" color="secondary" />
|
|
93
|
+
</View>
|
|
94
|
+
</Pressable>
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const styles = StyleSheet.create({
|
|
100
|
+
sectionContainer: {
|
|
101
|
+
marginBottom: 16,
|
|
102
|
+
borderRadius: 12,
|
|
103
|
+
overflow: 'hidden',
|
|
104
|
+
},
|
|
105
|
+
headerContainer: {
|
|
106
|
+
paddingHorizontal: 16,
|
|
107
|
+
paddingTop: 16,
|
|
108
|
+
paddingBottom: 8,
|
|
109
|
+
},
|
|
110
|
+
itemContainer: {
|
|
111
|
+
flexDirection: 'row',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
paddingHorizontal: 16,
|
|
114
|
+
paddingVertical: 16,
|
|
115
|
+
minHeight: 72,
|
|
116
|
+
},
|
|
117
|
+
content: {
|
|
118
|
+
flex: 1,
|
|
119
|
+
flexDirection: 'row',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
},
|
|
122
|
+
iconContainer: {
|
|
123
|
+
width: 48,
|
|
124
|
+
height: 48,
|
|
125
|
+
borderRadius: 12,
|
|
126
|
+
justifyContent: 'center',
|
|
127
|
+
alignItems: 'center',
|
|
128
|
+
marginRight: 16,
|
|
129
|
+
},
|
|
130
|
+
textContainer: {
|
|
131
|
+
flex: 1,
|
|
132
|
+
marginRight: 8,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legal Screen Component
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Layout and orchestration of legal documents list
|
|
5
|
+
* Delegates item rendering to LegalItem component
|
|
6
|
+
*
|
|
7
|
+
* All text content is passed as props (no localization dependency).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { View, StyleSheet } from "react-native";
|
|
12
|
+
import { useResponsiveDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
13
|
+
import { AtomicText } from "@umituz/react-native-design-system";
|
|
14
|
+
import { ScreenLayout } from "@umituz/react-native-design-system";
|
|
15
|
+
import { LegalItem } from "../components/LegalItem";
|
|
16
|
+
import { UrlHandlerService } from "../../domain/services/UrlHandlerService";
|
|
17
|
+
import { ContentValidationService } from "../../domain/services/ContentValidationService";
|
|
18
|
+
import { StyleCacheService } from "../../domain/services/StyleCacheService";
|
|
19
|
+
|
|
20
|
+
export interface LegalScreenProps {
|
|
21
|
+
/**
|
|
22
|
+
* Title of the screen
|
|
23
|
+
*/
|
|
24
|
+
title?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Description/subtitle text
|
|
27
|
+
*/
|
|
28
|
+
description?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Header text for documents section
|
|
31
|
+
*/
|
|
32
|
+
documentsHeader?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Privacy Policy button text
|
|
35
|
+
*/
|
|
36
|
+
privacyTitle?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Privacy Policy description
|
|
39
|
+
*/
|
|
40
|
+
privacyDescription?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Terms of Service button text
|
|
43
|
+
*/
|
|
44
|
+
termsTitle?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Terms of Service description
|
|
47
|
+
*/
|
|
48
|
+
termsDescription?: string;
|
|
49
|
+
/**
|
|
50
|
+
* EULA button text
|
|
51
|
+
*/
|
|
52
|
+
eulaTitle?: string;
|
|
53
|
+
/**
|
|
54
|
+
* EULA description
|
|
55
|
+
*/
|
|
56
|
+
eulaDescription?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Callback when Privacy Policy is pressed
|
|
59
|
+
*/
|
|
60
|
+
onPrivacyPress?: () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Callback when Terms of Service is pressed
|
|
63
|
+
*/
|
|
64
|
+
onTermsPress?: () => void;
|
|
65
|
+
/**
|
|
66
|
+
* Callback when EULA is pressed
|
|
67
|
+
* Icon name from Feather library (e.g., "shield", "file-text", "file") EULA URL
|
|
68
|
+
*/
|
|
69
|
+
onEulaPress?: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* EULA URL (defaults to Apple's standard EULA)
|
|
72
|
+
*/
|
|
73
|
+
eulaUrl?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Test ID for E2E testing
|
|
76
|
+
*/
|
|
77
|
+
testID?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const LegalScreen: React.FC<LegalScreenProps> = React.memo(({
|
|
81
|
+
title,
|
|
82
|
+
description,
|
|
83
|
+
documentsHeader,
|
|
84
|
+
privacyTitle,
|
|
85
|
+
privacyDescription,
|
|
86
|
+
termsTitle,
|
|
87
|
+
termsDescription,
|
|
88
|
+
eulaTitle,
|
|
89
|
+
eulaDescription,
|
|
90
|
+
onPrivacyPress,
|
|
91
|
+
onTermsPress,
|
|
92
|
+
onEulaPress,
|
|
93
|
+
eulaUrl,
|
|
94
|
+
testID = "legal-screen",
|
|
95
|
+
}) => {
|
|
96
|
+
const tokens = useResponsiveDesignTokens();
|
|
97
|
+
|
|
98
|
+
// Memoize styles to prevent recreation on every render
|
|
99
|
+
const styles = React.useMemo(() => {
|
|
100
|
+
const cacheKey = StyleCacheService.createTokenCacheKey(tokens);
|
|
101
|
+
return StyleCacheService.getCachedStyles(
|
|
102
|
+
'LegalScreen',
|
|
103
|
+
cacheKey,
|
|
104
|
+
() => createLegalScreenStyles(tokens)
|
|
105
|
+
);
|
|
106
|
+
}, [tokens]);
|
|
107
|
+
|
|
108
|
+
// Memoize EULA press handler to prevent child re-renders
|
|
109
|
+
const handleEulaPress = React.useCallback(async () => {
|
|
110
|
+
if (__DEV__) {
|
|
111
|
+
console.log('LegalScreen: EULA pressed', { eulaUrl });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (onEulaPress) {
|
|
115
|
+
onEulaPress();
|
|
116
|
+
} else if (eulaUrl) {
|
|
117
|
+
try {
|
|
118
|
+
await UrlHandlerService.openUrl(eulaUrl);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (__DEV__) {
|
|
121
|
+
console.error('LegalScreen: Error opening EULA URL', error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}, [onEulaPress, eulaUrl]);
|
|
126
|
+
|
|
127
|
+
// Memoize conditional rendering to prevent unnecessary re-renders
|
|
128
|
+
const showHeader = React.useMemo(() => !!(title), [title]);
|
|
129
|
+
const showDescription = React.useMemo(() => !!(description), [description]);
|
|
130
|
+
const showSectionHeader = React.useMemo(() => !!(documentsHeader), [documentsHeader]);
|
|
131
|
+
const showPrivacy = React.useMemo(() =>
|
|
132
|
+
ContentValidationService.shouldShowLegalItem(onPrivacyPress, privacyTitle),
|
|
133
|
+
[onPrivacyPress, privacyTitle]
|
|
134
|
+
);
|
|
135
|
+
const showTerms = React.useMemo(() =>
|
|
136
|
+
ContentValidationService.shouldShowLegalItem(onTermsPress, termsTitle),
|
|
137
|
+
[onTermsPress, termsTitle]
|
|
138
|
+
);
|
|
139
|
+
const showEula = React.useMemo(() =>
|
|
140
|
+
!!((onEulaPress || eulaUrl) && eulaTitle),
|
|
141
|
+
[onEulaPress, eulaUrl, eulaTitle]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Memoize header content
|
|
145
|
+
const headerContent = React.useMemo(() => {
|
|
146
|
+
if (!showHeader) return null;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<View style={styles.header}>
|
|
150
|
+
<AtomicText type="headlineLarge" color="textPrimary">
|
|
151
|
+
{title}
|
|
152
|
+
</AtomicText>
|
|
153
|
+
{showDescription && (
|
|
154
|
+
<AtomicText
|
|
155
|
+
type="bodyMedium"
|
|
156
|
+
color="textSecondary"
|
|
157
|
+
style={styles.headerSubtitle}
|
|
158
|
+
>
|
|
159
|
+
{description}
|
|
160
|
+
</AtomicText>
|
|
161
|
+
)}
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
}, [showHeader, showDescription, styles.header, styles.headerSubtitle, title, description]);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<ScreenLayout testID={testID} hideScrollIndicator>
|
|
168
|
+
{/* Header */}
|
|
169
|
+
{headerContent}
|
|
170
|
+
|
|
171
|
+
{/* Legal Documents Section */}
|
|
172
|
+
<View style={styles.section}>
|
|
173
|
+
{showSectionHeader && (
|
|
174
|
+
<AtomicText
|
|
175
|
+
type="labelLarge"
|
|
176
|
+
color="textSecondary"
|
|
177
|
+
style={styles.sectionHeader}
|
|
178
|
+
>
|
|
179
|
+
{documentsHeader}
|
|
180
|
+
</AtomicText>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* Privacy Policy */}
|
|
184
|
+
{showPrivacy && (
|
|
185
|
+
<LegalItem
|
|
186
|
+
iconName="shield"
|
|
187
|
+
title={privacyTitle!}
|
|
188
|
+
description={privacyDescription}
|
|
189
|
+
onPress={onPrivacyPress}
|
|
190
|
+
testID="privacy-policy-item"
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Terms of Service */}
|
|
195
|
+
{showTerms && (
|
|
196
|
+
<LegalItem
|
|
197
|
+
iconName="document-text"
|
|
198
|
+
title={termsTitle!}
|
|
199
|
+
description={termsDescription}
|
|
200
|
+
onPress={onTermsPress}
|
|
201
|
+
testID="terms-of-service-item"
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{/* EULA */}
|
|
206
|
+
{showEula && (
|
|
207
|
+
<LegalItem
|
|
208
|
+
iconName="document"
|
|
209
|
+
title={eulaTitle!}
|
|
210
|
+
description={eulaDescription}
|
|
211
|
+
onPress={handleEulaPress}
|
|
212
|
+
testID="eula-item"
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
</View>
|
|
216
|
+
</ScreenLayout>
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const createLegalScreenStyles = (tokens: DesignTokens) => {
|
|
221
|
+
return StyleSheet.create({
|
|
222
|
+
header: {
|
|
223
|
+
paddingBottom: tokens.spacing.lg,
|
|
224
|
+
paddingTop: tokens.spacing.md,
|
|
225
|
+
},
|
|
226
|
+
headerSubtitle: {
|
|
227
|
+
marginTop: tokens.spacing.xs,
|
|
228
|
+
},
|
|
229
|
+
section: {
|
|
230
|
+
marginTop: tokens.spacing.md,
|
|
231
|
+
},
|
|
232
|
+
sectionHeader: {
|
|
233
|
+
marginBottom: tokens.spacing.sm,
|
|
234
|
+
paddingHorizontal: tokens.spacing.md,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
};
|