create-react-native-starter-kit 1.0.0 → 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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # create-react-native-starter-kit
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/create-react-native-starter-kit.svg)](https://www.npmjs.com/package/create-react-native-starter-kit)
4
- [![license](https://img.shields.io/npm/l/create-react-native-starter-kit.svg)](./LICENSE)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
5
  [![GitHub](https://img.shields.io/badge/GitHub-satyam--narayan-181717?logo=github)](https://github.com/satyam-narayan/create-react-native-starter-kit)
6
6
  [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black)](https://www.buymeacoffee.com/satyamnarayan)
7
7
 
@@ -1,3 +1,4 @@
1
+ const crypto = require('crypto');
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
 
@@ -253,6 +254,204 @@ ${fontStrings}
253
254
  return modified;
254
255
  }
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
+
256
455
  /**
257
456
  * Keep CocoaPods on the same iOS minimum as the app target.
258
457
  */
@@ -372,6 +571,7 @@ function patchAllNative(projectDir) {
372
571
  patchGradleWrapper(projectDir);
373
572
  patchGradleProperties(projectDir);
374
573
  patchIOSInfoPlist(projectDir);
574
+ patchIOSSceneLifecycle(projectDir);
375
575
  patchIOSPodfile(projectDir);
376
576
  }
377
577
 
@@ -383,5 +583,6 @@ module.exports = {
383
583
  patchGradleWrapper,
384
584
  patchGradleProperties,
385
585
  patchIOSInfoPlist,
586
+ patchIOSSceneLifecycle,
386
587
  patchIOSPodfile,
387
588
  };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "create-react-native-starter-kit",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
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
5
  "main": "bin/create-react-native-starter-kit.js",
6
6
  "bin": {
7
- "create-react-native-starter-kit": "./bin/create-react-native-starter-kit.js"
7
+ "create-react-native-starter-kit": "bin/create-react-native-starter-kit.js"
8
8
  },
9
9
  "files": [
10
10
  "bin",
@@ -8,6 +8,7 @@ import Eye from './eye.svg';
8
8
  import EyeClose from './eyeClose.svg';
9
9
  import Folder from './folder.svg';
10
10
  import Home from './home.svg';
11
+ import Log_out from './log_out.svg';
11
12
  import Setting from './setting.svg';
12
13
  import Tooltip_dark from './tooltip_dark.svg';
13
14
  import Tooltip_light from './tooltip_light.svg';
@@ -24,6 +25,7 @@ export const Icons = {
24
25
  eyeClose: EyeClose,
25
26
  folder: Folder,
26
27
  home: Home,
28
+ log_out: Log_out,
27
29
  setting: Setting,
28
30
  tooltip_dark: Tooltip_dark,
29
31
  tooltip_light: Tooltip_light,
@@ -0,0 +1,4 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M10 4H6C4.89543 4 4 4.89543 4 6V18C4 19.1046 4.89543 20 6 20H10" stroke="#F2574C" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
3
+ <path d="M16 8L20 12M20 12L16 16M20 12H10" stroke="#F2574C" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
@@ -9,6 +9,7 @@ import { useAppTheme } from '@/context/ThemeContext';
9
9
  import Icon from '../Icon';
10
10
  import { isIOS } from '@/constants/device';
11
11
  import { FORM_FIELD } from './formFieldStyles';
12
+ import { spacing } from '@/theme';
12
13
 
13
14
  const FormInput = <T extends FieldValues>({
14
15
  control,
@@ -112,7 +113,7 @@ export default FormInput;
112
113
  const styles = StyleSheet.create({
113
114
  container: {
114
115
  width: '100%',
115
- marginBottom: 0,
116
+ marginBottom: spacing.md,
116
117
  },
117
118
  labelCont: {
118
119
  flexDirection: 'row',
@@ -1,26 +1,16 @@
1
1
  import { spacing } from '@/theme';
2
- import { normalize } from '@/utils/normalize';
3
2
  import { ICustomViewStyle } from 'react-native-reanimated-skeleton/lib/typescript/constants';
4
3
 
5
4
  const AVATAR_SIZE = 150;
6
5
 
7
- /** Matches Home content: title → avatar. */
6
+ /** Matches Home content: the centered AvatarPicker (the title is rendered outside the boundary). */
8
7
  export const homeShimmerLayout = (): ICustomViewStyle[] => [
9
- // Title
10
- {
11
- key: 'title',
12
- width: normalize(160),
13
- height: normalize(24),
14
- borderRadius: 8,
15
- alignSelf: 'center',
16
- marginTop: spacing.md,
17
- },
18
- // AvatarPicker
19
8
  {
20
9
  key: 'avatar',
21
10
  width: AVATAR_SIZE,
22
11
  height: AVATAR_SIZE,
23
12
  borderRadius: AVATAR_SIZE / 2,
13
+ alignSelf: 'center',
24
14
  marginTop: spacing.md,
25
15
  },
26
16
  ];
@@ -57,7 +57,7 @@ const MainTabs = ({ navigation }: props) => {
57
57
  overrideVariantColor: color.border,
58
58
  overrideVariantTextColor: color.text_secondary,
59
59
  }}
60
- icon="clock"
60
+ icon="log_out"
61
61
  onPressPrimaryBtn={() => {
62
62
  setIsAlertOpen(false);
63
63
  handleLogout();
@@ -54,10 +54,9 @@ const ProfileSetup = ({ navigation }: Props) => {
54
54
  };
55
55
  return (
56
56
  <KeyboardAvoidingView
57
- style={[layout.flexContainer,{paddingBottom:bottom+spacing.sm}]}
57
+ style={layout.flexContainer}
58
58
  behavior={isIOS ? 'padding' : 'height'}
59
59
  keyboardVerticalOffset={isIOS ? 64 : 0}
60
- contentContainerStyle={{flexGrow:1}}
61
60
  >
62
61
  <ScrollView showsVerticalScrollIndicator={false}>
63
62
  <FormInput
@@ -148,12 +147,14 @@ const ProfileSetup = ({ navigation }: Props) => {
148
147
  />
149
148
 
150
149
  </ScrollView>
150
+ {/* KeyboardAvoidingView overrides its own paddingBottom, so the inset is a margin here. */}
151
151
  <CustomButton
152
152
  title={t('common.continue')}
153
153
  onPress={handleSubmit(handleContinue)}
154
154
  variant="primary"
155
155
  fullWidth
156
- />
156
+ containerStyle={{ marginTop: spacing.sm, marginBottom: bottom + spacing.sm }}
157
+ />
157
158
  </KeyboardAvoidingView>
158
159
  );
159
160
  };
@@ -52,6 +52,7 @@ const Login = ({ navigation }: Props) => {
52
52
  weight="600"
53
53
  textColor={color.text_primary}
54
54
  textAlign="center"
55
+ style={layout.marginBottomLg}
55
56
  >
56
57
  {t('auth.welcome-back')}
57
58
  </CustomText>
@@ -69,8 +70,8 @@ const Login = ({ navigation }: Props) => {
69
70
  field: t('auth.email'),
70
71
  })}
71
72
  variant="text"
72
- isTooltip
73
- tooltipMessage='We"ll never share your email with anyone else.'
73
+ // isTooltip
74
+ // tooltipMessage='We"ll never share your email with anyone else.'
74
75
  />
75
76
 
76
77
  <FormInput
@@ -42,6 +42,7 @@ const Signup = ({ navigation }: Props) => {
42
42
  weight="600"
43
43
  textColor={color.text_primary}
44
44
  textAlign="center"
45
+ style={layout.marginBottomLg}
45
46
  >
46
47
  {t('auth.lets-get-started')}
47
48
  </CustomText>
@@ -19,4 +19,5 @@ export const layout = StyleSheet.create({
19
19
  separator: { height: 1, marginVertical: spacing.md },
20
20
  marginTopMd: { marginTop: spacing.md },
21
21
  marginBottomMd: { marginBottom: spacing.md },
22
+ marginBottomLg: { marginBottom: spacing.lg },
22
23
  });