@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,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,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
|
+
|