@highbeek/create-rnstarterkit 1.0.2-beta.1 → 1.0.2-beta.11
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/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/bin/create-rnstarterkit.js +21 -1
- package/dist/src/generators/appGenerator.js +2086 -67
- package/dist/templates/cli-base/.bundle/config +2 -0
- package/dist/templates/cli-base/.eslintrc.js +4 -0
- package/dist/templates/cli-base/.prettierrc.js +5 -0
- package/dist/templates/cli-base/.watchmanconfig +1 -0
- package/dist/templates/cli-base/App.tsx +223 -0
- package/dist/templates/cli-base/Gemfile +16 -0
- package/dist/templates/cli-base/Gemfile.lock +169 -0
- package/dist/templates/cli-base/README.md +97 -0
- package/dist/templates/cli-base/__tests__/App.test.tsx +13 -0
- package/dist/templates/cli-base/android/app/build.gradle +119 -0
- package/dist/templates/cli-base/android/app/debug.keystore +0 -0
- package/dist/templates/cli-base/android/app/proguard-rules.pro +10 -0
- package/dist/templates/cli-base/android/app/src/main/AndroidManifest.xml +27 -0
- package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainActivity.kt +22 -0
- package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainApplication.kt +27 -0
- package/dist/templates/cli-base/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/values/strings.xml +3 -0
- package/dist/templates/cli-base/android/app/src/main/res/values/styles.xml +9 -0
- package/dist/templates/cli-base/android/build.gradle +21 -0
- package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/dist/templates/cli-base/android/gradle.properties +44 -0
- package/dist/templates/cli-base/android/gradlew +251 -0
- package/dist/templates/cli-base/android/gradlew.bat +99 -0
- package/dist/templates/cli-base/android/settings.gradle +6 -0
- package/dist/templates/cli-base/app.json +4 -0
- package/dist/templates/cli-base/assets/images/icon.png +0 -0
- package/dist/templates/cli-base/assets/images/partial-react-logo.png +0 -0
- package/dist/templates/cli-base/assets/images/react-logo.png +0 -0
- package/dist/templates/cli-base/babel.config.js +3 -0
- package/dist/templates/cli-base/index.js +9 -0
- package/dist/templates/cli-base/ios/.xcode.env +11 -0
- package/dist/templates/cli-base/ios/BaseApp/AppDelegate.swift +48 -0
- package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/Contents.json +6 -0
- package/dist/templates/cli-base/ios/BaseApp/Info.plist +60 -0
- package/dist/templates/cli-base/ios/BaseApp/LaunchScreen.storyboard +47 -0
- package/dist/templates/cli-base/ios/BaseApp/PrivacyInfo.xcprivacy +37 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +494 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/xcshareddata/xcschemes/BaseApp.xcscheme +88 -0
- package/dist/templates/cli-base/ios/BaseApp.xcworkspace/contents.xcworkspacedata +10 -0
- package/dist/templates/cli-base/ios/Podfile +34 -0
- package/dist/templates/cli-base/ios/Podfile.lock +2165 -0
- package/dist/templates/cli-base/jest.config.js +7 -0
- package/dist/templates/cli-base/metro.config.js +11 -0
- package/dist/templates/cli-base/package-lock.json +11859 -0
- package/dist/templates/cli-base/package.json +43 -0
- package/dist/templates/cli-base/tsconfig.json +9 -0
- package/dist/templates/expo-base/.vscode/extensions.json +1 -0
- package/dist/templates/expo-base/.vscode/settings.json +7 -0
- package/dist/templates/expo-base/README.md +50 -0
- package/dist/templates/expo-base/app/_layout.tsx +12 -0
- package/dist/templates/expo-base/app/index.tsx +168 -0
- package/dist/templates/expo-base/app.json +48 -0
- package/dist/templates/expo-base/assets/images/android-icon-background.png +0 -0
- package/dist/templates/expo-base/assets/images/android-icon-foreground.png +0 -0
- package/dist/templates/expo-base/assets/images/android-icon-monochrome.png +0 -0
- package/dist/templates/expo-base/assets/images/favicon.png +0 -0
- package/dist/templates/expo-base/assets/images/icon.png +0 -0
- package/dist/templates/expo-base/assets/images/partial-react-logo.png +0 -0
- package/dist/templates/expo-base/assets/images/react-logo.png +0 -0
- package/dist/templates/expo-base/assets/images/react-logo@2x.png +0 -0
- package/dist/templates/expo-base/assets/images/react-logo@3x.png +0 -0
- package/dist/templates/expo-base/assets/images/splash-icon.png +0 -0
- package/dist/templates/expo-base/eslint.config.js +10 -0
- package/dist/templates/expo-base/package-lock.json +12916 -0
- package/dist/templates/expo-base/package.json +49 -0
- package/dist/templates/expo-base/tsconfig.json +17 -0
- package/dist/templates/optional/apiClient/api/client.ts +142 -0
- package/dist/templates/optional/apiClient/api/index.ts +1 -0
- package/dist/templates/optional/apiClient/config/env.cli.ts +5 -0
- package/dist/templates/optional/apiClient/config/env.expo.ts +4 -0
- package/dist/templates/optional/apiClient/config/env.ts +1 -0
- package/dist/templates/optional/apiClient/env.d.ts +3 -0
- package/dist/templates/optional/auth-context/api/endpoints/auth.ts +14 -0
- package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-context/context/AuthContext.tsx +47 -0
- package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +66 -0
- package/dist/templates/optional/auth-context/navigation/cli/BottomTabs.tsx +17 -0
- package/dist/templates/optional/auth-context/navigation/expo/BottomTabs.tsx +29 -0
- package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +15 -0
- package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +63 -0
- package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +11 -0
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +63 -0
- package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +11 -0
- package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-context/utils/storage.ts +13 -0
- package/dist/templates/optional/auth-redux/api/endpoints/auth.ts +14 -0
- package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +67 -0
- package/dist/templates/optional/auth-redux/navigation/cli/BottomTabs.tsx +17 -0
- package/dist/templates/optional/auth-redux/navigation/expo/BottomTabs.tsx +31 -0
- package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +16 -0
- package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +64 -0
- package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +11 -0
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +64 -0
- package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +11 -0
- package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-redux/store/authSlice.ts +25 -0
- package/dist/templates/optional/auth-redux/store/store.ts +11 -0
- package/dist/templates/optional/auth-zustand/api/endpoints/auth.ts +14 -0
- package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-zustand/navigation/BottomTabs.tsx +1 -0
- package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +72 -0
- package/dist/templates/optional/auth-zustand/navigation/cli/BottomTabs.tsx +17 -0
- package/dist/templates/optional/auth-zustand/navigation/expo/BottomTabs.tsx +31 -0
- package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +15 -0
- package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +63 -0
- package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +11 -0
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +63 -0
- package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +11 -0
- package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-zustand/store/authStore.ts +30 -0
- package/dist/templates/optional/auth-zustand/utils/storage.ts +13 -0
- package/dist/templates/optional/ci/.github/workflows/ci.yml +32 -0
- package/dist/templates/optional/error-boundary/components/ErrorBoundary.tsx +83 -0
- package/dist/templates/optional/formik/components/formik/FormikInput.tsx +45 -0
- package/dist/templates/optional/formik/components/formik/LoginForm.tsx +60 -0
- package/dist/templates/optional/formik/schemas/auth.schema.ts +17 -0
- package/dist/templates/optional/mmkv/utils/storage.ts +17 -0
- package/dist/templates/optional/react-hook-form/components/rhf/LoginForm.tsx +63 -0
- package/dist/templates/optional/react-hook-form/components/rhf/RHFInput.tsx +50 -0
- package/dist/templates/optional/react-hook-form/schemas/auth.schema.ts +29 -0
- package/dist/templates/optional/react-query/hooks/useAppMutation.ts +16 -0
- package/dist/templates/optional/react-query/hooks/useAppQuery.ts +12 -0
- package/dist/templates/optional/react-query/services/queryClient.ts +14 -0
- package/dist/templates/optional/redux/store/hooks.ts +6 -0
- package/dist/templates/optional/redux/store/store.ts +11 -0
- package/dist/templates/optional/swr/hooks/useSWRFetch.ts +14 -0
- package/dist/templates/optional/swr/providers/SWRProvider.tsx +21 -0
- package/dist/templates/optional/tsconfig.json +17 -0
- package/dist/templates/optional/zustand/store/appStore.ts +13 -0
- package/package.json +2 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"main": "expo-router/entry",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "expo start",
|
|
7
|
+
"android": "expo start --android",
|
|
8
|
+
"ios": "expo start --ios",
|
|
9
|
+
"web": "expo start --web",
|
|
10
|
+
"lint": "expo lint",
|
|
11
|
+
"format": "prettier --write .",
|
|
12
|
+
"type-check": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@expo/vector-icons": "^15.0.3",
|
|
16
|
+
"@react-navigation/bottom-tabs": "^7.4.0",
|
|
17
|
+
"@react-navigation/elements": "^2.6.3",
|
|
18
|
+
"@react-navigation/native": "^7.1.8",
|
|
19
|
+
"expo": "~54.0.33",
|
|
20
|
+
"expo-constants": "~18.0.13",
|
|
21
|
+
"expo-font": "~14.0.11",
|
|
22
|
+
"expo-haptics": "~15.0.8",
|
|
23
|
+
"expo-image": "~3.0.11",
|
|
24
|
+
"expo-linking": "~8.0.11",
|
|
25
|
+
"expo-router": "~6.0.23",
|
|
26
|
+
"expo-splash-screen": "~31.0.13",
|
|
27
|
+
"expo-status-bar": "~3.0.9",
|
|
28
|
+
"expo-symbols": "~1.0.8",
|
|
29
|
+
"expo-system-ui": "~6.0.9",
|
|
30
|
+
"expo-web-browser": "~15.0.10",
|
|
31
|
+
"react": "19.1.0",
|
|
32
|
+
"react-dom": "19.1.0",
|
|
33
|
+
"react-native": "0.81.5",
|
|
34
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
35
|
+
"react-native-worklets": "0.5.1",
|
|
36
|
+
"react-native-reanimated": "~4.1.1",
|
|
37
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
38
|
+
"react-native-screens": "~4.16.0",
|
|
39
|
+
"react-native-web": "~0.21.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "~19.1.0",
|
|
43
|
+
"typescript": "~5.9.2",
|
|
44
|
+
"eslint": "^9.25.0",
|
|
45
|
+
"eslint-config-expo": "~10.0.0",
|
|
46
|
+
"prettier": "^3.5.0"
|
|
47
|
+
},
|
|
48
|
+
"private": true
|
|
49
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { API_BASE_URL } from "../config/env";
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
data: unknown;
|
|
6
|
+
|
|
7
|
+
constructor(status: number, message: string, data: unknown) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ApiError";
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.data = data;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type RequestOptions = {
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
18
|
+
body?: unknown;
|
|
19
|
+
token?: string | null;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RequestInterceptor = (
|
|
24
|
+
input: {
|
|
25
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
26
|
+
path: string;
|
|
27
|
+
options: RequestOptions;
|
|
28
|
+
},
|
|
29
|
+
) =>
|
|
30
|
+
| {
|
|
31
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
32
|
+
path: string;
|
|
33
|
+
options: RequestOptions;
|
|
34
|
+
}
|
|
35
|
+
| Promise<{
|
|
36
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
37
|
+
path: string;
|
|
38
|
+
options: RequestOptions;
|
|
39
|
+
}>;
|
|
40
|
+
type ResponseInterceptor = (response: Response) => Response | Promise<Response>;
|
|
41
|
+
|
|
42
|
+
let authTokenGetter: (() => string | null | undefined) | null = null;
|
|
43
|
+
const requestInterceptors: RequestInterceptor[] = [];
|
|
44
|
+
const responseInterceptors: ResponseInterceptor[] = [];
|
|
45
|
+
|
|
46
|
+
export function setAuthTokenGetter(getter: (() => string | null | undefined) | null) {
|
|
47
|
+
authTokenGetter = getter;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function addRequestInterceptor(interceptor: RequestInterceptor) {
|
|
51
|
+
requestInterceptors.push(interceptor);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function addResponseInterceptor(interceptor: ResponseInterceptor) {
|
|
55
|
+
responseInterceptors.push(interceptor);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildUrl(path: string, query?: RequestOptions["query"]) {
|
|
59
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
60
|
+
const url = new URL(`${API_BASE_URL}${normalizedPath}`);
|
|
61
|
+
|
|
62
|
+
if (query) {
|
|
63
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
64
|
+
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return url.toString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function parseResponse<T>(response: Response): Promise<T> {
|
|
72
|
+
const contentType = response.headers.get("content-type") || "";
|
|
73
|
+
const data = contentType.includes("application/json")
|
|
74
|
+
? await response.json()
|
|
75
|
+
: await response.text();
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const message =
|
|
79
|
+
typeof data === "object" && data !== null && "message" in data
|
|
80
|
+
? String((data as { message: unknown }).message)
|
|
81
|
+
: `Request failed with status ${response.status}`;
|
|
82
|
+
|
|
83
|
+
throw new ApiError(response.status, message, data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return data as T;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function request<T>(
|
|
90
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
91
|
+
path: string,
|
|
92
|
+
options: RequestOptions = {},
|
|
93
|
+
): Promise<T> {
|
|
94
|
+
let requestInput = { method, path, options };
|
|
95
|
+
for (const interceptor of requestInterceptors) {
|
|
96
|
+
requestInput = await interceptor(requestInput);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { headers = {}, query, body, token, signal } = requestInput.options;
|
|
100
|
+
|
|
101
|
+
let response = await fetch(buildUrl(requestInput.path, query), {
|
|
102
|
+
method: requestInput.method,
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
106
|
+
...headers,
|
|
107
|
+
},
|
|
108
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
109
|
+
signal,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
for (const interceptor of responseInterceptors) {
|
|
113
|
+
response = await interceptor(response);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return parseResponse<T>(response);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const apiClient = {
|
|
120
|
+
get: <T>(path: string, options?: Omit<RequestOptions, "body">) =>
|
|
121
|
+
request<T>("GET", path, options),
|
|
122
|
+
post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
|
|
123
|
+
request<T>("POST", path, { ...options, body }),
|
|
124
|
+
put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
|
|
125
|
+
request<T>("PUT", path, { ...options, body }),
|
|
126
|
+
patch: <T>(path: string, body?: unknown, options?: RequestOptions) =>
|
|
127
|
+
request<T>("PATCH", path, { ...options, body }),
|
|
128
|
+
delete: <T>(path: string, options?: Omit<RequestOptions, "body">) =>
|
|
129
|
+
request<T>("DELETE", path, options),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Default interceptor setup: attach bearer token if configured.
|
|
133
|
+
addRequestInterceptor((input) => {
|
|
134
|
+
const token = input.options.token ?? authTokenGetter?.() ?? null;
|
|
135
|
+
return {
|
|
136
|
+
...input,
|
|
137
|
+
options: {
|
|
138
|
+
...input.options,
|
|
139
|
+
token,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { API_BASE_URL } from "./env.expo";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function assertCredentials(email: string, password: string) {
|
|
2
|
+
if (!email.trim()) throw new Error("Email is required");
|
|
3
|
+
if (!password.trim()) throw new Error("Password is required");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const loginApi = async (email: string, password: string) => {
|
|
7
|
+
assertCredentials(email, password);
|
|
8
|
+
return `demo-token-${email.trim().toLowerCase()}`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const registerApi = async (email: string, password: string) => {
|
|
12
|
+
assertCredentials(email, password);
|
|
13
|
+
return `demo-token-${email.trim().toLowerCase()}`;
|
|
14
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
3
|
+
import { Edge, SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
|
|
5
|
+
type ScreenLayoutProps = {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
padded?: boolean;
|
|
8
|
+
centered?: boolean;
|
|
9
|
+
edges?: Edge[];
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function ScreenLayout({
|
|
14
|
+
children,
|
|
15
|
+
padded = true,
|
|
16
|
+
centered = false,
|
|
17
|
+
edges = ["top", "left", "right"],
|
|
18
|
+
style,
|
|
19
|
+
}: ScreenLayoutProps) {
|
|
20
|
+
return (
|
|
21
|
+
<SafeAreaView
|
|
22
|
+
edges={edges}
|
|
23
|
+
style={[
|
|
24
|
+
styles.container,
|
|
25
|
+
padded && styles.padded,
|
|
26
|
+
centered && styles.centered,
|
|
27
|
+
style,
|
|
28
|
+
]}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</SafeAreaView>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const styles = StyleSheet.create({
|
|
36
|
+
container: {
|
|
37
|
+
flex: 1,
|
|
38
|
+
},
|
|
39
|
+
padded: {
|
|
40
|
+
padding: 20,
|
|
41
|
+
},
|
|
42
|
+
centered: {
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
justifyContent: "center",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { createContext, ReactNode, useEffect, useState } from "react";
|
|
2
|
+
import { getToken, removeToken, storeToken } from "../utils/storage";
|
|
3
|
+
|
|
4
|
+
interface AuthContextType {
|
|
5
|
+
token: string | null;
|
|
6
|
+
isHydrated: boolean;
|
|
7
|
+
login: (token: string) => Promise<void>;
|
|
8
|
+
logout: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const AuthContext = createContext<AuthContextType>({
|
|
12
|
+
token: null,
|
|
13
|
+
isHydrated: false,
|
|
14
|
+
login: async () => {},
|
|
15
|
+
logout: async () => {},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|
19
|
+
const [token, setToken] = useState<string | null>(null);
|
|
20
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const hydrate = async () => {
|
|
24
|
+
const savedToken = await getToken();
|
|
25
|
+
setToken(savedToken);
|
|
26
|
+
setIsHydrated(true);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
void hydrate();
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const login = async (nextToken: string) => {
|
|
33
|
+
setToken(nextToken);
|
|
34
|
+
await storeToken(nextToken);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const logout = async () => {
|
|
38
|
+
setToken(null);
|
|
39
|
+
await removeToken();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<AuthContext.Provider value={{ token, isHydrated, login, logout }}>
|
|
44
|
+
{children}
|
|
45
|
+
</AuthContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useState } from "react";
|
|
2
|
+
import { ActivityIndicator } from "react-native";
|
|
3
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4
|
+
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
5
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
6
|
+
import { AuthContext } from "../context/AuthContext";
|
|
7
|
+
import LoginScreen from "../screens/LoginScreen";
|
|
8
|
+
import RegisterScreen from "../screens/RegisterScreen";
|
|
9
|
+
import WelcomeScreen from "../screens/WelcomeScreen";
|
|
10
|
+
import BottomTabs from "./BottomTabs";
|
|
11
|
+
|
|
12
|
+
const Stack = createNativeStackNavigator();
|
|
13
|
+
const WELCOME_SEEN_KEY = "@rnskit_has_seen_welcome";
|
|
14
|
+
|
|
15
|
+
export default function ProtectedStack() {
|
|
16
|
+
const { token, isHydrated } = useContext(AuthContext);
|
|
17
|
+
const [showWelcome, setShowWelcome] = useState(false);
|
|
18
|
+
const [isCheckingWelcome, setIsCheckingWelcome] = useState(true);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const readWelcomeState = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const seen = await AsyncStorage.getItem(WELCOME_SEEN_KEY);
|
|
24
|
+
setShowWelcome(seen !== "true");
|
|
25
|
+
} finally {
|
|
26
|
+
setIsCheckingWelcome(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
void readWelcomeState();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const handleWelcomeContinue = async () => {
|
|
34
|
+
await AsyncStorage.setItem(WELCOME_SEEN_KEY, "true");
|
|
35
|
+
setShowWelcome(false);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!isHydrated || isCheckingWelcome) {
|
|
39
|
+
return (
|
|
40
|
+
<ScreenLayout centered padded={false}>
|
|
41
|
+
<ActivityIndicator />
|
|
42
|
+
</ScreenLayout>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (showWelcome) {
|
|
47
|
+
return <WelcomeScreen onContinue={() => void handleWelcomeContinue()} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Stack.Navigator>
|
|
52
|
+
{token ? (
|
|
53
|
+
<Stack.Screen
|
|
54
|
+
name="Main"
|
|
55
|
+
component={BottomTabs}
|
|
56
|
+
options={{ headerShown: false }}
|
|
57
|
+
/>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
<Stack.Screen name="Login" component={LoginScreen} />
|
|
61
|
+
<Stack.Screen name="Register" component={RegisterScreen} />
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</Stack.Navigator>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
|
3
|
+
import HomeScreen from "../../screens/HomeScreen";
|
|
4
|
+
import ProfileScreen from "../../screens/ProfileScreen";
|
|
5
|
+
import SettingsScreen from "../../screens/SettingsScreen";
|
|
6
|
+
|
|
7
|
+
const Tab = createBottomTabNavigator();
|
|
8
|
+
|
|
9
|
+
export default function BottomTabs() {
|
|
10
|
+
return (
|
|
11
|
+
<Tab.Navigator screenOptions={{ headerShown: false }}>
|
|
12
|
+
<Tab.Screen name="Home" component={HomeScreen} />
|
|
13
|
+
<Tab.Screen name="Profile" component={ProfileScreen} />
|
|
14
|
+
<Tab.Screen name="Settings" component={SettingsScreen} />
|
|
15
|
+
</Tab.Navigator>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
|
3
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
4
|
+
import HomeScreen from "../../screens/HomeScreen";
|
|
5
|
+
import ProfileScreen from "../../screens/ProfileScreen";
|
|
6
|
+
import SettingsScreen from "../../screens/SettingsScreen";
|
|
7
|
+
|
|
8
|
+
const Tab = createBottomTabNavigator();
|
|
9
|
+
|
|
10
|
+
export default function BottomTabs() {
|
|
11
|
+
return (
|
|
12
|
+
<Tab.Navigator
|
|
13
|
+
screenOptions={({ route }) => ({
|
|
14
|
+
headerShown: false,
|
|
15
|
+
tabBarIcon: ({ color, size }) => {
|
|
16
|
+
let iconName: string = "home";
|
|
17
|
+
if (route.name === "Home") iconName = "home";
|
|
18
|
+
else if (route.name === "Profile") iconName = "person";
|
|
19
|
+
else if (route.name === "Settings") iconName = "settings";
|
|
20
|
+
return <Ionicons name={iconName as any} size={size} color={color} />;
|
|
21
|
+
},
|
|
22
|
+
})}
|
|
23
|
+
>
|
|
24
|
+
<Tab.Screen name="Home" component={HomeScreen} />
|
|
25
|
+
<Tab.Screen name="Profile" component={ProfileScreen} />
|
|
26
|
+
<Tab.Screen name="Settings" component={SettingsScreen} />
|
|
27
|
+
</Tab.Navigator>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
import { Text, Button } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
4
|
+
import { AuthContext } from "../context/AuthContext";
|
|
5
|
+
|
|
6
|
+
export default function HomeScreen() {
|
|
7
|
+
const { logout } = useContext(AuthContext);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<ScreenLayout>
|
|
11
|
+
<Text>Welcome Home!</Text>
|
|
12
|
+
<Button title="Logout" onPress={() => void logout()} />
|
|
13
|
+
</ScreenLayout>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useContext, useState } from "react";
|
|
2
|
+
import { TextInput, Button, Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
4
|
+
import { AuthContext } from "../context/AuthContext";
|
|
5
|
+
import { loginApi } from "../api";
|
|
6
|
+
|
|
7
|
+
export default function LoginScreen() {
|
|
8
|
+
const { login } = useContext(AuthContext);
|
|
9
|
+
const [email, setEmail] = useState("");
|
|
10
|
+
const [password, setPassword] = useState("");
|
|
11
|
+
const [error, setError] = useState("");
|
|
12
|
+
|
|
13
|
+
const handleLogin = async () => {
|
|
14
|
+
try {
|
|
15
|
+
setError("");
|
|
16
|
+
const token = await loginApi(email, password);
|
|
17
|
+
await login(token);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
setError(err instanceof Error ? err.message : "Login failed");
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ScreenLayout>
|
|
25
|
+
<TextInput
|
|
26
|
+
placeholder="Email"
|
|
27
|
+
value={email}
|
|
28
|
+
onChangeText={setEmail}
|
|
29
|
+
autoCapitalize="none"
|
|
30
|
+
autoCorrect={false}
|
|
31
|
+
keyboardType="email-address"
|
|
32
|
+
placeholderTextColor="#888"
|
|
33
|
+
style={{
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: "#ccc",
|
|
36
|
+
borderRadius: 8,
|
|
37
|
+
paddingHorizontal: 12,
|
|
38
|
+
paddingVertical: 10,
|
|
39
|
+
color: "#111",
|
|
40
|
+
marginBottom: 12,
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
<TextInput
|
|
44
|
+
placeholder="Password"
|
|
45
|
+
value={password}
|
|
46
|
+
onChangeText={setPassword}
|
|
47
|
+
secureTextEntry
|
|
48
|
+
placeholderTextColor="#888"
|
|
49
|
+
style={{
|
|
50
|
+
borderWidth: 1,
|
|
51
|
+
borderColor: "#ccc",
|
|
52
|
+
borderRadius: 8,
|
|
53
|
+
paddingHorizontal: 12,
|
|
54
|
+
paddingVertical: 10,
|
|
55
|
+
color: "#111",
|
|
56
|
+
marginBottom: 12,
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
{!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
|
|
60
|
+
<Button title="Login" onPress={() => void handleLogin()} />
|
|
61
|
+
</ScreenLayout>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
4
|
+
|
|
5
|
+
export default function ProfileScreen() {
|
|
6
|
+
return (
|
|
7
|
+
<ScreenLayout>
|
|
8
|
+
<Text>This is the Profile Screen</Text>
|
|
9
|
+
</ScreenLayout>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useContext, useState } from "react";
|
|
2
|
+
import { TextInput, Button, Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
4
|
+
import { AuthContext } from "../context/AuthContext";
|
|
5
|
+
import { registerApi } from "../api";
|
|
6
|
+
|
|
7
|
+
export default function RegisterScreen() {
|
|
8
|
+
const { login } = useContext(AuthContext);
|
|
9
|
+
const [email, setEmail] = useState("");
|
|
10
|
+
const [password, setPassword] = useState("");
|
|
11
|
+
const [error, setError] = useState("");
|
|
12
|
+
|
|
13
|
+
const handleRegister = async () => {
|
|
14
|
+
try {
|
|
15
|
+
setError("");
|
|
16
|
+
const token = await registerApi(email, password);
|
|
17
|
+
await login(token);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
setError(err instanceof Error ? err.message : "Registration failed");
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ScreenLayout>
|
|
25
|
+
<TextInput
|
|
26
|
+
placeholder="Email"
|
|
27
|
+
value={email}
|
|
28
|
+
onChangeText={setEmail}
|
|
29
|
+
autoCapitalize="none"
|
|
30
|
+
autoCorrect={false}
|
|
31
|
+
keyboardType="email-address"
|
|
32
|
+
placeholderTextColor="#888"
|
|
33
|
+
style={{
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: "#ccc",
|
|
36
|
+
borderRadius: 8,
|
|
37
|
+
paddingHorizontal: 12,
|
|
38
|
+
paddingVertical: 10,
|
|
39
|
+
color: "#111",
|
|
40
|
+
marginBottom: 12,
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
<TextInput
|
|
44
|
+
placeholder="Password"
|
|
45
|
+
value={password}
|
|
46
|
+
onChangeText={setPassword}
|
|
47
|
+
secureTextEntry
|
|
48
|
+
placeholderTextColor="#888"
|
|
49
|
+
style={{
|
|
50
|
+
borderWidth: 1,
|
|
51
|
+
borderColor: "#ccc",
|
|
52
|
+
borderRadius: 8,
|
|
53
|
+
paddingHorizontal: 12,
|
|
54
|
+
paddingVertical: 10,
|
|
55
|
+
color: "#111",
|
|
56
|
+
marginBottom: 12,
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
{!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
|
|
60
|
+
<Button title="Register" onPress={() => void handleRegister()} />
|
|
61
|
+
</ScreenLayout>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
4
|
+
|
|
5
|
+
export default function SettingsScreen() {
|
|
6
|
+
return (
|
|
7
|
+
<ScreenLayout>
|
|
8
|
+
<Text>This is the Settings Screen</Text>
|
|
9
|
+
</ScreenLayout>
|
|
10
|
+
);
|
|
11
|
+
}
|