@umituz/react-native-settings 4.20.53 → 4.20.55

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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/src/domains/about/presentation/components/AboutSettingItem.tsx +39 -86
  3. package/src/domains/about/presentation/hooks/useAboutInfo.ts +80 -200
  4. package/src/domains/about/presentation/hooks/useAboutInfo.types.ts +32 -0
  5. package/src/domains/about/presentation/hooks/useAboutInfo.utils.ts +167 -0
  6. package/src/domains/about/utils/AppInfoFactory.ts +19 -0
  7. package/src/domains/about/utils/index.ts +2 -0
  8. package/src/domains/legal/index.ts +1 -0
  9. package/src/domains/legal/presentation/screens/LegalContentScreen.tsx +140 -0
  10. package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +17 -155
  11. package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +17 -155
  12. package/src/presentation/components/SettingsItemCard.tsx +2 -2
  13. package/src/presentation/navigation/SettingsStackNavigator.tsx +50 -129
  14. package/src/presentation/navigation/components/wrappers/AboutScreenWrapper.tsx +13 -0
  15. package/src/presentation/navigation/components/wrappers/LegalScreenWrapper.tsx +50 -0
  16. package/src/presentation/navigation/components/wrappers/SettingsScreenWrapper.tsx +32 -0
  17. package/src/presentation/navigation/components/wrappers/index.ts +9 -0
  18. package/src/presentation/navigation/utils/index.ts +5 -0
  19. package/src/presentation/navigation/utils/navigationScreenOptions.ts +56 -0
  20. package/src/presentation/navigation/utils/navigationTranslations.ts +46 -0
  21. package/src/presentation/screens/components/SettingsHeader.tsx +1 -1
  22. package/src/presentation/screens/types/BaseTypes.ts +12 -0
  23. package/src/presentation/screens/types/ContentConfig.ts +82 -0
  24. package/src/presentation/screens/types/SettingsConfig.ts +6 -4
  25. package/src/presentation/screens/types/UserFeatureConfig.ts +137 -0
  26. package/src/presentation/screens/types/index.ts +10 -8
  27. package/src/presentation/screens/types/FeatureConfig.ts +0 -263
@@ -0,0 +1,167 @@
1
+ /**
2
+ * useAboutInfo Hook Utilities
3
+ * Shared utility functions for about info hook
4
+ */
5
+
6
+ import type { MutableRefObject } from 'react';
7
+ import type { AppInfo, AboutConfig } from '../../domain/entities/AppInfo';
8
+ import type { AboutRepository } from '../../infrastructure/repositories/AboutRepository';
9
+ import { createDefaultAppInfo } from '../../utils/AppInfoFactory';
10
+
11
+ /**
12
+ * Safely set error if component is mounted
13
+ */
14
+ export const setErrorIfMounted = (
15
+ isMountedRef: MutableRefObject<boolean>,
16
+ setError: (error: string | null) => void,
17
+ err: string | null
18
+ ) => {
19
+ if (isMountedRef.current) {
20
+ setError(err);
21
+ }
22
+ };
23
+
24
+ /**
25
+ * Safely set loading state if component is mounted
26
+ */
27
+ export const setLoadingIfMounted = (
28
+ isMountedRef: MutableRefObject<boolean>,
29
+ setLoading: (loading: boolean) => void,
30
+ value: boolean
31
+ ) => {
32
+ if (isMountedRef.current) {
33
+ setLoading(value);
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Initialize app info with config
39
+ */
40
+ export const initializeAppInfo = async (
41
+ config: AboutConfig,
42
+ repository: AboutRepository,
43
+ isMountedRef: MutableRefObject<boolean>,
44
+ isInitializedRef: MutableRefObject<boolean>,
45
+ setAppInfo: (info: AppInfo | null) => void,
46
+ setError: (error: string | null) => void,
47
+ setLoading: (loading: boolean) => void,
48
+ force = false
49
+ ): Promise<void> => {
50
+ if (isInitializedRef.current && !force) {
51
+ return;
52
+ }
53
+
54
+ if (!isMountedRef.current) {
55
+ return;
56
+ }
57
+
58
+ setLoading(true);
59
+ setError(null);
60
+
61
+ try {
62
+ const defaultAppInfo = createDefaultAppInfo(config);
63
+ await repository.saveAppInfo(defaultAppInfo);
64
+
65
+ if (isMountedRef.current) {
66
+ setAppInfo(defaultAppInfo);
67
+ isInitializedRef.current = true;
68
+ }
69
+ } catch (err) {
70
+ setError(err instanceof Error ? err.message : 'Unknown error');
71
+ } finally {
72
+ setLoading(false);
73
+ }
74
+ };
75
+
76
+ /**
77
+ * Update app info with new config
78
+ */
79
+ export const updateAppInfoConfig = async (
80
+ config: AboutConfig,
81
+ repository: AboutRepository,
82
+ isMountedRef: MutableRefObject<boolean>,
83
+ setAppInfo: (info: AppInfo | null) => void,
84
+ setError: (error: string | null) => void,
85
+ setLoading: (loading: boolean) => void
86
+ ): Promise<void> => {
87
+ if (!isMountedRef.current) {
88
+ return;
89
+ }
90
+
91
+ setLoading(true);
92
+ setError(null);
93
+
94
+ try {
95
+ const updatedAppInfo = createDefaultAppInfo(config);
96
+ await repository.saveAppInfo(updatedAppInfo);
97
+
98
+ if (isMountedRef.current) {
99
+ setAppInfo(updatedAppInfo);
100
+ }
101
+ } catch (err) {
102
+ setError(err instanceof Error ? err.message : 'Unknown error');
103
+ } finally {
104
+ setLoading(false);
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Update app info with partial updates
110
+ */
111
+ export const updateAppInfoPartial = async (
112
+ updates: Partial<AppInfo>,
113
+ repository: AboutRepository,
114
+ isMountedRef: MutableRefObject<boolean>,
115
+ setAppInfo: (info: AppInfo | null) => void,
116
+ setError: (error: string | null) => void,
117
+ setLoading: (loading: boolean) => void
118
+ ): Promise<void> => {
119
+ if (!isMountedRef.current) {
120
+ return;
121
+ }
122
+
123
+ setLoading(true);
124
+ setError(null);
125
+
126
+ try {
127
+ const updatedInfo = await repository.updateAppInfo(updates);
128
+
129
+ if (isMountedRef.current) {
130
+ setAppInfo(updatedInfo);
131
+ }
132
+ } catch (err) {
133
+ setError(err instanceof Error ? err.message : 'Unknown error');
134
+ } finally {
135
+ setLoading(false);
136
+ }
137
+ };
138
+
139
+ /**
140
+ * Refresh app info from repository
141
+ */
142
+ export const refreshAppInfo = async (
143
+ repository: AboutRepository,
144
+ isMountedRef: MutableRefObject<boolean>,
145
+ setAppInfo: (info: AppInfo | null) => void,
146
+ setError: (error: string | null) => void,
147
+ setLoading: (loading: boolean) => void
148
+ ): Promise<void> => {
149
+ if (!isMountedRef.current) {
150
+ return;
151
+ }
152
+
153
+ setLoading(true);
154
+ setError(null);
155
+
156
+ try {
157
+ const refreshedInfo = await repository.getAppInfo();
158
+
159
+ if (isMountedRef.current) {
160
+ setAppInfo(refreshedInfo);
161
+ }
162
+ } catch (err) {
163
+ setError(err instanceof Error ? err.message : 'Unknown error');
164
+ } finally {
165
+ setLoading(false);
166
+ }
167
+ };
@@ -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
+ });
@@ -3,6 +3,8 @@
3
3
  * General purpose utilities for all applications
4
4
  */
5
5
 
6
+ export { createDefaultAppInfo } from './AppInfoFactory';
7
+
6
8
  /**
7
9
  * Create default configuration with overrides
8
10
  */
@@ -6,3 +6,4 @@
6
6
  export * from './presentation/screens/LegalScreen';
7
7
  export * from './presentation/screens/PrivacyPolicyScreen';
8
8
  export * from './presentation/screens/TermsOfServiceScreen';
9
+ export { LegalContentScreen } from './presentation/screens/LegalContentScreen';
@@ -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
+ });
@@ -2,46 +2,17 @@
2
2
  * Privacy Policy Screen Component
3
3
  * Display Privacy Policy content
4
4
  */
5
-
6
5
  import React from "react";
7
- import { View, ScrollView, StyleSheet } from "react-native";
8
- import { useSafeAreaInsets } from "react-native-safe-area-context";
9
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
10
- import { AtomicText, AtomicButton } from "@umituz/react-native-design-system";
11
- import { UrlHandlerService } from "../../domain/services/UrlHandlerService";
12
- import { ContentValidationService } from "../../domain/services/ContentValidationService";
13
- import { StyleCacheService } from "../../domain/services/StyleCacheService";
6
+ import { StyleSheet } from "react-native";
7
+ import { LegalContentScreen } from "./LegalContentScreen";
14
8
 
15
9
  export interface PrivacyPolicyScreenProps {
16
- /**
17
- * Privacy Policy content (HTML or plain text)
18
- * Either content or url must be provided
19
- */
20
10
  content?: string;
21
- /**
22
- * Privacy Policy URL (if content is not provided, will open URL)
23
- * Either content or url must be provided
24
- */
25
11
  url?: string;
26
- /**
27
- * Custom title
28
- */
29
12
  title: string;
30
- /**
31
- * Text for viewing online button (required when url is provided)
32
- */
33
13
  viewOnlineText?: string;
34
- /**
35
- * Text for open button (required when url is provided)
36
- */
37
14
  openText?: string;
38
- /**
39
- * Callback when URL is pressed (if content is not provided)
40
- */
41
15
  onUrlPress?: () => void;
42
- /**
43
- * Test ID for E2E testing
44
- */
45
16
  testID?: string;
46
17
  }
47
18
 
@@ -54,119 +25,7 @@ export const PrivacyPolicyScreen: React.FC<PrivacyPolicyScreenProps> = React.mem
54
25
  onUrlPress,
55
26
  testID = "privacy-policy-screen",
56
27
  }) => {
57
- const tokens = useAppDesignTokens();
58
- const insets = useSafeAreaInsets();
59
-
60
- // Validate required props
61
- React.useEffect(() => {
62
- ContentValidationService.validateScreenContent(
63
- content,
64
- url,
65
- title,
66
- viewOnlineText,
67
- openText,
68
- 'PrivacyPolicyScreen'
69
- );
70
- }, [content, url, title, viewOnlineText, openText]);
71
-
72
- // Use cached styles
73
- const styles = React.useMemo(() => {
74
- const cacheKey = StyleCacheService.createTokenCacheKey(tokens);
75
- return StyleCacheService.getCachedStyles(
76
- 'PrivacyPolicyScreen',
77
- cacheKey,
78
- () => createPrivacyPolicyStyles(tokens)
79
- );
80
- }, [tokens]);
81
-
82
- // Memoize URL press handler to prevent child re-renders
83
- const handleUrlPress = React.useCallback(async () => {
84
- if (onUrlPress) {
85
- onUrlPress();
86
- } else if (url) {
87
- try {
88
- await UrlHandlerService.openUrl(url);
89
- } catch {
90
- // Silent error handling
91
- }
92
- }
93
- }, [onUrlPress, url]);
94
-
95
- // Memoize container style to prevent object creation
96
- const containerStyle = React.useMemo(() => [
97
- styles.container,
98
- {
99
- backgroundColor: tokens.colors.backgroundPrimary,
100
- paddingTop: insets.top,
101
- },
102
- ], [styles.container, tokens.colors.backgroundPrimary, insets.top]);
103
-
104
- // Memoize conditional rendering
105
- const showContent = React.useMemo(() => !!(content), [content]);
106
- const showUrlSection = React.useMemo(() =>
107
- ContentValidationService.shouldShowUrlSection(url, onUrlPress),
108
- [url, onUrlPress]
109
- );
110
-
111
- // Memoize content section
112
- const contentSection = React.useMemo(() => {
113
- if (showContent) {
114
- return (
115
- <AtomicText type="bodyMedium" color="onSurface" style={styles.text}>
116
- {content}
117
- </AtomicText>
118
- );
119
- }
120
-
121
- if (showUrlSection) {
122
- return (
123
- <View style={styles.urlContainer}>
124
- <AtomicText
125
- type="bodyMedium"
126
- color="secondary"
127
- style={styles.urlText}
128
- >
129
- {viewOnlineText}
130
- </AtomicText>
131
- <AtomicButton
132
- variant="primary"
133
- onPress={handleUrlPress}
134
- fullWidth
135
- style={styles.urlButton}
136
- >
137
- {openText}
138
- </AtomicButton>
139
- </View>
140
- );
141
- }
142
-
143
- return null;
144
- }, [showContent, showUrlSection, styles.text, styles.urlContainer, styles.urlText, styles.urlButton, content, viewOnlineText, openText, handleUrlPress]);
145
-
146
- return (
147
- <View style={containerStyle} testID={testID}>
148
- <ScrollView
149
- contentContainerStyle={styles.scrollContent}
150
- showsVerticalScrollIndicator={false}
151
- >
152
- <View style={styles.content}>
153
- <AtomicText
154
- type="headlineLarge"
155
- color="primary"
156
- style={styles.title}
157
- >
158
- {title}
159
- </AtomicText>
160
-
161
- {contentSection}
162
- </View>
163
- </ScrollView>
164
- </View>
165
- );
166
- });
167
-
168
- const createPrivacyPolicyStyles = (tokens: any) => {
169
- return StyleSheet.create({
28
+ const createStyles = (tokens: any) => StyleSheet.create({
170
29
  container: {
171
30
  flex: 1,
172
31
  },
@@ -197,15 +56,18 @@ const createPrivacyPolicyStyles = (tokens: any) => {
197
56
  marginTop: 8,
198
57
  },
199
58
  });
200
- };
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
59
 
60
+ return (
61
+ <LegalContentScreen
62
+ content={content}
63
+ url={url}
64
+ title={title}
65
+ viewOnlineText={viewOnlineText}
66
+ openText={openText}
67
+ onUrlPress={onUrlPress}
68
+ testID={testID}
69
+ styleCacheKey="PrivacyPolicyScreen"
70
+ createStyles={createStyles}
71
+ />
72
+ );
73
+ });