@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.
- package/LICENSE +21 -0
- package/README.md +90 -56
- package/index.js +4 -0
- package/package.json +8 -2
- 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 -20
- 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 +4 -5
- 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,202 @@
|
|
|
1
|
+
import React, {useMemo, useState} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
Pressable,
|
|
5
|
+
ScrollView,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import Svg, {Path} from 'react-native-svg';
|
|
10
|
+
import {useTheme} from '../../core/theme/ThemeProvider';
|
|
11
|
+
import {CommonSizes} from '../../core/theme/commonSizes';
|
|
12
|
+
import {RTLAwareText} from './RTLAwareText';
|
|
13
|
+
import {RTLAwareView} from './RTLAwareView';
|
|
14
|
+
|
|
15
|
+
interface DropdownOption {
|
|
16
|
+
label: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DropdownProps {
|
|
21
|
+
label?: string;
|
|
22
|
+
value?: string;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
options: DropdownOption[];
|
|
25
|
+
onSelect: (value: string) => void;
|
|
26
|
+
error?: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ChevronDown({color}: {color: string}): JSX.Element {
|
|
30
|
+
return (
|
|
31
|
+
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
|
|
32
|
+
<Path
|
|
33
|
+
d="M6 9L12 15L18 9"
|
|
34
|
+
stroke={color}
|
|
35
|
+
strokeWidth={2}
|
|
36
|
+
strokeLinecap="round"
|
|
37
|
+
strokeLinejoin="round"
|
|
38
|
+
/>
|
|
39
|
+
</Svg>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function CheckIcon({color}: {color: string}): JSX.Element {
|
|
44
|
+
return (
|
|
45
|
+
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
|
|
46
|
+
<Path
|
|
47
|
+
d="M5 12.5L10 17.5L19 7"
|
|
48
|
+
stroke={color}
|
|
49
|
+
strokeWidth={2.2}
|
|
50
|
+
strokeLinecap="round"
|
|
51
|
+
strokeLinejoin="round"
|
|
52
|
+
/>
|
|
53
|
+
</Svg>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function Dropdown(props: DropdownProps): JSX.Element {
|
|
58
|
+
const {label, value, placeholder, options, onSelect, error} = props;
|
|
59
|
+
const {theme} = useTheme();
|
|
60
|
+
const [open, setOpen] = useState(false);
|
|
61
|
+
|
|
62
|
+
const selected = useMemo(
|
|
63
|
+
() => options.find(option => option.value === value),
|
|
64
|
+
[options, value],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const valueColorStyle = {
|
|
68
|
+
color: selected
|
|
69
|
+
? theme.colors.grayScale_700
|
|
70
|
+
: theme.colors.grayScale_200,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const fieldStyle: ViewStyle = {
|
|
74
|
+
flexDirection: 'row',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
justifyContent: 'space-between',
|
|
77
|
+
backgroundColor: theme.colors.grayScale_0,
|
|
78
|
+
borderColor: error ? theme.colors.error_400 : theme.colors.grayScale_50,
|
|
79
|
+
borderWidth: CommonSizes.borderWidth.medium,
|
|
80
|
+
borderRadius: CommonSizes.borderRadius.large,
|
|
81
|
+
paddingHorizontal: CommonSizes.spacing.xLarge,
|
|
82
|
+
paddingVertical: CommonSizes.spacing.xLarge,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<RTLAwareView style={styles.container}>
|
|
87
|
+
{label ? (
|
|
88
|
+
<RTLAwareText
|
|
89
|
+
style={[theme.text.bodyMediumBold, {color: theme.colors.grayScale_700}]}>
|
|
90
|
+
{label}
|
|
91
|
+
</RTLAwareText>
|
|
92
|
+
) : null}
|
|
93
|
+
|
|
94
|
+
<Pressable
|
|
95
|
+
onPress={() => setOpen(true)}
|
|
96
|
+
accessibilityRole="button"
|
|
97
|
+
accessibilityLabel={label ?? placeholder ?? 'Dropdown'}>
|
|
98
|
+
<RTLAwareView style={fieldStyle}>
|
|
99
|
+
<RTLAwareText
|
|
100
|
+
style={[
|
|
101
|
+
theme.text.bodyLargeRegular,
|
|
102
|
+
styles.flex1,
|
|
103
|
+
valueColorStyle,
|
|
104
|
+
]}>
|
|
105
|
+
{selected ? selected.label : placeholder ?? ''}
|
|
106
|
+
</RTLAwareText>
|
|
107
|
+
<ChevronDown color={theme.colors.grayScale_200} />
|
|
108
|
+
</RTLAwareView>
|
|
109
|
+
</Pressable>
|
|
110
|
+
|
|
111
|
+
{error ? (
|
|
112
|
+
<RTLAwareText
|
|
113
|
+
style={[theme.text.bodySmallRegular, {color: theme.colors.error_400}]}>
|
|
114
|
+
{error}
|
|
115
|
+
</RTLAwareText>
|
|
116
|
+
) : null}
|
|
117
|
+
|
|
118
|
+
<Modal transparent visible={open} animationType="fade">
|
|
119
|
+
<Pressable
|
|
120
|
+
style={styles.backdrop}
|
|
121
|
+
onPress={() => setOpen(false)}
|
|
122
|
+
accessibilityRole="button"
|
|
123
|
+
accessibilityLabel="Close dropdown">
|
|
124
|
+
<Pressable
|
|
125
|
+
style={[
|
|
126
|
+
styles.card,
|
|
127
|
+
{backgroundColor: theme.colors.grayScale_0},
|
|
128
|
+
]}
|
|
129
|
+
onPress={() => {}}>
|
|
130
|
+
<ScrollView bounces={false}>
|
|
131
|
+
{options.map(option => {
|
|
132
|
+
const isSelected = option.value === value;
|
|
133
|
+
const optionColorStyle = {
|
|
134
|
+
color: isSelected
|
|
135
|
+
? theme.colors.PlatinateBlue_400
|
|
136
|
+
: theme.colors.grayScale_700,
|
|
137
|
+
};
|
|
138
|
+
return (
|
|
139
|
+
<Pressable
|
|
140
|
+
key={option.value}
|
|
141
|
+
onPress={() => {
|
|
142
|
+
onSelect(option.value);
|
|
143
|
+
setOpen(false);
|
|
144
|
+
}}
|
|
145
|
+
accessibilityRole="button"
|
|
146
|
+
style={styles.optionRow}>
|
|
147
|
+
<RTLAwareView style={styles.optionInner}>
|
|
148
|
+
<RTLAwareText
|
|
149
|
+
style={[
|
|
150
|
+
theme.text.bodyLargeRegular,
|
|
151
|
+
styles.flex1,
|
|
152
|
+
optionColorStyle,
|
|
153
|
+
]}>
|
|
154
|
+
{option.label}
|
|
155
|
+
</RTLAwareText>
|
|
156
|
+
{isSelected ? (
|
|
157
|
+
<CheckIcon color={theme.colors.PlatinateBlue_400} />
|
|
158
|
+
) : null}
|
|
159
|
+
</RTLAwareView>
|
|
160
|
+
</Pressable>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</ScrollView>
|
|
164
|
+
</Pressable>
|
|
165
|
+
</Pressable>
|
|
166
|
+
</Modal>
|
|
167
|
+
</RTLAwareView>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const styles = StyleSheet.create({
|
|
172
|
+
container: {
|
|
173
|
+
width: '100%',
|
|
174
|
+
flexDirection: 'column',
|
|
175
|
+
gap: CommonSizes.spacing.medium,
|
|
176
|
+
} as ViewStyle,
|
|
177
|
+
backdrop: {
|
|
178
|
+
flex: 1,
|
|
179
|
+
backgroundColor: 'rgba(6, 8, 15, 0.55)',
|
|
180
|
+
alignItems: 'center',
|
|
181
|
+
justifyContent: 'center',
|
|
182
|
+
padding: CommonSizes.spacing.xxLarge,
|
|
183
|
+
},
|
|
184
|
+
card: {
|
|
185
|
+
width: '100%',
|
|
186
|
+
maxHeight: '60%',
|
|
187
|
+
borderRadius: CommonSizes.borderRadius.large,
|
|
188
|
+
overflow: 'hidden',
|
|
189
|
+
paddingVertical: CommonSizes.spacing.small,
|
|
190
|
+
},
|
|
191
|
+
optionRow: {
|
|
192
|
+
paddingHorizontal: CommonSizes.spacing.xLarge,
|
|
193
|
+
paddingVertical: CommonSizes.spacing.xLarge,
|
|
194
|
+
},
|
|
195
|
+
optionInner: {
|
|
196
|
+
flexDirection: 'row',
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
},
|
|
199
|
+
flex1: {
|
|
200
|
+
flex: 1,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
|
3
|
+
import {NaturalColors, PrimaryColors} from '../../core/theme/colors';
|
|
4
|
+
import {Fonts} from '../../core/theme/fonts';
|
|
5
|
+
import {CommonSizes} from '../../core/theme/commonSizes';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
fallback?: (reset: () => void, error: Error) => React.ReactNode;
|
|
10
|
+
onError?: (error: Error, info: React.ErrorInfo) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface State {
|
|
14
|
+
error: Error | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ErrorBoundary extends React.Component<Props, State> {
|
|
18
|
+
state: State = {error: null};
|
|
19
|
+
|
|
20
|
+
static getDerivedStateFromError(error: Error): State {
|
|
21
|
+
return {error};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
25
|
+
if (__DEV__) {
|
|
26
|
+
console.error('[ErrorBoundary]', error, info.componentStack);
|
|
27
|
+
}
|
|
28
|
+
this.props.onError?.(error, info);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
reset = () => this.setState({error: null});
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
if (!this.state.error) return this.props.children;
|
|
35
|
+
if (this.props.fallback) {
|
|
36
|
+
return this.props.fallback(this.reset, this.state.error);
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
<View style={styles.root}>
|
|
40
|
+
<Text style={styles.title}>Something went wrong</Text>
|
|
41
|
+
<Text style={styles.message} numberOfLines={6}>
|
|
42
|
+
{this.state.error.message}
|
|
43
|
+
</Text>
|
|
44
|
+
<TouchableOpacity style={styles.button} onPress={this.reset}>
|
|
45
|
+
<Text style={styles.buttonText}>Try again</Text>
|
|
46
|
+
</TouchableOpacity>
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
root: {
|
|
54
|
+
flex: 1,
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
padding: 24,
|
|
58
|
+
backgroundColor: NaturalColors.background_2,
|
|
59
|
+
},
|
|
60
|
+
title: {
|
|
61
|
+
fontFamily: Fonts.bold,
|
|
62
|
+
fontSize: CommonSizes.font.bodyXLarge,
|
|
63
|
+
marginBottom: 12,
|
|
64
|
+
},
|
|
65
|
+
message: {
|
|
66
|
+
fontFamily: Fonts.regular,
|
|
67
|
+
fontSize: CommonSizes.font.bodyMedium,
|
|
68
|
+
textAlign: 'center',
|
|
69
|
+
marginBottom: 24,
|
|
70
|
+
},
|
|
71
|
+
button: {
|
|
72
|
+
backgroundColor: PrimaryColors.PlatinateBlue_400,
|
|
73
|
+
paddingVertical: 12,
|
|
74
|
+
paddingHorizontal: 24,
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
},
|
|
77
|
+
buttonText: {
|
|
78
|
+
color: '#fff',
|
|
79
|
+
fontFamily: Fonts.bold,
|
|
80
|
+
fontSize: CommonSizes.font.bodyMedium,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {useMemo} from 'react';
|
|
2
2
|
import {FlashList, FlashListProps} from '@shopify/flash-list';
|
|
3
3
|
import {LoadState} from '../../../types';
|
|
4
4
|
import {TryAgain} from './TryAgain';
|
|
@@ -18,7 +18,6 @@ interface IProps extends FlashListProps<any> {
|
|
|
18
18
|
// Shared FlashList defaults. Tune estimatedItemSize per-screen for long/heavy lists.
|
|
19
19
|
const FlatListWrapperProps = {
|
|
20
20
|
keyExtractor: defaultKeyIdExtractor,
|
|
21
|
-
estimatedItemSize: 72,
|
|
22
21
|
ListEmptyComponent: (
|
|
23
22
|
<EmptyView
|
|
24
23
|
title={localization.empty.noData}
|
|
@@ -36,7 +35,7 @@ export function FlatListWrapper({
|
|
|
36
35
|
...props
|
|
37
36
|
}: IProps) {
|
|
38
37
|
const ListEmptyComponent = useMemo(() => {
|
|
39
|
-
if (loadState
|
|
38
|
+
if (loadState === LoadState.error) {
|
|
40
39
|
return (
|
|
41
40
|
<TryAgain
|
|
42
41
|
onPress={tryAgain}
|
|
@@ -49,10 +48,10 @@ export function FlatListWrapper({
|
|
|
49
48
|
}, [loadState, props.ListEmptyComponent, error, tryAgain]);
|
|
50
49
|
|
|
51
50
|
const refreshing = useMemo(() => {
|
|
52
|
-
return loadState
|
|
51
|
+
return loadState === LoadState.pullToRefresh;
|
|
53
52
|
}, [loadState]);
|
|
54
53
|
|
|
55
|
-
if (loadState
|
|
54
|
+
if (loadState === LoadState.firstLoad) {
|
|
56
55
|
return <LoadingComponent />;
|
|
57
56
|
} else {
|
|
58
57
|
return (
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Pressable, StyleSheet, View} from 'react-native';
|
|
3
|
+
import Svg, {Path} from 'react-native-svg';
|
|
4
|
+
import {CommonSizes} from '../../core/theme/commonSizes';
|
|
5
|
+
import {useTheme} from '../../core/theme/ThemeProvider';
|
|
6
|
+
import {RTLAwareText} from './RTLAwareText';
|
|
7
|
+
import {RTLAwareView} from './RTLAwareView';
|
|
8
|
+
|
|
9
|
+
interface ListItemProps {
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
left?: React.ReactNode;
|
|
13
|
+
right?: React.ReactNode;
|
|
14
|
+
onPress?: () => void;
|
|
15
|
+
showChevron?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A standard list row: an optional `left` slot, a title + optional muted
|
|
20
|
+
* subtitle, then a `right` slot or a trailing chevron. Becomes tappable when
|
|
21
|
+
* `onPress` is supplied.
|
|
22
|
+
*/
|
|
23
|
+
export function ListItem({
|
|
24
|
+
title,
|
|
25
|
+
subtitle,
|
|
26
|
+
left,
|
|
27
|
+
right,
|
|
28
|
+
onPress,
|
|
29
|
+
showChevron,
|
|
30
|
+
}: ListItemProps): JSX.Element {
|
|
31
|
+
const {theme} = useTheme();
|
|
32
|
+
|
|
33
|
+
const trailing =
|
|
34
|
+
right != null ? (
|
|
35
|
+
right
|
|
36
|
+
) : showChevron ? (
|
|
37
|
+
<Chevron color={theme.colors.grayScale_200} />
|
|
38
|
+
) : null;
|
|
39
|
+
|
|
40
|
+
const content = (
|
|
41
|
+
<RTLAwareView style={styles.row}>
|
|
42
|
+
{left != null ? <View>{left}</View> : null}
|
|
43
|
+
<View style={styles.textColumn}>
|
|
44
|
+
<RTLAwareText style={theme.text.bodyLargeBold}>{title}</RTLAwareText>
|
|
45
|
+
{subtitle != null ? (
|
|
46
|
+
<RTLAwareText
|
|
47
|
+
style={[
|
|
48
|
+
theme.text.bodySmallRegular,
|
|
49
|
+
{color: theme.colors.grayScale_200},
|
|
50
|
+
]}>
|
|
51
|
+
{subtitle}
|
|
52
|
+
</RTLAwareText>
|
|
53
|
+
) : null}
|
|
54
|
+
</View>
|
|
55
|
+
{trailing != null ? <View>{trailing}</View> : null}
|
|
56
|
+
</RTLAwareView>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (onPress) {
|
|
60
|
+
return <Pressable onPress={onPress}>{content}</Pressable>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return content;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Chevron({color}: {color: string}): JSX.Element {
|
|
67
|
+
return (
|
|
68
|
+
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
|
|
69
|
+
<Path
|
|
70
|
+
d="M9 6l6 6-6 6"
|
|
71
|
+
stroke={color}
|
|
72
|
+
strokeWidth={2}
|
|
73
|
+
strokeLinecap="round"
|
|
74
|
+
strokeLinejoin="round"
|
|
75
|
+
/>
|
|
76
|
+
</Svg>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const styles = StyleSheet.create({
|
|
81
|
+
row: {
|
|
82
|
+
flexDirection: 'row',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
gap: CommonSizes.spacing.large,
|
|
85
|
+
paddingVertical: CommonSizes.spacing.large,
|
|
86
|
+
},
|
|
87
|
+
textColumn: {
|
|
88
|
+
flex: 1,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {Image, View} from 'react-native';
|
|
2
|
+
import {Image, StyleSheet, View} from 'react-native';
|
|
3
3
|
import {CommonStyles} from '../../core/theme/commonStyles';
|
|
4
4
|
|
|
5
5
|
export const LoadingComponent = () => {
|
|
6
6
|
return (
|
|
7
7
|
<View style={CommonStyles.flexCenter}>
|
|
8
|
-
<Image resizeMode="cover" style={
|
|
8
|
+
<Image resizeMode="cover" style={styles.image} source={0} />
|
|
9
9
|
</View>
|
|
10
10
|
);
|
|
11
11
|
};
|
|
12
|
+
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
image: {
|
|
15
|
+
flex: 1,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Svg, {Defs, LinearGradient, Path, Rect, Stop} from 'react-native-svg';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fady Shawky "FS" brand mark — a bold filled F beside a clean S, in a rounded
|
|
6
|
+
* chip. Premium "Midnight" black + blue treatment. Pure geometry (filled rects
|
|
7
|
+
* for the F, one stroked path for the S), so it renders identically in
|
|
8
|
+
* react-native-svg and plain SVG and stays legible at small sizes.
|
|
9
|
+
*
|
|
10
|
+
* variants:
|
|
11
|
+
* - 'gradient' (default): blue-black chip, blue mark — the primary logo
|
|
12
|
+
* - 'mono': dark chip, white mark — for dark UI
|
|
13
|
+
* - 'light': near-white chip, ink mark — for light UI
|
|
14
|
+
* - 'mark': no chip, blue mark — inline / nav usage
|
|
15
|
+
*/
|
|
16
|
+
export type LogoVariant = 'gradient' | 'mono' | 'light' | 'mark';
|
|
17
|
+
|
|
18
|
+
interface LogoProps {
|
|
19
|
+
size?: number;
|
|
20
|
+
variant?: LogoVariant;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const S_PATH =
|
|
24
|
+
'M101 42 C101 32 91 28 83 28 C73 28 67 35 67 44 C67 52 75 56 84 59 C93 62 101 66 101 76 C101 86 91 91 83 91 C73 91 67 85 66 77';
|
|
25
|
+
|
|
26
|
+
function chipFill(variant: LogoVariant): string | undefined {
|
|
27
|
+
switch (variant) {
|
|
28
|
+
case 'gradient':
|
|
29
|
+
return 'url(#fsChip)';
|
|
30
|
+
case 'mono':
|
|
31
|
+
return '#0D1124';
|
|
32
|
+
case 'light':
|
|
33
|
+
return '#F4F6FE';
|
|
34
|
+
case 'mark':
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function markColor(variant: LogoVariant): string {
|
|
40
|
+
switch (variant) {
|
|
41
|
+
case 'gradient':
|
|
42
|
+
return '#6BA0FF';
|
|
43
|
+
case 'mono':
|
|
44
|
+
return '#FFFFFF';
|
|
45
|
+
case 'light':
|
|
46
|
+
return '#0A1230';
|
|
47
|
+
case 'mark':
|
|
48
|
+
return '#2F6BFF';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function Logo({size = 96, variant = 'gradient'}: LogoProps) {
|
|
53
|
+
const fill = chipFill(variant);
|
|
54
|
+
const c = markColor(variant);
|
|
55
|
+
return (
|
|
56
|
+
<Svg width={size} height={size} viewBox="0 0 120 120" fill="none">
|
|
57
|
+
<Defs>
|
|
58
|
+
<LinearGradient id="fsChip" x1="0" y1="0" x2="1" y2="1">
|
|
59
|
+
<Stop offset="0" stopColor="#0A1230" />
|
|
60
|
+
<Stop offset="1" stopColor="#1B45B8" />
|
|
61
|
+
</LinearGradient>
|
|
62
|
+
</Defs>
|
|
63
|
+
{fill ? <Rect x="0" y="0" width="120" height="120" rx="30" fill={fill} /> : null}
|
|
64
|
+
<Rect x="25" y="24" width="13" height="72" rx="3" fill={c} />
|
|
65
|
+
<Rect x="25" y="24" width="33" height="13" rx="3" fill={c} />
|
|
66
|
+
<Rect x="25" y="52" width="26" height="12" rx="3" fill={c} />
|
|
67
|
+
<Path
|
|
68
|
+
d={S_PATH}
|
|
69
|
+
stroke={c}
|
|
70
|
+
strokeWidth={13}
|
|
71
|
+
strokeLinecap="round"
|
|
72
|
+
strokeLinejoin="round"
|
|
73
|
+
fill="none"
|
|
74
|
+
/>
|
|
75
|
+
</Svg>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Modal, Pressable, StyleSheet, View, ViewStyle} from 'react-native';
|
|
3
|
+
import {ButtonType} from '../../../types';
|
|
4
|
+
import {useTheme} from '../../core/theme/ThemeProvider';
|
|
5
|
+
import {CommonSizes} from '../../core/theme/commonSizes';
|
|
6
|
+
import {PrimaryButton} from './PrimaryButton';
|
|
7
|
+
import {RTLAwareText} from './RTLAwareText';
|
|
8
|
+
|
|
9
|
+
type DialogActionVariant = 'primary' | 'ghost' | 'destructive';
|
|
10
|
+
|
|
11
|
+
interface DialogAction {
|
|
12
|
+
label: string;
|
|
13
|
+
onPress: () => void;
|
|
14
|
+
variant?: DialogActionVariant;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ModalDialogProps {
|
|
18
|
+
visible: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
title?: string;
|
|
21
|
+
message?: string;
|
|
22
|
+
actions?: DialogAction[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buttonTypeForVariant(variant?: DialogActionVariant): ButtonType {
|
|
26
|
+
switch (variant) {
|
|
27
|
+
case 'destructive':
|
|
28
|
+
return ButtonType.outlineNegative;
|
|
29
|
+
case 'ghost':
|
|
30
|
+
return ButtonType.borderless;
|
|
31
|
+
case 'primary':
|
|
32
|
+
default:
|
|
33
|
+
return ButtonType.solid;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A centered alert/confirm dialog built on react-native's Modal.
|
|
39
|
+
* Fades in over a dimmed midnight backdrop; tapping the backdrop closes it.
|
|
40
|
+
* Falls back to a single "OK" button when no actions are supplied.
|
|
41
|
+
*/
|
|
42
|
+
export function ModalDialog({
|
|
43
|
+
visible,
|
|
44
|
+
onClose,
|
|
45
|
+
title,
|
|
46
|
+
message,
|
|
47
|
+
actions,
|
|
48
|
+
}: ModalDialogProps): JSX.Element {
|
|
49
|
+
const {theme} = useTheme();
|
|
50
|
+
|
|
51
|
+
const cardStyle: ViewStyle = {
|
|
52
|
+
backgroundColor: theme.colors.grayScale_0,
|
|
53
|
+
borderRadius: CommonSizes.borderRadius.large,
|
|
54
|
+
padding: CommonSizes.spacing.xLarge,
|
|
55
|
+
maxWidth: 340,
|
|
56
|
+
width: '100%',
|
|
57
|
+
alignSelf: 'center',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Modal
|
|
62
|
+
transparent
|
|
63
|
+
visible={visible}
|
|
64
|
+
animationType="fade"
|
|
65
|
+
onRequestClose={onClose}>
|
|
66
|
+
<View style={styles.container}>
|
|
67
|
+
<Pressable
|
|
68
|
+
style={styles.backdrop}
|
|
69
|
+
onPress={onClose}
|
|
70
|
+
accessibilityRole="button"
|
|
71
|
+
accessibilityLabel="Close"
|
|
72
|
+
/>
|
|
73
|
+
<Pressable style={cardStyle} onPress={event => event.stopPropagation()}>
|
|
74
|
+
{title ? (
|
|
75
|
+
<RTLAwareText style={[theme.text.header4, styles.title]}>
|
|
76
|
+
{title}
|
|
77
|
+
</RTLAwareText>
|
|
78
|
+
) : null}
|
|
79
|
+
{message ? (
|
|
80
|
+
<RTLAwareText
|
|
81
|
+
style={[
|
|
82
|
+
theme.text.bodyMediumRegular,
|
|
83
|
+
styles.message,
|
|
84
|
+
{color: theme.colors.grayScale_200},
|
|
85
|
+
]}>
|
|
86
|
+
{message}
|
|
87
|
+
</RTLAwareText>
|
|
88
|
+
) : null}
|
|
89
|
+
<View style={styles.actions}>
|
|
90
|
+
{actions && actions.length > 0 ? (
|
|
91
|
+
actions.map((action, index) => (
|
|
92
|
+
<View
|
|
93
|
+
key={`${action.label}-${index}`}
|
|
94
|
+
style={
|
|
95
|
+
index > 0 ? styles.actionSpacing : undefined
|
|
96
|
+
}>
|
|
97
|
+
<PrimaryButton
|
|
98
|
+
label={action.label}
|
|
99
|
+
type={buttonTypeForVariant(action.variant)}
|
|
100
|
+
onPress={action.onPress}
|
|
101
|
+
/>
|
|
102
|
+
</View>
|
|
103
|
+
))
|
|
104
|
+
) : (
|
|
105
|
+
<PrimaryButton
|
|
106
|
+
label="OK"
|
|
107
|
+
type={ButtonType.solid}
|
|
108
|
+
onPress={onClose}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
</View>
|
|
112
|
+
</Pressable>
|
|
113
|
+
</View>
|
|
114
|
+
</Modal>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const styles = StyleSheet.create({
|
|
119
|
+
container: {
|
|
120
|
+
flex: 1,
|
|
121
|
+
justifyContent: 'center',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
paddingHorizontal: CommonSizes.spacing.xLarge,
|
|
124
|
+
},
|
|
125
|
+
backdrop: {
|
|
126
|
+
...StyleSheet.absoluteFill,
|
|
127
|
+
backgroundColor: 'rgba(6,8,15,0.6)',
|
|
128
|
+
},
|
|
129
|
+
title: {
|
|
130
|
+
marginBottom: CommonSizes.spacing.medium,
|
|
131
|
+
},
|
|
132
|
+
message: {
|
|
133
|
+
marginBottom: CommonSizes.spacing.large,
|
|
134
|
+
},
|
|
135
|
+
actions: {
|
|
136
|
+
marginTop: CommonSizes.spacing.medium,
|
|
137
|
+
},
|
|
138
|
+
actionSpacing: {
|
|
139
|
+
marginTop: CommonSizes.spacing.medium,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -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
|
+
});
|