@umituz/react-native-settings 4.23.85 → 4.23.87
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 +3 -3
- package/src/domains/about/presentation/hooks/useAboutInfo.ts +1 -1
- package/src/domains/faqs/presentation/screens/FAQScreen.tsx +1 -1
- package/src/domains/feedback/presentation/components/FeedbackForm.styles.ts +1 -1
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +11 -4
- package/src/domains/gamification/components/GamificationScreen/GamificationScreen.tsx +1 -6
- package/src/domains/gamification/store/gamificationStore.ts +6 -7
- package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +50 -181
- package/src/domains/localization/infrastructure/storage/localizationStoreUtils.ts +182 -0
- package/src/domains/notifications/infrastructure/utils/dev.ts +3 -3
- package/src/domains/notifications/reminders/presentation/components/ReminderForm.constants.ts +1 -1
- package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +52 -46
- package/src/infrastructure/types/commonComponentTypes.ts +142 -0
- package/src/infrastructure/utils/async/core.ts +109 -0
- package/src/infrastructure/utils/async/debounceAndBatch.ts +69 -0
- package/src/infrastructure/utils/async/index.ts +8 -0
- package/src/infrastructure/utils/async/retryAndTimeout.ts +57 -0
- package/src/infrastructure/utils/configFactory.ts +101 -0
- package/src/infrastructure/utils/errorHandlers.ts +249 -0
- package/src/infrastructure/utils/index.ts +5 -0
- package/src/infrastructure/utils/memoComparisonUtils.ts +0 -2
- package/src/infrastructure/utils/memoUtils.ts +10 -2
- package/src/infrastructure/utils/styleTokens.ts +132 -0
- package/src/infrastructure/utils/validation/core.ts +42 -0
- package/src/infrastructure/utils/validation/formValidators.ts +82 -0
- package/src/infrastructure/utils/validation/index.ts +37 -0
- package/src/infrastructure/utils/validation/numericValidators.ts +66 -0
- package/src/infrastructure/utils/validation/passwordValidator.ts +53 -0
- package/src/infrastructure/utils/validation/textValidators.ts +118 -0
- package/src/presentation/hooks/useSettingsScreenConfig.ts +32 -79
- package/src/presentation/navigation/SettingsStackNavigator.tsx +1 -24
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +1 -1
- package/src/presentation/utils/config-creators/base-configs.ts +54 -42
- package/src/presentation/utils/faqTranslator.ts +31 -0
- package/src/presentation/utils/index.ts +6 -1
- package/src/presentation/utils/screenFactory.ts +1 -1
- package/src/presentation/utils/settingsConfigFactory.ts +89 -0
- package/src/presentation/utils/useAuthHandlers.ts +98 -0
|
@@ -4,22 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useState, useMemo, useCallback } from 'react';
|
|
7
|
-
import { View, TextInput,
|
|
7
|
+
import { View, TextInput, ScrollView } from 'react-native';
|
|
8
8
|
import { AtomicText } from '@umituz/react-native-design-system';
|
|
9
9
|
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
10
10
|
import { TimePresetSelector } from './TimePresetSelector';
|
|
11
11
|
import { FrequencySelector } from './FrequencySelector';
|
|
12
12
|
import { WeekdaySelector } from './WeekdaySelector';
|
|
13
13
|
import { FormButton } from './FormButton';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
VALID_MINUTE_RANGE,
|
|
22
|
-
VALID_WEEKDAY_RANGE,
|
|
14
|
+
import { validateReminderForm } from '../../../../../infrastructure/utils/validation';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_HOUR,
|
|
17
|
+
DEFAULT_MINUTE,
|
|
18
|
+
DEFAULT_WEEKDAY,
|
|
19
|
+
MAX_TITLE_LENGTH,
|
|
20
|
+
MAX_BODY_LENGTH,
|
|
23
21
|
type ReminderFormProps,
|
|
24
22
|
} from './ReminderForm.constants';
|
|
25
23
|
import { createReminderFormStyles as createStyles } from './ReminderForm.styles';
|
|
@@ -44,25 +42,16 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
44
42
|
const [minute, setMinute] = useState(initialData?.minute ?? DEFAULT_MINUTE);
|
|
45
43
|
const [weekday, setWeekday] = useState(initialData?.weekday ?? DEFAULT_WEEKDAY);
|
|
46
44
|
const [isCustomTime, setIsCustomTime] = useState(!initialData?.timePresetId);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const isValidHour = useCallback((h: number): boolean => {
|
|
50
|
-
return h >= VALID_HOUR_RANGE.min && h <= VALID_HOUR_RANGE.max;
|
|
51
|
-
}, []);
|
|
52
|
-
|
|
53
|
-
const isValidMinute = useCallback((m: number): boolean => {
|
|
54
|
-
return m >= VALID_MINUTE_RANGE.min && m <= VALID_MINUTE_RANGE.max;
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
|
-
const isValidWeekday = useCallback((w: number): boolean => {
|
|
58
|
-
return w >= VALID_WEEKDAY_RANGE.min && w <= VALID_WEEKDAY_RANGE.max;
|
|
59
|
-
}, []);
|
|
45
|
+
// FIXED: Add error state for user feedback
|
|
46
|
+
const [error, setError] = useState<string | null>(null);
|
|
60
47
|
|
|
61
48
|
const handlePresetSelect = useCallback((preset: TimePreset) => {
|
|
62
49
|
setSelectedPresetId(preset.id);
|
|
63
50
|
setHour(preset.hour);
|
|
64
51
|
setMinute(preset.minute);
|
|
65
52
|
setIsCustomTime(false);
|
|
53
|
+
// Clear error when user changes something
|
|
54
|
+
setError(null);
|
|
66
55
|
}, []);
|
|
67
56
|
|
|
68
57
|
const handleCustomSelect = useCallback(() => {
|
|
@@ -74,29 +63,26 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
74
63
|
const trimmedTitle = title.trim();
|
|
75
64
|
const trimmedBody = body.trim();
|
|
76
65
|
|
|
77
|
-
// Validate
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
66
|
+
// Validate using centralized validation
|
|
67
|
+
const validationResult = validateReminderForm({
|
|
68
|
+
title: trimmedTitle,
|
|
69
|
+
body: trimmedBody,
|
|
70
|
+
frequency,
|
|
71
|
+
hour,
|
|
72
|
+
minute,
|
|
73
|
+
weekday,
|
|
74
|
+
maxTitleLength: MAX_TITLE_LENGTH,
|
|
75
|
+
maxBodyLength: MAX_BODY_LENGTH,
|
|
76
|
+
});
|
|
90
77
|
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
if (!validationResult.isValid) {
|
|
79
|
+
// FIXED: Show error to user
|
|
80
|
+
setError(validationResult.error || "Validation failed");
|
|
93
81
|
return;
|
|
94
82
|
}
|
|
95
83
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
84
|
+
// Clear error and proceed
|
|
85
|
+
setError(null);
|
|
100
86
|
|
|
101
87
|
// Sanitize input (React Native handles XSS, but we trim extra whitespace)
|
|
102
88
|
const sanitizedTitle = trimmedTitle.replace(/\s+/g, ' ').trim();
|
|
@@ -112,7 +98,7 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
112
98
|
weekday: frequency === 'weekly' ? weekday : undefined,
|
|
113
99
|
dayOfMonth: frequency === 'monthly' ? 1 : undefined,
|
|
114
100
|
});
|
|
115
|
-
}, [title, body, frequency, selectedPresetId, hour, minute, weekday, isCustomTime, onSave
|
|
101
|
+
}, [title, body, frequency, selectedPresetId, hour, minute, weekday, isCustomTime, onSave]);
|
|
116
102
|
|
|
117
103
|
return (
|
|
118
104
|
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
|
@@ -121,7 +107,10 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
121
107
|
<TextInput
|
|
122
108
|
style={styles.input}
|
|
123
109
|
value={title}
|
|
124
|
-
onChangeText={
|
|
110
|
+
onChangeText={(text) => {
|
|
111
|
+
setTitle(text);
|
|
112
|
+
setError(null); // Clear error on input
|
|
113
|
+
}}
|
|
125
114
|
placeholder={translations.titlePlaceholder}
|
|
126
115
|
placeholderTextColor={tokens.colors.textSecondary}
|
|
127
116
|
/>
|
|
@@ -132,7 +121,10 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
132
121
|
<TextInput
|
|
133
122
|
style={[styles.input, styles.multilineInput]}
|
|
134
123
|
value={body}
|
|
135
|
-
onChangeText={
|
|
124
|
+
onChangeText={(text) => {
|
|
125
|
+
setBody(text);
|
|
126
|
+
setError(null); // Clear error on input
|
|
127
|
+
}}
|
|
136
128
|
placeholder={translations.bodyPlaceholder}
|
|
137
129
|
placeholderTextColor={tokens.colors.textSecondary}
|
|
138
130
|
multiline
|
|
@@ -171,9 +163,23 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
171
163
|
</View>
|
|
172
164
|
)}
|
|
173
165
|
|
|
166
|
+
{/* FIXED: Show error message to user */}
|
|
167
|
+
{error && (
|
|
168
|
+
<View style={styles.section}>
|
|
169
|
+
<AtomicText type="bodySmall" color="error">
|
|
170
|
+
{error}
|
|
171
|
+
</AtomicText>
|
|
172
|
+
</View>
|
|
173
|
+
)}
|
|
174
|
+
|
|
174
175
|
<View style={styles.buttonRow}>
|
|
175
176
|
<FormButton label={translations.cancelButton} onPress={onCancel} variant="secondary" />
|
|
176
|
-
|
|
177
|
+
{/* FIXED: Disable button when form is invalid */}
|
|
178
|
+
<FormButton
|
|
179
|
+
label={translations.saveButton}
|
|
180
|
+
onPress={handleSave}
|
|
181
|
+
disabled={!title.trim() || !frequency}
|
|
182
|
+
/>
|
|
177
183
|
</View>
|
|
178
184
|
</ScrollView>
|
|
179
185
|
);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common Component Types
|
|
3
|
+
* Shared interfaces for common component props to reduce duplication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
7
|
+
import type { IconName } from "@umituz/react-native-design-system";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base props for settings item components
|
|
11
|
+
*/
|
|
12
|
+
export interface BaseSettingsItemProps {
|
|
13
|
+
title: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
icon?: IconName;
|
|
16
|
+
onPress?: () => void;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
style?: StyleProp<ViewStyle>;
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Base props for card components
|
|
24
|
+
*/
|
|
25
|
+
export interface BaseCardProps {
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
testID?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Base props for section components
|
|
32
|
+
*/
|
|
33
|
+
export interface BaseSectionProps {
|
|
34
|
+
title?: string;
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
testID?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Navigation item props
|
|
41
|
+
*/
|
|
42
|
+
export interface NavigationItemProps extends BaseSettingsItemProps {
|
|
43
|
+
route?: string;
|
|
44
|
+
showChevron?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Toggle item props (items with switches)
|
|
49
|
+
*/
|
|
50
|
+
export interface ToggleItemProps extends BaseSettingsItemProps {
|
|
51
|
+
showSwitch: true;
|
|
52
|
+
switchValue: boolean;
|
|
53
|
+
onSwitchChange: (value: boolean) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pressable item props
|
|
58
|
+
*/
|
|
59
|
+
export interface PressableItemProps extends BaseSettingsItemProps {
|
|
60
|
+
onPress: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Icon styling props
|
|
65
|
+
*/
|
|
66
|
+
export interface IconStyleProps {
|
|
67
|
+
iconBgColor?: string;
|
|
68
|
+
iconColor?: string;
|
|
69
|
+
iconSize?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Loading state props
|
|
74
|
+
*/
|
|
75
|
+
export interface LoadingStateProps {
|
|
76
|
+
loading?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Badge props (for notification counts, etc.)
|
|
81
|
+
*/
|
|
82
|
+
export interface BadgeProps {
|
|
83
|
+
badge?: string | number;
|
|
84
|
+
badgeColor?: string;
|
|
85
|
+
badgeTextColor?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Screen header props
|
|
90
|
+
*/
|
|
91
|
+
export interface ScreenHeaderProps {
|
|
92
|
+
title: string;
|
|
93
|
+
subtitle?: string;
|
|
94
|
+
showBackButton?: boolean;
|
|
95
|
+
onBackPress?: () => void;
|
|
96
|
+
rightElement?: React.ReactNode;
|
|
97
|
+
style?: StyleProp<ViewStyle>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* List item props
|
|
102
|
+
*/
|
|
103
|
+
export interface ListItemProps extends BaseSettingsItemProps {
|
|
104
|
+
rightElement?: React.ReactNode;
|
|
105
|
+
leftElement?: React.ReactNode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Modal props
|
|
110
|
+
*/
|
|
111
|
+
export interface BaseModalProps {
|
|
112
|
+
visible: boolean;
|
|
113
|
+
onClose: () => void;
|
|
114
|
+
title?: string;
|
|
115
|
+
testID?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Form input props
|
|
120
|
+
*/
|
|
121
|
+
export interface BaseInputProps {
|
|
122
|
+
value: string;
|
|
123
|
+
onChangeText: (text: string) => void;
|
|
124
|
+
placeholder?: string;
|
|
125
|
+
error?: string;
|
|
126
|
+
disabled?: boolean;
|
|
127
|
+
style?: StyleProp<ViewStyle>;
|
|
128
|
+
testID?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Button props
|
|
133
|
+
*/
|
|
134
|
+
export interface BaseButtonProps {
|
|
135
|
+
title: string;
|
|
136
|
+
onPress: () => void;
|
|
137
|
+
disabled?: boolean;
|
|
138
|
+
loading?: boolean;
|
|
139
|
+
variant?: "primary" | "secondary" | "danger" | "ghost";
|
|
140
|
+
style?: StyleProp<ViewStyle>;
|
|
141
|
+
testID?: string;
|
|
142
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Async Operation Utilities
|
|
3
|
+
* Base types and handlers for async operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ValidationResult } from "../validation";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Result type for async operations
|
|
10
|
+
*/
|
|
11
|
+
export type AsyncResult<T, E = Error> =
|
|
12
|
+
| { success: true; data: T }
|
|
13
|
+
| { success: false; error: E };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generic async handler with error handling
|
|
17
|
+
*/
|
|
18
|
+
export const handleAsyncOperation = async <T>(
|
|
19
|
+
operation: () => Promise<T>,
|
|
20
|
+
onError?: (error: Error) => void
|
|
21
|
+
): Promise<AsyncResult<T>> => {
|
|
22
|
+
try {
|
|
23
|
+
const data = await operation();
|
|
24
|
+
return { success: true, data };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
27
|
+
if (onError) {
|
|
28
|
+
onError(err);
|
|
29
|
+
}
|
|
30
|
+
return { success: false, error: err };
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Async operation with loading state
|
|
36
|
+
* FIXED: Properly handles errors in onSuccess callback
|
|
37
|
+
*/
|
|
38
|
+
export const createAsyncHandler = <T extends unknown[], R>(
|
|
39
|
+
handler: (...args: T) => Promise<R>,
|
|
40
|
+
options: {
|
|
41
|
+
onLoadingStart?: () => void;
|
|
42
|
+
onLoadingEnd?: () => void;
|
|
43
|
+
onError?: (error: Error) => void;
|
|
44
|
+
onSuccess?: (result: R) => void;
|
|
45
|
+
}
|
|
46
|
+
) => {
|
|
47
|
+
return async (...args: T): Promise<void> => {
|
|
48
|
+
const { onLoadingStart, onLoadingEnd, onError, onSuccess } = options;
|
|
49
|
+
|
|
50
|
+
let loadingStarted = false;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
onLoadingStart?.();
|
|
54
|
+
loadingStarted = true;
|
|
55
|
+
const result = await handler(...args);
|
|
56
|
+
// FIXED: Wrap onSuccess in try-catch to handle errors separately
|
|
57
|
+
try {
|
|
58
|
+
onSuccess?.(result);
|
|
59
|
+
} catch (callbackError) {
|
|
60
|
+
// Log callback error but don't treat it as handler error
|
|
61
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
62
|
+
console.error("[createAsyncHandler] onSuccess callback error:", callbackError);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
onError?.(err);
|
|
68
|
+
} finally {
|
|
69
|
+
// FIXED: Only call onLoadingEnd if it was started
|
|
70
|
+
if (loadingStarted) {
|
|
71
|
+
try {
|
|
72
|
+
onLoadingEnd?.();
|
|
73
|
+
} catch (callbackError) {
|
|
74
|
+
// Log callback error but don't throw
|
|
75
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
76
|
+
console.error("[createAsyncHandler] onLoadingEnd callback error:", callbackError);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Async operation with validation
|
|
86
|
+
*/
|
|
87
|
+
export const createValidatedAsyncHandler = <T, R>(
|
|
88
|
+
validator: (data: T) => ValidationResult,
|
|
89
|
+
handler: (data: T) => Promise<R>,
|
|
90
|
+
options: {
|
|
91
|
+
onValidationError?: (error: string) => void;
|
|
92
|
+
onError?: (error: Error) => void;
|
|
93
|
+
} = {}
|
|
94
|
+
) => {
|
|
95
|
+
return async (data: T): Promise<AsyncResult<R>> => {
|
|
96
|
+
const { onValidationError, onError } = options;
|
|
97
|
+
|
|
98
|
+
// Validate first
|
|
99
|
+
const validationResult = validator(data);
|
|
100
|
+
if (!validationResult.isValid) {
|
|
101
|
+
const error = new Error(validationResult.error || "Validation failed");
|
|
102
|
+
onValidationError?.(error.message);
|
|
103
|
+
return { success: false, error };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Execute handler
|
|
107
|
+
return handleAsyncOperation(() => handler(data), onError);
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce and Batch Utilities
|
|
3
|
+
* Utilities for debouncing and batching async operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AsyncResult } from "./core";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Debounced async operation
|
|
10
|
+
*/
|
|
11
|
+
export const createDebouncedAsyncOperation = <T extends unknown[], R>(
|
|
12
|
+
operation: (...args: T) => Promise<R>,
|
|
13
|
+
delayMs: number
|
|
14
|
+
): ((...args: T) => Promise<R>) => {
|
|
15
|
+
let timeoutId: NodeJS.Timeout | null = null;
|
|
16
|
+
|
|
17
|
+
return (...args: T): Promise<R> => {
|
|
18
|
+
if (timeoutId) {
|
|
19
|
+
clearTimeout(timeoutId);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
timeoutId = setTimeout(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await operation(...args);
|
|
26
|
+
resolve(result);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
reject(error);
|
|
29
|
+
}
|
|
30
|
+
}, delayMs);
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Batch async operations
|
|
37
|
+
*/
|
|
38
|
+
export const batchAsyncOperations = async <T, R>(
|
|
39
|
+
items: T[],
|
|
40
|
+
operation: (item: T) => Promise<R>,
|
|
41
|
+
options: {
|
|
42
|
+
concurrency?: number;
|
|
43
|
+
onProgress?: (completed: number, total: number) => void;
|
|
44
|
+
} = {}
|
|
45
|
+
): Promise<AsyncResult<R[]>> => {
|
|
46
|
+
const { concurrency = 5, onProgress } = options;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const results: R[] = [];
|
|
50
|
+
const batches: T[][] = [];
|
|
51
|
+
|
|
52
|
+
// Create batches
|
|
53
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
54
|
+
batches.push(items.slice(i, i + concurrency));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Process batches
|
|
58
|
+
for (const batch of batches) {
|
|
59
|
+
const batchResults = await Promise.all(batch.map(operation));
|
|
60
|
+
results.push(...batchResults);
|
|
61
|
+
onProgress?.(results.length, items.length);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { success: true, data: results };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
return { success: false, error: err };
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry and Timeout Utilities
|
|
3
|
+
* Utilities for handling retries and timeouts in async operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Retry utility for async operations
|
|
8
|
+
*/
|
|
9
|
+
export const retryAsyncOperation = async <T>(
|
|
10
|
+
operation: () => Promise<T>,
|
|
11
|
+
options: {
|
|
12
|
+
maxAttempts?: number;
|
|
13
|
+
delayMs?: number;
|
|
14
|
+
backoffMultiplier?: number;
|
|
15
|
+
onRetry?: (attempt: number, error: Error) => void;
|
|
16
|
+
} = {}
|
|
17
|
+
): Promise<T> => {
|
|
18
|
+
const {
|
|
19
|
+
maxAttempts = 3,
|
|
20
|
+
delayMs = 1000,
|
|
21
|
+
backoffMultiplier = 2,
|
|
22
|
+
onRetry,
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
let lastError: Error | undefined;
|
|
26
|
+
|
|
27
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return await operation();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
32
|
+
|
|
33
|
+
if (attempt < maxAttempts) {
|
|
34
|
+
onRetry?.(attempt, lastError);
|
|
35
|
+
const delay = delayMs * Math.pow(backoffMultiplier, attempt - 1);
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw lastError || new Error("Operation failed after retries");
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Timeout wrapper for async operations
|
|
46
|
+
*/
|
|
47
|
+
export const withTimeout = async <T>(
|
|
48
|
+
operation: Promise<T>,
|
|
49
|
+
timeoutMs: number,
|
|
50
|
+
timeoutMessage: string = "Operation timed out"
|
|
51
|
+
): Promise<T> => {
|
|
52
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
53
|
+
setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return Promise.race([operation, timeoutPromise]);
|
|
57
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Factory
|
|
3
|
+
* Generic configuration creator to reduce duplication in base-configs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TranslationFunction } from "../../presentation/utils/config-creators/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Feature visibility configuration
|
|
10
|
+
* - true: Always show (if navigation screen exists)
|
|
11
|
+
* - false: Never show
|
|
12
|
+
* - 'auto': Automatically detect (check if navigation screen exists and package is available)
|
|
13
|
+
*/
|
|
14
|
+
export type FeatureVisibility = boolean | "auto";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base configuration type for all settings items
|
|
18
|
+
*/
|
|
19
|
+
export interface BaseConfigType {
|
|
20
|
+
enabled?: FeatureVisibility;
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
route?: string;
|
|
25
|
+
onPress?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration parameters for creating a settings item config
|
|
30
|
+
*/
|
|
31
|
+
export interface ConfigCreatorParams {
|
|
32
|
+
t: TranslationFunction;
|
|
33
|
+
titleKey: string;
|
|
34
|
+
descriptionKey: string;
|
|
35
|
+
icon: string;
|
|
36
|
+
routeOrOnPress?: string | (() => void);
|
|
37
|
+
defaultRoute?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generic configuration creator function
|
|
42
|
+
* Reduces duplication across all config creators
|
|
43
|
+
*/
|
|
44
|
+
export const createBaseConfig = <T extends BaseConfigType = BaseConfigType>(
|
|
45
|
+
params: ConfigCreatorParams
|
|
46
|
+
): T => {
|
|
47
|
+
const { t, titleKey, descriptionKey, icon, routeOrOnPress, defaultRoute } = params;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
enabled: true,
|
|
51
|
+
title: t(titleKey),
|
|
52
|
+
description: t(descriptionKey),
|
|
53
|
+
icon,
|
|
54
|
+
route: typeof routeOrOnPress === "string" ? routeOrOnPress : defaultRoute,
|
|
55
|
+
onPress: typeof routeOrOnPress === "function" ? routeOrOnPress : undefined,
|
|
56
|
+
} as T;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a configuration with extended properties
|
|
61
|
+
*/
|
|
62
|
+
export const createConfigWithExtensions = <T extends BaseConfigType>(
|
|
63
|
+
baseParams: ConfigCreatorParams,
|
|
64
|
+
extensions: Partial<Omit<T, keyof BaseConfigType>>
|
|
65
|
+
): T => {
|
|
66
|
+
const baseConfig = createBaseConfig<T>(baseParams);
|
|
67
|
+
return { ...baseConfig, ...extensions };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a disabled configuration
|
|
72
|
+
*/
|
|
73
|
+
export const createDisabledConfig = <T extends BaseConfigType>(
|
|
74
|
+
params: Omit<ConfigCreatorParams, "routeOrOnPress" | "defaultRoute">
|
|
75
|
+
): T => {
|
|
76
|
+
const baseConfig = createBaseConfig<T>(params);
|
|
77
|
+
return { ...baseConfig, enabled: false } as T;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Batch create configurations
|
|
82
|
+
*/
|
|
83
|
+
export const createBatchConfigs = <T extends BaseConfigType>(
|
|
84
|
+
items: Array<{
|
|
85
|
+
titleKey: string;
|
|
86
|
+
descriptionKey: string;
|
|
87
|
+
icon: string;
|
|
88
|
+
routeOrOnPress?: string | (() => void);
|
|
89
|
+
}>,
|
|
90
|
+
t: TranslationFunction
|
|
91
|
+
): T[] => {
|
|
92
|
+
return items.map((item) =>
|
|
93
|
+
createBaseConfig<T>({
|
|
94
|
+
t,
|
|
95
|
+
titleKey: item.titleKey,
|
|
96
|
+
descriptionKey: item.descriptionKey,
|
|
97
|
+
icon: item.icon,
|
|
98
|
+
routeOrOnPress: item.routeOrOnPress,
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
};
|