@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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -0
  3. package/dist/bin/create-rnstarterkit.js +21 -1
  4. package/dist/src/generators/appGenerator.js +2086 -67
  5. package/dist/templates/cli-base/.bundle/config +2 -0
  6. package/dist/templates/cli-base/.eslintrc.js +4 -0
  7. package/dist/templates/cli-base/.prettierrc.js +5 -0
  8. package/dist/templates/cli-base/.watchmanconfig +1 -0
  9. package/dist/templates/cli-base/App.tsx +223 -0
  10. package/dist/templates/cli-base/Gemfile +16 -0
  11. package/dist/templates/cli-base/Gemfile.lock +169 -0
  12. package/dist/templates/cli-base/README.md +97 -0
  13. package/dist/templates/cli-base/__tests__/App.test.tsx +13 -0
  14. package/dist/templates/cli-base/android/app/build.gradle +119 -0
  15. package/dist/templates/cli-base/android/app/debug.keystore +0 -0
  16. package/dist/templates/cli-base/android/app/proguard-rules.pro +10 -0
  17. package/dist/templates/cli-base/android/app/src/main/AndroidManifest.xml +27 -0
  18. package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainActivity.kt +22 -0
  19. package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainApplication.kt +27 -0
  20. package/dist/templates/cli-base/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  21. package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  22. package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  23. package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  24. package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  25. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  26. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  27. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  28. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  29. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  30. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  31. package/dist/templates/cli-base/android/app/src/main/res/values/strings.xml +3 -0
  32. package/dist/templates/cli-base/android/app/src/main/res/values/styles.xml +9 -0
  33. package/dist/templates/cli-base/android/build.gradle +21 -0
  34. package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  35. package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  36. package/dist/templates/cli-base/android/gradle.properties +44 -0
  37. package/dist/templates/cli-base/android/gradlew +251 -0
  38. package/dist/templates/cli-base/android/gradlew.bat +99 -0
  39. package/dist/templates/cli-base/android/settings.gradle +6 -0
  40. package/dist/templates/cli-base/app.json +4 -0
  41. package/dist/templates/cli-base/assets/images/icon.png +0 -0
  42. package/dist/templates/cli-base/assets/images/partial-react-logo.png +0 -0
  43. package/dist/templates/cli-base/assets/images/react-logo.png +0 -0
  44. package/dist/templates/cli-base/babel.config.js +3 -0
  45. package/dist/templates/cli-base/index.js +9 -0
  46. package/dist/templates/cli-base/ios/.xcode.env +11 -0
  47. package/dist/templates/cli-base/ios/BaseApp/AppDelegate.swift +48 -0
  48. package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  49. package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/Contents.json +6 -0
  50. package/dist/templates/cli-base/ios/BaseApp/Info.plist +60 -0
  51. package/dist/templates/cli-base/ios/BaseApp/LaunchScreen.storyboard +47 -0
  52. package/dist/templates/cli-base/ios/BaseApp/PrivacyInfo.xcprivacy +37 -0
  53. package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +494 -0
  54. package/dist/templates/cli-base/ios/BaseApp.xcodeproj/xcshareddata/xcschemes/BaseApp.xcscheme +88 -0
  55. package/dist/templates/cli-base/ios/BaseApp.xcworkspace/contents.xcworkspacedata +10 -0
  56. package/dist/templates/cli-base/ios/Podfile +34 -0
  57. package/dist/templates/cli-base/ios/Podfile.lock +2165 -0
  58. package/dist/templates/cli-base/jest.config.js +7 -0
  59. package/dist/templates/cli-base/metro.config.js +11 -0
  60. package/dist/templates/cli-base/package-lock.json +11859 -0
  61. package/dist/templates/cli-base/package.json +43 -0
  62. package/dist/templates/cli-base/tsconfig.json +9 -0
  63. package/dist/templates/expo-base/.vscode/extensions.json +1 -0
  64. package/dist/templates/expo-base/.vscode/settings.json +7 -0
  65. package/dist/templates/expo-base/README.md +50 -0
  66. package/dist/templates/expo-base/app/_layout.tsx +12 -0
  67. package/dist/templates/expo-base/app/index.tsx +168 -0
  68. package/dist/templates/expo-base/app.json +48 -0
  69. package/dist/templates/expo-base/assets/images/android-icon-background.png +0 -0
  70. package/dist/templates/expo-base/assets/images/android-icon-foreground.png +0 -0
  71. package/dist/templates/expo-base/assets/images/android-icon-monochrome.png +0 -0
  72. package/dist/templates/expo-base/assets/images/favicon.png +0 -0
  73. package/dist/templates/expo-base/assets/images/icon.png +0 -0
  74. package/dist/templates/expo-base/assets/images/partial-react-logo.png +0 -0
  75. package/dist/templates/expo-base/assets/images/react-logo.png +0 -0
  76. package/dist/templates/expo-base/assets/images/react-logo@2x.png +0 -0
  77. package/dist/templates/expo-base/assets/images/react-logo@3x.png +0 -0
  78. package/dist/templates/expo-base/assets/images/splash-icon.png +0 -0
  79. package/dist/templates/expo-base/eslint.config.js +10 -0
  80. package/dist/templates/expo-base/package-lock.json +12916 -0
  81. package/dist/templates/expo-base/package.json +49 -0
  82. package/dist/templates/expo-base/tsconfig.json +17 -0
  83. package/dist/templates/optional/apiClient/api/client.ts +142 -0
  84. package/dist/templates/optional/apiClient/api/index.ts +1 -0
  85. package/dist/templates/optional/apiClient/config/env.cli.ts +5 -0
  86. package/dist/templates/optional/apiClient/config/env.expo.ts +4 -0
  87. package/dist/templates/optional/apiClient/config/env.ts +1 -0
  88. package/dist/templates/optional/apiClient/env.d.ts +3 -0
  89. package/dist/templates/optional/auth-context/api/endpoints/auth.ts +14 -0
  90. package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
  91. package/dist/templates/optional/auth-context/context/AuthContext.tsx +47 -0
  92. package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +66 -0
  93. package/dist/templates/optional/auth-context/navigation/cli/BottomTabs.tsx +17 -0
  94. package/dist/templates/optional/auth-context/navigation/expo/BottomTabs.tsx +29 -0
  95. package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +15 -0
  96. package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +63 -0
  97. package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +11 -0
  98. package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +63 -0
  99. package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +11 -0
  100. package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
  101. package/dist/templates/optional/auth-context/utils/storage.ts +13 -0
  102. package/dist/templates/optional/auth-redux/api/endpoints/auth.ts +14 -0
  103. package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
  104. package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +67 -0
  105. package/dist/templates/optional/auth-redux/navigation/cli/BottomTabs.tsx +17 -0
  106. package/dist/templates/optional/auth-redux/navigation/expo/BottomTabs.tsx +31 -0
  107. package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +16 -0
  108. package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +64 -0
  109. package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +11 -0
  110. package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +64 -0
  111. package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +11 -0
  112. package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
  113. package/dist/templates/optional/auth-redux/store/authSlice.ts +25 -0
  114. package/dist/templates/optional/auth-redux/store/store.ts +11 -0
  115. package/dist/templates/optional/auth-zustand/api/endpoints/auth.ts +14 -0
  116. package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
  117. package/dist/templates/optional/auth-zustand/navigation/BottomTabs.tsx +1 -0
  118. package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +72 -0
  119. package/dist/templates/optional/auth-zustand/navigation/cli/BottomTabs.tsx +17 -0
  120. package/dist/templates/optional/auth-zustand/navigation/expo/BottomTabs.tsx +31 -0
  121. package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +15 -0
  122. package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +63 -0
  123. package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +11 -0
  124. package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +63 -0
  125. package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +11 -0
  126. package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -0
  127. package/dist/templates/optional/auth-zustand/store/authStore.ts +30 -0
  128. package/dist/templates/optional/auth-zustand/utils/storage.ts +13 -0
  129. package/dist/templates/optional/ci/.github/workflows/ci.yml +32 -0
  130. package/dist/templates/optional/error-boundary/components/ErrorBoundary.tsx +83 -0
  131. package/dist/templates/optional/formik/components/formik/FormikInput.tsx +45 -0
  132. package/dist/templates/optional/formik/components/formik/LoginForm.tsx +60 -0
  133. package/dist/templates/optional/formik/schemas/auth.schema.ts +17 -0
  134. package/dist/templates/optional/mmkv/utils/storage.ts +17 -0
  135. package/dist/templates/optional/react-hook-form/components/rhf/LoginForm.tsx +63 -0
  136. package/dist/templates/optional/react-hook-form/components/rhf/RHFInput.tsx +50 -0
  137. package/dist/templates/optional/react-hook-form/schemas/auth.schema.ts +29 -0
  138. package/dist/templates/optional/react-query/hooks/useAppMutation.ts +16 -0
  139. package/dist/templates/optional/react-query/hooks/useAppQuery.ts +12 -0
  140. package/dist/templates/optional/react-query/services/queryClient.ts +14 -0
  141. package/dist/templates/optional/redux/store/hooks.ts +6 -0
  142. package/dist/templates/optional/redux/store/store.ts +11 -0
  143. package/dist/templates/optional/swr/hooks/useSWRFetch.ts +14 -0
  144. package/dist/templates/optional/swr/providers/SWRProvider.tsx +21 -0
  145. package/dist/templates/optional/tsconfig.json +17 -0
  146. package/dist/templates/optional/zustand/store/appStore.ts +13 -0
  147. 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,17 @@
1
+ {
2
+ "extends": "expo/tsconfig.base",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "paths": {
6
+ "@/*": [
7
+ "./*"
8
+ ]
9
+ }
10
+ },
11
+ "include": [
12
+ "**/*.ts",
13
+ "**/*.tsx",
14
+ ".expo/types/**/*.ts",
15
+ "expo-env.d.ts"
16
+ ]
17
+ }
@@ -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,5 @@
1
+ import { API_BASE_URL as DOTENV_API_BASE_URL } from "@env";
2
+
3
+ const FALLBACK_API_URL = "https://example.com/api";
4
+
5
+ export const API_BASE_URL = DOTENV_API_BASE_URL || FALLBACK_API_URL;
@@ -0,0 +1,4 @@
1
+ const FALLBACK_API_URL = "https://example.com/api";
2
+
3
+ export const API_BASE_URL =
4
+ process.env.EXPO_PUBLIC_API_URL ?? process.env.API_BASE_URL ?? FALLBACK_API_URL;
@@ -0,0 +1 @@
1
+ export { API_BASE_URL } from "./env.expo";
@@ -0,0 +1,3 @@
1
+ declare module "@env" {
2
+ export const API_BASE_URL: string;
3
+ }
@@ -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
+ }