@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,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for managing About information
|
|
3
|
+
* Provides reactive state management for About data
|
|
4
|
+
* Optimized for performance and memory safety
|
|
5
|
+
*/
|
|
6
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
7
|
+
import { AppInfo, AboutConfig } from '../../domain/entities/AppInfo';
|
|
8
|
+
import { AboutRepository } from '../../infrastructure/repositories/AboutRepository';
|
|
9
|
+
|
|
10
|
+
export interface UseAboutInfoOptions {
|
|
11
|
+
/** Initial configuration */
|
|
12
|
+
initialConfig?: AboutConfig;
|
|
13
|
+
/** Auto-initialize on mount */
|
|
14
|
+
autoInit?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseAboutInfoReturn {
|
|
18
|
+
/** Current app info */
|
|
19
|
+
appInfo: AppInfo | null;
|
|
20
|
+
/** Loading state */
|
|
21
|
+
loading: boolean;
|
|
22
|
+
/** Error state */
|
|
23
|
+
error: string | null;
|
|
24
|
+
/** Initialize with config */
|
|
25
|
+
initialize: (config: AboutConfig) => Promise<void>;
|
|
26
|
+
/** Update with new config */
|
|
27
|
+
update: (config: AboutConfig) => Promise<void>;
|
|
28
|
+
/** Update app info */
|
|
29
|
+
updateAppInfo: (updates: Partial<AppInfo>) => Promise<void>;
|
|
30
|
+
/** Refresh current app info */
|
|
31
|
+
refresh: () => Promise<void>;
|
|
32
|
+
/** Reset to initial state */
|
|
33
|
+
reset: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useAboutInfo = (
|
|
37
|
+
options: UseAboutInfoOptions = {}
|
|
38
|
+
): UseAboutInfoReturn => {
|
|
39
|
+
const { initialConfig, autoInit } = options;
|
|
40
|
+
const [repository] = useState(() => new AboutRepository());
|
|
41
|
+
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
|
42
|
+
const [loading, setLoading] = useState(false);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
// Prevent infinite loops and memory leaks
|
|
46
|
+
const isInitializedRef = useRef(false);
|
|
47
|
+
const isMountedRef = useRef(true);
|
|
48
|
+
|
|
49
|
+
const initialize = useCallback(async (config: AboutConfig, force = false) => {
|
|
50
|
+
// Prevent multiple initializations unless forced
|
|
51
|
+
if (isInitializedRef.current && !force) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if component is still mounted
|
|
56
|
+
if (!isMountedRef.current) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const defaultAppInfo: AppInfo = {
|
|
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
|
+
|
|
75
|
+
await repository.saveAppInfo(defaultAppInfo);
|
|
76
|
+
|
|
77
|
+
// Only update state if component is still mounted
|
|
78
|
+
if (isMountedRef.current) {
|
|
79
|
+
setAppInfo(defaultAppInfo);
|
|
80
|
+
isInitializedRef.current = true;
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (!isMountedRef.current) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
88
|
+
setError(errorMessage);
|
|
89
|
+
} finally {
|
|
90
|
+
// Only update loading state if component is still mounted
|
|
91
|
+
if (isMountedRef.current) {
|
|
92
|
+
setLoading(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, [repository]);
|
|
96
|
+
|
|
97
|
+
const update = useCallback(async (config: AboutConfig) => {
|
|
98
|
+
if (!isMountedRef.current) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setLoading(true);
|
|
103
|
+
setError(null);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const updatedAppInfo: AppInfo = {
|
|
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
|
+
|
|
117
|
+
await repository.saveAppInfo(updatedAppInfo);
|
|
118
|
+
|
|
119
|
+
// Only update state if component is still mounted
|
|
120
|
+
if (isMountedRef.current) {
|
|
121
|
+
setAppInfo(updatedAppInfo);
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (!isMountedRef.current) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
129
|
+
setError(errorMessage);
|
|
130
|
+
} finally {
|
|
131
|
+
// Only update loading state if component is still mounted
|
|
132
|
+
if (isMountedRef.current) {
|
|
133
|
+
setLoading(false);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}, [repository]);
|
|
137
|
+
|
|
138
|
+
const updateAppInfo = useCallback(async (updates: Partial<AppInfo>) => {
|
|
139
|
+
if (!appInfo || !isMountedRef.current) {
|
|
140
|
+
if (isMountedRef.current) {
|
|
141
|
+
setError('App info not initialized');
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setLoading(true);
|
|
147
|
+
setError(null);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const updatedInfo = await repository.updateAppInfo(updates);
|
|
151
|
+
|
|
152
|
+
// Only update state if component is still mounted
|
|
153
|
+
if (isMountedRef.current) {
|
|
154
|
+
setAppInfo(updatedInfo);
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (!isMountedRef.current) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
162
|
+
setError(errorMessage);
|
|
163
|
+
} finally {
|
|
164
|
+
// Only update loading state if component is still mounted
|
|
165
|
+
if (isMountedRef.current) {
|
|
166
|
+
setLoading(false);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}, [repository, appInfo]);
|
|
170
|
+
|
|
171
|
+
const refresh = useCallback(async () => {
|
|
172
|
+
if (!isMountedRef.current || !appInfo) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setLoading(true);
|
|
177
|
+
setError(null);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const refreshedInfo = await repository.getAppInfo();
|
|
181
|
+
|
|
182
|
+
// Only update state if component is still mounted
|
|
183
|
+
if (isMountedRef.current) {
|
|
184
|
+
setAppInfo(refreshedInfo);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (!isMountedRef.current) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
192
|
+
setError(errorMessage);
|
|
193
|
+
} finally {
|
|
194
|
+
// Only update loading state if component is still mounted
|
|
195
|
+
if (isMountedRef.current) {
|
|
196
|
+
setLoading(false);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}, [repository, appInfo]);
|
|
200
|
+
|
|
201
|
+
const reset = useCallback(() => {
|
|
202
|
+
if (!isMountedRef.current) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setAppInfo(null);
|
|
207
|
+
setError(null);
|
|
208
|
+
setLoading(false);
|
|
209
|
+
isInitializedRef.current = false;
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
// Cleanup on unmount to prevent memory leaks
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
return () => {
|
|
215
|
+
isMountedRef.current = false;
|
|
216
|
+
|
|
217
|
+
// Cleanup repository if it has destroy method
|
|
218
|
+
if (repository && typeof repository.destroy === 'function') {
|
|
219
|
+
repository.destroy();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}, [repository]);
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
// Set initial config when provided (if autoInit is not explicitly false)
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (initialConfig && autoInit !== false && isMountedRef.current && !isInitializedRef.current) {
|
|
229
|
+
const defaultAppInfo: AppInfo = {
|
|
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
|
+
|
|
240
|
+
setAppInfo(defaultAppInfo);
|
|
241
|
+
isInitializedRef.current = true;
|
|
242
|
+
}
|
|
243
|
+
}, [initialConfig, autoInit]);
|
|
244
|
+
|
|
245
|
+
// Auto-initialize with dependency optimization
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (autoInit === true && initialConfig && isMountedRef.current) {
|
|
248
|
+
initialize(initialConfig, true);
|
|
249
|
+
}
|
|
250
|
+
}, [autoInit, initialConfig, initialize]);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
appInfo,
|
|
254
|
+
loading,
|
|
255
|
+
error,
|
|
256
|
+
initialize,
|
|
257
|
+
update,
|
|
258
|
+
updateAppInfo,
|
|
259
|
+
refresh,
|
|
260
|
+
reset,
|
|
261
|
+
};
|
|
262
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* About Screen Component
|
|
3
|
+
* Main screen component for displaying app information
|
|
4
|
+
* Fully configurable and generic
|
|
5
|
+
* Optimized for performance and memory safety
|
|
6
|
+
*/
|
|
7
|
+
import React, { useMemo, useCallback } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
ScrollView,
|
|
12
|
+
ViewStyle,
|
|
13
|
+
TextStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import { AboutHeader } from '../components/AboutHeader';
|
|
16
|
+
import { AboutContent } from '../components/AboutContent';
|
|
17
|
+
import { AboutConfig } from '../../domain/entities/AppInfo';
|
|
18
|
+
|
|
19
|
+
export interface AboutScreenProps {
|
|
20
|
+
/** Configuration for the about screen */
|
|
21
|
+
config: AboutConfig;
|
|
22
|
+
/** Custom container style */
|
|
23
|
+
containerStyle?: ViewStyle;
|
|
24
|
+
/** Custom header style */
|
|
25
|
+
headerStyle?: ViewStyle;
|
|
26
|
+
/** Custom title style */
|
|
27
|
+
titleStyle?: TextStyle;
|
|
28
|
+
/** Custom version style */
|
|
29
|
+
versionStyle?: TextStyle;
|
|
30
|
+
/** Show app header with name and version */
|
|
31
|
+
showHeader?: boolean;
|
|
32
|
+
/** Custom header component */
|
|
33
|
+
headerComponent?: React.ReactNode;
|
|
34
|
+
/** Custom footer component */
|
|
35
|
+
footerComponent?: React.ReactNode;
|
|
36
|
+
/** Test ID for E2E testing */
|
|
37
|
+
testID?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
import { useAboutInfo } from '../hooks/useAboutInfo';
|
|
41
|
+
import { useResponsiveDesignTokens, AtomicText } from '@umituz/react-native-design-system';
|
|
42
|
+
|
|
43
|
+
export const AboutScreen: React.FC<AboutScreenProps> = ({
|
|
44
|
+
config,
|
|
45
|
+
containerStyle,
|
|
46
|
+
headerStyle,
|
|
47
|
+
titleStyle,
|
|
48
|
+
versionStyle,
|
|
49
|
+
showHeader = true,
|
|
50
|
+
headerComponent,
|
|
51
|
+
footerComponent,
|
|
52
|
+
testID,
|
|
53
|
+
}) => {
|
|
54
|
+
const tokens = useResponsiveDesignTokens();
|
|
55
|
+
const colors = tokens.colors;
|
|
56
|
+
|
|
57
|
+
const { appInfo, loading, error } = useAboutInfo({
|
|
58
|
+
autoInit: true,
|
|
59
|
+
initialConfig: config,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Memoize header rendering to prevent unnecessary re-renders
|
|
63
|
+
const renderHeader = useCallback(() => {
|
|
64
|
+
if (headerComponent) {
|
|
65
|
+
return headerComponent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!showHeader || !appInfo) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<AboutHeader
|
|
74
|
+
appInfo={appInfo}
|
|
75
|
+
containerStyle={headerStyle}
|
|
76
|
+
titleStyle={titleStyle}
|
|
77
|
+
versionStyle={versionStyle}
|
|
78
|
+
versionPrefix={config.texts?.versionPrefix}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}, [headerComponent, showHeader, appInfo, headerStyle, titleStyle, versionStyle, config.texts?.versionPrefix]);
|
|
82
|
+
|
|
83
|
+
// Memoize footer rendering
|
|
84
|
+
const renderFooter = useCallback(() => {
|
|
85
|
+
if (!footerComponent) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
|
91
|
+
{footerComponent}
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
}, [footerComponent, colors.border]);
|
|
95
|
+
|
|
96
|
+
// Memoize content rendering
|
|
97
|
+
const renderContent = useCallback(() => {
|
|
98
|
+
if (!appInfo) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<AboutContent
|
|
104
|
+
appInfo={appInfo}
|
|
105
|
+
config={config}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}, [appInfo, config]);
|
|
109
|
+
|
|
110
|
+
// Memoize container style to prevent unnecessary re-renders
|
|
111
|
+
const containerStyles = useMemo(() => {
|
|
112
|
+
return [
|
|
113
|
+
styles.container,
|
|
114
|
+
{ backgroundColor: colors.backgroundPrimary },
|
|
115
|
+
containerStyle
|
|
116
|
+
];
|
|
117
|
+
}, [containerStyle, colors.backgroundPrimary]);
|
|
118
|
+
|
|
119
|
+
const texts = config.texts || {};
|
|
120
|
+
|
|
121
|
+
if (loading) {
|
|
122
|
+
return (
|
|
123
|
+
<View style={containerStyles} testID={testID}>
|
|
124
|
+
<AtomicText
|
|
125
|
+
type="bodyMedium"
|
|
126
|
+
color="textSecondary"
|
|
127
|
+
style={styles.loadingText}
|
|
128
|
+
>
|
|
129
|
+
{texts.loading}
|
|
130
|
+
</AtomicText>
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (error) {
|
|
136
|
+
return (
|
|
137
|
+
<View style={containerStyles} testID={testID}>
|
|
138
|
+
<AtomicText
|
|
139
|
+
type="bodyMedium"
|
|
140
|
+
color="error"
|
|
141
|
+
style={styles.errorText}
|
|
142
|
+
>
|
|
143
|
+
{texts.errorPrefix} {error}
|
|
144
|
+
</AtomicText>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!appInfo) {
|
|
150
|
+
return (
|
|
151
|
+
<View style={containerStyles} testID={testID}>
|
|
152
|
+
<AtomicText
|
|
153
|
+
type="bodyMedium"
|
|
154
|
+
color="textSecondary"
|
|
155
|
+
style={styles.errorText}
|
|
156
|
+
>
|
|
157
|
+
{texts.noInfo}
|
|
158
|
+
</AtomicText>
|
|
159
|
+
</View>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<ScrollView
|
|
165
|
+
style={containerStyles}
|
|
166
|
+
testID={testID}
|
|
167
|
+
contentContainerStyle={{ paddingBottom: 32 }}
|
|
168
|
+
>
|
|
169
|
+
{renderHeader()}
|
|
170
|
+
{renderContent()}
|
|
171
|
+
{renderFooter()}
|
|
172
|
+
</ScrollView>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const styles = StyleSheet.create({
|
|
177
|
+
container: {
|
|
178
|
+
flex: 1,
|
|
179
|
+
},
|
|
180
|
+
footer: {
|
|
181
|
+
paddingVertical: 16,
|
|
182
|
+
paddingHorizontal: 16,
|
|
183
|
+
borderTopWidth: 1,
|
|
184
|
+
},
|
|
185
|
+
loadingText: {
|
|
186
|
+
textAlign: 'center',
|
|
187
|
+
fontSize: 16,
|
|
188
|
+
marginTop: 20,
|
|
189
|
+
},
|
|
190
|
+
errorText: {
|
|
191
|
+
textAlign: 'center',
|
|
192
|
+
fontSize: 16,
|
|
193
|
+
marginTop: 20,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple test for AboutScreen component
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { AboutScreen } from '../AboutScreen';
|
|
6
|
+
import { AppInfo, AboutConfig } from '../../domain/entities/AppInfo';
|
|
7
|
+
|
|
8
|
+
describe('AboutScreen', () => {
|
|
9
|
+
const mockAppInfo: AppInfo = {
|
|
10
|
+
name: 'Test App',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
description: 'Test Description',
|
|
13
|
+
developer: 'Test Developer',
|
|
14
|
+
contactEmail: 'test@example.com',
|
|
15
|
+
websiteUrl: 'https://example.com',
|
|
16
|
+
websiteDisplay: 'example.com',
|
|
17
|
+
moreAppsUrl: 'https://apps.example.com',
|
|
18
|
+
privacyPolicyUrl: 'https://example.com/privacy',
|
|
19
|
+
termsOfServiceUrl: 'https://example.com/terms',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const mockConfig: AboutConfig = {
|
|
23
|
+
appInfo: mockAppInfo,
|
|
24
|
+
actions: {
|
|
25
|
+
onEmailPress: jest.fn(),
|
|
26
|
+
onWebsitePress: jest.fn(),
|
|
27
|
+
onPrivacyPress: jest.fn(),
|
|
28
|
+
onTermsPress: jest.fn(),
|
|
29
|
+
onMoreAppsPress: jest.fn(),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
it('should render without crashing', () => {
|
|
34
|
+
const element = React.createElement(AboutScreen, { config: mockConfig });
|
|
35
|
+
expect(element).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should render app info after loading', () => {
|
|
39
|
+
const element = React.createElement(AboutScreen, { config: mockConfig });
|
|
40
|
+
expect(element).toBeTruthy();
|
|
41
|
+
expect(mockConfig.appInfo.name).toBe('Test App');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should render error state when initialization fails', () => {
|
|
45
|
+
const invalidConfig = null as unknown;
|
|
46
|
+
|
|
47
|
+
const element = React.createElement(AboutScreen, { config: invalidConfig });
|
|
48
|
+
expect(element).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should render no app info message when app info is null', () => {
|
|
52
|
+
const emptyConfig: AboutConfig = {
|
|
53
|
+
appInfo: null as unknown,
|
|
54
|
+
actions: {},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const element = React.createElement(AboutScreen, { config: emptyConfig });
|
|
58
|
+
expect(element).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should not render header when showHeader is false', () => {
|
|
62
|
+
const element = React.createElement(AboutScreen, { config: mockConfig, showHeader: false });
|
|
63
|
+
expect(element).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should render custom header component when provided', () => {
|
|
67
|
+
const CustomHeader = () => React.createElement('div', { testID: 'custom-header' }, 'Custom Header');
|
|
68
|
+
|
|
69
|
+
const element = React.createElement(AboutScreen, {
|
|
70
|
+
config: mockConfig,
|
|
71
|
+
headerComponent: React.createElement(CustomHeader)
|
|
72
|
+
});
|
|
73
|
+
expect(element).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should render custom footer component when provided', () => {
|
|
77
|
+
const CustomFooter = () => React.createElement('div', { testID: 'custom-footer' }, 'Custom Footer');
|
|
78
|
+
|
|
79
|
+
const element = React.createElement(AboutScreen, {
|
|
80
|
+
config: mockConfig,
|
|
81
|
+
footerComponent: React.createElement(CustomFooter)
|
|
82
|
+
});
|
|
83
|
+
expect(element).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should apply custom container style', () => {
|
|
87
|
+
const customStyle = { backgroundColor: 'red' };
|
|
88
|
+
|
|
89
|
+
const element = React.createElement(AboutScreen, {
|
|
90
|
+
config: mockConfig,
|
|
91
|
+
containerStyle: customStyle,
|
|
92
|
+
testID: 'screen'
|
|
93
|
+
});
|
|
94
|
+
expect(element).toBeTruthy();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should apply custom header style', () => {
|
|
98
|
+
const customStyle = { backgroundColor: 'blue' };
|
|
99
|
+
|
|
100
|
+
const element = React.createElement(AboutScreen, {
|
|
101
|
+
config: mockConfig,
|
|
102
|
+
headerStyle: customStyle
|
|
103
|
+
});
|
|
104
|
+
expect(element).toBeTruthy();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should apply custom title style', () => {
|
|
108
|
+
const customStyle = { color: 'green' } as React.CSSProperties;
|
|
109
|
+
|
|
110
|
+
const element = React.createElement(AboutScreen, {
|
|
111
|
+
config: mockConfig,
|
|
112
|
+
titleStyle: customStyle
|
|
113
|
+
});
|
|
114
|
+
expect(element).toBeTruthy();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should apply custom version style', () => {
|
|
118
|
+
const customStyle = { color: 'purple' };
|
|
119
|
+
|
|
120
|
+
const element = React.createElement(AboutScreen, {
|
|
121
|
+
config: mockConfig,
|
|
122
|
+
versionStyle: customStyle
|
|
123
|
+
});
|
|
124
|
+
expect(element).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle empty config', () => {
|
|
128
|
+
const emptyConfig: AboutConfig = {
|
|
129
|
+
appInfo: {
|
|
130
|
+
name: '',
|
|
131
|
+
version: '',
|
|
132
|
+
description: '',
|
|
133
|
+
developer: '',
|
|
134
|
+
contactEmail: '',
|
|
135
|
+
websiteUrl: '',
|
|
136
|
+
websiteDisplay: '',
|
|
137
|
+
moreAppsUrl: '',
|
|
138
|
+
privacyPolicyUrl: '',
|
|
139
|
+
termsOfServiceUrl: '',
|
|
140
|
+
},
|
|
141
|
+
actions: {},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const element = React.createElement(AboutScreen, { config: emptyConfig });
|
|
145
|
+
expect(element).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle config with only required fields', () => {
|
|
149
|
+
const minimalConfig: AboutConfig = {
|
|
150
|
+
appInfo: {
|
|
151
|
+
name: 'Minimal App',
|
|
152
|
+
version: '1.0.0',
|
|
153
|
+
description: '',
|
|
154
|
+
developer: '',
|
|
155
|
+
contactEmail: '',
|
|
156
|
+
websiteUrl: '',
|
|
157
|
+
websiteDisplay: '',
|
|
158
|
+
moreAppsUrl: '',
|
|
159
|
+
privacyPolicyUrl: '',
|
|
160
|
+
termsOfServiceUrl: '',
|
|
161
|
+
},
|
|
162
|
+
actions: {},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const element = React.createElement(AboutScreen, { config: minimalConfig });
|
|
166
|
+
expect(element).toBeTruthy();
|
|
167
|
+
expect(minimalConfig.appInfo.name).toBe('Minimal App');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should handle special characters in text', () => {
|
|
171
|
+
const configWithSpecialChars: AboutConfig = {
|
|
172
|
+
appInfo: {
|
|
173
|
+
...mockAppInfo,
|
|
174
|
+
name: 'App & Special <Chars>',
|
|
175
|
+
description: 'Description with "quotes" and \'apostrophes\'',
|
|
176
|
+
},
|
|
177
|
+
actions: {},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const element = React.createElement(AboutScreen, { config: configWithSpecialChars });
|
|
181
|
+
expect(element).toBeTruthy();
|
|
182
|
+
expect(configWithSpecialChars.appInfo.name).toBe('App & Special <Chars>');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle very long text', () => {
|
|
186
|
+
const configWithLongText: AboutConfig = {
|
|
187
|
+
appInfo: {
|
|
188
|
+
...mockAppInfo,
|
|
189
|
+
name: 'A'.repeat(100),
|
|
190
|
+
description: 'B'.repeat(500),
|
|
191
|
+
},
|
|
192
|
+
actions: {},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const element = React.createElement(AboutScreen, { config: configWithLongText });
|
|
196
|
+
expect(element).toBeTruthy();
|
|
197
|
+
expect(configWithLongText.appInfo.name.length).toBe(100);
|
|
198
|
+
});
|
|
199
|
+
});
|