@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.
- package/package.json +15 -6
- package/src/domains/about/presentation/components/AboutSection.tsx +14 -71
- package/src/domains/appearance/application/ports/IAppearanceRepository.ts +8 -0
- package/src/domains/appearance/hooks/index.ts +1 -1
- package/src/domains/appearance/hooks/useAppearance.ts +18 -58
- package/src/domains/appearance/hooks/useAppearanceActions.ts +20 -128
- package/src/domains/appearance/infrastructure/repositories/AppearanceRepository.ts +34 -0
- package/src/domains/appearance/infrastructure/services/AppearanceService.ts +51 -0
- package/src/domains/appearance/presentation/components/AppearanceSection.tsx +2 -2
- package/src/domains/appearance/presentation/hooks/mutations/useAppearanceMutations.ts +36 -0
- package/src/domains/appearance/presentation/hooks/queries/useAppearanceQuery.ts +15 -0
- package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +37 -40
- package/src/domains/faqs/presentation/components/FAQSection.tsx +1 -1
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +11 -15
- package/src/domains/feedback/presentation/components/SupportSection.tsx +2 -2
- package/src/domains/legal/presentation/components/LegalItem.tsx +13 -129
- package/src/index.ts +15 -9
- package/src/infrastructure/repositories/SettingsRepository.ts +105 -0
- package/src/infrastructure/services/SettingsService.ts +47 -0
- package/src/presentation/components/SettingItem.tsx +77 -129
- package/src/presentation/components/SettingsFooter.tsx +9 -25
- package/src/presentation/components/SettingsSection.tsx +9 -20
- package/src/presentation/hooks/mutations/useSettingsMutations.ts +58 -0
- package/src/presentation/hooks/queries/useSettingsQuery.ts +27 -0
- package/src/presentation/hooks/useSettings.ts +45 -0
- package/src/presentation/screens/components/SettingsContent.tsx +20 -247
- package/src/presentation/screens/components/sections/CustomSettingsList.tsx +31 -0
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +55 -0
- package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +43 -0
- package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +47 -0
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +84 -0
- package/src/presentation/screens/hooks/useFeatureDetection.ts +1 -16
- package/src/presentation/screens/types/FeatureConfig.ts +18 -0
- package/src/presentation/screens/types/SettingsConfig.ts +7 -0
- package/src/domains/appearance/infrastructure/services/appearanceService.ts +0 -301
- package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +0 -120
- package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +0 -132
- package/src/infrastructure/storage/SettingsStore.ts +0 -189
- package/src/infrastructure/storage/__tests__/SettingsStore.test.tsx +0 -302
- package/src/presentation/components/CloudSyncSetting.tsx +0 -58
- /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
|
-
|
|
File without changes
|