@highbeek/create-rnstarterkit 1.0.0 → 1.0.1-beta.0
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/bin/create-rnstarterkit.js +21 -7
- package/dist/src/generators/appGenerator.js +976 -60
- 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 +45 -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/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 +3 -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 +41 -0
- package/dist/templates/cli-base/tsconfig.json +8 -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 +9 -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/components/ui/collapsible.tsx +45 -0
- package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +32 -0
- package/dist/templates/expo-base/components/ui/icon-symbol.tsx +41 -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 +46 -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/context/AuthContext.tsx +47 -0
- package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +38 -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/utils/storage.ts +13 -0
- package/dist/templates/optional/auth-redux/api/endpoints/auth.ts +14 -0
- package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +30 -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 +15 -0
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +64 -0
- package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +15 -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/navigation/BottomTabs.tsx +1 -0
- package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +44 -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/store/authStore.ts +30 -0
- package/dist/templates/optional/auth-zustand/utils/storage.ts +13 -0
- package/package.json +2 -2
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@expo/vector-icons": "^15.0.3",
|
|
14
|
+
"@react-navigation/bottom-tabs": "^7.4.0",
|
|
15
|
+
"@react-navigation/elements": "^2.6.3",
|
|
16
|
+
"@react-navigation/native": "^7.1.8",
|
|
17
|
+
"expo": "~54.0.33",
|
|
18
|
+
"expo-constants": "~18.0.13",
|
|
19
|
+
"expo-font": "~14.0.11",
|
|
20
|
+
"expo-haptics": "~15.0.8",
|
|
21
|
+
"expo-image": "~3.0.11",
|
|
22
|
+
"expo-linking": "~8.0.11",
|
|
23
|
+
"expo-router": "~6.0.23",
|
|
24
|
+
"expo-splash-screen": "~31.0.13",
|
|
25
|
+
"expo-status-bar": "~3.0.9",
|
|
26
|
+
"expo-symbols": "~1.0.8",
|
|
27
|
+
"expo-system-ui": "~6.0.9",
|
|
28
|
+
"expo-web-browser": "~15.0.10",
|
|
29
|
+
"react": "19.1.0",
|
|
30
|
+
"react-dom": "19.1.0",
|
|
31
|
+
"react-native": "0.81.5",
|
|
32
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
33
|
+
"react-native-worklets": "0.5.1",
|
|
34
|
+
"react-native-reanimated": "~4.1.1",
|
|
35
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
36
|
+
"react-native-screens": "~4.16.0",
|
|
37
|
+
"react-native-web": "~0.21.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "~19.1.0",
|
|
41
|
+
"typescript": "~5.9.2",
|
|
42
|
+
"eslint": "^9.25.0",
|
|
43
|
+
"eslint-config-expo": "~10.0.0"
|
|
44
|
+
},
|
|
45
|
+
"private": true
|
|
46
|
+
}
|
|
@@ -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,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,38 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
import { ActivityIndicator, View } from "react-native";
|
|
3
|
+
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
4
|
+
import { AuthContext } from "../context/AuthContext";
|
|
5
|
+
import LoginScreen from "../screens/LoginScreen";
|
|
6
|
+
import RegisterScreen from "../screens/RegisterScreen";
|
|
7
|
+
import BottomTabs from "./BottomTabs";
|
|
8
|
+
|
|
9
|
+
const Stack = createNativeStackNavigator();
|
|
10
|
+
|
|
11
|
+
export default function ProtectedStack() {
|
|
12
|
+
const { token, isHydrated } = useContext(AuthContext);
|
|
13
|
+
|
|
14
|
+
if (!isHydrated) {
|
|
15
|
+
return (
|
|
16
|
+
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
|
|
17
|
+
<ActivityIndicator />
|
|
18
|
+
</View>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Stack.Navigator>
|
|
24
|
+
{token ? (
|
|
25
|
+
<Stack.Screen
|
|
26
|
+
name="Main"
|
|
27
|
+
component={BottomTabs}
|
|
28
|
+
options={{ headerShown: false }}
|
|
29
|
+
/>
|
|
30
|
+
) : (
|
|
31
|
+
<>
|
|
32
|
+
<Stack.Screen name="Login" component={LoginScreen} />
|
|
33
|
+
<Stack.Screen name="Register" component={RegisterScreen} />
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</Stack.Navigator>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -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 { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
import { AuthContext } from "../context/AuthContext";
|
|
5
|
+
|
|
6
|
+
export default function HomeScreen() {
|
|
7
|
+
const { logout } = useContext(AuthContext);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<SafeAreaView style={{ flex: 1, padding: 20 }}>
|
|
11
|
+
<Text>Welcome Home!</Text>
|
|
12
|
+
<Button title="Logout" onPress={() => void logout()} />
|
|
13
|
+
</SafeAreaView>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useContext, useState } from "react";
|
|
2
|
+
import { TextInput, Button, Text } from "react-native";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
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
|
+
<SafeAreaView style={{ flex: 1, padding: 20 }}>
|
|
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
|
+
</SafeAreaView>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
|
|
5
|
+
export default function ProfileScreen() {
|
|
6
|
+
return (
|
|
7
|
+
<SafeAreaView style={{ flex: 1, padding: 20 }}>
|
|
8
|
+
<Text>This is the Profile Screen</Text>
|
|
9
|
+
</SafeAreaView>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useContext, useState } from "react";
|
|
2
|
+
import { TextInput, Button, Text } from "react-native";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
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
|
+
<SafeAreaView style={{ flex: 1, padding: 20 }}>
|
|
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
|
+
</SafeAreaView>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
|
|
5
|
+
export default function SettingsScreen() {
|
|
6
|
+
return (
|
|
7
|
+
<SafeAreaView style={{ flex: 1, padding: 20 }}>
|
|
8
|
+
<Text>This is the Settings Screen</Text>
|
|
9
|
+
</SafeAreaView>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
2
|
+
|
|
3
|
+
export const storeToken = async (token: string) => {
|
|
4
|
+
await AsyncStorage.setItem("@token", token);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const getToken = async () => {
|
|
8
|
+
return await AsyncStorage.getItem("@token");
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const removeToken = async () => {
|
|
12
|
+
await AsyncStorage.removeItem("@token");
|
|
13
|
+
};
|
|
@@ -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,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
3
|
+
import { useSelector } from "react-redux";
|
|
4
|
+
import type { RootState } from "../store/store";
|
|
5
|
+
import LoginScreen from "../screens/LoginScreen";
|
|
6
|
+
import RegisterScreen from "../screens/RegisterScreen";
|
|
7
|
+
import BottomTabs from "./BottomTabs";
|
|
8
|
+
|
|
9
|
+
const Stack = createNativeStackNavigator();
|
|
10
|
+
|
|
11
|
+
export default function ProtectedStack() {
|
|
12
|
+
const token = useSelector((state: RootState) => state.auth.token);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Stack.Navigator>
|
|
16
|
+
{token ? (
|
|
17
|
+
<Stack.Screen
|
|
18
|
+
name="Main"
|
|
19
|
+
component={BottomTabs}
|
|
20
|
+
options={{ headerShown: false }}
|
|
21
|
+
/>
|
|
22
|
+
) : (
|
|
23
|
+
<>
|
|
24
|
+
<Stack.Screen name="Login" component={LoginScreen} />
|
|
25
|
+
<Stack.Screen name="Register" component={RegisterScreen} />
|
|
26
|
+
</>
|
|
27
|
+
)}
|
|
28
|
+
</Stack.Navigator>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -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,31 @@
|
|
|
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
|
+
|
|
18
|
+
if (route.name === "Home") iconName = "home";
|
|
19
|
+
else if (route.name === "Profile") iconName = "person";
|
|
20
|
+
else if (route.name === "Settings") iconName = "settings";
|
|
21
|
+
|
|
22
|
+
return <Ionicons name={iconName as any} size={size} color={color} />;
|
|
23
|
+
},
|
|
24
|
+
})}
|
|
25
|
+
>
|
|
26
|
+
<Tab.Screen name="Home" component={HomeScreen} />
|
|
27
|
+
<Tab.Screen name="Profile" component={ProfileScreen} />
|
|
28
|
+
<Tab.Screen name="Settings" component={SettingsScreen} />
|
|
29
|
+
</Tab.Navigator>
|
|
30
|
+
);
|
|
31
|
+
}
|