@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.
Files changed (103) hide show
  1. package/package.json +16 -15
  2. package/src/domains/about/__tests__/integration.test.tsx +328 -0
  3. package/src/domains/about/__tests__/types.d.ts +5 -0
  4. package/src/domains/about/domain/entities/AppInfo.ts +74 -0
  5. package/src/domains/about/domain/entities/__tests__/AppInfo.test.ts +93 -0
  6. package/src/domains/about/domain/repositories/IAboutRepository.ts +22 -0
  7. package/src/domains/about/index.ts +10 -0
  8. package/src/domains/about/infrastructure/repositories/AboutRepository.ts +68 -0
  9. package/src/domains/about/infrastructure/repositories/__tests__/AboutRepository.test.ts +153 -0
  10. package/src/domains/about/presentation/components/AboutContent.tsx +104 -0
  11. package/src/domains/about/presentation/components/AboutHeader.tsx +79 -0
  12. package/src/domains/about/presentation/components/AboutSection.tsx +134 -0
  13. package/src/domains/about/presentation/components/AboutSettingItem.tsx +208 -0
  14. package/src/domains/about/presentation/components/__tests__/AboutContent.simple.test.tsx +178 -0
  15. package/src/domains/about/presentation/components/__tests__/AboutContent.test.tsx +293 -0
  16. package/src/domains/about/presentation/components/__tests__/AboutHeader.test.tsx +201 -0
  17. package/src/domains/about/presentation/components/__tests__/AboutSettingItem.test.tsx +71 -0
  18. package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.simple.test.tsx +229 -0
  19. package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.test.tsx +240 -0
  20. package/src/domains/about/presentation/hooks/useAboutInfo.ts +262 -0
  21. package/src/domains/about/presentation/screens/AboutScreen.tsx +195 -0
  22. package/src/domains/about/presentation/screens/__tests__/AboutScreen.simple.test.tsx +199 -0
  23. package/src/domains/about/presentation/screens/__tests__/AboutScreen.test.tsx +366 -0
  24. package/src/domains/about/types/global.d.ts +15 -0
  25. package/src/domains/about/utils/__tests__/index.test.ts +408 -0
  26. package/src/domains/about/utils/index.ts +160 -0
  27. package/src/domains/appearance/__tests__/components/AppearanceScreen.test.tsx +195 -0
  28. package/src/domains/appearance/__tests__/hooks/index.test.tsx +232 -0
  29. package/src/domains/appearance/__tests__/integration/index.test.tsx +207 -0
  30. package/src/domains/appearance/__tests__/services/appearanceService.test.ts +299 -0
  31. package/src/domains/appearance/__tests__/setup.ts +96 -0
  32. package/src/domains/appearance/__tests__/stores/appearanceStore.test.tsx +175 -0
  33. package/src/domains/appearance/data/colorPalettes.ts +94 -0
  34. package/src/domains/appearance/hooks/index.ts +6 -0
  35. package/src/domains/appearance/hooks/useAppearance.ts +61 -0
  36. package/src/domains/appearance/hooks/useAppearanceActions.ts +144 -0
  37. package/src/domains/appearance/index.ts +7 -0
  38. package/src/domains/appearance/infrastructure/services/appearanceService.ts +301 -0
  39. package/src/domains/appearance/infrastructure/services/systemThemeDetection.ts +79 -0
  40. package/src/domains/appearance/infrastructure/services/validation.ts +91 -0
  41. package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +120 -0
  42. package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +132 -0
  43. package/src/domains/appearance/presentation/components/AppearanceHeader.tsx +67 -0
  44. package/src/domains/appearance/presentation/components/AppearancePreview.tsx +141 -0
  45. package/src/domains/appearance/presentation/components/AppearanceSection.tsx +139 -0
  46. package/src/domains/appearance/presentation/components/ColorPicker.tsx +113 -0
  47. package/src/domains/appearance/presentation/components/CustomColorsSection.tsx +186 -0
  48. package/src/domains/appearance/presentation/components/ThemeModeSection.tsx +110 -0
  49. package/src/domains/appearance/presentation/components/ThemeOption.tsx +138 -0
  50. package/src/domains/appearance/presentation/components/index.ts +6 -0
  51. package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +226 -0
  52. package/src/domains/appearance/presentation/screens/index.ts +2 -0
  53. package/src/domains/appearance/types/index.ts +54 -0
  54. package/src/domains/faqs/domain/entities/FAQEntity.ts +16 -0
  55. package/src/domains/faqs/domain/services/FAQSearchService.ts +36 -0
  56. package/src/domains/faqs/domain/services/index.ts +1 -0
  57. package/src/domains/faqs/index.ts +7 -0
  58. package/src/domains/faqs/presentation/components/FAQCategory.tsx +71 -0
  59. package/src/domains/faqs/presentation/components/FAQEmptyState.tsx +75 -0
  60. package/src/domains/faqs/presentation/components/FAQItem.tsx +103 -0
  61. package/src/domains/faqs/presentation/components/FAQSearchBar.tsx +70 -0
  62. package/src/domains/faqs/presentation/components/FAQSection.tsx +50 -0
  63. package/src/domains/faqs/presentation/components/index.ts +18 -0
  64. package/src/domains/faqs/presentation/hooks/index.ts +6 -0
  65. package/src/domains/faqs/presentation/hooks/useFAQExpansion.ts +51 -0
  66. package/src/domains/faqs/presentation/hooks/useFAQSearch.ts +33 -0
  67. package/src/domains/faqs/presentation/screens/FAQScreen.tsx +129 -0
  68. package/src/domains/faqs/presentation/screens/index.ts +2 -0
  69. package/src/domains/feedback/domain/entities/FeedbackEntity.ts +92 -0
  70. package/src/domains/feedback/domain/repositories/IFeedbackRepository.ts +28 -0
  71. package/src/domains/feedback/index.ts +6 -0
  72. package/src/domains/feedback/presentation/components/FeedbackForm.tsx +189 -0
  73. package/src/domains/feedback/presentation/components/FeedbackModal.tsx +111 -0
  74. package/src/domains/feedback/presentation/components/SupportSection.tsx +160 -0
  75. package/src/domains/feedback/presentation/hooks/useDeleteFeedback.ts +25 -0
  76. package/src/domains/feedback/presentation/hooks/useFeedbackForm.ts +59 -0
  77. package/src/domains/feedback/presentation/hooks/useSubmitFeedback.ts +55 -0
  78. package/src/domains/feedback/presentation/hooks/useUserFeedback.ts +29 -0
  79. package/src/domains/legal/__tests__/ContentValidationService.test.ts +195 -0
  80. package/src/domains/legal/__tests__/StyleCacheService.test.ts +110 -0
  81. package/src/domains/legal/__tests__/UrlHandlerService.test.ts +71 -0
  82. package/src/domains/legal/__tests__/setup.ts +82 -0
  83. package/src/domains/legal/domain/entities/LegalConfig.ts +26 -0
  84. package/src/domains/legal/domain/services/ContentValidationService.ts +89 -0
  85. package/src/domains/legal/domain/services/StyleCacheService.ts +97 -0
  86. package/src/domains/legal/domain/services/UrlHandlerService.ts +128 -0
  87. package/src/domains/legal/index.ts +8 -0
  88. package/src/domains/legal/presentation/components/LegalItem.tsx +177 -0
  89. package/src/domains/legal/presentation/components/LegalLinks.tsx +154 -0
  90. package/src/domains/legal/presentation/components/LegalSection.tsx +134 -0
  91. package/src/domains/legal/presentation/screens/LegalScreen.tsx +237 -0
  92. package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +214 -0
  93. package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +214 -0
  94. package/src/index.ts +19 -0
  95. package/src/presentation/components/DevSettingsSection.tsx +2 -2
  96. package/src/presentation/components/SettingItem.tsx +2 -2
  97. package/src/presentation/components/SettingsErrorBoundary.tsx +2 -2
  98. package/src/presentation/components/SettingsFooter.tsx +2 -2
  99. package/src/presentation/components/SettingsSection.tsx +2 -2
  100. package/src/presentation/navigation/SettingsStackNavigator.tsx +2 -2
  101. package/src/presentation/screens/SettingsScreen.tsx +2 -2
  102. package/src/presentation/screens/components/SettingsContent.tsx +2 -2
  103. package/src/presentation/screens/components/SettingsHeader.tsx +2 -2
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tests for UrlHandlerService
3
+ */
4
+
5
+ import { UrlHandlerService } from '../domain/services/UrlHandlerService';
6
+
7
+ describe('UrlHandlerService', () => {
8
+ beforeEach(() => {
9
+ UrlHandlerService.clearCache();
10
+ });
11
+
12
+ describe('isValidUrl', () => {
13
+ it('should return true for valid HTTP URLs', () => {
14
+ expect(UrlHandlerService['isValidUrl']('http://example.com')).toBe(true);
15
+ expect(UrlHandlerService['isValidUrl']('https://example.com')).toBe(true);
16
+ });
17
+
18
+ it('should return true for valid mailto URLs', () => {
19
+ expect(UrlHandlerService['isValidUrl']('mailto:test@example.com')).toBe(true);
20
+ });
21
+
22
+ it('should return true for valid tel URLs', () => {
23
+ expect(UrlHandlerService['isValidUrl']('tel:+1234567890')).toBe(true);
24
+ });
25
+
26
+ it('should return true for valid FTP URLs', () => {
27
+ expect(UrlHandlerService['isValidUrl']('ftp://example.com')).toBe(true);
28
+ });
29
+
30
+ it('should return false for invalid URLs', () => {
31
+ expect(UrlHandlerService['isValidUrl']('')).toBe(false);
32
+ expect(UrlHandlerService['isValidUrl']('invalid-url')).toBe(false);
33
+ expect(UrlHandlerService['isValidUrl']('www.example.com')).toBe(false);
34
+ expect(UrlHandlerService['isValidUrl'](undefined as any)).toBe(false);
35
+ expect(UrlHandlerService['isValidUrl'](null as any)).toBe(false);
36
+ expect(UrlHandlerService['isValidUrl'](123 as any)).toBe(false);
37
+ });
38
+ });
39
+
40
+ describe('canOpenUrl', () => {
41
+ it('should return false for invalid URLs', async () => {
42
+ const result = await UrlHandlerService.canOpenUrl('invalid-url');
43
+ expect(result).toBe(false);
44
+ });
45
+
46
+ it('should cache results', async () => {
47
+ const mockLinking = {
48
+ canOpenURL: jest.fn().mockResolvedValue(true)
49
+ };
50
+
51
+ // Mock the Linking module
52
+ jest.doMock('react-native', () => ({
53
+ Linking: mockLinking
54
+ }));
55
+
56
+ const url = 'https://example.com';
57
+ await UrlHandlerService.canOpenUrl(url);
58
+ await UrlHandlerService.canOpenUrl(url);
59
+
60
+ expect(mockLinking.canOpenURL).toHaveBeenCalledTimes(1);
61
+ });
62
+ });
63
+
64
+ describe('clearCache', () => {
65
+ it('should clear all caches', () => {
66
+ UrlHandlerService.clearCache();
67
+ // Test passes if no errors are thrown
68
+ expect(true).toBe(true);
69
+ });
70
+ });
71
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Jest setup file
3
+ */
4
+
5
+ // Mock React Native modules
6
+ jest.mock('react-native', () => ({
7
+ View: 'View',
8
+ Text: 'Text',
9
+ ScrollView: 'ScrollView',
10
+ TouchableOpacity: 'TouchableOpacity',
11
+ StyleSheet: {
12
+ create: jest.fn((styles) => styles),
13
+ },
14
+ Linking: {
15
+ canOpenURL: jest.fn(() => Promise.resolve(true)),
16
+ openURL: jest.fn(() => Promise.resolve()),
17
+ },
18
+ Platform: {
19
+ OS: 'ios',
20
+ select: jest.fn((obj) => obj.ios),
21
+ },
22
+ }));
23
+
24
+ // Mock design system modules (only if they exist)
25
+ try {
26
+ jest.mock('@umituz/react-native-design-system', () => ({
27
+ useAppDesignTokens: jest.fn(() => ({
28
+ colors: {
29
+ backgroundPrimary: '#ffffff',
30
+ primary: '#007AFF',
31
+ textPrimary: '#000000',
32
+ textSecondary: '#666666',
33
+ textTertiary: '#999999',
34
+ onSurface: '#000000',
35
+ secondary: '#5856D6',
36
+ info: '#007AFF',
37
+ },
38
+ spacing: {
39
+ xs: 4,
40
+ sm: 8,
41
+ md: 16,
42
+ lg: 24,
43
+ },
44
+ })),
45
+ }));
46
+ } catch (e) {
47
+ // Module not found, skip mocking
48
+ }
49
+
50
+ try {
51
+ jest.mock('@umituz/react-native-design-system', () => ({
52
+ AtomicText: 'AtomicText',
53
+ AtomicIcon: 'AtomicIcon',
54
+ AtomicButton: 'AtomicButton',
55
+ }));
56
+ } catch (e) {
57
+ // Module not found, skip mocking
58
+ }
59
+
60
+ try {
61
+ jest.mock('@umituz/react-native-design-system', () => ({
62
+ ScreenLayout: 'ScreenLayout',
63
+ }));
64
+ } catch (e) {
65
+ // Module not found, skip mocking
66
+ }
67
+
68
+ try {
69
+ jest.mock('react-native-safe-area-context', () => ({
70
+ useSafeAreaInsets: jest.fn(() => ({
71
+ top: 44,
72
+ bottom: 34,
73
+ left: 0,
74
+ right: 0,
75
+ })),
76
+ }));
77
+ } catch (e) {
78
+ // Module not found, skip mocking
79
+ }
80
+
81
+ // Global test setup
82
+ global.__DEV__ = true;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Legal Config
3
+ * Configuration for the Legal section
4
+ */
5
+ export interface LegalConfig {
6
+ /** Section title */
7
+ title?: string;
8
+ /** Section description */
9
+ description?: string;
10
+ /** Navigation route name */
11
+ route?: string;
12
+ /** Default navigation route name */
13
+ defaultRoute?: string;
14
+ /** Privacy Policy title */
15
+ privacyTitle?: string;
16
+ /** Terms of Service title */
17
+ termsTitle?: string;
18
+ /** EULA title */
19
+ eulaTitle?: string;
20
+ /** Privacy Policy URL */
21
+ privacyUrl?: string;
22
+ /** Terms of Service URL */
23
+ termsUrl?: string;
24
+ /** EULA URL */
25
+ eulaUrl?: string;
26
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Content Validation Service
3
+ * Single Responsibility: Validate content and URL requirements
4
+ * Extracted from screens to follow SOLID principles
5
+ */
6
+
7
+ export interface ContentValidationRule {
8
+ content?: string;
9
+ url?: string;
10
+ title?: string;
11
+ viewOnlineText?: string;
12
+ openText?: string;
13
+ privacyText?: string;
14
+ termsText?: string;
15
+ }
16
+
17
+ export class ContentValidationService {
18
+ /**
19
+ * Validate screen content requirements
20
+ */
21
+ static validateScreenContent(
22
+ content: string | undefined,
23
+ url: string | undefined,
24
+ title: string | undefined,
25
+ viewOnlineText: string | undefined,
26
+ openText: string | undefined,
27
+ screenName: string
28
+ ): void {
29
+ if (__DEV__) {
30
+ if (!content && !url) {
31
+ console.warn(`${screenName}: Either content or url must be provided`);
32
+ }
33
+ if (!title) {
34
+ console.warn(`${screenName}: title is required`);
35
+ }
36
+ if (url && !viewOnlineText) {
37
+ console.warn(`${screenName}: viewOnlineText is required when url is provided`);
38
+ }
39
+ if (url && !openText) {
40
+ console.warn(`${screenName}: openText is required when url is provided`);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Validate legal links requirements
47
+ */
48
+ static validateLegalLinks(
49
+ privacyPolicyUrl: string | undefined,
50
+ termsOfServiceUrl: string | undefined,
51
+ privacyText: string | undefined,
52
+ termsText: string | undefined,
53
+ onPrivacyPress: (() => void) | undefined,
54
+ onTermsPress: (() => void) | undefined
55
+ ): void {
56
+ if (__DEV__) {
57
+ if (privacyPolicyUrl && !privacyText && !onPrivacyPress) {
58
+ console.warn('LegalLinks: privacyText is required when privacyPolicyUrl is provided');
59
+ }
60
+ if (termsOfServiceUrl && !termsText && !onTermsPress) {
61
+ console.warn('LegalLinks: termsText is required when termsOfServiceUrl is provided');
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Check if content is valid
68
+ */
69
+ static hasValidContent(content?: string, url?: string): boolean {
70
+ return !!(content || url);
71
+ }
72
+
73
+ /**
74
+ * Check if URL section should be shown
75
+ */
76
+ static shouldShowUrlSection(url?: string, onUrlPress?: () => void): boolean {
77
+ return !!(url || onUrlPress);
78
+ }
79
+
80
+ /**
81
+ * Check if legal item should be shown
82
+ */
83
+ static shouldShowLegalItem(
84
+ onPress?: () => void,
85
+ title?: string
86
+ ): boolean {
87
+ return !!(onPress && title);
88
+ }
89
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Style Cache Service
3
+ * Single Responsibility: Manage style caching to prevent memory leaks
4
+ * Extracted from components to follow SOLID principles
5
+ */
6
+
7
+ import type { DesignTokens } from "@umituz/react-native-design-system";
8
+
9
+ export interface StyleCacheOptions {
10
+ maxSize?: number;
11
+ cacheKey?: string;
12
+ }
13
+
14
+ export class StyleCacheService {
15
+ private static caches = new Map<string, Map<string, any>>();
16
+ private static readonly DEFAULT_MAX_SIZE = 50;
17
+
18
+ /**
19
+ * Get or create cached styles
20
+ */
21
+ static getCachedStyles<T>(
22
+ cacheName: string,
23
+ cacheKey: string,
24
+ styleFactory: () => T,
25
+ maxSize: number = this.DEFAULT_MAX_SIZE
26
+ ): T {
27
+ let cache = this.caches.get(cacheName);
28
+
29
+ if (!cache) {
30
+ cache = new Map();
31
+ this.caches.set(cacheName, cache);
32
+ }
33
+
34
+ // Check if styles already cached
35
+ if (cache.has(cacheKey)) {
36
+ return cache.get(cacheKey);
37
+ }
38
+
39
+ // Create new styles and cache them
40
+ const styles = styleFactory();
41
+
42
+ // Limit cache size to prevent memory leaks
43
+ if (cache.size > maxSize) {
44
+ const firstKey = cache.keys().next().value;
45
+ if (firstKey) {
46
+ cache.delete(firstKey);
47
+ }
48
+ }
49
+
50
+ cache.set(cacheKey, styles);
51
+ return styles;
52
+ }
53
+
54
+ /**
55
+ * Clear specific cache
56
+ */
57
+ static clearCache(cacheName: string): void {
58
+ this.caches.delete(cacheName);
59
+ }
60
+
61
+ /**
62
+ * Clear all caches
63
+ */
64
+ static clearAllCaches(): void {
65
+ this.caches.clear();
66
+ }
67
+
68
+ /**
69
+ * Get cache size
70
+ */
71
+ static getCacheSize(cacheName: string): number {
72
+ const cache = this.caches.get(cacheName);
73
+ return cache ? cache.size : 0;
74
+ }
75
+
76
+ /**
77
+ * Create cache key from design tokens
78
+ */
79
+ static createTokenCacheKey(tokens: DesignTokens, additionalKeys?: Record<string, any>): string {
80
+ const tokenData = {
81
+ xs: tokens.spacing.xs,
82
+ sm: tokens.spacing.sm,
83
+ md: tokens.spacing.md,
84
+ lg: tokens.spacing.lg,
85
+ ...additionalKeys
86
+ };
87
+
88
+ return JSON.stringify(tokenData);
89
+ }
90
+
91
+ /**
92
+ * Create simple cache key
93
+ */
94
+ static createSimpleCacheKey(key: string): string {
95
+ return key;
96
+ }
97
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * URL Handler Service
3
+ * Single Responsibility: Handle URL operations
4
+ * Extracted from screens to follow SOLID principles
5
+ * Performance optimized with caching and error handling
6
+ */
7
+
8
+ import { Linking } from 'react-native';
9
+
10
+ // URL validation cache to avoid repeated checks
11
+ const urlValidationCache = new Map<string, boolean>();
12
+ const MAX_CACHE_SIZE = 100;
13
+
14
+ export class UrlHandlerService {
15
+ /**
16
+ * Get Linking module - using static import for stability
17
+ */
18
+ private static getLinkingModule(): any {
19
+ return Linking;
20
+ }
21
+
22
+ /**
23
+ * Clean cache to prevent memory leaks
24
+ */
25
+ private static cleanCache(): void {
26
+ if (urlValidationCache.size > MAX_CACHE_SIZE) {
27
+ const entriesToDelete = Array.from(urlValidationCache.keys()).slice(0, Math.floor(MAX_CACHE_SIZE / 2));
28
+ entriesToDelete.forEach(key => urlValidationCache.delete(key));
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Validate URL format to prevent invalid operations
34
+ */
35
+ private static isValidUrl(url: string): boolean {
36
+ if (!url || typeof url !== 'string') return false;
37
+
38
+ try {
39
+ // Basic URL validation without creating URL object to avoid memory overhead
40
+ return url.startsWith('http://') ||
41
+ url.startsWith('https://') ||
42
+ url.startsWith('mailto:') ||
43
+ url.startsWith('tel:') ||
44
+ url.startsWith('ftp://');
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Open URL in external browser with performance optimizations
52
+ */
53
+ static async openUrl(url: string): Promise<void> {
54
+ if (__DEV__) {
55
+ console.log('UrlHandlerService: Opening URL', { url });
56
+ }
57
+
58
+ if (!this.isValidUrl(url)) {
59
+ if (__DEV__) {
60
+ console.warn('UrlHandlerService: Invalid URL provided', { url });
61
+ }
62
+ return;
63
+ }
64
+
65
+ try {
66
+ const Linking = await this.getLinkingModule();
67
+
68
+ // Check cache first to avoid repeated canOpenURL calls
69
+ let canOpen = urlValidationCache.get(url);
70
+ if (canOpen === undefined) {
71
+ canOpen = await Linking.canOpenURL(url);
72
+
73
+ // Cache the result with size limit
74
+ this.cleanCache();
75
+ urlValidationCache.set(url, canOpen || false);
76
+ }
77
+
78
+ if (canOpen) {
79
+ await Linking.openURL(url);
80
+ } else {
81
+ if (__DEV__) {
82
+ console.warn('UrlHandlerService: Cannot open URL', { url });
83
+ }
84
+ }
85
+ } catch (error) {
86
+ if (__DEV__) {
87
+ console.error('UrlHandlerService: Error opening URL', { url, error });
88
+ }
89
+ // Don't throw error to prevent app crashes, just log it
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Check if URL can be opened with caching
95
+ */
96
+ static async canOpenUrl(url: string): Promise<boolean> {
97
+ if (!this.isValidUrl(url)) return false;
98
+
99
+ // Check cache first
100
+ const cached = urlValidationCache.get(url);
101
+ if (cached !== undefined) {
102
+ return cached;
103
+ }
104
+
105
+ try {
106
+ const Linking = await this.getLinkingModule();
107
+ const canOpen = await Linking.canOpenURL(url);
108
+
109
+ // Cache the result with size limit
110
+ this.cleanCache();
111
+ urlValidationCache.set(url, canOpen);
112
+
113
+ return canOpen;
114
+ } catch {
115
+ // Cache failure to prevent repeated attempts
116
+ this.cleanCache();
117
+ urlValidationCache.set(url, false);
118
+ return false;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Clear all caches (useful for testing or memory cleanup)
124
+ */
125
+ static clearCache(): void {
126
+ urlValidationCache.clear();
127
+ }
128
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Legal Domain
3
+ * Terms, privacy policy, licenses
4
+ */
5
+
6
+ export * from './presentation/screens/LegalScreen';
7
+ export * from './presentation/screens/PrivacyPolicyScreen';
8
+ export * from './presentation/screens/TermsOfServiceScreen';
@@ -0,0 +1,177 @@
1
+ /**
2
+ * LegalItem Component
3
+ *
4
+ * Single Responsibility: Display a single legal document item
5
+ * Reusable component for Privacy Policy, Terms of Service, and EULA items
6
+ */
7
+
8
+ import React from "react";
9
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
10
+ import { useResponsiveDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
11
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
12
+ import type { IconName } from "@umituz/react-native-design-system";
13
+ import { StyleCacheService } from "../../domain/services/StyleCacheService";
14
+
15
+ export interface LegalItemProps {
16
+ /**
17
+ * Icon name from Lucide library (e.g., "Shield", "FileText", "ScrollText")
18
+ * If not provided, will use emoji icon
19
+ */
20
+ iconName?: IconName;
21
+ /**
22
+ * Icon emoji or text (fallback if iconName not provided)
23
+ */
24
+ icon?: string;
25
+ /**
26
+ * Title text
27
+ */
28
+ title: string;
29
+ /**
30
+ * Optional description text
31
+ */
32
+ description?: string;
33
+ /**
34
+ * Callback when item is pressed
35
+ */
36
+ onPress?: () => void;
37
+ /**
38
+ * Test ID for E2E testing
39
+ */
40
+ testID?: string;
41
+ }
42
+
43
+ export const LegalItem: React.FC<LegalItemProps> = React.memo(({
44
+ iconName,
45
+ icon,
46
+ title,
47
+ description,
48
+ onPress,
49
+ testID,
50
+ }) => {
51
+ const tokens = useResponsiveDesignTokens();
52
+
53
+ // Memoize styles to prevent recreation on every render
54
+ const styles = React.useMemo(() => {
55
+ const cacheKey = StyleCacheService.createTokenCacheKey(tokens);
56
+ return StyleCacheService.getCachedStyles(
57
+ 'LegalItem',
58
+ cacheKey,
59
+ () => createLegalItemStyles(tokens)
60
+ );
61
+ }, [tokens]);
62
+
63
+ // Memoize icon rendering to prevent unnecessary re-renders
64
+ const renderIcon = React.useCallback(() => {
65
+ if (iconName) {
66
+ return (
67
+ <AtomicIcon
68
+ name={iconName}
69
+ size="md"
70
+ color="info"
71
+ />
72
+ );
73
+ }
74
+ if (icon) {
75
+ return (
76
+ <AtomicText type="bodyLarge" color="info">
77
+ {icon}
78
+ </AtomicText>
79
+ );
80
+ }
81
+ return null;
82
+ }, [iconName, icon]);
83
+
84
+ // Memoize icon container style to prevent object creation
85
+ const iconContainerStyle = React.useMemo(() => [
86
+ styles.iconContainer,
87
+ { backgroundColor: tokens.colors.info + "20" }
88
+ ], [styles.iconContainer, tokens.colors.info]);
89
+
90
+ // Memoize content to prevent unnecessary re-renders
91
+ const content = React.useMemo(() => (
92
+ <View style={styles.itemContent}>
93
+ <View style={styles.itemLeft}>
94
+ <View style={iconContainerStyle}>
95
+ {renderIcon()}
96
+ </View>
97
+ <View style={styles.itemText}>
98
+ <AtomicText type="bodyLarge" color="textPrimary">
99
+ {title}
100
+ </AtomicText>
101
+ {description && (
102
+ <AtomicText
103
+ type="bodySmall"
104
+ color="textSecondary"
105
+ style={styles.itemDescription}
106
+ >
107
+ {description}
108
+ </AtomicText>
109
+ )}
110
+ </View>
111
+ </View>
112
+ {onPress && (
113
+ <AtomicText type="bodyMedium" color="textSecondary">›</AtomicText>
114
+ )}
115
+ </View>
116
+ ), [styles.itemContent, styles.itemLeft, styles.itemText, styles.itemDescription, iconContainerStyle, renderIcon, title, description, onPress]);
117
+
118
+ // Memoize press handler to prevent child re-renders
119
+ const handlePress = React.useCallback(() => {
120
+ onPress?.();
121
+ }, [onPress]);
122
+
123
+ if (onPress) {
124
+ return (
125
+ <TouchableOpacity
126
+ style={styles.itemContainer}
127
+ onPress={handlePress}
128
+ testID={testID}
129
+ activeOpacity={0.7}
130
+ >
131
+ {content}
132
+ </TouchableOpacity>
133
+ );
134
+ }
135
+
136
+ return (
137
+ <View style={styles.itemContainer} testID={testID}>
138
+ {content}
139
+ </View>
140
+ );
141
+ });
142
+
143
+ const createLegalItemStyles = (tokens: DesignTokens) => {
144
+ return StyleSheet.create({
145
+ itemContainer: {
146
+ marginBottom: tokens.spacing.xs,
147
+ },
148
+ itemContent: {
149
+ flexDirection: "row",
150
+ alignItems: "center",
151
+ justifyContent: "space-between",
152
+ paddingHorizontal: tokens.spacing.md,
153
+ paddingVertical: tokens.spacing.md,
154
+ minHeight: 64,
155
+ },
156
+ itemLeft: {
157
+ flexDirection: "row",
158
+ alignItems: "center",
159
+ flex: 1,
160
+ },
161
+ iconContainer: {
162
+ width: 44,
163
+ height: 44,
164
+ borderRadius: 22,
165
+ alignItems: "center",
166
+ justifyContent: "center",
167
+ marginRight: tokens.spacing.md,
168
+ },
169
+ itemText: {
170
+ flex: 1,
171
+ },
172
+ itemDescription: {
173
+ marginTop: tokens.spacing.xs,
174
+ },
175
+ });
176
+ };
177
+