@umituz/react-native-settings 5.2.19 → 5.2.21
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/package.json +1 -1
- package/src/domains/about/presentation/hooks/useAboutInfo.ts +63 -98
- package/src/domains/feedback/presentation/components/SupportSection.tsx +2 -1
- package/src/domains/localization/infrastructure/hooks/useTranslation.ts +0 -3
- package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +0 -4
- package/src/domains/localization/infrastructure/storage/localizationStoreUtils.ts +6 -7
- package/src/domains/localization/presentation/components/LanguageItem.tsx +0 -2
- package/src/domains/localization/presentation/providers/LocalizationManager.tsx +2 -1
- package/src/domains/notifications/index.ts +7 -6
- package/src/domains/notifications/infrastructure/services/NotificationBadgeManager.ts +1 -1
- package/src/domains/notifications/infrastructure/services/NotificationManager.ts +1 -1
- package/src/domains/notifications/infrastructure/services/NotificationPermissions.ts +1 -1
- package/src/domains/notifications/infrastructure/storage/UnifiedNotificationStore.ts +223 -0
- package/src/domains/notifications/presentation/hooks/useNotificationSettingsUI.ts +2 -2
- package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.tsx +1 -1
- package/src/domains/notifications/quietHours/infrastructure/hooks/useQuietHoursActions.ts +6 -6
- package/src/domains/notifications/reminders/infrastructure/hooks/useReminderActions.ts +5 -5
- package/src/domains/notifications/reminders/presentation/screens/ReminderListScreen.tsx +1 -1
- package/src/domains/rating/presentation/hooks/useAppRating.tsx +2 -1
- package/src/infrastructure/storage/storeConfig.ts +114 -0
- package/src/infrastructure/utils/async/core.ts +3 -2
- package/src/infrastructure/utils/errorHandlers.ts +4 -2
- package/src/infrastructure/utils/index.ts +1 -1
- package/src/presentation/components/SettingsNavigationItem.tsx +109 -0
- package/src/presentation/navigation/utils/index.ts +8 -1
- package/src/presentation/navigation/utils/navigationHelpers.ts +118 -0
- package/src/presentation/screens/components/SettingsContent.tsx +21 -11
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +4 -2
- package/src/presentation/screens/types/UserFeatureConfig.ts +5 -4
- package/src/presentation/utils/config-creators/feature-configs.ts +3 -2
- package/src/presentation/utils/settingsConfigFactory.ts +2 -1
- package/src/utils/appUtils.ts +0 -6
- package/src/utils/errorUtils.ts +54 -0
- package/src/utils/hooks/index.ts +6 -0
- package/src/utils/hooks/useAsyncStateUpdate.ts +114 -0
- package/src/utils/hooks/useMountSafety.ts +30 -0
- package/src/utils/index.ts +2 -0
- package/src/domains/about/presentation/hooks/useAboutInfo.utils.ts +0 -167
- package/src/domains/notifications/infrastructure/storage/NotificationsStore.ts +0 -45
- package/src/domains/notifications/infrastructure/utils/dev.ts +0 -22
- package/src/domains/notifications/reminders/infrastructure/storage/RemindersStore.ts +0 -152
- package/src/infrastructure/utils/styleUtils.ts +0 -7
- package/src/presentation/screens/components/GamificationSettingsItem.tsx +0 -55
- package/src/presentation/screens/components/SubscriptionSettingsItem.tsx +0 -38
- package/src/presentation/screens/components/VideoTutorialSettingsItem.tsx +0 -51
- package/src/presentation/screens/components/WalletSettingsItem.tsx +0 -36
|
@@ -4,28 +4,28 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useCallback } from 'react';
|
|
7
|
-
import {
|
|
7
|
+
import { useNotificationStore, useQuietHours } from '../../../infrastructure/storage/UnifiedNotificationStore';
|
|
8
8
|
import type { QuietHoursConfig } from '../../../infrastructure/services/types';
|
|
9
9
|
|
|
10
10
|
export const useQuietHoursActions = () => {
|
|
11
11
|
const quietHours = useQuietHours();
|
|
12
|
-
const { updateQuietHours } =
|
|
12
|
+
const { updateQuietHours } = useNotificationStore();
|
|
13
13
|
|
|
14
14
|
const setQuietHoursEnabled = useCallback(async (enabled: boolean): Promise<void> => {
|
|
15
15
|
// Use getState() to avoid stale closure and race conditions
|
|
16
|
-
const currentQuietHours =
|
|
16
|
+
const currentQuietHours = useNotificationStore.getState().preferences.quietHours;
|
|
17
17
|
await updateQuietHours({ ...currentQuietHours, enabled });
|
|
18
18
|
}, [updateQuietHours]);
|
|
19
19
|
|
|
20
20
|
const setStartTime = useCallback(async (hour: number, minute: number): Promise<void> => {
|
|
21
21
|
// Use getState() to avoid stale closure and race conditions
|
|
22
|
-
const currentQuietHours =
|
|
22
|
+
const currentQuietHours = useNotificationStore.getState().preferences.quietHours;
|
|
23
23
|
await updateQuietHours({ ...currentQuietHours, startHour: hour, startMinute: minute });
|
|
24
24
|
}, [updateQuietHours]);
|
|
25
25
|
|
|
26
26
|
const setEndTime = useCallback(async (hour: number, minute: number): Promise<void> => {
|
|
27
27
|
// Use getState() to avoid stale closure and race conditions
|
|
28
|
-
const currentQuietHours =
|
|
28
|
+
const currentQuietHours = useNotificationStore.getState().preferences.quietHours;
|
|
29
29
|
await updateQuietHours({ ...currentQuietHours, endHour: hour, endMinute: minute });
|
|
30
30
|
}, [updateQuietHours]);
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ export const useQuietHoursActions = () => {
|
|
|
35
35
|
|
|
36
36
|
const isInQuietHours = useCallback((): boolean => {
|
|
37
37
|
// Use getState() to get current quietHours for consistency
|
|
38
|
-
const currentQuietHours =
|
|
38
|
+
const currentQuietHours = useNotificationStore.getState().preferences.quietHours;
|
|
39
39
|
|
|
40
40
|
if (!currentQuietHours.enabled) return false;
|
|
41
41
|
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useCallback, useMemo } from 'react';
|
|
7
|
-
import {
|
|
7
|
+
import { useNotificationStore } from '../../../infrastructure/storage/UnifiedNotificationStore';
|
|
8
8
|
import { NotificationScheduler } from '../../../infrastructure/services/NotificationScheduler';
|
|
9
9
|
import { generateReminderId } from '../../../infrastructure/utils/idGenerator';
|
|
10
10
|
import { buildTrigger } from '../../../infrastructure/utils/triggerBuilder';
|
|
11
11
|
import type { Reminder, CreateReminderInput, UpdateReminderInput } from '../../../infrastructure/services/types';
|
|
12
12
|
|
|
13
13
|
export const useReminderActions = () => {
|
|
14
|
-
const { addReminder, updateReminder, deleteReminder, toggleReminder: _toggleReminder } =
|
|
14
|
+
const { addReminder, updateReminder, deleteReminder, toggleReminder: _toggleReminder } = useNotificationStore();
|
|
15
15
|
|
|
16
16
|
// Lazy initialization of scheduler
|
|
17
17
|
const scheduler = useMemo(() => new NotificationScheduler(), []);
|
|
@@ -47,7 +47,7 @@ export const useReminderActions = () => {
|
|
|
47
47
|
|
|
48
48
|
const editReminder = useCallback(async (id: string, input: UpdateReminderInput): Promise<void> => {
|
|
49
49
|
// Get current state BEFORE async operations to prevent race condition
|
|
50
|
-
const existing =
|
|
50
|
+
const existing = useNotificationStore.getState().reminders.find(r => r.id === id);
|
|
51
51
|
|
|
52
52
|
if (!existing) {
|
|
53
53
|
throw new Error(`Reminder with id ${id} not found`);
|
|
@@ -86,7 +86,7 @@ export const useReminderActions = () => {
|
|
|
86
86
|
|
|
87
87
|
const removeReminder = useCallback(async (id: string): Promise<void> => {
|
|
88
88
|
// Get current state BEFORE async operations to prevent race condition
|
|
89
|
-
const reminder =
|
|
89
|
+
const reminder = useNotificationStore.getState().reminders.find(r => r.id === id);
|
|
90
90
|
|
|
91
91
|
if (!reminder) {
|
|
92
92
|
throw new Error(`Reminder with id ${id} not found`);
|
|
@@ -105,7 +105,7 @@ export const useReminderActions = () => {
|
|
|
105
105
|
|
|
106
106
|
const toggleReminderEnabled = useCallback(async (id: string): Promise<void> => {
|
|
107
107
|
// Get current state BEFORE async operations to prevent race condition
|
|
108
|
-
const reminder =
|
|
108
|
+
const reminder = useNotificationStore.getState().reminders.find(r => r.id === id);
|
|
109
109
|
|
|
110
110
|
if (!reminder) {
|
|
111
111
|
throw new Error(`Reminder with id ${id} not found`);
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
useAppDesignTokens
|
|
16
16
|
} from '@umituz/react-native-design-system';
|
|
17
17
|
import { ReminderItem } from '../components/ReminderItem';
|
|
18
|
-
import { useReminders, useRemindersLoading } from '
|
|
18
|
+
import { useReminders, useRemindersLoading } from '../../../infrastructure/storage/UnifiedNotificationStore';
|
|
19
19
|
import { useReminderActions } from '../../infrastructure/hooks/useReminderActions';
|
|
20
20
|
import type { Reminder, ReminderTranslations } from '../../../infrastructure/services/types';
|
|
21
21
|
import { devError } from '../../../../../utils/devUtils';
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import { DEFAULT_RATING_CONFIG } from "../../domain/entities/RatingConfig";
|
|
14
14
|
import * as RatingService from "../../application/services/RatingService";
|
|
15
15
|
import { RatingPromptModal } from "../components/RatingPromptModal";
|
|
16
|
+
import { isDev } from "../../../../utils/devUtils";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* App rating hook with 2-step prompt flow
|
|
@@ -72,7 +73,7 @@ export function useAppRating(config: RatingConfig): UseAppRatingResult {
|
|
|
72
73
|
await StoreReview.requestReview();
|
|
73
74
|
}
|
|
74
75
|
} catch (error) {
|
|
75
|
-
if (
|
|
76
|
+
if (isDev()) {
|
|
76
77
|
console.error('[useAppRating] Failed to request review:', error);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store Configuration Standards
|
|
3
|
+
* Standardized patterns for all Zustand stores
|
|
4
|
+
*
|
|
5
|
+
* Provides consistent configuration for:
|
|
6
|
+
* - Persistence settings
|
|
7
|
+
* - Version management
|
|
8
|
+
* - State partializing (excluding transient state)
|
|
9
|
+
* - Storage service integration
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StateStorage } from 'zustand/middleware';
|
|
13
|
+
import { storageService } from '@umituz/react-native-design-system';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Standard store configuration interface
|
|
17
|
+
*/
|
|
18
|
+
export interface StandardStoreConfig<T> {
|
|
19
|
+
/** Unique storage key name */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Store version for migrations */
|
|
22
|
+
version: number;
|
|
23
|
+
/** Enable persistence */
|
|
24
|
+
persist: boolean;
|
|
25
|
+
/** Custom partialize function to control what gets persisted */
|
|
26
|
+
partialize?: (state: T) => Partial<T>;
|
|
27
|
+
/** Storage service (defaults to design-system's storageService) */
|
|
28
|
+
storage?: StateStorage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create standardized store configuration
|
|
33
|
+
*
|
|
34
|
+
* @param config - Store configuration
|
|
35
|
+
* @returns Complete store config with defaults
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const config = createStoreConfig({
|
|
40
|
+
* name: 'my-store',
|
|
41
|
+
* version: 1,
|
|
42
|
+
* persist: true,
|
|
43
|
+
* partialize: excludeTransientState,
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const createStoreConfig = <T extends object>(
|
|
48
|
+
config: StandardStoreConfig<T>
|
|
49
|
+
): Required<StandardStoreConfig<T>> => {
|
|
50
|
+
return {
|
|
51
|
+
name: config.name,
|
|
52
|
+
version: config.version,
|
|
53
|
+
persist: config.persist,
|
|
54
|
+
partialize: config.partialize || ((state) => state),
|
|
55
|
+
storage: config.storage || storageService,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Base interface for store state with transient flags
|
|
61
|
+
*/
|
|
62
|
+
export interface BaseStoreState {
|
|
63
|
+
/** Loading state (never persisted) */
|
|
64
|
+
isLoading?: boolean;
|
|
65
|
+
/** Initialization state (never persisted) */
|
|
66
|
+
isInitialized?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Standard partialize function that excludes loading/initialization flags
|
|
71
|
+
* These flags are runtime state and should never be persisted
|
|
72
|
+
*
|
|
73
|
+
* @param state - Store state
|
|
74
|
+
* @returns State with transient flags excluded
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const config = createStoreConfig({
|
|
79
|
+
* name: 'my-store',
|
|
80
|
+
* version: 1,
|
|
81
|
+
* persist: true,
|
|
82
|
+
* partialize: excludeTransientState,
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export const excludeTransientState = <T extends BaseStoreState>(
|
|
87
|
+
state: T
|
|
88
|
+
): Partial<T> => {
|
|
89
|
+
const { isLoading, isInitialized, ...persistedState } = state;
|
|
90
|
+
return persistedState as Partial<T>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create partialize function that excludes specific keys
|
|
95
|
+
*
|
|
96
|
+
* @param excludeKeys - Keys to exclude from persistence
|
|
97
|
+
* @returns Partialize function
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const partialize = createPartializeExcluding(['isLoading', 'error', 'tempData']);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export const createPartializeExcluding = <T extends object>(
|
|
105
|
+
excludeKeys: (keyof T)[]
|
|
106
|
+
) => {
|
|
107
|
+
return (state: T): Partial<T> => {
|
|
108
|
+
const result = { ...state };
|
|
109
|
+
excludeKeys.forEach((key) => {
|
|
110
|
+
delete result[key];
|
|
111
|
+
});
|
|
112
|
+
return result;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ValidationResult } from "../validation";
|
|
7
|
+
import { isDev } from '../../../utils/devUtils';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Result type for async operations
|
|
@@ -58,7 +59,7 @@ export const createAsyncHandler = <T extends unknown[], R>(
|
|
|
58
59
|
onSuccess?.(result);
|
|
59
60
|
} catch (callbackError) {
|
|
60
61
|
// Log callback error but don't treat it as handler error
|
|
61
|
-
if (
|
|
62
|
+
if (isDev()) {
|
|
62
63
|
console.error("[createAsyncHandler] onSuccess callback error:", callbackError);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -72,7 +73,7 @@ export const createAsyncHandler = <T extends unknown[], R>(
|
|
|
72
73
|
onLoadingEnd?.();
|
|
73
74
|
} catch (callbackError) {
|
|
74
75
|
// Log callback error but don't throw
|
|
75
|
-
if (
|
|
76
|
+
if (isDev()) {
|
|
76
77
|
console.error("[createAsyncHandler] onLoadingEnd callback error:", callbackError);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* FIXED: Added safety checks for showToast and proper error handling
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isDev } from '../../utils/devUtils';
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Error types for better error classification
|
|
9
11
|
*/
|
|
@@ -182,7 +184,7 @@ export const logError = (
|
|
|
182
184
|
};
|
|
183
185
|
|
|
184
186
|
// Log errors in development mode
|
|
185
|
-
if (
|
|
187
|
+
if (isDev()) {
|
|
186
188
|
console.error("[Error]", logData);
|
|
187
189
|
}
|
|
188
190
|
|
|
@@ -216,7 +218,7 @@ export const handleError = (
|
|
|
216
218
|
}
|
|
217
219
|
} catch (toastError) {
|
|
218
220
|
// Log toast error but don't crash error handling
|
|
219
|
-
if (
|
|
221
|
+
if (isDev()) {
|
|
220
222
|
console.error("[ErrorHandlers] showToast failed:", toastError);
|
|
221
223
|
}
|
|
222
224
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Settings Navigation Item
|
|
3
|
+
* Consolidates VideoTutorialSettingsItem, GamificationSettingsItem,
|
|
4
|
+
* WalletSettingsItem, and SubscriptionSettingsItem into one reusable component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import type { IconName } from "@umituz/react-native-design-system";
|
|
9
|
+
import { SettingsItemCard } from "./SettingsItemCard";
|
|
10
|
+
import { useSettingsNavigation } from "../navigation/hooks/useSettingsNavigation";
|
|
11
|
+
import { createRouteOrPressHandler } from "../navigation/utils/navigationHelpers";
|
|
12
|
+
import type { SettingsStackParamList } from "../navigation/types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base configuration for navigation items
|
|
16
|
+
*/
|
|
17
|
+
export interface SettingsNavigationItemConfig {
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
icon?: string;
|
|
21
|
+
sectionTitle?: string;
|
|
22
|
+
route?: keyof SettingsStackParamList;
|
|
23
|
+
onPress?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Props for generic settings navigation item
|
|
28
|
+
*/
|
|
29
|
+
export interface SettingsNavigationItemProps<T extends SettingsNavigationItemConfig = SettingsNavigationItemConfig> {
|
|
30
|
+
/** Item configuration */
|
|
31
|
+
config: T;
|
|
32
|
+
/** Default icon if not specified in config */
|
|
33
|
+
defaultIcon?: IconName;
|
|
34
|
+
/** Default route if not specified in config */
|
|
35
|
+
defaultRoute?: keyof SettingsStackParamList;
|
|
36
|
+
/** Function to render custom description */
|
|
37
|
+
renderDescription?: (config: T) => string | undefined;
|
|
38
|
+
/** Remove background styling */
|
|
39
|
+
noBackground?: boolean;
|
|
40
|
+
/** Remove margin */
|
|
41
|
+
hideMargin?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generic Settings Navigation Item Component
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <SettingsNavigationItem
|
|
50
|
+
* config={videoConfig}
|
|
51
|
+
* defaultIcon="play-circle-outline"
|
|
52
|
+
* defaultRoute="VideoTutorial"
|
|
53
|
+
* />
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @example With custom description
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <SettingsNavigationItem
|
|
59
|
+
* config={gamificationConfig}
|
|
60
|
+
* defaultIcon="trophy-outline"
|
|
61
|
+
* defaultRoute="Gamification"
|
|
62
|
+
* renderDescription={(cfg) => `${level} • ${points}`}
|
|
63
|
+
* />
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
const SettingsNavigationItemComponent = <T extends SettingsNavigationItemConfig>({
|
|
67
|
+
config,
|
|
68
|
+
defaultIcon = "apps-outline",
|
|
69
|
+
defaultRoute,
|
|
70
|
+
renderDescription,
|
|
71
|
+
noBackground,
|
|
72
|
+
hideMargin,
|
|
73
|
+
}: SettingsNavigationItemProps<T>) => {
|
|
74
|
+
const navigation = useSettingsNavigation();
|
|
75
|
+
|
|
76
|
+
const handlePress = React.useMemo(
|
|
77
|
+
() => createRouteOrPressHandler(navigation.navigate, {
|
|
78
|
+
route: config.route,
|
|
79
|
+
onPress: config.onPress,
|
|
80
|
+
fallback: defaultRoute,
|
|
81
|
+
}),
|
|
82
|
+
[navigation, config.route, config.onPress, defaultRoute]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const icon = (config.icon || defaultIcon) as IconName;
|
|
86
|
+
const description = renderDescription ? renderDescription(config) : config.description;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<SettingsItemCard
|
|
90
|
+
title={config.title}
|
|
91
|
+
description={description}
|
|
92
|
+
icon={icon}
|
|
93
|
+
onPress={handlePress}
|
|
94
|
+
sectionTitle={config.sectionTitle}
|
|
95
|
+
noBackground={noBackground}
|
|
96
|
+
hideMargin={hideMargin}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Memoized Settings Navigation Item
|
|
103
|
+
* Uses shallow comparison by default
|
|
104
|
+
*/
|
|
105
|
+
export const SettingsNavigationItem = React.memo(
|
|
106
|
+
SettingsNavigationItemComponent
|
|
107
|
+
) as typeof SettingsNavigationItemComponent & { displayName?: string };
|
|
108
|
+
|
|
109
|
+
SettingsNavigationItem.displayName = "SettingsNavigationItem";
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Navigation Utilities
|
|
3
3
|
*/
|
|
4
|
-
export
|
|
4
|
+
export {
|
|
5
|
+
createNavigationHandler,
|
|
6
|
+
createRouteOrPressHandler,
|
|
7
|
+
createNavigationHandlerWithParams,
|
|
8
|
+
} from './navigationHelpers';
|
|
9
|
+
export type { NavigateFunction, RouteOrPressConfig } from './navigationHelpers';
|
|
10
|
+
|
|
11
|
+
export { createNotificationTranslations, createQuietHoursTranslations, createLegalScreenProps } from './navigationTranslations';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Helper Utilities
|
|
3
|
+
* Type-safe navigation handling to eliminate type casting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SettingsStackParamList } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Type for navigation function
|
|
10
|
+
*/
|
|
11
|
+
export type NavigateFunction = <RouteName extends keyof SettingsStackParamList>(
|
|
12
|
+
route: RouteName,
|
|
13
|
+
params?: SettingsStackParamList[RouteName]
|
|
14
|
+
) => void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a type-safe navigation handler
|
|
18
|
+
*
|
|
19
|
+
* @param navigate - Navigation function from useNavigation
|
|
20
|
+
* @param route - Target route name
|
|
21
|
+
* @param fallback - Optional fallback route if primary route is undefined
|
|
22
|
+
* @returns Navigation handler function
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const navigation = useSettingsNavigation();
|
|
27
|
+
* const handlePress = createNavigationHandler(
|
|
28
|
+
* navigation.navigate,
|
|
29
|
+
* config.route,
|
|
30
|
+
* 'Appearance'
|
|
31
|
+
* );
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const createNavigationHandler = <T extends keyof SettingsStackParamList>(
|
|
35
|
+
navigate: NavigateFunction,
|
|
36
|
+
route: T | undefined,
|
|
37
|
+
fallback?: T
|
|
38
|
+
) => {
|
|
39
|
+
return () => {
|
|
40
|
+
const targetRoute = route || fallback;
|
|
41
|
+
if (targetRoute) {
|
|
42
|
+
navigate(targetRoute);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for route or press handler
|
|
49
|
+
*/
|
|
50
|
+
export interface RouteOrPressConfig<T extends keyof SettingsStackParamList = keyof SettingsStackParamList> {
|
|
51
|
+
/** Route to navigate to */
|
|
52
|
+
route?: T;
|
|
53
|
+
/** Custom onPress handler */
|
|
54
|
+
onPress?: () => void;
|
|
55
|
+
/** Fallback route if primary route is undefined */
|
|
56
|
+
fallback?: T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a handler that supports both route navigation and custom onPress
|
|
61
|
+
* Prioritizes onPress if provided, otherwise navigates to route
|
|
62
|
+
*
|
|
63
|
+
* @param navigate - Navigation function from useNavigation
|
|
64
|
+
* @param config - Configuration with route and/or onPress
|
|
65
|
+
* @returns Handler function
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const navigation = useSettingsNavigation();
|
|
70
|
+
* const handlePress = createRouteOrPressHandler(
|
|
71
|
+
* navigation.navigate,
|
|
72
|
+
* { route: config.route, onPress: config.onPress, fallback: 'Appearance' }
|
|
73
|
+
* );
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export const createRouteOrPressHandler = <T extends keyof SettingsStackParamList>(
|
|
77
|
+
navigate: NavigateFunction,
|
|
78
|
+
config: RouteOrPressConfig<T>
|
|
79
|
+
) => {
|
|
80
|
+
return () => {
|
|
81
|
+
if (config.onPress) {
|
|
82
|
+
config.onPress();
|
|
83
|
+
} else {
|
|
84
|
+
const targetRoute = config.route || config.fallback;
|
|
85
|
+
if (targetRoute) {
|
|
86
|
+
navigate(targetRoute);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a navigation handler with parameters
|
|
94
|
+
*
|
|
95
|
+
* @param navigate - Navigation function from useNavigation
|
|
96
|
+
* @param route - Target route name
|
|
97
|
+
* @param params - Navigation parameters for the route
|
|
98
|
+
* @returns Navigation handler function
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const navigation = useSettingsNavigation();
|
|
103
|
+
* const handlePress = createNavigationHandlerWithParams(
|
|
104
|
+
* navigation.navigate,
|
|
105
|
+
* 'LegalDetail',
|
|
106
|
+
* { document: 'privacy' }
|
|
107
|
+
* );
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export const createNavigationHandlerWithParams = <T extends keyof SettingsStackParamList>(
|
|
111
|
+
navigate: NavigateFunction,
|
|
112
|
+
route: T,
|
|
113
|
+
params: SettingsStackParamList[T]
|
|
114
|
+
) => {
|
|
115
|
+
return () => {
|
|
116
|
+
navigate(route, params);
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useMemo } from "react";
|
|
1
|
+
import React, { useMemo, useCallback } from "react";
|
|
2
2
|
import { View, StyleSheet } from "react-native";
|
|
3
3
|
import { SettingsFooter } from "../../components/SettingsFooter";
|
|
4
4
|
import { SettingsSection } from "../../components/SettingsSection";
|
|
5
|
+
import { SettingsNavigationItem } from "../../components/SettingsNavigationItem";
|
|
5
6
|
import { DevSettingsSection } from "../../../domains/dev";
|
|
6
7
|
import { DisclaimerSetting } from "../../../domains/disclaimer";
|
|
7
8
|
import { ProfileSectionLoader } from "./sections/ProfileSectionLoader";
|
|
@@ -10,13 +11,9 @@ import { IdentitySettingsSection } from "./sections/IdentitySettingsSection";
|
|
|
10
11
|
import { SupportSettingsSection } from "./sections/SupportSettingsSection";
|
|
11
12
|
import { CustomSettingsList } from "./sections/CustomSettingsList";
|
|
12
13
|
import { hasAnyFeaturesEnabled } from "./utils/featureChecker";
|
|
14
|
+
import { useGamification } from "../../../domains/gamification";
|
|
13
15
|
import type { SettingsContentProps } from "./types/SettingsContentProps";
|
|
14
16
|
|
|
15
|
-
// Extracted Item Components
|
|
16
|
-
import { SubscriptionSettingsItem } from "./SubscriptionSettingsItem";
|
|
17
|
-
import { WalletSettingsItem } from "./WalletSettingsItem";
|
|
18
|
-
import { GamificationSettingsItem } from "./GamificationSettingsItem";
|
|
19
|
-
|
|
20
17
|
export const SettingsContent: React.FC<SettingsContentProps> = ({
|
|
21
18
|
normalizedConfig,
|
|
22
19
|
features,
|
|
@@ -31,11 +28,20 @@ export const SettingsContent: React.FC<SettingsContentProps> = ({
|
|
|
31
28
|
gamificationConfig,
|
|
32
29
|
}) => {
|
|
33
30
|
const translations = normalizedConfig.translations;
|
|
31
|
+
const { level } = useGamification(gamificationConfig);
|
|
32
|
+
|
|
34
33
|
const hasAnyFeatures = useMemo(
|
|
35
34
|
() => hasAnyFeaturesEnabled(features, customSections),
|
|
36
35
|
[features, customSections]
|
|
37
36
|
);
|
|
38
37
|
|
|
38
|
+
const renderGamificationDescription = useCallback(
|
|
39
|
+
(cfg: typeof normalizedConfig.gamification.config) => {
|
|
40
|
+
return cfg?.description || `${level.currentLevel} • ${level.currentPoints}`;
|
|
41
|
+
},
|
|
42
|
+
[level.currentLevel, level.currentPoints]
|
|
43
|
+
);
|
|
44
|
+
|
|
39
45
|
return (
|
|
40
46
|
<View style={styles.container}>
|
|
41
47
|
{showUserProfile && (
|
|
@@ -49,15 +55,17 @@ export const SettingsContent: React.FC<SettingsContentProps> = ({
|
|
|
49
55
|
|
|
50
56
|
{features.subscription && (normalizedConfig.subscription.config?.route || normalizedConfig.subscription.config?.onPress) && (
|
|
51
57
|
<SettingsSection title={translations?.sections?.subscription}>
|
|
52
|
-
<
|
|
53
|
-
config={normalizedConfig.subscription.config}
|
|
58
|
+
<SettingsNavigationItem
|
|
59
|
+
config={normalizedConfig.subscription.config}
|
|
60
|
+
defaultIcon="star"
|
|
54
61
|
/>
|
|
55
62
|
</SettingsSection>
|
|
56
63
|
)}
|
|
57
64
|
|
|
58
65
|
{features.wallet && normalizedConfig.wallet.config?.route && (
|
|
59
|
-
<
|
|
66
|
+
<SettingsNavigationItem
|
|
60
67
|
config={normalizedConfig.wallet.config}
|
|
68
|
+
defaultIcon="wallet"
|
|
61
69
|
/>
|
|
62
70
|
)}
|
|
63
71
|
|
|
@@ -65,9 +73,11 @@ export const SettingsContent: React.FC<SettingsContentProps> = ({
|
|
|
65
73
|
|
|
66
74
|
{features.gamification && (
|
|
67
75
|
<SettingsSection title={translations?.sections?.progress}>
|
|
68
|
-
<
|
|
76
|
+
<SettingsNavigationItem
|
|
69
77
|
config={normalizedConfig.gamification.config || {}}
|
|
70
|
-
|
|
78
|
+
defaultIcon="trophy-outline"
|
|
79
|
+
defaultRoute="Gamification"
|
|
80
|
+
renderDescription={renderGamificationDescription}
|
|
71
81
|
noBackground={true}
|
|
72
82
|
hideMargin={true}
|
|
73
83
|
/>
|
|
@@ -2,7 +2,7 @@ import React, { useCallback } from "react";
|
|
|
2
2
|
import { SupportSection } from "../../../../domains/feedback/presentation/components/SupportSection";
|
|
3
3
|
import { SettingsSection } from "../../../components/SettingsSection";
|
|
4
4
|
import { SettingsItemCard } from "../../../components/SettingsItemCard";
|
|
5
|
-
import {
|
|
5
|
+
import { SettingsNavigationItem } from "../../../components/SettingsNavigationItem";
|
|
6
6
|
import type { NormalizedConfig } from "../../utils/normalizeConfig";
|
|
7
7
|
import { compareConfigAndFeatures } from "../../../../infrastructure/utils/memoComparisonUtils";
|
|
8
8
|
import { useSettingsNavigation } from "../../../navigation/hooks/useSettingsNavigation";
|
|
@@ -97,8 +97,10 @@ export const SupportSettingsSection: React.FC<SupportSettingsSectionProps> = ({
|
|
|
97
97
|
)}
|
|
98
98
|
|
|
99
99
|
{features.videoTutorial && (
|
|
100
|
-
<
|
|
100
|
+
<SettingsNavigationItem
|
|
101
101
|
config={normalizedConfig.videoTutorial.config || {}}
|
|
102
|
+
defaultIcon="play-circle-outline"
|
|
103
|
+
defaultRoute="VideoTutorial"
|
|
102
104
|
noBackground={true}
|
|
103
105
|
hideMargin={true}
|
|
104
106
|
/>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { FeatureVisibility } from "./BaseTypes";
|
|
7
7
|
import type { FeedbackType } from "../../../domains/feedback/domain/entities/FeedbackEntity";
|
|
8
8
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
9
|
+
import type { SettingsStackParamList } from "../../navigation/types";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* User Profile Settings Configuration
|
|
@@ -114,7 +115,7 @@ export interface SubscriptionConfig {
|
|
|
114
115
|
/** Custom section title for grouping */
|
|
115
116
|
sectionTitle?: string;
|
|
116
117
|
/** Navigation route for subscription screen (preferred over onPress) */
|
|
117
|
-
route?:
|
|
118
|
+
route?: keyof SettingsStackParamList;
|
|
118
119
|
/** Handler to open subscription screen (use route instead for stack navigation) */
|
|
119
120
|
onPress?: () => void;
|
|
120
121
|
/** Whether user is premium (to show active status) */
|
|
@@ -136,7 +137,7 @@ export interface WalletConfig {
|
|
|
136
137
|
/** Custom section title for grouping */
|
|
137
138
|
sectionTitle?: string;
|
|
138
139
|
/** Navigation route for wallet screen */
|
|
139
|
-
route?:
|
|
140
|
+
route?: keyof SettingsStackParamList;
|
|
140
141
|
/** Current balance to display */
|
|
141
142
|
balance?: number;
|
|
142
143
|
}
|
|
@@ -156,7 +157,7 @@ export interface GamificationItemConfig {
|
|
|
156
157
|
/** Custom section title for grouping */
|
|
157
158
|
sectionTitle?: string;
|
|
158
159
|
/** Navigation route for gamification screen */
|
|
159
|
-
route?:
|
|
160
|
+
route?: keyof SettingsStackParamList;
|
|
160
161
|
/** Achievements to display */
|
|
161
162
|
achievementsCount?: number;
|
|
162
163
|
}
|
|
@@ -178,6 +179,6 @@ export interface VideoTutorialConfig {
|
|
|
178
179
|
/** Handler to open video tutorial screen */
|
|
179
180
|
onPress?: () => void;
|
|
180
181
|
/** Navigation route for video tutorial screen */
|
|
181
|
-
route?:
|
|
182
|
+
route?: keyof SettingsStackParamList;
|
|
182
183
|
}
|
|
183
184
|
|