@umituz/react-native-settings 1.1.0
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/LICENSE +22 -0
- package/README.md +194 -0
- package/package.json +55 -0
- package/src/domain/repositories/ISettingsRepository.ts +58 -0
- package/src/index.ts +45 -0
- package/src/infrastructure/storage/SettingsStore.ts +143 -0
- package/src/presentation/components/DisclaimerSetting.tsx +196 -0
- package/src/presentation/components/SettingItem.tsx +176 -0
- package/src/presentation/screens/AppearanceScreen.tsx +98 -0
- package/src/presentation/screens/LanguageSelectionScreen.tsx +203 -0
- package/src/presentation/screens/SettingsScreen.tsx +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ümit UZ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @umituz/react-native-settings
|
|
2
|
+
|
|
3
|
+
Settings management for React Native apps - user preferences, theme, language, notifications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **User Settings Management** - Theme, language, notifications, privacy settings
|
|
8
|
+
- ✅ **Zustand State Management** - Global settings state with Zustand
|
|
9
|
+
- ✅ **Persistent Storage** - Uses @umituz/react-native-storage for persistence
|
|
10
|
+
- ✅ **Settings Screens** - Pre-built settings screens (Settings, Appearance, Language Selection)
|
|
11
|
+
- ✅ **Setting Components** - Reusable setting item components
|
|
12
|
+
- ✅ **Type-Safe** - Full TypeScript support
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @umituz/react-native-settings
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Peer Dependencies
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install zustand @umituz/react-native-storage @umituz/react-native-design-system @umituz/react-native-design-system-theme @umituz/react-native-localization @umituz/react-native-notifications react-native-paper expo-linear-gradient
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Basic Settings Hook
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { useSettings } from '@umituz/react-native-settings';
|
|
32
|
+
|
|
33
|
+
const MyComponent = () => {
|
|
34
|
+
const { settings, loading, updateSettings, loadSettings } = useSettings();
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
loadSettings('user123');
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const handleThemeChange = async () => {
|
|
41
|
+
await updateSettings({ theme: 'dark' });
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View>
|
|
46
|
+
<Text>Current Theme: {settings?.theme}</Text>
|
|
47
|
+
<Button onPress={handleThemeChange}>Toggle Theme</Button>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Settings Screen
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { SettingsScreen } from '@umituz/react-native-settings';
|
|
57
|
+
|
|
58
|
+
// In your navigation stack
|
|
59
|
+
<Stack.Screen name="Settings" component={SettingsScreen} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Appearance Screen
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { AppearanceScreen } from '@umituz/react-native-settings';
|
|
66
|
+
|
|
67
|
+
// In your navigation stack
|
|
68
|
+
<Stack.Screen name="Appearance" component={AppearanceScreen} />
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Language Selection Screen
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { LanguageSelectionScreen } from '@umituz/react-native-settings';
|
|
75
|
+
|
|
76
|
+
// In your navigation stack
|
|
77
|
+
<Stack.Screen name="LanguageSelection" component={LanguageSelectionScreen} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Setting Item Component
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { SettingItem } from '@umituz/react-native-settings';
|
|
84
|
+
|
|
85
|
+
<SettingItem
|
|
86
|
+
icon="Palette"
|
|
87
|
+
title="Theme"
|
|
88
|
+
description="Change app theme"
|
|
89
|
+
value="Dark"
|
|
90
|
+
onPress={() => navigation.navigate('Appearance')}
|
|
91
|
+
iconGradient={['#FF6B6B', '#4ECDC4']}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Disclaimer Setting Component
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { DisclaimerSetting } from '@umituz/react-native-settings';
|
|
99
|
+
|
|
100
|
+
// In AboutScreen
|
|
101
|
+
<DisclaimerSetting />
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API Reference
|
|
105
|
+
|
|
106
|
+
### `useSettings()`
|
|
107
|
+
|
|
108
|
+
React hook for accessing settings state.
|
|
109
|
+
|
|
110
|
+
**Returns:**
|
|
111
|
+
- `settings: UserSettings | null` - Current settings
|
|
112
|
+
- `loading: boolean` - Loading state
|
|
113
|
+
- `error: string | null` - Error message
|
|
114
|
+
- `loadSettings(userId: string)` - Load settings for user
|
|
115
|
+
- `updateSettings(updates: Partial<UserSettings>)` - Update settings
|
|
116
|
+
- `resetSettings(userId: string)` - Reset to default settings
|
|
117
|
+
- `clearError()` - Clear error state
|
|
118
|
+
|
|
119
|
+
### `useSettingsStore()`
|
|
120
|
+
|
|
121
|
+
Direct access to Zustand store.
|
|
122
|
+
|
|
123
|
+
### `SettingsScreen`
|
|
124
|
+
|
|
125
|
+
Main settings screen component with sections for Appearance, General, About & Legal.
|
|
126
|
+
|
|
127
|
+
### `AppearanceScreen`
|
|
128
|
+
|
|
129
|
+
Appearance settings screen with language and theme controls.
|
|
130
|
+
|
|
131
|
+
### `LanguageSelectionScreen`
|
|
132
|
+
|
|
133
|
+
Language selection screen with search functionality.
|
|
134
|
+
|
|
135
|
+
### `SettingItem`
|
|
136
|
+
|
|
137
|
+
Reusable setting item component with gradient icons and Material Design styling.
|
|
138
|
+
|
|
139
|
+
**Props:**
|
|
140
|
+
- `icon: IconName` - Icon name from Lucide library
|
|
141
|
+
- `title: string` - Main title text
|
|
142
|
+
- `description?: string` - Optional description
|
|
143
|
+
- `value?: string` - Optional value to display on right
|
|
144
|
+
- `onPress?: () => void` - Callback when pressed
|
|
145
|
+
- `showChevron?: boolean` - Show chevron arrow (default: true if onPress exists)
|
|
146
|
+
- `rightElement?: React.ReactNode` - Custom right element
|
|
147
|
+
- `iconGradient?: string[]` - Gradient colors for icon background
|
|
148
|
+
- `disabled?: boolean` - Disable item
|
|
149
|
+
- `testID?: string` - Test ID
|
|
150
|
+
|
|
151
|
+
### `DisclaimerSetting`
|
|
152
|
+
|
|
153
|
+
Disclaimer component for health/wellness apps with modal display.
|
|
154
|
+
|
|
155
|
+
## Types
|
|
156
|
+
|
|
157
|
+
- `UserSettings` - User settings interface
|
|
158
|
+
- `SettingsError` - Settings error interface
|
|
159
|
+
- `SettingsResult<T>` - Result type for settings operations
|
|
160
|
+
- `ISettingsRepository` - Repository interface
|
|
161
|
+
|
|
162
|
+
## UserSettings Interface
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
interface UserSettings {
|
|
166
|
+
userId: string;
|
|
167
|
+
theme: 'light' | 'dark' | 'auto';
|
|
168
|
+
language: string;
|
|
169
|
+
notificationsEnabled: boolean;
|
|
170
|
+
emailNotifications: boolean;
|
|
171
|
+
pushNotifications: boolean;
|
|
172
|
+
soundEnabled: boolean;
|
|
173
|
+
vibrationEnabled: boolean;
|
|
174
|
+
privacyMode: boolean;
|
|
175
|
+
updatedAt: Date;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Important Notes
|
|
180
|
+
|
|
181
|
+
⚠️ **Storage**: This package uses `@umituz/react-native-storage` for all storage operations. Make sure to install it as a peer dependency.
|
|
182
|
+
|
|
183
|
+
⚠️ **Navigation**: Settings screens require navigation setup. Make sure to add them to your navigation stack.
|
|
184
|
+
|
|
185
|
+
⚠️ **Translations**: Settings screens require i18n translations. Make sure to provide translations for settings keys.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
190
|
+
|
|
191
|
+
## Author
|
|
192
|
+
|
|
193
|
+
Ümit UZ <umit@umituz.com>
|
|
194
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/react-native-settings",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Settings management for React Native apps - user preferences, theme, language, notifications",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"lint": "tsc --noEmit",
|
|
10
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
11
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react-native",
|
|
15
|
+
"settings",
|
|
16
|
+
"preferences",
|
|
17
|
+
"user-settings",
|
|
18
|
+
"theme",
|
|
19
|
+
"language",
|
|
20
|
+
"notifications"
|
|
21
|
+
],
|
|
22
|
+
"author": "Ümit UZ <umit@umituz.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/umituz/react-native-settings"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18.2.0",
|
|
30
|
+
"react-native": ">=0.74.0",
|
|
31
|
+
"zustand": "^5.0.2",
|
|
32
|
+
"@umituz/react-native-storage": "latest",
|
|
33
|
+
"@umituz/react-native-design-system": "latest",
|
|
34
|
+
"@umituz/react-native-design-system-theme": "latest",
|
|
35
|
+
"@umituz/react-native-localization": "latest",
|
|
36
|
+
"@umituz/react-native-notifications": "latest",
|
|
37
|
+
"react-native-paper": "^5.12.3",
|
|
38
|
+
"expo-linear-gradient": "~14.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^18.2.45",
|
|
42
|
+
"@types/react-native": "^0.73.0",
|
|
43
|
+
"react": "^18.2.0",
|
|
44
|
+
"react-native": "^0.74.0",
|
|
45
|
+
"typescript": "^5.3.3"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"src",
|
|
52
|
+
"README.md",
|
|
53
|
+
"LICENSE"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Repository Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines contracts for settings persistence and retrieval
|
|
5
|
+
* Pure business logic - no dependencies on external frameworks
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface UserSettings {
|
|
9
|
+
userId: string;
|
|
10
|
+
theme: 'light' | 'dark' | 'auto';
|
|
11
|
+
language: string;
|
|
12
|
+
notificationsEnabled: boolean;
|
|
13
|
+
emailNotifications: boolean;
|
|
14
|
+
pushNotifications: boolean;
|
|
15
|
+
soundEnabled: boolean;
|
|
16
|
+
vibrationEnabled: boolean;
|
|
17
|
+
privacyMode: boolean;
|
|
18
|
+
updatedAt: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SettingsError extends Error {
|
|
22
|
+
code: 'LOAD_FAILED' | 'SAVE_FAILED' | 'NOT_FOUND' | 'INVALID_DATA';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SettingsResult<T> = {
|
|
26
|
+
success: true;
|
|
27
|
+
data: T;
|
|
28
|
+
} | {
|
|
29
|
+
success: false;
|
|
30
|
+
error: SettingsError;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Settings Repository Interface
|
|
35
|
+
* Repository pattern for settings management
|
|
36
|
+
*/
|
|
37
|
+
export interface ISettingsRepository {
|
|
38
|
+
/**
|
|
39
|
+
* Load user settings from persistent storage
|
|
40
|
+
*/
|
|
41
|
+
loadSettings(userId: string): Promise<SettingsResult<UserSettings>>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Save user settings to persistent storage
|
|
45
|
+
*/
|
|
46
|
+
saveSettings(settings: Partial<UserSettings>): Promise<SettingsResult<UserSettings>>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reset settings to default values
|
|
50
|
+
*/
|
|
51
|
+
resetSettings(userId: string): Promise<SettingsResult<UserSettings>>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get default settings for a user
|
|
55
|
+
*/
|
|
56
|
+
getDefaultSettings(userId: string): UserSettings;
|
|
57
|
+
}
|
|
58
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-settings - Public API
|
|
3
|
+
*
|
|
4
|
+
* Settings management for React Native apps
|
|
5
|
+
* User preferences, theme, language, notifications
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { useSettings, useSettingsStore, SettingsScreen, AppearanceScreen, LanguageSelectionScreen, SettingItem, DisclaimerSetting } from '@umituz/react-native-settings';
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// DOMAIN LAYER - Repository Interfaces
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
ISettingsRepository,
|
|
17
|
+
UserSettings,
|
|
18
|
+
SettingsError,
|
|
19
|
+
SettingsResult,
|
|
20
|
+
} from './domain/repositories/ISettingsRepository';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// INFRASTRUCTURE LAYER - Storage
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
useSettingsStore,
|
|
28
|
+
useSettings,
|
|
29
|
+
} from './infrastructure/storage/SettingsStore';
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// PRESENTATION LAYER - Screens
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
export { SettingsScreen } from './presentation/screens/SettingsScreen';
|
|
36
|
+
export { AppearanceScreen } from './presentation/screens/AppearanceScreen';
|
|
37
|
+
export { LanguageSelectionScreen } from './presentation/screens/LanguageSelectionScreen';
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// PRESENTATION LAYER - Components
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
export { SettingItem } from './presentation/components/SettingItem';
|
|
44
|
+
export { DisclaimerSetting } from './presentation/components/DisclaimerSetting';
|
|
45
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
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 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
|
+
});
|
|
44
|
+
|
|
45
|
+
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
46
|
+
settings: null,
|
|
47
|
+
loading: false,
|
|
48
|
+
error: null,
|
|
49
|
+
|
|
50
|
+
loadSettings: async (userId: string) => {
|
|
51
|
+
set({ loading: true, error: null });
|
|
52
|
+
|
|
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);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
set({
|
|
66
|
+
settings: data,
|
|
67
|
+
loading: false,
|
|
68
|
+
error: null,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
updateSettings: async (updates: Partial<UserSettings>) => {
|
|
73
|
+
const { settings } = get();
|
|
74
|
+
|
|
75
|
+
// ✅ CLEAN CODE: Auto-initialize if settings not loaded
|
|
76
|
+
if (!settings) {
|
|
77
|
+
await get().loadSettings(DEFAULT_OFFLINE_USER_ID);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ✅ DEFENSIVE: Verify settings loaded successfully
|
|
81
|
+
const currentSettings = get().settings;
|
|
82
|
+
if (!currentSettings) {
|
|
83
|
+
set({ error: 'Failed to initialize settings' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set({ loading: true, error: null });
|
|
88
|
+
|
|
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
|
+
});
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
resetSettings: async (userId: string) => {
|
|
108
|
+
set({ loading: true, error: null });
|
|
109
|
+
|
|
110
|
+
const defaultSettings = getDefaultSettings(userId);
|
|
111
|
+
const storageKey = createUserKey(StorageKey.SETTINGS, userId);
|
|
112
|
+
|
|
113
|
+
// ✅ DRY: Storage domain replaces JSON.stringify + AsyncStorage + try/catch
|
|
114
|
+
const result = await storageRepository.setItem(storageKey, defaultSettings);
|
|
115
|
+
|
|
116
|
+
set({
|
|
117
|
+
settings: result.success ? defaultSettings : get().settings,
|
|
118
|
+
loading: false,
|
|
119
|
+
error: null,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
clearError: () => set({ error: null }),
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Hook for accessing settings state
|
|
128
|
+
*/
|
|
129
|
+
export const useSettings = () => {
|
|
130
|
+
const { settings, loading, error, loadSettings, updateSettings, resetSettings, clearError } =
|
|
131
|
+
useSettingsStore();
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
settings,
|
|
135
|
+
loading,
|
|
136
|
+
error,
|
|
137
|
+
loadSettings,
|
|
138
|
+
updateSettings,
|
|
139
|
+
resetSettings,
|
|
140
|
+
clearError,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DisclaimerSetting Component
|
|
3
|
+
*
|
|
4
|
+
* Displays health/wellness app disclaimer with important legal notice
|
|
5
|
+
* Used in About screens for apps that require medical/health disclaimers
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Tappable card that opens full disclaimer modal
|
|
9
|
+
* - Warning icon with background color
|
|
10
|
+
* - Internationalized title and message
|
|
11
|
+
* - Full-screen modal with scrollable content
|
|
12
|
+
* - NO shadows (CLAUDE.md compliance)
|
|
13
|
+
* - Universal across iOS, Android, Web (NO Platform.OS checks)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* - Import and use in AboutScreen
|
|
17
|
+
* - Requires translations: settings.disclaimer.title, settings.disclaimer.message, settings.disclaimer.shortMessage
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React, { useState } from 'react';
|
|
21
|
+
import {
|
|
22
|
+
View,
|
|
23
|
+
StyleSheet,
|
|
24
|
+
TouchableOpacity,
|
|
25
|
+
Modal,
|
|
26
|
+
ScrollView,
|
|
27
|
+
} from 'react-native';
|
|
28
|
+
|
|
29
|
+
import { useAppDesignTokens, withAlpha } from '@umituz/react-native-design-system-theme';
|
|
30
|
+
import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
31
|
+
import { useLocalization } from '@umituz/react-native-localization';
|
|
32
|
+
|
|
33
|
+
type DesignTokens = ReturnType<typeof useAppDesignTokens>;
|
|
34
|
+
|
|
35
|
+
export const DisclaimerSetting: React.FC = () => {
|
|
36
|
+
const { t } = useLocalization();
|
|
37
|
+
const tokens = useAppDesignTokens();
|
|
38
|
+
const styles = getStyles(tokens);
|
|
39
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<TouchableOpacity
|
|
44
|
+
style={[
|
|
45
|
+
styles.container,
|
|
46
|
+
{ backgroundColor: withAlpha(tokens.colors.warning, 0.1) },
|
|
47
|
+
]}
|
|
48
|
+
onPress={() => setModalVisible(true)}
|
|
49
|
+
activeOpacity={0.7}
|
|
50
|
+
testID="disclaimer-setting"
|
|
51
|
+
>
|
|
52
|
+
{/* Icon and Title Row */}
|
|
53
|
+
<View style={styles.headerRow}>
|
|
54
|
+
<View
|
|
55
|
+
style={[
|
|
56
|
+
styles.iconContainer,
|
|
57
|
+
{
|
|
58
|
+
backgroundColor: withAlpha(tokens.colors.warning, 0.2),
|
|
59
|
+
borderColor: withAlpha(tokens.colors.warning, 0.4),
|
|
60
|
+
borderWidth: 1,
|
|
61
|
+
},
|
|
62
|
+
]}
|
|
63
|
+
>
|
|
64
|
+
<AtomicIcon name="AlertTriangle" color="warning" />
|
|
65
|
+
</View>
|
|
66
|
+
<AtomicText type="bodyLarge" color="primary" style={styles.title}>
|
|
67
|
+
{t('settings.disclaimer.title')}
|
|
68
|
+
</AtomicText>
|
|
69
|
+
<AtomicIcon name="ArrowRight" color="secondary" size="sm" />
|
|
70
|
+
</View>
|
|
71
|
+
|
|
72
|
+
{/* Short Message */}
|
|
73
|
+
<AtomicText
|
|
74
|
+
type="bodySmall"
|
|
75
|
+
color="secondary"
|
|
76
|
+
style={styles.shortMessage}
|
|
77
|
+
>
|
|
78
|
+
{t('settings.disclaimer.shortMessage')}
|
|
79
|
+
</AtomicText>
|
|
80
|
+
</TouchableOpacity>
|
|
81
|
+
|
|
82
|
+
{/* Full Disclaimer Modal */}
|
|
83
|
+
<Modal
|
|
84
|
+
visible={modalVisible}
|
|
85
|
+
animationType="slide"
|
|
86
|
+
presentationStyle="pageSheet"
|
|
87
|
+
onRequestClose={() => setModalVisible(false)}
|
|
88
|
+
>
|
|
89
|
+
<View
|
|
90
|
+
style={[
|
|
91
|
+
styles.modalContainer,
|
|
92
|
+
{ backgroundColor: tokens.colors.backgroundPrimary },
|
|
93
|
+
]}
|
|
94
|
+
>
|
|
95
|
+
{/* Modal Header */}
|
|
96
|
+
<View
|
|
97
|
+
style={[
|
|
98
|
+
styles.modalHeader,
|
|
99
|
+
{ borderBottomColor: tokens.colors.borderLight },
|
|
100
|
+
]}
|
|
101
|
+
>
|
|
102
|
+
<AtomicText type="headlineMedium" color="primary">
|
|
103
|
+
{t('settings.disclaimer.title')}
|
|
104
|
+
</AtomicText>
|
|
105
|
+
<TouchableOpacity
|
|
106
|
+
onPress={() => setModalVisible(false)}
|
|
107
|
+
testID="close-disclaimer-modal"
|
|
108
|
+
>
|
|
109
|
+
<AtomicIcon name="X" color="primary" size="md" />
|
|
110
|
+
</TouchableOpacity>
|
|
111
|
+
</View>
|
|
112
|
+
|
|
113
|
+
{/* Scrollable Content */}
|
|
114
|
+
<ScrollView
|
|
115
|
+
style={styles.modalContent}
|
|
116
|
+
contentContainerStyle={styles.modalContentContainer}
|
|
117
|
+
>
|
|
118
|
+
<AtomicText
|
|
119
|
+
type="bodyMedium"
|
|
120
|
+
color="primary"
|
|
121
|
+
style={styles.modalText}
|
|
122
|
+
>
|
|
123
|
+
{t('settings.disclaimer.message')}
|
|
124
|
+
</AtomicText>
|
|
125
|
+
</ScrollView>
|
|
126
|
+
</View>
|
|
127
|
+
</Modal>
|
|
128
|
+
</>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const getStyles = (tokens: DesignTokens) =>
|
|
133
|
+
StyleSheet.create({
|
|
134
|
+
container: {
|
|
135
|
+
paddingHorizontal: tokens.spacing.md,
|
|
136
|
+
paddingVertical: tokens.spacing.md,
|
|
137
|
+
marginHorizontal: tokens.spacing.md,
|
|
138
|
+
marginTop: 8,
|
|
139
|
+
marginBottom: 8,
|
|
140
|
+
borderRadius: 12,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
headerRow: {
|
|
144
|
+
flexDirection: 'row',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
marginBottom: 12,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
iconContainer: {
|
|
150
|
+
width: 40,
|
|
151
|
+
height: 40,
|
|
152
|
+
borderRadius: 20,
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
justifyContent: 'center',
|
|
155
|
+
marginRight: 12,
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
title: {
|
|
159
|
+
flex: 1,
|
|
160
|
+
fontWeight: tokens.typography.labelLarge.fontWeight as any,
|
|
161
|
+
fontSize: tokens.typography.labelLarge.fontSize,
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
shortMessage: {
|
|
165
|
+
lineHeight: 18,
|
|
166
|
+
paddingLeft: 52, // Align with title (40px icon + 12px margin)
|
|
167
|
+
fontSize: 13,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
modalContainer: {
|
|
171
|
+
flex: 1,
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
modalHeader: {
|
|
175
|
+
flexDirection: 'row',
|
|
176
|
+
justifyContent: 'space-between',
|
|
177
|
+
alignItems: 'center',
|
|
178
|
+
paddingHorizontal: 20,
|
|
179
|
+
paddingVertical: 16,
|
|
180
|
+
borderBottomWidth: 1,
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
modalContent: {
|
|
184
|
+
flex: 1,
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
modalContentContainer: {
|
|
188
|
+
padding: 20,
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
modalText: {
|
|
192
|
+
lineHeight: 24,
|
|
193
|
+
fontSize: 15,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SettingItem Component - Paper List.Item Wrapper with Custom Styling
|
|
3
|
+
*
|
|
4
|
+
* Modern settings item built on React Native Paper:
|
|
5
|
+
* - Wraps Paper List.Item for Material Design compliance
|
|
6
|
+
* - Custom gradient icon backgrounds (LinearGradient)
|
|
7
|
+
* - Lucide icons integration (AtomicIcon)
|
|
8
|
+
* - Automatic theme-aware styling
|
|
9
|
+
* - Built-in ripple effects
|
|
10
|
+
* - Accessibility support
|
|
11
|
+
* - Fixed title truncation with proper layout constraints
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { View, StyleSheet } from 'react-native';
|
|
16
|
+
import { List } from 'react-native-paper';
|
|
17
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
18
|
+
import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
19
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
20
|
+
import type { IconName } from '@umituz/react-native-design-system';
|
|
21
|
+
import type { DesignTokens } from '@umituz/react-native-design-system';
|
|
22
|
+
|
|
23
|
+
interface SettingItemProps {
|
|
24
|
+
/** Icon name from Lucide library */
|
|
25
|
+
icon: IconName;
|
|
26
|
+
/** Main title text */
|
|
27
|
+
title: string;
|
|
28
|
+
/** Optional description text */
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Optional value to display on the right */
|
|
31
|
+
value?: string;
|
|
32
|
+
/** Callback when pressed */
|
|
33
|
+
onPress?: () => void;
|
|
34
|
+
/** Show chevron arrow on right (default: true if onPress exists) */
|
|
35
|
+
showChevron?: boolean;
|
|
36
|
+
/** Right element to display instead of chevron/value */
|
|
37
|
+
rightElement?: React.ReactNode;
|
|
38
|
+
/** Gradient colors for icon background */
|
|
39
|
+
iconGradient?: string[];
|
|
40
|
+
/** Make item look disabled */
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Test ID for E2E testing */
|
|
43
|
+
testID?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const SettingItem: React.FC<SettingItemProps> = ({
|
|
47
|
+
icon,
|
|
48
|
+
title,
|
|
49
|
+
description,
|
|
50
|
+
value,
|
|
51
|
+
onPress,
|
|
52
|
+
showChevron,
|
|
53
|
+
rightElement,
|
|
54
|
+
iconGradient,
|
|
55
|
+
disabled = false,
|
|
56
|
+
testID,
|
|
57
|
+
}) => {
|
|
58
|
+
const tokens = useAppDesignTokens();
|
|
59
|
+
const styles = getStyles(tokens);
|
|
60
|
+
|
|
61
|
+
// Gradient colors for icon background
|
|
62
|
+
const gradientColors: readonly [string, string, ...string[]] = (iconGradient && iconGradient.length >= 2)
|
|
63
|
+
? (iconGradient as unknown as readonly [string, string, ...string[]])
|
|
64
|
+
: [tokens.colors.surface, tokens.colors.surface] as const;
|
|
65
|
+
|
|
66
|
+
// Render gradient icon container for left prop
|
|
67
|
+
const renderLeft = (props: any) => (
|
|
68
|
+
<View style={styles.leftContainer}>
|
|
69
|
+
<LinearGradient
|
|
70
|
+
colors={gradientColors}
|
|
71
|
+
start={{ x: 0, y: 0 }}
|
|
72
|
+
end={{ x: 1, y: 1 }}
|
|
73
|
+
style={styles.iconContainer}
|
|
74
|
+
>
|
|
75
|
+
<AtomicIcon name={icon} size="md" color="primary" />
|
|
76
|
+
</LinearGradient>
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Render right side content for right prop
|
|
81
|
+
const renderRight = (props: any) => {
|
|
82
|
+
if (rightElement) {
|
|
83
|
+
return <View style={styles.rightContainer}>{rightElement}</View>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (value) {
|
|
87
|
+
return (
|
|
88
|
+
<View style={styles.rightContainer}>
|
|
89
|
+
<AtomicText type="bodyMedium" color="secondary" style={styles.value} numberOfLines={2}>
|
|
90
|
+
{value}
|
|
91
|
+
</AtomicText>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Show chevron if onPress exists and not explicitly disabled
|
|
97
|
+
if ((showChevron ?? true) && onPress) {
|
|
98
|
+
return (
|
|
99
|
+
<View style={styles.rightContainer}>
|
|
100
|
+
<AtomicIcon name="ChevronRight" size="sm" color="secondary" style={styles.chevron} />
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<List.Item
|
|
110
|
+
title={title}
|
|
111
|
+
description={description}
|
|
112
|
+
left={renderLeft}
|
|
113
|
+
right={renderRight}
|
|
114
|
+
onPress={onPress}
|
|
115
|
+
disabled={disabled}
|
|
116
|
+
testID={testID}
|
|
117
|
+
style={styles.listItem}
|
|
118
|
+
titleStyle={styles.title}
|
|
119
|
+
descriptionStyle={styles.description}
|
|
120
|
+
titleNumberOfLines={2}
|
|
121
|
+
titleEllipsizeMode="tail"
|
|
122
|
+
descriptionNumberOfLines={2}
|
|
123
|
+
descriptionEllipsizeMode="tail"
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const getStyles = (tokens: DesignTokens) =>
|
|
129
|
+
StyleSheet.create({
|
|
130
|
+
listItem: {
|
|
131
|
+
paddingVertical: tokens.spacing.sm,
|
|
132
|
+
paddingHorizontal: tokens.spacing.md,
|
|
133
|
+
minHeight: 64,
|
|
134
|
+
},
|
|
135
|
+
leftContainer: {
|
|
136
|
+
marginRight: tokens.spacing.md,
|
|
137
|
+
justifyContent: 'center',
|
|
138
|
+
},
|
|
139
|
+
iconContainer: {
|
|
140
|
+
width: 44,
|
|
141
|
+
height: 44,
|
|
142
|
+
borderRadius: 22,
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
justifyContent: 'center',
|
|
145
|
+
overflow: 'hidden',
|
|
146
|
+
borderWidth: 1,
|
|
147
|
+
borderColor: `${tokens.colors.primary}20`,
|
|
148
|
+
},
|
|
149
|
+
title: {
|
|
150
|
+
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
151
|
+
fontWeight: '600',
|
|
152
|
+
color: tokens.colors.textPrimary,
|
|
153
|
+
flexShrink: 1,
|
|
154
|
+
},
|
|
155
|
+
description: {
|
|
156
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
157
|
+
color: tokens.colors.textSecondary,
|
|
158
|
+
marginTop: tokens.spacing.xs,
|
|
159
|
+
opacity: 0.8,
|
|
160
|
+
flexShrink: 1,
|
|
161
|
+
},
|
|
162
|
+
rightContainer: {
|
|
163
|
+
justifyContent: 'center',
|
|
164
|
+
alignItems: 'flex-end',
|
|
165
|
+
maxWidth: '50%',
|
|
166
|
+
flexShrink: 0,
|
|
167
|
+
},
|
|
168
|
+
value: {
|
|
169
|
+
fontWeight: '500',
|
|
170
|
+
textAlign: 'right',
|
|
171
|
+
},
|
|
172
|
+
chevron: {
|
|
173
|
+
opacity: 0.6,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Settings Screen
|
|
3
|
+
*
|
|
4
|
+
* Modern appearance settings with Paper List.Section:
|
|
5
|
+
* - React Native Paper List.Section pattern
|
|
6
|
+
* - Lucide icons (Languages, Moon, Sun)
|
|
7
|
+
* - Language + Theme settings combined
|
|
8
|
+
* - Dynamic icon based on theme mode
|
|
9
|
+
* - Material Design 3 compliance
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { View, StyleSheet } from 'react-native';
|
|
14
|
+
import { List } from 'react-native-paper';
|
|
15
|
+
|
|
16
|
+
import { useNavigation } from '@react-navigation/native';
|
|
17
|
+
import { useTheme, useAppDesignTokens, type DesignTokens } from '@umituz/react-native-design-system-theme';
|
|
18
|
+
import { AtomicText, ScreenLayout } from '@umituz/react-native-design-system';
|
|
19
|
+
import { useLocalization, getLanguageByCode } from '@umituz/react-native-localization';
|
|
20
|
+
import { SettingItem } from '../components/SettingItem';
|
|
21
|
+
|
|
22
|
+
export const AppearanceScreen: React.FC = () => {
|
|
23
|
+
const { t, currentLanguage } = useLocalization();
|
|
24
|
+
const navigation = useNavigation();
|
|
25
|
+
const { themeMode, toggleTheme } = useTheme();
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
const styles = getStyles(tokens);
|
|
28
|
+
|
|
29
|
+
const currentLang = getLanguageByCode(currentLanguage);
|
|
30
|
+
const languageDisplay = currentLang ? `${currentLang.flag} ${currentLang.nativeName}` : 'English';
|
|
31
|
+
const themeDisplay = themeMode === 'dark' ? t('settings.darkMode') : t('settings.lightMode');
|
|
32
|
+
|
|
33
|
+
const handleLanguagePress = () => {
|
|
34
|
+
navigation.navigate('LanguageSelection' as never);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleThemeToggle = () => {
|
|
38
|
+
toggleTheme();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ScreenLayout testID="appearance-screen" hideScrollIndicator>
|
|
43
|
+
{/* Header */}
|
|
44
|
+
<View style={styles.header}>
|
|
45
|
+
<AtomicText type="headlineLarge" color="primary">
|
|
46
|
+
{t('settings.appearance.title')}
|
|
47
|
+
</AtomicText>
|
|
48
|
+
<AtomicText type="bodyMedium" color="secondary" style={styles.headerSubtitle}>
|
|
49
|
+
{t('settings.appearance.themeDescription')}
|
|
50
|
+
</AtomicText>
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
{/* Language Section */}
|
|
54
|
+
<List.Section>
|
|
55
|
+
<List.Subheader style={{ color: tokens.colors.textSecondary }}>{t('settings.language')}</List.Subheader>
|
|
56
|
+
<SettingItem
|
|
57
|
+
icon="Languages"
|
|
58
|
+
iconGradient={((tokens.colors as any).settingGradients?.language as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
|
|
59
|
+
title={t('settings.language')}
|
|
60
|
+
value={languageDisplay}
|
|
61
|
+
onPress={handleLanguagePress}
|
|
62
|
+
testID="language-button"
|
|
63
|
+
/>
|
|
64
|
+
</List.Section>
|
|
65
|
+
|
|
66
|
+
{/* Theme Section */}
|
|
67
|
+
<List.Section>
|
|
68
|
+
<List.Subheader style={{ color: tokens.colors.textSecondary }}>{t('settings.appearance.darkMode')}</List.Subheader>
|
|
69
|
+
<SettingItem
|
|
70
|
+
icon={themeMode === 'dark' ? 'Moon' : 'Sun'}
|
|
71
|
+
iconGradient={
|
|
72
|
+
themeMode === 'dark'
|
|
73
|
+
? (((tokens.colors as any).settingGradients?.themeDark as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary])
|
|
74
|
+
: (((tokens.colors as any).settingGradients?.themeLight as unknown as string[]) || [tokens.colors.secondary, tokens.colors.primary])
|
|
75
|
+
}
|
|
76
|
+
title={t('settings.appearance.darkMode')}
|
|
77
|
+
value={themeDisplay}
|
|
78
|
+
onPress={handleThemeToggle}
|
|
79
|
+
testID="theme-button"
|
|
80
|
+
/>
|
|
81
|
+
</List.Section>
|
|
82
|
+
</ScreenLayout>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getStyles = (tokens: DesignTokens) =>
|
|
87
|
+
StyleSheet.create({
|
|
88
|
+
header: {
|
|
89
|
+
paddingBottom: tokens.spacing.lg,
|
|
90
|
+
paddingTop: tokens.spacing.md,
|
|
91
|
+
},
|
|
92
|
+
headerSubtitle: {
|
|
93
|
+
marginTop: tokens.spacing.sm,
|
|
94
|
+
lineHeight: 20,
|
|
95
|
+
opacity: 0.8,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Selection Screen
|
|
3
|
+
*
|
|
4
|
+
* Language picker with search functionality
|
|
5
|
+
*
|
|
6
|
+
* App Factory - Universal Language Selector
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useMemo } from 'react';
|
|
10
|
+
import {
|
|
11
|
+
View,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
FlatList,
|
|
14
|
+
TouchableOpacity,
|
|
15
|
+
TextInput,
|
|
16
|
+
} from 'react-native';
|
|
17
|
+
import { useNavigation } from '@react-navigation/native';
|
|
18
|
+
import { useTheme, useAppDesignTokens, withAlpha, STATIC_TOKENS, type DesignTokens } from '@umituz/react-native-design-system-theme';
|
|
19
|
+
import { AtomicIcon, AtomicText, ScreenLayout } from '@umituz/react-native-design-system';
|
|
20
|
+
import { useLocalization, searchLanguages, Language, LANGUAGES } from '@umituz/react-native-localization';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Language Selection Screen Component
|
|
24
|
+
*/
|
|
25
|
+
export const LanguageSelectionScreen: React.FC = () => {
|
|
26
|
+
const navigation = useNavigation();
|
|
27
|
+
const { t, currentLanguage, setLanguage } = useLocalization();
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
30
|
+
const [selectedCode, setSelectedCode] = useState(currentLanguage);
|
|
31
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
32
|
+
|
|
33
|
+
const filteredLanguages = useMemo(() => {
|
|
34
|
+
return searchLanguages(searchQuery);
|
|
35
|
+
}, [searchQuery]);
|
|
36
|
+
|
|
37
|
+
const handleLanguageSelect = async (code: string) => {
|
|
38
|
+
setSelectedCode(code);
|
|
39
|
+
await setLanguage(code);
|
|
40
|
+
navigation.goBack();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const renderLanguageItem = ({ item }: { item: Language }) => {
|
|
44
|
+
const isSelected = selectedCode === item.code;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<TouchableOpacity
|
|
48
|
+
style={StyleSheet.flatten([
|
|
49
|
+
styles.languageItem,
|
|
50
|
+
{
|
|
51
|
+
borderColor: isSelected
|
|
52
|
+
? tokens.colors.primary
|
|
53
|
+
: tokens.colors.borderLight,
|
|
54
|
+
backgroundColor: isSelected
|
|
55
|
+
? withAlpha(tokens.colors.primary, 0.1)
|
|
56
|
+
: tokens.colors.surface,
|
|
57
|
+
},
|
|
58
|
+
])}
|
|
59
|
+
onPress={() => handleLanguageSelect(item.code)}
|
|
60
|
+
activeOpacity={0.7}
|
|
61
|
+
>
|
|
62
|
+
<View style={styles.languageContent}>
|
|
63
|
+
<AtomicText style={StyleSheet.flatten([STATIC_TOKENS.typography.headingLarge, styles.flag])}>
|
|
64
|
+
{item.flag}
|
|
65
|
+
</AtomicText>
|
|
66
|
+
<View style={styles.languageText}>
|
|
67
|
+
<AtomicText
|
|
68
|
+
style={StyleSheet.flatten([
|
|
69
|
+
STATIC_TOKENS.typography.bodyMedium,
|
|
70
|
+
styles.nativeName,
|
|
71
|
+
])}
|
|
72
|
+
>
|
|
73
|
+
{item.nativeName}
|
|
74
|
+
</AtomicText>
|
|
75
|
+
<AtomicText style={StyleSheet.flatten([{ color: tokens.colors.textSecondary }])}>
|
|
76
|
+
{item.name}
|
|
77
|
+
</AtomicText>
|
|
78
|
+
</View>
|
|
79
|
+
</View>
|
|
80
|
+
{isSelected && (
|
|
81
|
+
<AtomicIcon
|
|
82
|
+
name="CircleCheck"
|
|
83
|
+
size="md"
|
|
84
|
+
color="primary"
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
</TouchableOpacity>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ScreenLayout scrollable={false} testID="language-selection-screen">
|
|
93
|
+
{/* Search Input */}
|
|
94
|
+
<View
|
|
95
|
+
style={StyleSheet.flatten([
|
|
96
|
+
styles.searchContainer,
|
|
97
|
+
{
|
|
98
|
+
borderColor: isFocused ? tokens.colors.primary : tokens.colors.borderLight,
|
|
99
|
+
borderWidth: isFocused ? 2 : 1.5,
|
|
100
|
+
backgroundColor: tokens.colors.surface,
|
|
101
|
+
},
|
|
102
|
+
])}
|
|
103
|
+
>
|
|
104
|
+
<AtomicIcon
|
|
105
|
+
name="Search"
|
|
106
|
+
size="md"
|
|
107
|
+
color="secondary"
|
|
108
|
+
style={styles.searchIcon}
|
|
109
|
+
/>
|
|
110
|
+
<TextInput
|
|
111
|
+
style={StyleSheet.flatten([styles.searchInput, { color: tokens.colors.textPrimary }])}
|
|
112
|
+
placeholder={t('settings.languageSelection.searchPlaceholder')}
|
|
113
|
+
placeholderTextColor={tokens.colors.textSecondary}
|
|
114
|
+
value={searchQuery}
|
|
115
|
+
onChangeText={setSearchQuery}
|
|
116
|
+
onFocus={() => setIsFocused(true)}
|
|
117
|
+
onBlur={() => setIsFocused(false)}
|
|
118
|
+
autoCapitalize="none"
|
|
119
|
+
autoCorrect={false}
|
|
120
|
+
/>
|
|
121
|
+
{searchQuery.length > 0 && (
|
|
122
|
+
<TouchableOpacity
|
|
123
|
+
onPress={() => setSearchQuery('')}
|
|
124
|
+
style={styles.clearButton}
|
|
125
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
126
|
+
>
|
|
127
|
+
<AtomicIcon
|
|
128
|
+
name="X"
|
|
129
|
+
size="sm"
|
|
130
|
+
color="secondary"
|
|
131
|
+
/>
|
|
132
|
+
</TouchableOpacity>
|
|
133
|
+
)}
|
|
134
|
+
</View>
|
|
135
|
+
|
|
136
|
+
{/* Language List */}
|
|
137
|
+
<FlatList
|
|
138
|
+
data={filteredLanguages}
|
|
139
|
+
renderItem={renderLanguageItem}
|
|
140
|
+
keyExtractor={item => item.code}
|
|
141
|
+
contentContainerStyle={styles.listContent}
|
|
142
|
+
showsVerticalScrollIndicator={false}
|
|
143
|
+
keyboardShouldPersistTaps="handled"
|
|
144
|
+
/>
|
|
145
|
+
</ScreenLayout>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const styles = StyleSheet.create({
|
|
150
|
+
searchContainer: {
|
|
151
|
+
flexDirection: 'row',
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
marginHorizontal: 20,
|
|
154
|
+
marginBottom: 24,
|
|
155
|
+
paddingHorizontal: 16,
|
|
156
|
+
paddingVertical: 8,
|
|
157
|
+
borderRadius: STATIC_TOKENS.borders.radius.lg,
|
|
158
|
+
},
|
|
159
|
+
searchIcon: {
|
|
160
|
+
marginRight: 8,
|
|
161
|
+
},
|
|
162
|
+
searchInput: {
|
|
163
|
+
flex: 1,
|
|
164
|
+
fontSize: STATIC_TOKENS.typography.bodyMedium.fontSize,
|
|
165
|
+
padding: 0,
|
|
166
|
+
fontWeight: '500',
|
|
167
|
+
},
|
|
168
|
+
clearButton: {
|
|
169
|
+
padding: STATIC_TOKENS.spacing.xs,
|
|
170
|
+
},
|
|
171
|
+
listContent: {
|
|
172
|
+
paddingHorizontal: 20,
|
|
173
|
+
paddingBottom: 32,
|
|
174
|
+
},
|
|
175
|
+
languageItem: {
|
|
176
|
+
flexDirection: 'row',
|
|
177
|
+
alignItems: 'center',
|
|
178
|
+
justifyContent: 'space-between',
|
|
179
|
+
padding: STATIC_TOKENS.spacing.md,
|
|
180
|
+
borderRadius: STATIC_TOKENS.borders.radius.lg,
|
|
181
|
+
borderWidth: 2,
|
|
182
|
+
marginBottom: 8,
|
|
183
|
+
},
|
|
184
|
+
languageContent: {
|
|
185
|
+
flexDirection: 'row',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
flex: 1,
|
|
188
|
+
gap: 16,
|
|
189
|
+
},
|
|
190
|
+
flag: {
|
|
191
|
+
fontSize: STATIC_TOKENS.typography.headingLarge.fontSize,
|
|
192
|
+
},
|
|
193
|
+
languageText: {
|
|
194
|
+
flex: 1,
|
|
195
|
+
gap: 2,
|
|
196
|
+
},
|
|
197
|
+
nativeName: {
|
|
198
|
+
fontWeight: '600',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
export default LanguageSelectionScreen;
|
|
203
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Screen
|
|
3
|
+
*
|
|
4
|
+
* Modern settings with Paper List.Section pattern:
|
|
5
|
+
* - React Native Paper List.Section + List.Subheader
|
|
6
|
+
* - Organized sections (Appearance, General, About & Legal)
|
|
7
|
+
* - Paper Divider for visual separation
|
|
8
|
+
* - Material Design 3 compliance
|
|
9
|
+
* - OFFLINE MODE: No account, premium, feedback, or donation
|
|
10
|
+
* - Optimized spacing for better visual density
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { List, Divider } from 'react-native-paper';
|
|
15
|
+
|
|
16
|
+
import { useNavigation } from '@react-navigation/native';
|
|
17
|
+
import { useTheme } from '@umituz/react-native-design-system-theme';
|
|
18
|
+
import { ScreenLayout } from '@umituz/react-native-design-system';
|
|
19
|
+
import { SettingItem } from '../components/SettingItem';
|
|
20
|
+
import { getLanguageByCode, useLocalization } from '@umituz/react-native-localization';
|
|
21
|
+
import { notificationService } from '@umituz/react-native-notifications';
|
|
22
|
+
import { useTranslation } from 'react-i18next';
|
|
23
|
+
|
|
24
|
+
export const SettingsScreen: React.FC = () => {
|
|
25
|
+
const navigation = useNavigation();
|
|
26
|
+
const { theme, themeMode } = useTheme();
|
|
27
|
+
const { currentLanguage } = useLocalization();
|
|
28
|
+
const { t } = useTranslation();
|
|
29
|
+
|
|
30
|
+
const currentLang = getLanguageByCode(currentLanguage);
|
|
31
|
+
const languageDisplay = currentLang ? `${currentLang.flag} ${currentLang.nativeName}` : 'English';
|
|
32
|
+
const themeDisplay = themeMode === 'dark' ? t('settings.darkMode') : t('settings.lightMode');
|
|
33
|
+
|
|
34
|
+
const handleAppearancePress = () => {
|
|
35
|
+
navigation.navigate('Appearance' as never);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleAboutPress = () => {
|
|
39
|
+
navigation.navigate('About' as never);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleLegalPress = () => {
|
|
43
|
+
navigation.navigate('Legal' as never);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleNotificationsPress = async () => {
|
|
47
|
+
const hasPermissions = await notificationService.hasPermissions();
|
|
48
|
+
if (!hasPermissions) {
|
|
49
|
+
await notificationService.requestPermissions();
|
|
50
|
+
}
|
|
51
|
+
navigation.navigate('Notifications' as never);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ScreenLayout testID="settings-screen" hideScrollIndicator>
|
|
56
|
+
{/* Appearance Section */}
|
|
57
|
+
<List.Section style={{ marginBottom: 8 }}>
|
|
58
|
+
<List.Subheader style={{ color: theme.colors.textSecondary }}>{t('settings.sections.appearance')}</List.Subheader>
|
|
59
|
+
<SettingItem
|
|
60
|
+
icon="Palette"
|
|
61
|
+
iconGradient={theme.colors.settingGradients.themeLight as unknown as string[]}
|
|
62
|
+
title={t('settings.appearance.title')}
|
|
63
|
+
description={t('settings.appearance.themeDescription')}
|
|
64
|
+
onPress={handleAppearancePress}
|
|
65
|
+
testID="appearance-button"
|
|
66
|
+
/>
|
|
67
|
+
</List.Section>
|
|
68
|
+
|
|
69
|
+
{/* General Section */}
|
|
70
|
+
<List.Section style={{ marginBottom: 8 }}>
|
|
71
|
+
<List.Subheader style={{ color: theme.colors.textSecondary }}>{t('settings.sections.general')}</List.Subheader>
|
|
72
|
+
<SettingItem
|
|
73
|
+
icon="Bell"
|
|
74
|
+
iconGradient={theme.colors.settingGradients.notifications as unknown as string[]}
|
|
75
|
+
title={t('settings.notifications.title')}
|
|
76
|
+
description={t('settings.notifications.description')}
|
|
77
|
+
onPress={handleNotificationsPress}
|
|
78
|
+
testID="notifications-button"
|
|
79
|
+
/>
|
|
80
|
+
</List.Section>
|
|
81
|
+
|
|
82
|
+
{/* About & Legal Section */}
|
|
83
|
+
<List.Section style={{ marginBottom: 8 }}>
|
|
84
|
+
<List.Subheader style={{ color: theme.colors.textSecondary }}>{t('settings.sections.about')}</List.Subheader>
|
|
85
|
+
<SettingItem
|
|
86
|
+
icon="Info"
|
|
87
|
+
iconGradient={theme.colors.settingGradients.info as unknown as string[]}
|
|
88
|
+
title={t('settings.about.title')}
|
|
89
|
+
description={t('settings.about.description')}
|
|
90
|
+
onPress={handleAboutPress}
|
|
91
|
+
testID="about-button"
|
|
92
|
+
/>
|
|
93
|
+
<Divider />
|
|
94
|
+
<SettingItem
|
|
95
|
+
icon="FileText"
|
|
96
|
+
iconGradient={theme.colors.settingGradients.info as unknown as string[]}
|
|
97
|
+
title={t('settings.legal.title')}
|
|
98
|
+
description={t('settings.legal.description')}
|
|
99
|
+
onPress={handleLegalPress}
|
|
100
|
+
testID="legal-button"
|
|
101
|
+
/>
|
|
102
|
+
</List.Section>
|
|
103
|
+
</ScreenLayout>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|