create-react-native-starter-kit 0.0.0-stage → 1.0.1

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 (211) 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 +588 -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 +35 -0
  48. package/template/src/assets/icons/log_out.svg +4 -0
  49. package/template/src/assets/icons/setting.svg +4 -0
  50. package/template/src/assets/icons/tabs/home/filled_dark.svg +3 -0
  51. package/template/src/assets/icons/tabs/home/filled_light.svg +3 -0
  52. package/template/src/assets/icons/tabs/home/outline.svg +4 -0
  53. package/template/src/assets/icons/tabs/index.ts +31 -0
  54. package/template/src/assets/icons/tabs/profile/filled_dark.svg +6 -0
  55. package/template/src/assets/icons/tabs/profile/filled_light.svg +6 -0
  56. package/template/src/assets/icons/tabs/profile/outline.svg +3 -0
  57. package/template/src/assets/icons/tooltip_dark.svg +1 -0
  58. package/template/src/assets/icons/tooltip_light.svg +1 -0
  59. package/template/src/assets/icons/upArrow.svg +3 -0
  60. package/template/src/assets/images/back.png +0 -0
  61. package/template/src/assets/images/index.ts +7 -0
  62. package/template/src/assets/images/placeholder/profile.png +0 -0
  63. package/template/src/components/shared/AppImage.tsx +190 -0
  64. package/template/src/components/shared/AvatarPicker.tsx +175 -0
  65. package/template/src/components/shared/CustomButton.tsx +273 -0
  66. package/template/src/components/shared/DocumentPicker.tsx +86 -0
  67. package/template/src/components/shared/GlobalLoader.tsx +47 -0
  68. package/template/src/components/shared/Icon.tsx +28 -0
  69. package/template/src/components/shared/PressableIcon.tsx +104 -0
  70. package/template/src/components/shared/bottom-sheet/BaseBottomSheet.tsx +30 -0
  71. package/template/src/components/shared/bottom-sheet/BaseBottomSheetModal.tsx +50 -0
  72. package/template/src/components/shared/bottom-sheet/sheets/ImagePickerSheet.tsx +310 -0
  73. package/template/src/components/shared/bottom-sheet/types.ts +4 -0
  74. package/template/src/components/shared/form-input/FormInput.tsx +140 -0
  75. package/template/src/components/shared/form-input/FormInputRenderer.tsx +82 -0
  76. package/template/src/components/shared/form-input/FullNameInput.tsx +243 -0
  77. package/template/src/components/shared/form-input/formFieldStyles.ts +43 -0
  78. package/template/src/components/shared/form-input/type.ts +188 -0
  79. package/template/src/components/shared/form-input/variants/CountryPhoneVariant.tsx +289 -0
  80. package/template/src/components/shared/form-input/variants/DateTimeVariant.tsx +280 -0
  81. package/template/src/components/shared/form-input/variants/DropdownVariant.tsx +221 -0
  82. package/template/src/components/shared/form-input/variants/TextInputVariant.tsx +300 -0
  83. package/template/src/components/shared/form-input/variants/ToggleVariant.tsx +194 -0
  84. package/template/src/components/shared/modal/BaseModal.tsx +83 -0
  85. package/template/src/components/shared/modal/common/AlertModal.tsx +173 -0
  86. package/template/src/components/shared/navigation/AnimatedTabIcon.tsx +37 -0
  87. package/template/src/components/shared/navigation/HeaderActionButton.tsx +70 -0
  88. package/template/src/components/shared/navigation/HeaderBackButton.tsx +38 -0
  89. package/template/src/components/shared/navigation/StackHeader.tsx +105 -0
  90. package/template/src/components/shared/navigation/tabIconRender.tsx +14 -0
  91. package/template/src/components/shared/offline/OfflineQueryBoundary.tsx +25 -0
  92. package/template/src/components/shared/offline/OfflineState.tsx +229 -0
  93. package/template/src/components/shared/offline/index.ts +6 -0
  94. package/template/src/components/shared/offline/types.ts +15 -0
  95. package/template/src/components/shared/offline/wifi-icon-animation/BrokenWifiAnimation.tsx +116 -0
  96. package/template/src/components/shared/offline/wifi-icon-animation/WifiPiece.tsx +80 -0
  97. package/template/src/components/shared/offline/wifi-icon-animation/geometry.ts +163 -0
  98. package/template/src/components/shared/offline/wifi-icon-animation/useWifiIconAnimationActive.ts +37 -0
  99. package/template/src/components/shared/skelton/BaseSkelton.tsx +71 -0
  100. package/template/src/components/shared/skelton/ShimmerHolder.ts +6 -0
  101. package/template/src/components/shared/skelton/holders/HomeShimmer.tsx +25 -0
  102. package/template/src/components/shared/skelton/holders/index.ts +1 -0
  103. package/template/src/components/shared/skelton/layouts/avatar.layout.ts +18 -0
  104. package/template/src/components/shared/skelton/layouts/home.layout.ts +16 -0
  105. package/template/src/components/shared/skelton/layouts/index.ts +3 -0
  106. package/template/src/components/shared/skelton/layouts/text.layout.ts +18 -0
  107. package/template/src/components/shared/text/CustomText.tsx +85 -0
  108. package/template/src/components/shared/text/ShimmerText.tsx +159 -0
  109. package/template/src/components/shared/text/types.ts +40 -0
  110. package/template/src/constants/device.ts +12 -0
  111. package/template/src/constants/endpoint.ts +11 -0
  112. package/template/src/constants/keyExtractor.ts +6 -0
  113. package/template/src/constants/limits.ts +9 -0
  114. package/template/src/constants/queryKey.ts +3 -0
  115. package/template/src/constants/regex.ts +7 -0
  116. package/template/src/context/NetworkContext.tsx +102 -0
  117. package/template/src/context/ThemeContext.tsx +56 -0
  118. package/template/src/hooks/app/index.ts +2 -0
  119. package/template/src/hooks/app/useGetProfile.ts +38 -0
  120. package/template/src/hooks/app/useUpdateProfile.ts +21 -0
  121. package/template/src/hooks/useAppDispatch.ts +6 -0
  122. package/template/src/hooks/useAppSelector.ts +4 -0
  123. package/template/src/hooks/useDebounce.ts +15 -0
  124. package/template/src/hooks/useDocumentPicker.ts +102 -0
  125. package/template/src/hooks/useImagePicker.ts +83 -0
  126. package/template/src/i18n/i18next.d.ts +9 -0
  127. package/template/src/i18n/index.ts +16 -0
  128. package/template/src/i18n/locales/en.json +274 -0
  129. package/template/src/i18n/locales/ru.json +165 -0
  130. package/template/src/i18n/resources.ts +11 -0
  131. package/template/src/redux/selectors/auth.selectors.ts +4 -0
  132. package/template/src/redux/slice/auth.slice.ts +23 -0
  133. package/template/src/redux/slice/loader.slice.ts +25 -0
  134. package/template/src/redux/slice/settings.slice.ts +26 -0
  135. package/template/src/redux/slice/user.slice.ts +25 -0
  136. package/template/src/screens/app/main-tab/index.tsx +73 -0
  137. package/template/src/screens/app/main-tab/style.ts +0 -0
  138. package/template/src/screens/app/profile-setup/index.tsx +162 -0
  139. package/template/src/screens/app/profile-setup/styles.ts +3 -0
  140. package/template/src/screens/auth/forgot-password/index.tsx +59 -0
  141. package/template/src/screens/auth/forgot-password/styles.ts +0 -0
  142. package/template/src/screens/auth/index.ts +4 -0
  143. package/template/src/screens/auth/login/index.tsx +138 -0
  144. package/template/src/screens/auth/login/styles.ts +3 -0
  145. package/template/src/screens/auth/signup/index.tsx +111 -0
  146. package/template/src/screens/auth/signup/styles.ts +3 -0
  147. package/template/src/screens/auth/verify-email/index.tsx +15 -0
  148. package/template/src/screens/auth/verify-email/styles.ts +3 -0
  149. package/template/src/screens/tab/home/index.tsx +49 -0
  150. package/template/src/screens/tab/home/styles.ts +3 -0
  151. package/template/src/screens/tab/index.ts +2 -0
  152. package/template/src/screens/tab/profile/index.tsx +50 -0
  153. package/template/src/screens/tab/profile/styles.ts +3 -0
  154. package/template/src/services/api/auth/auth.api.ts +22 -0
  155. package/template/src/services/api/auth/auth.endpoints.ts +7 -0
  156. package/template/src/services/api/auth/auth.types.ts +9 -0
  157. package/template/src/services/api/auth/index.ts +3 -0
  158. package/template/src/services/api/axios/axios.instance.ts +15 -0
  159. package/template/src/services/api/axios/axios.interceptors.ts +43 -0
  160. package/template/src/services/api/axios/index.ts +2 -0
  161. package/template/src/services/api/mock/mockAdapter.ts +97 -0
  162. package/template/src/services/api/user/index.ts +2 -0
  163. package/template/src/services/api/user/user.api.ts +67 -0
  164. package/template/src/services/api/user/user.endpoints.ts +0 -0
  165. package/template/src/services/api/user/user.types.ts +0 -0
  166. package/template/src/services/errors/backendErrorCodes.ts +7 -0
  167. package/template/src/services/errors/errorHandler.ts +94 -0
  168. package/template/src/services/errors/firebaseErrorMap.ts +61 -0
  169. package/template/src/services/errors/httpStatusMap.ts +47 -0
  170. package/template/src/services/errors/index.ts +4 -0
  171. package/template/src/services/network/index.ts +1 -0
  172. package/template/src/services/network/onlineManager.ts +22 -0
  173. package/template/src/services/query/index.ts +1 -0
  174. package/template/src/services/query/queryClient.ts +23 -0
  175. package/template/src/services/toast/ToastCard.tsx +137 -0
  176. package/template/src/services/toast/ToastConfig.tsx +32 -0
  177. package/template/src/services/toast/index.ts +3 -0
  178. package/template/src/services/toast/toast.ts +68 -0
  179. package/template/src/storage/mmkvAdapter.ts +3 -0
  180. package/template/src/storage/mmkvInstance.ts +16 -0
  181. package/template/src/store/index.ts +15 -0
  182. package/template/src/store/mmkv.ts +6 -0
  183. package/template/src/store/persist.ts +10 -0
  184. package/template/src/store/rootReducer.ts +26 -0
  185. package/template/src/theme/dark.ts +40 -0
  186. package/template/src/theme/fonts.ts +20 -0
  187. package/template/src/theme/index.ts +6 -0
  188. package/template/src/theme/layout.ts +23 -0
  189. package/template/src/theme/light.ts +41 -0
  190. package/template/src/theme/palette.ts +32 -0
  191. package/template/src/theme/spacing.ts +7 -0
  192. package/template/src/theme/typography.ts +80 -0
  193. package/template/src/types/api.types.ts +34 -0
  194. package/template/src/types/env.d.ts +6 -0
  195. package/template/src/types/index.ts +2 -0
  196. package/template/src/types/navigation.types.ts +94 -0
  197. package/template/src/utils/clearAppSession.ts +10 -0
  198. package/template/src/utils/date.ts +19 -0
  199. package/template/src/utils/documentPickerHandler.ts +96 -0
  200. package/template/src/utils/getFileNameFromUri.ts +18 -0
  201. package/template/src/utils/getInitials.ts +13 -0
  202. package/template/src/utils/hindiText.ts +33 -0
  203. package/template/src/utils/imageCompress.ts +97 -0
  204. package/template/src/utils/imagePickerHandler.ts +200 -0
  205. package/template/src/utils/maskPhone.ts +17 -0
  206. package/template/src/utils/normalize.ts +12 -0
  207. package/template/src/validation/messages.ts +11 -0
  208. package/template/src/validation/rules.ts +44 -0
  209. package/template/src/validation/schema/auth.schema.ts +53 -0
  210. package/template/src/validation/schema/user.schema.ts +46 -0
  211. package/template/src/validation/tValError.ts +13 -0
@@ -0,0 +1,588 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Find files recursively matching a pattern or name
7
+ */
8
+ function findFile(dir, fileName) {
9
+ if (!fs.existsSync(dir)) return null;
10
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
11
+ for (const entry of entries) {
12
+ const fullPath = path.join(dir, entry.name);
13
+ if (entry.isDirectory()) {
14
+ if (entry.name === 'node_modules' || entry.name === 'Pods' || entry.name === 'build') continue;
15
+ const found = findFile(fullPath, fileName);
16
+ if (found) return found;
17
+ } else if (entry.name === fileName) {
18
+ return fullPath;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * Patch android/app/build.gradle
26
+ */
27
+ function patchAndroidBuildGradle(projectDir) {
28
+ const gradlePath = path.join(projectDir, 'android', 'app', 'build.gradle');
29
+ if (!fs.existsSync(gradlePath)) {
30
+ console.log('⚠️ Could not find android/app/build.gradle');
31
+ return false;
32
+ }
33
+
34
+ let content = fs.readFileSync(gradlePath, 'utf8');
35
+
36
+ // Check if androidx.activity is already added
37
+ if (!content.includes('androidx.activity:activity')) {
38
+ const target = 'dependencies {';
39
+ const injection = `dependencies {
40
+ implementation("androidx.activity:activity:1.+") //by own`;
41
+ if (content.includes(target)) {
42
+ content = content.replace(target, injection);
43
+ fs.writeFileSync(gradlePath, content, 'utf8');
44
+ console.log('✅ Patched android/app/build.gradle');
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Patch MainActivity.kt
53
+ */
54
+ function patchAndroidMainActivity(projectDir) {
55
+ const javaDir = path.join(projectDir, 'android', 'app', 'src', 'main', 'java');
56
+ const mainActivityPath = findFile(javaDir, 'MainActivity.kt');
57
+
58
+ if (!mainActivityPath) {
59
+ console.log('⚠️ Could not find MainActivity.kt');
60
+ return false;
61
+ }
62
+
63
+ let content = fs.readFileSync(mainActivityPath, 'utf8');
64
+
65
+ const requiredImports = [
66
+ 'android.os.Bundle',
67
+ 'com.facebook.react.ReactApplication',
68
+ 'com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory',
69
+ ].filter(name => !new RegExp(`^import\\s+${name.replace(/\./g, '\\.')}\\s*$`, 'm').test(content));
70
+
71
+ if (requiredImports.length > 0) {
72
+ const importBlock = `//By Own\n${requiredImports.map(name => `import ${name}`).join('\n')}\n`;
73
+ // Match only the package line itself so the inserted block always ends on its own line.
74
+ const packageLineRegex = /^package\s+[\w.]+[ \t]*\r?\n/m;
75
+ if (packageLineRegex.test(content)) {
76
+ content = content.replace(packageLineRegex, match => `${match}\n${importBlock}`);
77
+ } else {
78
+ content = `${importBlock}\n${content}`;
79
+ }
80
+ }
81
+
82
+ const activityHooks = `
83
+ //By Own
84
+ override fun onCreate(savedInstanceState: Bundle?) {
85
+ supportFragmentManager.fragmentFactory = RNScreensFragmentFactory()
86
+ super.onCreate(savedInstanceState)
87
+ reloadIfDisplaySizeChanged()
88
+ }
89
+
90
+ /**
91
+ * Changing "Display size" recreates this activity but keeps the JS runtime alive, so sizes
92
+ * computed once at JS load (normalize(), screen-width constants) stay stale and layouts break.
93
+ * Reloading the bundle recomputes them for the new density.
94
+ */
95
+ private fun reloadIfDisplaySizeChanged() {
96
+ val densityDpi = resources.configuration.densityDpi
97
+ if (lastDensityDpi != 0 && lastDensityDpi != densityDpi) {
98
+ (application as ReactApplication).reactHost?.reload("Display size changed")
99
+ }
100
+ lastDensityDpi = densityDpi
101
+ }
102
+
103
+ //By Own
104
+ private companion object {
105
+ /** Survives activity recreation (same process); 0 means first launch. */
106
+ var lastDensityDpi = 0
107
+ }
108
+ `;
109
+
110
+ if (!content.includes('reloadIfDisplaySizeChanged')) {
111
+ const classRegex = /(class\s+MainActivity\s*:\s*ReactActivity\(\)\s*\{)/;
112
+ if (classRegex.test(content)) {
113
+ content = content.replace(classRegex, `$1${activityHooks}`);
114
+ }
115
+ }
116
+
117
+ fs.writeFileSync(mainActivityPath, content, 'utf8');
118
+ console.log('✅ Patched MainActivity.kt');
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Patch AndroidManifest.xml
124
+ */
125
+ function patchAndroidManifest(projectDir) {
126
+ const manifestPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
127
+ if (!fs.existsSync(manifestPath)) {
128
+ console.log('⚠️ Could not find AndroidManifest.xml');
129
+ return false;
130
+ }
131
+
132
+ let content = fs.readFileSync(manifestPath, 'utf8');
133
+ let modified = false;
134
+
135
+ if (!content.includes('xmlns:tools')) {
136
+ content = content.replace(
137
+ '<manifest xmlns:android="http://schemas.android.com/apk/res/android"',
138
+ '<manifest xmlns:android="http://schemas.android.com/apk/res/android"\n xmlns:tools="http://schemas.android.com/tools"',
139
+ );
140
+ modified = true;
141
+ }
142
+
143
+ if (!content.includes('android.permission.CAMERA')) {
144
+ const permissionBlock = ` <uses-permission android:name="android.permission.CAMERA" />
145
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
146
+ <uses-permission
147
+ android:name="android.permission.READ_EXTERNAL_STORAGE"
148
+ android:maxSdkVersion="32" />
149
+ <!-- RN debug adds this for Metro over LAN; use adb reverse instead. -->
150
+ <uses-permission
151
+ android:name="android.permission.ACCESS_LOCAL_NETWORK"
152
+ tools:node="remove" />
153
+
154
+ `;
155
+ if (content.includes('<application')) {
156
+ content = content.replace('<application', `${permissionBlock}<application`);
157
+ modified = true;
158
+ }
159
+ }
160
+
161
+ if (!content.includes('enableOnBackInvokedCallback')) {
162
+ content = content.replace(
163
+ '<application',
164
+ '<application\n android:enableOnBackInvokedCallback="false"',
165
+ );
166
+ modified = true;
167
+ }
168
+
169
+ if (
170
+ content.includes('<activity') &&
171
+ !content.includes('android:screenOrientation=')
172
+ ) {
173
+ content = content.replace(
174
+ '<activity',
175
+ '<activity\n android:screenOrientation="portrait"',
176
+ );
177
+ modified = true;
178
+ }
179
+
180
+ if (modified) {
181
+ fs.writeFileSync(manifestPath, content, 'utf8');
182
+ console.log('✅ Patched AndroidManifest.xml');
183
+ }
184
+ return modified;
185
+ }
186
+
187
+ /**
188
+ * Get all font files from src/assets/fonts
189
+ */
190
+ function getFontFiles(projectDir) {
191
+ const fontsDir = path.join(projectDir, 'src', 'assets', 'fonts');
192
+ if (!fs.existsSync(fontsDir)) return [];
193
+ return fs.readdirSync(fontsDir).filter(f => f.endsWith('.ttf') || f.endsWith('.otf'));
194
+ }
195
+
196
+ /**
197
+ * Patch iOS Info.plist
198
+ */
199
+ function patchIOSInfoPlist(projectDir) {
200
+ const iosDir = path.join(projectDir, 'ios');
201
+ const infoPlistPath = findFile(iosDir, 'Info.plist');
202
+
203
+ if (!infoPlistPath) {
204
+ console.log('⚠️ Could not find iOS Info.plist');
205
+ return false;
206
+ }
207
+
208
+ let content = fs.readFileSync(infoPlistPath, 'utf8');
209
+ let modified = false;
210
+
211
+ // Add Photo & Camera Permissions
212
+ const permissionsToAdd = [];
213
+ if (!content.includes('NSPhotoLibraryUsageDescription')) {
214
+ permissionsToAdd.push(`\t<key>NSPhotoLibraryUsageDescription</key>
215
+ \t<string>We need access to your photo gallery to update your profile picture.</string>`);
216
+ }
217
+ if (!content.includes('NSCameraUsageDescription')) {
218
+ permissionsToAdd.push(`\t<key>NSCameraUsageDescription</key>
219
+ \t<string>We need access to your camera to take photos for your profile picture.</string>`);
220
+ }
221
+
222
+ // Add UIAppFonts
223
+ const fonts = getFontFiles(projectDir);
224
+ if (fonts.length > 0 && !content.includes('<key>UIAppFonts</key>')) {
225
+ const fontStrings = fonts.map(font => `\t\t<string>${font}</string>`).join('\n');
226
+ permissionsToAdd.push(`\t<key>UIAppFonts</key>
227
+ \t<array>
228
+ ${fontStrings}
229
+ \t</array>`);
230
+ }
231
+
232
+ if (content.includes('UIInterfaceOrientationLandscapeLeft')) {
233
+ content = content.replace(
234
+ /<key>UISupportedInterfaceOrientations<\/key>\s*<array>[\s\S]*?<\/array>/,
235
+ `<key>UISupportedInterfaceOrientations</key>
236
+ <array>
237
+ <string>UIInterfaceOrientationPortrait</string>
238
+ </array>`,
239
+ );
240
+ modified = true;
241
+ }
242
+
243
+ if (permissionsToAdd.length > 0) {
244
+ const insertion = permissionsToAdd.join('\n') + '\n</dict>';
245
+ content = content.replace(/<\/dict>\s*<\/plist>/, `${insertion}\n</plist>`);
246
+ modified = true;
247
+ }
248
+
249
+ if (modified) {
250
+ fs.writeFileSync(infoPlistPath, content, 'utf8');
251
+ console.log('✅ Patched iOS Info.plist with permissions and custom fonts');
252
+ }
253
+
254
+ return modified;
255
+ }
256
+
257
+ const SCENE_MANIFEST = `\t<key>UIApplicationSceneManifest</key>
258
+ \t<dict>
259
+ \t\t<key>UIApplicationSupportsMultipleScenes</key>
260
+ \t\t<false/>
261
+ \t\t<key>UISceneConfigurations</key>
262
+ \t\t<dict>
263
+ \t\t\t<key>UIWindowSceneSessionRoleApplication</key>
264
+ \t\t\t<array>
265
+ \t\t\t\t<dict>
266
+ \t\t\t\t\t<key>UISceneConfigurationName</key>
267
+ \t\t\t\t\t<string>Default Configuration</string>
268
+ \t\t\t\t\t<key>UISceneDelegateClassName</key>
269
+ \t\t\t\t\t<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
270
+ \t\t\t\t</dict>
271
+ \t\t\t</array>
272
+ \t\t</dict>
273
+ \t</dict>`;
274
+
275
+ // Same shape as the current react-native-community/template, limited to APIs that exist in
276
+ // React Native 0.87 (no connectionOptions overload and no scene-based RCTLinkingManager helpers).
277
+ const APP_DELEGATE_SOURCE = `import UIKit
278
+
279
+ @main
280
+ class AppDelegate: UIResponder, UIApplicationDelegate {
281
+ func application(
282
+ _ application: UIApplication,
283
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
284
+ ) -> Bool {
285
+ true
286
+ }
287
+ }
288
+ `;
289
+
290
+ const sceneDelegateSource = moduleName => `import React
291
+ import React_RCTAppDelegate
292
+ import ReactAppDependencyProvider
293
+ import UIKit
294
+
295
+ class SceneDelegate: RCTDefaultReactNativeFactoryDelegate, UIWindowSceneDelegate {
296
+ var window: UIWindow?
297
+ var reactNativeFactory: RCTReactNativeFactory?
298
+
299
+ func scene(
300
+ _ scene: UIScene,
301
+ willConnectTo session: UISceneSession,
302
+ options connectionOptions: UIScene.ConnectionOptions
303
+ ) {
304
+ guard let windowScene = scene as? UIWindowScene else {
305
+ return
306
+ }
307
+
308
+ dependencyProvider = RCTAppDependencyProvider()
309
+ reactNativeFactory = RCTReactNativeFactory(delegate: self)
310
+ window = UIWindow(windowScene: windowScene)
311
+
312
+ var launchOptions: [AnyHashable: Any] = [:]
313
+ if let url = connectionOptions.urlContexts.first?.url {
314
+ launchOptions[UIApplication.LaunchOptionsKey.url] = url
315
+ }
316
+
317
+ reactNativeFactory?.startReactNative(
318
+ withModuleName: "${moduleName}",
319
+ in: window,
320
+ launchOptions: launchOptions
321
+ )
322
+ }
323
+
324
+ func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
325
+ guard let url = URLContexts.first?.url else {
326
+ return
327
+ }
328
+ RCTLinkingManager.application(UIApplication.shared, open: url, options: [:])
329
+ }
330
+
331
+ func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
332
+ RCTLinkingManager.application(
333
+ UIApplication.shared,
334
+ continue: userActivity,
335
+ restorationHandler: { _ in }
336
+ )
337
+ }
338
+
339
+ override func sourceURL(for bridge: RCTBridge) -> URL? {
340
+ self.bundleURL()
341
+ }
342
+
343
+ override func bundleURL() -> URL? {
344
+ #if DEBUG
345
+ RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
346
+ #else
347
+ Bundle.main.url(forResource: "main", withExtension: "jsbundle")
348
+ #endif
349
+ }
350
+ }
351
+ `;
352
+
353
+ /** Xcode object IDs are 24 uppercase hex chars and only need to be unique within the pbxproj. */
354
+ function newPbxprojId(pbxproj, taken = []) {
355
+ for (;;) {
356
+ const id = crypto.randomBytes(12).toString('hex').toUpperCase();
357
+ if (!pbxproj.includes(id) && !taken.includes(id)) return id;
358
+ }
359
+ }
360
+
361
+ function findXcodeProject(iosDir) {
362
+ if (!fs.existsSync(iosDir)) return null;
363
+ const projectDir = fs
364
+ .readdirSync(iosDir)
365
+ .find(entry => entry.endsWith('.xcodeproj') && entry !== 'Pods.xcodeproj');
366
+ if (!projectDir) return null;
367
+ const pbxprojPath = path.join(iosDir, projectDir, 'project.pbxproj');
368
+ return fs.existsSync(pbxprojPath) ? pbxprojPath : null;
369
+ }
370
+
371
+ /**
372
+ * Add SceneDelegate.swift next to every AppDelegate.swift entry (file reference, group, Sources
373
+ * phase). Returns null when the project does not have the layout the template generates.
374
+ */
375
+ function addSceneDelegateToPbxproj(pbxproj, appFolder) {
376
+ if (pbxproj.includes('SceneDelegate.swift')) return pbxproj;
377
+
378
+ const buildFileLine = /^([ \t]*)(\w+) \/\* AppDelegate\.swift in Sources \*\/ = \{isa = PBXBuildFile; fileRef = (\w+) \/\* AppDelegate\.swift \*\/; \};$/m;
379
+ const buildFile = pbxproj.match(buildFileLine);
380
+ if (!buildFile) return null;
381
+ const [, indent, appBuildId, appRefId] = buildFile;
382
+
383
+ const fileRefLine = new RegExp(`^[ \\t]*${appRefId} /\\* AppDelegate\\.swift \\*/ = \\{isa = PBXFileReference;.*\\};$`, 'm');
384
+ const groupChild = new RegExp(`^([ \\t]*)${appRefId} /\\* AppDelegate\\.swift \\*/,$`, 'm');
385
+ const sourcesEntry = new RegExp(`^([ \\t]*)${appBuildId} /\\* AppDelegate\\.swift in Sources \\*/,$`, 'm');
386
+ if (!fileRefLine.test(pbxproj) || !groupChild.test(pbxproj) || !sourcesEntry.test(pbxproj)) {
387
+ return null;
388
+ }
389
+
390
+ const sceneFileRefId = newPbxprojId(pbxproj);
391
+ const sceneBuildFileId = newPbxprojId(pbxproj, [sceneFileRefId]);
392
+
393
+ return pbxproj
394
+ .replace(
395
+ buildFileLine,
396
+ match =>
397
+ `${match}\n${indent}${sceneBuildFileId} /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ${sceneFileRefId} /* SceneDelegate.swift */; };`,
398
+ )
399
+ .replace(
400
+ fileRefLine,
401
+ match =>
402
+ `${match}\n${indent}${sceneFileRefId} /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = ${appFolder}/SceneDelegate.swift; sourceTree = "<group>"; };`,
403
+ )
404
+ .replace(groupChild, (match, childIndent) => `${match}\n${childIndent}${sceneFileRefId} /* SceneDelegate.swift */,`)
405
+ .replace(
406
+ sourcesEntry,
407
+ (match, entryIndent) => `${match}\n${entryIndent}${sceneBuildFileId} /* SceneDelegate.swift in Sources */,`,
408
+ );
409
+ }
410
+
411
+ /**
412
+ * Bring an AppDelegate-only iOS project (React Native <= 0.87) to the scene-based layout of the
413
+ * current template: SceneDelegate.swift owns the window, AppDelegate is empty, Info.plist declares
414
+ * the scene. Newer projects that already ship SceneDelegate.swift are left as generated.
415
+ */
416
+ function patchIOSSceneLifecycle(projectDir) {
417
+ const iosDir = path.join(projectDir, 'ios');
418
+ const appDelegatePath = findFile(iosDir, 'AppDelegate.swift');
419
+ if (!appDelegatePath) {
420
+ console.log('⚠️ Could not find AppDelegate.swift, skipped UIScene setup');
421
+ return false;
422
+ }
423
+
424
+ const appDir = path.dirname(appDelegatePath);
425
+ const sceneDelegatePath = path.join(appDir, 'SceneDelegate.swift');
426
+ if (fs.existsSync(sceneDelegatePath)) {
427
+ return false;
428
+ }
429
+
430
+ const appDelegate = fs.readFileSync(appDelegatePath, 'utf8');
431
+ const moduleName = appDelegate.match(/withModuleName:\s*"([^"]+)"/);
432
+ const infoPlistPath = path.join(appDir, 'Info.plist');
433
+ const pbxprojPath = findXcodeProject(iosDir);
434
+ const pbxproj = pbxprojPath && addSceneDelegateToPbxproj(fs.readFileSync(pbxprojPath, 'utf8'), path.basename(appDir));
435
+
436
+ if (!moduleName || !appDelegate.includes('RCTReactNativeFactory') || !fs.existsSync(infoPlistPath) || !pbxproj) {
437
+ console.log('⚠️ iOS project does not match the React Native template, skipped UIScene setup');
438
+ return false;
439
+ }
440
+
441
+ let infoPlist = fs.readFileSync(infoPlistPath, 'utf8');
442
+ if (!infoPlist.includes('UIApplicationSceneManifest')) {
443
+ infoPlist = infoPlist.replace(/<\/dict>\s*<\/plist>\s*$/, `${SCENE_MANIFEST}\n</dict>\n</plist>\n`);
444
+ }
445
+
446
+ fs.writeFileSync(sceneDelegatePath, sceneDelegateSource(moduleName[1]), 'utf8');
447
+ fs.writeFileSync(appDelegatePath, APP_DELEGATE_SOURCE, 'utf8');
448
+ fs.writeFileSync(infoPlistPath, infoPlist, 'utf8');
449
+ fs.writeFileSync(pbxprojPath, pbxproj, 'utf8');
450
+
451
+ console.log('✅ Moved iOS app startup to SceneDelegate.swift (UIScene life cycle)');
452
+ return true;
453
+ }
454
+
455
+ /**
456
+ * Keep CocoaPods on the same iOS minimum as the app target.
457
+ */
458
+ function patchIOSPodfile(projectDir) {
459
+ const podfilePath = path.join(projectDir, 'ios', 'Podfile');
460
+ if (!fs.existsSync(podfilePath)) {
461
+ return false;
462
+ }
463
+
464
+ let content = fs.readFileSync(podfilePath, 'utf8');
465
+ if (content.includes("min_ios = '15.1'")) {
466
+ return false;
467
+ }
468
+
469
+ const marker = 'react_native_post_install(';
470
+ const start = content.indexOf(marker);
471
+ if (start === -1) {
472
+ return false;
473
+ }
474
+
475
+ const callEnd = content.indexOf('\n )', start);
476
+ if (callEnd === -1) {
477
+ return false;
478
+ }
479
+
480
+ const injection = `
481
+ # Force every pod (including resource bundles) to match the app's
482
+ # iOS 15.1 minimum. Without this, some pods stay on 9.0 and Xcode fails.
483
+ min_ios = '15.1'
484
+ installer.pods_project.build_configurations.each do |config|
485
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios
486
+ end
487
+ installer.pods_project.targets.each do |target|
488
+ target.build_configurations.each do |config|
489
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios
490
+ end
491
+ end`;
492
+
493
+ content =
494
+ content.slice(0, callEnd + '\n )'.length) +
495
+ injection +
496
+ content.slice(callEnd + '\n )'.length);
497
+ fs.writeFileSync(podfilePath, content, 'utf8');
498
+ console.log('✅ Patched ios/Podfile deployment target');
499
+ return true;
500
+ }
501
+
502
+ /**
503
+ * The wrapper default (10s) drops the Gradle distribution download on slow networks.
504
+ */
505
+ function patchGradleWrapper(projectDir) {
506
+ const wrapperPath = path.join(
507
+ projectDir,
508
+ 'android',
509
+ 'gradle',
510
+ 'wrapper',
511
+ 'gradle-wrapper.properties',
512
+ );
513
+ if (!fs.existsSync(wrapperPath)) {
514
+ return false;
515
+ }
516
+
517
+ const timeoutMs = 180000;
518
+ let content = fs.readFileSync(wrapperPath, 'utf8');
519
+ const match = content.match(/^networkTimeout=(\d+)\s*$/m);
520
+
521
+ if (match && Number(match[1]) >= timeoutMs) {
522
+ return false;
523
+ }
524
+
525
+ if (match) {
526
+ content = content.replace(
527
+ /^networkTimeout=\d+\s*$/m,
528
+ `networkTimeout=${timeoutMs}`,
529
+ );
530
+ } else if (content.includes('distributionUrl=')) {
531
+ content = content.replace(
532
+ /^(distributionUrl=.*)$/m,
533
+ `$1\nnetworkTimeout=${timeoutMs}`,
534
+ );
535
+ } else {
536
+ content = `${content.replace(/\s*$/, '')}\nnetworkTimeout=${timeoutMs}\n`;
537
+ }
538
+
539
+ fs.writeFileSync(wrapperPath, content, 'utf8');
540
+ console.log('✅ Patched gradle-wrapper.properties (networkTimeout)');
541
+ return true;
542
+ }
543
+
544
+ /**
545
+ * Draw behind system bars. Screens already use safe area insets.
546
+ */
547
+ function patchGradleProperties(projectDir) {
548
+ const propsPath = path.join(projectDir, 'android', 'gradle.properties');
549
+ if (!fs.existsSync(propsPath)) {
550
+ return false;
551
+ }
552
+
553
+ let content = fs.readFileSync(propsPath, 'utf8');
554
+ if (!content.includes('edgeToEdgeEnabled=false')) {
555
+ return false;
556
+ }
557
+
558
+ content = content.replace('edgeToEdgeEnabled=false', 'edgeToEdgeEnabled=true');
559
+ fs.writeFileSync(propsPath, content, 'utf8');
560
+ console.log('✅ Patched android/gradle.properties (edgeToEdgeEnabled)');
561
+ return true;
562
+ }
563
+
564
+ /**
565
+ * Run all native patches
566
+ */
567
+ function patchAllNative(projectDir) {
568
+ patchAndroidBuildGradle(projectDir);
569
+ patchAndroidMainActivity(projectDir);
570
+ patchAndroidManifest(projectDir);
571
+ patchGradleWrapper(projectDir);
572
+ patchGradleProperties(projectDir);
573
+ patchIOSInfoPlist(projectDir);
574
+ patchIOSSceneLifecycle(projectDir);
575
+ patchIOSPodfile(projectDir);
576
+ }
577
+
578
+ module.exports = {
579
+ patchAllNative,
580
+ patchAndroidBuildGradle,
581
+ patchAndroidMainActivity,
582
+ patchAndroidManifest,
583
+ patchGradleWrapper,
584
+ patchGradleProperties,
585
+ patchIOSInfoPlist,
586
+ patchIOSSceneLifecycle,
587
+ patchIOSPodfile,
588
+ };
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.1",
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