@highbeek/create-rnstarterkit 1.0.2-beta.5 → 1.1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/bin/create-rnstarterkit.js +205 -7
- package/dist/src/generators/appGenerator.js +2474 -58
- package/dist/src/generators/codeGenerator.js +289 -0
- package/dist/templates/cli-base/App.tsx +199 -21
- 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 +1 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +6 -0
- package/dist/templates/cli-base/ios/Podfile +5 -0
- package/dist/templates/cli-base/jest.config.js +4 -0
- package/dist/templates/cli-base/package.json +7 -4
- package/dist/templates/cli-base/tsconfig.json +1 -0
- package/dist/templates/expo-base/app/_layout.tsx +7 -18
- package/dist/templates/expo-base/app/home.tsx +37 -0
- package/dist/templates/expo-base/app/index.tsx +170 -0
- package/dist/templates/expo-base/app.json +2 -1
- package/dist/templates/expo-base/package.json +7 -3
- package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +33 -5
- package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +42 -7
- package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +42 -7
- package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +39 -2
- package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +43 -8
- package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +7 -10
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +61 -11
- package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +6 -9
- package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +34 -6
- package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +42 -7
- package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +42 -7
- package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -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/i18n/src/i18n/hooks/useAppTranslation.ts +28 -0
- package/dist/templates/optional/i18n/src/i18n/i18n.ts +30 -0
- package/dist/templates/optional/i18n/src/i18n/locales/en.json +32 -0
- package/dist/templates/optional/i18n/src/i18n/locales/es.json +32 -0
- package/dist/templates/optional/maestro/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/02_login.yaml +13 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/03_logout.yaml +16 -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/sentry/src/utils/sentry.ts +24 -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 +40 -5
- package/dist/templates/expo-base/App.tsx +0 -32
- package/dist/templates/expo-base/app/(tabs)/_layout.tsx +0 -35
- package/dist/templates/expo-base/app/(tabs)/explore.tsx +0 -112
- package/dist/templates/expo-base/app/(tabs)/index.tsx +0 -98
- package/dist/templates/expo-base/app/modal.tsx +0 -29
- package/dist/templates/expo-base/components/external-link.tsx +0 -25
- package/dist/templates/expo-base/components/haptic-tab.tsx +0 -18
- package/dist/templates/expo-base/components/hello-wave.tsx +0 -19
- package/dist/templates/expo-base/components/parallax-scroll-view.tsx +0 -79
- package/dist/templates/expo-base/components/themed-text.tsx +0 -60
- package/dist/templates/expo-base/components/themed-view.tsx +0 -14
- package/dist/templates/expo-base/components/ui/collapsible.tsx +0 -45
- package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +0 -32
- package/dist/templates/expo-base/components/ui/icon-symbol.tsx +0 -41
- package/dist/templates/expo-base/constants/theme.ts +0 -53
- package/dist/templates/expo-base/hooks/use-color-scheme.ts +0 -1
- package/dist/templates/expo-base/hooks/use-color-scheme.web.ts +0 -21
- package/dist/templates/expo-base/hooks/use-theme-color.ts +0 -21
- package/dist/templates/expo-base/scripts/reset-project.js +0 -112
- package/dist/templates/optional/apiClient/api/client.axios.ts +0 -124
- /package/dist/templates/optional/auth-context/api/{authApi.ts → endpoints/auth.ts} +0 -0
- /package/dist/templates/optional/auth-redux/api/{authApi.ts → endpoints/auth.ts} +0 -0
- /package/dist/templates/optional/auth-zustand/api/{authApi.ts → endpoints/auth.ts} +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateCode = generateCode;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Project context detection
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
async function detectProjectConfig() {
|
|
13
|
+
let dir = process.cwd();
|
|
14
|
+
const root = path_1.default.parse(dir).root;
|
|
15
|
+
while (dir !== root) {
|
|
16
|
+
const pkgPath = path_1.default.join(dir, "package.json");
|
|
17
|
+
if (await fs_extra_1.default.pathExists(pkgPath)) {
|
|
18
|
+
const pkg = await fs_extra_1.default.readJson(pkgPath);
|
|
19
|
+
if (pkg.rnstarterkitConfig) {
|
|
20
|
+
return { config: pkg.rnstarterkitConfig, projectRoot: dir };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
dir = path_1.default.dirname(dir);
|
|
24
|
+
}
|
|
25
|
+
throw new Error("❌ No rnstarterkitConfig found in package.json.\n" +
|
|
26
|
+
" Run this command from inside a project created with create-rnstarterkit.");
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
function toPascalCase(str) {
|
|
32
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
33
|
+
}
|
|
34
|
+
function toCamelCase(str) {
|
|
35
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
36
|
+
}
|
|
37
|
+
async function writeAndLog(filePath, content) {
|
|
38
|
+
if (await fs_extra_1.default.pathExists(filePath)) {
|
|
39
|
+
console.error(`❌ File already exists: ${filePath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
|
|
43
|
+
await fs_extra_1.default.writeFile(filePath, content, "utf8");
|
|
44
|
+
console.log(`✅ Created ${path_1.default.relative(process.cwd(), filePath)}`);
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// screen generator
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
function screenStateImports(config) {
|
|
50
|
+
if (!config.auth)
|
|
51
|
+
return "";
|
|
52
|
+
if (config.state === "Redux Toolkit") {
|
|
53
|
+
return `import { useAppSelector } from '../store/hooks';\n`;
|
|
54
|
+
}
|
|
55
|
+
if (config.state === "Zustand") {
|
|
56
|
+
return `import { useAuthStore } from '../store/authStore';\n`;
|
|
57
|
+
}
|
|
58
|
+
if (config.state === "Context API") {
|
|
59
|
+
return `import { useContext } from 'react';\nimport { AuthContext } from '../context/AuthContext';\n`;
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
function screenStateHook(config) {
|
|
64
|
+
if (!config.auth)
|
|
65
|
+
return "";
|
|
66
|
+
if (config.state === "Redux Toolkit") {
|
|
67
|
+
return `\n const token = useAppSelector((state) => state.auth.token);\n`;
|
|
68
|
+
}
|
|
69
|
+
if (config.state === "Zustand") {
|
|
70
|
+
return `\n const { token } = useAuthStore();\n`;
|
|
71
|
+
}
|
|
72
|
+
if (config.state === "Context API") {
|
|
73
|
+
return `\n const { token } = useContext(AuthContext);\n`;
|
|
74
|
+
}
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
async function generateScreen(name, config, projectRoot) {
|
|
78
|
+
const screenName = toPascalCase(name);
|
|
79
|
+
const fileName = `${screenName}Screen.tsx`;
|
|
80
|
+
const targetDir = config.platform === "Expo"
|
|
81
|
+
? path_1.default.join(projectRoot, "app")
|
|
82
|
+
: path_1.default.join(projectRoot, "screens");
|
|
83
|
+
const filePath = path_1.default.join(targetDir, fileName);
|
|
84
|
+
const stateImports = screenStateImports(config);
|
|
85
|
+
const stateHook = screenStateHook(config);
|
|
86
|
+
const layoutImport = config.platform === "React Native CLI"
|
|
87
|
+
? `import ScreenLayout from '../components/layout/ScreenLayout';\n`
|
|
88
|
+
: "";
|
|
89
|
+
const wrapper = config.platform === "React Native CLI"
|
|
90
|
+
? ["<ScreenLayout>", " </ScreenLayout>"]
|
|
91
|
+
: ["<View style={styles.container}>", " </View>"];
|
|
92
|
+
const content = config.platform === "Expo"
|
|
93
|
+
? `import React from 'react';
|
|
94
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
95
|
+
${stateImports}
|
|
96
|
+
export default function ${screenName}Screen() {${stateHook}
|
|
97
|
+
return (
|
|
98
|
+
${wrapper[0]}
|
|
99
|
+
<Text style={styles.title}>${screenName}</Text>
|
|
100
|
+
${wrapper[1]}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const styles = StyleSheet.create({
|
|
105
|
+
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20 },
|
|
106
|
+
title: { fontSize: 24, fontWeight: 'bold', color: '#111827' },
|
|
107
|
+
});
|
|
108
|
+
`
|
|
109
|
+
: `import React from 'react';
|
|
110
|
+
import { Text, StyleSheet } from 'react-native';
|
|
111
|
+
${layoutImport}${stateImports}
|
|
112
|
+
export default function ${screenName}Screen() {${stateHook}
|
|
113
|
+
return (
|
|
114
|
+
${wrapper[0]}
|
|
115
|
+
<Text style={styles.title}>${screenName}</Text>
|
|
116
|
+
${wrapper[1]}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const styles = StyleSheet.create({
|
|
121
|
+
title: { fontSize: 24, fontWeight: 'bold', color: '#111827' },
|
|
122
|
+
});
|
|
123
|
+
`;
|
|
124
|
+
await writeAndLog(filePath, content);
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// component generator
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
async function generateComponent(name, projectRoot) {
|
|
130
|
+
const componentName = toPascalCase(name);
|
|
131
|
+
const filePath = path_1.default.join(projectRoot, "components", `${componentName}.tsx`);
|
|
132
|
+
const content = `import React from 'react';
|
|
133
|
+
import { View, Text, StyleSheet, type ViewStyle } from 'react-native';
|
|
134
|
+
|
|
135
|
+
type ${componentName}Props = {
|
|
136
|
+
label?: string;
|
|
137
|
+
style?: ViewStyle;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export function ${componentName}({ label = '${componentName}', style }: ${componentName}Props) {
|
|
141
|
+
return (
|
|
142
|
+
<View style={[styles.container, style]}>
|
|
143
|
+
<Text style={styles.label}>{label}</Text>
|
|
144
|
+
</View>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const styles = StyleSheet.create({
|
|
149
|
+
container: { padding: 12 },
|
|
150
|
+
label: { fontSize: 14, color: '#374151' },
|
|
151
|
+
});
|
|
152
|
+
`;
|
|
153
|
+
await writeAndLog(filePath, content);
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// hook generator
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
async function generateHook(name, projectRoot) {
|
|
159
|
+
const baseName = name.startsWith("use") ? name.slice(3) : name;
|
|
160
|
+
const hookName = `use${toPascalCase(baseName)}`;
|
|
161
|
+
const filePath = path_1.default.join(projectRoot, "hooks", `${hookName}.ts`);
|
|
162
|
+
const content = `import { useState, useCallback } from 'react';
|
|
163
|
+
|
|
164
|
+
type ${toPascalCase(baseName)}State = {
|
|
165
|
+
data: null;
|
|
166
|
+
isLoading: boolean;
|
|
167
|
+
error: string | null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export function ${hookName}() {
|
|
171
|
+
const [state, setState] = useState<${toPascalCase(baseName)}State>({
|
|
172
|
+
data: null,
|
|
173
|
+
isLoading: false,
|
|
174
|
+
error: null,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const execute = useCallback(async () => {
|
|
178
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
179
|
+
try {
|
|
180
|
+
// TODO: implement hook logic
|
|
181
|
+
setState((prev) => ({ ...prev, isLoading: false }));
|
|
182
|
+
} catch (err) {
|
|
183
|
+
setState((prev) => ({
|
|
184
|
+
...prev,
|
|
185
|
+
isLoading: false,
|
|
186
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
return { ...state, execute };
|
|
192
|
+
}
|
|
193
|
+
`;
|
|
194
|
+
await writeAndLog(filePath, content);
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// slice generator
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
async function generateSlice(name, config, projectRoot) {
|
|
200
|
+
if (config.state !== "Redux Toolkit") {
|
|
201
|
+
console.error(`❌ Slice generation requires Redux Toolkit. This project uses: ${config.state || "None"}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const sliceName = toCamelCase(name);
|
|
205
|
+
const SliceName = toPascalCase(name);
|
|
206
|
+
const filePath = path_1.default.join(projectRoot, "store", `${sliceName}Slice.ts`);
|
|
207
|
+
// --- write the slice file ---
|
|
208
|
+
const sliceContent = `import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
|
209
|
+
|
|
210
|
+
interface ${SliceName}State {
|
|
211
|
+
// TODO: define your state shape
|
|
212
|
+
data: null;
|
|
213
|
+
isLoading: boolean;
|
|
214
|
+
error: string | null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const initialState: ${SliceName}State = {
|
|
218
|
+
data: null,
|
|
219
|
+
isLoading: false,
|
|
220
|
+
error: null,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const ${sliceName}Slice = createSlice({
|
|
224
|
+
name: '${sliceName}',
|
|
225
|
+
initialState,
|
|
226
|
+
reducers: {
|
|
227
|
+
setLoading(state, _action: PayloadAction<boolean>) {
|
|
228
|
+
state.isLoading = _action.payload;
|
|
229
|
+
},
|
|
230
|
+
setError(state, action: PayloadAction<string | null>) {
|
|
231
|
+
state.error = action.payload;
|
|
232
|
+
},
|
|
233
|
+
reset() {
|
|
234
|
+
return initialState;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
export const { setLoading, setError, reset } = ${sliceName}Slice.actions;
|
|
240
|
+
export default ${sliceName}Slice.reducer;
|
|
241
|
+
`;
|
|
242
|
+
await writeAndLog(filePath, sliceContent);
|
|
243
|
+
// --- patch store.ts to register the new reducer ---
|
|
244
|
+
const storePath = path_1.default.join(projectRoot, "store", "store.ts");
|
|
245
|
+
if (await fs_extra_1.default.pathExists(storePath)) {
|
|
246
|
+
let storeContent = await fs_extra_1.default.readFile(storePath, "utf8");
|
|
247
|
+
const importLine = `import ${sliceName}Reducer from './${sliceName}Slice';\n`;
|
|
248
|
+
if (!storeContent.includes(importLine)) {
|
|
249
|
+
// Add import after last existing import
|
|
250
|
+
storeContent = storeContent.replace(/^(import .+\n)+/m, (match) => `${match}${importLine}`);
|
|
251
|
+
}
|
|
252
|
+
// Add reducer key inside configureStore reducer object
|
|
253
|
+
if (!storeContent.includes(`${sliceName}:`)) {
|
|
254
|
+
storeContent = storeContent.replace(/reducer:\s*\{/, `reducer: {\n ${sliceName}: ${sliceName}Reducer,`);
|
|
255
|
+
}
|
|
256
|
+
await fs_extra_1.default.writeFile(storePath, storeContent, "utf8");
|
|
257
|
+
console.log(`🔗 Registered ${sliceName} in store/store.ts`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.warn(`⚠️ store/store.ts not found — add the reducer manually:\n` +
|
|
261
|
+
` import ${sliceName}Reducer from './${sliceName}Slice';\n` +
|
|
262
|
+
` // add ${sliceName}: ${sliceName}Reducer to your store`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Public entry point
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
async function generateCode(type, name) {
|
|
269
|
+
const { config, projectRoot } = await detectProjectConfig();
|
|
270
|
+
const validTypes = ["screen", "component", "hook", "slice"];
|
|
271
|
+
if (!validTypes.includes(type)) {
|
|
272
|
+
console.error(`❌ Unknown type "${type}". Valid types: ${validTypes.join(" | ")}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
switch (type) {
|
|
276
|
+
case "screen":
|
|
277
|
+
await generateScreen(name, config, projectRoot);
|
|
278
|
+
break;
|
|
279
|
+
case "component":
|
|
280
|
+
await generateComponent(name, projectRoot);
|
|
281
|
+
break;
|
|
282
|
+
case "hook":
|
|
283
|
+
await generateHook(name, projectRoot);
|
|
284
|
+
break;
|
|
285
|
+
case "slice":
|
|
286
|
+
await generateSlice(name, config, projectRoot);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -1,44 +1,222 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Image,
|
|
5
|
+
ImageSourcePropType,
|
|
6
|
+
Pressable,
|
|
7
|
+
StatusBar,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
useWindowDimensions,
|
|
11
|
+
View,
|
|
12
|
+
ViewToken,
|
|
13
|
+
} from "react-native";
|
|
10
14
|
import {
|
|
11
15
|
SafeAreaProvider,
|
|
12
|
-
|
|
13
|
-
} from
|
|
16
|
+
SafeAreaView,
|
|
17
|
+
} from "react-native-safe-area-context";
|
|
18
|
+
|
|
19
|
+
type Slide = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
image: ImageSourcePropType;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const slides: Slide[] = [
|
|
27
|
+
{
|
|
28
|
+
id: "1",
|
|
29
|
+
title: "Welcome to RN Starter Kit",
|
|
30
|
+
description: "Start with a clean React Native CLI project structure.",
|
|
31
|
+
image: require("./assets/images/react-logo.png"),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "2",
|
|
35
|
+
title: "Ready for Scale",
|
|
36
|
+
description: "Build faster with sensible architecture you can extend.",
|
|
37
|
+
image: require("./assets/images/partial-react-logo.png"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "3",
|
|
41
|
+
title: "Make It Yours",
|
|
42
|
+
description: "Use this onboarding as a placeholder for your product design.",
|
|
43
|
+
image: require("./assets/images/icon.png"),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
14
46
|
|
|
15
47
|
function App() {
|
|
16
|
-
const
|
|
48
|
+
const [isOnboarded, setIsOnboarded] = useState(false);
|
|
17
49
|
|
|
18
50
|
return (
|
|
19
51
|
<SafeAreaProvider>
|
|
20
|
-
<StatusBar barStyle=
|
|
21
|
-
|
|
52
|
+
<StatusBar barStyle="dark-content" />
|
|
53
|
+
{isOnboarded ? (
|
|
54
|
+
<HomeScreen />
|
|
55
|
+
) : (
|
|
56
|
+
<WelcomeScreen onFinish={() => setIsOnboarded(true)} />
|
|
57
|
+
)}
|
|
22
58
|
</SafeAreaProvider>
|
|
23
59
|
);
|
|
24
60
|
}
|
|
25
61
|
|
|
26
|
-
function
|
|
27
|
-
const
|
|
62
|
+
function WelcomeScreen({ onFinish }: { onFinish: () => void }) {
|
|
63
|
+
const { width } = useWindowDimensions();
|
|
64
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
65
|
+
const listRef = useRef<FlatList<Slide>>(null);
|
|
66
|
+
|
|
67
|
+
const onViewableItemsChanged = useRef(
|
|
68
|
+
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
|
69
|
+
if (viewableItems[0]?.index != null) {
|
|
70
|
+
setActiveIndex(viewableItems[0].index);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
).current;
|
|
74
|
+
|
|
75
|
+
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }).current;
|
|
76
|
+
|
|
77
|
+
const handleNext = () => {
|
|
78
|
+
const nextIndex = activeIndex + 1;
|
|
79
|
+
if (nextIndex < slides.length) {
|
|
80
|
+
listRef.current?.scrollToIndex({ index: nextIndex, animated: true });
|
|
81
|
+
} else {
|
|
82
|
+
onFinish();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isLastSlide = activeIndex === slides.length - 1;
|
|
28
87
|
|
|
29
88
|
return (
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
89
|
+
<SafeAreaView style={styles.safeArea}>
|
|
90
|
+
<FlatList
|
|
91
|
+
ref={listRef}
|
|
92
|
+
data={slides}
|
|
93
|
+
horizontal
|
|
94
|
+
pagingEnabled
|
|
95
|
+
bounces={false}
|
|
96
|
+
keyExtractor={(item) => item.id}
|
|
97
|
+
showsHorizontalScrollIndicator={false}
|
|
98
|
+
renderItem={({ item }) => (
|
|
99
|
+
<View style={[styles.slide, { width }]}>
|
|
100
|
+
<Image source={item.image} style={styles.image} resizeMode="contain" />
|
|
101
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
102
|
+
<Text style={styles.description}>{item.description}</Text>
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
106
|
+
viewabilityConfig={viewabilityConfig}
|
|
34
107
|
/>
|
|
35
|
-
|
|
108
|
+
|
|
109
|
+
<View style={styles.footer}>
|
|
110
|
+
<View style={styles.dotsRow}>
|
|
111
|
+
{slides.map((slide, index) => (
|
|
112
|
+
<View
|
|
113
|
+
key={slide.id}
|
|
114
|
+
style={[styles.dot, index === activeIndex && styles.dotActive]}
|
|
115
|
+
/>
|
|
116
|
+
))}
|
|
117
|
+
</View>
|
|
118
|
+
|
|
119
|
+
<Pressable style={styles.button} onPress={handleNext}>
|
|
120
|
+
<Text style={styles.buttonLabel}>{isLastSlide ? "Get Started" : "Next"}</Text>
|
|
121
|
+
</Pressable>
|
|
122
|
+
</View>
|
|
123
|
+
</SafeAreaView>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function HomeScreen() {
|
|
128
|
+
return (
|
|
129
|
+
<SafeAreaView style={homeStyles.container}>
|
|
130
|
+
<Text style={homeStyles.title}>Home</Text>
|
|
131
|
+
<Text style={homeStyles.subtitle}>
|
|
132
|
+
Replace this screen with your app content.
|
|
133
|
+
</Text>
|
|
134
|
+
</SafeAreaView>
|
|
36
135
|
);
|
|
37
136
|
}
|
|
38
137
|
|
|
39
138
|
const styles = StyleSheet.create({
|
|
139
|
+
safeArea: {
|
|
140
|
+
flex: 1,
|
|
141
|
+
backgroundColor: "#F7F7F9",
|
|
142
|
+
},
|
|
143
|
+
slide: {
|
|
144
|
+
flex: 1,
|
|
145
|
+
paddingHorizontal: 24,
|
|
146
|
+
justifyContent: "center",
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
},
|
|
149
|
+
image: {
|
|
150
|
+
width: 220,
|
|
151
|
+
height: 220,
|
|
152
|
+
marginBottom: 28,
|
|
153
|
+
},
|
|
154
|
+
title: {
|
|
155
|
+
fontSize: 28,
|
|
156
|
+
fontWeight: "700",
|
|
157
|
+
color: "#111827",
|
|
158
|
+
textAlign: "center",
|
|
159
|
+
marginBottom: 12,
|
|
160
|
+
},
|
|
161
|
+
description: {
|
|
162
|
+
fontSize: 16,
|
|
163
|
+
lineHeight: 24,
|
|
164
|
+
color: "#4B5563",
|
|
165
|
+
textAlign: "center",
|
|
166
|
+
maxWidth: 320,
|
|
167
|
+
},
|
|
168
|
+
footer: {
|
|
169
|
+
paddingHorizontal: 24,
|
|
170
|
+
paddingBottom: 28,
|
|
171
|
+
gap: 20,
|
|
172
|
+
},
|
|
173
|
+
dotsRow: {
|
|
174
|
+
flexDirection: "row",
|
|
175
|
+
justifyContent: "center",
|
|
176
|
+
gap: 8,
|
|
177
|
+
},
|
|
178
|
+
dot: {
|
|
179
|
+
width: 8,
|
|
180
|
+
height: 8,
|
|
181
|
+
borderRadius: 999,
|
|
182
|
+
backgroundColor: "#D1D5DB",
|
|
183
|
+
},
|
|
184
|
+
dotActive: {
|
|
185
|
+
width: 24,
|
|
186
|
+
backgroundColor: "#111827",
|
|
187
|
+
},
|
|
188
|
+
button: {
|
|
189
|
+
height: 52,
|
|
190
|
+
borderRadius: 12,
|
|
191
|
+
backgroundColor: "#111827",
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
},
|
|
195
|
+
buttonLabel: {
|
|
196
|
+
color: "#FFFFFF",
|
|
197
|
+
fontSize: 16,
|
|
198
|
+
fontWeight: "600",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const homeStyles = StyleSheet.create({
|
|
40
203
|
container: {
|
|
41
204
|
flex: 1,
|
|
205
|
+
backgroundColor: "#F7F7F9",
|
|
206
|
+
alignItems: "center",
|
|
207
|
+
justifyContent: "center",
|
|
208
|
+
padding: 24,
|
|
209
|
+
},
|
|
210
|
+
title: {
|
|
211
|
+
fontSize: 28,
|
|
212
|
+
fontWeight: "700",
|
|
213
|
+
color: "#111827",
|
|
214
|
+
marginBottom: 8,
|
|
215
|
+
},
|
|
216
|
+
subtitle: {
|
|
217
|
+
fontSize: 16,
|
|
218
|
+
color: "#4B5563",
|
|
219
|
+
textAlign: "center",
|
|
42
220
|
},
|
|
43
221
|
});
|
|
44
222
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -259,7 +259,10 @@
|
|
|
259
259
|
buildSettings = {
|
|
260
260
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
261
261
|
CLANG_ENABLE_MODULES = YES;
|
|
262
|
+
CODE_SIGN_IDENTITY = "";
|
|
263
|
+
CODE_SIGN_STYLE = Manual;
|
|
262
264
|
CURRENT_PROJECT_VERSION = 1;
|
|
265
|
+
DEVELOPMENT_TEAM = "";
|
|
263
266
|
ENABLE_BITCODE = NO;
|
|
264
267
|
INFOPLIST_FILE = BaseApp/Info.plist;
|
|
265
268
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
|
@@ -289,7 +292,10 @@
|
|
|
289
292
|
buildSettings = {
|
|
290
293
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
291
294
|
CLANG_ENABLE_MODULES = YES;
|
|
295
|
+
CODE_SIGN_IDENTITY = "";
|
|
296
|
+
CODE_SIGN_STYLE = Manual;
|
|
292
297
|
CURRENT_PROJECT_VERSION = 1;
|
|
298
|
+
DEVELOPMENT_TEAM = "";
|
|
293
299
|
INFOPLIST_FILE = BaseApp/Info.plist;
|
|
294
300
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
|
295
301
|
LD_RUNPATH_SEARCH_PATHS = (
|
|
@@ -30,5 +30,10 @@ target 'BaseApp' do
|
|
|
30
30
|
:mac_catalyst_enabled => false,
|
|
31
31
|
# :ccache_enabled => true
|
|
32
32
|
)
|
|
33
|
+
installer.pods_project.targets.each do |target|
|
|
34
|
+
target.build_configurations.each do |config|
|
|
35
|
+
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
33
38
|
end
|
|
34
39
|
end
|
|
@@ -5,20 +5,23 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"android": "react-native run-android",
|
|
7
7
|
"ios": "react-native run-ios",
|
|
8
|
-
"lint": "eslint .",
|
|
9
8
|
"start": "react-native start",
|
|
9
|
+
"lint": "eslint . --max-warnings 0",
|
|
10
|
+
"format": "prettier --write .",
|
|
11
|
+
"type-check": "tsc --noEmit",
|
|
10
12
|
"test": "jest"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
15
|
+
"@babel/runtime": "^7.25.0",
|
|
13
16
|
"react": "19.2.3",
|
|
14
17
|
"react-native": "0.84.0",
|
|
15
|
-
"
|
|
16
|
-
"react-native-
|
|
18
|
+
"react-native-safe-area-context": "^5.5.2",
|
|
19
|
+
"react-native-worklets": "^0.7.0",
|
|
20
|
+
"react-native-reanimated": "^4.2.0"
|
|
17
21
|
},
|
|
18
22
|
"devDependencies": {
|
|
19
23
|
"@babel/core": "^7.25.2",
|
|
20
24
|
"@babel/preset-env": "^7.25.3",
|
|
21
|
-
"@babel/runtime": "^7.25.0",
|
|
22
25
|
"@react-native-community/cli": "20.1.0",
|
|
23
26
|
"@react-native-community/cli-platform-android": "20.1.0",
|
|
24
27
|
"@react-native-community/cli-platform-ios": "20.1.0",
|
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { StatusBar } from 'expo-status-bar';
|
|
4
|
-
import 'react-native-reanimated';
|
|
5
|
-
|
|
6
|
-
import { useColorScheme } from '@/hooks/use-color-scheme';
|
|
7
|
-
|
|
8
|
-
export const unstable_settings = {
|
|
9
|
-
anchor: '(tabs)',
|
|
10
|
-
};
|
|
1
|
+
import { Stack } from "expo-router";
|
|
2
|
+
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
11
3
|
|
|
12
4
|
export default function RootLayout() {
|
|
13
|
-
const colorScheme = useColorScheme();
|
|
14
|
-
|
|
15
5
|
return (
|
|
16
|
-
<
|
|
17
|
-
<Stack>
|
|
18
|
-
<Stack.Screen name="
|
|
19
|
-
<Stack.Screen name="
|
|
6
|
+
<SafeAreaProvider>
|
|
7
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
8
|
+
<Stack.Screen name="index" />
|
|
9
|
+
<Stack.Screen name="home" />
|
|
20
10
|
</Stack>
|
|
21
|
-
|
|
22
|
-
</ThemeProvider>
|
|
11
|
+
</SafeAreaProvider>
|
|
23
12
|
);
|
|
24
13
|
}
|