@umituz/react-native-settings 1.5.0 → 1.6.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/README.md +133 -17
- package/package.json +2 -1
- package/src/index.ts +11 -0
- package/src/presentation/components/SettingItem.tsx +144 -150
- package/src/presentation/components/SettingsFooter.tsx +46 -0
- package/src/presentation/components/SettingsSection.tsx +59 -0
- package/src/presentation/components/UserProfileHeader.tsx +129 -0
- package/src/presentation/screens/SettingsScreen.tsx +182 -305
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npm install @umituz/react-native-settings
|
|
|
20
20
|
## Peer Dependencies
|
|
21
21
|
|
|
22
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
|
|
23
|
+
npm install zustand lucide-react-native @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
24
|
```
|
|
25
25
|
|
|
26
26
|
## Usage
|
|
@@ -55,8 +55,25 @@ const MyComponent = () => {
|
|
|
55
55
|
```tsx
|
|
56
56
|
import { SettingsScreen } from '@umituz/react-native-settings';
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Basic usage
|
|
59
59
|
<Stack.Screen name="Settings" component={SettingsScreen} />
|
|
60
|
+
|
|
61
|
+
// With user profile header
|
|
62
|
+
<SettingsScreen
|
|
63
|
+
showUserProfile={true}
|
|
64
|
+
userProfile={{
|
|
65
|
+
displayName: "John Doe",
|
|
66
|
+
userId: "user123",
|
|
67
|
+
isGuest: false,
|
|
68
|
+
accountSettingsRoute: "AccountSettings",
|
|
69
|
+
}}
|
|
70
|
+
config={{
|
|
71
|
+
appearance: true,
|
|
72
|
+
notifications: true,
|
|
73
|
+
about: true,
|
|
74
|
+
legal: true,
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
60
77
|
```
|
|
61
78
|
|
|
62
79
|
### Appearance Screen
|
|
@@ -81,17 +98,82 @@ import { LanguageSelectionScreen } from '@umituz/react-native-settings';
|
|
|
81
98
|
|
|
82
99
|
```tsx
|
|
83
100
|
import { SettingItem } from '@umituz/react-native-settings';
|
|
101
|
+
import { Palette, Bell } from 'lucide-react-native';
|
|
84
102
|
|
|
103
|
+
// Basic setting item
|
|
85
104
|
<SettingItem
|
|
86
|
-
icon=
|
|
87
|
-
title="
|
|
88
|
-
|
|
89
|
-
value="Dark"
|
|
105
|
+
icon={Palette}
|
|
106
|
+
title="Appearance"
|
|
107
|
+
value="Theme and language settings"
|
|
90
108
|
onPress={() => navigation.navigate('Appearance')}
|
|
91
|
-
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
// With switch
|
|
112
|
+
<SettingItem
|
|
113
|
+
icon={Bell}
|
|
114
|
+
title="Notifications"
|
|
115
|
+
showSwitch={true}
|
|
116
|
+
switchValue={enabled}
|
|
117
|
+
onSwitchChange={setEnabled}
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
// Custom colors
|
|
121
|
+
<SettingItem
|
|
122
|
+
icon={Palette}
|
|
123
|
+
title="Appearance"
|
|
124
|
+
iconColor="#F59E0B"
|
|
125
|
+
titleColor="#F59E0B"
|
|
126
|
+
onPress={() => {}}
|
|
92
127
|
/>
|
|
93
128
|
```
|
|
94
129
|
|
|
130
|
+
### Settings Section Component
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { SettingsSection, SettingItem } from '@umituz/react-native-settings';
|
|
134
|
+
import { Palette, Bell } from 'lucide-react-native';
|
|
135
|
+
|
|
136
|
+
<SettingsSection title="APP SETTINGS">
|
|
137
|
+
<SettingItem
|
|
138
|
+
icon={Palette}
|
|
139
|
+
title="Appearance"
|
|
140
|
+
value="Theme and language settings"
|
|
141
|
+
onPress={() => navigation.navigate('Appearance')}
|
|
142
|
+
/>
|
|
143
|
+
<SettingItem
|
|
144
|
+
icon={Bell}
|
|
145
|
+
title="Notifications"
|
|
146
|
+
showSwitch={true}
|
|
147
|
+
switchValue={enabled}
|
|
148
|
+
onSwitchChange={setEnabled}
|
|
149
|
+
isLast={true}
|
|
150
|
+
/>
|
|
151
|
+
</SettingsSection>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### User Profile Header Component
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { UserProfileHeader } from '@umituz/react-native-settings';
|
|
158
|
+
|
|
159
|
+
<UserProfileHeader
|
|
160
|
+
displayName="John Doe"
|
|
161
|
+
userId="user123"
|
|
162
|
+
isGuest={false}
|
|
163
|
+
avatarUrl="https://example.com/avatar.jpg"
|
|
164
|
+
accountSettingsRoute="AccountSettings"
|
|
165
|
+
onPress={() => navigation.navigate('AccountSettings')}
|
|
166
|
+
/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Settings Footer Component
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { SettingsFooter } from '@umituz/react-native-settings';
|
|
173
|
+
|
|
174
|
+
<SettingsFooter versionText="Version 1.0.0" />
|
|
175
|
+
```
|
|
176
|
+
|
|
95
177
|
### Disclaimer Setting Component
|
|
96
178
|
|
|
97
179
|
```tsx
|
|
@@ -122,7 +204,14 @@ Direct access to Zustand store.
|
|
|
122
204
|
|
|
123
205
|
### `SettingsScreen`
|
|
124
206
|
|
|
125
|
-
|
|
207
|
+
Modern settings screen with organized sections and optional user profile header.
|
|
208
|
+
|
|
209
|
+
**Props:**
|
|
210
|
+
- `config?: SettingsConfig` - Configuration for which features to show
|
|
211
|
+
- `showUserProfile?: boolean` - Show user profile header
|
|
212
|
+
- `userProfile?: UserProfileHeaderProps` - User profile props
|
|
213
|
+
- `showFooter?: boolean` - Show footer with version
|
|
214
|
+
- `footerText?: string` - Custom footer text
|
|
126
215
|
|
|
127
216
|
### `AppearanceScreen`
|
|
128
217
|
|
|
@@ -134,19 +223,46 @@ Language selection screen with search functionality.
|
|
|
134
223
|
|
|
135
224
|
### `SettingItem`
|
|
136
225
|
|
|
137
|
-
|
|
226
|
+
Modern setting item component with Lucide icons and switch support.
|
|
138
227
|
|
|
139
228
|
**Props:**
|
|
140
|
-
- `icon:
|
|
229
|
+
- `icon: React.ComponentType` - Icon component from lucide-react-native
|
|
141
230
|
- `title: string` - Main title text
|
|
142
|
-
- `
|
|
143
|
-
- `value?: string` - Optional value to display on right
|
|
231
|
+
- `value?: string` - Optional description/value text (shown below title)
|
|
144
232
|
- `onPress?: () => void` - Callback when pressed
|
|
145
|
-
- `
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
- `
|
|
233
|
+
- `showSwitch?: boolean` - Show switch instead of chevron
|
|
234
|
+
- `switchValue?: boolean` - Switch value
|
|
235
|
+
- `onSwitchChange?: (value: boolean) => void` - Switch change handler
|
|
236
|
+
- `isLast?: boolean` - Is last item (no divider)
|
|
237
|
+
- `iconColor?: string` - Custom icon color
|
|
238
|
+
- `titleColor?: string` - Custom title color
|
|
239
|
+
|
|
240
|
+
### `SettingsSection`
|
|
241
|
+
|
|
242
|
+
Section container with title and styled content area.
|
|
243
|
+
|
|
244
|
+
**Props:**
|
|
245
|
+
- `title: string` - Section title (uppercase)
|
|
246
|
+
- `children: React.ReactNode` - Section content
|
|
247
|
+
|
|
248
|
+
### `UserProfileHeader`
|
|
249
|
+
|
|
250
|
+
User profile header with avatar, name, and ID.
|
|
251
|
+
|
|
252
|
+
**Props:**
|
|
253
|
+
- `displayName?: string` - User display name
|
|
254
|
+
- `userId?: string` - User ID
|
|
255
|
+
- `isGuest?: boolean` - Whether user is guest
|
|
256
|
+
- `avatarUrl?: string` - Custom avatar URL
|
|
257
|
+
- `accountSettingsRoute?: string` - Navigation route for account settings
|
|
258
|
+
- `onPress?: () => void` - Custom onPress handler
|
|
259
|
+
|
|
260
|
+
### `SettingsFooter`
|
|
261
|
+
|
|
262
|
+
Footer component displaying app version.
|
|
263
|
+
|
|
264
|
+
**Props:**
|
|
265
|
+
- `versionText?: string` - Custom version text (optional)
|
|
150
266
|
|
|
151
267
|
### `DisclaimerSetting`
|
|
152
268
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Settings management for React Native apps - user preferences, theme, language, notifications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"react": ">=18.2.0",
|
|
30
30
|
"react-native": ">=0.74.0",
|
|
31
31
|
"zustand": "^5.0.2",
|
|
32
|
+
"lucide-react-native": "^0.468.0",
|
|
32
33
|
"@umituz/react-native-storage": "latest",
|
|
33
34
|
"@umituz/react-native-design-system": "latest",
|
|
34
35
|
"@umituz/react-native-design-system-theme": "latest",
|
package/src/index.ts
CHANGED
|
@@ -48,5 +48,16 @@ export type { SettingsConfig } from './presentation/screens/types';
|
|
|
48
48
|
// =============================================================================
|
|
49
49
|
|
|
50
50
|
export { SettingItem } from './presentation/components/SettingItem';
|
|
51
|
+
export type { SettingItemProps } from './presentation/components/SettingItem';
|
|
52
|
+
|
|
53
|
+
export { SettingsSection } from './presentation/components/SettingsSection';
|
|
54
|
+
export type { SettingsSectionProps } from './presentation/components/SettingsSection';
|
|
55
|
+
|
|
56
|
+
export { SettingsFooter } from './presentation/components/SettingsFooter';
|
|
57
|
+
export type { SettingsFooterProps } from './presentation/components/SettingsFooter';
|
|
58
|
+
|
|
59
|
+
export { UserProfileHeader } from './presentation/components/UserProfileHeader';
|
|
60
|
+
export type { UserProfileHeaderProps } from './presentation/components/UserProfileHeader';
|
|
61
|
+
|
|
51
62
|
export { DisclaimerSetting } from './presentation/components/DisclaimerSetting';
|
|
52
63
|
|
|
@@ -1,173 +1,167 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Modern
|
|
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
|
|
2
|
+
* Setting Item Component
|
|
3
|
+
* Single Responsibility: Render a single settings item
|
|
4
|
+
* Modern design with Lucide icons and switch support
|
|
12
5
|
*/
|
|
13
6
|
|
|
14
|
-
import React from
|
|
15
|
-
import { View, StyleSheet,
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
19
|
-
import type { IconName } from '@umituz/react-native-design-system-atoms';
|
|
20
|
-
import type { DesignTokens } from '@umituz/react-native-design-system-theme';
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, Text, TouchableOpacity, StyleSheet, Switch } from "react-native";
|
|
9
|
+
import { ChevronRight } from "lucide-react-native";
|
|
10
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
21
11
|
|
|
22
|
-
interface SettingItemProps {
|
|
23
|
-
/** Icon
|
|
24
|
-
icon:
|
|
12
|
+
export interface SettingItemProps {
|
|
13
|
+
/** Icon component from lucide-react-native */
|
|
14
|
+
icon: React.ComponentType<{ size?: number; color?: string }>;
|
|
25
15
|
/** Main title text */
|
|
26
16
|
title: string;
|
|
27
|
-
/** Optional description text */
|
|
28
|
-
description?: string;
|
|
29
|
-
/** Optional value to display on the right */
|
|
17
|
+
/** Optional description/value text */
|
|
30
18
|
value?: string;
|
|
31
19
|
/** Callback when pressed */
|
|
32
20
|
onPress?: () => void;
|
|
33
|
-
/** Show
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
|
|
21
|
+
/** Show switch instead of chevron */
|
|
22
|
+
showSwitch?: boolean;
|
|
23
|
+
/** Switch value */
|
|
24
|
+
switchValue?: boolean;
|
|
25
|
+
/** Switch change handler */
|
|
26
|
+
onSwitchChange?: (value: boolean) => void;
|
|
27
|
+
/** Is last item in section (no divider) */
|
|
28
|
+
isLast?: boolean;
|
|
29
|
+
/** Custom icon color */
|
|
30
|
+
iconColor?: string;
|
|
31
|
+
/** Custom title color */
|
|
32
|
+
titleColor?: string;
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
export const SettingItem: React.FC<SettingItemProps> = ({
|
|
46
|
-
icon,
|
|
36
|
+
icon: Icon,
|
|
47
37
|
title,
|
|
48
|
-
description,
|
|
49
38
|
value,
|
|
50
39
|
onPress,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
showSwitch = false,
|
|
41
|
+
switchValue,
|
|
42
|
+
onSwitchChange,
|
|
43
|
+
isLast = false,
|
|
44
|
+
iconColor,
|
|
45
|
+
titleColor,
|
|
56
46
|
}) => {
|
|
57
47
|
const tokens = useAppDesignTokens();
|
|
58
|
-
const
|
|
48
|
+
const colors = tokens.colors;
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<TouchableOpacity
|
|
53
|
+
style={[
|
|
54
|
+
styles.container,
|
|
55
|
+
{ backgroundColor: colors.backgroundPrimary },
|
|
56
|
+
]}
|
|
57
|
+
onPress={onPress}
|
|
58
|
+
disabled={showSwitch}
|
|
59
|
+
activeOpacity={0.7}
|
|
60
|
+
>
|
|
61
|
+
<View style={styles.content}>
|
|
62
|
+
<View
|
|
63
|
+
style={[
|
|
64
|
+
styles.iconContainer,
|
|
65
|
+
{
|
|
66
|
+
backgroundColor: iconColor
|
|
67
|
+
? `${iconColor}15`
|
|
68
|
+
: `${colors.primary}15`,
|
|
69
|
+
},
|
|
70
|
+
]}
|
|
71
|
+
>
|
|
72
|
+
<Icon size={20} color={iconColor || colors.primary} />
|
|
73
|
+
</View>
|
|
74
|
+
<View style={styles.textContainer}>
|
|
75
|
+
<Text
|
|
76
|
+
style={[
|
|
77
|
+
styles.title,
|
|
78
|
+
{ color: titleColor || colors.textPrimary },
|
|
79
|
+
]}
|
|
80
|
+
numberOfLines={1}
|
|
81
|
+
>
|
|
82
|
+
{title}
|
|
83
|
+
</Text>
|
|
84
|
+
{value && !showSwitch && (
|
|
85
|
+
<Text
|
|
86
|
+
style={[styles.value, { color: colors.textSecondary }]}
|
|
87
|
+
numberOfLines={1}
|
|
88
|
+
>
|
|
89
|
+
{value}
|
|
90
|
+
</Text>
|
|
91
|
+
)}
|
|
92
|
+
</View>
|
|
93
|
+
</View>
|
|
64
94
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
<View style={styles.rightContainer}>
|
|
96
|
+
{showSwitch ? (
|
|
97
|
+
<Switch
|
|
98
|
+
value={switchValue}
|
|
99
|
+
onValueChange={onSwitchChange}
|
|
100
|
+
trackColor={{
|
|
101
|
+
false: `${colors.textSecondary}30`,
|
|
102
|
+
true: colors.primary,
|
|
103
|
+
}}
|
|
104
|
+
thumbColor="#FFFFFF"
|
|
105
|
+
/>
|
|
106
|
+
) : (
|
|
107
|
+
<ChevronRight size={18} color={colors.textSecondary} />
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
</TouchableOpacity>
|
|
78
111
|
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)}
|
|
89
|
-
</View>
|
|
90
|
-
|
|
91
|
-
{/* Right: Value, chevron, or custom element */}
|
|
92
|
-
<View style={styles.rightContainer}>
|
|
93
|
-
{rightElement ? (
|
|
94
|
-
rightElement
|
|
95
|
-
) : value ? (
|
|
96
|
-
<AtomicText type="bodyMedium" color="textSecondary" style={styles.value} numberOfLines={2}>
|
|
97
|
-
{value}
|
|
98
|
-
</AtomicText>
|
|
99
|
-
) : (showChevron ?? true) && onPress ? (
|
|
100
|
-
<AtomicIcon name="ChevronRight" size="sm" customColor={tokens.colors.textSecondary} style={styles.chevron} />
|
|
101
|
-
) : null}
|
|
102
|
-
</View>
|
|
103
|
-
</View>
|
|
112
|
+
{!isLast && (
|
|
113
|
+
<View
|
|
114
|
+
style={[
|
|
115
|
+
styles.divider,
|
|
116
|
+
{ backgroundColor: `${colors.textSecondary}20` },
|
|
117
|
+
]}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
</>
|
|
104
121
|
);
|
|
105
|
-
|
|
106
|
-
if (onPress && !disabled) {
|
|
107
|
-
return (
|
|
108
|
-
<Pressable onPress={onPress} testID={testID} style={styles.pressable}>
|
|
109
|
-
{content}
|
|
110
|
-
</Pressable>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return <View testID={testID}>{content}</View>;
|
|
115
122
|
};
|
|
116
123
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
alignItems: 'flex-end',
|
|
162
|
-
maxWidth: '50%',
|
|
163
|
-
flexShrink: 0,
|
|
164
|
-
},
|
|
165
|
-
value: {
|
|
166
|
-
fontWeight: '500',
|
|
167
|
-
textAlign: 'right',
|
|
168
|
-
},
|
|
169
|
-
chevron: {
|
|
170
|
-
opacity: 0.6,
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
124
|
+
const styles = StyleSheet.create({
|
|
125
|
+
container: {
|
|
126
|
+
flexDirection: "row",
|
|
127
|
+
alignItems: "center",
|
|
128
|
+
justifyContent: "space-between",
|
|
129
|
+
paddingHorizontal: 16,
|
|
130
|
+
paddingVertical: 14,
|
|
131
|
+
},
|
|
132
|
+
content: {
|
|
133
|
+
flexDirection: "row",
|
|
134
|
+
alignItems: "center",
|
|
135
|
+
flex: 1,
|
|
136
|
+
},
|
|
137
|
+
iconContainer: {
|
|
138
|
+
width: 40,
|
|
139
|
+
height: 40,
|
|
140
|
+
borderRadius: 8,
|
|
141
|
+
justifyContent: "center",
|
|
142
|
+
alignItems: "center",
|
|
143
|
+
marginRight: 12,
|
|
144
|
+
},
|
|
145
|
+
textContainer: {
|
|
146
|
+
flex: 1,
|
|
147
|
+
minWidth: 0,
|
|
148
|
+
},
|
|
149
|
+
title: {
|
|
150
|
+
fontSize: 16,
|
|
151
|
+
fontWeight: "500",
|
|
152
|
+
},
|
|
153
|
+
value: {
|
|
154
|
+
fontSize: 12,
|
|
155
|
+
fontWeight: "400",
|
|
156
|
+
marginTop: 2,
|
|
157
|
+
},
|
|
158
|
+
rightContainer: {
|
|
159
|
+
flexDirection: "row",
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
gap: 8,
|
|
162
|
+
},
|
|
163
|
+
divider: {
|
|
164
|
+
height: 1,
|
|
165
|
+
marginLeft: 68,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Footer Component
|
|
3
|
+
* Single Responsibility: Display app version information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
|
|
11
|
+
export interface SettingsFooterProps {
|
|
12
|
+
/** Custom version text (optional) */
|
|
13
|
+
versionText?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SettingsFooter: React.FC<SettingsFooterProps> = ({
|
|
17
|
+
versionText,
|
|
18
|
+
}) => {
|
|
19
|
+
const { t } = useLocalization();
|
|
20
|
+
const tokens = useAppDesignTokens();
|
|
21
|
+
const colors = tokens.colors;
|
|
22
|
+
|
|
23
|
+
const displayText =
|
|
24
|
+
versionText ||
|
|
25
|
+
`${t("settings.about.version")} ${t("settings.about.versionNumber")}`;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={styles.container}>
|
|
29
|
+
<Text style={[styles.text, { color: colors.textSecondary }]}>
|
|
30
|
+
{displayText}
|
|
31
|
+
</Text>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const styles = StyleSheet.create({
|
|
37
|
+
container: {
|
|
38
|
+
paddingVertical: 24,
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
},
|
|
41
|
+
text: {
|
|
42
|
+
fontSize: 12,
|
|
43
|
+
fontWeight: "500",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Section Component
|
|
3
|
+
* Single Responsibility: Render a settings section with title and container
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
|
|
10
|
+
export interface SettingsSectionProps {
|
|
11
|
+
/** Section title */
|
|
12
|
+
title: string;
|
|
13
|
+
/** Section content */
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SettingsSection: React.FC<SettingsSectionProps> = ({
|
|
18
|
+
title,
|
|
19
|
+
children,
|
|
20
|
+
}) => {
|
|
21
|
+
const tokens = useAppDesignTokens();
|
|
22
|
+
const colors = tokens.colors;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View style={styles.container}>
|
|
26
|
+
<Text style={[styles.title, { color: colors.textSecondary }]}>
|
|
27
|
+
{title}
|
|
28
|
+
</Text>
|
|
29
|
+
<View
|
|
30
|
+
style={[
|
|
31
|
+
styles.content,
|
|
32
|
+
{ backgroundColor: `${colors.textSecondary}10` },
|
|
33
|
+
]}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</View>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
container: {
|
|
43
|
+
marginBottom: 24,
|
|
44
|
+
},
|
|
45
|
+
title: {
|
|
46
|
+
fontSize: 12,
|
|
47
|
+
fontWeight: "700",
|
|
48
|
+
textTransform: "uppercase",
|
|
49
|
+
letterSpacing: 1,
|
|
50
|
+
paddingHorizontal: 16,
|
|
51
|
+
paddingBottom: 8,
|
|
52
|
+
},
|
|
53
|
+
content: {
|
|
54
|
+
borderRadius: 12,
|
|
55
|
+
marginHorizontal: 16,
|
|
56
|
+
overflow: "hidden",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Profile Header Component
|
|
3
|
+
* Displays user avatar, name, and ID
|
|
4
|
+
* Works for both guest and authenticated users
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, Text, TouchableOpacity, StyleSheet, Image } from "react-native";
|
|
9
|
+
import { ChevronRight } from "lucide-react-native";
|
|
10
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
11
|
+
import { useNavigation } from "@react-navigation/native";
|
|
12
|
+
|
|
13
|
+
export interface UserProfileHeaderProps {
|
|
14
|
+
/** User display name */
|
|
15
|
+
displayName?: string;
|
|
16
|
+
/** User ID */
|
|
17
|
+
userId?: string;
|
|
18
|
+
/** Whether user is guest */
|
|
19
|
+
isGuest?: boolean;
|
|
20
|
+
/** Avatar URL (optional) */
|
|
21
|
+
avatarUrl?: string;
|
|
22
|
+
/** Navigation route for account settings */
|
|
23
|
+
accountSettingsRoute?: string;
|
|
24
|
+
/** Custom onPress handler */
|
|
25
|
+
onPress?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
|
29
|
+
displayName,
|
|
30
|
+
userId,
|
|
31
|
+
isGuest = false,
|
|
32
|
+
avatarUrl,
|
|
33
|
+
accountSettingsRoute = "AccountSettings",
|
|
34
|
+
onPress,
|
|
35
|
+
}) => {
|
|
36
|
+
const tokens = useAppDesignTokens();
|
|
37
|
+
const navigation = useNavigation();
|
|
38
|
+
const colors = tokens.colors;
|
|
39
|
+
|
|
40
|
+
const finalDisplayName = displayName || (isGuest ? "Guest" : "User");
|
|
41
|
+
const finalUserId = userId || "Unknown";
|
|
42
|
+
const avatarName = isGuest ? "Guest" : finalDisplayName;
|
|
43
|
+
const finalAvatarUrl =
|
|
44
|
+
avatarUrl ||
|
|
45
|
+
`https://ui-avatars.com/api/?name=${encodeURIComponent(avatarName)}&background=${colors.primary.replace("#", "")}&color=fff&size=64`;
|
|
46
|
+
|
|
47
|
+
const handlePress = () => {
|
|
48
|
+
if (onPress) {
|
|
49
|
+
onPress();
|
|
50
|
+
} else if (accountSettingsRoute) {
|
|
51
|
+
navigation.navigate(accountSettingsRoute as never);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<TouchableOpacity
|
|
57
|
+
style={[
|
|
58
|
+
styles.container,
|
|
59
|
+
{
|
|
60
|
+
backgroundColor: colors.backgroundPrimary,
|
|
61
|
+
borderColor: colors.borderLight,
|
|
62
|
+
},
|
|
63
|
+
]}
|
|
64
|
+
onPress={handlePress}
|
|
65
|
+
activeOpacity={0.7}
|
|
66
|
+
>
|
|
67
|
+
<View style={styles.content}>
|
|
68
|
+
<Image
|
|
69
|
+
source={{ uri: finalAvatarUrl }}
|
|
70
|
+
style={[styles.avatar, { borderColor: `${colors.primary}40` }]}
|
|
71
|
+
/>
|
|
72
|
+
<View style={styles.textContainer}>
|
|
73
|
+
<Text
|
|
74
|
+
style={[styles.name, { color: colors.textPrimary }]}
|
|
75
|
+
numberOfLines={1}
|
|
76
|
+
>
|
|
77
|
+
{finalDisplayName}
|
|
78
|
+
</Text>
|
|
79
|
+
<Text
|
|
80
|
+
style={[styles.id, { color: colors.textSecondary }]}
|
|
81
|
+
numberOfLines={1}
|
|
82
|
+
>
|
|
83
|
+
ID: {finalUserId.substring(0, 8)}...
|
|
84
|
+
</Text>
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
<ChevronRight size={20} color={colors.textSecondary} />
|
|
88
|
+
</TouchableOpacity>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
container: {
|
|
94
|
+
flexDirection: "row",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
justifyContent: "space-between",
|
|
97
|
+
paddingHorizontal: 16,
|
|
98
|
+
paddingVertical: 16,
|
|
99
|
+
marginHorizontal: 16,
|
|
100
|
+
marginTop: 0,
|
|
101
|
+
borderRadius: 12,
|
|
102
|
+
borderWidth: 1,
|
|
103
|
+
},
|
|
104
|
+
content: {
|
|
105
|
+
flexDirection: "row",
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
flex: 1,
|
|
108
|
+
},
|
|
109
|
+
avatar: {
|
|
110
|
+
width: 48,
|
|
111
|
+
height: 48,
|
|
112
|
+
borderRadius: 24,
|
|
113
|
+
borderWidth: 2,
|
|
114
|
+
},
|
|
115
|
+
textContainer: {
|
|
116
|
+
marginLeft: 12,
|
|
117
|
+
flex: 1,
|
|
118
|
+
},
|
|
119
|
+
name: {
|
|
120
|
+
fontSize: 16,
|
|
121
|
+
fontWeight: "600",
|
|
122
|
+
marginBottom: 4,
|
|
123
|
+
},
|
|
124
|
+
id: {
|
|
125
|
+
fontSize: 12,
|
|
126
|
+
fontWeight: "400",
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
@@ -1,389 +1,266 @@
|
|
|
1
1
|
/**
|
|
2
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
|
-
* - Configurable features via SettingsConfig prop
|
|
3
|
+
* Modern settings screen with user profile header and organized sections
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
|
-
import React, { useMemo } from
|
|
15
|
-
import {
|
|
16
|
-
|
|
6
|
+
import React, { useMemo, useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
ScrollView,
|
|
10
|
+
StatusBar,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
Alert,
|
|
13
|
+
DeviceEventEmitter,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
16
|
+
import { useNavigation, CommonActions } from "@react-navigation/native";
|
|
17
|
+
import {
|
|
18
|
+
useDesignSystemTheme,
|
|
19
|
+
useAppDesignTokens,
|
|
20
|
+
} from "@umituz/react-native-design-system-theme";
|
|
21
|
+
import { Palette, Bell, Info, FileText } from "lucide-react-native";
|
|
22
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
23
|
+
import { SettingItem } from "../components/SettingItem";
|
|
24
|
+
import { SettingsSection } from "../components/SettingsSection";
|
|
25
|
+
import { SettingsFooter } from "../components/SettingsFooter";
|
|
26
|
+
import { UserProfileHeader } from "../components/UserProfileHeader";
|
|
27
|
+
import { SettingsConfig } from "./types";
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
import { useDesignSystemTheme, useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
20
|
-
import { ScreenLayout } from '@umituz/react-native-design-system-organisms';
|
|
21
|
-
import { AtomicIcon, AtomicText } from '@umituz/react-native-design-system-atoms';
|
|
22
|
-
import { SettingItem } from '../components/SettingItem';
|
|
23
|
-
import { getLanguageByCode, useLocalization } from '@umituz/react-native-localization';
|
|
24
|
-
import { SettingsConfig } from './types';
|
|
25
|
-
|
|
26
|
-
// Optional notification service - only import if package is available
|
|
29
|
+
// Optional notification service
|
|
27
30
|
let notificationService: any = null;
|
|
28
31
|
try {
|
|
29
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
-
notificationService = require(
|
|
33
|
+
notificationService = require("@umituz/react-native-notifications")
|
|
34
|
+
.notificationService;
|
|
31
35
|
} catch {
|
|
32
|
-
// Package not available
|
|
36
|
+
// Package not available
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
// Optional onboarding store
|
|
39
|
+
// Optional onboarding store
|
|
36
40
|
let useOnboardingStore: any = null;
|
|
37
41
|
try {
|
|
38
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
-
const onboardingPackage = require(
|
|
43
|
+
const onboardingPackage = require("@umituz/react-native-onboarding");
|
|
40
44
|
useOnboardingStore = onboardingPackage.useOnboardingStore;
|
|
41
45
|
} catch {
|
|
42
|
-
// Package not available
|
|
46
|
+
// Package not available
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
|
-
* Check if
|
|
50
|
+
* Check if navigation screen exists
|
|
47
51
|
*/
|
|
48
|
-
const hasNavigationScreen = (
|
|
52
|
+
const hasNavigationScreen = (
|
|
53
|
+
navigation: any,
|
|
54
|
+
screenName: string,
|
|
55
|
+
): boolean => {
|
|
49
56
|
try {
|
|
50
57
|
const state = navigation.getState();
|
|
51
58
|
if (!state) return false;
|
|
52
59
|
|
|
53
|
-
// Recursively check all routes in navigation state
|
|
54
60
|
const checkRoutes = (routes: any[]): boolean => {
|
|
55
61
|
if (!routes || !Array.isArray(routes)) return false;
|
|
56
|
-
|
|
62
|
+
|
|
57
63
|
for (const route of routes) {
|
|
58
|
-
if (route.name === screenName)
|
|
64
|
+
if (route.name === screenName) return true;
|
|
65
|
+
if (route.state?.routes && checkRoutes(route.state.routes)) {
|
|
59
66
|
return true;
|
|
60
67
|
}
|
|
61
|
-
// Check nested navigators
|
|
62
|
-
if (route.state?.routes) {
|
|
63
|
-
if (checkRoutes(route.state.routes)) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
68
|
}
|
|
68
69
|
return false;
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
return checkRoutes(state.routes || []);
|
|
72
73
|
} catch {
|
|
73
|
-
// If we can't check navigation state, assume it's not available
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export interface SettingsScreenProps {
|
|
79
|
-
/**
|
|
80
|
-
* Configuration for which settings features to show
|
|
81
|
-
* @default { appearance: 'auto', notifications: 'auto', about: 'auto', legal: 'auto' }
|
|
82
|
-
*/
|
|
83
79
|
config?: SettingsConfig;
|
|
80
|
+
/** Show user profile header */
|
|
81
|
+
showUserProfile?: boolean;
|
|
82
|
+
/** User profile props */
|
|
83
|
+
userProfile?: {
|
|
84
|
+
displayName?: string;
|
|
85
|
+
userId?: string;
|
|
86
|
+
isGuest?: boolean;
|
|
87
|
+
avatarUrl?: string;
|
|
88
|
+
accountSettingsRoute?: string;
|
|
89
|
+
onPress?: () => void;
|
|
90
|
+
};
|
|
91
|
+
/** Show footer with version */
|
|
92
|
+
showFooter?: boolean;
|
|
93
|
+
/** Custom footer text */
|
|
94
|
+
footerText?: string;
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|
87
|
-
config = {}
|
|
97
|
+
export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|
98
|
+
config = {},
|
|
99
|
+
showUserProfile = false,
|
|
100
|
+
userProfile,
|
|
101
|
+
showFooter = true,
|
|
102
|
+
footerText,
|
|
88
103
|
}) => {
|
|
89
104
|
const navigation = useNavigation();
|
|
90
105
|
const { themeMode } = useDesignSystemTheme();
|
|
91
106
|
const tokens = useAppDesignTokens();
|
|
92
107
|
const insets = useSafeAreaInsets();
|
|
93
|
-
const {
|
|
94
|
-
const
|
|
108
|
+
const { t } = useLocalization();
|
|
109
|
+
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
|
|
95
110
|
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const themeDisplay = themeMode === 'dark' ? t('settings.darkMode') : t('settings.lightMode');
|
|
111
|
+
const isDark = themeMode === "dark";
|
|
112
|
+
const colors = tokens.colors;
|
|
99
113
|
|
|
100
|
-
// Determine which features should be shown
|
|
101
114
|
const features = useMemo(() => {
|
|
102
|
-
const appearanceConfig = config
|
|
103
|
-
const notificationsConfig = config
|
|
104
|
-
const aboutConfig = config
|
|
105
|
-
const legalConfig = config
|
|
115
|
+
const appearanceConfig = config?.appearance ?? "auto";
|
|
116
|
+
const notificationsConfig = config?.notifications ?? "auto";
|
|
117
|
+
const aboutConfig = config?.about ?? "auto";
|
|
118
|
+
const legalConfig = config?.legal ?? "auto";
|
|
106
119
|
|
|
107
120
|
return {
|
|
108
|
-
appearance:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
appearance:
|
|
122
|
+
appearanceConfig === true ||
|
|
123
|
+
(appearanceConfig === "auto" &&
|
|
124
|
+
hasNavigationScreen(navigation, "Appearance")),
|
|
125
|
+
notifications:
|
|
126
|
+
notificationsConfig === true ||
|
|
127
|
+
(notificationsConfig === "auto" &&
|
|
128
|
+
notificationService !== null &&
|
|
129
|
+
hasNavigationScreen(navigation, "Notifications")),
|
|
130
|
+
about:
|
|
131
|
+
aboutConfig === true ||
|
|
132
|
+
(aboutConfig === "auto" && hasNavigationScreen(navigation, "About")),
|
|
133
|
+
legal:
|
|
134
|
+
legalConfig === true ||
|
|
135
|
+
(legalConfig === "auto" && hasNavigationScreen(navigation, "Legal")),
|
|
118
136
|
};
|
|
119
137
|
}, [config, navigation]);
|
|
120
138
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const handleAboutPress = () => {
|
|
126
|
-
navigation.navigate('About' as never);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const handleLegalPress = () => {
|
|
130
|
-
navigation.navigate('Legal' as never);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const handleNotificationsPress = async () => {
|
|
134
|
-
if (notificationService) {
|
|
139
|
+
const handleNotificationsToggle = async (value: boolean) => {
|
|
140
|
+
if (notificationService && !value) {
|
|
135
141
|
const hasPermissions = await notificationService.hasPermissions();
|
|
136
142
|
if (!hasPermissions) {
|
|
137
143
|
await notificationService.requestPermissions();
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
|
-
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const handleClose = () => {
|
|
144
|
-
// Try to go back in current navigator first
|
|
145
|
-
if (navigation.canGoBack()) {
|
|
146
|
-
navigation.goBack();
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// If we can't go back in current navigator, try to find parent navigator
|
|
151
|
-
// This handles the case where Settings is the root screen of a stack
|
|
152
|
-
let parent = navigation.getParent();
|
|
153
|
-
let depth = 0;
|
|
154
|
-
const maxDepth = 5; // Safety limit to prevent infinite loops
|
|
155
|
-
|
|
156
|
-
// Traverse up the navigation tree to find a navigator that can go back
|
|
157
|
-
while (parent && depth < maxDepth) {
|
|
158
|
-
if (parent.canGoBack()) {
|
|
159
|
-
parent.goBack();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
parent = parent.getParent();
|
|
163
|
-
depth++;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// If no parent can go back, try using CommonActions to go back
|
|
167
|
-
// This is a fallback for edge cases
|
|
168
|
-
try {
|
|
169
|
-
navigation.dispatch(CommonActions.goBack());
|
|
170
|
-
} catch (error) {
|
|
171
|
-
// If all else fails, silently fail (close button just won't work)
|
|
172
|
-
/* eslint-disable-next-line no-console */
|
|
173
|
-
if (__DEV__) {
|
|
174
|
-
console.warn('[SettingsScreen] Could not navigate back:', error);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
146
|
+
setNotificationsEnabled(value);
|
|
177
147
|
};
|
|
178
148
|
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const onboardingStore = useOnboardingStore.getState();
|
|
187
|
-
// Reset onboarding state
|
|
188
|
-
await onboardingStore.reset();
|
|
189
|
-
// Emit event to trigger navigation to onboarding
|
|
190
|
-
DeviceEventEmitter.emit('reset-onboarding');
|
|
191
|
-
// Close settings first - try parent navigator if current can't go back
|
|
192
|
-
if (navigation.canGoBack()) {
|
|
193
|
-
navigation.goBack();
|
|
194
|
-
} else {
|
|
195
|
-
// Try to find parent navigator that can go back
|
|
196
|
-
let parent = navigation.getParent();
|
|
197
|
-
let depth = 0;
|
|
198
|
-
const maxDepth = 5;
|
|
199
|
-
|
|
200
|
-
while (parent && depth < maxDepth) {
|
|
201
|
-
if (parent.canGoBack()) {
|
|
202
|
-
parent.goBack();
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
parent = parent.getParent();
|
|
206
|
-
depth++;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Fallback to CommonActions
|
|
210
|
-
if (!parent || depth >= maxDepth) {
|
|
211
|
-
try {
|
|
212
|
-
navigation.dispatch(CommonActions.goBack());
|
|
213
|
-
} catch (error) {
|
|
214
|
-
/* eslint-disable-next-line no-console */
|
|
215
|
-
if (__DEV__) {
|
|
216
|
-
console.warn('[SettingsScreen] Could not navigate back:', error);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
149
|
+
const handleNotificationsPress = async () => {
|
|
150
|
+
if (notificationService) {
|
|
151
|
+
const hasPermissions = await notificationService.hasPermissions();
|
|
152
|
+
if (!hasPermissions) {
|
|
153
|
+
await notificationService.requestPermissions();
|
|
220
154
|
}
|
|
221
|
-
// Small delay to ensure navigation completes
|
|
222
|
-
setTimeout(() => {
|
|
223
|
-
DeviceEventEmitter.emit('show-onboarding');
|
|
224
|
-
}, 100);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
Alert.alert(
|
|
227
|
-
'Error',
|
|
228
|
-
'Failed to show onboarding. Please try again.',
|
|
229
|
-
[{ text: 'OK' }],
|
|
230
|
-
);
|
|
231
155
|
}
|
|
156
|
+
navigation.navigate("Notifications" as never);
|
|
232
157
|
};
|
|
233
158
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
console.log('[SettingsScreen] Navigation state:', navigation.getState());
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Check if any features are enabled
|
|
243
|
-
const hasAnyFeatures = features.appearance || features.notifications || features.about || features.legal;
|
|
159
|
+
const hasAnyFeatures =
|
|
160
|
+
features.appearance ||
|
|
161
|
+
features.notifications ||
|
|
162
|
+
features.about ||
|
|
163
|
+
features.legal;
|
|
244
164
|
|
|
245
165
|
return (
|
|
246
|
-
<
|
|
247
|
-
{
|
|
248
|
-
<View style={[
|
|
249
|
-
styles.header,
|
|
250
|
-
{
|
|
251
|
-
borderBottomColor: tokens.colors.borderLight,
|
|
252
|
-
backgroundColor: tokens.colors.surface,
|
|
253
|
-
paddingTop: insets.top,
|
|
254
|
-
}
|
|
255
|
-
]}>
|
|
256
|
-
<AtomicText type="headlineLarge" style={{ color: tokens.colors.textPrimary, flex: 1 }}>
|
|
257
|
-
{t('navigation.settings') || 'Settings'}
|
|
258
|
-
</AtomicText>
|
|
259
|
-
<TouchableOpacity
|
|
260
|
-
onPress={handleClose}
|
|
261
|
-
style={styles.closeButton}
|
|
262
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
263
|
-
testID="close-settings-button"
|
|
264
|
-
>
|
|
265
|
-
<AtomicIcon name="X" size="lg" color="primary" />
|
|
266
|
-
</TouchableOpacity>
|
|
267
|
-
</View>
|
|
268
|
-
|
|
269
|
-
{/* Appearance Section */}
|
|
270
|
-
{features.appearance && (
|
|
271
|
-
<View style={{ marginBottom: tokens.spacing.md }}>
|
|
272
|
-
<AtomicText type="labelMedium" color="textSecondary" style={styles.sectionHeader}>
|
|
273
|
-
{t('settings.sections.appearance')}
|
|
274
|
-
</AtomicText>
|
|
275
|
-
<SettingItem
|
|
276
|
-
icon="Palette"
|
|
277
|
-
iconGradient={((tokens.colors as any).settingGradients?.themeLight as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
|
|
278
|
-
title={t('settings.appearance.title')}
|
|
279
|
-
description={t('settings.appearance.themeDescription')}
|
|
280
|
-
onPress={handleAppearancePress}
|
|
281
|
-
testID="appearance-button"
|
|
282
|
-
/>
|
|
283
|
-
</View>
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
{/* General Section - Notifications */}
|
|
287
|
-
{features.notifications && (
|
|
288
|
-
<View style={{ marginBottom: tokens.spacing.md }}>
|
|
289
|
-
<AtomicText type="labelMedium" color="textSecondary" style={styles.sectionHeader}>
|
|
290
|
-
{t('settings.sections.general')}
|
|
291
|
-
</AtomicText>
|
|
292
|
-
<SettingItem
|
|
293
|
-
icon="Bell"
|
|
294
|
-
iconGradient={((tokens.colors as any).settingGradients?.notifications as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
|
|
295
|
-
title={t('settings.notifications.title')}
|
|
296
|
-
description={t('settings.notifications.description')}
|
|
297
|
-
onPress={handleNotificationsPress}
|
|
298
|
-
testID="notifications-button"
|
|
299
|
-
/>
|
|
300
|
-
</View>
|
|
301
|
-
)}
|
|
166
|
+
<View style={[styles.container, { backgroundColor: colors.backgroundPrimary }]}>
|
|
167
|
+
<StatusBar barStyle={isDark ? "light-content" : "dark-content"} />
|
|
302
168
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
169
|
+
<ScrollView
|
|
170
|
+
style={styles.scrollView}
|
|
171
|
+
contentContainerStyle={[
|
|
172
|
+
styles.scrollContent,
|
|
173
|
+
{
|
|
174
|
+
paddingTop: insets.top + 16,
|
|
175
|
+
paddingBottom: 100,
|
|
176
|
+
},
|
|
177
|
+
]}
|
|
178
|
+
showsVerticalScrollIndicator={false}
|
|
179
|
+
>
|
|
180
|
+
{showUserProfile && (
|
|
181
|
+
<UserProfileHeader
|
|
182
|
+
displayName={userProfile?.displayName}
|
|
183
|
+
userId={userProfile?.userId}
|
|
184
|
+
isGuest={userProfile?.isGuest}
|
|
185
|
+
avatarUrl={userProfile?.avatarUrl}
|
|
186
|
+
accountSettingsRoute={userProfile?.accountSettingsRoute}
|
|
187
|
+
onPress={userProfile?.onPress}
|
|
316
188
|
/>
|
|
317
|
-
|
|
318
|
-
)}
|
|
189
|
+
)}
|
|
319
190
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
<View style={{ marginBottom: tokens.spacing.md }}>
|
|
323
|
-
<AtomicText type="labelMedium" color="textSecondary" style={styles.sectionHeader}>
|
|
324
|
-
{t('settings.sections.about')}
|
|
325
|
-
</AtomicText>
|
|
326
|
-
{features.about && (
|
|
191
|
+
{features.appearance && (
|
|
192
|
+
<SettingsSection title={t("settings.sections.app.title")}>
|
|
327
193
|
<SettingItem
|
|
328
|
-
icon=
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
onPress={handleAboutPress}
|
|
333
|
-
testID="about-button"
|
|
194
|
+
icon={Palette}
|
|
195
|
+
title={t("settings.appearance.title")}
|
|
196
|
+
value={t("settings.appearance.description")}
|
|
197
|
+
onPress={() => navigation.navigate("Appearance" as never)}
|
|
334
198
|
/>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
{
|
|
199
|
+
</SettingsSection>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{features.notifications && (
|
|
203
|
+
<SettingsSection title={t("settings.sections.general")}>
|
|
340
204
|
<SettingItem
|
|
341
|
-
icon=
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
205
|
+
icon={Bell}
|
|
206
|
+
title={t("settings.notifications.title")}
|
|
207
|
+
showSwitch={true}
|
|
208
|
+
switchValue={notificationsEnabled}
|
|
209
|
+
onSwitchChange={handleNotificationsToggle}
|
|
210
|
+
isLast={true}
|
|
347
211
|
/>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
212
|
+
</SettingsSection>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{(features.about || features.legal) && (
|
|
216
|
+
<SettingsSection title={t("settings.sections.about")}>
|
|
217
|
+
{features.about && (
|
|
218
|
+
<SettingItem
|
|
219
|
+
icon={Info}
|
|
220
|
+
title={t("settings.about.title")}
|
|
221
|
+
value={t("settings.about.description")}
|
|
222
|
+
onPress={() => navigation.navigate("About" as never)}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
{features.legal && (
|
|
226
|
+
<SettingItem
|
|
227
|
+
icon={FileText}
|
|
228
|
+
title={t("settings.legal.title")}
|
|
229
|
+
value={t("settings.legal.description")}
|
|
230
|
+
onPress={() => navigation.navigate("Legal" as never)}
|
|
231
|
+
isLast={true}
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
</SettingsSection>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{!hasAnyFeatures && (
|
|
238
|
+
<View style={styles.emptyContainer}>
|
|
239
|
+
<SettingsSection
|
|
240
|
+
title={t("settings.noOptionsAvailable") || "No settings available"}
|
|
241
|
+
>
|
|
242
|
+
<View />
|
|
243
|
+
</SettingsSection>
|
|
244
|
+
</View>
|
|
245
|
+
)}
|
|
351
246
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<AtomicText type="labelMedium" color="textSecondary" style={styles.sectionHeader}>
|
|
356
|
-
{t('settings.noOptionsAvailable') || 'No settings available'}
|
|
357
|
-
</AtomicText>
|
|
358
|
-
</View>
|
|
359
|
-
)}
|
|
360
|
-
</ScreenLayout>
|
|
247
|
+
{showFooter && <SettingsFooter versionText={footerText} />}
|
|
248
|
+
</ScrollView>
|
|
249
|
+
</View>
|
|
361
250
|
);
|
|
362
251
|
};
|
|
363
252
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
alignItems: 'center',
|
|
368
|
-
justifyContent: 'space-between',
|
|
369
|
-
paddingHorizontal: tokens.spacing.md,
|
|
370
|
-
paddingBottom: tokens.spacing.md,
|
|
371
|
-
paddingTop: tokens.spacing.md,
|
|
372
|
-
borderBottomWidth: 1,
|
|
373
|
-
zIndex: 1000,
|
|
253
|
+
const styles = StyleSheet.create({
|
|
254
|
+
container: {
|
|
255
|
+
flex: 1,
|
|
374
256
|
},
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
paddingTop: tokens.spacing.lg,
|
|
378
|
-
paddingBottom: tokens.spacing.md,
|
|
379
|
-
textTransform: 'uppercase',
|
|
380
|
-
letterSpacing: 1,
|
|
381
|
-
fontWeight: '600',
|
|
382
|
-
fontSize: 12,
|
|
257
|
+
scrollView: {
|
|
258
|
+
flex: 1,
|
|
383
259
|
},
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
260
|
+
scrollContent: {
|
|
261
|
+
flexGrow: 1,
|
|
262
|
+
},
|
|
263
|
+
emptyContainer: {
|
|
264
|
+
paddingVertical: 24,
|
|
387
265
|
},
|
|
388
266
|
});
|
|
389
|
-
|