@umituz/react-native-settings 4.17.26 → 4.17.30

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 (41) hide show
  1. package/package.json +15 -6
  2. package/src/domains/about/presentation/components/AboutSection.tsx +14 -71
  3. package/src/domains/appearance/application/ports/IAppearanceRepository.ts +8 -0
  4. package/src/domains/appearance/hooks/index.ts +1 -1
  5. package/src/domains/appearance/hooks/useAppearance.ts +18 -58
  6. package/src/domains/appearance/hooks/useAppearanceActions.ts +20 -128
  7. package/src/domains/appearance/infrastructure/repositories/AppearanceRepository.ts +34 -0
  8. package/src/domains/appearance/infrastructure/services/AppearanceService.ts +51 -0
  9. package/src/domains/appearance/presentation/components/AppearanceSection.tsx +2 -2
  10. package/src/domains/appearance/presentation/hooks/mutations/useAppearanceMutations.ts +36 -0
  11. package/src/domains/appearance/presentation/hooks/queries/useAppearanceQuery.ts +15 -0
  12. package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +37 -40
  13. package/src/domains/faqs/presentation/components/FAQSection.tsx +1 -1
  14. package/src/domains/feedback/presentation/components/FeedbackModal.tsx +11 -15
  15. package/src/domains/feedback/presentation/components/SupportSection.tsx +2 -2
  16. package/src/domains/legal/presentation/components/LegalItem.tsx +13 -129
  17. package/src/index.ts +15 -9
  18. package/src/infrastructure/repositories/SettingsRepository.ts +105 -0
  19. package/src/infrastructure/services/SettingsService.ts +47 -0
  20. package/src/presentation/components/SettingItem.tsx +77 -129
  21. package/src/presentation/components/SettingsFooter.tsx +9 -25
  22. package/src/presentation/components/SettingsSection.tsx +9 -20
  23. package/src/presentation/hooks/mutations/useSettingsMutations.ts +58 -0
  24. package/src/presentation/hooks/queries/useSettingsQuery.ts +27 -0
  25. package/src/presentation/hooks/useSettings.ts +45 -0
  26. package/src/presentation/screens/components/SettingsContent.tsx +20 -247
  27. package/src/presentation/screens/components/sections/CustomSettingsList.tsx +31 -0
  28. package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +55 -0
  29. package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +43 -0
  30. package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +47 -0
  31. package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +84 -0
  32. package/src/presentation/screens/hooks/useFeatureDetection.ts +1 -16
  33. package/src/presentation/screens/types/FeatureConfig.ts +18 -0
  34. package/src/presentation/screens/types/SettingsConfig.ts +7 -0
  35. package/src/domains/appearance/infrastructure/services/appearanceService.ts +0 -301
  36. package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +0 -120
  37. package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +0 -132
  38. package/src/infrastructure/storage/SettingsStore.ts +0 -189
  39. package/src/infrastructure/storage/__tests__/SettingsStore.test.tsx +0 -302
  40. package/src/presentation/components/CloudSyncSetting.tsx +0 -58
  41. /package/src/{domain/repositories → application/ports}/ISettingsRepository.ts +0 -0
@@ -1,189 +0,0 @@
1
- /**
2
- * Settings Store - Zustand State Management
3
- *
4
- * Global settings state for app preferences
5
- * Manages theme, language, notifications, and privacy settings
6
- *
7
- * DDD ARCHITECTURE: Uses @umituz/react-native-storage for all storage operations
8
- * - Type-safe storage with StorageKey enum
9
- * - Result pattern for error handling
10
- * - Single source of truth for all storage
11
- */
12
-
13
- import { create } from 'zustand';
14
- import { storageRepository, StorageKey, createUserKey, unwrap } from '@umituz/react-native-storage';
15
- import type { UserSettings } from '../../domain/repositories/ISettingsRepository';
16
-
17
- interface SettingsStore {
18
- // State
19
- settings: UserSettings | null;
20
- loading: boolean;
21
- error: string | null;
22
-
23
- // Actions
24
- loadSettings: (userId: string) => Promise<void>;
25
- updateSettings: (updates: Partial<UserSettings>) => Promise<void>;
26
- resetSettings: (userId: string) => Promise<void>;
27
- clearError: () => void;
28
- }
29
-
30
- const DEFAULT_OFFLINE_USER_ID = 'offline_user';
31
-
32
- const DEFAULT_SETTINGS_CACHE = new Map<string, UserSettings>();
33
-
34
- const getDefaultSettings = (userId: string): UserSettings => {
35
- if (DEFAULT_SETTINGS_CACHE.has(userId)) {
36
- return DEFAULT_SETTINGS_CACHE.get(userId)!;
37
- }
38
-
39
- const settings = {
40
- userId,
41
- theme: 'auto' as const,
42
- language: 'en-US',
43
- notificationsEnabled: true,
44
- emailNotifications: true,
45
- pushNotifications: true,
46
- soundEnabled: true,
47
- vibrationEnabled: true,
48
- privacyMode: false,
49
- disclaimerAccepted: false,
50
- updatedAt: new Date(),
51
- };
52
-
53
- DEFAULT_SETTINGS_CACHE.set(userId, settings);
54
- return settings;
55
- };
56
-
57
- export const useSettingsStore = create<SettingsStore>((set, get) => ({
58
- settings: null,
59
- loading: false,
60
- error: null,
61
-
62
- loadSettings: async (userId: string) => {
63
- if (__DEV__) {
64
- console.log('SettingsStore: Loading settings for user:', userId);
65
- }
66
-
67
- set({ loading: true, error: null });
68
-
69
- try {
70
- const defaultSettings = getDefaultSettings(userId);
71
- const storageKey = createUserKey(StorageKey.USER_PREFERENCES, userId);
72
-
73
- // ✅ DRY: Storage domain handles JSON parse, error handling
74
- const result = await storageRepository.getItem<UserSettings>(storageKey, defaultSettings);
75
- const data = unwrap(result, defaultSettings);
76
-
77
- // ✅ CLEAN CODE: Auto-save defaults if not exists
78
- if (!result.success) {
79
- await storageRepository.setItem(storageKey, defaultSettings);
80
- }
81
-
82
- set({
83
- settings: data,
84
- loading: false,
85
- error: null,
86
- });
87
-
88
- if (__DEV__) {
89
- console.log('SettingsStore: Settings loaded successfully');
90
- }
91
- } catch (error) {
92
- if (__DEV__) {
93
- console.error('SettingsStore: Failed to load settings:', error);
94
- }
95
- set({ loading: false, error: 'Failed to load settings' });
96
- }
97
- },
98
-
99
- updateSettings: async (updates: Partial<UserSettings>) => {
100
- if (__DEV__) {
101
- console.log('SettingsStore: Updating settings with:', updates);
102
- }
103
-
104
- const { settings } = get();
105
-
106
- // ✅ CLEAN CODE: Auto-initialize if settings not loaded
107
- if (!settings) {
108
- await get().loadSettings(DEFAULT_OFFLINE_USER_ID);
109
- }
110
-
111
- // ✅ DEFENSIVE: Verify settings loaded successfully
112
- const currentSettings = get().settings;
113
- if (!currentSettings) {
114
- const errorMsg = 'Failed to initialize settings';
115
- if (__DEV__) {
116
- console.error('SettingsStore:', errorMsg);
117
- }
118
- set({ error: errorMsg });
119
- return;
120
- }
121
-
122
- set({ loading: true, error: null });
123
-
124
- try {
125
- const updatedSettings: UserSettings = {
126
- ...currentSettings,
127
- ...updates,
128
- updatedAt: new Date(),
129
- };
130
-
131
- const storageKey = createUserKey(StorageKey.USER_PREFERENCES, currentSettings.userId);
132
-
133
- // ✅ DRY: Storage domain replaces JSON.stringify + AsyncStorage + try/catch
134
- const result = await storageRepository.setItem(storageKey, updatedSettings);
135
-
136
- set({
137
- settings: result.success ? updatedSettings : currentSettings,
138
- loading: false,
139
- error: null,
140
- });
141
-
142
- if (__DEV__) {
143
- console.log('SettingsStore: Settings updated successfully');
144
- }
145
- } catch (error) {
146
- if (__DEV__) {
147
- console.error('SettingsStore: Failed to update settings:', error);
148
- }
149
- set({ loading: false, error: 'Failed to update settings' });
150
- }
151
- },
152
-
153
- resetSettings: async (userId: string) => {
154
- set({ loading: true, error: null });
155
-
156
- const defaultSettings = getDefaultSettings(userId);
157
- const storageKey = createUserKey(StorageKey.USER_PREFERENCES, userId);
158
-
159
- // ✅ DRY: Storage domain replaces JSON.stringify + AsyncStorage + try/catch
160
- const result = await storageRepository.setItem(storageKey, defaultSettings);
161
-
162
- set({
163
- settings: result.success ? defaultSettings : get().settings,
164
- loading: false,
165
- error: null,
166
- });
167
- },
168
-
169
- clearError: () => set({ error: null }),
170
- }));
171
-
172
- /**
173
- * Hook for accessing settings state
174
- */
175
- export const useSettings = () => {
176
- const { settings, loading, error, loadSettings, updateSettings, resetSettings, clearError } =
177
- useSettingsStore();
178
-
179
- return {
180
- settings,
181
- loading,
182
- error,
183
- loadSettings,
184
- updateSettings,
185
- resetSettings,
186
- clearError,
187
- };
188
- };
189
-
@@ -1,302 +0,0 @@
1
- /**
2
- * Tests for SettingsStore and useSettings Hook
3
- */
4
-
5
- import { renderHook, act } from '@testing-library/react-hooks';
6
- import { useSettings, useSettingsStore } from '../SettingsStore';
7
- import type { UserSettings } from '../../../domain/repositories/ISettingsRepository';
8
-
9
- // Mock storage repository
10
- const mockStorageRepository = {
11
- getItem: jest.fn(),
12
- setItem: jest.fn(),
13
- removeItem: jest.fn(),
14
- clear: jest.fn(),
15
- };
16
-
17
- jest.mock('@umituz/react-native-storage', () => ({
18
- storageRepository: mockStorageRepository,
19
- StorageKey: {
20
- SETTINGS: 'settings',
21
- },
22
- createUserKey: (key: string, userId: string) => `${key}_${userId}`,
23
- unwrap: (result: any, defaultValue: any) => result.success ? result.data : defaultValue,
24
- }));
25
-
26
- describe('SettingsStore', () => {
27
- const mockUserId = 'test-user-123';
28
- const mockSettings: UserSettings = {
29
- userId: mockUserId,
30
- theme: 'dark',
31
- language: 'en-US',
32
- notificationsEnabled: true,
33
- emailNotifications: false,
34
- pushNotifications: true,
35
- soundEnabled: true,
36
- vibrationEnabled: false,
37
- privacyMode: false,
38
- updatedAt: new Date('2023-01-01'),
39
- };
40
-
41
- beforeEach(() => {
42
- jest.clearAllMocks();
43
- });
44
-
45
- describe('loadSettings', () => {
46
- it('loads settings successfully from storage', async () => {
47
- mockStorageRepository.getItem.mockResolvedValue({
48
- success: true,
49
- data: mockSettings,
50
- });
51
-
52
- const { result } = renderHook(() => useSettingsStore());
53
-
54
- await act(async () => {
55
- await result.current.loadSettings(mockUserId);
56
- });
57
-
58
- expect(result.current.settings).toEqual(mockSettings);
59
- expect(result.current.loading).toBe(false);
60
- expect(result.current.error).toBeNull();
61
- expect(mockStorageRepository.getItem).toHaveBeenCalledWith(
62
- `settings_${mockUserId}`,
63
- expect.any(Object)
64
- );
65
- });
66
-
67
- it('uses default settings when storage fails', async () => {
68
- mockStorageRepository.getItem.mockResolvedValue({
69
- success: false,
70
- error: 'Storage error',
71
- });
72
- mockStorageRepository.setItem.mockResolvedValue({
73
- success: true,
74
- });
75
-
76
- const { result } = renderHook(() => useSettingsStore());
77
-
78
- await act(async () => {
79
- await result.current.loadSettings(mockUserId);
80
- });
81
-
82
- expect(result.current.settings).toBeDefined();
83
- expect(result.current.settings?.userId).toBe(mockUserId);
84
- expect(result.current.settings?.theme).toBe('auto');
85
- expect(result.current.loading).toBe(false);
86
- expect(result.current.error).toBeNull();
87
- expect(mockStorageRepository.setItem).toHaveBeenCalled();
88
- });
89
-
90
- it('handles loading state correctly', async () => {
91
- mockStorageRepository.getItem.mockImplementation(() => new Promise(resolve =>
92
- setTimeout(() => resolve({ success: true, data: mockSettings }), 100)
93
- ));
94
-
95
- const { result } = renderHook(() => useSettingsStore());
96
-
97
- act(() => {
98
- result.current.loadSettings(mockUserId);
99
- });
100
-
101
- expect(result.current.loading).toBe(true);
102
-
103
- await act(async () => {
104
- await new Promise(resolve => setTimeout(resolve, 150));
105
- });
106
-
107
- expect(result.current.loading).toBe(false);
108
- expect(result.current.settings).toEqual(mockSettings);
109
- });
110
-
111
- it('handles storage exceptions', async () => {
112
- mockStorageRepository.getItem.mockRejectedValue(new Error('Storage exception'));
113
-
114
- const { result } = renderHook(() => useSettingsStore());
115
-
116
- await act(async () => {
117
- await result.current.loadSettings(mockUserId);
118
- });
119
-
120
- expect(result.current.loading).toBe(false);
121
- expect(result.current.error).toBe('Failed to load settings');
122
- expect(result.current.settings).toBeNull();
123
- });
124
- });
125
-
126
- describe('updateSettings', () => {
127
- it('updates settings successfully', async () => {
128
- // First load settings
129
- mockStorageRepository.getItem.mockResolvedValue({
130
- success: true,
131
- data: mockSettings,
132
- });
133
- mockStorageRepository.setItem.mockResolvedValue({
134
- success: true,
135
- });
136
-
137
- const { result } = renderHook(() => useSettingsStore());
138
-
139
- await act(async () => {
140
- await result.current.loadSettings(mockUserId);
141
- });
142
-
143
- // Then update settings
144
- const updates = { theme: 'light' as const, notificationsEnabled: false };
145
-
146
- await act(async () => {
147
- await result.current.updateSettings(updates);
148
- });
149
-
150
- expect(result.current.settings?.theme).toBe('light');
151
- expect(result.current.settings?.notificationsEnabled).toBe(false);
152
- expect(result.current.settings?.updatedAt).toBeInstanceOf(Date);
153
- expect(mockStorageRepository.setItem).toHaveBeenCalledWith(
154
- `settings_${mockUserId}`,
155
- expect.objectContaining({
156
- theme: 'light',
157
- notificationsEnabled: false,
158
- })
159
- );
160
- });
161
-
162
- it('auto-initializes settings when not loaded', async () => {
163
- mockStorageRepository.getItem
164
- .mockResolvedValueOnce({ success: false, error: 'Not found' })
165
- .mockResolvedValueOnce({ success: true, data: expect.any(Object) });
166
- mockStorageRepository.setItem.mockResolvedValue({ success: true });
167
-
168
- const { result } = renderHook(() => useSettingsStore());
169
-
170
- await act(async () => {
171
- await result.current.updateSettings({ theme: 'light' as const });
172
- });
173
-
174
- expect(result.current.settings).toBeDefined();
175
- expect(result.current.settings?.theme).toBe('light');
176
- });
177
-
178
- it('handles update failures gracefully', async () => {
179
- mockStorageRepository.getItem.mockResolvedValue({
180
- success: true,
181
- data: mockSettings,
182
- });
183
- mockStorageRepository.setItem.mockResolvedValue({
184
- success: false,
185
- error: 'Update failed',
186
- });
187
-
188
- const { result } = renderHook(() => useSettingsStore());
189
-
190
- await act(async () => {
191
- await result.current.loadSettings(mockUserId);
192
- });
193
-
194
- const originalSettings = result.current.settings;
195
-
196
- await act(async () => {
197
- await result.current.updateSettings({ theme: 'light' as const });
198
- });
199
-
200
- // Settings should remain unchanged on failure
201
- expect(result.current.settings).toEqual(originalSettings);
202
- expect(result.current.loading).toBe(false);
203
- });
204
-
205
- it('handles update exceptions', async () => {
206
- mockStorageRepository.getItem.mockResolvedValue({
207
- success: true,
208
- data: mockSettings,
209
- });
210
- mockStorageRepository.setItem.mockRejectedValue(new Error('Update exception'));
211
-
212
- const { result } = renderHook(() => useSettingsStore());
213
-
214
- await act(async () => {
215
- await result.current.loadSettings(mockUserId);
216
- });
217
-
218
- await act(async () => {
219
- await result.current.updateSettings({ theme: 'light' as const });
220
- });
221
-
222
- expect(result.current.loading).toBe(false);
223
- expect(result.current.error).toBe('Failed to update settings');
224
- });
225
- });
226
-
227
- describe('resetSettings', () => {
228
- it('resets settings to defaults', async () => {
229
- mockStorageRepository.getItem.mockResolvedValue({
230
- success: true,
231
- data: mockSettings,
232
- });
233
- mockStorageRepository.setItem.mockResolvedValue({
234
- success: true,
235
- });
236
-
237
- const { result } = renderHook(() => useSettingsStore());
238
-
239
- await act(async () => {
240
- await result.current.loadSettings(mockUserId);
241
- });
242
-
243
- await act(async () => {
244
- await result.current.resetSettings(mockUserId);
245
- });
246
-
247
- expect(result.current.settings?.theme).toBe('auto');
248
- expect(result.current.settings?.language).toBe('en-US');
249
- expect(result.current.settings?.notificationsEnabled).toBe(true);
250
- expect(mockStorageRepository.setItem).toHaveBeenCalledWith(
251
- `settings_${mockUserId}`,
252
- expect.objectContaining({
253
- theme: 'auto',
254
- language: 'en-US',
255
- })
256
- );
257
- });
258
- });
259
-
260
- describe('clearError', () => {
261
- it('clears error state', async () => {
262
- mockStorageRepository.getItem.mockRejectedValue(new Error('Test error'));
263
-
264
- const { result } = renderHook(() => useSettingsStore());
265
-
266
- await act(async () => {
267
- await result.current.loadSettings(mockUserId);
268
- });
269
-
270
- expect(result.current.error).toBe('Failed to load settings');
271
-
272
- act(() => {
273
- result.current.clearError();
274
- });
275
-
276
- expect(result.current.error).toBeNull();
277
- });
278
- });
279
- });
280
-
281
- describe('useSettings Hook', () => {
282
- it('provides all store methods and state', () => {
283
- const { result } = renderHook(() => useSettings());
284
-
285
- expect(result.current).toHaveProperty('settings');
286
- expect(result.current).toHaveProperty('loading');
287
- expect(result.current).toHaveProperty('error');
288
- expect(result.current).toHaveProperty('loadSettings');
289
- expect(result.current).toHaveProperty('updateSettings');
290
- expect(result.current).toHaveProperty('resetSettings');
291
- expect(result.current).toHaveProperty('clearError');
292
- });
293
-
294
- it('is a thin wrapper around useSettingsStore', () => {
295
- const storeResult = renderHook(() => useSettingsStore()).result;
296
- const hookResult = renderHook(() => useSettings()).result;
297
-
298
- expect(Object.keys(storeResult.current)).toEqual(
299
- expect.arrayContaining(Object.keys(hookResult.current))
300
- );
301
- });
302
- });
@@ -1,58 +0,0 @@
1
- /**
2
- * Cloud Sync Setting Component
3
- * Single Responsibility: Display cloud sync setting item
4
- */
5
-
6
- import React, { useCallback } from "react";
7
- import { SettingItem } from "./SettingItem";
8
- import type { SettingItemProps } from "./SettingItem";
9
-
10
- export interface CloudSyncSettingProps {
11
- title?: string;
12
- description?: string;
13
- isSyncing?: boolean;
14
- lastSynced?: Date | null;
15
- onPress?: () => void;
16
- iconColor?: string;
17
- titleColor?: string;
18
- }
19
-
20
- export const CloudSyncSetting: React.FC<CloudSyncSettingProps> = ({
21
- title,
22
- description,
23
- isSyncing = false,
24
- lastSynced,
25
- onPress,
26
- iconColor,
27
- titleColor,
28
- }) => {
29
- const formatLastSynced = useCallback((date: Date | null | undefined): string => {
30
- if (!date) return "never_synced";
31
- const now = new Date();
32
- const diff = now.getTime() - date.getTime();
33
- const minutes = Math.floor(diff / 60000);
34
- const hours = Math.floor(minutes / 60);
35
- const days = Math.floor(hours / 24);
36
-
37
- if (minutes < 1) return "just_now";
38
- if (minutes < 60) return `${minutes}m_ago`;
39
- if (hours < 24) return `${hours}h_ago`;
40
- if (days < 7) return `${days}d_ago`;
41
- return date.toLocaleDateString();
42
- }, []);
43
-
44
- const displayDescription = description || (isSyncing ? "syncing" : lastSynced ? `last_synced_${formatLastSynced(lastSynced)}` : "sync_to_cloud");
45
-
46
- return (
47
- <SettingItem
48
- icon="cloud-outline"
49
- title={title || "cloud_sync"}
50
- value={displayDescription}
51
- onPress={onPress}
52
- iconColor={iconColor}
53
- titleColor={titleColor}
54
- disabled={isSyncing}
55
- />
56
- );
57
- };
58
-