@umituz/react-native-settings 5.2.34 → 5.2.36

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 (70) hide show
  1. package/package.json +2 -4
  2. package/src/domains/about/presentation/screens/AboutScreenContent.tsx +87 -63
  3. package/src/domains/appearance/data/colorPalettes.ts +0 -23
  4. package/src/domains/appearance/presentation/components/CustomColorsSection.tsx +2 -4
  5. package/src/domains/appearance/presentation/components/ThemeOption.tsx +2 -2
  6. package/src/domains/dev/presentation/components/DevSettingsSection.tsx +5 -2
  7. package/src/domains/faqs/presentation/screens/FAQScreen.tsx +19 -25
  8. package/src/domains/feedback/presentation/components/FeedbackForm.tsx +160 -81
  9. package/src/domains/gamification/components/GamificationScreen/GamificationScreenWithConfig.tsx +11 -11
  10. package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +0 -2
  11. package/src/domains/localization/infrastructure/storage/localizationStoreUtils.ts +1 -1
  12. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +85 -48
  13. package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +0 -15
  14. package/src/domains/notifications/presentation/screens/NotificationsScreen.tsx +1 -3
  15. package/src/domains/notifications/reminders/presentation/components/ReminderForm.constants.ts +0 -4
  16. package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +69 -31
  17. package/src/domains/rating/presentation/components/StarRating.tsx +7 -13
  18. package/src/infrastructure/utils/configFactory.ts +0 -26
  19. package/src/infrastructure/utils/constants/textLimits.ts +0 -2
  20. package/src/infrastructure/utils/sanitizers.ts +1 -25
  21. package/src/infrastructure/utils/validation/core.ts +0 -33
  22. package/src/infrastructure/utils/validation/formValidators.ts +7 -1
  23. package/src/infrastructure/utils/validation/index.ts +2 -33
  24. package/src/infrastructure/utils/validators.ts +0 -6
  25. package/src/presentation/navigation/utils/index.ts +1 -7
  26. package/src/presentation/navigation/utils/navigationHelpers.ts +2 -87
  27. package/src/presentation/screens/components/SettingsContent.tsx +4 -19
  28. package/src/presentation/screens/components/sections/CustomSettingsList.tsx +3 -8
  29. package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +0 -4
  30. package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +0 -4
  31. package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +0 -4
  32. package/src/presentation/utils/screenFactory.ts +0 -25
  33. package/src/utils/appUtils.ts +0 -18
  34. package/src/utils/devUtils.ts +0 -10
  35. package/src/utils/errorUtils.ts +0 -22
  36. package/src/domains/about/utils/index.ts +0 -156
  37. package/src/domains/faqs/domain/services/index.ts +0 -1
  38. package/src/domains/faqs/presentation/screens/index.ts +0 -2
  39. package/src/domains/gamification/components/GamificationScreen/Header.tsx +0 -30
  40. package/src/domains/legal/presentation/components/LegalLinks.tsx +0 -137
  41. package/src/domains/legal/presentation/components/index.ts +0 -5
  42. package/src/domains/localization/infrastructure/config/languagesData.ts +0 -26
  43. package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +0 -37
  44. package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +0 -34
  45. package/src/infrastructure/storage/storeConfig.ts +0 -114
  46. package/src/infrastructure/types/commonComponentTypes.ts +0 -142
  47. package/src/infrastructure/utils/async/core.ts +0 -110
  48. package/src/infrastructure/utils/async/debounceAndBatch.ts +0 -69
  49. package/src/infrastructure/utils/async/index.ts +0 -8
  50. package/src/infrastructure/utils/async/retryAndTimeout.ts +0 -65
  51. package/src/infrastructure/utils/dateUtils.ts +0 -61
  52. package/src/infrastructure/utils/errorHandlers.ts +0 -250
  53. package/src/infrastructure/utils/index.ts +0 -12
  54. package/src/infrastructure/utils/memoComparisonUtils.ts +0 -66
  55. package/src/infrastructure/utils/memoUtils.ts +0 -167
  56. package/src/infrastructure/utils/styleTokens.ts +0 -145
  57. package/src/infrastructure/utils/styles/componentStyles.ts +0 -90
  58. package/src/infrastructure/utils/styles/index.ts +0 -9
  59. package/src/infrastructure/utils/styles/layoutStyles.ts +0 -56
  60. package/src/infrastructure/utils/styles/spacingStyles.ts +0 -33
  61. package/src/infrastructure/utils/styles/styleHelpers.ts +0 -22
  62. package/src/infrastructure/utils/translationHelpers.ts +0 -81
  63. package/src/infrastructure/utils/validation/numericValidators.ts +0 -66
  64. package/src/infrastructure/utils/validation/passwordValidator.ts +0 -53
  65. package/src/infrastructure/utils/validation/textValidators.ts +0 -118
  66. package/src/presentation/components/ErrorBoundary/SettingsErrorBoundary.tsx +0 -105
  67. package/src/presentation/components/ErrorBoundary/index.ts +0 -12
  68. package/src/presentation/components/ErrorBoundary/withErrorBoundary.tsx +0 -45
  69. package/src/utils/hooks/index.ts +0 -6
  70. package/src/utils/index.ts +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "5.2.34",
3
+ "version": "5.2.36",
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",
@@ -37,10 +37,8 @@
37
37
  "type": "git",
38
38
  "url": "https://github.com/umituz/react-native-settings"
39
39
  },
40
- "dependencies": {
41
- "firebase": "^12.7.0"
42
- },
43
40
  "peerDependencies": {
41
+ "firebase": ">=10.0.0",
44
42
  "@expo/vector-icons": ">=14.0.0",
45
43
  "@react-navigation/native": ">=6.0.0",
46
44
  "@react-navigation/stack": ">=6.0.0",
@@ -3,7 +3,7 @@
3
3
  * Pure presentational component for About screen
4
4
  * No business logic, only rendering
5
5
  */
6
- import React, { useMemo, useCallback } from 'react';
6
+ import React, { useMemo } from 'react';
7
7
  import {
8
8
  View,
9
9
  StyleSheet,
@@ -15,14 +15,80 @@ import type { DesignTokens } from '@umituz/react-native-design-system';
15
15
  import type { AboutScreenProps } from './AboutScreen';
16
16
 
17
17
  export interface AboutScreenContentProps extends Omit<AboutScreenProps, 'config'> {
18
- /** App info data */
19
18
  appInfo: AppInfo;
20
- /** Configuration for the about screen */
21
19
  config: AboutConfig;
22
- /** Design tokens */
23
20
  _tokens: DesignTokens;
24
21
  }
25
22
 
23
+ interface AboutHeaderSectionProps {
24
+ appInfo: AppInfo;
25
+ config: AboutConfig;
26
+ showHeader: boolean;
27
+ headerComponent?: React.ReactNode;
28
+ headerStyle?: AboutScreenContentProps['headerStyle'];
29
+ titleStyle?: AboutScreenContentProps['titleStyle'];
30
+ versionStyle?: AboutScreenContentProps['versionStyle'];
31
+ }
32
+
33
+ const AboutHeaderSection: React.FC<AboutHeaderSectionProps> = ({
34
+ appInfo,
35
+ config,
36
+ showHeader,
37
+ headerComponent,
38
+ headerStyle,
39
+ titleStyle,
40
+ versionStyle,
41
+ }) => {
42
+ if (headerComponent) {
43
+ return <>{headerComponent}</>;
44
+ }
45
+ if (!showHeader || !appInfo) {
46
+ return null;
47
+ }
48
+ return (
49
+ <AboutHeader
50
+ appInfo={appInfo}
51
+ containerStyle={headerStyle}
52
+ titleStyle={titleStyle}
53
+ versionStyle={versionStyle}
54
+ versionPrefix={config.texts?.versionPrefix}
55
+ />
56
+ );
57
+ };
58
+
59
+ interface AboutFooterSectionProps {
60
+ footerComponent?: React.ReactNode;
61
+ borderColor: string;
62
+ footerStyle: ReturnType<typeof getStyles>['footer'];
63
+ }
64
+
65
+ const AboutFooterSection: React.FC<AboutFooterSectionProps> = ({
66
+ footerComponent,
67
+ borderColor,
68
+ footerStyle,
69
+ }) => {
70
+ if (!footerComponent) {
71
+ return null;
72
+ }
73
+ return (
74
+ <View style={[footerStyle, { borderTopColor: borderColor }]}>
75
+ {footerComponent}
76
+ </View>
77
+ );
78
+ };
79
+
80
+ interface AboutContentSectionProps {
81
+ appInfo: AppInfo;
82
+ config: AboutConfig;
83
+ }
84
+
85
+ const AboutContentSection: React.FC<AboutContentSectionProps> = ({ appInfo, config }) => {
86
+ if (!appInfo) {
87
+ return null;
88
+ }
89
+ return <AboutContent appInfo={appInfo} config={config} />;
90
+ };
91
+
26
92
  export const AboutScreenContent: React.FC<AboutScreenContentProps> = ({
27
93
  appInfo,
28
94
  config,
@@ -39,71 +105,29 @@ export const AboutScreenContent: React.FC<AboutScreenContentProps> = ({
39
105
  const styles = getStyles(_tokens);
40
106
  const colors = _tokens.colors;
41
107
 
42
- // Memoize header rendering to prevent unnecessary re-renders
43
- const renderHeader = useCallback(() => {
44
- if (headerComponent) {
45
- return headerComponent;
46
- }
108
+ const containerStyles = useMemo(() => [
109
+ styles.container,
110
+ { backgroundColor: colors.backgroundPrimary },
111
+ containerStyle
112
+ ], [containerStyle, colors.backgroundPrimary, styles]);
47
113
 
48
- if (!showHeader || !appInfo) {
49
- return null;
50
- }
51
-
52
- return (
53
- <AboutHeader
114
+ return (
115
+ <View style={containerStyles} testID={testID}>
116
+ <AboutHeaderSection
54
117
  appInfo={appInfo}
55
- containerStyle={headerStyle}
118
+ config={config}
119
+ showHeader={showHeader}
120
+ headerComponent={headerComponent}
121
+ headerStyle={headerStyle}
56
122
  titleStyle={titleStyle}
57
123
  versionStyle={versionStyle}
58
- versionPrefix={config.texts?.versionPrefix}
59
124
  />
60
- );
61
- }, [headerComponent, showHeader, appInfo, headerStyle, titleStyle, versionStyle, config.texts?.versionPrefix]);
62
-
63
- // Memoize footer rendering
64
- const renderFooter = useCallback(() => {
65
- if (!footerComponent) {
66
- return null;
67
- }
68
-
69
- return (
70
- <View style={[styles.footer, { borderTopColor: colors.border }]}>
71
- {footerComponent}
72
- </View>
73
- );
74
- }, [footerComponent, colors.border, styles]);
75
-
76
- // Memoize content rendering
77
- const renderContent = useCallback(() => {
78
- if (!appInfo) {
79
- return null;
80
- }
81
-
82
- return (
83
- <AboutContent
84
- appInfo={appInfo}
85
- config={config}
125
+ <AboutContentSection appInfo={appInfo} config={config} />
126
+ <AboutFooterSection
127
+ footerComponent={footerComponent}
128
+ borderColor={colors.border}
129
+ footerStyle={styles.footer}
86
130
  />
87
- );
88
- }, [appInfo, config]);
89
-
90
- // Memoize container style to prevent unnecessary re-renders
91
- const containerStyles = useMemo(() => {
92
- return [
93
- styles.container,
94
- { backgroundColor: colors.backgroundPrimary },
95
- containerStyle
96
- ];
97
- }, [containerStyle, colors.backgroundPrimary, styles]);
98
-
99
- return (
100
- <View
101
- style={containerStyles}
102
- testID={testID}
103
- >
104
- {renderHeader()}
105
- {renderContent()}
106
- {renderFooter()}
107
131
  </View>
108
132
  );
109
133
  };
@@ -64,26 +64,3 @@ export const DEFAULT_ACCENT_COLORS: ColorPalette = {
64
64
  ],
65
65
  };
66
66
 
67
- /**
68
- * Generate custom color palette
69
- * @param baseColor - Base color to generate variations from
70
- * @param count - Number of variations to generate
71
- * @returns Color palette with variations
72
- */
73
- export const generateColorPalette = (
74
- baseColor: string,
75
- count: number = 8
76
- ): ColorPalette => {
77
- const colors: string[] = [];
78
-
79
- for (let i = 0; i < count; i++) {
80
- // Simple color variation logic - in real implementation,
81
- // this would use HSL/HSV color space manipulation
82
- colors.push(baseColor);
83
- }
84
-
85
- return {
86
- name: "custom",
87
- colors,
88
- };
89
- };
@@ -81,8 +81,6 @@ export const CustomColorsSection: React.FC<CustomColorsSectionProps> = ({
81
81
  // Memoize styles to prevent unnecessary re-creation
82
82
  const styles = useMemo(() => getStyles(tokens), [tokens]);
83
83
 
84
- // Memoize color fields to prevent unnecessary re-renders
85
- const colorFieldsMemo = useMemo(() => colorFields, [colorFields]);
86
84
 
87
85
  // Stable callback for color change to prevent infinite re-renders
88
86
  const handleColorChange = useCallback((key: keyof CustomThemeColors, color: string) => {
@@ -104,7 +102,7 @@ export const CustomColorsSection: React.FC<CustomColorsSectionProps> = ({
104
102
 
105
103
  // Memoize color pickers to prevent unnecessary re-renders
106
104
  const colorPickers = useMemo(() => {
107
- return colorFieldsMemo.map((field) => (
105
+ return colorFields.map((field) => (
108
106
  <ColorPicker
109
107
  key={field.key}
110
108
  label={field.label}
@@ -113,7 +111,7 @@ export const CustomColorsSection: React.FC<CustomColorsSectionProps> = ({
113
111
  colors={field.colorPalette}
114
112
  />
115
113
  ));
116
- }, [colorFieldsMemo, localCustomColors, handleColorChange]);
114
+ }, [colorFields, localCustomColors, handleColorChange]);
117
115
 
118
116
  // Memoize reset button to prevent unnecessary re-renders
119
117
  const resetButton = useMemo(() => {
@@ -113,9 +113,9 @@ export const ThemeOption: React.FC<ThemeOptionProps> = React.memo(({
113
113
  {featuresTitle}
114
114
  </AtomicText>
115
115
  ) : null}
116
- {features.map((feature, index) => (
116
+ {features.map((feature) => (
117
117
  <AtomicText
118
- key={`feature-${index}-${feature.slice(0, 10)}`}
118
+ key={feature}
119
119
  type="bodySmall"
120
120
  color="secondary"
121
121
  style={styles.feature}
@@ -39,11 +39,14 @@ export interface DevSettingsProps {
39
39
  customDevComponents?: React.ReactNode[];
40
40
  }
41
41
 
42
+ const EMPTY_TEXTS: Partial<typeof DEFAULT_TEXTS> = {};
43
+ const EMPTY_DEV_COMPONENTS: React.ReactNode[] = [];
44
+
42
45
  export const DevSettingsSection: React.FC<DevSettingsProps> = ({
43
46
  enabled = true,
44
47
  onAfterClear,
45
- texts = {},
46
- customDevComponents = [],
48
+ texts = EMPTY_TEXTS,
49
+ customDevComponents = EMPTY_DEV_COMPONENTS,
47
50
  }) => {
48
51
  const tokens = useAppDesignTokens();
49
52
 
@@ -122,37 +122,31 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
122
122
  <View style={{ height: tokens.spacing.xl * 2 }} />
123
123
  ), [tokens.spacing.xl]);
124
124
 
125
- const renderContent = () => {
126
- return (
127
- <View style={{ flex: 1 }}>
128
- <FlatList
129
- data={filteredCategories}
130
- renderItem={renderCategory}
131
- keyExtractor={keyExtractor}
132
- ListHeaderComponent={renderListHeader}
133
- ListEmptyComponent={renderListEmpty}
134
- ListFooterComponent={renderListFooter}
135
- style={[styles.content, customStyles?.content]}
136
- contentContainerStyle={{ paddingVertical: tokens.spacing.md }}
137
- showsVerticalScrollIndicator={false}
138
- initialNumToRender={5}
139
- maxToRenderPerBatch={5}
140
- windowSize={10}
141
- removeClippedSubviews={true}
142
- />
143
- </View>
144
- );
145
- };
146
-
147
125
  return (
148
- <ScreenLayout
149
- edges={['bottom']}
126
+ <ScreenLayout
127
+ edges={['bottom']}
150
128
  scrollable={false}
151
129
  header={header}
152
130
  >
153
131
  <View style={[styles.container, customStyles?.container]}>
154
132
  <View style={{ alignSelf: 'center', width: '100%', maxWidth: contentMaxWidth, flex: 1 }}>
155
- {renderContent()}
133
+ <View style={{ flex: 1 }}>
134
+ <FlatList
135
+ data={filteredCategories}
136
+ renderItem={renderCategory}
137
+ keyExtractor={keyExtractor}
138
+ ListHeaderComponent={renderListHeader}
139
+ ListEmptyComponent={renderListEmpty}
140
+ ListFooterComponent={renderListFooter}
141
+ style={[styles.content, customStyles?.content]}
142
+ contentContainerStyle={{ paddingVertical: tokens.spacing.md }}
143
+ showsVerticalScrollIndicator={false}
144
+ initialNumToRender={5}
145
+ maxToRenderPerBatch={5}
146
+ windowSize={10}
147
+ removeClippedSubviews={true}
148
+ />
149
+ </View>
156
150
  </View>
157
151
  </View>
158
152
  </ScreenLayout>
@@ -3,13 +3,140 @@
3
3
  * Form for submitting user feedback with type, rating, and description
4
4
  */
5
5
 
6
- import React, { useState } from "react";
6
+ import React, { useReducer } 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
10
  import { validateFeedbackForm } from "../../../../infrastructure/utils/validation";
11
11
  import type { FeedbackFormProps } from "./FeedbackFormProps";
12
12
  import { getFeedbackFormStyles as getStyles } from "./FeedbackForm.styles";
13
+ import type { DesignTokens } from "@umituz/react-native-design-system";
14
+
15
+ interface FeedbackFormState {
16
+ selectedType: FeedbackType;
17
+ rating: FeedbackRating;
18
+ description: string;
19
+ title: string;
20
+ error: string | null;
21
+ isSubmittingLocal: boolean;
22
+ }
23
+
24
+ type FeedbackFormAction =
25
+ | { type: 'SET_TYPE'; payload: FeedbackType }
26
+ | { type: 'SET_RATING'; payload: FeedbackRating }
27
+ | { type: 'SET_DESCRIPTION'; payload: string }
28
+ | { type: 'SET_TITLE'; payload: string }
29
+ | { type: 'SET_ERROR'; payload: string | null }
30
+ | { type: 'SET_SUBMITTING'; payload: boolean }
31
+ | { type: 'RESET_FORM'; payload: FeedbackRating };
32
+
33
+ function feedbackFormReducer(state: FeedbackFormState, action: FeedbackFormAction): FeedbackFormState {
34
+ switch (action.type) {
35
+ case 'SET_TYPE':
36
+ return { ...state, selectedType: action.payload };
37
+ case 'SET_RATING':
38
+ return { ...state, rating: action.payload };
39
+ case 'SET_DESCRIPTION':
40
+ return { ...state, description: action.payload, error: null };
41
+ case 'SET_TITLE':
42
+ return { ...state, title: action.payload };
43
+ case 'SET_ERROR':
44
+ return { ...state, error: action.payload };
45
+ case 'SET_SUBMITTING':
46
+ return { ...state, isSubmittingLocal: action.payload };
47
+ case 'RESET_FORM':
48
+ return { ...state, description: "", title: "", rating: action.payload };
49
+ }
50
+ }
51
+
52
+ const STAR_VALUES = [1, 2, 3, 4, 5] as const;
53
+
54
+ interface FeedbackRatingSectionProps {
55
+ rating: FeedbackRating;
56
+ onRatingChange: (star: FeedbackRating) => void;
57
+ ratingLabel: string;
58
+ styles: ReturnType<typeof getStyles>;
59
+ tokens: DesignTokens;
60
+ }
61
+
62
+ const FeedbackRatingSection: React.FC<FeedbackRatingSectionProps> = ({
63
+ rating,
64
+ onRatingChange,
65
+ ratingLabel,
66
+ styles,
67
+ tokens,
68
+ }) => (
69
+ <View style={styles.ratingContainer}>
70
+ <AtomicText type="bodyMedium" style={{ marginBottom: 8, color: tokens.colors.textSecondary }}>
71
+ {ratingLabel}
72
+ </AtomicText>
73
+ <View style={styles.stars}>
74
+ {STAR_VALUES.map((star) => (
75
+ <TouchableOpacity
76
+ key={star}
77
+ onPress={() => onRatingChange(star as FeedbackRating)}
78
+ style={styles.starButton}
79
+ >
80
+ <AtomicIcon
81
+ name={star <= rating ? "star" : "star-outline"}
82
+ customSize={32}
83
+ customColor={star <= rating ? tokens.colors.warning : tokens.colors.border}
84
+ />
85
+ </TouchableOpacity>
86
+ ))}
87
+ </View>
88
+ </View>
89
+ );
90
+
91
+ interface FeedbackTypeSelectorProps {
92
+ feedbackTypes: Array<{ type: FeedbackType; label: string }>;
93
+ selectedType: FeedbackType;
94
+ onTypeSelect: (type: FeedbackType) => void;
95
+ styles: ReturnType<typeof getStyles>;
96
+ tokens: DesignTokens;
97
+ }
98
+
99
+ const FeedbackTypeSelector: React.FC<FeedbackTypeSelectorProps> = ({
100
+ feedbackTypes,
101
+ selectedType,
102
+ onTypeSelect,
103
+ styles,
104
+ tokens,
105
+ }) => (
106
+ <ScrollView
107
+ horizontal
108
+ showsHorizontalScrollIndicator={false}
109
+ contentContainerStyle={styles.typeScroll}
110
+ style={styles.typeContainer}
111
+ >
112
+ {feedbackTypes.map((item) => {
113
+ const isSelected = selectedType === item.type;
114
+ return (
115
+ <TouchableOpacity
116
+ key={item.type}
117
+ style={[
118
+ styles.typeButton,
119
+ {
120
+ backgroundColor: isSelected ? tokens.colors.primary : tokens.colors.surface,
121
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
122
+ },
123
+ ]}
124
+ onPress={() => onTypeSelect(item.type)}
125
+ >
126
+ <AtomicText
127
+ type="bodySmall"
128
+ style={{
129
+ color: isSelected ? tokens.colors.onPrimary : tokens.colors.textSecondary,
130
+ fontWeight: isSelected ? "600" : "400",
131
+ }}
132
+ >
133
+ {item.label}
134
+ </AtomicText>
135
+ </TouchableOpacity>
136
+ );
137
+ })}
138
+ </ScrollView>
139
+ );
13
140
 
14
141
  export const FeedbackForm: React.FC<FeedbackFormProps> = ({
15
142
  onSubmit,
@@ -19,15 +146,19 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
19
146
  }) => {
20
147
  const tokens = useAppDesignTokens();
21
148
  const styles = getStyles(tokens);
22
- const [selectedType, setSelectedType] = useState<FeedbackType>(initialType || texts.feedbackTypes[0].type);
23
- const [rating, setRating] = useState<FeedbackRating>(5);
24
- const [description, setDescription] = useState("");
25
- const [title, setTitle] = useState("");
26
- const [error, setError] = useState<string | null>(null);
27
- const [isSubmittingLocal, setIsSubmittingLocal] = useState(false);
149
+
150
+ const [state, dispatch] = useReducer(feedbackFormReducer, {
151
+ selectedType: initialType || texts.feedbackTypes[0].type,
152
+ rating: 5,
153
+ description: "",
154
+ title: "",
155
+ error: null,
156
+ isSubmittingLocal: false,
157
+ });
158
+
159
+ const { selectedType, rating, description, title, error, isSubmittingLocal } = state;
28
160
 
29
161
  const handleSubmit = async () => {
30
- // Validate using centralized validation
31
162
  const validationResult = validateFeedbackForm({
32
163
  type: selectedType,
33
164
  rating,
@@ -35,12 +166,12 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
35
166
  });
36
167
 
37
168
  if (!validationResult.isValid) {
38
- setError(validationResult.error || "Validation failed");
169
+ dispatch({ type: 'SET_ERROR', payload: validationResult.error || "Validation failed" });
39
170
  return;
40
171
  }
41
172
 
42
- setIsSubmittingLocal(true);
43
- setError(null);
173
+ dispatch({ type: 'SET_SUBMITTING', payload: true });
174
+ dispatch({ type: 'SET_ERROR', payload: null });
44
175
 
45
176
  try {
46
177
  await onSubmit({
@@ -50,90 +181,38 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
50
181
  title: title || texts.defaultTitle(selectedType),
51
182
  });
52
183
 
53
- // Clear form on success
54
- setDescription("");
55
- setTitle("");
56
- setRating(5);
184
+ dispatch({ type: 'RESET_FORM', payload: 5 });
57
185
  } catch (err) {
58
186
  const errorMessage = err instanceof Error ? err.message : "Failed to submit feedback";
59
- setError(errorMessage);
187
+ dispatch({ type: 'SET_ERROR', payload: errorMessage });
60
188
  } finally {
61
- setIsSubmittingLocal(false);
189
+ dispatch({ type: 'SET_SUBMITTING', payload: false });
62
190
  }
63
191
  };
64
192
 
65
- const renderRating = () => (
66
- <View style={styles.ratingContainer}>
67
- <AtomicText type="bodyMedium" style={{ marginBottom: 8, color: tokens.colors.textSecondary }}>
68
- {texts.ratingLabel}
69
- </AtomicText>
70
- <View style={styles.stars}>
71
- {[1, 2, 3, 4, 5].map((star) => (
72
- <TouchableOpacity
73
- key={star}
74
- onPress={() => setRating(star as FeedbackRating)}
75
- style={styles.starButton}
76
- >
77
- <AtomicIcon
78
- name={star <= rating ? "star" : "star-outline"}
79
- customSize={32}
80
- customColor={star <= rating ? tokens.colors.warning : tokens.colors.border}
81
- />
82
- </TouchableOpacity>
83
- ))}
84
- </View>
85
- </View>
86
- );
87
-
88
- const renderTypeSelector = () => (
89
- <ScrollView
90
- horizontal
91
- showsHorizontalScrollIndicator={false}
92
- contentContainerStyle={styles.typeScroll}
93
- style={styles.typeContainer}
94
- >
95
- {texts.feedbackTypes.map((item) => {
96
- const isSelected = selectedType === item.type;
97
-
98
- return (
99
- <TouchableOpacity
100
- key={item.type}
101
- style={[
102
- styles.typeButton,
103
- {
104
- backgroundColor: isSelected ? tokens.colors.primary : tokens.colors.surface,
105
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
106
- },
107
- ]}
108
- onPress={() => setSelectedType(item.type)}
109
- >
110
- <AtomicText
111
- type="bodySmall"
112
- style={{
113
- color: isSelected ? tokens.colors.onPrimary : tokens.colors.textSecondary,
114
- fontWeight: isSelected ? "600" : "400",
115
- }}
116
- >
117
- {item.label}
118
- </AtomicText>
119
- </TouchableOpacity>
120
- );
121
- })}
122
- </ScrollView>
123
- );
124
-
125
193
  return (
126
194
  <View style={styles.container}>
127
- {renderTypeSelector()}
195
+ <FeedbackTypeSelector
196
+ feedbackTypes={texts.feedbackTypes}
197
+ selectedType={selectedType}
198
+ onTypeSelect={(type: FeedbackType) => dispatch({ type: 'SET_TYPE', payload: type })}
199
+ styles={styles}
200
+ tokens={tokens}
201
+ />
128
202
 
129
- {renderRating()}
203
+ <FeedbackRatingSection
204
+ rating={rating}
205
+ onRatingChange={(star: FeedbackRating) => dispatch({ type: 'SET_RATING', payload: star })}
206
+ ratingLabel={texts.ratingLabel}
207
+ styles={styles}
208
+ tokens={tokens}
209
+ />
130
210
 
131
211
  <View style={styles.inputContainer}>
132
212
  <TextInput
133
213
  value={description}
134
214
  onChangeText={(text) => {
135
- setDescription(text);
136
- setError(null); // Clear error on input
215
+ dispatch({ type: 'SET_DESCRIPTION', payload: text });
137
216
  }}
138
217
  placeholder={texts.descriptionPlaceholder}
139
218
  placeholderTextColor={tokens.colors.textTertiary}