@umituz/react-native-settings 2.0.0 → 2.4.1

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 (160) hide show
  1. package/README.md +129 -3
  2. package/lib/__tests__/setup.d.ts +5 -0
  3. package/lib/__tests__/setup.d.ts.map +1 -0
  4. package/lib/__tests__/setup.js +143 -0
  5. package/lib/__tests__/setup.js.map +1 -0
  6. package/lib/domain/repositories/ISettingsRepository.d.ts +51 -0
  7. package/lib/domain/repositories/ISettingsRepository.d.ts.map +1 -0
  8. package/lib/domain/repositories/ISettingsRepository.js +8 -0
  9. package/lib/domain/repositories/ISettingsRepository.js.map +1 -0
  10. package/lib/index.d.ts +35 -0
  11. package/lib/index.d.ts.map +1 -0
  12. package/lib/index.js +32 -0
  13. package/lib/index.js.map +1 -0
  14. package/lib/infrastructure/storage/SettingsStore.d.ts +36 -0
  15. package/lib/infrastructure/storage/SettingsStore.d.ts.map +1 -0
  16. package/lib/infrastructure/storage/SettingsStore.js +144 -0
  17. package/lib/infrastructure/storage/SettingsStore.js.map +1 -0
  18. package/lib/presentation/components/CloudSyncSetting.d.ts +16 -0
  19. package/lib/presentation/components/CloudSyncSetting.d.ts.map +1 -0
  20. package/lib/presentation/components/CloudSyncSetting.js +30 -0
  21. package/lib/presentation/components/CloudSyncSetting.js.map +1 -0
  22. package/lib/presentation/components/DisclaimerCard.d.ts +15 -0
  23. package/lib/presentation/components/DisclaimerCard.d.ts.map +1 -0
  24. package/lib/presentation/components/DisclaimerCard.js +73 -0
  25. package/lib/presentation/components/DisclaimerCard.js.map +1 -0
  26. package/lib/presentation/components/DisclaimerModal.d.ts +13 -0
  27. package/lib/presentation/components/DisclaimerModal.d.ts.map +1 -0
  28. package/lib/presentation/components/DisclaimerModal.js +62 -0
  29. package/lib/presentation/components/DisclaimerModal.js.map +1 -0
  30. package/lib/presentation/components/DisclaimerSetting.d.ts +39 -0
  31. package/lib/presentation/components/DisclaimerSetting.d.ts.map +1 -0
  32. package/lib/presentation/components/DisclaimerSetting.js +59 -0
  33. package/lib/presentation/components/DisclaimerSetting.js.map +1 -0
  34. package/lib/presentation/components/SettingItem.d.ts +45 -0
  35. package/lib/presentation/components/SettingItem.d.ts.map +1 -0
  36. package/lib/presentation/components/SettingItem.js +113 -0
  37. package/lib/presentation/components/SettingItem.js.map +1 -0
  38. package/lib/presentation/components/SettingsErrorBoundary.d.ts +23 -0
  39. package/lib/presentation/components/SettingsErrorBoundary.d.ts.map +1 -0
  40. package/lib/presentation/components/SettingsErrorBoundary.js +73 -0
  41. package/lib/presentation/components/SettingsErrorBoundary.js.map +1 -0
  42. package/lib/presentation/components/SettingsFooter.d.ts +11 -0
  43. package/lib/presentation/components/SettingsFooter.d.ts.map +1 -0
  44. package/lib/presentation/components/SettingsFooter.js +31 -0
  45. package/lib/presentation/components/SettingsFooter.js.map +1 -0
  46. package/lib/presentation/components/SettingsSection.d.ts +13 -0
  47. package/lib/presentation/components/SettingsSection.d.ts.map +1 -0
  48. package/lib/presentation/components/SettingsSection.js +37 -0
  49. package/lib/presentation/components/SettingsSection.js.map +1 -0
  50. package/lib/presentation/components/StorageClearSetting.d.ts +16 -0
  51. package/lib/presentation/components/StorageClearSetting.d.ts.map +1 -0
  52. package/lib/presentation/components/StorageClearSetting.js +21 -0
  53. package/lib/presentation/components/StorageClearSetting.js.map +1 -0
  54. package/lib/presentation/components/UserProfileHeader.d.ts +30 -0
  55. package/lib/presentation/components/UserProfileHeader.d.ts.map +1 -0
  56. package/lib/presentation/components/UserProfileHeader.js +119 -0
  57. package/lib/presentation/components/UserProfileHeader.js.map +1 -0
  58. package/lib/presentation/screens/AppearanceScreen.d.ts +8 -0
  59. package/lib/presentation/screens/AppearanceScreen.d.ts.map +1 -0
  60. package/lib/presentation/screens/AppearanceScreen.js +8 -0
  61. package/lib/presentation/screens/AppearanceScreen.js.map +1 -0
  62. package/lib/presentation/screens/SettingsScreen.d.ts +38 -0
  63. package/lib/presentation/screens/SettingsScreen.d.ts.map +1 -0
  64. package/lib/presentation/screens/SettingsScreen.js +37 -0
  65. package/lib/presentation/screens/SettingsScreen.js.map +1 -0
  66. package/lib/presentation/screens/components/AboutLegalSection.d.ts +15 -0
  67. package/lib/presentation/screens/components/AboutLegalSection.d.ts.map +1 -0
  68. package/lib/presentation/screens/components/AboutLegalSection.js +28 -0
  69. package/lib/presentation/screens/components/AboutLegalSection.js.map +1 -0
  70. package/lib/presentation/screens/components/AppearanceSection.d.ts +12 -0
  71. package/lib/presentation/screens/components/AppearanceSection.d.ts.map +1 -0
  72. package/lib/presentation/screens/components/AppearanceSection.js +21 -0
  73. package/lib/presentation/screens/components/AppearanceSection.js.map +1 -0
  74. package/lib/presentation/screens/components/LanguageSection.d.ts +12 -0
  75. package/lib/presentation/screens/components/LanguageSection.d.ts.map +1 -0
  76. package/lib/presentation/screens/components/LanguageSection.js +26 -0
  77. package/lib/presentation/screens/components/LanguageSection.js.map +1 -0
  78. package/lib/presentation/screens/components/NotificationsSection.d.ts +12 -0
  79. package/lib/presentation/screens/components/NotificationsSection.d.ts.map +1 -0
  80. package/lib/presentation/screens/components/NotificationsSection.js +58 -0
  81. package/lib/presentation/screens/components/NotificationsSection.js.map +1 -0
  82. package/lib/presentation/screens/components/SettingsContent.d.ts +36 -0
  83. package/lib/presentation/screens/components/SettingsContent.d.ts.map +1 -0
  84. package/lib/presentation/screens/components/SettingsContent.js +81 -0
  85. package/lib/presentation/screens/components/SettingsContent.js.map +1 -0
  86. package/lib/presentation/screens/components/SettingsHeader.d.ts +12 -0
  87. package/lib/presentation/screens/components/SettingsHeader.d.ts.map +1 -0
  88. package/lib/presentation/screens/components/SettingsHeader.js +59 -0
  89. package/lib/presentation/screens/components/SettingsHeader.js.map +1 -0
  90. package/lib/presentation/screens/components/index.d.ts +9 -0
  91. package/lib/presentation/screens/components/index.d.ts.map +1 -0
  92. package/lib/presentation/screens/components/index.js +9 -0
  93. package/lib/presentation/screens/components/index.js.map +1 -0
  94. package/lib/presentation/screens/hooks/useFeatureDetection.d.ts +21 -0
  95. package/lib/presentation/screens/hooks/useFeatureDetection.d.ts.map +1 -0
  96. package/lib/presentation/screens/hooks/useFeatureDetection.js +82 -0
  97. package/lib/presentation/screens/hooks/useFeatureDetection.js.map +1 -0
  98. package/lib/presentation/screens/types/CustomSection.d.ts +19 -0
  99. package/lib/presentation/screens/types/CustomSection.d.ts.map +1 -0
  100. package/lib/presentation/screens/types/CustomSection.js +6 -0
  101. package/lib/presentation/screens/types/CustomSection.js.map +1 -0
  102. package/lib/presentation/screens/types/ExtendedConfig.d.ts +68 -0
  103. package/lib/presentation/screens/types/ExtendedConfig.d.ts.map +1 -0
  104. package/lib/presentation/screens/types/ExtendedConfig.js +6 -0
  105. package/lib/presentation/screens/types/ExtendedConfig.js.map +1 -0
  106. package/lib/presentation/screens/types/FeatureConfig.d.ts +95 -0
  107. package/lib/presentation/screens/types/FeatureConfig.d.ts.map +1 -0
  108. package/lib/presentation/screens/types/FeatureConfig.js +6 -0
  109. package/lib/presentation/screens/types/FeatureConfig.js.map +1 -0
  110. package/lib/presentation/screens/types/SettingsConfig.d.ts +97 -0
  111. package/lib/presentation/screens/types/SettingsConfig.d.ts.map +1 -0
  112. package/lib/presentation/screens/types/SettingsConfig.js +6 -0
  113. package/lib/presentation/screens/types/SettingsConfig.js.map +1 -0
  114. package/lib/presentation/screens/types/index.d.ts +10 -0
  115. package/lib/presentation/screens/types/index.d.ts.map +1 -0
  116. package/lib/presentation/screens/types/index.js +6 -0
  117. package/lib/presentation/screens/types/index.js.map +1 -0
  118. package/lib/presentation/screens/utils/normalizeConfig.d.ts +44 -0
  119. package/lib/presentation/screens/utils/normalizeConfig.d.ts.map +1 -0
  120. package/lib/presentation/screens/utils/normalizeConfig.js +38 -0
  121. package/lib/presentation/screens/utils/normalizeConfig.js.map +1 -0
  122. package/package.json +46 -11
  123. package/src/__tests__/integration.test.tsx +371 -0
  124. package/src/__tests__/performance.test.tsx +369 -0
  125. package/src/__tests__/setup.test.tsx +20 -0
  126. package/src/__tests__/setup.ts +157 -0
  127. package/src/index.ts +9 -0
  128. package/src/infrastructure/storage/SettingsStore.ts +90 -45
  129. package/src/infrastructure/storage/__tests__/SettingsStore.test.tsx +302 -0
  130. package/src/presentation/components/CloudSyncSetting.tsx +11 -17
  131. package/src/presentation/components/DisclaimerCard.tsx +115 -0
  132. package/src/presentation/components/DisclaimerModal.tsx +104 -0
  133. package/src/presentation/components/DisclaimerSetting.tsx +77 -159
  134. package/src/presentation/components/SettingItem.tsx +11 -2
  135. package/src/presentation/components/SettingsErrorBoundary.tsx +126 -0
  136. package/src/presentation/components/StorageClearSetting.tsx +13 -8
  137. package/src/presentation/components/UserProfileHeader.tsx +48 -11
  138. package/src/presentation/components/__tests__/CloudSyncSetting.test.tsx +78 -0
  139. package/src/presentation/components/__tests__/DisclaimerCard.test.tsx +208 -0
  140. package/src/presentation/components/__tests__/DisclaimerModal.test.tsx +236 -0
  141. package/src/presentation/components/__tests__/DisclaimerSetting.test.tsx +74 -0
  142. package/src/presentation/components/__tests__/SettingItem.test.tsx +189 -0
  143. package/src/presentation/components/__tests__/SettingsErrorBoundary.test.tsx +186 -0
  144. package/src/presentation/screens/SettingsScreen.tsx +29 -159
  145. package/src/presentation/screens/__tests__/SettingsScreen.test.tsx +322 -0
  146. package/src/presentation/screens/components/AboutLegalSection.tsx +14 -5
  147. package/src/presentation/screens/components/AppearanceSection.tsx +1 -1
  148. package/src/presentation/screens/components/LanguageSection.tsx +2 -1
  149. package/src/presentation/screens/components/NotificationsSection.tsx +19 -14
  150. package/src/presentation/screens/components/SettingsContent.tsx +167 -0
  151. package/src/presentation/screens/components/SettingsHeader.tsx +79 -0
  152. package/src/presentation/screens/hooks/__tests__/useFeatureDetection.test.tsx +261 -0
  153. package/src/presentation/screens/hooks/useFeatureDetection.ts +15 -5
  154. package/src/presentation/screens/types/CustomSection.ts +20 -0
  155. package/src/presentation/screens/types/ExtendedConfig.ts +68 -0
  156. package/src/presentation/screens/types/FeatureConfig.ts +102 -0
  157. package/src/presentation/screens/types/SettingsConfig.ts +116 -0
  158. package/src/presentation/screens/types/index.ts +20 -0
  159. package/src/presentation/screens/utils/normalizeConfig.ts +2 -1
  160. package/src/presentation/screens/types.ts +0 -263
@@ -29,18 +29,29 @@ interface SettingsStore {
29
29
 
30
30
  const DEFAULT_OFFLINE_USER_ID = 'offline_user';
31
31
 
32
- const getDefaultSettings = (userId: string): UserSettings => ({
33
- userId,
34
- theme: 'auto',
35
- language: 'en-US',
36
- notificationsEnabled: true,
37
- emailNotifications: true,
38
- pushNotifications: true,
39
- soundEnabled: true,
40
- vibrationEnabled: true,
41
- privacyMode: false,
42
- updatedAt: new Date(),
43
- });
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
+ updatedAt: new Date(),
50
+ };
51
+
52
+ DEFAULT_SETTINGS_CACHE.set(userId, settings);
53
+ return settings;
54
+ };
44
55
 
45
56
  export const useSettingsStore = create<SettingsStore>((set, get) => ({
46
57
  settings: null,
@@ -48,28 +59,47 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
48
59
  error: null,
49
60
 
50
61
  loadSettings: async (userId: string) => {
62
+ if (__DEV__) {
63
+ console.log('SettingsStore: Loading settings for user:', userId);
64
+ }
65
+
51
66
  set({ loading: true, error: null });
52
67
 
53
- const defaultSettings = getDefaultSettings(userId);
54
- const storageKey = createUserKey(StorageKey.SETTINGS, userId);
55
-
56
- // ✅ DRY: Storage domain handles JSON parse, error handling
57
- const result = await storageRepository.getItem<UserSettings>(storageKey, defaultSettings);
58
- const data = unwrap(result, defaultSettings);
59
-
60
- // ✅ CLEAN CODE: Auto-save defaults if not exists
61
- if (!result.success) {
62
- await storageRepository.setItem(storageKey, defaultSettings);
68
+ try {
69
+ const defaultSettings = getDefaultSettings(userId);
70
+ const storageKey = createUserKey(StorageKey.SETTINGS, userId);
71
+
72
+ // DRY: Storage domain handles JSON parse, error handling
73
+ const result = await storageRepository.getItem<UserSettings>(storageKey, defaultSettings);
74
+ const data = unwrap(result, defaultSettings);
75
+
76
+ // ✅ CLEAN CODE: Auto-save defaults if not exists
77
+ if (!result.success) {
78
+ await storageRepository.setItem(storageKey, defaultSettings);
79
+ }
80
+
81
+ set({
82
+ settings: data,
83
+ loading: false,
84
+ error: null,
85
+ });
86
+
87
+ if (__DEV__) {
88
+ console.log('SettingsStore: Settings loaded successfully');
89
+ }
90
+ } catch (error) {
91
+ if (__DEV__) {
92
+ console.error('SettingsStore: Failed to load settings:', error);
93
+ }
94
+ set({ loading: false, error: 'Failed to load settings' });
63
95
  }
64
-
65
- set({
66
- settings: data,
67
- loading: false,
68
- error: null,
69
- });
70
96
  },
71
97
 
72
98
  updateSettings: async (updates: Partial<UserSettings>) => {
99
+ if (__DEV__) {
100
+ console.log('SettingsStore: Updating settings with:', updates);
101
+ }
102
+
73
103
  const { settings } = get();
74
104
 
75
105
  // ✅ CLEAN CODE: Auto-initialize if settings not loaded
@@ -80,28 +110,43 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
80
110
  // ✅ DEFENSIVE: Verify settings loaded successfully
81
111
  const currentSettings = get().settings;
82
112
  if (!currentSettings) {
83
- set({ error: 'Failed to initialize settings' });
113
+ const errorMsg = 'Failed to initialize settings';
114
+ if (__DEV__) {
115
+ console.error('SettingsStore:', errorMsg);
116
+ }
117
+ set({ error: errorMsg });
84
118
  return;
85
119
  }
86
120
 
87
121
  set({ loading: true, error: null });
88
122
 
89
- const updatedSettings: UserSettings = {
90
- ...currentSettings,
91
- ...updates,
92
- updatedAt: new Date(),
93
- };
94
-
95
- const storageKey = createUserKey(StorageKey.SETTINGS, currentSettings.userId);
96
-
97
- // ✅ DRY: Storage domain replaces JSON.stringify + AsyncStorage + try/catch
98
- const result = await storageRepository.setItem(storageKey, updatedSettings);
99
-
100
- set({
101
- settings: result.success ? updatedSettings : currentSettings,
102
- loading: false,
103
- error: null,
104
- });
123
+ try {
124
+ const updatedSettings: UserSettings = {
125
+ ...currentSettings,
126
+ ...updates,
127
+ updatedAt: new Date(),
128
+ };
129
+
130
+ const storageKey = createUserKey(StorageKey.SETTINGS, currentSettings.userId);
131
+
132
+ // DRY: Storage domain replaces JSON.stringify + AsyncStorage + try/catch
133
+ const result = await storageRepository.setItem(storageKey, updatedSettings);
134
+
135
+ set({
136
+ settings: result.success ? updatedSettings : currentSettings,
137
+ loading: false,
138
+ error: null,
139
+ });
140
+
141
+ if (__DEV__) {
142
+ console.log('SettingsStore: Settings updated successfully');
143
+ }
144
+ } catch (error) {
145
+ if (__DEV__) {
146
+ console.error('SettingsStore: Failed to update settings:', error);
147
+ }
148
+ set({ loading: false, error: 'Failed to update settings' });
149
+ }
105
150
  },
106
151
 
107
152
  resetSettings: async (userId: string) => {
@@ -0,0 +1,302 @@
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
+ });
@@ -3,7 +3,7 @@
3
3
  * Single Responsibility: Display cloud sync setting item
4
4
  */
5
5
 
6
- import React from "react";
6
+ import React, { useCallback } from "react";
7
7
  import { Cloud } from "lucide-react-native";
8
8
  import { SettingItem } from "./SettingItem";
9
9
  import type { SettingItemProps } from "./SettingItem";
@@ -19,7 +19,7 @@ export interface CloudSyncSettingProps {
19
19
  }
20
20
 
21
21
  export const CloudSyncSetting: React.FC<CloudSyncSettingProps> = ({
22
- title = "Cloud Sync",
22
+ title,
23
23
  description,
24
24
  isSyncing = false,
25
25
  lastSynced,
@@ -27,33 +27,27 @@ export const CloudSyncSetting: React.FC<CloudSyncSettingProps> = ({
27
27
  iconColor,
28
28
  titleColor,
29
29
  }) => {
30
- const formatLastSynced = (date: Date | null | undefined): string => {
31
- if (!date) return "Never synced";
30
+ const formatLastSynced = useCallback((date: Date | null | undefined): string => {
31
+ if (!date) return "never_synced";
32
32
  const now = new Date();
33
33
  const diff = now.getTime() - date.getTime();
34
34
  const minutes = Math.floor(diff / 60000);
35
35
  const hours = Math.floor(minutes / 60);
36
36
  const days = Math.floor(hours / 24);
37
37
 
38
- if (minutes < 1) return "Just now";
39
- if (minutes < 60) return `${minutes}m ago`;
40
- if (hours < 24) return `${hours}h ago`;
41
- if (days < 7) return `${days}d ago`;
38
+ if (minutes < 1) return "just_now";
39
+ if (minutes < 60) return `${minutes}m_ago`;
40
+ if (hours < 24) return `${hours}h_ago`;
41
+ if (days < 7) return `${days}d_ago`;
42
42
  return date.toLocaleDateString();
43
- };
43
+ }, []);
44
44
 
45
- const displayDescription =
46
- description ||
47
- (isSyncing
48
- ? "Syncing..."
49
- : lastSynced
50
- ? `Last synced: ${formatLastSynced(lastSynced)}`
51
- : "Sync your data to the cloud");
45
+ const displayDescription = description || (isSyncing ? "syncing" : lastSynced ? `last_synced_${formatLastSynced(lastSynced)}` : "sync_to_cloud");
52
46
 
53
47
  return (
54
48
  <SettingItem
55
49
  icon={Cloud}
56
- title={title}
50
+ title={title || "cloud_sync"}
57
51
  value={displayDescription}
58
52
  onPress={onPress}
59
53
  iconColor={iconColor}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Disclaimer Card Component
3
+ * Extracted from DisclaimerSetting to follow single responsibility and 200-line rules
4
+ */
5
+
6
+ import React from 'react';
7
+ import {
8
+ View,
9
+ StyleSheet,
10
+ TouchableOpacity,
11
+ } from 'react-native';
12
+
13
+ import { useAppDesignTokens, withAlpha } from '@umituz/react-native-design-system-theme';
14
+ import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system-atoms';
15
+
16
+ export interface DisclaimerCardProps {
17
+ title: string;
18
+ shortMessage: string;
19
+ iconName: string;
20
+ iconColor: string;
21
+ backgroundColor: string;
22
+ onPress: () => void;
23
+ }
24
+
25
+ export const DisclaimerCard: React.FC<DisclaimerCardProps> = ({
26
+ title,
27
+ shortMessage,
28
+ iconName,
29
+ iconColor,
30
+ backgroundColor,
31
+ onPress,
32
+ }) => {
33
+ const tokens = useAppDesignTokens();
34
+ const styles = getStyles(tokens);
35
+
36
+ return (
37
+ <TouchableOpacity
38
+ style={[
39
+ styles.container,
40
+ { backgroundColor },
41
+ ]}
42
+ onPress={onPress}
43
+ activeOpacity={0.7}
44
+ testID="disclaimer-setting"
45
+ >
46
+ {/* Icon and Title Row */}
47
+ <View style={styles.headerRow}>
48
+ <View
49
+ style={[
50
+ styles.iconContainer,
51
+ {
52
+ backgroundColor: withAlpha(iconColor, 0.2),
53
+ borderColor: withAlpha(iconColor, 0.4),
54
+ borderWidth: 1,
55
+ },
56
+ ]}
57
+ >
58
+ <AtomicIcon name={iconName} color="warning" />
59
+ </View>
60
+ <AtomicText type="bodyLarge" color="primary" style={styles.title}>
61
+ {title}
62
+ </AtomicText>
63
+ <AtomicIcon name="ArrowRight" color="secondary" size="sm" />
64
+ </View>
65
+
66
+ {/* Short Message */}
67
+ <AtomicText
68
+ type="bodySmall"
69
+ color="secondary"
70
+ style={styles.shortMessage}
71
+ >
72
+ {shortMessage}
73
+ </AtomicText>
74
+ </TouchableOpacity>
75
+ );
76
+ };
77
+
78
+ const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
79
+ StyleSheet.create({
80
+ container: {
81
+ paddingHorizontal: tokens.spacing.md,
82
+ paddingVertical: tokens.spacing.md,
83
+ marginHorizontal: tokens.spacing.md,
84
+ marginTop: 8,
85
+ marginBottom: 8,
86
+ borderRadius: 12,
87
+ },
88
+
89
+ headerRow: {
90
+ flexDirection: 'row',
91
+ alignItems: 'center',
92
+ marginBottom: 12,
93
+ },
94
+
95
+ iconContainer: {
96
+ width: 40,
97
+ height: 40,
98
+ borderRadius: 20,
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ marginRight: 12,
102
+ },
103
+
104
+ title: {
105
+ flex: 1,
106
+ fontWeight: tokens.typography.labelLarge.fontWeight as any,
107
+ fontSize: tokens.typography.labelLarge.fontSize,
108
+ },
109
+
110
+ shortMessage: {
111
+ lineHeight: 18,
112
+ paddingLeft: 52, // Align with title (40px icon + 12px margin)
113
+ fontSize: 13,
114
+ },
115
+ });