@fadyshawky/react-native-magic 2.2.1 → 2.3.0

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -56
  3. package/index.js +4 -0
  4. package/package.json +8 -2
  5. package/scripts/askPackageName.js +10 -5
  6. package/template/.env.development +8 -6
  7. package/template/.env.example +15 -5
  8. package/template/.env.production +8 -6
  9. package/template/.env.staging +8 -6
  10. package/template/.eslintrc.js +14 -0
  11. package/template/.husky/pre-commit +1 -0
  12. package/template/App.tsx +47 -16
  13. package/template/__tests__/App.test.tsx +28 -10
  14. package/template/babel.config.js +20 -1
  15. package/template/docs/ARCHITECTURE.md +40 -10
  16. package/template/docs/BEST_PRACTICES.md +10 -1
  17. package/template/docs/CUSTOMIZATION.md +118 -5
  18. package/template/docs/design-system.html +1164 -0
  19. package/template/docs/wireframes.html +411 -0
  20. package/template/index.js +10 -0
  21. package/template/jest.config.js +16 -1
  22. package/template/jest.setup.js +61 -0
  23. package/template/package-lock.json +12178 -8293
  24. package/template/package.json +53 -20
  25. package/template/react-native.config.js +3 -0
  26. package/template/resources/fonts/.gitkeep +0 -0
  27. package/template/scripts/ci-sync-env.cjs +71 -0
  28. package/template/src/assets/brand/logo-mark.svg +8 -0
  29. package/template/src/assets/brand/logo-mono.svg +9 -0
  30. package/template/src/assets/brand/logo-primary.svg +15 -0
  31. package/template/src/assets/brand/wordmark-dark.svg +18 -0
  32. package/template/src/common/components/AppBottomSheet.tsx +87 -0
  33. package/template/src/common/components/AppSwitch.tsx +75 -0
  34. package/template/src/common/components/AppTextInput.tsx +161 -0
  35. package/template/src/common/components/Avatar.tsx +75 -0
  36. package/template/src/common/components/Badge.tsx +66 -0
  37. package/template/src/common/components/CardScroller.tsx +58 -0
  38. package/template/src/common/components/Cards.tsx +13 -7
  39. package/template/src/common/components/Carousel.tsx +196 -0
  40. package/template/src/common/components/Checkbox.tsx +85 -0
  41. package/template/src/common/components/Chip.tsx +55 -0
  42. package/template/src/common/components/Dropdown.tsx +202 -0
  43. package/template/src/common/components/ErrorBoundary.tsx +82 -0
  44. package/template/src/common/components/FlatListWrapper.tsx +4 -5
  45. package/template/src/common/components/ListItem.tsx +90 -0
  46. package/template/src/common/components/LoadingComponent.tsx +8 -2
  47. package/template/src/common/components/Logo.tsx +77 -0
  48. package/template/src/common/components/ModalDialog.tsx +141 -0
  49. package/template/src/common/components/NetworkBanner.tsx +47 -0
  50. package/template/src/common/components/OTPInput.tsx +0 -1
  51. package/template/src/common/components/PrimaryButton.tsx +0 -14
  52. package/template/src/common/components/PrimaryTextInput.tsx +66 -130
  53. package/template/src/common/components/RadioGroup.tsx +95 -0
  54. package/template/src/common/components/SafeText.tsx +4 -3
  55. package/template/src/common/components/SearchBar.tsx +7 -5
  56. package/template/src/common/components/SegmentedControl.tsx +77 -0
  57. package/template/src/common/components/Skeleton.tsx +47 -0
  58. package/template/src/common/components/TryAgain.tsx +4 -2
  59. package/template/src/common/helpers/arrayHelpers.ts +2 -2
  60. package/template/src/common/helpers/defaultKeyIdExtractor.ts +1 -1
  61. package/template/src/common/helpers/regexHelpers.ts +1 -2
  62. package/template/src/common/helpers/stringsHelpers.ts +0 -1
  63. package/template/src/common/hooks/useBackHandler.ts +5 -2
  64. package/template/src/common/hooks/useEventRegister.ts +1 -1
  65. package/template/src/common/hooks/useFlatListActions.ts +1 -1
  66. package/template/src/common/hooks/useWhyDidYouUpdate.ts +1 -1
  67. package/template/src/common/localization/LocalizationProvider.tsx +1 -1
  68. package/template/src/common/localization/RTLInitializer.tsx +1 -1
  69. package/template/src/common/localization/dateFormatter.ts +0 -1
  70. package/template/src/common/localization/intlFormatter.ts +0 -1
  71. package/template/src/common/localization/localization.ts +2 -2
  72. package/template/src/common/localization/translations/homeLocalization.ts +14 -0
  73. package/template/src/common/localization/translations/loginLocalization.ts +8 -0
  74. package/template/src/common/localization/translations/mainNavigationLocalization.ts +2 -0
  75. package/template/src/common/localization/translations/profileLocalization.ts +16 -0
  76. package/template/src/common/utils/index.tsx +0 -6
  77. package/template/src/common/validations/commonValidations.ts +2 -2
  78. package/template/src/core/api/errorHandler.ts +1 -1
  79. package/template/src/core/api/responseHandlers.ts +1 -3
  80. package/template/src/core/api/serverHeaders.ts +61 -12
  81. package/template/src/core/notifications/notificationAuth.ts +6 -0
  82. package/template/src/core/notifications/notificationService.ts +125 -0
  83. package/template/src/core/notifications/routeFromNotificationData.ts +32 -0
  84. package/template/src/core/store/categories/categoriesActions.ts +25 -0
  85. package/template/src/core/store/categories/categoriesSlice.ts +51 -0
  86. package/template/src/core/store/categories/categoriesState.ts +19 -0
  87. package/template/src/core/store/rootReducer.ts +2 -0
  88. package/template/src/core/store/store.tsx +6 -1
  89. package/template/src/core/store/user/userActions.ts +75 -14
  90. package/template/src/core/store/user/userSlice.ts +49 -26
  91. package/template/src/core/store/user/userState.ts +6 -4
  92. package/template/src/core/theme/ThemeProvider.tsx +5 -3
  93. package/template/src/core/theme/brand.ts +50 -0
  94. package/template/src/core/theme/colors.ts +113 -99
  95. package/template/src/core/theme/commonConsts.ts +2 -2
  96. package/template/src/core/theme/commonStyles.ts +1 -1
  97. package/template/src/core/theme/themes.ts +2 -0
  98. package/template/src/core/theme/types.ts +4 -2
  99. package/template/src/core/utils/stringUtils.ts +1 -1
  100. package/template/src/design-system/index.ts +2 -0
  101. package/template/src/design-system/tokens/brand.ts +6 -0
  102. package/template/src/design-system/tokens/index.ts +3 -0
  103. package/template/src/design-system/tokens/palette.ts +4 -0
  104. package/template/src/design-system/tokens/typography-spacing.ts +2 -0
  105. package/template/src/navigation/AuthStack.tsx +1 -4
  106. package/template/src/navigation/HeaderComponents.tsx +6 -3
  107. package/template/src/navigation/MainStack.tsx +18 -6
  108. package/template/src/navigation/RootNavigation.tsx +4 -7
  109. package/template/src/navigation/TabBar.tsx +7 -6
  110. package/template/src/navigation/types.ts +10 -31
  111. package/template/src/screens/Login/Login.tsx +47 -47
  112. package/template/src/screens/OTP/OTPScreen.tsx +6 -9
  113. package/template/src/screens/components/ComponentsScreen.tsx +301 -0
  114. package/template/src/screens/home/HomeScreen.tsx +143 -1
  115. package/template/src/screens/home/hooks/useHomeData.ts +19 -5
  116. package/template/src/screens/index.tsx +1 -0
  117. package/template/src/screens/profile/Profile.tsx +139 -2
  118. package/template/src/screens/splash/Splash.tsx +44 -11
  119. package/template/src/sheetManager/sheets.tsx +1 -1
  120. package/template/tsconfig.json +14 -2
  121. package/template/types/globals.d.ts +43 -0
  122. package/template/types/index.ts +2 -6
  123. package/template/types/modules.d.ts +9 -0
  124. package/template/types/react-native-config.d.ts +0 -2
  125. package/.vscode/settings.json +0 -8
  126. package/CHANGELOG.md +0 -119
  127. package/CODE_OF_CONDUCT.md +0 -83
  128. package/CONTRIBUTING.md +0 -60
  129. package/local.properties +0 -1
  130. package/template/src/common/components/ImageCropPickerButton.tsx +0 -107
  131. package/template/src/common/components/PhotoTakingButton.tsx +0 -94
  132. package/template/src/common/helpers/imageHelpers.ts +0 -5
  133. package/template/src/common/helpers/inAppReviewHelper.ts +0 -30
  134. package/template/src/common/helpers/orientationHelpers.ts +0 -25
  135. package/template/src/common/helpers/shareHelpers.ts +0 -47
  136. package/template/src/common/utils/FeesCaalculation.tsx +0 -37
  137. package/template/src/common/utils/printData.tsx +0 -161
  138. package/template/src/common/validations/examples/TextInputWithValidation.tsx +0 -229
@@ -4,7 +4,6 @@ import {
4
4
  Image,
5
5
  ImageStyle,
6
6
  ImageURISource,
7
- Platform,
8
7
  StyleProp,
9
8
  StyleSheet,
10
9
  Text,
@@ -192,18 +191,6 @@ function createButtonStyles(theme: Theme) {
192
191
  width: '100%',
193
192
  };
194
193
 
195
- const commonLabelStyle: TextStyle = {
196
- ...createThemedStyles(theme).h4_bold,
197
- color: theme.colors.white,
198
- textAlign: 'center',
199
- textAlignVertical: 'center',
200
- ...Platform.select({
201
- android: {
202
- textTransform: 'uppercase',
203
- } as TextStyle,
204
- }),
205
- };
206
-
207
194
  const commonIcon: ImageStyle = {
208
195
  width: 22,
209
196
  height: 22,
@@ -268,7 +255,6 @@ function createButtonStyles(theme: Theme) {
268
255
  }
269
256
 
270
257
  function createSmallSolidStyles(theme: Theme): IStyles {
271
- const commonStyles = createThemedStyles(theme);
272
258
  return StyleSheet.create({
273
259
  button: {
274
260
  padding: CommonSizes.spacing.medium,
@@ -1,4 +1,3 @@
1
- import {GradientBorderView} from '@good-react-native/gradient-border';
2
1
  import React, {
3
2
  FC,
4
3
  memo,
@@ -9,25 +8,20 @@ import React, {
9
8
  useState,
10
9
  } from 'react';
11
10
  import {
12
- NativeSyntheticEvent,
13
11
  Platform,
14
12
  StyleSheet,
15
13
  Text,
16
14
  TextInput,
17
- TextInputFocusEventData,
18
15
  TextInputProps,
19
- TextInputSubmitEditingEventData,
20
16
  TextStyle,
21
17
  View,
22
18
  ViewStyle,
23
19
  } from 'react-native';
24
20
  import {useTheme} from '../../core/theme/ThemeProvider';
25
- import {PrimaryColors, AlertColors, NaturalColors} from '../../core/theme/colors';
26
- import {isIos} from '../../core/theme/commonConsts';
21
+ import {PrimaryColors, AlertColors} from '../../core/theme/colors';
27
22
  import {CommonSizes} from '../../core/theme/commonSizes';
28
23
  import {CommonStyles} from '../../core/theme/commonStyles';
29
24
  import {scaleHeight} from '../../core/theme/scaling';
30
- import {localization} from '../localization/localization';
31
25
  import {regexValidation} from '../validations/regexValidator';
32
26
 
33
27
  interface IProps extends TextInputProps {
@@ -63,26 +57,26 @@ interface IProps extends TextInputProps {
63
57
  export const PrimaryTextInput: FC<IProps> = memo(
64
58
  ({
65
59
  style,
66
- blurOnSubmit = true,
67
- disableFullscreenUI = true,
68
- enablesReturnKeyAutomatically = true,
69
- underlineColorAndroid,
70
- placeholderTextColor,
60
+ blurOnSubmit: _blurOnSubmit = true,
61
+ disableFullscreenUI: _disableFullscreenUI = true,
62
+ enablesReturnKeyAutomatically: _enablesReturnKeyAutomatically = true,
63
+ underlineColorAndroid: _underlineColorAndroid,
64
+ placeholderTextColor: _placeholderTextColor,
71
65
  editable = true,
72
- clearButtonMode = 'while-editing',
73
- label,
66
+ clearButtonMode: _clearButtonMode = 'while-editing',
67
+ label: _label,
74
68
  keyboardType = 'numeric',
75
69
  error,
76
70
  hint,
77
- containerStyle,
71
+ containerStyle: _containerStyle,
78
72
  inputRef,
79
- nextInputFocusRef,
73
+ nextInputFocusRef: _nextInputFocusRef,
80
74
  onTouchStart,
81
75
  onFocus,
82
76
  onBlur,
83
- onSubmitEditing,
84
- required,
85
- optional,
77
+ onSubmitEditing: _onSubmitEditing,
78
+ required: _required,
79
+ optional: _optional,
86
80
  width,
87
81
  height,
88
82
  regex,
@@ -93,17 +87,8 @@ export const PrimaryTextInput: FC<IProps> = memo(
93
87
  const {theme} = useTheme();
94
88
  const [regexError, setRegexError] = useState<string | null>(null);
95
89
 
96
- // Ensure gradient colors are always a valid array (BVLinearGradient crashes on null/undefined)
97
- const gradientColors = useMemo(
98
- () => [
99
- theme.colors.mutedLavender ?? NaturalColors.naturalColor_100,
100
- theme.colors.indigoBlue ?? PrimaryColors.PlatinateBlue_400,
101
- ],
102
- [theme.colors.mutedLavender, theme.colors.indigoBlue],
103
- );
104
-
105
90
  const onLocalFocus = useCallback(
106
- (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
91
+ (e: any) => {
107
92
  setFocused(true);
108
93
  onFocus && onFocus(e);
109
94
  },
@@ -111,31 +96,13 @@ export const PrimaryTextInput: FC<IProps> = memo(
111
96
  );
112
97
 
113
98
  const onLocalBlur = useCallback(
114
- (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
99
+ (e: any) => {
115
100
  setFocused(false);
116
101
  onBlur && onBlur(e);
117
102
  },
118
103
  [onBlur, setFocused],
119
104
  );
120
105
 
121
- const inputContainerStyle = useMemo(() => {
122
- return getInputContainerStyle(
123
- isFocused,
124
- error,
125
- onTouchStart ? true : editable,
126
- );
127
- }, [isFocused, error, editable, onTouchStart]);
128
-
129
- const onLocalSubmitEditing = useCallback(
130
- (e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
131
- onSubmitEditing && onSubmitEditing(e);
132
- nextInputFocusRef &&
133
- nextInputFocusRef.current &&
134
- nextInputFocusRef.current.focus();
135
- },
136
- [nextInputFocusRef, onSubmitEditing],
137
- );
138
-
139
106
  const pointerEvents = useMemo(() => {
140
107
  return onTouchStart ? 'none' : undefined;
141
108
  }, [onTouchStart]);
@@ -155,52 +122,50 @@ export const PrimaryTextInput: FC<IProps> = memo(
155
122
  props.onChangeText(text);
156
123
  }
157
124
  },
125
+ // props.onChangeText is referenced directly; keep deps stable to avoid
126
+ // re-creating the callback on every parent render.
127
+ // eslint-disable-next-line react-hooks/exhaustive-deps
158
128
  [regex, regexErrorMessage, props.onChangeText],
159
129
  );
160
130
 
131
+ const containerStyle: ViewStyle = {
132
+ ...styles.outerContainer,
133
+ width: width ?? '100%',
134
+ };
135
+
136
+ const inputWrapperStyle: ViewStyle = {
137
+ ...styles.inputWrapper,
138
+ borderColor: error
139
+ ? theme.colors.red
140
+ : isFocused
141
+ ? theme.colors.indigoBlue
142
+ : theme.colors.strokeDeactive,
143
+ height: height ?? scaleHeight(84),
144
+ backgroundColor: theme.colors.backgroundOpacity,
145
+ };
146
+
147
+ const textInputStyle: TextStyle = {
148
+ ...theme.text.body1,
149
+ paddingStart: CommonSizes.spacing.medium,
150
+ ...Platform.select({
151
+ android: {
152
+ paddingEnd: CommonSizes.spacing.medium,
153
+ },
154
+ }),
155
+ };
156
+
161
157
  return (
162
- <View
163
- style={{
164
- justifyContent: 'space-between',
165
- gap: CommonSizes.spacing.small,
166
- width: width ?? '100%',
167
- }}>
168
- <GradientBorderView
169
- gradientProps={{
170
- colors: gradientColors,
171
- }}
172
- style={{
173
- borderWidth: CommonSizes.borderWidth.small,
174
- borderRadius: CommonSizes.borderRadius.medium,
175
- height: height ?? scaleHeight(84),
176
- width: '100%',
177
- backgroundColor: theme.colors.backgroundOpacity,
178
- }}>
158
+ <View style={containerStyle}>
159
+ <View style={inputWrapperStyle}>
179
160
  <TextInput
180
161
  disableFullscreenUI={true}
181
162
  selectionColor={selectionColor}
182
163
  {...props}
183
164
  pointerEvents={pointerEvents}
184
165
  ref={inputRef}
185
- style={[
186
- {
187
- ...theme.text.body1,
188
- width: '100%',
189
- zIndex: 2,
190
- alignSelf: 'flex-start',
191
- alignItems: 'center',
192
- justifyContent: 'center',
193
- flex: 1,
194
- textAlignVertical: 'center',
195
- paddingStart: CommonSizes.spacing.medium,
196
- ...Platform.select({
197
- android: {
198
- paddingEnd: CommonSizes.spacing.medium,
199
- },
200
- }),
201
- },
202
- style,
203
- ]}
166
+ onFocus={onLocalFocus}
167
+ onBlur={onLocalBlur}
168
+ style={[styles.textInput, textInputStyle, style]}
204
169
  onChangeText={handleChangeText}
205
170
  placeholderTextColor={theme.colors.tintColor}
206
171
  autoCapitalize="none"
@@ -208,38 +173,13 @@ export const PrimaryTextInput: FC<IProps> = memo(
208
173
  keyboardType={keyboardType}
209
174
  editable={editable}
210
175
  />
211
- </GradientBorderView>
176
+ </View>
212
177
  <BottomText error={error || regexError} hint={hint} />
213
178
  </View>
214
179
  );
215
180
  },
216
181
  );
217
182
 
218
- const Label: FC<{text?: string; required?: boolean; optional?: boolean}> = memo(
219
- ({text, required, optional}) => {
220
- const {theme} = useTheme();
221
- if (text != null) {
222
- return (
223
- <Text
224
- style={{
225
- ...theme.text.label,
226
- color: theme.colors.indigoBlue,
227
- }}
228
- numberOfLines={1}>
229
- {text +
230
- (required
231
- ? localization.common.required
232
- : optional
233
- ? localization.common.optional
234
- : '')}
235
- </Text>
236
- );
237
- } else {
238
- return null;
239
- }
240
- },
241
- );
242
-
243
183
  const BottomText: FC<{error?: string | null; hint?: string}> = memo(
244
184
  ({error, hint}) => {
245
185
  const {theme} = useTheme();
@@ -257,26 +197,6 @@ const BottomText: FC<{error?: string | null; hint?: string}> = memo(
257
197
  },
258
198
  );
259
199
 
260
- function getInputContainerStyle(
261
- isFocused: boolean,
262
- error?: string | null,
263
- isEditable?: boolean,
264
- ): ViewStyle {
265
- if (isIos) {
266
- return !isEditable ? styles.disabledInputContainer : styles.inputContainer;
267
- } else {
268
- if (isFocused) {
269
- return styles.focusedInputContainer;
270
- } else if (!isEditable) {
271
- return styles.disabledInputContainer;
272
- } else if (error) {
273
- return styles.errorInputContainer;
274
- } else {
275
- return styles.inputContainer;
276
- }
277
- }
278
- }
279
-
280
200
  const selectionColor = PrimaryColors.PlatinateBlue_400;
281
201
 
282
202
  const commonInputContainer: TextStyle = {
@@ -291,6 +211,22 @@ const commonInputContainer: TextStyle = {
291
211
  };
292
212
 
293
213
  const styles = StyleSheet.create({
214
+ outerContainer: {
215
+ justifyContent: 'space-between',
216
+ gap: CommonSizes.spacing.small,
217
+ } as ViewStyle,
218
+ inputWrapper: {
219
+ flexDirection: 'row',
220
+ alignItems: 'center',
221
+ borderWidth: CommonSizes.borderWidth.small,
222
+ borderRadius: CommonSizes.borderRadius.medium,
223
+ width: '100%',
224
+ } as ViewStyle,
225
+ textInput: {
226
+ width: '100%',
227
+ flex: 1,
228
+ textAlignVertical: 'center',
229
+ } as TextStyle,
294
230
  container: {
295
231
  flexDirection: 'column',
296
232
  } as ViewStyle,
@@ -0,0 +1,95 @@
1
+ import React from 'react';
2
+ import {Pressable, StyleSheet, View, ViewStyle} from 'react-native';
3
+ import {useTheme} from '../../core/theme/ThemeProvider';
4
+ import {CommonSizes} from '../../core/theme/commonSizes';
5
+ import {RTLAwareText} from './RTLAwareText';
6
+ import {RTLAwareView} from './RTLAwareView';
7
+
8
+ interface RadioOption {
9
+ label: string;
10
+ value: string;
11
+ }
12
+
13
+ interface RadioGroupProps {
14
+ value?: string;
15
+ options: RadioOption[];
16
+ onChange: (value: string) => void;
17
+ }
18
+
19
+ export function RadioGroup(props: RadioGroupProps): JSX.Element {
20
+ const {value, options, onChange} = props;
21
+ const {theme} = useTheme();
22
+
23
+ return (
24
+ <RTLAwareView style={styles.container}>
25
+ {options.map(option => {
26
+ const isSelected = option.value === value;
27
+ const ringStyle: ViewStyle = {
28
+ width: 20,
29
+ height: 20,
30
+ borderRadius: CommonSizes.borderRadius.full,
31
+ borderWidth: CommonSizes.borderWidth.medium,
32
+ borderColor: isSelected
33
+ ? theme.colors.PlatinateBlue_400
34
+ : theme.colors.grayScale_50,
35
+ alignItems: 'center',
36
+ justifyContent: 'center',
37
+ };
38
+ return (
39
+ <Pressable
40
+ key={option.value}
41
+ onPress={() => onChange(option.value)}
42
+ hitSlop={6}
43
+ accessibilityRole="radio"
44
+ accessibilityState={{selected: isSelected}}
45
+ style={styles.rowPressable}>
46
+ <RTLAwareView style={styles.row}>
47
+ <View style={ringStyle}>
48
+ {isSelected ? (
49
+ <View
50
+ style={[
51
+ styles.dot,
52
+ {backgroundColor: theme.colors.PlatinateBlue_400},
53
+ ]}
54
+ />
55
+ ) : null}
56
+ </View>
57
+ <RTLAwareText
58
+ style={[
59
+ theme.text.bodyLargeRegular,
60
+ styles.label,
61
+ {color: theme.colors.grayScale_700},
62
+ ]}>
63
+ {option.label}
64
+ </RTLAwareText>
65
+ </RTLAwareView>
66
+ </Pressable>
67
+ );
68
+ })}
69
+ </RTLAwareView>
70
+ );
71
+ }
72
+
73
+ const styles = StyleSheet.create({
74
+ container: {
75
+ width: '100%',
76
+ flexDirection: 'column',
77
+ gap: CommonSizes.spacing.large,
78
+ } as ViewStyle,
79
+ rowPressable: {
80
+ paddingVertical: CommonSizes.spacing.small,
81
+ },
82
+ row: {
83
+ flexDirection: 'row',
84
+ alignItems: 'center',
85
+ } as ViewStyle,
86
+ dot: {
87
+ width: 10,
88
+ height: 10,
89
+ borderRadius: 5,
90
+ },
91
+ label: {
92
+ marginStart: CommonSizes.spacing.large,
93
+ flexShrink: 1,
94
+ },
95
+ });
@@ -21,10 +21,11 @@ export const SafeText: React.FC<SafeTextProps> = ({children, ...props}) => {
21
21
 
22
22
  // If it's a React element, process its children
23
23
  if (React.isValidElement(node)) {
24
+ const element = node as React.ReactElement<{children?: React.ReactNode}>;
24
25
  return React.cloneElement(
25
- node,
26
- node.props,
27
- React.Children.map(node.props.children, processChildren),
26
+ element,
27
+ element.props,
28
+ React.Children.map(element.props.children, processChildren),
28
29
  );
29
30
  }
30
31
 
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import {
3
3
  StyleSheet,
4
4
  TextInput,
5
- View,
6
5
  ViewStyle,
7
6
  TextStyle,
8
7
  TouchableOpacity,
@@ -11,7 +10,6 @@ import {
11
10
  KeyboardTypeOptions,
12
11
  TextInputProps,
13
12
  I18nManager,
14
- NativeModules,
15
13
  } from 'react-native';
16
14
  import {useTheme} from '../../core/theme/ThemeProvider';
17
15
  import {
@@ -65,6 +63,10 @@ export const SearchBar: React.FC<SearchBarProps> = ({
65
63
  borderColor: theme.colors.mutedLavender30,
66
64
  };
67
65
 
66
+ const textAlignStyle: TextStyle = {
67
+ textAlign: isRTL ? 'right' : 'left',
68
+ };
69
+
68
70
  // Set keyboard language specific properties
69
71
  const getKeyboardProps = (): Partial<TextInputProps> => {
70
72
  if (Platform.OS === 'ios') {
@@ -104,6 +106,8 @@ export const SearchBar: React.FC<SearchBarProps> = ({
104
106
  }
105
107
  }, 100);
106
108
  }
109
+ // Only re-run when language or value changes; inputRef is a stable ref.
110
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
111
  }, [currentLanguage, value]);
108
112
 
109
113
  return (
@@ -129,9 +133,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
129
133
  style={[
130
134
  styles.input,
131
135
  theme.text.SearchBar,
132
- {
133
- textAlign: isRTL ? 'right' : 'left',
134
- },
136
+ textAlignStyle,
135
137
  inputStyle as TextStyle,
136
138
  ]}
137
139
  placeholder={placeholder || t('search', 'common')}
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import {Pressable, StyleSheet, View} from 'react-native';
3
+ import {CommonSizes} from '../../core/theme/commonSizes';
4
+ import {useTheme} from '../../core/theme/ThemeProvider';
5
+ import {RTLAwareText} from './RTLAwareText';
6
+
7
+ interface SegmentedControlProps {
8
+ segments: string[];
9
+ index: number;
10
+ onChange: (index: number) => void;
11
+ }
12
+
13
+ /**
14
+ * A rounded, pill-shaped segmented control. The active segment fills with
15
+ * primary blue + white label; inactive labels are muted. Segments share equal
16
+ * width via flex.
17
+ */
18
+ export function SegmentedControl({
19
+ segments,
20
+ index,
21
+ onChange,
22
+ }: SegmentedControlProps): JSX.Element {
23
+ const {theme} = useTheme();
24
+
25
+ return (
26
+ <View
27
+ style={[
28
+ styles.container,
29
+ {
30
+ backgroundColor: theme.colors.grayScale_0,
31
+ borderColor: theme.colors.grayScale_50,
32
+ borderWidth: CommonSizes.borderWidth.small,
33
+ },
34
+ ]}>
35
+ {segments.map((segment, segmentIndex) => {
36
+ const isActive = segmentIndex === index;
37
+ const segLabel = {
38
+ textAlign: 'center' as const,
39
+ color: isActive ? '#FFFFFF' : theme.colors.grayScale_200,
40
+ };
41
+ return (
42
+ <Pressable
43
+ key={`segment-${segmentIndex}`}
44
+ onPress={() => onChange(segmentIndex)}
45
+ style={[
46
+ styles.segment,
47
+ isActive
48
+ ? {backgroundColor: theme.colors.PlatinateBlue_400}
49
+ : null,
50
+ ]}>
51
+ <RTLAwareText
52
+ numberOfLines={1}
53
+ style={[theme.text.bodyMediumBold, segLabel]}>
54
+ {segment}
55
+ </RTLAwareText>
56
+ </Pressable>
57
+ );
58
+ })}
59
+ </View>
60
+ );
61
+ }
62
+
63
+ const styles = StyleSheet.create({
64
+ container: {
65
+ flexDirection: 'row',
66
+ alignItems: 'center',
67
+ borderRadius: CommonSizes.borderRadius.full,
68
+ padding: 4,
69
+ },
70
+ segment: {
71
+ flex: 1,
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ paddingVertical: CommonSizes.spacing.medium,
75
+ borderRadius: CommonSizes.borderRadius.full,
76
+ },
77
+ });
@@ -0,0 +1,47 @@
1
+ import React, {useEffect, useRef} from 'react';
2
+ import {Animated, DimensionValue, ViewStyle} from 'react-native';
3
+ import {useTheme} from '../../core/theme/ThemeProvider';
4
+ import {CommonSizes} from '../../core/theme/commonSizes';
5
+
6
+ interface SkeletonProps {
7
+ width?: number | string;
8
+ height?: number;
9
+ radius?: number;
10
+ }
11
+
12
+ /**
13
+ * A lightweight shimmer placeholder. Loops its opacity between 0.4 and 1 to
14
+ * signal loading content. Built on Animated only — no extra dependencies.
15
+ */
16
+ export function Skeleton({width, height, radius}: SkeletonProps): JSX.Element {
17
+ const {theme} = useTheme();
18
+ const opacity = useRef(new Animated.Value(0.4)).current;
19
+
20
+ useEffect(() => {
21
+ const animation = Animated.loop(
22
+ Animated.sequence([
23
+ Animated.timing(opacity, {
24
+ toValue: 1,
25
+ duration: 700,
26
+ useNativeDriver: true,
27
+ }),
28
+ Animated.timing(opacity, {
29
+ toValue: 0.4,
30
+ duration: 700,
31
+ useNativeDriver: true,
32
+ }),
33
+ ]),
34
+ );
35
+ animation.start();
36
+ return () => animation.stop();
37
+ }, [opacity]);
38
+
39
+ const blockStyle: ViewStyle = {
40
+ width: (width ?? '100%') as DimensionValue,
41
+ height: height ?? 16,
42
+ borderRadius: radius ?? CommonSizes.borderRadius.medium,
43
+ backgroundColor: theme.colors.grayScale_50,
44
+ };
45
+
46
+ return <Animated.View style={[blockStyle, {opacity}]} />;
47
+ }
@@ -19,8 +19,7 @@ interface IProps {
19
19
  export const TryAgain: FC<IProps> = memo(
20
20
  ({onPress, errorText = localization.errors.unknownErrorHasOccurred}) => {
21
21
  return (
22
- <View
23
- style={{...CommonStyles.flexCenter, backgroundColor: 'transparent'}}>
22
+ <View style={[CommonStyles.flexCenter, styles.container]}>
24
23
  <Text style={styles.title}>{errorText}</Text>
25
24
  {onPress != null && (
26
25
  <TouchableOpacity onPressIn={onPress}>
@@ -35,6 +34,9 @@ export const TryAgain: FC<IProps> = memo(
35
34
  );
36
35
 
37
36
  const styles = StyleSheet.create({
37
+ container: {
38
+ backgroundColor: 'transparent',
39
+ },
38
40
  title: {
39
41
  ...CommonStyles.normalText,
40
42
  textAlign: 'center',
@@ -4,7 +4,7 @@ export function getItemIndex<T, V>(
4
4
  comparisonValue: V,
5
5
  ) {
6
6
  const index = data.findIndex(
7
- item => item[comparisonParam] == comparisonValue,
7
+ item => item[comparisonParam] === comparisonValue,
8
8
  );
9
9
 
10
10
  const itemExists = index > -1;
@@ -17,7 +17,7 @@ export function filterBySearch<T>(
17
17
  param: keyof T,
18
18
  searchText: string,
19
19
  ): T[] {
20
- if (searchText != '') {
20
+ if (searchText !== '') {
21
21
  const lowerCaseSearch = searchText.toLowerCase();
22
22
 
23
23
  return data.filter(item =>
@@ -1,5 +1,5 @@
1
1
  export function defaultKeyIdExtractor<T extends {id: string | number}>(
2
2
  item: T,
3
3
  ): string {
4
- return item?._id;
4
+ return ((item as any)?._id ?? item?.id ?? '').toString();
5
5
  }
@@ -1,7 +1,6 @@
1
1
  export function isEmail(email: string): boolean {
2
2
  const emailRegex =
3
- // eslint-disable-next-line max-len
4
- /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
3
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
5
4
 
6
5
  return emailRegex.test(email);
7
6
  }
@@ -8,7 +8,6 @@ export function removeHtmlTags(text: string): string {
8
8
 
9
9
  export function removeEmojis(text: string): string {
10
10
  return text.replace(
11
- // eslint-disable-next-line max-len
12
11
  /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g,
13
12
  '',
14
13
  );
@@ -3,8 +3,11 @@ import {BackHandler} from 'react-native';
3
3
 
4
4
  export function useBackHandler(handler: () => boolean, deps?: DependencyList) {
5
5
  useEffect(() => {
6
- BackHandler.addEventListener('hardwareBackPress', handler);
6
+ const subscription = BackHandler.addEventListener(
7
+ 'hardwareBackPress',
8
+ handler,
9
+ );
7
10
 
8
- return () => BackHandler.removeEventListener('hardwareBackPress', handler);
11
+ return () => subscription.remove();
9
12
  }, [deps, handler]);
10
13
  }