@umituz/react-native-settings 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- // In your navigation stack
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="Palette"
87
- title="Theme"
88
- description="Change app theme"
89
- value="Dark"
105
+ icon={Palette}
106
+ title="Appearance"
107
+ value="Theme and language settings"
90
108
  onPress={() => navigation.navigate('Appearance')}
91
- iconGradient={['#FF6B6B', '#4ECDC4']}
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
- Main settings screen component with sections for Appearance, General, About & Legal.
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
- Reusable setting item component with gradient icons and Material Design styling.
226
+ Modern setting item component with Lucide icons and switch support.
138
227
 
139
228
  **Props:**
140
- - `icon: IconName` - Icon name from Lucide library
229
+ - `icon: React.ComponentType` - Icon component from lucide-react-native
141
230
  - `title: string` - Main title text
142
- - `description?: string` - Optional description
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
- - `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
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.5.0",
3
+ "version": "1.6.1",
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
- * 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
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 'react';
15
- import { View, StyleSheet, Pressable } from 'react-native';
16
- import { LinearGradient } from 'expo-linear-gradient';
17
- import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system-atoms';
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, type LucideIcon } from "lucide-react-native";
10
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
21
11
 
22
- interface SettingItemProps {
23
- /** Icon name from Lucide library */
24
- icon: IconName;
12
+ export interface SettingItemProps {
13
+ /** Icon component from lucide-react-native */
14
+ icon: LucideIcon | 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 chevron arrow on right (default: true if onPress exists) */
34
- showChevron?: boolean;
35
- /** Right element to display instead of chevron/value */
36
- rightElement?: React.ReactNode;
37
- /** Gradient colors for icon background */
38
- iconGradient?: string[];
39
- /** Make item look disabled */
40
- disabled?: boolean;
41
- /** Test ID for E2E testing */
42
- testID?: string;
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
- showChevron,
52
- rightElement,
53
- iconGradient,
54
- disabled = false,
55
- testID,
40
+ showSwitch = false,
41
+ switchValue,
42
+ onSwitchChange,
43
+ isLast = false,
44
+ iconColor,
45
+ titleColor,
56
46
  }) => {
57
47
  const tokens = useAppDesignTokens();
58
- const styles = getStyles(tokens);
48
+ const colors = tokens.colors;
59
49
 
60
- // Gradient colors for icon background
61
- const gradientColors: readonly [string, string, ...string[]] = (iconGradient && iconGradient.length >= 2)
62
- ? (iconGradient as unknown as readonly [string, string, ...string[]])
63
- : [tokens.colors.surface, tokens.colors.surface] as const;
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
- const content = (
66
- <View style={[styles.listItem, disabled && styles.disabled]}>
67
- {/* Left: Icon with gradient */}
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>
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
- {/* Center: Title and description */}
80
- <View style={styles.contentContainer}>
81
- <AtomicText type="bodyLarge" color="textPrimary" style={styles.title} numberOfLines={2}>
82
- {title}
83
- </AtomicText>
84
- {description && (
85
- <AtomicText type="bodySmall" color="textSecondary" style={styles.description} numberOfLines={2}>
86
- {description}
87
- </AtomicText>
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 getStyles = (tokens: DesignTokens) =>
118
- StyleSheet.create({
119
- pressable: {
120
- borderRadius: tokens.borders.radius.md,
121
- },
122
- listItem: {
123
- flexDirection: 'row',
124
- alignItems: 'center',
125
- paddingVertical: tokens.spacing.sm,
126
- paddingHorizontal: tokens.spacing.md,
127
- minHeight: 64,
128
- },
129
- disabled: {
130
- opacity: 0.5,
131
- },
132
- contentContainer: {
133
- flex: 1,
134
- marginLeft: tokens.spacing.md,
135
- marginRight: tokens.spacing.md,
136
- },
137
- leftContainer: {
138
- marginRight: tokens.spacing.md,
139
- justifyContent: 'center',
140
- },
141
- iconContainer: {
142
- width: 44,
143
- height: 44,
144
- borderRadius: 22,
145
- alignItems: 'center',
146
- justifyContent: 'center',
147
- overflow: 'hidden',
148
- borderWidth: 1,
149
- borderColor: `${tokens.colors.primary}20`,
150
- },
151
- title: {
152
- fontWeight: '600',
153
- marginBottom: tokens.spacing.xs,
154
- },
155
- description: {
156
- marginTop: tokens.spacing.xs,
157
- opacity: 0.8,
158
- },
159
- rightContainer: {
160
- justifyContent: 'center',
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 'react';
15
- import { DeviceEventEmitter, Alert, View, TouchableOpacity, StyleSheet } from 'react-native';
16
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
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
- import { useNavigation, CommonActions } from '@react-navigation/native';
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('@umituz/react-native-notifications').notificationService;
33
+ notificationService = require("@umituz/react-native-notifications")
34
+ .notificationService;
31
35
  } catch {
32
- // Package not available, notificationService will be null
36
+ // Package not available
33
37
  }
34
38
 
35
- // Optional onboarding store - only import if package is available
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('@umituz/react-native-onboarding');
43
+ const onboardingPackage = require("@umituz/react-native-onboarding");
40
44
  useOnboardingStore = onboardingPackage.useOnboardingStore;
41
45
  } catch {
42
- // Package not available, useOnboardingStore will be null
46
+ // Package not available
43
47
  }
44
48
 
45
49
  /**
46
- * Check if a navigation screen exists in the navigation state
50
+ * Check if navigation screen exists
47
51
  */
48
- const hasNavigationScreen = (navigation: any, screenName: string): boolean => {
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 { currentLanguage, t } = useLocalization();
94
- const styles = getStyles(tokens);
108
+ const { t } = useLocalization();
109
+ const [notificationsEnabled, setNotificationsEnabled] = useState(true);
95
110
 
96
- const currentLang = getLanguageByCode(currentLanguage);
97
- const languageDisplay = currentLang ? `${currentLang.flag} ${currentLang.nativeName}` : 'English';
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.appearance ?? 'auto';
103
- const notificationsConfig = config.notifications ?? 'auto';
104
- const aboutConfig = config.about ?? 'auto';
105
- const legalConfig = config.legal ?? 'auto';
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: appearanceConfig === true ||
109
- (appearanceConfig === 'auto' && hasNavigationScreen(navigation, 'Appearance')),
110
- notifications: notificationsConfig === true ||
111
- (notificationsConfig === 'auto' &&
112
- notificationService !== null &&
113
- hasNavigationScreen(navigation, 'Notifications')),
114
- about: aboutConfig === true ||
115
- (aboutConfig === 'auto' && hasNavigationScreen(navigation, 'About')),
116
- legal: legalConfig === true ||
117
- (legalConfig === 'auto' && hasNavigationScreen(navigation, 'Legal')),
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 handleAppearancePress = () => {
122
- navigation.navigate('Appearance' as never);
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
- navigation.navigate('Notifications' as never);
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 handleShowOnboarding = async () => {
180
- if (!useOnboardingStore) {
181
- Alert.alert('Error', 'Onboarding package is not available');
182
- return;
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
- // Debug: Log features to help diagnose empty screen issues
235
- /* eslint-disable-next-line no-console */
236
- if (__DEV__) {
237
- console.log('[SettingsScreen] Features:', features);
238
- console.log('[SettingsScreen] Config:', config);
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
- <ScreenLayout testID="settings-screen" hideScrollIndicator>
247
- {/* Header with Close Button */}
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
- {/* Development/Test: Show Onboarding */}
304
- {__DEV__ && useOnboardingStore && (
305
- <View style={{ marginBottom: tokens.spacing.md }}>
306
- <AtomicText type="labelMedium" color="textSecondary" style={styles.sectionHeader}>
307
- Development
308
- </AtomicText>
309
- <SettingItem
310
- icon="Play"
311
- iconGradient={((tokens.colors as any).settingGradients?.info as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
312
- title="Show Onboarding (Dev)"
313
- description="Navigate to onboarding screen"
314
- onPress={handleShowOnboarding}
315
- testID="show-onboarding-button"
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
- </View>
318
- )}
189
+ )}
319
190
 
320
- {/* About & Legal Section */}
321
- {(features.about || features.legal) && (
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="Info"
329
- iconGradient={((tokens.colors as any).settingGradients?.info as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
330
- title={t('settings.about.title')}
331
- description={t('settings.about.description')}
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
- {features.about && features.legal && (
337
- <View style={{ height: 1, backgroundColor: tokens.colors.borderLight, marginVertical: tokens.spacing.sm }} />
338
- )}
339
- {features.legal && (
199
+ </SettingsSection>
200
+ )}
201
+
202
+ {features.notifications && (
203
+ <SettingsSection title={t("settings.sections.general")}>
340
204
  <SettingItem
341
- icon="FileText"
342
- iconGradient={((tokens.colors as any).settingGradients?.info as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
343
- title={t('settings.legal.title')}
344
- description={t('settings.legal.description')}
345
- onPress={handleLegalPress}
346
- testID="legal-button"
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
- </View>
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
- {/* Fallback: Show message if no features are enabled */}
353
- {!hasAnyFeatures && (
354
- <View>
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 getStyles = (tokens: any) => StyleSheet.create({
365
- header: {
366
- flexDirection: 'row',
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
- sectionHeader: {
376
- paddingHorizontal: tokens.spacing.lg,
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
- closeButton: {
385
- padding: tokens.spacing.sm,
386
- marginLeft: tokens.spacing.sm,
260
+ scrollContent: {
261
+ flexGrow: 1,
262
+ },
263
+ emptyContainer: {
264
+ paddingVertical: 24,
387
265
  },
388
266
  });
389
-