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.
Files changed (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -2
  3. package/bin/create-react-native-starter-kit.js +95 -0
  4. package/lib/copy-src.js +127 -0
  5. package/lib/creator.js +211 -0
  6. package/lib/deps.js +58 -0
  7. package/lib/patch-configs.js +318 -0
  8. package/lib/patch-native.js +387 -0
  9. package/package.json +47 -4
  10. package/scripts/sync-template.js +52 -0
  11. package/template/.env.example +3 -0
  12. package/template/patches/react-native-element-dropdown+2.12.4.patch +67 -0
  13. package/template/src/app/App.tsx +25 -0
  14. package/template/src/app/navigation/root.tsx +24 -0
  15. package/template/src/app/navigation/stacks/app.stack.tsx +67 -0
  16. package/template/src/app/navigation/stacks/app.tab.tsx +72 -0
  17. package/template/src/app/navigation/stacks/auth.stack.tsx +56 -0
  18. package/template/src/app/providers.tsx +43 -0
  19. package/template/src/assets/fonts/InterTight-Black.ttf +0 -0
  20. package/template/src/assets/fonts/InterTight-BlackItalic.ttf +0 -0
  21. package/template/src/assets/fonts/InterTight-Bold.ttf +0 -0
  22. package/template/src/assets/fonts/InterTight-BoldItalic.ttf +0 -0
  23. package/template/src/assets/fonts/InterTight-ExtraBold.ttf +0 -0
  24. package/template/src/assets/fonts/InterTight-ExtraBoldItalic.ttf +0 -0
  25. package/template/src/assets/fonts/InterTight-ExtraLight.ttf +0 -0
  26. package/template/src/assets/fonts/InterTight-ExtraLightItalic.ttf +0 -0
  27. package/template/src/assets/fonts/InterTight-Italic.ttf +0 -0
  28. package/template/src/assets/fonts/InterTight-Light.ttf +0 -0
  29. package/template/src/assets/fonts/InterTight-LightItalic.ttf +0 -0
  30. package/template/src/assets/fonts/InterTight-Medium.ttf +0 -0
  31. package/template/src/assets/fonts/InterTight-MediumItalic.ttf +0 -0
  32. package/template/src/assets/fonts/InterTight-Regular.ttf +0 -0
  33. package/template/src/assets/fonts/InterTight-SemiBold.ttf +0 -0
  34. package/template/src/assets/fonts/InterTight-SemiBoldItalic.ttf +0 -0
  35. package/template/src/assets/fonts/InterTight-Thin.ttf +0 -0
  36. package/template/src/assets/fonts/InterTight-ThinItalic.ttf +0 -0
  37. package/template/src/assets/icons/back.svg +3 -0
  38. package/template/src/assets/icons/calendar.svg +3 -0
  39. package/template/src/assets/icons/claim_upload.svg +3 -0
  40. package/template/src/assets/icons/clock.svg +1 -0
  41. package/template/src/assets/icons/downArrow.svg +3 -0
  42. package/template/src/assets/icons/edit.svg +3 -0
  43. package/template/src/assets/icons/eye.svg +4 -0
  44. package/template/src/assets/icons/eyeClose.svg +7 -0
  45. package/template/src/assets/icons/folder.svg +5 -0
  46. package/template/src/assets/icons/home.svg +4 -0
  47. package/template/src/assets/icons/index.ts +33 -0
  48. package/template/src/assets/icons/setting.svg +4 -0
  49. package/template/src/assets/icons/tabs/home/filled_dark.svg +3 -0
  50. package/template/src/assets/icons/tabs/home/filled_light.svg +3 -0
  51. package/template/src/assets/icons/tabs/home/outline.svg +4 -0
  52. package/template/src/assets/icons/tabs/index.ts +31 -0
  53. package/template/src/assets/icons/tabs/profile/filled_dark.svg +6 -0
  54. package/template/src/assets/icons/tabs/profile/filled_light.svg +6 -0
  55. package/template/src/assets/icons/tabs/profile/outline.svg +3 -0
  56. package/template/src/assets/icons/tooltip_dark.svg +1 -0
  57. package/template/src/assets/icons/tooltip_light.svg +1 -0
  58. package/template/src/assets/icons/upArrow.svg +3 -0
  59. package/template/src/assets/images/back.png +0 -0
  60. package/template/src/assets/images/index.ts +7 -0
  61. package/template/src/assets/images/placeholder/profile.png +0 -0
  62. package/template/src/components/shared/AppImage.tsx +190 -0
  63. package/template/src/components/shared/AvatarPicker.tsx +175 -0
  64. package/template/src/components/shared/CustomButton.tsx +273 -0
  65. package/template/src/components/shared/DocumentPicker.tsx +86 -0
  66. package/template/src/components/shared/GlobalLoader.tsx +47 -0
  67. package/template/src/components/shared/Icon.tsx +28 -0
  68. package/template/src/components/shared/PressableIcon.tsx +104 -0
  69. package/template/src/components/shared/bottom-sheet/BaseBottomSheet.tsx +30 -0
  70. package/template/src/components/shared/bottom-sheet/BaseBottomSheetModal.tsx +50 -0
  71. package/template/src/components/shared/bottom-sheet/sheets/ImagePickerSheet.tsx +310 -0
  72. package/template/src/components/shared/bottom-sheet/types.ts +4 -0
  73. package/template/src/components/shared/form-input/FormInput.tsx +139 -0
  74. package/template/src/components/shared/form-input/FormInputRenderer.tsx +82 -0
  75. package/template/src/components/shared/form-input/FullNameInput.tsx +243 -0
  76. package/template/src/components/shared/form-input/formFieldStyles.ts +43 -0
  77. package/template/src/components/shared/form-input/type.ts +188 -0
  78. package/template/src/components/shared/form-input/variants/CountryPhoneVariant.tsx +289 -0
  79. package/template/src/components/shared/form-input/variants/DateTimeVariant.tsx +280 -0
  80. package/template/src/components/shared/form-input/variants/DropdownVariant.tsx +221 -0
  81. package/template/src/components/shared/form-input/variants/TextInputVariant.tsx +300 -0
  82. package/template/src/components/shared/form-input/variants/ToggleVariant.tsx +194 -0
  83. package/template/src/components/shared/modal/BaseModal.tsx +83 -0
  84. package/template/src/components/shared/modal/common/AlertModal.tsx +173 -0
  85. package/template/src/components/shared/navigation/AnimatedTabIcon.tsx +37 -0
  86. package/template/src/components/shared/navigation/HeaderActionButton.tsx +70 -0
  87. package/template/src/components/shared/navigation/HeaderBackButton.tsx +38 -0
  88. package/template/src/components/shared/navigation/StackHeader.tsx +105 -0
  89. package/template/src/components/shared/navigation/tabIconRender.tsx +14 -0
  90. package/template/src/components/shared/offline/OfflineQueryBoundary.tsx +25 -0
  91. package/template/src/components/shared/offline/OfflineState.tsx +229 -0
  92. package/template/src/components/shared/offline/index.ts +6 -0
  93. package/template/src/components/shared/offline/types.ts +15 -0
  94. package/template/src/components/shared/offline/wifi-icon-animation/BrokenWifiAnimation.tsx +116 -0
  95. package/template/src/components/shared/offline/wifi-icon-animation/WifiPiece.tsx +80 -0
  96. package/template/src/components/shared/offline/wifi-icon-animation/geometry.ts +163 -0
  97. package/template/src/components/shared/offline/wifi-icon-animation/useWifiIconAnimationActive.ts +37 -0
  98. package/template/src/components/shared/skelton/BaseSkelton.tsx +71 -0
  99. package/template/src/components/shared/skelton/ShimmerHolder.ts +6 -0
  100. package/template/src/components/shared/skelton/holders/HomeShimmer.tsx +25 -0
  101. package/template/src/components/shared/skelton/holders/index.ts +1 -0
  102. package/template/src/components/shared/skelton/layouts/avatar.layout.ts +18 -0
  103. package/template/src/components/shared/skelton/layouts/home.layout.ts +26 -0
  104. package/template/src/components/shared/skelton/layouts/index.ts +3 -0
  105. package/template/src/components/shared/skelton/layouts/text.layout.ts +18 -0
  106. package/template/src/components/shared/text/CustomText.tsx +85 -0
  107. package/template/src/components/shared/text/ShimmerText.tsx +159 -0
  108. package/template/src/components/shared/text/types.ts +40 -0
  109. package/template/src/constants/device.ts +12 -0
  110. package/template/src/constants/endpoint.ts +11 -0
  111. package/template/src/constants/keyExtractor.ts +6 -0
  112. package/template/src/constants/limits.ts +9 -0
  113. package/template/src/constants/queryKey.ts +3 -0
  114. package/template/src/constants/regex.ts +7 -0
  115. package/template/src/context/NetworkContext.tsx +102 -0
  116. package/template/src/context/ThemeContext.tsx +56 -0
  117. package/template/src/hooks/app/index.ts +2 -0
  118. package/template/src/hooks/app/useGetProfile.ts +38 -0
  119. package/template/src/hooks/app/useUpdateProfile.ts +21 -0
  120. package/template/src/hooks/useAppDispatch.ts +6 -0
  121. package/template/src/hooks/useAppSelector.ts +4 -0
  122. package/template/src/hooks/useDebounce.ts +15 -0
  123. package/template/src/hooks/useDocumentPicker.ts +102 -0
  124. package/template/src/hooks/useImagePicker.ts +83 -0
  125. package/template/src/i18n/i18next.d.ts +9 -0
  126. package/template/src/i18n/index.ts +16 -0
  127. package/template/src/i18n/locales/en.json +274 -0
  128. package/template/src/i18n/locales/ru.json +165 -0
  129. package/template/src/i18n/resources.ts +11 -0
  130. package/template/src/redux/selectors/auth.selectors.ts +4 -0
  131. package/template/src/redux/slice/auth.slice.ts +23 -0
  132. package/template/src/redux/slice/loader.slice.ts +25 -0
  133. package/template/src/redux/slice/settings.slice.ts +26 -0
  134. package/template/src/redux/slice/user.slice.ts +25 -0
  135. package/template/src/screens/app/main-tab/index.tsx +73 -0
  136. package/template/src/screens/app/main-tab/style.ts +0 -0
  137. package/template/src/screens/app/profile-setup/index.tsx +161 -0
  138. package/template/src/screens/app/profile-setup/styles.ts +3 -0
  139. package/template/src/screens/auth/forgot-password/index.tsx +59 -0
  140. package/template/src/screens/auth/forgot-password/styles.ts +0 -0
  141. package/template/src/screens/auth/index.ts +4 -0
  142. package/template/src/screens/auth/login/index.tsx +137 -0
  143. package/template/src/screens/auth/login/styles.ts +3 -0
  144. package/template/src/screens/auth/signup/index.tsx +110 -0
  145. package/template/src/screens/auth/signup/styles.ts +3 -0
  146. package/template/src/screens/auth/verify-email/index.tsx +15 -0
  147. package/template/src/screens/auth/verify-email/styles.ts +3 -0
  148. package/template/src/screens/tab/home/index.tsx +49 -0
  149. package/template/src/screens/tab/home/styles.ts +3 -0
  150. package/template/src/screens/tab/index.ts +2 -0
  151. package/template/src/screens/tab/profile/index.tsx +50 -0
  152. package/template/src/screens/tab/profile/styles.ts +3 -0
  153. package/template/src/services/api/auth/auth.api.ts +22 -0
  154. package/template/src/services/api/auth/auth.endpoints.ts +7 -0
  155. package/template/src/services/api/auth/auth.types.ts +9 -0
  156. package/template/src/services/api/auth/index.ts +3 -0
  157. package/template/src/services/api/axios/axios.instance.ts +15 -0
  158. package/template/src/services/api/axios/axios.interceptors.ts +43 -0
  159. package/template/src/services/api/axios/index.ts +2 -0
  160. package/template/src/services/api/mock/mockAdapter.ts +97 -0
  161. package/template/src/services/api/user/index.ts +2 -0
  162. package/template/src/services/api/user/user.api.ts +67 -0
  163. package/template/src/services/api/user/user.endpoints.ts +0 -0
  164. package/template/src/services/api/user/user.types.ts +0 -0
  165. package/template/src/services/errors/backendErrorCodes.ts +7 -0
  166. package/template/src/services/errors/errorHandler.ts +94 -0
  167. package/template/src/services/errors/firebaseErrorMap.ts +61 -0
  168. package/template/src/services/errors/httpStatusMap.ts +47 -0
  169. package/template/src/services/errors/index.ts +4 -0
  170. package/template/src/services/network/index.ts +1 -0
  171. package/template/src/services/network/onlineManager.ts +22 -0
  172. package/template/src/services/query/index.ts +1 -0
  173. package/template/src/services/query/queryClient.ts +23 -0
  174. package/template/src/services/toast/ToastCard.tsx +137 -0
  175. package/template/src/services/toast/ToastConfig.tsx +32 -0
  176. package/template/src/services/toast/index.ts +3 -0
  177. package/template/src/services/toast/toast.ts +68 -0
  178. package/template/src/storage/mmkvAdapter.ts +3 -0
  179. package/template/src/storage/mmkvInstance.ts +16 -0
  180. package/template/src/store/index.ts +15 -0
  181. package/template/src/store/mmkv.ts +6 -0
  182. package/template/src/store/persist.ts +10 -0
  183. package/template/src/store/rootReducer.ts +26 -0
  184. package/template/src/theme/dark.ts +40 -0
  185. package/template/src/theme/fonts.ts +20 -0
  186. package/template/src/theme/index.ts +6 -0
  187. package/template/src/theme/layout.ts +22 -0
  188. package/template/src/theme/light.ts +41 -0
  189. package/template/src/theme/palette.ts +32 -0
  190. package/template/src/theme/spacing.ts +7 -0
  191. package/template/src/theme/typography.ts +80 -0
  192. package/template/src/types/api.types.ts +34 -0
  193. package/template/src/types/env.d.ts +6 -0
  194. package/template/src/types/index.ts +2 -0
  195. package/template/src/types/navigation.types.ts +94 -0
  196. package/template/src/utils/clearAppSession.ts +10 -0
  197. package/template/src/utils/date.ts +19 -0
  198. package/template/src/utils/documentPickerHandler.ts +96 -0
  199. package/template/src/utils/getFileNameFromUri.ts +18 -0
  200. package/template/src/utils/getInitials.ts +13 -0
  201. package/template/src/utils/hindiText.ts +33 -0
  202. package/template/src/utils/imageCompress.ts +97 -0
  203. package/template/src/utils/imagePickerHandler.ts +200 -0
  204. package/template/src/utils/maskPhone.ts +17 -0
  205. package/template/src/utils/normalize.ts +12 -0
  206. package/template/src/validation/messages.ts +11 -0
  207. package/template/src/validation/rules.ts +44 -0
  208. package/template/src/validation/schema/auth.schema.ts +53 -0
  209. package/template/src/validation/schema/user.schema.ts +46 -0
  210. 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,3 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const styles = StyleSheet.create({});
@@ -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,3 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const styles = StyleSheet.create({});
@@ -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,3 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const styles = StyleSheet.create({});
@@ -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,3 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const styles = StyleSheet.create({});
@@ -0,0 +1,2 @@
1
+ export { default as Home } from './home';
2
+ export { default as Profile } from './profile';
@@ -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,3 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const styles = StyleSheet.create({});
@@ -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,7 @@
1
+ import { ENDPOINTS } from '@/constants/endpoint';
2
+
3
+ export const AUTH_ENDPOINTS = {
4
+ LOGIN: ENDPOINTS.auth.login,
5
+ SIGNUP: ENDPOINTS.auth.signup,
6
+ LOGOUT: ENDPOINTS.auth.logout,
7
+ } as const;
@@ -0,0 +1,9 @@
1
+ export interface LoginResponse {
2
+ access_token: string;
3
+ }
4
+
5
+ //Arguments for login API
6
+ export type LoginArgs = {
7
+ email: string;
8
+ password: string;
9
+ };
@@ -0,0 +1,3 @@
1
+ export * from './auth.api';
2
+ export * from './auth.types';
3
+ export * from './auth.endpoints';
@@ -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,2 @@
1
+ export * from './axios.instance';
2
+ export * from './axios.interceptors';
@@ -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,2 @@
1
+ export { getProfile, updateProfile } from './user.api';
2
+ export type { UserProfile, UpdateProfileArgs } from './user.api';
@@ -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