@fadyshawky/react-native-magic 2.2.0 → 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.
- package/LICENSE +21 -0
- package/README.md +90 -55
- package/index.js +4 -0
- package/package.json +9 -3
- package/scripts/askPackageName.js +10 -5
- package/template/.env.development +8 -6
- package/template/.env.example +15 -5
- package/template/.env.production +8 -6
- package/template/.env.staging +8 -6
- package/template/.eslintrc.js +14 -0
- package/template/.husky/pre-commit +1 -0
- package/template/App.tsx +47 -16
- package/template/__tests__/App.test.tsx +28 -10
- package/template/babel.config.js +20 -1
- package/template/docs/ARCHITECTURE.md +40 -10
- package/template/docs/BEST_PRACTICES.md +10 -1
- package/template/docs/CUSTOMIZATION.md +118 -5
- package/template/docs/design-system.html +1164 -0
- package/template/docs/wireframes.html +411 -0
- package/template/index.js +10 -0
- package/template/jest.config.js +16 -1
- package/template/jest.setup.js +61 -0
- package/template/package-lock.json +12178 -8293
- package/template/package.json +53 -19
- package/template/react-native.config.js +3 -0
- package/template/resources/fonts/.gitkeep +0 -0
- package/template/scripts/ci-sync-env.cjs +71 -0
- package/template/src/assets/brand/logo-mark.svg +8 -0
- package/template/src/assets/brand/logo-mono.svg +9 -0
- package/template/src/assets/brand/logo-primary.svg +15 -0
- package/template/src/assets/brand/wordmark-dark.svg +18 -0
- package/template/src/common/components/AppBottomSheet.tsx +87 -0
- package/template/src/common/components/AppSwitch.tsx +75 -0
- package/template/src/common/components/AppTextInput.tsx +161 -0
- package/template/src/common/components/Avatar.tsx +75 -0
- package/template/src/common/components/Badge.tsx +66 -0
- package/template/src/common/components/CardScroller.tsx +58 -0
- package/template/src/common/components/Cards.tsx +13 -7
- package/template/src/common/components/Carousel.tsx +196 -0
- package/template/src/common/components/Checkbox.tsx +85 -0
- package/template/src/common/components/Chip.tsx +55 -0
- package/template/src/common/components/Dropdown.tsx +202 -0
- package/template/src/common/components/ErrorBoundary.tsx +82 -0
- package/template/src/common/components/FlatListWrapper.tsx +8 -8
- package/template/src/common/components/ListItem.tsx +90 -0
- package/template/src/common/components/LoadingComponent.tsx +8 -2
- package/template/src/common/components/Logo.tsx +77 -0
- package/template/src/common/components/ModalDialog.tsx +141 -0
- package/template/src/common/components/NetworkBanner.tsx +47 -0
- package/template/src/common/components/OTPInput.tsx +0 -1
- package/template/src/common/components/PrimaryButton.tsx +0 -14
- package/template/src/common/components/PrimaryTextInput.tsx +66 -130
- package/template/src/common/components/RadioGroup.tsx +95 -0
- package/template/src/common/components/SafeText.tsx +4 -3
- package/template/src/common/components/SearchBar.tsx +7 -5
- package/template/src/common/components/SegmentedControl.tsx +77 -0
- package/template/src/common/components/Skeleton.tsx +47 -0
- package/template/src/common/components/TryAgain.tsx +4 -2
- package/template/src/common/helpers/arrayHelpers.ts +2 -2
- package/template/src/common/helpers/defaultKeyIdExtractor.ts +1 -1
- package/template/src/common/helpers/regexHelpers.ts +1 -2
- package/template/src/common/helpers/stringsHelpers.ts +0 -1
- package/template/src/common/hooks/useBackHandler.ts +5 -2
- package/template/src/common/hooks/useEventRegister.ts +1 -1
- package/template/src/common/hooks/useFlatListActions.ts +1 -1
- package/template/src/common/hooks/useWhyDidYouUpdate.ts +1 -1
- package/template/src/common/localization/LocalizationProvider.tsx +1 -1
- package/template/src/common/localization/RTLInitializer.tsx +1 -1
- package/template/src/common/localization/dateFormatter.ts +0 -1
- package/template/src/common/localization/intlFormatter.ts +0 -1
- package/template/src/common/localization/localization.ts +2 -2
- package/template/src/common/localization/translations/homeLocalization.ts +14 -0
- package/template/src/common/localization/translations/loginLocalization.ts +8 -0
- package/template/src/common/localization/translations/mainNavigationLocalization.ts +2 -0
- package/template/src/common/localization/translations/profileLocalization.ts +16 -0
- package/template/src/common/utils/index.tsx +0 -6
- package/template/src/common/validations/commonValidations.ts +2 -2
- package/template/src/core/api/errorHandler.ts +1 -1
- package/template/src/core/api/responseHandlers.ts +1 -3
- package/template/src/core/api/serverHeaders.ts +61 -12
- package/template/src/core/notifications/notificationAuth.ts +6 -0
- package/template/src/core/notifications/notificationService.ts +125 -0
- package/template/src/core/notifications/routeFromNotificationData.ts +32 -0
- package/template/src/core/store/categories/categoriesActions.ts +25 -0
- package/template/src/core/store/categories/categoriesSlice.ts +51 -0
- package/template/src/core/store/categories/categoriesState.ts +19 -0
- package/template/src/core/store/rootReducer.ts +2 -0
- package/template/src/core/store/store.tsx +6 -1
- package/template/src/core/store/user/userActions.ts +75 -14
- package/template/src/core/store/user/userSlice.ts +49 -26
- package/template/src/core/store/user/userState.ts +6 -4
- package/template/src/core/theme/ThemeProvider.tsx +5 -3
- package/template/src/core/theme/brand.ts +50 -0
- package/template/src/core/theme/colors.ts +113 -99
- package/template/src/core/theme/commonConsts.ts +2 -2
- package/template/src/core/theme/commonStyles.ts +1 -1
- package/template/src/core/theme/themes.ts +2 -0
- package/template/src/core/theme/types.ts +4 -2
- package/template/src/core/utils/stringUtils.ts +1 -1
- package/template/src/design-system/index.ts +2 -0
- package/template/src/design-system/tokens/brand.ts +6 -0
- package/template/src/design-system/tokens/index.ts +3 -0
- package/template/src/design-system/tokens/palette.ts +4 -0
- package/template/src/design-system/tokens/typography-spacing.ts +2 -0
- package/template/src/navigation/AuthStack.tsx +1 -4
- package/template/src/navigation/HeaderComponents.tsx +6 -3
- package/template/src/navigation/MainStack.tsx +18 -6
- package/template/src/navigation/RootNavigation.tsx +4 -7
- package/template/src/navigation/TabBar.tsx +7 -6
- package/template/src/navigation/types.ts +10 -31
- package/template/src/screens/Login/Login.tsx +47 -47
- package/template/src/screens/OTP/OTPScreen.tsx +6 -9
- package/template/src/screens/components/ComponentsScreen.tsx +301 -0
- package/template/src/screens/home/HomeScreen.tsx +143 -1
- package/template/src/screens/home/hooks/useHomeData.ts +19 -5
- package/template/src/screens/index.tsx +1 -0
- package/template/src/screens/profile/Profile.tsx +139 -2
- package/template/src/screens/splash/Splash.tsx +44 -11
- package/template/src/sheetManager/sheets.tsx +1 -1
- package/template/tsconfig.json +14 -2
- package/template/types/globals.d.ts +43 -0
- package/template/types/index.ts +2 -6
- package/template/types/modules.d.ts +9 -0
- package/template/types/react-native-config.d.ts +0 -2
- package/.vscode/settings.json +0 -8
- package/CHANGELOG.md +0 -119
- package/CODE_OF_CONDUCT.md +0 -83
- package/CONTRIBUTING.md +0 -60
- package/local.properties +0 -1
- package/template/src/common/components/ImageCropPickerButton.tsx +0 -107
- package/template/src/common/components/PhotoTakingButton.tsx +0 -94
- package/template/src/common/helpers/imageHelpers.ts +0 -5
- package/template/src/common/helpers/inAppReviewHelper.ts +0 -30
- package/template/src/common/helpers/orientationHelpers.ts +0 -25
- package/template/src/common/helpers/shareHelpers.ts +0 -47
- package/template/src/common/utils/FeesCaalculation.tsx +0 -37
- package/template/src/common/utils/printData.tsx +0 -161
- package/template/src/common/validations/examples/TextInputWithValidation.tsx +0 -229
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import NetInfo, {NetInfoState} from '@react-native-community/netinfo';
|
|
2
|
+
import React, {useEffect, useState} from 'react';
|
|
3
|
+
import {StyleSheet, Text, View} from 'react-native';
|
|
4
|
+
import {Fonts} from '../../core/theme/fonts';
|
|
5
|
+
import {CommonSizes} from '../../core/theme/commonSizes';
|
|
6
|
+
|
|
7
|
+
export const NetworkBanner = () => {
|
|
8
|
+
const [offline, setOffline] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const unsubscribe = NetInfo.addEventListener((state: NetInfoState) => {
|
|
12
|
+
const connected = !!state.isConnected && state.isInternetReachable !== false;
|
|
13
|
+
setOffline(!connected);
|
|
14
|
+
});
|
|
15
|
+
NetInfo.fetch().then(state => {
|
|
16
|
+
const connected = !!state.isConnected && state.isInternetReachable !== false;
|
|
17
|
+
setOffline(!connected);
|
|
18
|
+
});
|
|
19
|
+
return unsubscribe;
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
if (!offline) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View style={styles.banner} pointerEvents="none">
|
|
26
|
+
<Text style={styles.text}>No internet connection</Text>
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create({
|
|
32
|
+
banner: {
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
bottom: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
backgroundColor: '#D32F2F',
|
|
38
|
+
paddingVertical: 8,
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
zIndex: 9999,
|
|
41
|
+
},
|
|
42
|
+
text: {
|
|
43
|
+
color: '#fff',
|
|
44
|
+
fontFamily: Fonts.bold,
|
|
45
|
+
fontSize: CommonSizes.font.bodySmall,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
React.Children.map(
|
|
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',
|