@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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAppearance Hook
|
|
3
|
+
*
|
|
4
|
+
* Hook for accessing appearance state and actions
|
|
5
|
+
* Single Responsibility: Presentation layer data access
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback } from "react";
|
|
9
|
+
import { useAppearanceStore } from "../infrastructure/stores/appearanceStore";
|
|
10
|
+
import { appearanceService } from "../infrastructure/services/appearanceService";
|
|
11
|
+
import type { ThemeMode, CustomThemeColors } from "../types";
|
|
12
|
+
|
|
13
|
+
export const useAppearance = () => {
|
|
14
|
+
const store = useAppearanceStore();
|
|
15
|
+
|
|
16
|
+
const setThemeMode = useCallback(
|
|
17
|
+
async (mode: ThemeMode) => {
|
|
18
|
+
await appearanceService.setThemeMode(mode);
|
|
19
|
+
},
|
|
20
|
+
[]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const toggleTheme = useCallback(
|
|
24
|
+
async () => {
|
|
25
|
+
await appearanceService.toggleTheme();
|
|
26
|
+
},
|
|
27
|
+
[]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const setCustomColors = useCallback(
|
|
31
|
+
async (colors: CustomThemeColors) => {
|
|
32
|
+
await appearanceService.setCustomColors(colors);
|
|
33
|
+
},
|
|
34
|
+
[]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const resetCustomColors = useCallback(
|
|
38
|
+
async () => {
|
|
39
|
+
await appearanceService.resetCustomColors();
|
|
40
|
+
},
|
|
41
|
+
[]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const reset = useCallback(
|
|
45
|
+
async () => {
|
|
46
|
+
await appearanceService.reset();
|
|
47
|
+
},
|
|
48
|
+
[]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
themeMode: store.settings.themeMode,
|
|
53
|
+
customColors: store.settings.customColors,
|
|
54
|
+
isInitialized: store.isInitialized,
|
|
55
|
+
setThemeMode,
|
|
56
|
+
toggleTheme,
|
|
57
|
+
setCustomColors,
|
|
58
|
+
resetCustomColors,
|
|
59
|
+
reset,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Actions Hook
|
|
3
|
+
* Single Responsibility: Handle appearance-related presentation actions
|
|
4
|
+
* Business logic extracted to service layer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useState, useEffect, useRef } from "react";
|
|
8
|
+
import { useAppearance } from "./useAppearance";
|
|
9
|
+
import type {
|
|
10
|
+
ThemeMode,
|
|
11
|
+
CustomThemeColors,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
|
|
14
|
+
export interface UseAppearanceActionsReturn {
|
|
15
|
+
localCustomColors: CustomThemeColors;
|
|
16
|
+
handleThemeSelect: (mode: ThemeMode) => Promise<void>;
|
|
17
|
+
handleColorChange: (key: keyof CustomThemeColors, color: string) => void;
|
|
18
|
+
handleResetColors: (onConfirm?: () => void) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AppearanceActionsConfig {
|
|
22
|
+
onResetConfirm?: () => void;
|
|
23
|
+
onResetCancel?: () => void;
|
|
24
|
+
resetTitle?: string;
|
|
25
|
+
resetMessage?: string;
|
|
26
|
+
confirmLabel?: string;
|
|
27
|
+
cancelLabel?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useAppearanceActions(
|
|
31
|
+
config?: AppearanceActionsConfig
|
|
32
|
+
): UseAppearanceActionsReturn {
|
|
33
|
+
const { customColors, setThemeMode, setCustomColors, resetCustomColors } =
|
|
34
|
+
useAppearance();
|
|
35
|
+
|
|
36
|
+
// Use ref to prevent unnecessary re-renders and memory leaks
|
|
37
|
+
const configRef = useRef(config);
|
|
38
|
+
configRef.current = config;
|
|
39
|
+
|
|
40
|
+
// Initialize local state with custom colors, prevent unnecessary updates
|
|
41
|
+
const [localCustomColors, setLocalCustomColors] = useState<CustomThemeColors>(() =>
|
|
42
|
+
customColors || {}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Sync local state with store changes, but only if different
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (customColors && JSON.stringify(customColors) !== JSON.stringify(localCustomColors)) {
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
console.log("[useAppearanceActions] Syncing local colors with store");
|
|
50
|
+
}
|
|
51
|
+
setLocalCustomColors(customColors);
|
|
52
|
+
}
|
|
53
|
+
}, [customColors]); // Only depend on customColors, not localCustomColors
|
|
54
|
+
|
|
55
|
+
const handleThemeSelect = useCallback(
|
|
56
|
+
async (mode: ThemeMode) => {
|
|
57
|
+
try {
|
|
58
|
+
await setThemeMode(mode);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (__DEV__) {
|
|
61
|
+
console.error("[useAppearanceActions] Failed to set theme mode:", error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
[setThemeMode],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const handleColorChange = useCallback(
|
|
69
|
+
(key: keyof CustomThemeColors, color: string) => {
|
|
70
|
+
try {
|
|
71
|
+
// Prevent unnecessary updates if color hasn't changed
|
|
72
|
+
if (localCustomColors[key] === color) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const newColors = {
|
|
77
|
+
...localCustomColors,
|
|
78
|
+
[key]: color,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.log("[useAppearanceActions] Updating color:", key, color);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setLocalCustomColors(newColors);
|
|
86
|
+
setCustomColors(newColors);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (__DEV__) {
|
|
89
|
+
console.error("[useAppearanceActions] Failed to update color:", error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[localCustomColors, setCustomColors],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const handleResetColors = useCallback(
|
|
97
|
+
(onConfirm?: () => void) => {
|
|
98
|
+
try {
|
|
99
|
+
// Generic reset handler - the host app should handle the UI confirmation
|
|
100
|
+
const resetAction = async () => {
|
|
101
|
+
try {
|
|
102
|
+
setLocalCustomColors({});
|
|
103
|
+
await resetCustomColors();
|
|
104
|
+
onConfirm?.();
|
|
105
|
+
configRef.current?.onResetConfirm?.();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (__DEV__) {
|
|
108
|
+
console.error("[useAppearanceActions] Failed to reset colors:", error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// If no custom config provided, just reset directly
|
|
114
|
+
if (!configRef.current?.resetTitle) {
|
|
115
|
+
resetAction();
|
|
116
|
+
}
|
|
117
|
+
// Otherwise, the host app should handle showing the confirmation dialog
|
|
118
|
+
// and call resetAction when confirmed
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (__DEV__) {
|
|
121
|
+
console.error("[useAppearanceActions] Failed to handle reset colors:", error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[resetCustomColors],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Cleanup effect
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
return () => {
|
|
131
|
+
// Cleanup any pending operations or references
|
|
132
|
+
if (__DEV__) {
|
|
133
|
+
console.log("[useAppearanceActions] Cleanup");
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
localCustomColors,
|
|
140
|
+
handleThemeSelect,
|
|
141
|
+
handleColorChange,
|
|
142
|
+
handleResetColors,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Service
|
|
3
|
+
*
|
|
4
|
+
* Business logic for appearance management
|
|
5
|
+
* Single Responsibility: Business logic only, no presentation logic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AppearanceStorage } from "../storage/appearanceStorage";
|
|
9
|
+
import { useAppearanceStore } from "../stores/appearanceStore";
|
|
10
|
+
import {
|
|
11
|
+
useTheme,
|
|
12
|
+
useDesignSystemTheme,
|
|
13
|
+
type ThemeMode,
|
|
14
|
+
type CustomThemeColors,
|
|
15
|
+
} from "@umituz/react-native-design-system";
|
|
16
|
+
import { getSystemTheme } from "./systemThemeDetection";
|
|
17
|
+
import { validateAppearanceSettings } from "./validation";
|
|
18
|
+
import type { AppearanceSettings } from "../../types";
|
|
19
|
+
|
|
20
|
+
export class AppearanceService {
|
|
21
|
+
private readonly DEFAULT_THEME_MODE: ThemeMode = "light"; // Use system preference as default
|
|
22
|
+
private _isInitialized = false;
|
|
23
|
+
private initPromise: Promise<void> | null = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize appearance settings
|
|
27
|
+
* Business logic: Coordinate initialization process
|
|
28
|
+
*/
|
|
29
|
+
async initialize(): Promise<void> {
|
|
30
|
+
// Prevent multiple initializations
|
|
31
|
+
if (this._isInitialized || this.initPromise) {
|
|
32
|
+
return this.initPromise || Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.initPromise = this._performInitialization();
|
|
36
|
+
return this.initPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async _performInitialization(): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log("[AppearanceService] Initializing appearance settings");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const savedSettings = await AppearanceStorage.getSettings();
|
|
46
|
+
|
|
47
|
+
if (savedSettings) {
|
|
48
|
+
// Load saved settings
|
|
49
|
+
useAppearanceStore.getState().setSettings(savedSettings);
|
|
50
|
+
useAppearanceStore.getState().setInitialized(true);
|
|
51
|
+
|
|
52
|
+
// Sync with design system theme
|
|
53
|
+
await this.syncWithDesignSystem(savedSettings);
|
|
54
|
+
} else {
|
|
55
|
+
// Use system theme as default, fallback to light
|
|
56
|
+
const systemTheme = getSystemTheme();
|
|
57
|
+
const defaultSettings: AppearanceSettings = {
|
|
58
|
+
themeMode: systemTheme || this.DEFAULT_THEME_MODE,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
useAppearanceStore.getState().setSettings(defaultSettings);
|
|
62
|
+
useAppearanceStore.getState().setInitialized(true);
|
|
63
|
+
|
|
64
|
+
// Sync with design system theme
|
|
65
|
+
await this.syncWithDesignSystem(defaultSettings);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._isInitialized = true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
console.error("[AppearanceService] Initialization failed:", error);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fallback to system theme or light mode on error
|
|
75
|
+
const systemTheme = getSystemTheme();
|
|
76
|
+
const fallbackSettings: AppearanceSettings = {
|
|
77
|
+
themeMode: systemTheme || this.DEFAULT_THEME_MODE,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
useAppearanceStore.getState().setSettings(fallbackSettings);
|
|
81
|
+
useAppearanceStore.getState().setInitialized(true);
|
|
82
|
+
|
|
83
|
+
await this.syncWithDesignSystem(fallbackSettings);
|
|
84
|
+
this._isInitialized = true;
|
|
85
|
+
} finally {
|
|
86
|
+
this.initPromise = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get current theme mode
|
|
92
|
+
* Business logic: Provide theme mode data
|
|
93
|
+
*/
|
|
94
|
+
getThemeMode(): ThemeMode {
|
|
95
|
+
return useAppearanceStore.getState().settings.themeMode;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set theme mode
|
|
100
|
+
* Business logic: Validate and apply theme mode
|
|
101
|
+
*/
|
|
102
|
+
async setThemeMode(mode: ThemeMode): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
if (__DEV__) {
|
|
105
|
+
console.log("[AppearanceService] Setting theme mode:", mode);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate theme mode
|
|
109
|
+
if (!mode || (mode !== 'light' && mode !== 'dark')) {
|
|
110
|
+
throw new Error(`Invalid theme mode: ${mode}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const currentSettings = useAppearanceStore.getState().settings;
|
|
114
|
+
const newSettings: AppearanceSettings = {
|
|
115
|
+
...currentSettings,
|
|
116
|
+
themeMode: mode,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Update store
|
|
120
|
+
useAppearanceStore.getState().updateThemeMode(mode);
|
|
121
|
+
|
|
122
|
+
// Persist to storage
|
|
123
|
+
await AppearanceStorage.setSettings(newSettings);
|
|
124
|
+
|
|
125
|
+
// Sync with design system
|
|
126
|
+
await this.syncWithDesignSystem(newSettings);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
129
|
+
if (__DEV__) {
|
|
130
|
+
console.error("[AppearanceService] Failed to set theme mode:", errorMessage);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Toggle theme mode
|
|
138
|
+
* Business logic: Toggle between light and dark
|
|
139
|
+
*/
|
|
140
|
+
async toggleTheme(): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
const currentMode = this.getThemeMode();
|
|
143
|
+
const newMode: ThemeMode =
|
|
144
|
+
currentMode === "light" ? "dark" : "light";
|
|
145
|
+
|
|
146
|
+
await this.setThemeMode(newMode);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (__DEV__) {
|
|
149
|
+
console.error("[AppearanceService] Failed to toggle theme:", error);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get custom colors
|
|
157
|
+
* Business logic: Provide custom colors data
|
|
158
|
+
*/
|
|
159
|
+
getCustomColors(): CustomThemeColors | undefined {
|
|
160
|
+
return useAppearanceStore.getState().settings.customColors;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set custom colors
|
|
165
|
+
* Business logic: Validate and apply custom colors
|
|
166
|
+
*/
|
|
167
|
+
async setCustomColors(colors: CustomThemeColors): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
if (__DEV__) {
|
|
170
|
+
console.log("[AppearanceService] Setting custom colors:", colors);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Validate custom colors
|
|
174
|
+
const validation = validateAppearanceSettings({ customColors: colors });
|
|
175
|
+
if (!validation.isValid) {
|
|
176
|
+
throw new Error(`Invalid custom colors: ${validation.errors.join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const currentSettings = useAppearanceStore.getState().settings;
|
|
180
|
+
const newSettings: AppearanceSettings = {
|
|
181
|
+
...currentSettings,
|
|
182
|
+
customColors: {
|
|
183
|
+
...currentSettings.customColors,
|
|
184
|
+
...colors,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Update store
|
|
189
|
+
useAppearanceStore.getState().updateCustomColors(newSettings.customColors);
|
|
190
|
+
|
|
191
|
+
// Persist to storage
|
|
192
|
+
await AppearanceStorage.setSettings(newSettings);
|
|
193
|
+
|
|
194
|
+
// Sync with design system
|
|
195
|
+
await this.syncWithDesignSystem(newSettings);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
198
|
+
if (__DEV__) {
|
|
199
|
+
console.error("[AppearanceService] Failed to set custom colors:", errorMessage);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Reset custom colors to defaults
|
|
207
|
+
* Business logic: Reset custom colors
|
|
208
|
+
*/
|
|
209
|
+
async resetCustomColors(): Promise<void> {
|
|
210
|
+
try {
|
|
211
|
+
if (__DEV__) {
|
|
212
|
+
console.log("[AppearanceService] Resetting custom colors");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const currentSettings = useAppearanceStore.getState().settings;
|
|
216
|
+
const newSettings: AppearanceSettings = {
|
|
217
|
+
...currentSettings,
|
|
218
|
+
customColors: undefined,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Update store
|
|
222
|
+
useAppearanceStore.getState().updateCustomColors(undefined);
|
|
223
|
+
|
|
224
|
+
// Persist to storage
|
|
225
|
+
await AppearanceStorage.setSettings(newSettings);
|
|
226
|
+
|
|
227
|
+
// Sync with design System
|
|
228
|
+
await this.syncWithDesignSystem(newSettings);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (__DEV__) {
|
|
231
|
+
console.error("[AppearanceService] Failed to reset custom colors:", error);
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Reset all appearance settings
|
|
239
|
+
* Business logic: Reset all settings
|
|
240
|
+
*/
|
|
241
|
+
async reset(): Promise<void> {
|
|
242
|
+
try {
|
|
243
|
+
if (__DEV__) {
|
|
244
|
+
console.log("[AppearanceService] Resetting all appearance settings");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Clear storage
|
|
248
|
+
await AppearanceStorage.clear();
|
|
249
|
+
|
|
250
|
+
// Reset store to defaults
|
|
251
|
+
useAppearanceStore.getState().resetState();
|
|
252
|
+
|
|
253
|
+
// Reset design system theme to system preference
|
|
254
|
+
const systemTheme = getSystemTheme();
|
|
255
|
+
const defaultSettings: AppearanceSettings = {
|
|
256
|
+
themeMode: systemTheme || this.DEFAULT_THEME_MODE,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
await this.syncWithDesignSystem(defaultSettings);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (__DEV__) {
|
|
262
|
+
console.error("[AppearanceService] Failed to reset appearance:", error);
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if appearance is initialized
|
|
270
|
+
* Business logic: Provide initialization status
|
|
271
|
+
*/
|
|
272
|
+
isInitialized(): boolean {
|
|
273
|
+
return useAppearanceStore.getState().isInitialized;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Sync appearance settings with design system
|
|
278
|
+
* Private helper method
|
|
279
|
+
*/
|
|
280
|
+
private async syncWithDesignSystem(settings: AppearanceSettings): Promise<void> {
|
|
281
|
+
try {
|
|
282
|
+
// Sync theme mode
|
|
283
|
+
useTheme.getState().setThemeMode(settings.themeMode);
|
|
284
|
+
useDesignSystemTheme.getState().setThemeMode(settings.themeMode);
|
|
285
|
+
|
|
286
|
+
// Sync custom colors
|
|
287
|
+
useDesignSystemTheme.getState().setCustomColors(settings.customColors);
|
|
288
|
+
|
|
289
|
+
if (__DEV__) {
|
|
290
|
+
console.log("[AppearanceService] Synced with design system:", settings);
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (__DEV__) {
|
|
294
|
+
console.error("[AppearanceService] Failed to sync with design system:", error);
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export const appearanceService = new AppearanceService();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System Theme Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for detecting device theme preferences
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Appearance, Platform } from 'react-native';
|
|
8
|
+
import type { ThemeMode } from '@umituz/react-native-design-system';
|
|
9
|
+
|
|
10
|
+
declare const window: any;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get system theme mode from device settings
|
|
14
|
+
* @returns System theme mode ('light' | 'dark' | null)
|
|
15
|
+
*/
|
|
16
|
+
export const getSystemTheme = (): ThemeMode | null => {
|
|
17
|
+
try {
|
|
18
|
+
// On web, use matchMedia
|
|
19
|
+
if (Platform.OS === 'web') {
|
|
20
|
+
if (typeof window !== 'undefined' && (window as any).matchMedia) {
|
|
21
|
+
const darkModeQuery = (window as any).matchMedia('(prefers-color-scheme: dark)');
|
|
22
|
+
if (darkModeQuery.matches) {
|
|
23
|
+
return 'dark';
|
|
24
|
+
}
|
|
25
|
+
return 'light';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// On native platforms, use Appearance API
|
|
30
|
+
const colorScheme = Appearance.getColorScheme();
|
|
31
|
+
return colorScheme === 'dark' ? 'dark' : 'light';
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (__DEV__) {
|
|
34
|
+
console.warn('[getSystemTheme] Failed to detect system theme:', error);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if system theme is dark
|
|
42
|
+
* @returns true if system prefers dark mode
|
|
43
|
+
*/
|
|
44
|
+
export const isSystemThemeDark = (): boolean => {
|
|
45
|
+
return getSystemTheme() === 'dark';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if system theme is light
|
|
50
|
+
* @returns true if system prefers light mode
|
|
51
|
+
*/
|
|
52
|
+
export const isSystemThemeLight = (): boolean => {
|
|
53
|
+
return getSystemTheme() === 'light';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add system theme change listener
|
|
58
|
+
* @param callback - Function to call when theme changes
|
|
59
|
+
* @returns Cleanup function to remove listener
|
|
60
|
+
*/
|
|
61
|
+
export const addSystemThemeListener = (
|
|
62
|
+
callback: (themeMode: ThemeMode) => void,
|
|
63
|
+
): (() => void) => {
|
|
64
|
+
try {
|
|
65
|
+
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
|
66
|
+
const themeMode: ThemeMode = colorScheme === 'dark' ? 'dark' : 'light';
|
|
67
|
+
callback(themeMode);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
subscription?.remove();
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (__DEV__) {
|
|
75
|
+
console.warn('[addSystemThemeListener] Failed to add listener:', error);
|
|
76
|
+
}
|
|
77
|
+
return () => { }; // Return empty cleanup function
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Validation functions for appearance settings and colors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ThemeMode, CustomThemeColors } from '@umituz/react-native-design-system';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate theme mode
|
|
11
|
+
* @param mode - Theme mode to validate
|
|
12
|
+
* @returns true if valid theme mode
|
|
13
|
+
*/
|
|
14
|
+
export const isValidThemeMode = (mode: string): mode is ThemeMode => {
|
|
15
|
+
return mode === 'light' || mode === 'dark';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate hex color format
|
|
20
|
+
* @param color - Color string to validate
|
|
21
|
+
* @returns true if valid hex color
|
|
22
|
+
*/
|
|
23
|
+
export const isValidHexColor = (color: string): boolean => {
|
|
24
|
+
if (!color || typeof color !== 'string') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hexRegex = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
|
|
29
|
+
return hexRegex.test(color);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate custom colors object
|
|
34
|
+
* @param colors - Custom colors to validate
|
|
35
|
+
* @returns Validation result with errors
|
|
36
|
+
*/
|
|
37
|
+
export const validateCustomColors = (
|
|
38
|
+
colors: CustomThemeColors
|
|
39
|
+
): { isValid: boolean; errors: string[] } => {
|
|
40
|
+
const errors: string[] = [];
|
|
41
|
+
|
|
42
|
+
// Check each color if provided
|
|
43
|
+
const colorFields: (keyof CustomThemeColors)[] = [
|
|
44
|
+
'primary', 'primaryLight', 'primaryDark',
|
|
45
|
+
'secondary', 'secondaryLight', 'secondaryDark',
|
|
46
|
+
'accent', 'accentLight', 'accentDark',
|
|
47
|
+
'buttonPrimary', 'buttonSecondary'
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const field of colorFields) {
|
|
51
|
+
const color = colors[field];
|
|
52
|
+
if (color && !isValidHexColor(color)) {
|
|
53
|
+
errors.push(`Invalid ${field} color: ${color}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
isValid: errors.length === 0,
|
|
59
|
+
errors
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate appearance settings
|
|
65
|
+
* @param settings - Settings to validate
|
|
66
|
+
* @returns Validation result with errors
|
|
67
|
+
*/
|
|
68
|
+
export const validateAppearanceSettings = (settings: {
|
|
69
|
+
themeMode?: string;
|
|
70
|
+
customColors?: CustomThemeColors;
|
|
71
|
+
}): { isValid: boolean; errors: string[] } => {
|
|
72
|
+
const errors: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Validate theme mode
|
|
75
|
+
if (settings.themeMode && !isValidThemeMode(settings.themeMode)) {
|
|
76
|
+
errors.push(`Invalid theme mode: ${settings.themeMode}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate custom colors
|
|
80
|
+
if (settings.customColors) {
|
|
81
|
+
const colorValidation = validateCustomColors(settings.customColors);
|
|
82
|
+
if (!colorValidation.isValid) {
|
|
83
|
+
errors.push(...colorValidation.errors);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
isValid: errors.length === 0,
|
|
89
|
+
errors
|
|
90
|
+
};
|
|
91
|
+
};
|