create-react-native-starter-kit 0.0.0-stage → 1.0.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 +378 -2
- package/bin/create-react-native-starter-kit.js +95 -0
- package/lib/copy-src.js +127 -0
- package/lib/creator.js +211 -0
- package/lib/deps.js +58 -0
- package/lib/patch-configs.js +318 -0
- package/lib/patch-native.js +387 -0
- package/package.json +47 -4
- package/scripts/sync-template.js +52 -0
- package/template/.env.example +3 -0
- package/template/patches/react-native-element-dropdown+2.12.4.patch +67 -0
- package/template/src/app/App.tsx +25 -0
- package/template/src/app/navigation/root.tsx +24 -0
- package/template/src/app/navigation/stacks/app.stack.tsx +67 -0
- package/template/src/app/navigation/stacks/app.tab.tsx +72 -0
- package/template/src/app/navigation/stacks/auth.stack.tsx +56 -0
- package/template/src/app/providers.tsx +43 -0
- package/template/src/assets/fonts/InterTight-Black.ttf +0 -0
- package/template/src/assets/fonts/InterTight-BlackItalic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-Bold.ttf +0 -0
- package/template/src/assets/fonts/InterTight-BoldItalic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-ExtraBold.ttf +0 -0
- package/template/src/assets/fonts/InterTight-ExtraBoldItalic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-ExtraLight.ttf +0 -0
- package/template/src/assets/fonts/InterTight-ExtraLightItalic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-Italic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-Light.ttf +0 -0
- package/template/src/assets/fonts/InterTight-LightItalic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-Medium.ttf +0 -0
- package/template/src/assets/fonts/InterTight-MediumItalic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-Regular.ttf +0 -0
- package/template/src/assets/fonts/InterTight-SemiBold.ttf +0 -0
- package/template/src/assets/fonts/InterTight-SemiBoldItalic.ttf +0 -0
- package/template/src/assets/fonts/InterTight-Thin.ttf +0 -0
- package/template/src/assets/fonts/InterTight-ThinItalic.ttf +0 -0
- package/template/src/assets/icons/back.svg +3 -0
- package/template/src/assets/icons/calendar.svg +3 -0
- package/template/src/assets/icons/claim_upload.svg +3 -0
- package/template/src/assets/icons/clock.svg +1 -0
- package/template/src/assets/icons/downArrow.svg +3 -0
- package/template/src/assets/icons/edit.svg +3 -0
- package/template/src/assets/icons/eye.svg +4 -0
- package/template/src/assets/icons/eyeClose.svg +7 -0
- package/template/src/assets/icons/folder.svg +5 -0
- package/template/src/assets/icons/home.svg +4 -0
- package/template/src/assets/icons/index.ts +33 -0
- package/template/src/assets/icons/setting.svg +4 -0
- package/template/src/assets/icons/tabs/home/filled_dark.svg +3 -0
- package/template/src/assets/icons/tabs/home/filled_light.svg +3 -0
- package/template/src/assets/icons/tabs/home/outline.svg +4 -0
- package/template/src/assets/icons/tabs/index.ts +31 -0
- package/template/src/assets/icons/tabs/profile/filled_dark.svg +6 -0
- package/template/src/assets/icons/tabs/profile/filled_light.svg +6 -0
- package/template/src/assets/icons/tabs/profile/outline.svg +3 -0
- package/template/src/assets/icons/tooltip_dark.svg +1 -0
- package/template/src/assets/icons/tooltip_light.svg +1 -0
- package/template/src/assets/icons/upArrow.svg +3 -0
- package/template/src/assets/images/back.png +0 -0
- package/template/src/assets/images/index.ts +7 -0
- package/template/src/assets/images/placeholder/profile.png +0 -0
- package/template/src/components/shared/AppImage.tsx +190 -0
- package/template/src/components/shared/AvatarPicker.tsx +175 -0
- package/template/src/components/shared/CustomButton.tsx +273 -0
- package/template/src/components/shared/DocumentPicker.tsx +86 -0
- package/template/src/components/shared/GlobalLoader.tsx +47 -0
- package/template/src/components/shared/Icon.tsx +28 -0
- package/template/src/components/shared/PressableIcon.tsx +104 -0
- package/template/src/components/shared/bottom-sheet/BaseBottomSheet.tsx +30 -0
- package/template/src/components/shared/bottom-sheet/BaseBottomSheetModal.tsx +50 -0
- package/template/src/components/shared/bottom-sheet/sheets/ImagePickerSheet.tsx +310 -0
- package/template/src/components/shared/bottom-sheet/types.ts +4 -0
- package/template/src/components/shared/form-input/FormInput.tsx +139 -0
- package/template/src/components/shared/form-input/FormInputRenderer.tsx +82 -0
- package/template/src/components/shared/form-input/FullNameInput.tsx +243 -0
- package/template/src/components/shared/form-input/formFieldStyles.ts +43 -0
- package/template/src/components/shared/form-input/type.ts +188 -0
- package/template/src/components/shared/form-input/variants/CountryPhoneVariant.tsx +289 -0
- package/template/src/components/shared/form-input/variants/DateTimeVariant.tsx +280 -0
- package/template/src/components/shared/form-input/variants/DropdownVariant.tsx +221 -0
- package/template/src/components/shared/form-input/variants/TextInputVariant.tsx +300 -0
- package/template/src/components/shared/form-input/variants/ToggleVariant.tsx +194 -0
- package/template/src/components/shared/modal/BaseModal.tsx +83 -0
- package/template/src/components/shared/modal/common/AlertModal.tsx +173 -0
- package/template/src/components/shared/navigation/AnimatedTabIcon.tsx +37 -0
- package/template/src/components/shared/navigation/HeaderActionButton.tsx +70 -0
- package/template/src/components/shared/navigation/HeaderBackButton.tsx +38 -0
- package/template/src/components/shared/navigation/StackHeader.tsx +105 -0
- package/template/src/components/shared/navigation/tabIconRender.tsx +14 -0
- package/template/src/components/shared/offline/OfflineQueryBoundary.tsx +25 -0
- package/template/src/components/shared/offline/OfflineState.tsx +229 -0
- package/template/src/components/shared/offline/index.ts +6 -0
- package/template/src/components/shared/offline/types.ts +15 -0
- package/template/src/components/shared/offline/wifi-icon-animation/BrokenWifiAnimation.tsx +116 -0
- package/template/src/components/shared/offline/wifi-icon-animation/WifiPiece.tsx +80 -0
- package/template/src/components/shared/offline/wifi-icon-animation/geometry.ts +163 -0
- package/template/src/components/shared/offline/wifi-icon-animation/useWifiIconAnimationActive.ts +37 -0
- package/template/src/components/shared/skelton/BaseSkelton.tsx +71 -0
- package/template/src/components/shared/skelton/ShimmerHolder.ts +6 -0
- package/template/src/components/shared/skelton/holders/HomeShimmer.tsx +25 -0
- package/template/src/components/shared/skelton/holders/index.ts +1 -0
- package/template/src/components/shared/skelton/layouts/avatar.layout.ts +18 -0
- package/template/src/components/shared/skelton/layouts/home.layout.ts +26 -0
- package/template/src/components/shared/skelton/layouts/index.ts +3 -0
- package/template/src/components/shared/skelton/layouts/text.layout.ts +18 -0
- package/template/src/components/shared/text/CustomText.tsx +85 -0
- package/template/src/components/shared/text/ShimmerText.tsx +159 -0
- package/template/src/components/shared/text/types.ts +40 -0
- package/template/src/constants/device.ts +12 -0
- package/template/src/constants/endpoint.ts +11 -0
- package/template/src/constants/keyExtractor.ts +6 -0
- package/template/src/constants/limits.ts +9 -0
- package/template/src/constants/queryKey.ts +3 -0
- package/template/src/constants/regex.ts +7 -0
- package/template/src/context/NetworkContext.tsx +102 -0
- package/template/src/context/ThemeContext.tsx +56 -0
- package/template/src/hooks/app/index.ts +2 -0
- package/template/src/hooks/app/useGetProfile.ts +38 -0
- package/template/src/hooks/app/useUpdateProfile.ts +21 -0
- package/template/src/hooks/useAppDispatch.ts +6 -0
- package/template/src/hooks/useAppSelector.ts +4 -0
- package/template/src/hooks/useDebounce.ts +15 -0
- package/template/src/hooks/useDocumentPicker.ts +102 -0
- package/template/src/hooks/useImagePicker.ts +83 -0
- package/template/src/i18n/i18next.d.ts +9 -0
- package/template/src/i18n/index.ts +16 -0
- package/template/src/i18n/locales/en.json +274 -0
- package/template/src/i18n/locales/ru.json +165 -0
- package/template/src/i18n/resources.ts +11 -0
- package/template/src/redux/selectors/auth.selectors.ts +4 -0
- package/template/src/redux/slice/auth.slice.ts +23 -0
- package/template/src/redux/slice/loader.slice.ts +25 -0
- package/template/src/redux/slice/settings.slice.ts +26 -0
- package/template/src/redux/slice/user.slice.ts +25 -0
- package/template/src/screens/app/main-tab/index.tsx +73 -0
- package/template/src/screens/app/main-tab/style.ts +0 -0
- package/template/src/screens/app/profile-setup/index.tsx +161 -0
- package/template/src/screens/app/profile-setup/styles.ts +3 -0
- package/template/src/screens/auth/forgot-password/index.tsx +59 -0
- package/template/src/screens/auth/forgot-password/styles.ts +0 -0
- package/template/src/screens/auth/index.ts +4 -0
- package/template/src/screens/auth/login/index.tsx +137 -0
- package/template/src/screens/auth/login/styles.ts +3 -0
- package/template/src/screens/auth/signup/index.tsx +110 -0
- package/template/src/screens/auth/signup/styles.ts +3 -0
- package/template/src/screens/auth/verify-email/index.tsx +15 -0
- package/template/src/screens/auth/verify-email/styles.ts +3 -0
- package/template/src/screens/tab/home/index.tsx +49 -0
- package/template/src/screens/tab/home/styles.ts +3 -0
- package/template/src/screens/tab/index.ts +2 -0
- package/template/src/screens/tab/profile/index.tsx +50 -0
- package/template/src/screens/tab/profile/styles.ts +3 -0
- package/template/src/services/api/auth/auth.api.ts +22 -0
- package/template/src/services/api/auth/auth.endpoints.ts +7 -0
- package/template/src/services/api/auth/auth.types.ts +9 -0
- package/template/src/services/api/auth/index.ts +3 -0
- package/template/src/services/api/axios/axios.instance.ts +15 -0
- package/template/src/services/api/axios/axios.interceptors.ts +43 -0
- package/template/src/services/api/axios/index.ts +2 -0
- package/template/src/services/api/mock/mockAdapter.ts +97 -0
- package/template/src/services/api/user/index.ts +2 -0
- package/template/src/services/api/user/user.api.ts +67 -0
- package/template/src/services/api/user/user.endpoints.ts +0 -0
- package/template/src/services/api/user/user.types.ts +0 -0
- package/template/src/services/errors/backendErrorCodes.ts +7 -0
- package/template/src/services/errors/errorHandler.ts +94 -0
- package/template/src/services/errors/firebaseErrorMap.ts +61 -0
- package/template/src/services/errors/httpStatusMap.ts +47 -0
- package/template/src/services/errors/index.ts +4 -0
- package/template/src/services/network/index.ts +1 -0
- package/template/src/services/network/onlineManager.ts +22 -0
- package/template/src/services/query/index.ts +1 -0
- package/template/src/services/query/queryClient.ts +23 -0
- package/template/src/services/toast/ToastCard.tsx +137 -0
- package/template/src/services/toast/ToastConfig.tsx +32 -0
- package/template/src/services/toast/index.ts +3 -0
- package/template/src/services/toast/toast.ts +68 -0
- package/template/src/storage/mmkvAdapter.ts +3 -0
- package/template/src/storage/mmkvInstance.ts +16 -0
- package/template/src/store/index.ts +15 -0
- package/template/src/store/mmkv.ts +6 -0
- package/template/src/store/persist.ts +10 -0
- package/template/src/store/rootReducer.ts +26 -0
- package/template/src/theme/dark.ts +40 -0
- package/template/src/theme/fonts.ts +20 -0
- package/template/src/theme/index.ts +6 -0
- package/template/src/theme/layout.ts +22 -0
- package/template/src/theme/light.ts +41 -0
- package/template/src/theme/palette.ts +32 -0
- package/template/src/theme/spacing.ts +7 -0
- package/template/src/theme/typography.ts +80 -0
- package/template/src/types/api.types.ts +34 -0
- package/template/src/types/env.d.ts +6 -0
- package/template/src/types/index.ts +2 -0
- package/template/src/types/navigation.types.ts +94 -0
- package/template/src/utils/clearAppSession.ts +10 -0
- package/template/src/utils/date.ts +19 -0
- package/template/src/utils/documentPickerHandler.ts +96 -0
- package/template/src/utils/getFileNameFromUri.ts +18 -0
- package/template/src/utils/getInitials.ts +13 -0
- package/template/src/utils/hindiText.ts +33 -0
- package/template/src/utils/imageCompress.ts +97 -0
- package/template/src/utils/imagePickerHandler.ts +200 -0
- package/template/src/utils/maskPhone.ts +17 -0
- package/template/src/utils/normalize.ts +12 -0
- package/template/src/validation/messages.ts +11 -0
- package/template/src/validation/rules.ts +44 -0
- package/template/src/validation/schema/auth.schema.ts +53 -0
- package/template/src/validation/schema/user.schema.ts +46 -0
- package/template/src/validation/tValError.ts +13 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import CustomButton from '@/components/shared/CustomButton';
|
|
2
|
+
import CustomText from '@/components/shared/text/CustomText';
|
|
3
|
+
import FormInput from '@/components/shared/form-input/FormInput';
|
|
4
|
+
import { useAppTheme } from '@/context/ThemeContext';
|
|
5
|
+
import { AuthStackScreenProps } from '@/types';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { useForm } from 'react-hook-form';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
10
|
+
import { View } from 'react-native';
|
|
11
|
+
import { layout } from '@/theme/layout';
|
|
12
|
+
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|
13
|
+
import { loginAction } from '@/redux/slice/auth.slice';
|
|
14
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
15
|
+
import { loginSchema } from '@/validation/schema/auth.schema';
|
|
16
|
+
import { tValError } from '@/validation/tValError';
|
|
17
|
+
import { InferType } from 'yup';
|
|
18
|
+
import { LIMITS } from '@/constants/limits';
|
|
19
|
+
|
|
20
|
+
type Props = AuthStackScreenProps<'Login'>;
|
|
21
|
+
|
|
22
|
+
export type LoginFormType = InferType<typeof loginSchema>;
|
|
23
|
+
|
|
24
|
+
const Login = ({ navigation }: Props) => {
|
|
25
|
+
const {
|
|
26
|
+
control,
|
|
27
|
+
formState: { errors },
|
|
28
|
+
handleSubmit,
|
|
29
|
+
} = useForm<LoginFormType>({
|
|
30
|
+
resolver: yupResolver(loginSchema),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { color } = useAppTheme();
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
const dispatch = useAppDispatch();
|
|
36
|
+
|
|
37
|
+
const onNavigateToSignup = () => {
|
|
38
|
+
navigation.navigate('Signup');
|
|
39
|
+
};
|
|
40
|
+
const onNavigateToForgotPassword = () => {
|
|
41
|
+
navigation.navigate('ForgotPassword');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleLogin = () => {
|
|
45
|
+
dispatch(loginAction({ access_token: 'dummy_token' }));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<SafeAreaView style={layout.flexContainer}>
|
|
50
|
+
<CustomText
|
|
51
|
+
variant="header"
|
|
52
|
+
weight="600"
|
|
53
|
+
textColor={color.text_primary}
|
|
54
|
+
textAlign="center"
|
|
55
|
+
>
|
|
56
|
+
{t('auth.welcome-back')}
|
|
57
|
+
</CustomText>
|
|
58
|
+
<FormInput
|
|
59
|
+
control={control}
|
|
60
|
+
name="email"
|
|
61
|
+
//If you don't want to use validation schema, you can add rules like this
|
|
62
|
+
// rules={rules.email}
|
|
63
|
+
label={t('auth.email')}
|
|
64
|
+
placeholder={t('placeholder.enterEmail')}
|
|
65
|
+
textInputProps={{
|
|
66
|
+
keyboardType: 'email-address',
|
|
67
|
+
}}
|
|
68
|
+
error={tValError(t, errors.email, {
|
|
69
|
+
field: t('auth.email'),
|
|
70
|
+
})}
|
|
71
|
+
variant="text"
|
|
72
|
+
isTooltip
|
|
73
|
+
tooltipMessage='We"ll never share your email with anyone else.'
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<FormInput
|
|
77
|
+
control={control}
|
|
78
|
+
name="password"
|
|
79
|
+
label={t('auth.password')}
|
|
80
|
+
placeholder={t('placeholder.enterPassword')}
|
|
81
|
+
error={tValError(t, errors.password, {
|
|
82
|
+
field: t('auth.password'),
|
|
83
|
+
...LIMITS.PASSWORD,
|
|
84
|
+
})}
|
|
85
|
+
isPassword
|
|
86
|
+
variant="text"
|
|
87
|
+
// isTooltip
|
|
88
|
+
// tooltipMessage="Make sure your password is strong and secure."
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<CustomButton
|
|
92
|
+
title={t('auth.login')}
|
|
93
|
+
onPress={handleSubmit(handleLogin)}
|
|
94
|
+
variant="primary"
|
|
95
|
+
fullWidth
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<View style={layout.rowGapCenter}>
|
|
99
|
+
<CustomText textColor={color.text_secondary}>
|
|
100
|
+
{t('auth.forgotPassword')}
|
|
101
|
+
</CustomText>
|
|
102
|
+
<CustomText
|
|
103
|
+
textColor={color.secondary}
|
|
104
|
+
onPress={onNavigateToForgotPassword}
|
|
105
|
+
>
|
|
106
|
+
{t('auth.reset')}
|
|
107
|
+
</CustomText>
|
|
108
|
+
</View>
|
|
109
|
+
|
|
110
|
+
<View style={layout.flexEnd}>
|
|
111
|
+
<View style={layout.rowGapCenter}>
|
|
112
|
+
<CustomText textColor={color.text_secondary}>
|
|
113
|
+
{t('auth.dont-have-an-account')}
|
|
114
|
+
</CustomText>
|
|
115
|
+
<CustomText textColor={color.secondary} onPress={onNavigateToSignup}>
|
|
116
|
+
{t('auth.create-account')}
|
|
117
|
+
</CustomText>
|
|
118
|
+
</View>
|
|
119
|
+
|
|
120
|
+
<View style={[layout.separator, { backgroundColor: color.border }]} />
|
|
121
|
+
|
|
122
|
+
<CustomText textColor={color.text_secondary} textAlign="center">
|
|
123
|
+
{t('auth.by-continuing-you-agree-to-our')}{' '}
|
|
124
|
+
<CustomText textColor={color.text_primary} onPress={() => {}}>
|
|
125
|
+
{t('auth.terms-of-service')}
|
|
126
|
+
</CustomText>{' '}
|
|
127
|
+
and{' '}
|
|
128
|
+
<CustomText textColor={color.text_primary} onPress={() => {}}>
|
|
129
|
+
{t('auth.privacy-policy')}
|
|
130
|
+
</CustomText>
|
|
131
|
+
</CustomText>
|
|
132
|
+
</View>
|
|
133
|
+
</SafeAreaView>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default Login;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import CustomButton from '@/components/shared/CustomButton';
|
|
2
|
+
import CustomText from '@/components/shared/text/CustomText';
|
|
3
|
+
import FormInput from '@/components/shared/form-input/FormInput';
|
|
4
|
+
import { useAppTheme } from '@/context/ThemeContext';
|
|
5
|
+
import { AuthStackScreenProps } from '@/types';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { useForm } from 'react-hook-form';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
10
|
+
import { View } from 'react-native';
|
|
11
|
+
import { layout } from '@/theme/layout';
|
|
12
|
+
import { InferType } from 'yup';
|
|
13
|
+
import { registerSchema } from '@/validation/schema/auth.schema';
|
|
14
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
15
|
+
import { tValError } from '@/validation/tValError';
|
|
16
|
+
import { LIMITS } from '@/constants/limits';
|
|
17
|
+
|
|
18
|
+
type Props = AuthStackScreenProps<'Signup'>;
|
|
19
|
+
|
|
20
|
+
export type RegisterFormType = InferType<typeof registerSchema>;
|
|
21
|
+
|
|
22
|
+
const Signup = ({ navigation }: Props) => {
|
|
23
|
+
const {
|
|
24
|
+
control,
|
|
25
|
+
formState: { errors },
|
|
26
|
+
handleSubmit,
|
|
27
|
+
} = useForm<RegisterFormType>({
|
|
28
|
+
resolver: yupResolver(registerSchema),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { color } = useAppTheme();
|
|
32
|
+
const { t } = useTranslation();
|
|
33
|
+
|
|
34
|
+
const onNavigateToLogin = () => {
|
|
35
|
+
navigation.navigate('Login');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<SafeAreaView style={layout.flexContainer}>
|
|
40
|
+
<CustomText
|
|
41
|
+
variant="header"
|
|
42
|
+
weight="600"
|
|
43
|
+
textColor={color.text_primary}
|
|
44
|
+
textAlign="center"
|
|
45
|
+
>
|
|
46
|
+
{t('auth.lets-get-started')}
|
|
47
|
+
</CustomText>
|
|
48
|
+
<FormInput
|
|
49
|
+
control={control}
|
|
50
|
+
name="email"
|
|
51
|
+
placeholder={t('placeholder.enterEmail')}
|
|
52
|
+
label={t('auth.email')}
|
|
53
|
+
textInputProps={{
|
|
54
|
+
keyboardType: 'email-address',
|
|
55
|
+
}}
|
|
56
|
+
error={tValError(t, errors.email, {
|
|
57
|
+
field: t('auth.email'),
|
|
58
|
+
...LIMITS.EMAIL,
|
|
59
|
+
})}
|
|
60
|
+
variant="text"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<FormInput
|
|
64
|
+
control={control}
|
|
65
|
+
name="password"
|
|
66
|
+
label={t('auth.password')}
|
|
67
|
+
placeholder={t('placeholder.enterPassword')}
|
|
68
|
+
isPassword
|
|
69
|
+
variant="text"
|
|
70
|
+
error={tValError(t, errors.password, {
|
|
71
|
+
field: t('auth.password'),
|
|
72
|
+
...LIMITS.PASSWORD,
|
|
73
|
+
})}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<CustomButton
|
|
77
|
+
title={t('auth.create-account')}
|
|
78
|
+
onPress={handleSubmit(onNavigateToLogin)}
|
|
79
|
+
variant="primary"
|
|
80
|
+
fullWidth
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<View style={layout.flexEnd}>
|
|
84
|
+
<View style={layout.rowGapCenter}>
|
|
85
|
+
<CustomText textColor={color.text_secondary}>
|
|
86
|
+
{t('auth.alreadyHaveAccount')}
|
|
87
|
+
</CustomText>
|
|
88
|
+
<CustomText textColor={color.secondary} onPress={onNavigateToLogin}>
|
|
89
|
+
Login
|
|
90
|
+
</CustomText>
|
|
91
|
+
</View>
|
|
92
|
+
|
|
93
|
+
<View style={[layout.separator, { backgroundColor: color.border }]} />
|
|
94
|
+
|
|
95
|
+
<CustomText textColor={color.text_secondary} textAlign="center">
|
|
96
|
+
{t('auth.by-continuing-you-agree-to-our')}{' '}
|
|
97
|
+
<CustomText textColor={color.text_primary} onPress={() => {}}>
|
|
98
|
+
{t('auth.terms-of-service')}
|
|
99
|
+
</CustomText>{' '}
|
|
100
|
+
and{' '}
|
|
101
|
+
<CustomText textColor={color.text_primary} onPress={() => {}}>
|
|
102
|
+
{t('auth.privacy-policy')}
|
|
103
|
+
</CustomText>
|
|
104
|
+
</CustomText>
|
|
105
|
+
</View>
|
|
106
|
+
</SafeAreaView>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export default Signup;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AuthStackScreenProps } from '@/types';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, Text } from 'react-native';
|
|
4
|
+
|
|
5
|
+
type props = AuthStackScreenProps<'VerifyEmail'>;
|
|
6
|
+
|
|
7
|
+
const VerifyEmail = ({}: props) => {
|
|
8
|
+
return (
|
|
9
|
+
<View>
|
|
10
|
+
<Text>VerifyEmail Screen</Text>
|
|
11
|
+
</View>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default VerifyEmail;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import CustomText from '@/components/shared/text/CustomText';
|
|
2
|
+
import AvatarPicker from '@/components/shared/AvatarPicker';
|
|
3
|
+
import { OfflineQueryBoundary } from '@/components/shared/offline';
|
|
4
|
+
import { layout } from '@/theme/layout';
|
|
5
|
+
import { TabScreenProps } from '@/types';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { ShimmerHolder } from '@/components/shared/skelton/ShimmerHolder';
|
|
8
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
|
|
11
|
+
type props = TabScreenProps<'Home'>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set to `false`, then turn on airplane mode, to see the offline screen.
|
|
15
|
+
* In a real screen pass whether the query has data, e.g. `hasData={!!data}`.
|
|
16
|
+
*/
|
|
17
|
+
const HAS_DATA = true;
|
|
18
|
+
|
|
19
|
+
const Home = ({}: props) => {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<SafeAreaView style={layout.flex}>
|
|
24
|
+
<CustomText
|
|
25
|
+
variant="header"
|
|
26
|
+
textAlign="center"
|
|
27
|
+
style={layout.marginTopMd}
|
|
28
|
+
>
|
|
29
|
+
{t('home-screen')}
|
|
30
|
+
</CustomText>
|
|
31
|
+
<OfflineQueryBoundary
|
|
32
|
+
hasData={HAS_DATA}
|
|
33
|
+
loading={<ShimmerHolder.Home />}
|
|
34
|
+
>
|
|
35
|
+
<AvatarPicker
|
|
36
|
+
editable
|
|
37
|
+
width={150}
|
|
38
|
+
height={150}
|
|
39
|
+
editIconSize={39}
|
|
40
|
+
source="both"
|
|
41
|
+
onImageChange={(image: any) => console.log('Selected image:', image)}
|
|
42
|
+
isLoading={false}
|
|
43
|
+
/>
|
|
44
|
+
</OfflineQueryBoundary>
|
|
45
|
+
</SafeAreaView>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default Home;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import CustomButton from '@/components/shared/CustomButton';
|
|
2
|
+
import CustomText from '@/components/shared/text/CustomText';
|
|
3
|
+
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|
4
|
+
import { hideLoader, showLoader } from '@/redux/slice/loader.slice';
|
|
5
|
+
import { layout } from '@/theme/layout';
|
|
6
|
+
import { TabScreenProps } from '@/types';
|
|
7
|
+
import React, { useEffect } from 'react';
|
|
8
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
9
|
+
|
|
10
|
+
type props = TabScreenProps<'Profile'>;
|
|
11
|
+
|
|
12
|
+
const Profile = ({ navigation }: props) => {
|
|
13
|
+
const dispatch = useAppDispatch();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Global loader reference: `showLoader()` puts a full-screen overlay on top of the app
|
|
17
|
+
* and `hideLoader()` removes it. Use it for blocking work (submit, upload, delete);
|
|
18
|
+
* use a shimmer (see Home) for first-time screen data instead.
|
|
19
|
+
*/
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const loadData = async () => {
|
|
22
|
+
dispatch(showLoader());
|
|
23
|
+
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve as any, 2000));
|
|
25
|
+
|
|
26
|
+
dispatch(hideLoader());
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
loadData();
|
|
30
|
+
}, [dispatch]);
|
|
31
|
+
|
|
32
|
+
const handlePress = () => {
|
|
33
|
+
navigation.navigate('MainTabs');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<SafeAreaView>
|
|
38
|
+
<CustomText
|
|
39
|
+
variant="header"
|
|
40
|
+
textAlign="center"
|
|
41
|
+
style={layout.marginTopMd}
|
|
42
|
+
>
|
|
43
|
+
Profile Screen
|
|
44
|
+
</CustomText>
|
|
45
|
+
<CustomButton title="Go to Main Tab" onPress={handlePress} />
|
|
46
|
+
</SafeAreaView>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default Profile;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ApiResponse, ApiSuccess } from '@/types';
|
|
2
|
+
import { apiClient } from '../axios';
|
|
3
|
+
import { AUTH_ENDPOINTS } from './auth.endpoints';
|
|
4
|
+
import { LoginArgs, LoginResponse } from './auth.types';
|
|
5
|
+
|
|
6
|
+
export const loginApi = async (
|
|
7
|
+
data: LoginArgs,
|
|
8
|
+
): Promise<ApiSuccess<LoginResponse>> => {
|
|
9
|
+
const res = await apiClient.post<ApiResponse<LoginResponse>>(
|
|
10
|
+
AUTH_ENDPOINTS.LOGIN,
|
|
11
|
+
data,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (!res.data.result) {
|
|
15
|
+
throw new Error(res.data.message || 'Login failed');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
data: res.data.payload,
|
|
20
|
+
message: res.data.message,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { mockAdapter } from '../mock/mockAdapter';
|
|
3
|
+
|
|
4
|
+
/** No API_BASE_URL in `.env` → every request is answered by the built-in mock backend. */
|
|
5
|
+
export const USE_MOCK_API = !process.env.API_BASE_URL;
|
|
6
|
+
|
|
7
|
+
export const apiClient = axios.create({
|
|
8
|
+
baseURL: process.env.API_BASE_URL,
|
|
9
|
+
timeout: 15000,
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
Accept: 'application/json',
|
|
13
|
+
},
|
|
14
|
+
...(USE_MOCK_API ? { adapter: mockAdapter } : {}),
|
|
15
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AxiosError } from 'axios';
|
|
2
|
+
import { store } from '@/store';
|
|
3
|
+
import { isDeviceOnline } from '@/services/network';
|
|
4
|
+
import { clearAppSession } from '@/utils/clearAppSession';
|
|
5
|
+
import { apiClient } from './axios.instance';
|
|
6
|
+
|
|
7
|
+
/* ================= REQUEST ================= */
|
|
8
|
+
|
|
9
|
+
apiClient.interceptors.request.use(
|
|
10
|
+
config => {
|
|
11
|
+
if (!isDeviceOnline()) {
|
|
12
|
+
return Promise.reject(
|
|
13
|
+
new AxiosError('Network Error', AxiosError.ERR_NETWORK, config),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const token = store.getState().auth?.access_token;
|
|
18
|
+
|
|
19
|
+
if (token) {
|
|
20
|
+
config.headers = config.headers ?? {};
|
|
21
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return config;
|
|
25
|
+
},
|
|
26
|
+
error => Promise.reject(error),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/* ================= RESPONSE ================= */
|
|
30
|
+
|
|
31
|
+
apiClient.interceptors.response.use(
|
|
32
|
+
res => res,
|
|
33
|
+
async error => {
|
|
34
|
+
const status = error.response?.status;
|
|
35
|
+
const token = store.getState().auth?.access_token;
|
|
36
|
+
|
|
37
|
+
if (status === 401 && token) {
|
|
38
|
+
await clearAppSession();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Promise.reject(error);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AxiosError, type AxiosAdapter, type AxiosResponse } from 'axios';
|
|
2
|
+
import { ENDPOINTS } from '@/constants/endpoint';
|
|
3
|
+
import type { ApiResponse } from '@/types';
|
|
4
|
+
import type { UserProfile } from '../user/user.api';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Built-in fake backend used while `API_BASE_URL` is not set in `.env`.
|
|
8
|
+
* Lets screens call the real API functions and hooks on day one.
|
|
9
|
+
* Add a handler here for every new endpoint you want to design against.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const MOCK_DELAY_MS = 600;
|
|
13
|
+
|
|
14
|
+
let mockProfile: UserProfile = {
|
|
15
|
+
id: 'user_1',
|
|
16
|
+
isProfileSetup: true,
|
|
17
|
+
name: 'Alex Johnson',
|
|
18
|
+
email: 'alex@example.com',
|
|
19
|
+
image_url: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type MockHandler = (body: unknown) => { message: string; payload: unknown };
|
|
23
|
+
|
|
24
|
+
/** React Native's FormData exposes its fields through getParts(). */
|
|
25
|
+
const readFormData = (body: unknown): Record<string, unknown> => {
|
|
26
|
+
const parts =
|
|
27
|
+
(body as { getParts?: () => Array<Record<string, unknown>> })?.getParts?.() ?? [];
|
|
28
|
+
return parts.reduce<Record<string, unknown>>((acc, part) => {
|
|
29
|
+
const key = part.fieldName as string;
|
|
30
|
+
acc[key] = part.string ?? part.uri;
|
|
31
|
+
return acc;
|
|
32
|
+
}, {});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handlers: Record<string, MockHandler> = {
|
|
36
|
+
[`post ${ENDPOINTS.auth.login}`]: () => ({
|
|
37
|
+
message: 'Logged in',
|
|
38
|
+
payload: { access_token: 'mock-access-token' },
|
|
39
|
+
}),
|
|
40
|
+
[`post ${ENDPOINTS.auth.signup}`]: () => ({
|
|
41
|
+
message: 'Account created',
|
|
42
|
+
payload: { access_token: 'mock-access-token' },
|
|
43
|
+
}),
|
|
44
|
+
[`post ${ENDPOINTS.auth.logout}`]: () => ({
|
|
45
|
+
message: 'Logged out',
|
|
46
|
+
payload: null,
|
|
47
|
+
}),
|
|
48
|
+
[`get ${ENDPOINTS.app.profile}`]: () => ({
|
|
49
|
+
message: 'Profile loaded',
|
|
50
|
+
payload: mockProfile,
|
|
51
|
+
}),
|
|
52
|
+
[`patch ${ENDPOINTS.app.updateProfile}`]: body => {
|
|
53
|
+
mockProfile = { ...mockProfile, ...readFormData(body) };
|
|
54
|
+
return { message: 'Profile updated successfully', payload: mockProfile };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const wrap = <T>(message: string, payload: T, result = true): ApiResponse<T> => ({
|
|
59
|
+
result,
|
|
60
|
+
requestId: `mock-${Date.now()}`,
|
|
61
|
+
message,
|
|
62
|
+
messageLBL: message,
|
|
63
|
+
payload,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const mockAdapter: AxiosAdapter = async config => {
|
|
67
|
+
await new Promise<void>(resolve => setTimeout(resolve, MOCK_DELAY_MS));
|
|
68
|
+
|
|
69
|
+
const key = `${(config.method ?? 'get').toLowerCase()} ${config.url ?? ''}`;
|
|
70
|
+
const handler = handlers[key];
|
|
71
|
+
|
|
72
|
+
if (!handler) {
|
|
73
|
+
const response: AxiosResponse = {
|
|
74
|
+
data: wrap(`No mock for ${key}`, null, false),
|
|
75
|
+
status: 404,
|
|
76
|
+
statusText: 'Not Found',
|
|
77
|
+
headers: {},
|
|
78
|
+
config,
|
|
79
|
+
};
|
|
80
|
+
throw new AxiosError(
|
|
81
|
+
`No mock handler for ${key}. Add one in services/api/mock/mockAdapter.ts`,
|
|
82
|
+
AxiosError.ERR_BAD_REQUEST,
|
|
83
|
+
config,
|
|
84
|
+
undefined,
|
|
85
|
+
response,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { message, payload } = handler(config.data);
|
|
90
|
+
return {
|
|
91
|
+
data: wrap(message, payload),
|
|
92
|
+
status: 200,
|
|
93
|
+
statusText: 'OK',
|
|
94
|
+
headers: {},
|
|
95
|
+
config,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ApiResponse, ApiSuccess } from '@/types';
|
|
2
|
+
import { ENDPOINTS } from '@/constants/endpoint';
|
|
3
|
+
import { getCompressedImageForUpload } from '@/utils/imageCompress';
|
|
4
|
+
import { apiClient } from '../axios';
|
|
5
|
+
|
|
6
|
+
export type UserProfile = {
|
|
7
|
+
id: string;
|
|
8
|
+
isProfileSetup: boolean;
|
|
9
|
+
name?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
image_url?: string | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Text fields are sent as-is; `image_url` is a local file URI that gets compressed and uploaded. */
|
|
15
|
+
export type UpdateProfileArgs = {
|
|
16
|
+
image_url?: string;
|
|
17
|
+
} & Record<string, string | undefined>;
|
|
18
|
+
|
|
19
|
+
export const getProfile = async (): Promise<ApiSuccess<UserProfile>> => {
|
|
20
|
+
const res = await apiClient.get<ApiResponse<UserProfile>>(
|
|
21
|
+
ENDPOINTS.app.profile,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!res.data.result) {
|
|
25
|
+
throw new Error(res.data.message || 'Failed to load profile');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
data: res.data.payload,
|
|
30
|
+
message: res.data.message,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const updateProfile = async (
|
|
35
|
+
args: UpdateProfileArgs,
|
|
36
|
+
): Promise<ApiSuccess<UserProfile>> => {
|
|
37
|
+
const formData = new FormData();
|
|
38
|
+
const { image_url, ...fields } = args;
|
|
39
|
+
|
|
40
|
+
Object.entries(fields).forEach(([key, value]) => {
|
|
41
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
42
|
+
formData.append(key, value);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (image_url) {
|
|
47
|
+
const file = await getCompressedImageForUpload(image_url);
|
|
48
|
+
if (file) {
|
|
49
|
+
formData.append('image_url', file as unknown as Blob);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const res = await apiClient.patch<ApiResponse<UserProfile>>(
|
|
54
|
+
ENDPOINTS.app.updateProfile,
|
|
55
|
+
formData,
|
|
56
|
+
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!res.data.result) {
|
|
60
|
+
throw new Error(res.data.message || 'Failed to update profile');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
data: res.data.payload,
|
|
65
|
+
message: res.data.message,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
File without changes
|
|
File without changes
|