@umituz/react-native-settings 4.23.86 → 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 +1 -1
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +10 -3
- 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/reminders/presentation/components/ReminderForm.tsx +51 -45
- 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/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/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/settingsConfigFactory.ts +89 -0
- package/src/presentation/utils/useAuthHandlers.ts +98 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "4.23.
|
|
3
|
+
"version": "4.23.87",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -7,6 +7,7 @@ import React, { useState } from "react";
|
|
|
7
7
|
import { View, TouchableOpacity, ScrollView, TextInput } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText, AtomicButton, AtomicIcon } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
|
|
10
|
+
import { validateFeedbackForm } from "../../../../infrastructure/utils/validation";
|
|
10
11
|
|
|
11
12
|
import { getFeedbackFormStyles as getStyles } from "./FeedbackForm.styles";
|
|
12
13
|
|
|
@@ -40,9 +41,15 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
|
|
|
40
41
|
const [isSubmittingLocal, setIsSubmittingLocal] = useState(false);
|
|
41
42
|
|
|
42
43
|
const handleSubmit = async () => {
|
|
43
|
-
// Validate
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Validate using centralized validation
|
|
45
|
+
const validationResult = validateFeedbackForm({
|
|
46
|
+
type: selectedType,
|
|
47
|
+
rating,
|
|
48
|
+
description,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!validationResult.isValid) {
|
|
52
|
+
setError(validationResult.error || "Validation failed");
|
|
46
53
|
return;
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -78,16 +78,16 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
|
|
|
78
78
|
const state = get();
|
|
79
79
|
const pointsToAdd = currentConfig?.pointsPerAction ?? 15;
|
|
80
80
|
|
|
81
|
+
const newTotalTasks = state.totalTasksCompleted + 1;
|
|
82
|
+
|
|
81
83
|
set({
|
|
82
|
-
totalTasksCompleted:
|
|
84
|
+
totalTasksCompleted: newTotalTasks,
|
|
83
85
|
points: state.points + pointsToAdd,
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// Check achievements
|
|
90
|
-
get().checkAchievements();
|
|
88
|
+
const actions = get() as GamificationActions;
|
|
89
|
+
actions.updateStreak();
|
|
90
|
+
actions.checkAchievements();
|
|
91
91
|
},
|
|
92
92
|
|
|
93
93
|
updateStreak: () => {
|
|
@@ -121,7 +121,6 @@ export const useGamificationStore = createStore<GamificationState, GamificationA
|
|
|
121
121
|
|
|
122
122
|
const state = get();
|
|
123
123
|
|
|
124
|
-
// Safety check for achievements array
|
|
125
124
|
if (!state.achievements || state.achievements.length === 0) {
|
|
126
125
|
return [];
|
|
127
126
|
}
|
|
@@ -3,187 +3,56 @@
|
|
|
3
3
|
* Creates and manages localization state with proper separation of concerns
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { create } from
|
|
7
|
-
import type { LocalizationState, LocalizationActions, LocalizationGetters } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { languageRepository } from '../repository/LanguageRepository';
|
|
11
|
-
|
|
12
|
-
declare const __DEV__: boolean;
|
|
6
|
+
import { create } from "zustand";
|
|
7
|
+
import type { LocalizationState, LocalizationActions, LocalizationGetters } from "./types/LocalizationState";
|
|
8
|
+
import { languageRepository } from "../repository/LanguageRepository";
|
|
9
|
+
import { InitializationManager, LanguageSwitchManager, localizationGetters } from "./localizationStoreUtils";
|
|
13
10
|
|
|
14
11
|
type LocalizationStoreType = LocalizationState & LocalizationActions & LocalizationGetters;
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
isInitialized
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
set({
|
|
63
|
-
currentLanguage: 'en-US',
|
|
64
|
-
isRTL: false,
|
|
65
|
-
isInitialized: true,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
throw error; // Re-throw to allow error handling
|
|
69
|
-
} finally {
|
|
70
|
-
initializeInProgress = false;
|
|
71
|
-
initializePromise = null;
|
|
72
|
-
}
|
|
73
|
-
})();
|
|
74
|
-
|
|
75
|
-
return initializePromise;
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
setLanguage: async (languageCode: string) => {
|
|
79
|
-
// Validate input
|
|
80
|
-
if (!languageCode || typeof languageCode !== 'string') {
|
|
81
|
-
throw new Error('Invalid language code provided');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Clear existing timer
|
|
85
|
-
if (languageSwitchTimer) {
|
|
86
|
-
clearTimeout(languageSwitchTimer);
|
|
87
|
-
languageSwitchTimer = null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return new Promise<void>((resolve, reject) => {
|
|
91
|
-
// Add resolver to pending list
|
|
92
|
-
pendingResolvers.push(() => {
|
|
93
|
-
// Resolve successfully
|
|
94
|
-
resolve();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Create rejection handler
|
|
98
|
-
const rejectAndCleanup = (error: Error) => {
|
|
99
|
-
// Remove this resolver
|
|
100
|
-
const index = pendingResolvers.findIndex(r => r === resolve);
|
|
101
|
-
if (index > -1) {
|
|
102
|
-
pendingResolvers.splice(index, 1);
|
|
103
|
-
}
|
|
104
|
-
reject(error);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
languageSwitchTimer = setTimeout(async () => {
|
|
108
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const result = await LanguageSwitcher.switchLanguage(languageCode);
|
|
113
|
-
|
|
114
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
set({
|
|
118
|
-
currentLanguage: result.languageCode,
|
|
119
|
-
isRTL: result.isRTL,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Resolve ALL pending promises
|
|
126
|
-
const resolvers = [...pendingResolvers];
|
|
127
|
-
pendingResolvers.length = 0; // Clear array
|
|
128
|
-
resolvers.forEach(r => r());
|
|
129
|
-
} catch (error) {
|
|
130
|
-
const errorMessage = error instanceof Error ? error : new Error(String(error));
|
|
131
|
-
|
|
132
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Reject all pending promises
|
|
136
|
-
const resolvers = [...pendingResolvers];
|
|
137
|
-
pendingResolvers.length = 0; // Clear array
|
|
138
|
-
resolvers.forEach(() => {
|
|
139
|
-
// Each resolver is wrapped to handle rejection
|
|
140
|
-
// Note: We can't reject promises already created, so we just clear them
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Reject this specific promise
|
|
144
|
-
rejectAndCleanup(errorMessage);
|
|
145
|
-
} finally {
|
|
146
|
-
languageSwitchTimer = null;
|
|
147
|
-
}
|
|
148
|
-
}, LANGUAGE_SWITCH_DEBOUNCE_MS);
|
|
149
|
-
});
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
reset: () => {
|
|
153
|
-
// Clear any pending language switch
|
|
154
|
-
if (languageSwitchTimer) {
|
|
155
|
-
clearTimeout(languageSwitchTimer);
|
|
156
|
-
languageSwitchTimer = null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Resolve any pending promises to prevent hanging
|
|
160
|
-
const resolvers = [...pendingResolvers];
|
|
161
|
-
pendingResolvers.length = 0; // Clear array
|
|
162
|
-
resolvers.forEach(r => r());
|
|
163
|
-
|
|
164
|
-
// Reset mutex
|
|
165
|
-
initializeInProgress = false;
|
|
166
|
-
initializePromise = null;
|
|
167
|
-
|
|
168
|
-
set({
|
|
169
|
-
currentLanguage: 'en-US',
|
|
170
|
-
isRTL: false,
|
|
171
|
-
isInitialized: false,
|
|
172
|
-
});
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
// Getters
|
|
176
|
-
getCurrentLanguage: () => {
|
|
177
|
-
const { currentLanguage } = get();
|
|
178
|
-
return languageRepository.getLanguageByCode(currentLanguage);
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
isLanguageSupported: (code: string) => {
|
|
182
|
-
return languageRepository.isLanguageSupported(code);
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
getSupportedLanguages: () => {
|
|
186
|
-
return languageRepository.getLanguages();
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
});
|
|
13
|
+
// Instance-level managers
|
|
14
|
+
const initManager = new InitializationManager();
|
|
15
|
+
const switchManager = new LanguageSwitchManager();
|
|
16
|
+
|
|
17
|
+
export const useLocalizationStore = create<LocalizationStoreType>((set, get) => ({
|
|
18
|
+
// State
|
|
19
|
+
currentLanguage: "en-US",
|
|
20
|
+
isRTL: false,
|
|
21
|
+
isInitialized: false,
|
|
22
|
+
supportedLanguages: languageRepository.getLanguages(),
|
|
23
|
+
|
|
24
|
+
// Actions
|
|
25
|
+
initialize: async () => {
|
|
26
|
+
const { isInitialized } = get();
|
|
27
|
+
await initManager.initialize(isInitialized, set);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
setLanguage: async (languageCode: string) => {
|
|
31
|
+
await switchManager.setLanguage(languageCode, set);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
reset: () => {
|
|
35
|
+
initManager.reset();
|
|
36
|
+
switchManager.reset();
|
|
37
|
+
|
|
38
|
+
set({
|
|
39
|
+
currentLanguage: "en-US",
|
|
40
|
+
isRTL: false,
|
|
41
|
+
isInitialized: false,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Getters
|
|
46
|
+
getCurrentLanguage: () => {
|
|
47
|
+
const { currentLanguage } = get();
|
|
48
|
+
return localizationGetters.getCurrentLanguage(currentLanguage);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
isLanguageSupported: (code: string) => {
|
|
52
|
+
return localizationGetters.isLanguageSupported(code);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
getSupportedLanguages: () => {
|
|
56
|
+
return localizationGetters.getSupportedLanguages();
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localization Store Utilities
|
|
3
|
+
* Extracted business logic for localization store
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LanguageInitializer } from "./LanguageInitializer";
|
|
7
|
+
import { LanguageSwitcher } from "./LanguageSwitcher";
|
|
8
|
+
import { languageRepository } from "../repository/LanguageRepository";
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
12
|
+
export const LANGUAGE_SWITCH_DEBOUNCE_MS = 300;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Manages localization initialization state
|
|
16
|
+
*/
|
|
17
|
+
export class InitializationManager {
|
|
18
|
+
private inProgress = false;
|
|
19
|
+
private promise: Promise<void> | null = null;
|
|
20
|
+
|
|
21
|
+
async initialize(
|
|
22
|
+
isAlreadyInitialized: boolean,
|
|
23
|
+
setState: (state: Partial<{ currentLanguage: string; isRTL: boolean; isInitialized: boolean }>) => void
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
// Return existing promise if initialization is in progress
|
|
26
|
+
if (this.inProgress && this.promise) {
|
|
27
|
+
return this.promise;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Return if already initialized
|
|
31
|
+
if (isAlreadyInitialized) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Set mutex and create promise
|
|
36
|
+
this.inProgress = true;
|
|
37
|
+
this.promise = (async () => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await LanguageInitializer.initialize();
|
|
40
|
+
|
|
41
|
+
setState({
|
|
42
|
+
currentLanguage: result.languageCode,
|
|
43
|
+
isRTL: result.isRTL,
|
|
44
|
+
isInitialized: true,
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Log and set fallback state
|
|
48
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
49
|
+
console.error("Localization initialization failed:", error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setState({
|
|
53
|
+
currentLanguage: "en-US",
|
|
54
|
+
isRTL: false,
|
|
55
|
+
isInitialized: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
throw error; // Re-throw to allow error handling
|
|
59
|
+
} finally {
|
|
60
|
+
this.inProgress = false;
|
|
61
|
+
this.promise = null;
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
|
|
65
|
+
return this.promise;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reset(): void {
|
|
69
|
+
this.inProgress = false;
|
|
70
|
+
this.promise = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Manages debounced language switching with pending promise handling
|
|
76
|
+
* FIXED: Properly rejects pending promises on error
|
|
77
|
+
*/
|
|
78
|
+
export class LanguageSwitchManager {
|
|
79
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
80
|
+
private pendingResolvers: Array<{ resolve: () => void; reject: (error: Error) => void }> = [];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sets language with debounce and promise handling
|
|
84
|
+
*/
|
|
85
|
+
setLanguage(
|
|
86
|
+
languageCode: string,
|
|
87
|
+
setState: (state: Partial<{ currentLanguage: string; isRTL: boolean }>) => void
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
// Validate input
|
|
90
|
+
if (!languageCode || typeof languageCode !== "string") {
|
|
91
|
+
return Promise.reject(new Error("Invalid language code provided"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clear existing timer
|
|
95
|
+
if (this.timer) {
|
|
96
|
+
clearTimeout(this.timer);
|
|
97
|
+
this.timer = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return new Promise<void>((resolve, reject) => {
|
|
101
|
+
// Add resolver and rejector to pending list
|
|
102
|
+
const pendingItem = { resolve, reject };
|
|
103
|
+
this.pendingResolvers.push(pendingItem);
|
|
104
|
+
|
|
105
|
+
this.timer = setTimeout(async () => {
|
|
106
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
107
|
+
console.log("[Localization] Switching language to:", languageCode);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const result = await LanguageSwitcher.switchLanguage(languageCode);
|
|
112
|
+
|
|
113
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
114
|
+
console.log("[Localization] Language switched successfully");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const stateUpdate = {
|
|
118
|
+
currentLanguage: result.languageCode,
|
|
119
|
+
isRTL: result.isRTL,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
setState(stateUpdate);
|
|
123
|
+
|
|
124
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
125
|
+
console.log("[Localization] State updated:", stateUpdate);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Resolve ALL pending promises
|
|
129
|
+
const resolvers = [...this.pendingResolvers];
|
|
130
|
+
this.pendingResolvers = [];
|
|
131
|
+
resolvers.forEach((r) => r.resolve());
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const errorMessage =
|
|
134
|
+
error instanceof Error ? error : new Error(String(error));
|
|
135
|
+
|
|
136
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
137
|
+
console.error("[Localization] Language switch failed:", errorMessage);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Reject ALL pending promises - FIXED: Actually rejects them now
|
|
141
|
+
const resolvers = [...this.pendingResolvers];
|
|
142
|
+
this.pendingResolvers = [];
|
|
143
|
+
resolvers.forEach((r) => r.reject(errorMessage));
|
|
144
|
+
} finally {
|
|
145
|
+
this.timer = null;
|
|
146
|
+
}
|
|
147
|
+
}, LANGUAGE_SWITCH_DEBOUNCE_MS);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Clears any pending language switch and resolves all promises
|
|
153
|
+
*/
|
|
154
|
+
reset(): void {
|
|
155
|
+
if (this.timer) {
|
|
156
|
+
clearTimeout(this.timer);
|
|
157
|
+
this.timer = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Resolve any pending promises to prevent hanging
|
|
161
|
+
const resolvers = [...this.pendingResolvers];
|
|
162
|
+
this.pendingResolvers = [];
|
|
163
|
+
resolvers.forEach((r) => r.resolve());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Localization getters
|
|
169
|
+
*/
|
|
170
|
+
export const localizationGetters = {
|
|
171
|
+
getCurrentLanguage: (currentLanguage: string) => {
|
|
172
|
+
return languageRepository.getLanguageByCode(currentLanguage);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
isLanguageSupported: (code: string) => {
|
|
176
|
+
return languageRepository.isLanguageSupported(code);
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
getSupportedLanguages: () => {
|
|
180
|
+
return languageRepository.getLanguages();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
@@ -11,15 +11,13 @@ 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
|
);
|