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,387 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Find files recursively matching a pattern or name
6
+ */
7
+ function findFile(dir, fileName) {
8
+ if (!fs.existsSync(dir)) return null;
9
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
10
+ for (const entry of entries) {
11
+ const fullPath = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ if (entry.name === 'node_modules' || entry.name === 'Pods' || entry.name === 'build') continue;
14
+ const found = findFile(fullPath, fileName);
15
+ if (found) return found;
16
+ } else if (entry.name === fileName) {
17
+ return fullPath;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ /**
24
+ * Patch android/app/build.gradle
25
+ */
26
+ function patchAndroidBuildGradle(projectDir) {
27
+ const gradlePath = path.join(projectDir, 'android', 'app', 'build.gradle');
28
+ if (!fs.existsSync(gradlePath)) {
29
+ console.log('⚠️ Could not find android/app/build.gradle');
30
+ return false;
31
+ }
32
+
33
+ let content = fs.readFileSync(gradlePath, 'utf8');
34
+
35
+ // Check if androidx.activity is already added
36
+ if (!content.includes('androidx.activity:activity')) {
37
+ const target = 'dependencies {';
38
+ const injection = `dependencies {
39
+ implementation("androidx.activity:activity:1.+") //by own`;
40
+ if (content.includes(target)) {
41
+ content = content.replace(target, injection);
42
+ fs.writeFileSync(gradlePath, content, 'utf8');
43
+ console.log('✅ Patched android/app/build.gradle');
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Patch MainActivity.kt
52
+ */
53
+ function patchAndroidMainActivity(projectDir) {
54
+ const javaDir = path.join(projectDir, 'android', 'app', 'src', 'main', 'java');
55
+ const mainActivityPath = findFile(javaDir, 'MainActivity.kt');
56
+
57
+ if (!mainActivityPath) {
58
+ console.log('⚠️ Could not find MainActivity.kt');
59
+ return false;
60
+ }
61
+
62
+ let content = fs.readFileSync(mainActivityPath, 'utf8');
63
+
64
+ const requiredImports = [
65
+ 'android.os.Bundle',
66
+ 'com.facebook.react.ReactApplication',
67
+ 'com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory',
68
+ ].filter(name => !new RegExp(`^import\\s+${name.replace(/\./g, '\\.')}\\s*$`, 'm').test(content));
69
+
70
+ if (requiredImports.length > 0) {
71
+ const importBlock = `//By Own\n${requiredImports.map(name => `import ${name}`).join('\n')}\n`;
72
+ // Match only the package line itself so the inserted block always ends on its own line.
73
+ const packageLineRegex = /^package\s+[\w.]+[ \t]*\r?\n/m;
74
+ if (packageLineRegex.test(content)) {
75
+ content = content.replace(packageLineRegex, match => `${match}\n${importBlock}`);
76
+ } else {
77
+ content = `${importBlock}\n${content}`;
78
+ }
79
+ }
80
+
81
+ const activityHooks = `
82
+ //By Own
83
+ override fun onCreate(savedInstanceState: Bundle?) {
84
+ supportFragmentManager.fragmentFactory = RNScreensFragmentFactory()
85
+ super.onCreate(savedInstanceState)
86
+ reloadIfDisplaySizeChanged()
87
+ }
88
+
89
+ /**
90
+ * Changing "Display size" recreates this activity but keeps the JS runtime alive, so sizes
91
+ * computed once at JS load (normalize(), screen-width constants) stay stale and layouts break.
92
+ * Reloading the bundle recomputes them for the new density.
93
+ */
94
+ private fun reloadIfDisplaySizeChanged() {
95
+ val densityDpi = resources.configuration.densityDpi
96
+ if (lastDensityDpi != 0 && lastDensityDpi != densityDpi) {
97
+ (application as ReactApplication).reactHost?.reload("Display size changed")
98
+ }
99
+ lastDensityDpi = densityDpi
100
+ }
101
+
102
+ //By Own
103
+ private companion object {
104
+ /** Survives activity recreation (same process); 0 means first launch. */
105
+ var lastDensityDpi = 0
106
+ }
107
+ `;
108
+
109
+ if (!content.includes('reloadIfDisplaySizeChanged')) {
110
+ const classRegex = /(class\s+MainActivity\s*:\s*ReactActivity\(\)\s*\{)/;
111
+ if (classRegex.test(content)) {
112
+ content = content.replace(classRegex, `$1${activityHooks}`);
113
+ }
114
+ }
115
+
116
+ fs.writeFileSync(mainActivityPath, content, 'utf8');
117
+ console.log('✅ Patched MainActivity.kt');
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Patch AndroidManifest.xml
123
+ */
124
+ function patchAndroidManifest(projectDir) {
125
+ const manifestPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
126
+ if (!fs.existsSync(manifestPath)) {
127
+ console.log('⚠️ Could not find AndroidManifest.xml');
128
+ return false;
129
+ }
130
+
131
+ let content = fs.readFileSync(manifestPath, 'utf8');
132
+ let modified = false;
133
+
134
+ if (!content.includes('xmlns:tools')) {
135
+ content = content.replace(
136
+ '<manifest xmlns:android="http://schemas.android.com/apk/res/android"',
137
+ '<manifest xmlns:android="http://schemas.android.com/apk/res/android"\n xmlns:tools="http://schemas.android.com/tools"',
138
+ );
139
+ modified = true;
140
+ }
141
+
142
+ if (!content.includes('android.permission.CAMERA')) {
143
+ const permissionBlock = ` <uses-permission android:name="android.permission.CAMERA" />
144
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
145
+ <uses-permission
146
+ android:name="android.permission.READ_EXTERNAL_STORAGE"
147
+ android:maxSdkVersion="32" />
148
+ <!-- RN debug adds this for Metro over LAN; use adb reverse instead. -->
149
+ <uses-permission
150
+ android:name="android.permission.ACCESS_LOCAL_NETWORK"
151
+ tools:node="remove" />
152
+
153
+ `;
154
+ if (content.includes('<application')) {
155
+ content = content.replace('<application', `${permissionBlock}<application`);
156
+ modified = true;
157
+ }
158
+ }
159
+
160
+ if (!content.includes('enableOnBackInvokedCallback')) {
161
+ content = content.replace(
162
+ '<application',
163
+ '<application\n android:enableOnBackInvokedCallback="false"',
164
+ );
165
+ modified = true;
166
+ }
167
+
168
+ if (
169
+ content.includes('<activity') &&
170
+ !content.includes('android:screenOrientation=')
171
+ ) {
172
+ content = content.replace(
173
+ '<activity',
174
+ '<activity\n android:screenOrientation="portrait"',
175
+ );
176
+ modified = true;
177
+ }
178
+
179
+ if (modified) {
180
+ fs.writeFileSync(manifestPath, content, 'utf8');
181
+ console.log('✅ Patched AndroidManifest.xml');
182
+ }
183
+ return modified;
184
+ }
185
+
186
+ /**
187
+ * Get all font files from src/assets/fonts
188
+ */
189
+ function getFontFiles(projectDir) {
190
+ const fontsDir = path.join(projectDir, 'src', 'assets', 'fonts');
191
+ if (!fs.existsSync(fontsDir)) return [];
192
+ return fs.readdirSync(fontsDir).filter(f => f.endsWith('.ttf') || f.endsWith('.otf'));
193
+ }
194
+
195
+ /**
196
+ * Patch iOS Info.plist
197
+ */
198
+ function patchIOSInfoPlist(projectDir) {
199
+ const iosDir = path.join(projectDir, 'ios');
200
+ const infoPlistPath = findFile(iosDir, 'Info.plist');
201
+
202
+ if (!infoPlistPath) {
203
+ console.log('⚠️ Could not find iOS Info.plist');
204
+ return false;
205
+ }
206
+
207
+ let content = fs.readFileSync(infoPlistPath, 'utf8');
208
+ let modified = false;
209
+
210
+ // Add Photo & Camera Permissions
211
+ const permissionsToAdd = [];
212
+ if (!content.includes('NSPhotoLibraryUsageDescription')) {
213
+ permissionsToAdd.push(`\t<key>NSPhotoLibraryUsageDescription</key>
214
+ \t<string>We need access to your photo gallery to update your profile picture.</string>`);
215
+ }
216
+ if (!content.includes('NSCameraUsageDescription')) {
217
+ permissionsToAdd.push(`\t<key>NSCameraUsageDescription</key>
218
+ \t<string>We need access to your camera to take photos for your profile picture.</string>`);
219
+ }
220
+
221
+ // Add UIAppFonts
222
+ const fonts = getFontFiles(projectDir);
223
+ if (fonts.length > 0 && !content.includes('<key>UIAppFonts</key>')) {
224
+ const fontStrings = fonts.map(font => `\t\t<string>${font}</string>`).join('\n');
225
+ permissionsToAdd.push(`\t<key>UIAppFonts</key>
226
+ \t<array>
227
+ ${fontStrings}
228
+ \t</array>`);
229
+ }
230
+
231
+ if (content.includes('UIInterfaceOrientationLandscapeLeft')) {
232
+ content = content.replace(
233
+ /<key>UISupportedInterfaceOrientations<\/key>\s*<array>[\s\S]*?<\/array>/,
234
+ `<key>UISupportedInterfaceOrientations</key>
235
+ <array>
236
+ <string>UIInterfaceOrientationPortrait</string>
237
+ </array>`,
238
+ );
239
+ modified = true;
240
+ }
241
+
242
+ if (permissionsToAdd.length > 0) {
243
+ const insertion = permissionsToAdd.join('\n') + '\n</dict>';
244
+ content = content.replace(/<\/dict>\s*<\/plist>/, `${insertion}\n</plist>`);
245
+ modified = true;
246
+ }
247
+
248
+ if (modified) {
249
+ fs.writeFileSync(infoPlistPath, content, 'utf8');
250
+ console.log('✅ Patched iOS Info.plist with permissions and custom fonts');
251
+ }
252
+
253
+ return modified;
254
+ }
255
+
256
+ /**
257
+ * Keep CocoaPods on the same iOS minimum as the app target.
258
+ */
259
+ function patchIOSPodfile(projectDir) {
260
+ const podfilePath = path.join(projectDir, 'ios', 'Podfile');
261
+ if (!fs.existsSync(podfilePath)) {
262
+ return false;
263
+ }
264
+
265
+ let content = fs.readFileSync(podfilePath, 'utf8');
266
+ if (content.includes("min_ios = '15.1'")) {
267
+ return false;
268
+ }
269
+
270
+ const marker = 'react_native_post_install(';
271
+ const start = content.indexOf(marker);
272
+ if (start === -1) {
273
+ return false;
274
+ }
275
+
276
+ const callEnd = content.indexOf('\n )', start);
277
+ if (callEnd === -1) {
278
+ return false;
279
+ }
280
+
281
+ const injection = `
282
+ # Force every pod (including resource bundles) to match the app's
283
+ # iOS 15.1 minimum. Without this, some pods stay on 9.0 and Xcode fails.
284
+ min_ios = '15.1'
285
+ installer.pods_project.build_configurations.each do |config|
286
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios
287
+ end
288
+ installer.pods_project.targets.each do |target|
289
+ target.build_configurations.each do |config|
290
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios
291
+ end
292
+ end`;
293
+
294
+ content =
295
+ content.slice(0, callEnd + '\n )'.length) +
296
+ injection +
297
+ content.slice(callEnd + '\n )'.length);
298
+ fs.writeFileSync(podfilePath, content, 'utf8');
299
+ console.log('✅ Patched ios/Podfile deployment target');
300
+ return true;
301
+ }
302
+
303
+ /**
304
+ * The wrapper default (10s) drops the Gradle distribution download on slow networks.
305
+ */
306
+ function patchGradleWrapper(projectDir) {
307
+ const wrapperPath = path.join(
308
+ projectDir,
309
+ 'android',
310
+ 'gradle',
311
+ 'wrapper',
312
+ 'gradle-wrapper.properties',
313
+ );
314
+ if (!fs.existsSync(wrapperPath)) {
315
+ return false;
316
+ }
317
+
318
+ const timeoutMs = 180000;
319
+ let content = fs.readFileSync(wrapperPath, 'utf8');
320
+ const match = content.match(/^networkTimeout=(\d+)\s*$/m);
321
+
322
+ if (match && Number(match[1]) >= timeoutMs) {
323
+ return false;
324
+ }
325
+
326
+ if (match) {
327
+ content = content.replace(
328
+ /^networkTimeout=\d+\s*$/m,
329
+ `networkTimeout=${timeoutMs}`,
330
+ );
331
+ } else if (content.includes('distributionUrl=')) {
332
+ content = content.replace(
333
+ /^(distributionUrl=.*)$/m,
334
+ `$1\nnetworkTimeout=${timeoutMs}`,
335
+ );
336
+ } else {
337
+ content = `${content.replace(/\s*$/, '')}\nnetworkTimeout=${timeoutMs}\n`;
338
+ }
339
+
340
+ fs.writeFileSync(wrapperPath, content, 'utf8');
341
+ console.log('✅ Patched gradle-wrapper.properties (networkTimeout)');
342
+ return true;
343
+ }
344
+
345
+ /**
346
+ * Draw behind system bars. Screens already use safe area insets.
347
+ */
348
+ function patchGradleProperties(projectDir) {
349
+ const propsPath = path.join(projectDir, 'android', 'gradle.properties');
350
+ if (!fs.existsSync(propsPath)) {
351
+ return false;
352
+ }
353
+
354
+ let content = fs.readFileSync(propsPath, 'utf8');
355
+ if (!content.includes('edgeToEdgeEnabled=false')) {
356
+ return false;
357
+ }
358
+
359
+ content = content.replace('edgeToEdgeEnabled=false', 'edgeToEdgeEnabled=true');
360
+ fs.writeFileSync(propsPath, content, 'utf8');
361
+ console.log('✅ Patched android/gradle.properties (edgeToEdgeEnabled)');
362
+ return true;
363
+ }
364
+
365
+ /**
366
+ * Run all native patches
367
+ */
368
+ function patchAllNative(projectDir) {
369
+ patchAndroidBuildGradle(projectDir);
370
+ patchAndroidMainActivity(projectDir);
371
+ patchAndroidManifest(projectDir);
372
+ patchGradleWrapper(projectDir);
373
+ patchGradleProperties(projectDir);
374
+ patchIOSInfoPlist(projectDir);
375
+ patchIOSPodfile(projectDir);
376
+ }
377
+
378
+ module.exports = {
379
+ patchAllNative,
380
+ patchAndroidBuildGradle,
381
+ patchAndroidMainActivity,
382
+ patchAndroidManifest,
383
+ patchGradleWrapper,
384
+ patchGradleProperties,
385
+ patchIOSInfoPlist,
386
+ patchIOSPodfile,
387
+ };
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "create-react-native-starter-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Create a React Native CLI app with a ready folder structure, shared components, theming, i18n, API layer, offline handling and native setup done for you",
5
+ "main": "bin/create-react-native-starter-kit.js",
6
+ "bin": {
7
+ "create-react-native-starter-kit": "./bin/create-react-native-starter-kit.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "scripts",
13
+ "template",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "sync-template": "node scripts/sync-template.js",
19
+ "test": "node test-patchers.js",
20
+ "prepack": "node scripts/sync-template.js"
21
+ },
22
+ "keywords": [
23
+ "react-native",
24
+ "react-native-cli",
25
+ "starter",
26
+ "boilerplate",
27
+ "generator",
28
+ "scaffolder",
29
+ "template",
30
+ "starter-kit",
31
+ "typescript",
32
+ "react-navigation",
33
+ "redux-toolkit",
34
+ "tanstack-query"
35
+ ],
36
+ "author": "Satyam Narayan (https://github.com/satyam-narayan)",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/satyam-narayan/create-react-native-starter-kit.git"
41
+ },
42
+ "homepage": "https://github.com/satyam-narayan/create-react-native-starter-kit#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/satyam-narayan/create-react-native-starter-kit/issues"
45
+ },
46
+ "engines": {
47
+ "node": ">=22.13.0"
48
+ }
49
+ }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Refreshes cli/template from the starter repo that contains this cli folder.
5
+ * Runs automatically before `npm pack` / `npm publish`.
6
+ * When the cli folder is used on its own (no parent repo), the existing template is kept.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { copyDirectoryRecursive } = require('../lib/copy-src');
12
+
13
+ const CLI_ROOT = path.resolve(__dirname, '..');
14
+ const REPO_ROOT = path.resolve(CLI_ROOT, '..');
15
+ const TEMPLATE_DIR = path.join(CLI_ROOT, 'template');
16
+
17
+ const DIRECTORIES = ['src', 'patches'];
18
+ const FILES = ['.env.example'];
19
+
20
+ function main() {
21
+ if (!fs.existsSync(path.join(REPO_ROOT, 'src'))) {
22
+ if (!fs.existsSync(path.join(TEMPLATE_DIR, 'src'))) {
23
+ console.error('❌ No starter src/ found next to cli/ and cli/template is empty.');
24
+ process.exit(1);
25
+ }
26
+ console.log('ℹ️ Starter repo not found next to cli/. Keeping the existing template.');
27
+ return;
28
+ }
29
+
30
+ fs.rmSync(TEMPLATE_DIR, { recursive: true, force: true });
31
+ fs.mkdirSync(TEMPLATE_DIR, { recursive: true });
32
+
33
+ for (const dir of DIRECTORIES) {
34
+ const from = path.join(REPO_ROOT, dir);
35
+ if (fs.existsSync(from)) {
36
+ copyDirectoryRecursive(from, path.join(TEMPLATE_DIR, dir));
37
+ console.log(`✅ template/${dir}`);
38
+ }
39
+ }
40
+
41
+ for (const file of FILES) {
42
+ const from = path.join(REPO_ROOT, file);
43
+ if (fs.existsSync(from)) {
44
+ fs.copyFileSync(from, path.join(TEMPLATE_DIR, file));
45
+ console.log(`✅ template/${file}`);
46
+ }
47
+ }
48
+
49
+ console.log('🎉 cli/template is up to date.');
50
+ }
51
+
52
+ main();
@@ -0,0 +1,3 @@
1
+ # Leave API_BASE_URL unset to use the built-in mock API (src/services/api/mock).
2
+ # Set it to your backend URL to call the real server.
3
+ # API_BASE_URL=https://api.example.com
@@ -0,0 +1,67 @@
1
+ diff --git a/node_modules/react-native-element-dropdown/lib/commonjs/components/Dropdown/index.js b/node_modules/react-native-element-dropdown/lib/commonjs/components/Dropdown/index.js
2
+ index 3166ef9..cdbc26d 100644
3
+ --- a/node_modules/react-native-element-dropdown/lib/commonjs/components/Dropdown/index.js
4
+ +++ b/node_modules/react-native-element-dropdown/lib/commonjs/components/Dropdown/index.js
5
+ @@ -167,14 +167,14 @@ const DropdownComponent = /*#__PURE__*/_react.default.forwardRef((props, current
6
+ if (mode === 'auto') {
7
+ isFull = false;
8
+ }
9
+ - const top = isFull ? 20 : height + pageY + 2;
10
+ + const top = isFull ? 20 : height + pageY;
11
+ const bottom = H - top + height;
12
+ const left = _reactNative.I18nManager.isRTL ? W - width - pageX : pageX;
13
+ setPosition({
14
+ isFull,
15
+ width: Math.floor(width),
16
+ - top: Math.floor(top + statusBarHeight),
17
+ - bottom: Math.floor(bottom - statusBarHeight),
18
+ + top: Math.floor(top),
19
+ + bottom: Math.floor(bottom),
20
+ left: Math.floor(left),
21
+ height: Math.floor(height)
22
+ });
23
+ diff --git a/node_modules/react-native-element-dropdown/lib/module/components/Dropdown/index.js b/node_modules/react-native-element-dropdown/lib/module/components/Dropdown/index.js
24
+ index 914d4d1..213f70b 100644
25
+ --- a/node_modules/react-native-element-dropdown/lib/module/components/Dropdown/index.js
26
+ +++ b/node_modules/react-native-element-dropdown/lib/module/components/Dropdown/index.js
27
+ @@ -159,14 +159,14 @@ const DropdownComponent = /*#__PURE__*/React.forwardRef((props, currentRef) => {
28
+ if (mode === 'auto') {
29
+ isFull = false;
30
+ }
31
+ - const top = isFull ? 20 : height + pageY + 2;
32
+ + const top = isFull ? 20 : height + pageY;
33
+ const bottom = H - top + height;
34
+ const left = I18nManager.isRTL ? W - width - pageX : pageX;
35
+ setPosition({
36
+ isFull,
37
+ width: Math.floor(width),
38
+ - top: Math.floor(top + statusBarHeight),
39
+ - bottom: Math.floor(bottom - statusBarHeight),
40
+ + top: Math.floor(top),
41
+ + bottom: Math.floor(bottom),
42
+ left: Math.floor(left),
43
+ height: Math.floor(height)
44
+ });
45
+ diff --git a/node_modules/react-native-element-dropdown/src/components/Dropdown/index.tsx b/node_modules/react-native-element-dropdown/src/components/Dropdown/index.tsx
46
+ index 2a73f2f..55d0da5 100644
47
+ --- a/node_modules/react-native-element-dropdown/src/components/Dropdown/index.tsx
48
+ +++ b/node_modules/react-native-element-dropdown/src/components/Dropdown/index.tsx
49
+ @@ -201,15 +201,15 @@ const DropdownComponent = React.forwardRef<IDropdownRef, DropdownProps<any>>(
50
+ isFull = false;
51
+ }
52
+
53
+ - const top = isFull ? 20 : height + pageY + 2;
54
+ + const top = isFull ? 20 : height + pageY;
55
+ const bottom = H - top + height;
56
+ const left = I18nManager.isRTL ? W - width - pageX : pageX;
57
+
58
+ setPosition({
59
+ isFull,
60
+ width: Math.floor(width),
61
+ - top: Math.floor(top + statusBarHeight),
62
+ - bottom: Math.floor(bottom - statusBarHeight),
63
+ + top: Math.floor(top),
64
+ + bottom: Math.floor(bottom),
65
+ left: Math.floor(left),
66
+ height: Math.floor(height),
67
+ });
@@ -0,0 +1,25 @@
1
+ import { StatusBar } from 'react-native';
2
+ import { AppProviders } from './providers';
3
+ import { RootNavigator } from './navigation/root';
4
+ import '@/i18n';
5
+ import Toast from 'react-native-toast-message';
6
+ import { useAppTheme } from '@/context/ThemeContext';
7
+ import { toastConfig } from '@/services/toast';
8
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
9
+
10
+ const App = () => {
11
+ const { theme, mode } = useAppTheme();
12
+ const isDark = mode === 'dark';
13
+
14
+ return (
15
+ <GestureHandlerRootView>
16
+ <AppProviders>
17
+ <StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} />
18
+ <RootNavigator />
19
+ <Toast config={toastConfig} />
20
+ </AppProviders>
21
+ </GestureHandlerRootView>
22
+ );
23
+ };
24
+
25
+ export default App;
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { NavigationContainer } from '@react-navigation/native';
3
+ import { navigationRef } from '@/types';
4
+ import { useAppSelector } from '@/hooks/useAppSelector';
5
+ import { selectIsAuthenticated } from '@/redux/selectors/auth.selectors';
6
+ import AppStack from './stacks/app.stack';
7
+ import AuthStack from './stacks/auth.stack';
8
+ import GlobalLoader from '@/components/shared/GlobalLoader';
9
+
10
+ interface RootNavigatorProps {
11
+ onReady?: () => void;
12
+ }
13
+
14
+ export const RootNavigator: React.FC<RootNavigatorProps> = ({ onReady }) => {
15
+ const isAuthenticated = useAppSelector(selectIsAuthenticated);
16
+ return (
17
+ <>
18
+ <NavigationContainer ref={navigationRef} onReady={onReady}>
19
+ {isAuthenticated ? <AppStack /> : <AuthStack />}
20
+ </NavigationContainer>
21
+ <GlobalLoader />
22
+ </>
23
+ );
24
+ };
@@ -0,0 +1,67 @@
1
+ import { useAppTheme } from '@/context/ThemeContext';
2
+ import MainTabs from '@/screens/app/main-tab';
3
+ import ProfileSetup from '@/screens/app/profile-setup';
4
+ import { FontFamily } from '@/theme';
5
+ import { layout } from '@/theme/layout';
6
+ import { AppStackParamList } from '@/types';
7
+ import { createNativeStackNavigator } from '@react-navigation/native-stack';
8
+ import AppBottomTabs from './app.tab';
9
+ import { useAppSelector } from '@/hooks/useAppSelector';
10
+ import { StyleSheet } from 'react-native';
11
+
12
+ const Stack = createNativeStackNavigator<AppStackParamList>();
13
+
14
+ const AppStack = () => {
15
+ const { color } = useAppTheme();
16
+ const { isProfileSetup } = useAppSelector(state => state.user);
17
+ return (
18
+ <Stack.Navigator
19
+ initialRouteName={isProfileSetup ? 'BottomTabs' : 'ProfileSetup'}
20
+ screenOptions={{
21
+ headerTitleAlign: 'center',
22
+ headerShown: true,
23
+ headerTitleStyle: [
24
+ styles.headerTitle,
25
+ { color: color.text_primary },
26
+ ],
27
+ contentStyle: [
28
+ { backgroundColor: color.background_primary },
29
+ layout.flex,
30
+ ],
31
+ headerStyle: { backgroundColor: color.background_secondary },
32
+ headerBackVisible: false,
33
+ animation: 'slide_from_right',
34
+ // headerShadowVisible: false,
35
+ }}
36
+ >
37
+ <Stack.Screen
38
+ name="ProfileSetup"
39
+ component={ProfileSetup}
40
+ options={{
41
+ headerTitle: 'Profile Setup',
42
+ }}
43
+ />
44
+ <Stack.Screen
45
+ component={AppBottomTabs}
46
+ name="BottomTabs"
47
+ options={{ headerShown: false }}
48
+ />
49
+ <Stack.Screen
50
+ name="MainTabs"
51
+ component={MainTabs}
52
+ options={{
53
+ headerTitle: 'Main Tab',
54
+ }}
55
+ />
56
+ </Stack.Navigator>
57
+ );
58
+ };
59
+
60
+ export default AppStack;
61
+
62
+ const styles = StyleSheet.create({
63
+ headerTitle: {
64
+ fontFamily: FontFamily.InterTightMedium,
65
+ fontSize: 20,
66
+ },
67
+ });