@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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/src/domains/feedback/presentation/components/FeedbackForm.tsx +10 -3
  3. package/src/domains/gamification/store/gamificationStore.ts +6 -7
  4. package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +50 -181
  5. package/src/domains/localization/infrastructure/storage/localizationStoreUtils.ts +182 -0
  6. package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +51 -45
  7. package/src/infrastructure/types/commonComponentTypes.ts +142 -0
  8. package/src/infrastructure/utils/async/core.ts +109 -0
  9. package/src/infrastructure/utils/async/debounceAndBatch.ts +69 -0
  10. package/src/infrastructure/utils/async/index.ts +8 -0
  11. package/src/infrastructure/utils/async/retryAndTimeout.ts +57 -0
  12. package/src/infrastructure/utils/configFactory.ts +101 -0
  13. package/src/infrastructure/utils/errorHandlers.ts +249 -0
  14. package/src/infrastructure/utils/index.ts +5 -0
  15. package/src/infrastructure/utils/memoUtils.ts +10 -2
  16. package/src/infrastructure/utils/styleTokens.ts +132 -0
  17. package/src/infrastructure/utils/validation/core.ts +42 -0
  18. package/src/infrastructure/utils/validation/formValidators.ts +82 -0
  19. package/src/infrastructure/utils/validation/index.ts +37 -0
  20. package/src/infrastructure/utils/validation/numericValidators.ts +66 -0
  21. package/src/infrastructure/utils/validation/passwordValidator.ts +53 -0
  22. package/src/infrastructure/utils/validation/textValidators.ts +118 -0
  23. package/src/presentation/hooks/useSettingsScreenConfig.ts +32 -79
  24. package/src/presentation/utils/config-creators/base-configs.ts +54 -42
  25. package/src/presentation/utils/faqTranslator.ts +31 -0
  26. package/src/presentation/utils/index.ts +6 -1
  27. package/src/presentation/utils/settingsConfigFactory.ts +89 -0
  28. 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.86",
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 input
44
- if (!description.trim()) {
45
- setError("Please provide a description");
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: state.totalTasksCompleted + 1,
84
+ totalTasksCompleted: newTotalTasks,
83
85
  points: state.points + pointsToAdd,
84
86
  });
85
87
 
86
- // Update streak
87
- get().updateStreak();
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 'zustand';
7
- import type { LocalizationState, LocalizationActions, LocalizationGetters } from './types/LocalizationState';
8
- import { LanguageInitializer } from './LanguageInitializer';
9
- import { LanguageSwitcher } from './LanguageSwitcher';
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
- const LANGUAGE_SWITCH_DEBOUNCE_MS = 300;
17
-
18
- export const useLocalizationStore = create<LocalizationStoreType>((set, get) => {
19
- // Instance-level state to prevent memory leaks
20
- let initializeInProgress = false;
21
- let initializePromise: Promise<void> | null = null;
22
- let languageSwitchTimer: ReturnType<typeof setTimeout> | null = null;
23
- const pendingResolvers: Array<() => void> = [];
24
-
25
- return {
26
- // State
27
- currentLanguage: 'en-US',
28
- isRTL: false,
29
- isInitialized: false,
30
- supportedLanguages: languageRepository.getLanguages(),
31
-
32
- // Actions
33
- initialize: async () => {
34
- const { isInitialized: alreadyInitialized } = get();
35
-
36
- // Return existing promise if initialization is in progress
37
- if (initializeInProgress && initializePromise) {
38
- return initializePromise;
39
- }
40
-
41
- // Return if already initialized
42
- if (alreadyInitialized) {
43
- return;
44
- }
45
-
46
- // Set mutex and create promise
47
- initializeInProgress = true;
48
- initializePromise = (async () => {
49
- try {
50
- const result = await LanguageInitializer.initialize();
51
-
52
- set({
53
- currentLanguage: result.languageCode,
54
- isRTL: result.isRTL,
55
- isInitialized: true,
56
- });
57
- } catch (error) {
58
- // Log and set fallback state
59
- if (typeof __DEV__ !== "undefined" && __DEV__) {
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
- DEFAULT_HOUR,
16
- DEFAULT_MINUTE,
17
- DEFAULT_WEEKDAY,
18
- MAX_TITLE_LENGTH,
19
- MAX_BODY_LENGTH,
20
- VALID_HOUR_RANGE,
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
- // Validation helper functions
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 title
78
- if (!trimmedTitle) {
79
- return;
80
- }
81
-
82
- if (trimmedTitle.length > MAX_TITLE_LENGTH) {
83
- return;
84
- }
85
-
86
- // Validate body length
87
- if (trimmedBody.length > MAX_BODY_LENGTH) {
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
- // Validate time values
92
- if (!isValidHour(hour) || !isValidMinute(minute)) {
78
+ if (!validationResult.isValid) {
79
+ // FIXED: Show error to user
80
+ setError(validationResult.error || "Validation failed");
93
81
  return;
94
82
  }
95
83
 
96
- // Validate weekday if frequency is weekly
97
- if (frequency === 'weekly' && !isValidWeekday(weekday)) {
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, isValidHour, isValidMinute, isValidWeekday]);
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={setTitle}
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={setBody}
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
- <FormButton label={translations.saveButton} onPress={handleSave} disabled={!title.trim()} />
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
  );