@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.
- package/dist/src/generators/appGenerator.js +620 -21
- package/dist/templates/expo-base/app/_layout.tsx +4 -19
- package/dist/templates/expo-base/app/index.tsx +9 -0
- package/dist/templates/expo-base/app.json +2 -0
- package/dist/templates/expo-base/package.json +2 -1
- package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +1 -1
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +1 -1
- package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +1 -1
- package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +1 -1
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +1 -1
- package/package.json +1 -1
- package/dist/templates/expo-base/App.tsx +0 -32
- package/dist/templates/expo-base/app/(tabs)/_layout.tsx +0 -35
- package/dist/templates/expo-base/app/(tabs)/explore.tsx +0 -112
- package/dist/templates/expo-base/app/(tabs)/index.tsx +0 -98
- package/dist/templates/expo-base/app/modal.tsx +0 -29
- package/dist/templates/expo-base/components/external-link.tsx +0 -25
- package/dist/templates/expo-base/components/haptic-tab.tsx +0 -18
- package/dist/templates/expo-base/components/hello-wave.tsx +0 -19
- package/dist/templates/expo-base/components/parallax-scroll-view.tsx +0 -79
- package/dist/templates/expo-base/components/themed-text.tsx +0 -60
- package/dist/templates/expo-base/components/themed-view.tsx +0 -14
- package/dist/templates/expo-base/constants/theme.ts +0 -53
- package/dist/templates/expo-base/hooks/use-color-scheme.ts +0 -1
- package/dist/templates/expo-base/hooks/use-color-scheme.web.ts +0 -21
- package/dist/templates/expo-base/hooks/use-theme-color.ts +0 -21
- package/dist/templates/expo-base/scripts/reset-project.js +0 -112
- package/dist/templates/optional/apiClient/api/client.axios.ts +0 -124
- /package/dist/templates/optional/auth-context/api/{authApi.ts → endpoints/auth.ts} +0 -0
- /package/dist/templates/optional/auth-redux/api/{authApi.ts → endpoints/auth.ts} +0 -0
- /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
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
222
|
-
|
|
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 = {
|