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 +1 -1
- package/lib/patch-native.js +201 -0
- package/package.json +2 -2
- package/template/src/assets/icons/index.ts +2 -0
- package/template/src/assets/icons/log_out.svg +4 -0
- package/template/src/components/shared/form-input/FormInput.tsx +2 -1
- package/template/src/components/shared/skelton/layouts/home.layout.ts +2 -12
- package/template/src/screens/app/main-tab/index.tsx +1 -1
- package/template/src/screens/app/profile-setup/index.tsx +4 -3
- package/template/src/screens/auth/login/index.tsx +3 -2
- package/template/src/screens/auth/signup/index.tsx +1 -0
- package/template/src/theme/layout.ts +1 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# create-react-native-starter-kit
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/create-react-native-starter-kit)
|
|
4
|
-
[](./LICENSE)
|
|
5
5
|
[](https://github.com/satyam-narayan/create-react-native-starter-kit)
|
|
6
6
|
[](https://www.buymeacoffee.com/satyamnarayan)
|
|
7
7
|
|
package/lib/patch-native.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
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:
|
|
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
|
|
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
|
];
|
|
@@ -54,10 +54,9 @@ const ProfileSetup = ({ navigation }: Props) => {
|
|
|
54
54
|
};
|
|
55
55
|
return (
|
|
56
56
|
<KeyboardAvoidingView
|
|
57
|
-
style={
|
|
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
|