@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 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
+