@highbeek/create-rnstarterkit 1.0.2-beta.5 → 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 (31) hide show
  1. package/dist/src/generators/appGenerator.js +620 -21
  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/app.json +2 -0
  5. package/dist/templates/expo-base/package.json +2 -1
  6. package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +1 -1
  7. package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +1 -1
  8. package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +1 -1
  9. package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +1 -1
  10. package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +1 -1
  11. package/package.json +1 -1
  12. package/dist/templates/expo-base/App.tsx +0 -32
  13. package/dist/templates/expo-base/app/(tabs)/_layout.tsx +0 -35
  14. package/dist/templates/expo-base/app/(tabs)/explore.tsx +0 -112
  15. package/dist/templates/expo-base/app/(tabs)/index.tsx +0 -98
  16. package/dist/templates/expo-base/app/modal.tsx +0 -29
  17. package/dist/templates/expo-base/components/external-link.tsx +0 -25
  18. package/dist/templates/expo-base/components/haptic-tab.tsx +0 -18
  19. package/dist/templates/expo-base/components/hello-wave.tsx +0 -19
  20. package/dist/templates/expo-base/components/parallax-scroll-view.tsx +0 -79
  21. package/dist/templates/expo-base/components/themed-text.tsx +0 -60
  22. package/dist/templates/expo-base/components/themed-view.tsx +0 -14
  23. package/dist/templates/expo-base/constants/theme.ts +0 -53
  24. package/dist/templates/expo-base/hooks/use-color-scheme.ts +0 -1
  25. package/dist/templates/expo-base/hooks/use-color-scheme.web.ts +0 -21
  26. package/dist/templates/expo-base/hooks/use-theme-color.ts +0 -21
  27. package/dist/templates/expo-base/scripts/reset-project.js +0 -112
  28. package/dist/templates/optional/apiClient/api/client.axios.ts +0 -124
  29. /package/dist/templates/optional/auth-context/api/{authApi.ts → endpoints/auth.ts} +0 -0
  30. /package/dist/templates/optional/auth-redux/api/{authApi.ts → endpoints/auth.ts} +0 -0
  31. /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,8 +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);
33
- await writeAuthAppShell(targetPath, state);
32
+ if (platform === "Expo") {
33
+ await writeExpoRouterAuthRoutes(targetPath, state);
34
+ }
35
+ else {
36
+ await setAuthTabsByPlatform(targetPath, platform);
37
+ await writeAuthAppShell(targetPath, state);
38
+ }
34
39
  }
35
40
  }
36
41
  if (apiClient) {
@@ -46,6 +51,7 @@ async function generateApp(options) {
46
51
  if (storage === "MMKV")
47
52
  await copyOptionalModule("mmkv", targetPath);
48
53
  await configureStateAndAuthDependencies(targetPath, {
54
+ platform,
49
55
  state,
50
56
  auth,
51
57
  storage,
@@ -53,6 +59,7 @@ async function generateApp(options) {
53
59
  await configureAbsoluteImports(targetPath, platform, absoluteImports);
54
60
  await configureDataFetching(targetPath, dataFetching);
55
61
  await configureApiClientTransport(targetPath, apiClient, apiClientType);
62
+ await syncApiIndex(targetPath);
56
63
  await configureValidation(targetPath, validation);
57
64
  if (typescript) {
58
65
  const tsconfigTemplate = path_1.default.join(templateRoot, "optional/tsconfig.json");
@@ -144,26 +151,26 @@ async function ensureCliApiEnvSupport(targetPath) {
144
151
  },
145
152
  });
146
153
  }
147
- async function createStandardStructure(targetPath) {
148
- const directories = [
154
+ async function createStandardStructure(targetPath, platform) {
155
+ const commonDirectories = [
149
156
  "assets",
150
157
  "assets/icons",
151
158
  "assets/images",
152
159
  "assets/fonts",
153
160
  "components",
154
- "navigation",
155
- "screens",
156
161
  "hooks",
157
162
  "utils",
158
163
  "services",
164
+ "api",
165
+ "config",
166
+ "context",
159
167
  ];
168
+ const cliOnlyDirectories = ["navigation", "screens"];
169
+ const directories = platform === "Expo"
170
+ ? commonDirectories
171
+ : [...commonDirectories, ...cliOnlyDirectories];
160
172
  for (const directory of directories) {
161
- const absoluteDir = path_1.default.join(targetPath, directory);
162
- await fs_extra_1.default.ensureDir(absoluteDir);
163
- const gitkeepPath = path_1.default.join(absoluteDir, ".gitkeep");
164
- if (!(await fs_extra_1.default.pathExists(gitkeepPath))) {
165
- await fs_extra_1.default.writeFile(gitkeepPath, "", "utf8");
166
- }
173
+ await fs_extra_1.default.ensureDir(path_1.default.join(targetPath, directory));
167
174
  }
168
175
  }
169
176
  async function configureAbsoluteImports(targetPath, platform, absoluteImports) {
@@ -204,24 +211,156 @@ export const queryClient = new QueryClient();
204
211
  }
205
212
  async function configureApiClientTransport(targetPath, apiClient, apiClientType) {
206
213
  if (apiClient && apiClientType === "Axios") {
207
- const templateRoot = await resolveTemplateRoot();
208
214
  await ensureDependencies(targetPath, {
209
215
  dependencies: {
210
216
  axios: "^1.12.2",
211
217
  },
212
218
  });
213
- 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";
214
225
  import { API_BASE_URL } from "../config/env";
215
226
 
216
- 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({
217
247
  baseURL: API_BASE_URL,
218
248
  timeout: 15000,
219
249
  });
220
- `);
221
- 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"), {
222
- overwrite: true,
223
- });
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));
224
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"));
345
+ }
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");
225
364
  }
226
365
  async function configureValidation(targetPath, validation) {
227
366
  const dependencies = {};
@@ -237,7 +376,7 @@ async function configureValidation(targetPath, validation) {
237
376
  }
238
377
  async function configureStateAndAuthDependencies(targetPath, options) {
239
378
  const dependencies = {};
240
- if (options.auth) {
379
+ if (options.auth && options.platform === "React Native CLI") {
241
380
  dependencies["@react-navigation/native-stack"] = "^7.3.29";
242
381
  }
243
382
  if (options.state === "Redux Toolkit") {
@@ -255,6 +394,466 @@ async function configureStateAndAuthDependencies(targetPath, options) {
255
394
  await ensureDependencies(targetPath, { dependencies });
256
395
  }
257
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
+ }
258
857
  async function writeAuthAppShell(targetPath, state) {
259
858
  const appPath = path_1.default.join(targetPath, "App.tsx");
260
859
  const byState = {