@highbeek/create-rnstarterkit 1.0.2-beta.4 → 1.0.2-beta.7

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 (29) hide show
  1. package/dist/src/generators/appGenerator.js +696 -19
  2. package/dist/templates/expo-base/app/_layout.tsx +4 -19
  3. package/dist/templates/expo-base/app/index.tsx +9 -0
  4. package/dist/templates/expo-base/package.json +0 -1
  5. package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +1 -1
  6. package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +1 -1
  7. package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +1 -1
  8. package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +1 -1
  9. package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +1 -1
  10. package/package.json +1 -1
  11. package/dist/templates/expo-base/app/(tabs)/_layout.tsx +0 -35
  12. package/dist/templates/expo-base/app/(tabs)/explore.tsx +0 -112
  13. package/dist/templates/expo-base/app/(tabs)/index.tsx +0 -98
  14. package/dist/templates/expo-base/app/modal.tsx +0 -29
  15. package/dist/templates/expo-base/components/external-link.tsx +0 -25
  16. package/dist/templates/expo-base/components/haptic-tab.tsx +0 -18
  17. package/dist/templates/expo-base/components/hello-wave.tsx +0 -19
  18. package/dist/templates/expo-base/components/parallax-scroll-view.tsx +0 -79
  19. package/dist/templates/expo-base/components/themed-text.tsx +0 -60
  20. package/dist/templates/expo-base/components/themed-view.tsx +0 -14
  21. package/dist/templates/expo-base/constants/theme.ts +0 -53
  22. package/dist/templates/expo-base/hooks/use-color-scheme.ts +0 -1
  23. package/dist/templates/expo-base/hooks/use-color-scheme.web.ts +0 -21
  24. package/dist/templates/expo-base/hooks/use-theme-color.ts +0 -21
  25. package/dist/templates/expo-base/scripts/reset-project.js +0 -112
  26. package/dist/templates/optional/apiClient/api/client.axios.ts +0 -124
  27. /package/dist/templates/optional/auth-context/api/{authApi.ts → endpoints/auth.ts} +0 -0
  28. /package/dist/templates/optional/auth-redux/api/{authApi.ts → endpoints/auth.ts} +0 -0
  29. /package/dist/templates/optional/auth-zustand/api/{authApi.ts → endpoints/auth.ts} +0 -0
@@ -18,7 +18,7 @@ async function generateApp(options) {
18
18
  filter: (src) => shouldCopyPath(src, templatePath),
19
19
  });
20
20
  await replaceProjectName(targetPath, projectName);
21
- await createStandardStructure(targetPath);
21
+ await createStandardStructure(targetPath, platform);
22
22
  if (auth) {
23
23
  const authFolder = state === "Redux Toolkit"
24
24
  ? "auth-redux"
@@ -29,7 +29,13 @@ async function generateApp(options) {
29
29
  if (authFolder === "auth-context" ||
30
30
  authFolder === "auth-zustand" ||
31
31
  authFolder === "auth-redux") {
32
- await setAuthTabsByPlatform(targetPath, platform);
32
+ if (platform === "Expo") {
33
+ await writeExpoRouterAuthRoutes(targetPath, state);
34
+ }
35
+ else {
36
+ await setAuthTabsByPlatform(targetPath, platform);
37
+ await writeAuthAppShell(targetPath, state);
38
+ }
33
39
  }
34
40
  }
35
41
  if (apiClient) {
@@ -44,9 +50,16 @@ async function generateApp(options) {
44
50
  await copyOptionalModule("react-query", targetPath);
45
51
  if (storage === "MMKV")
46
52
  await copyOptionalModule("mmkv", targetPath);
53
+ await configureStateAndAuthDependencies(targetPath, {
54
+ platform,
55
+ state,
56
+ auth,
57
+ storage,
58
+ });
47
59
  await configureAbsoluteImports(targetPath, platform, absoluteImports);
48
60
  await configureDataFetching(targetPath, dataFetching);
49
61
  await configureApiClientTransport(targetPath, apiClient, apiClientType);
62
+ await syncApiIndex(targetPath);
50
63
  await configureValidation(targetPath, validation);
51
64
  if (typescript) {
52
65
  const tsconfigTemplate = path_1.default.join(templateRoot, "optional/tsconfig.json");
@@ -138,26 +151,26 @@ async function ensureCliApiEnvSupport(targetPath) {
138
151
  },
139
152
  });
140
153
  }
141
- async function createStandardStructure(targetPath) {
142
- const directories = [
154
+ async function createStandardStructure(targetPath, platform) {
155
+ const commonDirectories = [
143
156
  "assets",
144
157
  "assets/icons",
145
158
  "assets/images",
146
159
  "assets/fonts",
147
160
  "components",
148
- "navigation",
149
- "screens",
150
161
  "hooks",
151
162
  "utils",
152
163
  "services",
164
+ "api",
165
+ "config",
166
+ "context",
153
167
  ];
168
+ const cliOnlyDirectories = ["navigation", "screens"];
169
+ const directories = platform === "Expo"
170
+ ? commonDirectories
171
+ : [...commonDirectories, ...cliOnlyDirectories];
154
172
  for (const directory of directories) {
155
- const absoluteDir = path_1.default.join(targetPath, directory);
156
- await fs_extra_1.default.ensureDir(absoluteDir);
157
- const gitkeepPath = path_1.default.join(absoluteDir, ".gitkeep");
158
- if (!(await fs_extra_1.default.pathExists(gitkeepPath))) {
159
- await fs_extra_1.default.writeFile(gitkeepPath, "", "utf8");
160
- }
173
+ await fs_extra_1.default.ensureDir(path_1.default.join(targetPath, directory));
161
174
  }
162
175
  }
163
176
  async function configureAbsoluteImports(targetPath, platform, absoluteImports) {
@@ -198,25 +211,157 @@ export const queryClient = new QueryClient();
198
211
  }
199
212
  async function configureApiClientTransport(targetPath, apiClient, apiClientType) {
200
213
  if (apiClient && apiClientType === "Axios") {
201
- const templateRoot = await resolveTemplateRoot();
202
214
  await ensureDependencies(targetPath, {
203
215
  dependencies: {
204
216
  axios: "^1.12.2",
205
217
  },
206
218
  });
207
- await writeIfMissing(path_1.default.join(targetPath, "api/axiosClient.ts"), `import axios from "axios";
219
+ await fs_extra_1.default.writeFile(path_1.default.join(targetPath, "api/client.ts"), `import axios, {
220
+ AxiosError,
221
+ AxiosRequestConfig,
222
+ AxiosResponse,
223
+ InternalAxiosRequestConfig,
224
+ } from "axios";
208
225
  import { API_BASE_URL } from "../config/env";
209
226
 
210
- export const axiosClient = axios.create({
227
+ export class ApiError extends Error {
228
+ status: number;
229
+ data: unknown;
230
+
231
+ constructor(status: number, message: string, data: unknown) {
232
+ super(message);
233
+ this.name = "ApiError";
234
+ this.status = status;
235
+ this.data = data;
236
+ }
237
+ }
238
+
239
+ type RequestOptions = {
240
+ headers?: Record<string, string>;
241
+ query?: Record<string, string | number | boolean | undefined>;
242
+ token?: string | null;
243
+ signal?: AbortSignal;
244
+ };
245
+
246
+ const client = axios.create({
211
247
  baseURL: API_BASE_URL,
212
248
  timeout: 15000,
213
249
  });
214
- `);
215
- await fs_extra_1.default.copy(path_1.default.join(templateRoot, "optional/apiClient/api/client.axios.ts"), path_1.default.join(targetPath, "api/client.ts"), {
216
- overwrite: true,
217
- });
250
+
251
+ let authTokenGetter: (() => string | null | undefined) | null = null;
252
+
253
+ export function setAuthTokenGetter(getter: (() => string | null | undefined) | null) {
254
+ authTokenGetter = getter;
255
+ }
256
+
257
+ function resolveErrorMessage(data: unknown, status: number) {
258
+ if (
259
+ typeof data === "object" &&
260
+ data !== null &&
261
+ "message" in data &&
262
+ typeof (data as { message: unknown }).message === "string"
263
+ ) {
264
+ return (data as { message: string }).message;
265
+ }
266
+ return \`Request failed with status \${status}\`;
267
+ }
268
+
269
+ export function addRequestInterceptor(
270
+ interceptor: (
271
+ config: InternalAxiosRequestConfig,
272
+ ) =>
273
+ | InternalAxiosRequestConfig
274
+ | Promise<InternalAxiosRequestConfig>,
275
+ ) {
276
+ return client.interceptors.request.use(interceptor);
277
+ }
278
+
279
+ export function addResponseInterceptor(
280
+ onFulfilled?: (
281
+ response: AxiosResponse,
282
+ ) => AxiosResponse | Promise<AxiosResponse>,
283
+ onRejected?: (error: AxiosError) => unknown,
284
+ ) {
285
+ return client.interceptors.response.use(onFulfilled, onRejected);
286
+ }
287
+
288
+ addRequestInterceptor((config) => {
289
+ const token = authTokenGetter?.();
290
+ if (token) {
291
+ config.headers.Authorization = \`Bearer \${token}\`;
292
+ }
293
+ return config;
294
+ });
295
+
296
+ addResponseInterceptor(
297
+ (response) => response,
298
+ (error) => {
299
+ if (axios.isAxiosError(error)) {
300
+ const status = error.response?.status ?? 500;
301
+ const data = error.response?.data ?? null;
302
+ return Promise.reject(new ApiError(status, resolveErrorMessage(data, status), data));
303
+ }
304
+ return Promise.reject(error);
305
+ },
306
+ );
307
+
308
+ async function request<T>(
309
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
310
+ path: string,
311
+ body?: unknown,
312
+ options: RequestOptions = {},
313
+ ): Promise<T> {
314
+ const { headers = {}, query, token, signal } = options;
315
+ const requestConfig: AxiosRequestConfig = {
316
+ method,
317
+ url: path,
318
+ data: body,
319
+ params: query,
320
+ signal,
321
+ headers: {
322
+ ...(token ? { Authorization: \`Bearer \${token}\` } : {}),
323
+ ...headers,
324
+ },
325
+ };
326
+ const response = await client.request<T>(requestConfig);
327
+ return response.data;
328
+ }
329
+
330
+ export const apiClient = {
331
+ get: <T>(path: string, options?: RequestOptions) =>
332
+ request<T>("GET", path, undefined, options),
333
+ post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
334
+ request<T>("POST", path, body, options),
335
+ put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
336
+ request<T>("PUT", path, body, options),
337
+ patch: <T>(path: string, body?: unknown, options?: RequestOptions) =>
338
+ request<T>("PATCH", path, body, options),
339
+ delete: <T>(path: string, options?: RequestOptions) =>
340
+ request<T>("DELETE", path, undefined, options),
341
+ };
342
+ `, "utf8");
343
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "api/client.axios.ts"));
344
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "api/axiosClient.ts"));
218
345
  }
219
346
  }
347
+ async function syncApiIndex(targetPath) {
348
+ const apiDir = path_1.default.join(targetPath, "api");
349
+ if (!(await fs_extra_1.default.pathExists(apiDir)))
350
+ return;
351
+ const exports = [];
352
+ if (await fs_extra_1.default.pathExists(path_1.default.join(apiDir, "client.ts"))) {
353
+ exports.push('export * from "./client";');
354
+ }
355
+ if (await fs_extra_1.default.pathExists(path_1.default.join(apiDir, "endpoints/auth.ts"))) {
356
+ exports.push('export * from "./endpoints/auth";');
357
+ }
358
+ else if (await fs_extra_1.default.pathExists(path_1.default.join(apiDir, "authApi.ts"))) {
359
+ exports.push('export * from "./authApi";');
360
+ }
361
+ if (exports.length === 0)
362
+ return;
363
+ await fs_extra_1.default.writeFile(path_1.default.join(apiDir, "index.ts"), `${exports.join("\n")}\n`, "utf8");
364
+ }
220
365
  async function configureValidation(targetPath, validation) {
221
366
  const dependencies = {};
222
367
  if (validation.includes("Formik"))
@@ -229,6 +374,538 @@ async function configureValidation(targetPath, validation) {
229
374
  await ensureDependencies(targetPath, { dependencies });
230
375
  }
231
376
  }
377
+ async function configureStateAndAuthDependencies(targetPath, options) {
378
+ const dependencies = {};
379
+ if (options.auth && options.platform === "React Native CLI") {
380
+ dependencies["@react-navigation/native-stack"] = "^7.3.29";
381
+ }
382
+ if (options.state === "Redux Toolkit") {
383
+ dependencies["@reduxjs/toolkit"] = "^2.9.0";
384
+ dependencies["react-redux"] = "^9.2.0";
385
+ }
386
+ if (options.state === "Zustand") {
387
+ dependencies.zustand = "^5.0.8";
388
+ }
389
+ if (options.storage === "AsyncStorage" &&
390
+ (options.state === "Context API" || options.state === "Zustand")) {
391
+ dependencies["@react-native-async-storage/async-storage"] = "^2.2.0";
392
+ }
393
+ if (Object.keys(dependencies).length > 0) {
394
+ await ensureDependencies(targetPath, { dependencies });
395
+ }
396
+ }
397
+ async function writeExpoRouterAuthRoutes(targetPath, state) {
398
+ const appDir = path_1.default.join(targetPath, "app");
399
+ const authDir = path_1.default.join(appDir, "(auth)");
400
+ const tabsDir = path_1.default.join(appDir, "(tabs)");
401
+ await fs_extra_1.default.ensureDir(authDir);
402
+ await fs_extra_1.default.ensureDir(tabsDir);
403
+ const stateBindings = getExpoAuthStateBindings(state);
404
+ const routeFiles = getExpoAuthRouteFiles(state);
405
+ await fs_extra_1.default.writeFile(path_1.default.join(appDir, "_layout.tsx"), stateBindings.rootLayout, "utf8");
406
+ await fs_extra_1.default.writeFile(path_1.default.join(appDir, "index.tsx"), stateBindings.indexRoute, "utf8");
407
+ await fs_extra_1.default.writeFile(path_1.default.join(authDir, "_layout.tsx"), stateBindings.authLayout, "utf8");
408
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "_layout.tsx"), stateBindings.tabsLayout, "utf8");
409
+ await fs_extra_1.default.writeFile(path_1.default.join(authDir, "login.tsx"), routeFiles.login, "utf8");
410
+ await fs_extra_1.default.writeFile(path_1.default.join(authDir, "register.tsx"), routeFiles.register, "utf8");
411
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "index.tsx"), routeFiles.home, "utf8");
412
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "profile.tsx"), routeFiles.profile, "utf8");
413
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "settings.tsx"), routeFiles.settings, "utf8");
414
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "navigation"));
415
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "screens"));
416
+ }
417
+ function getExpoAuthStateBindings(state) {
418
+ if (state === "Redux Toolkit") {
419
+ return {
420
+ rootLayout: `import React from "react";
421
+ import { Slot } from "expo-router";
422
+ import { Provider } from "react-redux";
423
+ import { store } from "../store/store";
424
+
425
+ export default function RootLayout() {
426
+ return (
427
+ <Provider store={store}>
428
+ <Slot />
429
+ </Provider>
430
+ );
431
+ }
432
+ `,
433
+ indexRoute: `import React from "react";
434
+ import { Redirect } from "expo-router";
435
+ import { useSelector } from "react-redux";
436
+ import type { RootState } from "../store/store";
437
+
438
+ export default function Index() {
439
+ const token = useSelector((state: RootState) => state.auth.token);
440
+ return <Redirect href={token ? "/(tabs)" : "/(auth)/login"} />;
441
+ }
442
+ `,
443
+ authLayout: `import React from "react";
444
+ import { Redirect, Stack } from "expo-router";
445
+ import { useSelector } from "react-redux";
446
+ import type { RootState } from "../../store/store";
447
+
448
+ export default function AuthLayout() {
449
+ const token = useSelector((state: RootState) => state.auth.token);
450
+ if (token) return <Redirect href="/(tabs)" />;
451
+
452
+ return (
453
+ <Stack screenOptions={{ headerShown: false }}>
454
+ <Stack.Screen name="login" />
455
+ <Stack.Screen name="register" />
456
+ </Stack>
457
+ );
458
+ }
459
+ `,
460
+ tabsLayout: `import React from "react";
461
+ import { Redirect, Tabs } from "expo-router";
462
+ import { useSelector } from "react-redux";
463
+ import type { RootState } from "../../store/store";
464
+
465
+ export default function TabsLayout() {
466
+ const token = useSelector((state: RootState) => state.auth.token);
467
+ if (!token) return <Redirect href="/(auth)/login" />;
468
+
469
+ return (
470
+ <Tabs screenOptions={{ headerShown: false }}>
471
+ <Tabs.Screen name="index" options={{ title: "Home" }} />
472
+ <Tabs.Screen name="profile" options={{ title: "Profile" }} />
473
+ <Tabs.Screen name="settings" options={{ title: "Settings" }} />
474
+ </Tabs>
475
+ );
476
+ }
477
+ `,
478
+ };
479
+ }
480
+ if (state === "Zustand") {
481
+ return {
482
+ rootLayout: `import React from "react";
483
+ import { Slot } from "expo-router";
484
+
485
+ export default function RootLayout() {
486
+ return <Slot />;
487
+ }
488
+ `,
489
+ indexRoute: `import React from "react";
490
+ import { useEffect } from "react";
491
+ import { ActivityIndicator, View } from "react-native";
492
+ import { Redirect } from "expo-router";
493
+ import { useAuthStore } from "../store/authStore";
494
+
495
+ export default function Index() {
496
+ const token = useAuthStore((state) => state.token);
497
+ const isHydrated = useAuthStore((state) => state.isHydrated);
498
+ const hydrate = useAuthStore((state) => state.hydrate);
499
+
500
+ useEffect(() => {
501
+ void hydrate();
502
+ }, [hydrate]);
503
+
504
+ if (!isHydrated) {
505
+ return (
506
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
507
+ <ActivityIndicator />
508
+ </View>
509
+ );
510
+ }
511
+
512
+ return <Redirect href={token ? "/(tabs)" : "/(auth)/login"} />;
513
+ }
514
+ `,
515
+ authLayout: `import React, { useEffect } from "react";
516
+ import { ActivityIndicator, View } from "react-native";
517
+ import { Redirect, Stack } from "expo-router";
518
+ import { useAuthStore } from "../../store/authStore";
519
+
520
+ export default function AuthLayout() {
521
+ const token = useAuthStore((state) => state.token);
522
+ const isHydrated = useAuthStore((state) => state.isHydrated);
523
+ const hydrate = useAuthStore((state) => state.hydrate);
524
+
525
+ useEffect(() => {
526
+ void hydrate();
527
+ }, [hydrate]);
528
+
529
+ if (!isHydrated) {
530
+ return (
531
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
532
+ <ActivityIndicator />
533
+ </View>
534
+ );
535
+ }
536
+
537
+ if (token) return <Redirect href="/(tabs)" />;
538
+
539
+ return (
540
+ <Stack screenOptions={{ headerShown: false }}>
541
+ <Stack.Screen name="login" />
542
+ <Stack.Screen name="register" />
543
+ </Stack>
544
+ );
545
+ }
546
+ `,
547
+ tabsLayout: `import React, { useEffect } from "react";
548
+ import { ActivityIndicator, View } from "react-native";
549
+ import { Redirect, Tabs } from "expo-router";
550
+ import { useAuthStore } from "../../store/authStore";
551
+
552
+ export default function TabsLayout() {
553
+ const token = useAuthStore((state) => state.token);
554
+ const isHydrated = useAuthStore((state) => state.isHydrated);
555
+ const hydrate = useAuthStore((state) => state.hydrate);
556
+
557
+ useEffect(() => {
558
+ void hydrate();
559
+ }, [hydrate]);
560
+
561
+ if (!isHydrated) {
562
+ return (
563
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
564
+ <ActivityIndicator />
565
+ </View>
566
+ );
567
+ }
568
+
569
+ if (!token) return <Redirect href="/(auth)/login" />;
570
+
571
+ return (
572
+ <Tabs screenOptions={{ headerShown: false }}>
573
+ <Tabs.Screen name="index" options={{ title: "Home" }} />
574
+ <Tabs.Screen name="profile" options={{ title: "Profile" }} />
575
+ <Tabs.Screen name="settings" options={{ title: "Settings" }} />
576
+ </Tabs>
577
+ );
578
+ }
579
+ `,
580
+ };
581
+ }
582
+ return {
583
+ rootLayout: `import React from "react";
584
+ import { Slot } from "expo-router";
585
+ import { AuthProvider } from "../context/AuthContext";
586
+
587
+ export default function RootLayout() {
588
+ return (
589
+ <AuthProvider>
590
+ <Slot />
591
+ </AuthProvider>
592
+ );
593
+ }
594
+ `,
595
+ indexRoute: `import React, { useContext } from "react";
596
+ import { ActivityIndicator, View } from "react-native";
597
+ import { Redirect } from "expo-router";
598
+ import { AuthContext } from "../context/AuthContext";
599
+
600
+ export default function Index() {
601
+ const { token, isHydrated } = useContext(AuthContext);
602
+
603
+ if (!isHydrated) {
604
+ return (
605
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
606
+ <ActivityIndicator />
607
+ </View>
608
+ );
609
+ }
610
+
611
+ return <Redirect href={token ? "/(tabs)" : "/(auth)/login"} />;
612
+ }
613
+ `,
614
+ authLayout: `import React, { useContext } from "react";
615
+ import { ActivityIndicator, View } from "react-native";
616
+ import { Redirect, Stack } from "expo-router";
617
+ import { AuthContext } from "../../context/AuthContext";
618
+
619
+ export default function AuthLayout() {
620
+ const { token, isHydrated } = useContext(AuthContext);
621
+
622
+ if (!isHydrated) {
623
+ return (
624
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
625
+ <ActivityIndicator />
626
+ </View>
627
+ );
628
+ }
629
+
630
+ if (token) return <Redirect href="/(tabs)" />;
631
+
632
+ return (
633
+ <Stack screenOptions={{ headerShown: false }}>
634
+ <Stack.Screen name="login" />
635
+ <Stack.Screen name="register" />
636
+ </Stack>
637
+ );
638
+ }
639
+ `,
640
+ tabsLayout: `import React, { useContext } from "react";
641
+ import { ActivityIndicator, View } from "react-native";
642
+ import { Redirect, Tabs } from "expo-router";
643
+ import { AuthContext } from "../../context/AuthContext";
644
+
645
+ export default function TabsLayout() {
646
+ const { token, isHydrated } = useContext(AuthContext);
647
+
648
+ if (!isHydrated) {
649
+ return (
650
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
651
+ <ActivityIndicator />
652
+ </View>
653
+ );
654
+ }
655
+
656
+ if (!token) return <Redirect href="/(auth)/login" />;
657
+
658
+ return (
659
+ <Tabs screenOptions={{ headerShown: false }}>
660
+ <Tabs.Screen name="index" options={{ title: "Home" }} />
661
+ <Tabs.Screen name="profile" options={{ title: "Profile" }} />
662
+ <Tabs.Screen name="settings" options={{ title: "Settings" }} />
663
+ </Tabs>
664
+ );
665
+ }
666
+ `,
667
+ };
668
+ }
669
+ function getExpoAuthRouteFiles(state) {
670
+ const register = `import React from "react";
671
+ import { Text, View } from "react-native";
672
+
673
+ export default function RegisterScreen() {
674
+ return (
675
+ <View style={{ padding: 20 }}>
676
+ <Text>RegisterScreen</Text>
677
+ </View>
678
+ );
679
+ }
680
+ `;
681
+ const profile = `import React from "react";
682
+ import { Text, View } from "react-native";
683
+
684
+ export default function ProfileScreen() {
685
+ return (
686
+ <View style={{ padding: 20 }}>
687
+ <Text>ProfileScreen</Text>
688
+ </View>
689
+ );
690
+ }
691
+ `;
692
+ const settings = `import React from "react";
693
+ import { Text, View } from "react-native";
694
+
695
+ export default function SettingsScreen() {
696
+ return (
697
+ <View style={{ padding: 20 }}>
698
+ <Text>SettingsScreen</Text>
699
+ </View>
700
+ );
701
+ }
702
+ `;
703
+ if (state === "Redux Toolkit") {
704
+ return {
705
+ login: `import React, { useState } from "react";
706
+ import { Button, TextInput, View } from "react-native";
707
+ import { useDispatch } from "react-redux";
708
+ import { loginApi } from "../../api";
709
+ import { login } from "../../store/authSlice";
710
+
711
+ export default function LoginScreen() {
712
+ const dispatch = useDispatch();
713
+ const [email, setEmail] = useState("");
714
+ const [password, setPassword] = useState("");
715
+
716
+ const handleLogin = async () => {
717
+ const token = await loginApi(email, password);
718
+ dispatch(login(token));
719
+ };
720
+
721
+ return (
722
+ <View style={{ padding: 20 }}>
723
+ <TextInput placeholder="Email" value={email} onChangeText={setEmail} />
724
+ <TextInput
725
+ placeholder="Password"
726
+ value={password}
727
+ onChangeText={setPassword}
728
+ secureTextEntry
729
+ />
730
+ <Button title="Login" onPress={() => void handleLogin()} />
731
+ </View>
732
+ );
733
+ }
734
+ `,
735
+ register,
736
+ home: `import React from "react";
737
+ import { Button, Text, View } from "react-native";
738
+ import { useDispatch } from "react-redux";
739
+ import { logout } from "../../store/authSlice";
740
+
741
+ export default function HomeScreen() {
742
+ const dispatch = useDispatch();
743
+
744
+ return (
745
+ <View style={{ padding: 20 }}>
746
+ <Text>Welcome Home!</Text>
747
+ <Button title="Logout" onPress={() => dispatch(logout())} />
748
+ </View>
749
+ );
750
+ }
751
+ `,
752
+ profile,
753
+ settings,
754
+ };
755
+ }
756
+ if (state === "Zustand") {
757
+ return {
758
+ login: `import React, { useState } from "react";
759
+ import { Button, TextInput, View } from "react-native";
760
+ import { loginApi } from "../../api";
761
+ import { useAuthStore } from "../../store/authStore";
762
+
763
+ export default function LoginScreen() {
764
+ const login = useAuthStore((state) => state.login);
765
+ const [email, setEmail] = useState("");
766
+ const [password, setPassword] = useState("");
767
+
768
+ const handleLogin = async () => {
769
+ const token = await loginApi(email, password);
770
+ await login(token);
771
+ };
772
+
773
+ return (
774
+ <View style={{ padding: 20 }}>
775
+ <TextInput placeholder="Email" value={email} onChangeText={setEmail} />
776
+ <TextInput
777
+ placeholder="Password"
778
+ value={password}
779
+ onChangeText={setPassword}
780
+ secureTextEntry
781
+ />
782
+ <Button title="Login" onPress={() => void handleLogin()} />
783
+ </View>
784
+ );
785
+ }
786
+ `,
787
+ register,
788
+ home: `import React from "react";
789
+ import { Button, Text, View } from "react-native";
790
+ import { useAuthStore } from "../../store/authStore";
791
+
792
+ export default function HomeScreen() {
793
+ const logout = useAuthStore((state) => state.logout);
794
+
795
+ return (
796
+ <View style={{ padding: 20 }}>
797
+ <Text>Welcome Home!</Text>
798
+ <Button title="Logout" onPress={() => void logout()} />
799
+ </View>
800
+ );
801
+ }
802
+ `,
803
+ profile,
804
+ settings,
805
+ };
806
+ }
807
+ return {
808
+ login: `import React, { useContext, useState } from "react";
809
+ import { Button, TextInput, View } from "react-native";
810
+ import { loginApi } from "../../api";
811
+ import { AuthContext } from "../../context/AuthContext";
812
+
813
+ export default function LoginScreen() {
814
+ const { login } = useContext(AuthContext);
815
+ const [email, setEmail] = useState("");
816
+ const [password, setPassword] = useState("");
817
+
818
+ const handleLogin = async () => {
819
+ const token = await loginApi(email, password);
820
+ await login(token);
821
+ };
822
+
823
+ return (
824
+ <View style={{ padding: 20 }}>
825
+ <TextInput placeholder="Email" value={email} onChangeText={setEmail} />
826
+ <TextInput
827
+ placeholder="Password"
828
+ value={password}
829
+ onChangeText={setPassword}
830
+ secureTextEntry
831
+ />
832
+ <Button title="Login" onPress={() => void handleLogin()} />
833
+ </View>
834
+ );
835
+ }
836
+ `,
837
+ register,
838
+ home: `import React, { useContext } from "react";
839
+ import { Button, Text, View } from "react-native";
840
+ import { AuthContext } from "../../context/AuthContext";
841
+
842
+ export default function HomeScreen() {
843
+ const { logout } = useContext(AuthContext);
844
+
845
+ return (
846
+ <View style={{ padding: 20 }}>
847
+ <Text>Welcome Home!</Text>
848
+ <Button title="Logout" onPress={() => void logout()} />
849
+ </View>
850
+ );
851
+ }
852
+ `,
853
+ profile,
854
+ settings,
855
+ };
856
+ }
857
+ async function writeAuthAppShell(targetPath, state) {
858
+ const appPath = path_1.default.join(targetPath, "App.tsx");
859
+ const byState = {
860
+ "Context API": `import React from "react";
861
+ import { NavigationContainer } from "@react-navigation/native";
862
+ import { AuthProvider } from "./context/AuthContext";
863
+ import ProtectedStack from "./navigation/ProtectedStack";
864
+
865
+ export default function App() {
866
+ return (
867
+ <AuthProvider>
868
+ <NavigationContainer>
869
+ <ProtectedStack />
870
+ </NavigationContainer>
871
+ </AuthProvider>
872
+ );
873
+ }
874
+ `,
875
+ "Redux Toolkit": `import React from "react";
876
+ import { NavigationContainer } from "@react-navigation/native";
877
+ import { Provider } from "react-redux";
878
+ import ProtectedStack from "./navigation/ProtectedStack";
879
+ import { store } from "./store/store";
880
+
881
+ export default function App() {
882
+ return (
883
+ <Provider store={store}>
884
+ <NavigationContainer>
885
+ <ProtectedStack />
886
+ </NavigationContainer>
887
+ </Provider>
888
+ );
889
+ }
890
+ `,
891
+ Zustand: `import React from "react";
892
+ import { NavigationContainer } from "@react-navigation/native";
893
+ import ProtectedStack from "./navigation/ProtectedStack";
894
+
895
+ export default function App() {
896
+ return (
897
+ <NavigationContainer>
898
+ <ProtectedStack />
899
+ </NavigationContainer>
900
+ );
901
+ }
902
+ `,
903
+ };
904
+ const content = byState[state];
905
+ if (!content)
906
+ return;
907
+ await fs_extra_1.default.writeFile(appPath, content, "utf8");
908
+ }
232
909
  async function configureTsconfigAlias(targetPath, absoluteImports) {
233
910
  const tsconfigPath = path_1.default.join(targetPath, "tsconfig.json");
234
911
  if (!(await fs_extra_1.default.pathExists(tsconfigPath)))