@umituz/react-native-settings 1.4.1 → 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.
@@ -1,369 +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, AtomicIcon, AtomicText, SectionHeader, SectionContainer, AtomicDivider } from '@umituz/react-native-design-system';
21
- import { SettingItem } from '../components/SettingItem';
22
- import { getLanguageByCode, useLocalization } from '@umituz/react-native-localization';
23
- import { SettingsConfig } from './types';
24
-
25
- // Optional notification service - only import if package is available
29
+ // Optional notification service
26
30
  let notificationService: any = null;
27
31
  try {
28
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
- notificationService = require('@umituz/react-native-notifications').notificationService;
33
+ notificationService = require("@umituz/react-native-notifications")
34
+ .notificationService;
30
35
  } catch {
31
- // Package not available, notificationService will be null
36
+ // Package not available
32
37
  }
33
38
 
34
- // Optional onboarding store - only import if package is available
39
+ // Optional onboarding store
35
40
  let useOnboardingStore: any = null;
36
41
  try {
37
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports
38
- const onboardingPackage = require('@umituz/react-native-onboarding');
43
+ const onboardingPackage = require("@umituz/react-native-onboarding");
39
44
  useOnboardingStore = onboardingPackage.useOnboardingStore;
40
45
  } catch {
41
- // Package not available, useOnboardingStore will be null
46
+ // Package not available
42
47
  }
43
48
 
44
49
  /**
45
- * Check if a navigation screen exists in the navigation state
50
+ * Check if navigation screen exists
46
51
  */
47
- const hasNavigationScreen = (navigation: any, screenName: string): boolean => {
52
+ const hasNavigationScreen = (
53
+ navigation: any,
54
+ screenName: string,
55
+ ): boolean => {
48
56
  try {
49
57
  const state = navigation.getState();
50
58
  if (!state) return false;
51
59
 
52
- // Recursively check all routes in navigation state
53
60
  const checkRoutes = (routes: any[]): boolean => {
54
61
  if (!routes || !Array.isArray(routes)) return false;
55
-
62
+
56
63
  for (const route of routes) {
57
- if (route.name === screenName) {
64
+ if (route.name === screenName) return true;
65
+ if (route.state?.routes && checkRoutes(route.state.routes)) {
58
66
  return true;
59
67
  }
60
- // Check nested navigators
61
- if (route.state?.routes) {
62
- if (checkRoutes(route.state.routes)) {
63
- return true;
64
- }
65
- }
66
68
  }
67
69
  return false;
68
70
  };
69
71
 
70
72
  return checkRoutes(state.routes || []);
71
73
  } catch {
72
- // If we can't check navigation state, assume it's not available
73
74
  return false;
74
75
  }
75
76
  };
76
77
 
77
78
  export interface SettingsScreenProps {
78
- /**
79
- * Configuration for which settings features to show
80
- * @default { appearance: 'auto', notifications: 'auto', about: 'auto', legal: 'auto' }
81
- */
82
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;
83
95
  }
84
96
 
85
- export const SettingsScreen: React.FC<SettingsScreenProps> = ({
86
- config = {}
97
+ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
98
+ config = {},
99
+ showUserProfile = false,
100
+ userProfile,
101
+ showFooter = true,
102
+ footerText,
87
103
  }) => {
88
104
  const navigation = useNavigation();
89
- // Only read themeMode, no theme logic here - theme logic belongs in theme package
90
105
  const { themeMode } = useDesignSystemTheme();
91
106
  const tokens = useAppDesignTokens();
92
107
  const insets = useSafeAreaInsets();
93
- const { currentLanguage, t } = useLocalization();
108
+ const { t } = useLocalization();
109
+ const [notificationsEnabled, setNotificationsEnabled] = useState(true);
94
110
 
95
- const currentLang = getLanguageByCode(currentLanguage);
96
- const languageDisplay = currentLang ? `${currentLang.flag} ${currentLang.nativeName}` : 'English';
97
- const themeDisplay = themeMode === 'dark' ? t('settings.darkMode') : t('settings.lightMode');
111
+ const isDark = themeMode === "dark";
112
+ const colors = tokens.colors;
98
113
 
99
- // Determine which features should be shown
100
114
  const features = useMemo(() => {
101
- const appearanceConfig = config.appearance ?? 'auto';
102
- const notificationsConfig = config.notifications ?? 'auto';
103
- const aboutConfig = config.about ?? 'auto';
104
- 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";
105
119
 
106
120
  return {
107
- appearance: appearanceConfig === true ||
108
- (appearanceConfig === 'auto' && hasNavigationScreen(navigation, 'Appearance')),
109
- notifications: notificationsConfig === true ||
110
- (notificationsConfig === 'auto' &&
111
- notificationService !== null &&
112
- hasNavigationScreen(navigation, 'Notifications')),
113
- about: aboutConfig === true ||
114
- (aboutConfig === 'auto' && hasNavigationScreen(navigation, 'About')),
115
- legal: legalConfig === true ||
116
- (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")),
117
136
  };
118
137
  }, [config, navigation]);
119
138
 
120
- const handleAppearancePress = () => {
121
- navigation.navigate('Appearance' as never);
122
- };
123
-
124
- const handleAboutPress = () => {
125
- navigation.navigate('About' as never);
126
- };
127
-
128
- const handleLegalPress = () => {
129
- navigation.navigate('Legal' as never);
130
- };
131
-
132
- const handleNotificationsPress = async () => {
133
- if (notificationService) {
139
+ const handleNotificationsToggle = async (value: boolean) => {
140
+ if (notificationService && !value) {
134
141
  const hasPermissions = await notificationService.hasPermissions();
135
142
  if (!hasPermissions) {
136
143
  await notificationService.requestPermissions();
137
144
  }
138
145
  }
139
- navigation.navigate('Notifications' as never);
140
- };
141
-
142
- const handleClose = () => {
143
- // Try to go back in current navigator first
144
- if (navigation.canGoBack()) {
145
- navigation.goBack();
146
- return;
147
- }
148
-
149
- // If we can't go back in current navigator, try to find parent navigator
150
- // This handles the case where Settings is the root screen of a stack
151
- let parent = navigation.getParent();
152
- let depth = 0;
153
- const maxDepth = 5; // Safety limit to prevent infinite loops
154
-
155
- // Traverse up the navigation tree to find a navigator that can go back
156
- while (parent && depth < maxDepth) {
157
- if (parent.canGoBack()) {
158
- parent.goBack();
159
- return;
160
- }
161
- parent = parent.getParent();
162
- depth++;
163
- }
164
-
165
- // If no parent can go back, try using CommonActions to go back
166
- // This is a fallback for edge cases
167
- try {
168
- navigation.dispatch(CommonActions.goBack());
169
- } catch (error) {
170
- // If all else fails, silently fail (close button just won't work)
171
- /* eslint-disable-next-line no-console */
172
- if (__DEV__) {
173
- console.warn('[SettingsScreen] Could not navigate back:', error);
174
- }
175
- }
146
+ setNotificationsEnabled(value);
176
147
  };
177
148
 
178
- const handleShowOnboarding = async () => {
179
- if (!useOnboardingStore) {
180
- Alert.alert('Error', 'Onboarding package is not available');
181
- return;
182
- }
183
-
184
- try {
185
- const onboardingStore = useOnboardingStore.getState();
186
- // Reset onboarding state
187
- await onboardingStore.reset();
188
- // Emit event to trigger navigation to onboarding
189
- DeviceEventEmitter.emit('reset-onboarding');
190
- // Close settings first - try parent navigator if current can't go back
191
- if (navigation.canGoBack()) {
192
- navigation.goBack();
193
- } else {
194
- // Try to find parent navigator that can go back
195
- let parent = navigation.getParent();
196
- let depth = 0;
197
- const maxDepth = 5;
198
-
199
- while (parent && depth < maxDepth) {
200
- if (parent.canGoBack()) {
201
- parent.goBack();
202
- break;
203
- }
204
- parent = parent.getParent();
205
- depth++;
206
- }
207
-
208
- // Fallback to CommonActions
209
- if (!parent || depth >= maxDepth) {
210
- try {
211
- navigation.dispatch(CommonActions.goBack());
212
- } catch (error) {
213
- /* eslint-disable-next-line no-console */
214
- if (__DEV__) {
215
- console.warn('[SettingsScreen] Could not navigate back:', error);
216
- }
217
- }
218
- }
149
+ const handleNotificationsPress = async () => {
150
+ if (notificationService) {
151
+ const hasPermissions = await notificationService.hasPermissions();
152
+ if (!hasPermissions) {
153
+ await notificationService.requestPermissions();
219
154
  }
220
- // Small delay to ensure navigation completes
221
- setTimeout(() => {
222
- DeviceEventEmitter.emit('show-onboarding');
223
- }, 100);
224
- } catch (error) {
225
- Alert.alert(
226
- 'Error',
227
- 'Failed to show onboarding. Please try again.',
228
- [{ text: 'OK' }],
229
- );
230
155
  }
156
+ navigation.navigate("Notifications" as never);
231
157
  };
232
158
 
233
- // Debug: Log features to help diagnose empty screen issues
234
- /* eslint-disable-next-line no-console */
235
- if (__DEV__) {
236
- console.log('[SettingsScreen] Features:', features);
237
- console.log('[SettingsScreen] Config:', config);
238
- console.log('[SettingsScreen] Navigation state:', navigation.getState());
239
- }
240
-
241
- // Check if any features are enabled
242
- 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;
243
164
 
244
165
  return (
245
- <ScreenLayout testID="settings-screen" hideScrollIndicator>
246
- {/* Header with Close Button */}
247
- <View style={[
248
- styles.header,
249
- {
250
- borderBottomColor: tokens.colors.borderLight,
251
- backgroundColor: tokens.colors.surface,
252
- paddingTop: insets.top,
253
- }
254
- ]}>
255
- <AtomicText type="headlineLarge" style={{ color: tokens.colors.textPrimary, flex: 1 }}>
256
- {t('navigation.settings') || 'Settings'}
257
- </AtomicText>
258
- <TouchableOpacity
259
- onPress={handleClose}
260
- style={styles.closeButton}
261
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
262
- testID="close-settings-button"
263
- >
264
- <AtomicIcon name="X" size="lg" color="primary" />
265
- </TouchableOpacity>
266
- </View>
166
+ <View style={[styles.container, { backgroundColor: colors.backgroundPrimary }]}>
167
+ <StatusBar barStyle={isDark ? "light-content" : "dark-content"} />
267
168
 
268
- {/* Appearance Section */}
269
- {features.appearance && (
270
- <SectionContainer>
271
- <SectionHeader>{t('settings.sections.appearance')}</SectionHeader>
272
- <SettingItem
273
- icon="Palette"
274
- iconGradient={((tokens.colors as any).settingGradients?.themeLight as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
275
- title={t('settings.appearance.title')}
276
- description={t('settings.appearance.themeDescription')}
277
- onPress={handleAppearancePress}
278
- testID="appearance-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}
279
188
  />
280
- </SectionContainer>
281
- )}
189
+ )}
282
190
 
283
- {/* General Section - Notifications */}
284
- {features.notifications && (
285
- <SectionContainer>
286
- <SectionHeader>{t('settings.sections.general')}</SectionHeader>
287
- <SettingItem
288
- icon="Bell"
289
- iconGradient={((tokens.colors as any).settingGradients?.notifications as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
290
- title={t('settings.notifications.title')}
291
- description={t('settings.notifications.description')}
292
- onPress={handleNotificationsPress}
293
- testID="notifications-button"
294
- />
295
- </SectionContainer>
296
- )}
297
-
298
- {/* Development/Test: Show Onboarding */}
299
- {__DEV__ && useOnboardingStore && (
300
- <SectionContainer>
301
- <SectionHeader>Development</SectionHeader>
302
- <SettingItem
303
- icon="Play"
304
- iconGradient={((tokens.colors as any).settingGradients?.info as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
305
- title="Show Onboarding (Dev)"
306
- description="Navigate to onboarding screen"
307
- onPress={handleShowOnboarding}
308
- testID="show-onboarding-button"
309
- />
310
- </SectionContainer>
311
- )}
312
-
313
- {/* About & Legal Section */}
314
- {(features.about || features.legal) && (
315
- <SectionContainer>
316
- <SectionHeader>{t('settings.sections.about')}</SectionHeader>
317
- {features.about && (
191
+ {features.appearance && (
192
+ <SettingsSection title={t("settings.sections.app.title")}>
318
193
  <SettingItem
319
- icon="Info"
320
- iconGradient={((tokens.colors as any).settingGradients?.info as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
321
- title={t('settings.about.title')}
322
- description={t('settings.about.description')}
323
- onPress={handleAboutPress}
324
- 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)}
325
198
  />
326
- )}
327
- {features.about && features.legal && <AtomicDivider />}
328
- {features.legal && (
199
+ </SettingsSection>
200
+ )}
201
+
202
+ {features.notifications && (
203
+ <SettingsSection title={t("settings.sections.general")}>
329
204
  <SettingItem
330
- icon="FileText"
331
- iconGradient={((tokens.colors as any).settingGradients?.info as unknown as string[]) || [tokens.colors.primary, tokens.colors.secondary]}
332
- title={t('settings.legal.title')}
333
- description={t('settings.legal.description')}
334
- onPress={handleLegalPress}
335
- testID="legal-button"
205
+ icon={Bell}
206
+ title={t("settings.notifications.title")}
207
+ showSwitch={true}
208
+ switchValue={notificationsEnabled}
209
+ onSwitchChange={handleNotificationsToggle}
210
+ isLast={true}
336
211
  />
337
- )}
338
- </SectionContainer>
339
- )}
212
+ </SettingsSection>
213
+ )}
340
214
 
341
- {/* Fallback: Show message if no features are enabled */}
342
- {!hasAnyFeatures && (
343
- <SectionContainer>
344
- <SectionHeader>
345
- {t('settings.noOptionsAvailable') || 'No settings available'}
346
- </SectionHeader>
347
- </SectionContainer>
348
- )}
349
- </ScreenLayout>
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
+ )}
246
+
247
+ {showFooter && <SettingsFooter versionText={footerText} />}
248
+ </ScrollView>
249
+ </View>
350
250
  );
351
251
  };
352
252
 
353
253
  const styles = StyleSheet.create({
354
- header: {
355
- flexDirection: 'row',
356
- alignItems: 'center',
357
- justifyContent: 'space-between',
358
- paddingHorizontal: 16,
359
- paddingBottom: 12,
360
- paddingTop: 12,
361
- borderBottomWidth: 1,
362
- zIndex: 1000,
254
+ container: {
255
+ flex: 1,
256
+ },
257
+ scrollView: {
258
+ flex: 1,
363
259
  },
364
- closeButton: {
365
- padding: 8,
366
- marginLeft: 8,
260
+ scrollContent: {
261
+ flexGrow: 1,
262
+ },
263
+ emptyContainer: {
264
+ paddingVertical: 24,
367
265
  },
368
266
  });
369
-