@umituz/react-native-settings 4.20.52 → 4.20.54
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/about/presentation/components/AboutSettingItem.tsx +39 -86
- package/src/domains/about/presentation/hooks/useAboutInfo.ts +39 -101
- package/src/domains/about/presentation/hooks/useAboutInfo.types.ts +32 -0
- package/src/domains/about/utils/AppInfoFactory.ts +19 -0
- package/src/domains/about/utils/index.ts +2 -0
- package/src/domains/legal/index.ts +1 -0
- package/src/domains/legal/presentation/screens/LegalContentScreen.tsx +140 -0
- package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +17 -155
- package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +17 -155
- package/src/presentation/navigation/SettingsStackNavigator.tsx +50 -129
- package/src/presentation/navigation/components/wrappers/AboutScreenWrapper.tsx +13 -0
- package/src/presentation/navigation/components/wrappers/LegalScreenWrapper.tsx +50 -0
- package/src/presentation/navigation/components/wrappers/SettingsScreenWrapper.tsx +32 -0
- package/src/presentation/navigation/components/wrappers/index.ts +9 -0
- package/src/presentation/navigation/utils/index.ts +5 -0
- package/src/presentation/navigation/utils/navigationScreenOptions.ts +56 -0
- package/src/presentation/navigation/utils/navigationTranslations.ts +46 -0
- package/src/presentation/screens/types/BaseTypes.ts +12 -0
- package/src/presentation/screens/types/ContentConfig.ts +82 -0
- package/src/presentation/screens/types/SettingsConfig.ts +6 -4
- package/src/presentation/screens/types/UserFeatureConfig.ts +137 -0
- package/src/presentation/screens/types/index.ts +10 -8
- package/src/presentation/screens/types/FeatureConfig.ts +0 -263
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "4.20.
|
|
3
|
+
"version": "4.20.54",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, about, legal, appearance, feedback, FAQs, and rating",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Fully configurable and generic
|
|
5
5
|
* Optimized for performance and memory safety
|
|
6
6
|
*/
|
|
7
|
-
import React, {
|
|
7
|
+
import React, { memo, useCallback } from 'react';
|
|
8
8
|
import {
|
|
9
9
|
TouchableOpacity,
|
|
10
10
|
View,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ViewStyle,
|
|
14
14
|
TextStyle,
|
|
15
15
|
} from 'react-native';
|
|
16
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
16
17
|
|
|
17
18
|
export interface AboutSettingItemProps {
|
|
18
19
|
/** Icon component (any React component) */
|
|
@@ -45,9 +46,7 @@ export interface AboutSettingItemProps {
|
|
|
45
46
|
chevronColor?: string;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export const AboutSettingItem: React.FC<AboutSettingItemProps> = ({
|
|
49
|
+
export const AboutSettingItem: React.FC<AboutSettingItemProps> = memo(({
|
|
51
50
|
icon,
|
|
52
51
|
title,
|
|
53
52
|
description,
|
|
@@ -67,107 +66,61 @@ export const AboutSettingItem: React.FC<AboutSettingItemProps> = ({
|
|
|
67
66
|
const styles = getStyles(tokens);
|
|
68
67
|
const colors = tokens.colors;
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
const Container = useMemo(() => {
|
|
72
|
-
return onPress ? TouchableOpacity : View;
|
|
73
|
-
}, [onPress]) as React.ComponentType<React.ComponentProps<typeof TouchableOpacity | typeof View>>;
|
|
74
|
-
|
|
75
|
-
// Memoize container styles
|
|
76
|
-
const containerStyles = useMemo(() => {
|
|
77
|
-
return [
|
|
78
|
-
styles.container,
|
|
79
|
-
{ backgroundColor: colors.surface },
|
|
80
|
-
disabled && styles.disabled,
|
|
81
|
-
containerStyle
|
|
82
|
-
];
|
|
83
|
-
}, [disabled, containerStyle, colors.surface, styles]);
|
|
69
|
+
const Container = onPress ? TouchableOpacity : View;
|
|
84
70
|
|
|
85
|
-
// Memoize icon container styles
|
|
86
|
-
const iconContainerStyles = useMemo(() => {
|
|
87
|
-
return [
|
|
88
|
-
styles.iconContainer,
|
|
89
|
-
iconContainerStyle
|
|
90
|
-
];
|
|
91
|
-
}, [iconContainerStyle, styles]);
|
|
92
|
-
|
|
93
|
-
// Memoize chevron styles
|
|
94
|
-
const chevronStyles = useMemo(() => {
|
|
95
|
-
return [
|
|
96
|
-
styles.chevron,
|
|
97
|
-
{ color: chevronColor || colors.textSecondary }
|
|
98
|
-
];
|
|
99
|
-
}, [chevronColor, colors.textSecondary, styles]);
|
|
100
|
-
|
|
101
|
-
// Memoize press handler to prevent unnecessary re-renders
|
|
102
71
|
const handlePress = useCallback(() => {
|
|
103
72
|
if (onPress && !disabled) {
|
|
104
73
|
onPress();
|
|
105
74
|
}
|
|
106
75
|
}, [onPress, disabled]);
|
|
107
76
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
77
|
+
const containerStyles = [
|
|
78
|
+
styles.container,
|
|
79
|
+
{ backgroundColor: colors.surface },
|
|
80
|
+
disabled && styles.disabled,
|
|
81
|
+
containerStyle,
|
|
82
|
+
];
|
|
113
83
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
84
|
+
const iconContainerStyles = [
|
|
85
|
+
styles.iconContainer,
|
|
86
|
+
iconContainerStyle,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const chevronStyles = [
|
|
90
|
+
styles.chevron,
|
|
91
|
+
{ color: chevronColor || colors.textSecondary },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Container
|
|
96
|
+
style={containerStyles}
|
|
97
|
+
onPress={handlePress}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
testID={testID}
|
|
100
|
+
>
|
|
101
|
+
{icon && <View style={iconContainerStyles}>{icon}</View>}
|
|
120
102
|
|
|
121
|
-
// Memoize content rendering
|
|
122
|
-
const renderContent = useMemo(() => {
|
|
123
|
-
return (
|
|
124
103
|
<View style={styles.content}>
|
|
125
|
-
<Text style={[styles.title, { color: colors.textPrimary }, titleStyle]}>
|
|
104
|
+
<Text style={[styles.title, { color: colors.textPrimary }, titleStyle]}>
|
|
105
|
+
{title}
|
|
106
|
+
</Text>
|
|
126
107
|
{description && (
|
|
127
108
|
<Text style={[styles.description, { color: colors.textSecondary }, descriptionStyle]}>
|
|
128
109
|
{description}
|
|
129
110
|
</Text>
|
|
130
111
|
)}
|
|
131
112
|
</View>
|
|
132
|
-
);
|
|
133
|
-
}, [title, description, titleStyle, descriptionStyle, colors.textPrimary, colors.textSecondary, styles]);
|
|
134
|
-
|
|
135
|
-
// Memoize value rendering
|
|
136
|
-
const renderValue = useMemo(() => {
|
|
137
|
-
if (!value) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<Text style={[styles.value, { color: colors.textSecondary }, valueStyle]}>{value}</Text>
|
|
143
|
-
);
|
|
144
|
-
}, [value, valueStyle, colors.textSecondary, styles]);
|
|
145
|
-
|
|
146
|
-
// Memoize chevron rendering
|
|
147
|
-
const renderChevron = useMemo(() => {
|
|
148
|
-
if (!showChevron) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
113
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
114
|
+
{value && (
|
|
115
|
+
<Text style={[styles.value, { color: colors.textSecondary }, valueStyle]}>
|
|
116
|
+
{value}
|
|
117
|
+
</Text>
|
|
118
|
+
)}
|
|
156
119
|
|
|
157
|
-
|
|
158
|
-
<Container
|
|
159
|
-
style={containerStyles}
|
|
160
|
-
onPress={handlePress}
|
|
161
|
-
disabled={disabled}
|
|
162
|
-
testID={testID}
|
|
163
|
-
>
|
|
164
|
-
{renderIcon}
|
|
165
|
-
{renderContent}
|
|
166
|
-
{renderValue}
|
|
167
|
-
{renderChevron}
|
|
120
|
+
{showChevron && <Text style={chevronStyles}>›</Text>}
|
|
168
121
|
</Container>
|
|
169
122
|
);
|
|
170
|
-
};
|
|
123
|
+
});
|
|
171
124
|
|
|
172
125
|
const getStyles = (tokens: any) => StyleSheet.create({
|
|
173
126
|
container: {
|
|
@@ -206,4 +159,4 @@ const getStyles = (tokens: any) => StyleSheet.create({
|
|
|
206
159
|
fontSize: 20,
|
|
207
160
|
fontWeight: '300',
|
|
208
161
|
},
|
|
209
|
-
});
|
|
162
|
+
});
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Provides reactive state management for About data
|
|
4
4
|
* Optimized for performance and memory safety
|
|
5
5
|
*/
|
|
6
|
-
import { useState,
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
7
|
import { AppInfo, AboutConfig } from '../../domain/entities/AppInfo';
|
|
8
8
|
import { AboutRepository } from '../../infrastructure/repositories/AboutRepository';
|
|
9
|
+
import { createDefaultAppInfo } from '../../utils/AppInfoFactory';
|
|
9
10
|
|
|
10
11
|
export interface UseAboutInfoOptions {
|
|
11
12
|
/** Initial configuration */
|
|
@@ -42,161 +43,114 @@ export const useAboutInfo = (
|
|
|
42
43
|
const [loading, setLoading] = useState(false);
|
|
43
44
|
const [error, setError] = useState<string | null>(null);
|
|
44
45
|
|
|
45
|
-
// Prevent infinite loops and memory leaks
|
|
46
46
|
const isInitializedRef = useRef(false);
|
|
47
47
|
const isMountedRef = useRef(true);
|
|
48
48
|
|
|
49
|
+
const setErrorIfMounted = useCallback((err: string | null) => {
|
|
50
|
+
if (isMountedRef.current) {
|
|
51
|
+
setError(err);
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const setLoadingIfMounted = useCallback((value: boolean) => {
|
|
56
|
+
if (isMountedRef.current) {
|
|
57
|
+
setLoading(value);
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
49
61
|
const initialize = useCallback(async (config: AboutConfig, force = false) => {
|
|
50
|
-
// Prevent multiple initializations unless forced
|
|
51
62
|
if (isInitializedRef.current && !force) {
|
|
52
63
|
return;
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
// Check if component is still mounted
|
|
56
66
|
if (!isMountedRef.current) {
|
|
57
67
|
return;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
setLoadingIfMounted(true);
|
|
71
|
+
setErrorIfMounted(null);
|
|
62
72
|
|
|
63
73
|
try {
|
|
64
|
-
const defaultAppInfo
|
|
65
|
-
name: config.appInfo?.name || '',
|
|
66
|
-
version: config.appInfo?.version || '1.0.0',
|
|
67
|
-
description: config.appInfo?.description,
|
|
68
|
-
developer: config.appInfo?.developer,
|
|
69
|
-
contactEmail: config.appInfo?.contactEmail,
|
|
70
|
-
websiteUrl: config.appInfo?.websiteUrl,
|
|
71
|
-
websiteDisplay: config.appInfo?.websiteDisplay,
|
|
72
|
-
moreAppsUrl: config.appInfo?.moreAppsUrl,
|
|
73
|
-
};
|
|
74
|
-
|
|
74
|
+
const defaultAppInfo = createDefaultAppInfo(config);
|
|
75
75
|
await repository.saveAppInfo(defaultAppInfo);
|
|
76
76
|
|
|
77
|
-
// Only update state if component is still mounted
|
|
78
77
|
if (isMountedRef.current) {
|
|
79
78
|
setAppInfo(defaultAppInfo);
|
|
80
79
|
isInitializedRef.current = true;
|
|
81
80
|
}
|
|
82
81
|
} catch (err) {
|
|
83
|
-
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
88
|
-
setError(errorMessage);
|
|
82
|
+
setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
|
|
89
83
|
} finally {
|
|
90
|
-
|
|
91
|
-
if (isMountedRef.current) {
|
|
92
|
-
setLoading(false);
|
|
93
|
-
}
|
|
84
|
+
setLoadingIfMounted(false);
|
|
94
85
|
}
|
|
95
|
-
}, [repository]);
|
|
86
|
+
}, [repository, setErrorIfMounted, setLoadingIfMounted]);
|
|
96
87
|
|
|
97
88
|
const update = useCallback(async (config: AboutConfig) => {
|
|
98
89
|
if (!isMountedRef.current) {
|
|
99
90
|
return;
|
|
100
91
|
}
|
|
101
92
|
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
setLoadingIfMounted(true);
|
|
94
|
+
setErrorIfMounted(null);
|
|
104
95
|
|
|
105
96
|
try {
|
|
106
|
-
const updatedAppInfo
|
|
107
|
-
name: config.appInfo?.name || '',
|
|
108
|
-
version: config.appInfo?.version || '1.0.0',
|
|
109
|
-
description: config.appInfo?.description,
|
|
110
|
-
developer: config.appInfo?.developer,
|
|
111
|
-
contactEmail: config.appInfo?.contactEmail,
|
|
112
|
-
websiteUrl: config.appInfo?.websiteUrl,
|
|
113
|
-
websiteDisplay: config.appInfo?.websiteDisplay,
|
|
114
|
-
moreAppsUrl: config.appInfo?.moreAppsUrl,
|
|
115
|
-
};
|
|
116
|
-
|
|
97
|
+
const updatedAppInfo = createDefaultAppInfo(config);
|
|
117
98
|
await repository.saveAppInfo(updatedAppInfo);
|
|
118
99
|
|
|
119
|
-
// Only update state if component is still mounted
|
|
120
100
|
if (isMountedRef.current) {
|
|
121
101
|
setAppInfo(updatedAppInfo);
|
|
122
102
|
}
|
|
123
103
|
} catch (err) {
|
|
124
|
-
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
129
|
-
setError(errorMessage);
|
|
104
|
+
setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
|
|
130
105
|
} finally {
|
|
131
|
-
|
|
132
|
-
if (isMountedRef.current) {
|
|
133
|
-
setLoading(false);
|
|
134
|
-
}
|
|
106
|
+
setLoadingIfMounted(false);
|
|
135
107
|
}
|
|
136
|
-
}, [repository]);
|
|
108
|
+
}, [repository, setErrorIfMounted, setLoadingIfMounted]);
|
|
137
109
|
|
|
138
110
|
const updateAppInfo = useCallback(async (updates: Partial<AppInfo>) => {
|
|
139
111
|
if (!appInfo || !isMountedRef.current) {
|
|
140
112
|
if (isMountedRef.current) {
|
|
141
|
-
|
|
113
|
+
setErrorIfMounted('App info not initialized');
|
|
142
114
|
}
|
|
143
115
|
return;
|
|
144
116
|
}
|
|
145
117
|
|
|
146
|
-
|
|
147
|
-
|
|
118
|
+
setLoadingIfMounted(true);
|
|
119
|
+
setErrorIfMounted(null);
|
|
148
120
|
|
|
149
121
|
try {
|
|
150
122
|
const updatedInfo = await repository.updateAppInfo(updates);
|
|
151
123
|
|
|
152
|
-
// Only update state if component is still mounted
|
|
153
124
|
if (isMountedRef.current) {
|
|
154
125
|
setAppInfo(updatedInfo);
|
|
155
126
|
}
|
|
156
127
|
} catch (err) {
|
|
157
|
-
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
162
|
-
setError(errorMessage);
|
|
128
|
+
setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
|
|
163
129
|
} finally {
|
|
164
|
-
|
|
165
|
-
if (isMountedRef.current) {
|
|
166
|
-
setLoading(false);
|
|
167
|
-
}
|
|
130
|
+
setLoadingIfMounted(false);
|
|
168
131
|
}
|
|
169
|
-
}, [repository, appInfo]);
|
|
132
|
+
}, [repository, appInfo, setErrorIfMounted, setLoadingIfMounted]);
|
|
170
133
|
|
|
171
134
|
const refresh = useCallback(async () => {
|
|
172
135
|
if (!isMountedRef.current || !appInfo) {
|
|
173
136
|
return;
|
|
174
137
|
}
|
|
175
138
|
|
|
176
|
-
|
|
177
|
-
|
|
139
|
+
setLoadingIfMounted(true);
|
|
140
|
+
setErrorIfMounted(null);
|
|
178
141
|
|
|
179
142
|
try {
|
|
180
143
|
const refreshedInfo = await repository.getAppInfo();
|
|
181
144
|
|
|
182
|
-
// Only update state if component is still mounted
|
|
183
145
|
if (isMountedRef.current) {
|
|
184
146
|
setAppInfo(refreshedInfo);
|
|
185
147
|
}
|
|
186
148
|
} catch (err) {
|
|
187
|
-
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
192
|
-
setError(errorMessage);
|
|
149
|
+
setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
|
|
193
150
|
} finally {
|
|
194
|
-
|
|
195
|
-
if (isMountedRef.current) {
|
|
196
|
-
setLoading(false);
|
|
197
|
-
}
|
|
151
|
+
setLoadingIfMounted(false);
|
|
198
152
|
}
|
|
199
|
-
}, [repository, appInfo]);
|
|
153
|
+
}, [repository, appInfo, setErrorIfMounted, setLoadingIfMounted]);
|
|
200
154
|
|
|
201
155
|
const reset = useCallback(() => {
|
|
202
156
|
if (!isMountedRef.current) {
|
|
@@ -209,40 +163,24 @@ export const useAboutInfo = (
|
|
|
209
163
|
isInitializedRef.current = false;
|
|
210
164
|
}, []);
|
|
211
165
|
|
|
212
|
-
// Cleanup on unmount to prevent memory leaks
|
|
213
166
|
useEffect(() => {
|
|
214
167
|
return () => {
|
|
215
168
|
isMountedRef.current = false;
|
|
216
169
|
|
|
217
|
-
// Cleanup repository if it has destroy method
|
|
218
170
|
if (repository && typeof repository.destroy === 'function') {
|
|
219
171
|
repository.destroy();
|
|
220
172
|
}
|
|
221
173
|
};
|
|
222
174
|
}, [repository]);
|
|
223
175
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Set initial config when provided (if autoInit is not explicitly false)
|
|
227
176
|
useEffect(() => {
|
|
228
177
|
if (initialConfig && autoInit !== false && isMountedRef.current && !isInitializedRef.current) {
|
|
229
|
-
const defaultAppInfo
|
|
230
|
-
name: initialConfig.appInfo?.name || '',
|
|
231
|
-
version: initialConfig.appInfo?.version || '1.0.0',
|
|
232
|
-
description: initialConfig.appInfo?.description,
|
|
233
|
-
developer: initialConfig.appInfo?.developer,
|
|
234
|
-
contactEmail: initialConfig.appInfo?.contactEmail,
|
|
235
|
-
websiteUrl: initialConfig.appInfo?.websiteUrl,
|
|
236
|
-
websiteDisplay: initialConfig.appInfo?.websiteDisplay,
|
|
237
|
-
moreAppsUrl: initialConfig.appInfo?.moreAppsUrl,
|
|
238
|
-
};
|
|
239
|
-
|
|
178
|
+
const defaultAppInfo = createDefaultAppInfo(initialConfig);
|
|
240
179
|
setAppInfo(defaultAppInfo);
|
|
241
180
|
isInitializedRef.current = true;
|
|
242
181
|
}
|
|
243
182
|
}, [initialConfig, autoInit]);
|
|
244
183
|
|
|
245
|
-
// Auto-initialize with dependency optimization
|
|
246
184
|
useEffect(() => {
|
|
247
185
|
if (autoInit === true && initialConfig && isMountedRef.current) {
|
|
248
186
|
initialize(initialConfig, true);
|
|
@@ -259,4 +197,4 @@ export const useAboutInfo = (
|
|
|
259
197
|
refresh,
|
|
260
198
|
reset,
|
|
261
199
|
};
|
|
262
|
-
};
|
|
200
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAboutInfo Hook Types
|
|
3
|
+
* Type definitions for about info hook
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AppInfo, AboutConfig } from '../../domain/entities/AppInfo';
|
|
7
|
+
|
|
8
|
+
export interface UseAboutInfoOptions {
|
|
9
|
+
/** Initial configuration */
|
|
10
|
+
initialConfig?: AboutConfig;
|
|
11
|
+
/** Auto-initialize on mount */
|
|
12
|
+
autoInit?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseAboutInfoReturn {
|
|
16
|
+
/** Current app info */
|
|
17
|
+
appInfo: AppInfo | null;
|
|
18
|
+
/** Loading state */
|
|
19
|
+
loading: boolean;
|
|
20
|
+
/** Error state */
|
|
21
|
+
error: string | null;
|
|
22
|
+
/** Initialize with config */
|
|
23
|
+
initialize: (config: AboutConfig) => Promise<void>;
|
|
24
|
+
/** Update with new config */
|
|
25
|
+
update: (config: AboutConfig) => Promise<void>;
|
|
26
|
+
/** Update app info */
|
|
27
|
+
updateAppInfo: (updates: Partial<AppInfo>) => Promise<void>;
|
|
28
|
+
/** Refresh current app info */
|
|
29
|
+
refresh: () => Promise<void>;
|
|
30
|
+
/** Reset to initial state */
|
|
31
|
+
reset: () => void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppInfo Factory Utility
|
|
3
|
+
* Creates AppInfo objects from AboutConfig
|
|
4
|
+
*/
|
|
5
|
+
import { AppInfo, AboutConfig } from '../domain/entities/AppInfo';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a default AppInfo object from AboutConfig
|
|
9
|
+
*/
|
|
10
|
+
export const createDefaultAppInfo = (config: AboutConfig): AppInfo => ({
|
|
11
|
+
name: config.appInfo?.name || '',
|
|
12
|
+
version: config.appInfo?.version || '1.0.0',
|
|
13
|
+
description: config.appInfo?.description,
|
|
14
|
+
developer: config.appInfo?.developer,
|
|
15
|
+
contactEmail: config.appInfo?.contactEmail,
|
|
16
|
+
websiteUrl: config.appInfo?.websiteUrl,
|
|
17
|
+
websiteDisplay: config.appInfo?.websiteDisplay,
|
|
18
|
+
moreAppsUrl: config.appInfo?.moreAppsUrl,
|
|
19
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Legal Content Screen Component
|
|
3
|
+
* Shared logic for legal document screens
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { View, ScrollView, StyleSheet } from "react-native";
|
|
7
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system";
|
|
10
|
+
import { UrlHandlerService } from "../../domain/services/UrlHandlerService";
|
|
11
|
+
import { ContentValidationService } from "../../domain/services/ContentValidationService";
|
|
12
|
+
import { StyleCacheService } from "../../domain/services/StyleCacheService";
|
|
13
|
+
|
|
14
|
+
export interface LegalContentScreenProps {
|
|
15
|
+
content?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
title: string;
|
|
18
|
+
viewOnlineText?: string;
|
|
19
|
+
openText?: string;
|
|
20
|
+
onUrlPress?: () => void;
|
|
21
|
+
testID?: string;
|
|
22
|
+
styleCacheKey: string;
|
|
23
|
+
createStyles: (tokens: any) => ReturnType<typeof StyleSheet.create>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const LegalContentScreen: React.FC<LegalContentScreenProps> = React.memo(({
|
|
27
|
+
content,
|
|
28
|
+
url,
|
|
29
|
+
title,
|
|
30
|
+
viewOnlineText,
|
|
31
|
+
openText,
|
|
32
|
+
onUrlPress,
|
|
33
|
+
testID,
|
|
34
|
+
styleCacheKey,
|
|
35
|
+
createStyles,
|
|
36
|
+
}) => {
|
|
37
|
+
const tokens = useAppDesignTokens();
|
|
38
|
+
const insets = useSafeAreaInsets();
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
ContentValidationService.validateScreenContent(
|
|
42
|
+
content,
|
|
43
|
+
url,
|
|
44
|
+
title,
|
|
45
|
+
viewOnlineText,
|
|
46
|
+
openText,
|
|
47
|
+
styleCacheKey
|
|
48
|
+
);
|
|
49
|
+
}, [content, url, title, viewOnlineText, openText, styleCacheKey]);
|
|
50
|
+
|
|
51
|
+
const styles = React.useMemo(() => {
|
|
52
|
+
const cacheKey = StyleCacheService.createTokenCacheKey(tokens);
|
|
53
|
+
return StyleCacheService.getCachedStyles(
|
|
54
|
+
styleCacheKey,
|
|
55
|
+
cacheKey,
|
|
56
|
+
() => createStyles(tokens)
|
|
57
|
+
);
|
|
58
|
+
}, [tokens, styleCacheKey, createStyles]);
|
|
59
|
+
|
|
60
|
+
const handleUrlPress = React.useCallback(async () => {
|
|
61
|
+
if (onUrlPress) {
|
|
62
|
+
onUrlPress();
|
|
63
|
+
} else if (url) {
|
|
64
|
+
try {
|
|
65
|
+
await UrlHandlerService.openUrl(url);
|
|
66
|
+
} catch {
|
|
67
|
+
// Silent error handling
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, [onUrlPress, url]);
|
|
71
|
+
|
|
72
|
+
const containerStyle = React.useMemo(() => [
|
|
73
|
+
styles.container,
|
|
74
|
+
{
|
|
75
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
76
|
+
paddingTop: insets.top,
|
|
77
|
+
},
|
|
78
|
+
], [styles.container, tokens.colors.backgroundPrimary, insets.top]);
|
|
79
|
+
|
|
80
|
+
const showContent = React.useMemo(() => !!(content), [content]);
|
|
81
|
+
const showUrlSection = React.useMemo(() =>
|
|
82
|
+
ContentValidationService.shouldShowUrlSection(url, onUrlPress),
|
|
83
|
+
[url, onUrlPress]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const contentSection = React.useMemo(() => {
|
|
87
|
+
if (showContent) {
|
|
88
|
+
return (
|
|
89
|
+
<AtomicText type="bodyMedium" color="onSurface" style={styles.text}>
|
|
90
|
+
{content}
|
|
91
|
+
</AtomicText>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (showUrlSection) {
|
|
96
|
+
return (
|
|
97
|
+
<View style={styles.urlContainer}>
|
|
98
|
+
<AtomicText
|
|
99
|
+
type="bodyMedium"
|
|
100
|
+
color="secondary"
|
|
101
|
+
style={styles.urlText}
|
|
102
|
+
>
|
|
103
|
+
{viewOnlineText}
|
|
104
|
+
</AtomicText>
|
|
105
|
+
<AtomicButton
|
|
106
|
+
variant="primary"
|
|
107
|
+
onPress={handleUrlPress}
|
|
108
|
+
fullWidth
|
|
109
|
+
style={styles.urlButton}
|
|
110
|
+
>
|
|
111
|
+
{openText}
|
|
112
|
+
</AtomicButton>
|
|
113
|
+
</View>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}, [showContent, showUrlSection, styles, content, viewOnlineText, openText, handleUrlPress]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View style={containerStyle} testID={testID}>
|
|
122
|
+
<ScrollView
|
|
123
|
+
contentContainerStyle={styles.scrollContent}
|
|
124
|
+
showsVerticalScrollIndicator={false}
|
|
125
|
+
>
|
|
126
|
+
<View style={styles.content}>
|
|
127
|
+
<AtomicText
|
|
128
|
+
type="headlineLarge"
|
|
129
|
+
color="primary"
|
|
130
|
+
style={styles.title}
|
|
131
|
+
>
|
|
132
|
+
{title}
|
|
133
|
+
</AtomicText>
|
|
134
|
+
|
|
135
|
+
{contentSection}
|
|
136
|
+
</View>
|
|
137
|
+
</ScrollView>
|
|
138
|
+
</View>
|
|
139
|
+
);
|
|
140
|
+
});
|