expo-bbase 1.0.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/README.md +118 -0
- package/bin/cli.js +2 -0
- package/dist/index.js +3611 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3611 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
createProject: () => createProject,
|
|
34
|
+
run: () => run
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
var import_chalk = __toESM(require("chalk"));
|
|
38
|
+
var import_prompts = __toESM(require("prompts"));
|
|
39
|
+
var import_ora = __toESM(require("ora"));
|
|
40
|
+
var import_path2 = __toESM(require("path"));
|
|
41
|
+
var import_commander = require("commander");
|
|
42
|
+
|
|
43
|
+
// src/commands/create.ts
|
|
44
|
+
function registerCreateCommand(program) {
|
|
45
|
+
program.command("create <project-name>").description("Create a new Expo project with selected modules").action(async (projectName) => {
|
|
46
|
+
await createProject(projectName);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/utils/lines.ts
|
|
51
|
+
function lines(...args) {
|
|
52
|
+
return args.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/modules/network.ts
|
|
56
|
+
var networkModule = {
|
|
57
|
+
id: "network",
|
|
58
|
+
name: "\u7F51\u7EDC\u8BF7\u6C42",
|
|
59
|
+
description: "TanStack Query \u5C01\u88C5\uFF0C\u7EDF\u4E00\u62E6\u622A\u5668\u3001\u9519\u8BEF\u5904\u7406",
|
|
60
|
+
defaultChecked: true,
|
|
61
|
+
dependencies: {
|
|
62
|
+
"@tanstack/react-query": "^5.60.0"
|
|
63
|
+
},
|
|
64
|
+
devDependencies: {},
|
|
65
|
+
files: [
|
|
66
|
+
{
|
|
67
|
+
path: "src/api/client.ts",
|
|
68
|
+
content: lines(
|
|
69
|
+
'import { Alert } from "react-native";',
|
|
70
|
+
"",
|
|
71
|
+
'const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || "https://api.example.com";',
|
|
72
|
+
"",
|
|
73
|
+
"interface RequestConfig {",
|
|
74
|
+
' method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";',
|
|
75
|
+
" url: string;",
|
|
76
|
+
" data?: unknown;",
|
|
77
|
+
" headers?: Record<string, string>;",
|
|
78
|
+
" params?: Record<string, string>;",
|
|
79
|
+
"}",
|
|
80
|
+
"",
|
|
81
|
+
"interface ApiResponse<T> {",
|
|
82
|
+
" data: T;",
|
|
83
|
+
" message: string;",
|
|
84
|
+
" code: number;",
|
|
85
|
+
"}",
|
|
86
|
+
"",
|
|
87
|
+
"class ApiClient {",
|
|
88
|
+
" private baseUrl: string;",
|
|
89
|
+
" private defaultHeaders: Record<string, string>;",
|
|
90
|
+
" private requestInterceptors: Array<(config: RequestConfig) => RequestConfig> = [];",
|
|
91
|
+
" private responseInterceptors: Array<(response: Response) => Response> = [];",
|
|
92
|
+
"",
|
|
93
|
+
" constructor(baseUrl: string) {",
|
|
94
|
+
" this.baseUrl = baseUrl;",
|
|
95
|
+
" this.defaultHeaders = {",
|
|
96
|
+
' "Content-Type": "application/json",',
|
|
97
|
+
' Accept: "application/json",',
|
|
98
|
+
" };",
|
|
99
|
+
" }",
|
|
100
|
+
"",
|
|
101
|
+
" /** Add a request interceptor */",
|
|
102
|
+
" addRequestInterceptor(interceptor: (config: RequestConfig) => RequestConfig): void {",
|
|
103
|
+
" this.requestInterceptors.push(interceptor);",
|
|
104
|
+
" }",
|
|
105
|
+
"",
|
|
106
|
+
" /** Add a response interceptor */",
|
|
107
|
+
" addResponseInterceptor(interceptor: (response: Response) => Response): void {",
|
|
108
|
+
" this.responseInterceptors.push(interceptor);",
|
|
109
|
+
" }",
|
|
110
|
+
"",
|
|
111
|
+
" /** Set the authorization token */",
|
|
112
|
+
" setAuthToken(token: string): void {",
|
|
113
|
+
' this.defaultHeaders["Authorization"] = `Bearer ${token}`;',
|
|
114
|
+
" }",
|
|
115
|
+
"",
|
|
116
|
+
" /** Clear the authorization token */",
|
|
117
|
+
" clearAuthToken(): void {",
|
|
118
|
+
' delete this.defaultHeaders["Authorization"];',
|
|
119
|
+
" }",
|
|
120
|
+
"",
|
|
121
|
+
" /** Apply request interceptors */",
|
|
122
|
+
" private applyRequestInterceptors(config: RequestConfig): RequestConfig {",
|
|
123
|
+
" return this.requestInterceptors.reduce(",
|
|
124
|
+
" (acc, interceptor) => interceptor(acc),",
|
|
125
|
+
" config",
|
|
126
|
+
" );",
|
|
127
|
+
" }",
|
|
128
|
+
"",
|
|
129
|
+
" /** Build URL with query params */",
|
|
130
|
+
" private buildUrl(url: string, params?: Record<string, string>): string {",
|
|
131
|
+
" const fullUrl = new URL(url, this.baseUrl);",
|
|
132
|
+
" if (params) {",
|
|
133
|
+
" Object.entries(params).forEach(([key, value]) => {",
|
|
134
|
+
" fullUrl.searchParams.append(key, value);",
|
|
135
|
+
" });",
|
|
136
|
+
" }",
|
|
137
|
+
" return fullUrl.toString();",
|
|
138
|
+
" }",
|
|
139
|
+
"",
|
|
140
|
+
" /** Core request method */",
|
|
141
|
+
" async request<T>(config: RequestConfig): Promise<ApiResponse<T>> {",
|
|
142
|
+
" const interceptedConfig = this.applyRequestInterceptors(config);",
|
|
143
|
+
" const url = this.buildUrl(interceptedConfig.url, interceptedConfig.params);",
|
|
144
|
+
"",
|
|
145
|
+
" try {",
|
|
146
|
+
" const response = await fetch(url, {",
|
|
147
|
+
" method: interceptedConfig.method,",
|
|
148
|
+
" headers: {",
|
|
149
|
+
" ...this.defaultHeaders,",
|
|
150
|
+
" ...interceptedConfig.headers,",
|
|
151
|
+
" },",
|
|
152
|
+
" body: interceptedConfig.data",
|
|
153
|
+
" ? JSON.stringify(interceptedConfig.data)",
|
|
154
|
+
" : undefined,",
|
|
155
|
+
" });",
|
|
156
|
+
"",
|
|
157
|
+
" if (!response.ok) {",
|
|
158
|
+
" const errorData = await response.json().catch(() => null);",
|
|
159
|
+
" throw new ApiError(",
|
|
160
|
+
" response.status,",
|
|
161
|
+
" errorData?.message || response.statusText,",
|
|
162
|
+
" errorData",
|
|
163
|
+
" );",
|
|
164
|
+
" }",
|
|
165
|
+
"",
|
|
166
|
+
" const data: ApiResponse<T> = await response.json();",
|
|
167
|
+
" return data;",
|
|
168
|
+
" } catch (error) {",
|
|
169
|
+
" if (error instanceof ApiError) {",
|
|
170
|
+
" throw error;",
|
|
171
|
+
" }",
|
|
172
|
+
' throw new ApiError(0, "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5", null);',
|
|
173
|
+
" }",
|
|
174
|
+
" }",
|
|
175
|
+
"",
|
|
176
|
+
" /** GET request */",
|
|
177
|
+
" async get<T>(url: string, params?: Record<string, string>): Promise<ApiResponse<T>> {",
|
|
178
|
+
' return this.request<T>({ method: "GET", url, params });',
|
|
179
|
+
" }",
|
|
180
|
+
"",
|
|
181
|
+
" /** POST request */",
|
|
182
|
+
" async post<T>(url: string, data?: unknown): Promise<ApiResponse<T>> {",
|
|
183
|
+
' return this.request<T>({ method: "POST", url, data });',
|
|
184
|
+
" }",
|
|
185
|
+
"",
|
|
186
|
+
" /** PUT request */",
|
|
187
|
+
" async put<T>(url: string, data?: unknown): Promise<ApiResponse<T>> {",
|
|
188
|
+
' return this.request<T>({ method: "PUT", url, data });',
|
|
189
|
+
" }",
|
|
190
|
+
"",
|
|
191
|
+
" /** PATCH request */",
|
|
192
|
+
" async patch<T>(url: string, data?: unknown): Promise<ApiResponse<T>> {",
|
|
193
|
+
' return this.request<T>({ method: "PATCH", url, data });',
|
|
194
|
+
" }",
|
|
195
|
+
"",
|
|
196
|
+
" /** DELETE request */",
|
|
197
|
+
" async delete<T>(url: string): Promise<ApiResponse<T>> {",
|
|
198
|
+
' return this.request<T>({ method: "DELETE", url });',
|
|
199
|
+
" }",
|
|
200
|
+
"}",
|
|
201
|
+
"",
|
|
202
|
+
"/** Custom API error class */",
|
|
203
|
+
"export class ApiError extends Error {",
|
|
204
|
+
" code: number;",
|
|
205
|
+
" detail: unknown;",
|
|
206
|
+
"",
|
|
207
|
+
" constructor(code: number, message: string, detail: unknown) {",
|
|
208
|
+
" super(message);",
|
|
209
|
+
' this.name = "ApiError";',
|
|
210
|
+
" this.code = code;",
|
|
211
|
+
" this.detail = detail;",
|
|
212
|
+
" }",
|
|
213
|
+
"}",
|
|
214
|
+
"",
|
|
215
|
+
"/** Global error handler */",
|
|
216
|
+
"export function handleApiError(error: unknown): void {",
|
|
217
|
+
" if (error instanceof ApiError) {",
|
|
218
|
+
" if (error.code === 401) {",
|
|
219
|
+
' Alert.alert("\u8BA4\u8BC1\u5931\u8D25", "\u8BF7\u91CD\u65B0\u767B\u5F55");',
|
|
220
|
+
" } else if (error.code === 403) {",
|
|
221
|
+
' Alert.alert("\u6743\u9650\u4E0D\u8DB3", "\u60A8\u6CA1\u6709\u6743\u9650\u6267\u884C\u6B64\u64CD\u4F5C");',
|
|
222
|
+
" } else if (error.code === 0) {",
|
|
223
|
+
' Alert.alert("\u7F51\u7EDC\u9519\u8BEF", error.message);',
|
|
224
|
+
" } else {",
|
|
225
|
+
' Alert.alert("\u8BF7\u6C42\u5931\u8D25", error.message);',
|
|
226
|
+
" }",
|
|
227
|
+
" } else {",
|
|
228
|
+
' Alert.alert("\u672A\u77E5\u9519\u8BEF", "\u8BF7\u7A0D\u540E\u91CD\u8BD5");',
|
|
229
|
+
" }",
|
|
230
|
+
"}",
|
|
231
|
+
"",
|
|
232
|
+
"/** Singleton API client instance */",
|
|
233
|
+
"export const apiClient = new ApiClient(API_BASE_URL);",
|
|
234
|
+
"",
|
|
235
|
+
"export default apiClient;"
|
|
236
|
+
)
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
path: "src/api/interceptors.ts",
|
|
240
|
+
content: lines(
|
|
241
|
+
'import type { RequestConfig } from "./client";',
|
|
242
|
+
'import { apiClient } from "./client";',
|
|
243
|
+
"",
|
|
244
|
+
"/**",
|
|
245
|
+
" * Initialize all request interceptors.",
|
|
246
|
+
" * Called once during app startup.",
|
|
247
|
+
" */",
|
|
248
|
+
"export function setupRequestInterceptors(): void {",
|
|
249
|
+
" apiClient.addRequestInterceptor((config: RequestConfig) => {",
|
|
250
|
+
" return config;",
|
|
251
|
+
" });",
|
|
252
|
+
"",
|
|
253
|
+
" if (__DEV__) {",
|
|
254
|
+
" apiClient.addRequestInterceptor((config: RequestConfig) => {",
|
|
255
|
+
" console.log(",
|
|
256
|
+
" `[API Request] ${config.method} ${config.url}`,",
|
|
257
|
+
' config.data ? config.data : ""',
|
|
258
|
+
" );",
|
|
259
|
+
" return config;",
|
|
260
|
+
" });",
|
|
261
|
+
" }",
|
|
262
|
+
"}",
|
|
263
|
+
"",
|
|
264
|
+
"/**",
|
|
265
|
+
" * Initialize all response interceptors.",
|
|
266
|
+
" * Called once during app startup.",
|
|
267
|
+
" */",
|
|
268
|
+
"export function setupResponseInterceptors(): void {",
|
|
269
|
+
" if (__DEV__) {",
|
|
270
|
+
" apiClient.addResponseInterceptor((response: Response) => {",
|
|
271
|
+
" console.log(`[API Response] ${response.status} ${response.url}`);",
|
|
272
|
+
" return response;",
|
|
273
|
+
" });",
|
|
274
|
+
" }",
|
|
275
|
+
"}",
|
|
276
|
+
"",
|
|
277
|
+
"/**",
|
|
278
|
+
" * Setup all interceptors at once.",
|
|
279
|
+
" */",
|
|
280
|
+
"export function setupInterceptors(): void {",
|
|
281
|
+
" setupRequestInterceptors();",
|
|
282
|
+
" setupResponseInterceptors();",
|
|
283
|
+
"}"
|
|
284
|
+
)
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
path: "src/api/types.ts",
|
|
288
|
+
content: lines(
|
|
289
|
+
"/** Common API response types */",
|
|
290
|
+
"",
|
|
291
|
+
"/** Paginated response wrapper */",
|
|
292
|
+
"export interface PaginatedResponse<T> {",
|
|
293
|
+
" data: T[];",
|
|
294
|
+
" total: number;",
|
|
295
|
+
" page: number;",
|
|
296
|
+
" pageSize: number;",
|
|
297
|
+
" totalPages: number;",
|
|
298
|
+
"}",
|
|
299
|
+
"",
|
|
300
|
+
"/** Standard API response */",
|
|
301
|
+
"export interface ApiResponse<T> {",
|
|
302
|
+
" data: T;",
|
|
303
|
+
" message: string;",
|
|
304
|
+
" code: number;",
|
|
305
|
+
"}",
|
|
306
|
+
"",
|
|
307
|
+
"/** Common error response */",
|
|
308
|
+
"export interface ErrorResponse {",
|
|
309
|
+
" message: string;",
|
|
310
|
+
" code: number;",
|
|
311
|
+
" errors?: Record<string, string[]>;",
|
|
312
|
+
"}",
|
|
313
|
+
"",
|
|
314
|
+
"/** User type example */",
|
|
315
|
+
"export interface User {",
|
|
316
|
+
" id: string;",
|
|
317
|
+
" email: string;",
|
|
318
|
+
" name: string;",
|
|
319
|
+
" avatar?: string;",
|
|
320
|
+
" createdAt: string;",
|
|
321
|
+
" updatedAt: string;",
|
|
322
|
+
"}",
|
|
323
|
+
"",
|
|
324
|
+
"/** Auth response */",
|
|
325
|
+
"export interface AuthResponse {",
|
|
326
|
+
" user: User;",
|
|
327
|
+
" token: string;",
|
|
328
|
+
" refreshToken: string;",
|
|
329
|
+
"}"
|
|
330
|
+
)
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
path: "src/api/queries.ts",
|
|
334
|
+
content: lines(
|
|
335
|
+
'import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";',
|
|
336
|
+
'import { apiClient, handleApiError } from "./client";',
|
|
337
|
+
'import type { ApiResponse } from "./types";',
|
|
338
|
+
"",
|
|
339
|
+
"/**",
|
|
340
|
+
" * Example query hook for fetching data.",
|
|
341
|
+
" */",
|
|
342
|
+
"export function useApiQuery<T>(",
|
|
343
|
+
" key: string[],",
|
|
344
|
+
" url: string,",
|
|
345
|
+
" params?: Record<string, string>",
|
|
346
|
+
") {",
|
|
347
|
+
" return useQuery({",
|
|
348
|
+
" queryKey: key,",
|
|
349
|
+
" queryFn: () => apiClient.get<T>(url, params),",
|
|
350
|
+
" retry: 2,",
|
|
351
|
+
" staleTime: 5 * 60 * 1000,",
|
|
352
|
+
" });",
|
|
353
|
+
"}",
|
|
354
|
+
"",
|
|
355
|
+
"/**",
|
|
356
|
+
" * Example mutation hook for posting data.",
|
|
357
|
+
" */",
|
|
358
|
+
"export function useApiMutation<TData, TVariables>(",
|
|
359
|
+
" url: string,",
|
|
360
|
+
' method: "POST" | "PUT" | "PATCH" = "POST"',
|
|
361
|
+
") {",
|
|
362
|
+
" const queryClient = useQueryClient();",
|
|
363
|
+
"",
|
|
364
|
+
" return useMutation({",
|
|
365
|
+
" mutationFn: (variables: TVariables) => {",
|
|
366
|
+
" switch (method) {",
|
|
367
|
+
' case "PUT":',
|
|
368
|
+
" return apiClient.put<TData>(url, variables);",
|
|
369
|
+
' case "PATCH":',
|
|
370
|
+
" return apiClient.patch<TData>(url, variables);",
|
|
371
|
+
" default:",
|
|
372
|
+
" return apiClient.post<TData>(url, variables);",
|
|
373
|
+
" }",
|
|
374
|
+
" },",
|
|
375
|
+
" onSuccess: () => {",
|
|
376
|
+
" queryClient.invalidateQueries({ queryKey: [url] });",
|
|
377
|
+
" },",
|
|
378
|
+
" onError: (error) => {",
|
|
379
|
+
" handleApiError(error);",
|
|
380
|
+
" },",
|
|
381
|
+
" });",
|
|
382
|
+
"}",
|
|
383
|
+
"",
|
|
384
|
+
"/**",
|
|
385
|
+
" * Example: Fetch current user profile",
|
|
386
|
+
" */",
|
|
387
|
+
"export function useUserProfile() {",
|
|
388
|
+
" return useApiQuery<ApiResponse<{ name: string; email: string }>>(",
|
|
389
|
+
' ["user", "profile"],',
|
|
390
|
+
' "/user/profile"',
|
|
391
|
+
" );",
|
|
392
|
+
"}"
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
],
|
|
396
|
+
layoutProviders: ["<QueryClientProvider client={queryClient}>"],
|
|
397
|
+
layoutImports: [
|
|
398
|
+
'import { QueryClient, QueryClientProvider } from "@tanstack/react-query";',
|
|
399
|
+
"const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 2, staleTime: 5 * 60 * 1000 } } });"
|
|
400
|
+
]
|
|
401
|
+
};
|
|
402
|
+
var network_default = networkModule;
|
|
403
|
+
|
|
404
|
+
// src/modules/state.ts
|
|
405
|
+
var stateModule = {
|
|
406
|
+
id: "state",
|
|
407
|
+
name: "\u72B6\u6001\u7BA1\u7406",
|
|
408
|
+
description: "Zustand stores \u6A21\u677F\uFF0Cpersist \u4E2D\u95F4\u4EF6",
|
|
409
|
+
defaultChecked: true,
|
|
410
|
+
dependencies: {
|
|
411
|
+
zustand: "^5.0.0"
|
|
412
|
+
},
|
|
413
|
+
devDependencies: {},
|
|
414
|
+
files: [
|
|
415
|
+
{
|
|
416
|
+
path: "src/stores/index.ts",
|
|
417
|
+
content: lines('export { useUserStore } from "./slices/userSlice";')
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
path: "src/stores/slices/userSlice.ts",
|
|
421
|
+
content: lines(
|
|
422
|
+
'import { create } from "zustand";',
|
|
423
|
+
'import { persist, createJSONStorage } from "zustand/middleware";',
|
|
424
|
+
"",
|
|
425
|
+
"interface User {",
|
|
426
|
+
" id: string;",
|
|
427
|
+
" email: string;",
|
|
428
|
+
" name: string;",
|
|
429
|
+
" avatar?: string;",
|
|
430
|
+
"}",
|
|
431
|
+
"",
|
|
432
|
+
"interface UserState {",
|
|
433
|
+
" user: User | null;",
|
|
434
|
+
" token: string | null;",
|
|
435
|
+
" isAuthenticated: boolean;",
|
|
436
|
+
" hasHydrated: boolean;",
|
|
437
|
+
"",
|
|
438
|
+
" setUser: (user: User, token: string) => void;",
|
|
439
|
+
" updateUser: (partial: Partial<User>) => void;",
|
|
440
|
+
" clearUser: () => void;",
|
|
441
|
+
" setHasHydrated: (value: boolean) => void;",
|
|
442
|
+
"}",
|
|
443
|
+
"",
|
|
444
|
+
"export const useUserStore = create<UserState>()(",
|
|
445
|
+
" persist(",
|
|
446
|
+
" (set, get) => ({",
|
|
447
|
+
" user: null,",
|
|
448
|
+
" token: null,",
|
|
449
|
+
" isAuthenticated: false,",
|
|
450
|
+
" hasHydrated: false,",
|
|
451
|
+
"",
|
|
452
|
+
" setUser: (user: User, token: string) => {",
|
|
453
|
+
" set({ user, token, isAuthenticated: true });",
|
|
454
|
+
" },",
|
|
455
|
+
"",
|
|
456
|
+
" updateUser: (partial: Partial<User>) => {",
|
|
457
|
+
" const currentUser = get().user;",
|
|
458
|
+
" if (currentUser) {",
|
|
459
|
+
" set({ user: { ...currentUser, ...partial } });",
|
|
460
|
+
" }",
|
|
461
|
+
" },",
|
|
462
|
+
"",
|
|
463
|
+
" clearUser: () => {",
|
|
464
|
+
" set({ user: null, token: null, isAuthenticated: false });",
|
|
465
|
+
" },",
|
|
466
|
+
"",
|
|
467
|
+
" setHasHydrated: (value: boolean) => {",
|
|
468
|
+
" set({ hasHydrated: value });",
|
|
469
|
+
" },",
|
|
470
|
+
" }),",
|
|
471
|
+
" {",
|
|
472
|
+
' name: "user-store",',
|
|
473
|
+
" storage: createJSONStorage(() => {",
|
|
474
|
+
" try {",
|
|
475
|
+
' const { MMKV } = require("react-native-mmkv");',
|
|
476
|
+
" const storage = new MMKV();",
|
|
477
|
+
" return {",
|
|
478
|
+
" getItem: (name: string) => {",
|
|
479
|
+
" const value = storage.getString(name);",
|
|
480
|
+
" return value ?? null;",
|
|
481
|
+
" },",
|
|
482
|
+
" setItem: (name: string, value: string) => {",
|
|
483
|
+
" storage.set(name, value);",
|
|
484
|
+
" },",
|
|
485
|
+
" removeItem: (name: string) => {",
|
|
486
|
+
" storage.delete(name);",
|
|
487
|
+
" },",
|
|
488
|
+
" };",
|
|
489
|
+
" } catch {",
|
|
490
|
+
" return {",
|
|
491
|
+
" getItem: () => null,",
|
|
492
|
+
" setItem: () => {},",
|
|
493
|
+
" removeItem: () => {},",
|
|
494
|
+
" };",
|
|
495
|
+
" }",
|
|
496
|
+
" }),",
|
|
497
|
+
" partialize: (state) => ({",
|
|
498
|
+
" user: state.user,",
|
|
499
|
+
" token: state.token,",
|
|
500
|
+
" isAuthenticated: state.isAuthenticated,",
|
|
501
|
+
" }),",
|
|
502
|
+
" onRehydrateStorage: () => (state) => {",
|
|
503
|
+
" state?.setHasHydrated(true);",
|
|
504
|
+
" },",
|
|
505
|
+
" }",
|
|
506
|
+
" )",
|
|
507
|
+
");"
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
};
|
|
512
|
+
var state_default = stateModule;
|
|
513
|
+
|
|
514
|
+
// src/modules/storage.ts
|
|
515
|
+
var storageModule = {
|
|
516
|
+
id: "storage",
|
|
517
|
+
name: "\u672C\u5730\u5B58\u50A8",
|
|
518
|
+
description: "MMKV \u5C01\u88C5\uFF0C\u7C7B\u578B\u5B89\u5168 API",
|
|
519
|
+
defaultChecked: true,
|
|
520
|
+
dependencies: {
|
|
521
|
+
"react-native-mmkv": "^3.1.0"
|
|
522
|
+
},
|
|
523
|
+
devDependencies: {},
|
|
524
|
+
files: [
|
|
525
|
+
{
|
|
526
|
+
path: "src/storage/index.ts",
|
|
527
|
+
content: lines(
|
|
528
|
+
'import { MMKV } from "react-native-mmkv";',
|
|
529
|
+
"",
|
|
530
|
+
"/**",
|
|
531
|
+
" * Centralized MMKV storage instance.",
|
|
532
|
+
" */",
|
|
533
|
+
"const storage = new MMKV({",
|
|
534
|
+
' id: "app-storage",',
|
|
535
|
+
" encryptionKey: process.env.EXPO_PUBLIC_STORAGE_KEY || undefined,",
|
|
536
|
+
"});",
|
|
537
|
+
"",
|
|
538
|
+
"/** Storage keys */",
|
|
539
|
+
"export const StorageKeys = {",
|
|
540
|
+
' AUTH_TOKEN: "auth_token",',
|
|
541
|
+
' REFRESH_TOKEN: "refresh_token",',
|
|
542
|
+
' USER_DATA: "user_data",',
|
|
543
|
+
' THEME: "theme",',
|
|
544
|
+
' LANGUAGE: "language",',
|
|
545
|
+
' ONBOARDING_COMPLETED: "onboarding_completed",',
|
|
546
|
+
' LAST_SYNC_TIME: "last_sync_time",',
|
|
547
|
+
' CACHE_VERSION: "cache_version",',
|
|
548
|
+
"} as const;",
|
|
549
|
+
"",
|
|
550
|
+
"export type StorageKey = (typeof StorageKeys)[keyof typeof StorageKeys];",
|
|
551
|
+
"",
|
|
552
|
+
"export function getString(key: StorageKey): string | null {",
|
|
553
|
+
" return storage.getString(key) ?? null;",
|
|
554
|
+
"}",
|
|
555
|
+
"",
|
|
556
|
+
"export function setString(key: StorageKey, value: string): void {",
|
|
557
|
+
" storage.set(key, value);",
|
|
558
|
+
"}",
|
|
559
|
+
"",
|
|
560
|
+
"export function getNumber(key: StorageKey): number | null {",
|
|
561
|
+
" const value = storage.getNumber(key);",
|
|
562
|
+
" return value ?? null;",
|
|
563
|
+
"}",
|
|
564
|
+
"",
|
|
565
|
+
"export function setNumber(key: StorageKey, value: number): void {",
|
|
566
|
+
" storage.set(key, value);",
|
|
567
|
+
"}",
|
|
568
|
+
"",
|
|
569
|
+
"export function getBoolean(key: StorageKey): boolean | null {",
|
|
570
|
+
" const value = storage.getBoolean(key);",
|
|
571
|
+
" return value ?? null;",
|
|
572
|
+
"}",
|
|
573
|
+
"",
|
|
574
|
+
"export function setBoolean(key: StorageKey, value: boolean): void {",
|
|
575
|
+
" storage.set(key, value);",
|
|
576
|
+
"}",
|
|
577
|
+
"",
|
|
578
|
+
"export function getJson<T>(key: StorageKey): T | null {",
|
|
579
|
+
" const raw = storage.getString(key);",
|
|
580
|
+
" if (raw === undefined) return null;",
|
|
581
|
+
" try {",
|
|
582
|
+
" return JSON.parse(raw) as T;",
|
|
583
|
+
" } catch {",
|
|
584
|
+
" return null;",
|
|
585
|
+
" }",
|
|
586
|
+
"}",
|
|
587
|
+
"",
|
|
588
|
+
"export function setJson<T>(key: StorageKey, value: T): void {",
|
|
589
|
+
" storage.set(key, JSON.stringify(value));",
|
|
590
|
+
"}",
|
|
591
|
+
"",
|
|
592
|
+
"export function remove(key: StorageKey): void {",
|
|
593
|
+
" storage.delete(key);",
|
|
594
|
+
"}",
|
|
595
|
+
"",
|
|
596
|
+
"export function contains(key: StorageKey): boolean {",
|
|
597
|
+
" return storage.contains(key);",
|
|
598
|
+
"}",
|
|
599
|
+
"",
|
|
600
|
+
"export function clearAll(): void {",
|
|
601
|
+
" storage.clearAll();",
|
|
602
|
+
"}",
|
|
603
|
+
"",
|
|
604
|
+
"export function getAllKeys(): string[] {",
|
|
605
|
+
" return storage.getAllKeys();",
|
|
606
|
+
"}",
|
|
607
|
+
"",
|
|
608
|
+
"export default storage;"
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
],
|
|
612
|
+
appConfig: {
|
|
613
|
+
plugins: [
|
|
614
|
+
[
|
|
615
|
+
"react-native-mmkv",
|
|
616
|
+
{
|
|
617
|
+
MMKV_APP_ID: "app-storage"
|
|
618
|
+
}
|
|
619
|
+
]
|
|
620
|
+
]
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
var storage_default = storageModule;
|
|
624
|
+
|
|
625
|
+
// src/modules/payment.ts
|
|
626
|
+
var paymentModule = {
|
|
627
|
+
id: "payment",
|
|
628
|
+
name: "Apple/iOS \u652F\u4ED8",
|
|
629
|
+
description: "react-native-iap \u5C01\u88C5",
|
|
630
|
+
defaultChecked: false,
|
|
631
|
+
dependencies: {
|
|
632
|
+
"react-native-iap": "^12.15.0"
|
|
633
|
+
},
|
|
634
|
+
devDependencies: {},
|
|
635
|
+
files: [
|
|
636
|
+
{
|
|
637
|
+
path: "src/modules/payment/index.ts",
|
|
638
|
+
content: lines(
|
|
639
|
+
'export { usePayment } from "./usePayment";',
|
|
640
|
+
'export type { ProductInfo, PurchaseResult, PaymentError } from "./types";'
|
|
641
|
+
)
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
path: "src/modules/payment/types.ts",
|
|
645
|
+
content: lines(
|
|
646
|
+
'import type { Product, Purchase } from "react-native-iap";',
|
|
647
|
+
"",
|
|
648
|
+
"export interface ProductInfo {",
|
|
649
|
+
" productId: string;",
|
|
650
|
+
" title: string;",
|
|
651
|
+
" description: string;",
|
|
652
|
+
" price: string;",
|
|
653
|
+
" currency: string;",
|
|
654
|
+
"}",
|
|
655
|
+
"",
|
|
656
|
+
"export interface PurchaseResult {",
|
|
657
|
+
" success: boolean;",
|
|
658
|
+
" purchase?: Purchase;",
|
|
659
|
+
" error?: PaymentError;",
|
|
660
|
+
"}",
|
|
661
|
+
"",
|
|
662
|
+
"export interface PaymentError {",
|
|
663
|
+
" code: string;",
|
|
664
|
+
" message: string;",
|
|
665
|
+
" nativeError?: unknown;",
|
|
666
|
+
"}",
|
|
667
|
+
"",
|
|
668
|
+
"export interface PaymentConfig {",
|
|
669
|
+
" productIds: string[];",
|
|
670
|
+
" subscriptionIds?: string[];",
|
|
671
|
+
"}"
|
|
672
|
+
)
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
path: "src/modules/payment/usePayment.ts",
|
|
676
|
+
content: lines(
|
|
677
|
+
'import { useCallback, useEffect, useRef, useState } from "react";',
|
|
678
|
+
"import {",
|
|
679
|
+
" initConnection,",
|
|
680
|
+
" endConnection,",
|
|
681
|
+
" getProducts,",
|
|
682
|
+
" requestPurchase,",
|
|
683
|
+
" requestSubscription,",
|
|
684
|
+
" finishTransaction,",
|
|
685
|
+
" purchaseUpdatedListener,",
|
|
686
|
+
" purchaseErrorListener,",
|
|
687
|
+
" Product,",
|
|
688
|
+
" Purchase,",
|
|
689
|
+
" PurchaseError,",
|
|
690
|
+
'} from "react-native-iap";',
|
|
691
|
+
'import type { PaymentConfig, ProductInfo, PurchaseResult, PaymentError as AppPaymentError } from "./types";',
|
|
692
|
+
"",
|
|
693
|
+
"export function usePayment(config: PaymentConfig) {",
|
|
694
|
+
" const [products, setProducts] = useState<ProductInfo[]>([]);",
|
|
695
|
+
" const [isLoading, setIsLoading] = useState(false);",
|
|
696
|
+
" const [isConnected, setIsConnected] = useState(false);",
|
|
697
|
+
" const purchaseUpdateSubscription = useRef<ReturnType<typeof purchaseUpdatedListener> | null>(null);",
|
|
698
|
+
" const purchaseErrorSubscription = useRef<ReturnType<typeof purchaseErrorListener> | null>(null);",
|
|
699
|
+
"",
|
|
700
|
+
" const initialize = useCallback(async () => {",
|
|
701
|
+
" try {",
|
|
702
|
+
" setIsLoading(true);",
|
|
703
|
+
" const connected = await initConnection();",
|
|
704
|
+
" setIsConnected(connected);",
|
|
705
|
+
"",
|
|
706
|
+
" if (connected) {",
|
|
707
|
+
" const allIds = [...config.productIds, ...(config.subscriptionIds || [])];",
|
|
708
|
+
" if (allIds.length > 0) {",
|
|
709
|
+
" const fetchedProducts = await getProducts({ skus: allIds });",
|
|
710
|
+
" const mapped = fetchedProducts.map(mapProductToInfo);",
|
|
711
|
+
" setProducts(mapped);",
|
|
712
|
+
" }",
|
|
713
|
+
" }",
|
|
714
|
+
" } catch (error) {",
|
|
715
|
+
' console.error("[Payment] Initialization failed:", error);',
|
|
716
|
+
" } finally {",
|
|
717
|
+
" setIsLoading(false);",
|
|
718
|
+
" }",
|
|
719
|
+
" }, [config.productIds, config.subscriptionIds]);",
|
|
720
|
+
"",
|
|
721
|
+
" const handlePurchase = useCallback(",
|
|
722
|
+
" async (purchase: Purchase): Promise<void> => {",
|
|
723
|
+
" const receipt = purchase.transactionReceipt;",
|
|
724
|
+
" if (receipt) {",
|
|
725
|
+
" // TODO: Validate receipt with your backend server",
|
|
726
|
+
" await finishTransaction({ purchase, isConsumable: false });",
|
|
727
|
+
" }",
|
|
728
|
+
" },",
|
|
729
|
+
" []",
|
|
730
|
+
" );",
|
|
731
|
+
"",
|
|
732
|
+
" const buyProduct = useCallback(",
|
|
733
|
+
" async (productId: string): Promise<PurchaseResult> => {",
|
|
734
|
+
" try {",
|
|
735
|
+
" const purchase = await requestPurchase({ sku: productId });",
|
|
736
|
+
" if (purchase && purchase.length > 0) {",
|
|
737
|
+
" await handlePurchase(purchase[0]);",
|
|
738
|
+
" return { success: true, purchase: purchase[0] };",
|
|
739
|
+
" }",
|
|
740
|
+
" return { success: false };",
|
|
741
|
+
" } catch (error) {",
|
|
742
|
+
" const paymentError = mapError(error);",
|
|
743
|
+
" return { success: false, error: paymentError };",
|
|
744
|
+
" }",
|
|
745
|
+
" },",
|
|
746
|
+
" [handlePurchase]",
|
|
747
|
+
" );",
|
|
748
|
+
"",
|
|
749
|
+
" const buySubscription = useCallback(",
|
|
750
|
+
" async (subscriptionId: string): Promise<PurchaseResult> => {",
|
|
751
|
+
" try {",
|
|
752
|
+
" const purchase = await requestSubscription({ sku: subscriptionId });",
|
|
753
|
+
" if (purchase && purchase.length > 0) {",
|
|
754
|
+
" await handlePurchase(purchase[0]);",
|
|
755
|
+
" return { success: true, purchase: purchase[0] };",
|
|
756
|
+
" }",
|
|
757
|
+
" return { success: false };",
|
|
758
|
+
" } catch (error) {",
|
|
759
|
+
" const paymentError = mapError(error);",
|
|
760
|
+
" return { success: false, error: paymentError };",
|
|
761
|
+
" }",
|
|
762
|
+
" },",
|
|
763
|
+
" [handlePurchase]",
|
|
764
|
+
" );",
|
|
765
|
+
"",
|
|
766
|
+
" useEffect(() => {",
|
|
767
|
+
" initialize();",
|
|
768
|
+
"",
|
|
769
|
+
" purchaseUpdateSubscription.current = purchaseUpdatedListener(",
|
|
770
|
+
" async (purchase: Purchase) => {",
|
|
771
|
+
' console.log("[Payment] Purchase updated:", purchase.transactionId);',
|
|
772
|
+
" await handlePurchase(purchase);",
|
|
773
|
+
" }",
|
|
774
|
+
" );",
|
|
775
|
+
"",
|
|
776
|
+
" purchaseErrorSubscription.current = purchaseErrorListener(",
|
|
777
|
+
" (error: PurchaseError) => {",
|
|
778
|
+
' console.error("[Payment] Purchase error:", error.message);',
|
|
779
|
+
" }",
|
|
780
|
+
" );",
|
|
781
|
+
"",
|
|
782
|
+
" return () => {",
|
|
783
|
+
" purchaseUpdateSubscription.current?.remove();",
|
|
784
|
+
" purchaseErrorSubscription.current?.remove();",
|
|
785
|
+
" endConnection();",
|
|
786
|
+
" };",
|
|
787
|
+
" }, [initialize, handlePurchase]);",
|
|
788
|
+
"",
|
|
789
|
+
" return {",
|
|
790
|
+
" products,",
|
|
791
|
+
" isLoading,",
|
|
792
|
+
" isConnected,",
|
|
793
|
+
" buyProduct,",
|
|
794
|
+
" buySubscription,",
|
|
795
|
+
" refresh: initialize,",
|
|
796
|
+
" };",
|
|
797
|
+
"}",
|
|
798
|
+
"",
|
|
799
|
+
"function mapProductToInfo(product: Product): ProductInfo {",
|
|
800
|
+
" return {",
|
|
801
|
+
" productId: product.productId,",
|
|
802
|
+
" title: product.title,",
|
|
803
|
+
" description: product.description,",
|
|
804
|
+
" price: product.price,",
|
|
805
|
+
' currency: product.currency || "USD",',
|
|
806
|
+
" };",
|
|
807
|
+
"}",
|
|
808
|
+
"",
|
|
809
|
+
"function mapError(error: unknown): AppPaymentError {",
|
|
810
|
+
" if (error instanceof Error) {",
|
|
811
|
+
" return {",
|
|
812
|
+
' code: "PURCHASE_ERROR",',
|
|
813
|
+
" message: error.message,",
|
|
814
|
+
" };",
|
|
815
|
+
" }",
|
|
816
|
+
" return {",
|
|
817
|
+
' code: "UNKNOWN_ERROR",',
|
|
818
|
+
' message: "An unknown error occurred",',
|
|
819
|
+
" };",
|
|
820
|
+
"}"
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
]
|
|
824
|
+
};
|
|
825
|
+
var payment_default = paymentModule;
|
|
826
|
+
|
|
827
|
+
// src/modules/form.ts
|
|
828
|
+
var formModule = {
|
|
829
|
+
id: "form",
|
|
830
|
+
name: "\u8868\u5355\u9A8C\u8BC1",
|
|
831
|
+
description: "React Hook Form + Zod",
|
|
832
|
+
defaultChecked: false,
|
|
833
|
+
dependencies: {
|
|
834
|
+
"react-hook-form": "^7.54.0",
|
|
835
|
+
"@hookform/resolvers": "^3.9.0",
|
|
836
|
+
zod: "^3.24.0"
|
|
837
|
+
},
|
|
838
|
+
devDependencies: {},
|
|
839
|
+
files: [
|
|
840
|
+
{
|
|
841
|
+
path: "src/modules/form/index.ts",
|
|
842
|
+
content: lines(
|
|
843
|
+
'export { useForm } from "react-hook-form";',
|
|
844
|
+
'export { z } from "zod";',
|
|
845
|
+
'export { zodResolver } from "@hookform/resolvers/zod";',
|
|
846
|
+
'export * from "./schemas/auth";'
|
|
847
|
+
)
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
path: "src/modules/form/schemas/auth.ts",
|
|
851
|
+
content: lines(
|
|
852
|
+
'import { z } from "zod";',
|
|
853
|
+
"",
|
|
854
|
+
"export const loginSchema = z.object({",
|
|
855
|
+
" email: z",
|
|
856
|
+
" .string()",
|
|
857
|
+
' .min(1, "\u8BF7\u8F93\u5165\u90AE\u7BB1\u5730\u5740")',
|
|
858
|
+
' .email("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740"),',
|
|
859
|
+
" password: z",
|
|
860
|
+
" .string()",
|
|
861
|
+
' .min(6, "\u5BC6\u7801\u81F3\u5C116\u4F4D")',
|
|
862
|
+
' .max(50, "\u5BC6\u7801\u6700\u591A50\u4F4D"),',
|
|
863
|
+
"});",
|
|
864
|
+
"",
|
|
865
|
+
"export const registerSchema = z",
|
|
866
|
+
" .object({",
|
|
867
|
+
" name: z",
|
|
868
|
+
" .string()",
|
|
869
|
+
' .min(2, "\u59D3\u540D\u81F3\u5C112\u4E2A\u5B57\u7B26")',
|
|
870
|
+
' .max(50, "\u59D3\u540D\u6700\u591A50\u4E2A\u5B57\u7B26"),',
|
|
871
|
+
" email: z",
|
|
872
|
+
" .string()",
|
|
873
|
+
' .min(1, "\u8BF7\u8F93\u5165\u90AE\u7BB1\u5730\u5740")',
|
|
874
|
+
' .email("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740"),',
|
|
875
|
+
" password: z",
|
|
876
|
+
" .string()",
|
|
877
|
+
' .min(6, "\u5BC6\u7801\u81F3\u5C116\u4F4D")',
|
|
878
|
+
' .max(50, "\u5BC6\u7801\u6700\u591A50\u4F4D"),',
|
|
879
|
+
' confirmPassword: z.string().min(1, "\u8BF7\u786E\u8BA4\u5BC6\u7801"),',
|
|
880
|
+
" })",
|
|
881
|
+
" .refine((data) => data.password === data.confirmPassword, {",
|
|
882
|
+
' message: "\u4E24\u6B21\u5BC6\u7801\u8F93\u5165\u4E0D\u4E00\u81F4",',
|
|
883
|
+
' path: ["confirmPassword"],',
|
|
884
|
+
" });",
|
|
885
|
+
"",
|
|
886
|
+
"export const resetPasswordSchema = z.object({",
|
|
887
|
+
" email: z",
|
|
888
|
+
" .string()",
|
|
889
|
+
' .min(1, "\u8BF7\u8F93\u5165\u90AE\u7BB1\u5730\u5740")',
|
|
890
|
+
' .email("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740"),',
|
|
891
|
+
"});",
|
|
892
|
+
"",
|
|
893
|
+
"export const changePasswordSchema = z",
|
|
894
|
+
" .object({",
|
|
895
|
+
' currentPassword: z.string().min(1, "\u8BF7\u8F93\u5165\u5F53\u524D\u5BC6\u7801"),',
|
|
896
|
+
" newPassword: z",
|
|
897
|
+
" .string()",
|
|
898
|
+
' .min(6, "\u65B0\u5BC6\u7801\u81F3\u5C116\u4F4D")',
|
|
899
|
+
' .max(50, "\u65B0\u5BC6\u7801\u6700\u591A50\u4F4D"),',
|
|
900
|
+
' confirmPassword: z.string().min(1, "\u8BF7\u786E\u8BA4\u65B0\u5BC6\u7801"),',
|
|
901
|
+
" })",
|
|
902
|
+
" .refine((data) => data.newPassword === data.confirmPassword, {",
|
|
903
|
+
' message: "\u4E24\u6B21\u5BC6\u7801\u8F93\u5165\u4E0D\u4E00\u81F4",',
|
|
904
|
+
' path: ["confirmPassword"],',
|
|
905
|
+
" });",
|
|
906
|
+
"",
|
|
907
|
+
"export type LoginFormData = z.infer<typeof loginSchema>;",
|
|
908
|
+
"export type RegisterFormData = z.infer<typeof registerSchema>;",
|
|
909
|
+
"export type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;",
|
|
910
|
+
"export type ChangePasswordFormData = z.infer<typeof changePasswordSchema>;"
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
};
|
|
915
|
+
var form_default = formModule;
|
|
916
|
+
|
|
917
|
+
// src/modules/image.ts
|
|
918
|
+
var imageModule = {
|
|
919
|
+
id: "image",
|
|
920
|
+
name: "\u56FE\u7247",
|
|
921
|
+
description: "expo-image \u5C01\u88C5",
|
|
922
|
+
defaultChecked: false,
|
|
923
|
+
dependencies: {
|
|
924
|
+
"expo-image": "~2.0.0"
|
|
925
|
+
},
|
|
926
|
+
devDependencies: {},
|
|
927
|
+
files: [
|
|
928
|
+
{
|
|
929
|
+
path: "src/modules/media/CachedImage.tsx",
|
|
930
|
+
content: lines(
|
|
931
|
+
'import React, { useMemo } from "react";',
|
|
932
|
+
'import { Image, type ImageProps } from "expo-image";',
|
|
933
|
+
'import { StyleSheet, View, ActivityIndicator } from "react-native";',
|
|
934
|
+
"",
|
|
935
|
+
'interface CachedImageProps extends Omit<ImageProps, "source"> {',
|
|
936
|
+
" uri: string;",
|
|
937
|
+
" width?: number;",
|
|
938
|
+
" height?: number;",
|
|
939
|
+
" borderRadius?: number;",
|
|
940
|
+
" placeholderColor?: string;",
|
|
941
|
+
" showLoading?: boolean;",
|
|
942
|
+
"}",
|
|
943
|
+
"",
|
|
944
|
+
"export function CachedImage({",
|
|
945
|
+
" uri,",
|
|
946
|
+
" width,",
|
|
947
|
+
" height,",
|
|
948
|
+
" borderRadius,",
|
|
949
|
+
' placeholderColor = "#f0f0f0",',
|
|
950
|
+
" showLoading = true,",
|
|
951
|
+
" style,",
|
|
952
|
+
" ...rest",
|
|
953
|
+
"}: CachedImageProps) {",
|
|
954
|
+
" const containerStyle = useMemo(",
|
|
955
|
+
" () =>",
|
|
956
|
+
" StyleSheet.compose(",
|
|
957
|
+
" {",
|
|
958
|
+
" width,",
|
|
959
|
+
" height,",
|
|
960
|
+
" borderRadius,",
|
|
961
|
+
" backgroundColor: placeholderColor,",
|
|
962
|
+
' overflow: "hidden",',
|
|
963
|
+
" },",
|
|
964
|
+
" style as any",
|
|
965
|
+
" ),",
|
|
966
|
+
" [width, height, borderRadius, placeholderColor, style]",
|
|
967
|
+
" );",
|
|
968
|
+
"",
|
|
969
|
+
" return (",
|
|
970
|
+
" <View style={containerStyle}>",
|
|
971
|
+
" <Image",
|
|
972
|
+
" source={{ uri }}",
|
|
973
|
+
" style={StyleSheet.absoluteFillObject}",
|
|
974
|
+
' contentFit="cover"',
|
|
975
|
+
" transition={200}",
|
|
976
|
+
" placeholder={{",
|
|
977
|
+
' blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj",',
|
|
978
|
+
" }}",
|
|
979
|
+
" {...rest}",
|
|
980
|
+
" />",
|
|
981
|
+
" {showLoading && (",
|
|
982
|
+
" <Image.LoadingIndicatorContainer",
|
|
983
|
+
" style={StyleSheet.absoluteFillObject}",
|
|
984
|
+
" >",
|
|
985
|
+
' <ActivityIndicator color="#999" />',
|
|
986
|
+
" </Image.LoadingIndicatorContainer>",
|
|
987
|
+
" )}",
|
|
988
|
+
" </View>",
|
|
989
|
+
" );",
|
|
990
|
+
"}",
|
|
991
|
+
"",
|
|
992
|
+
"export default CachedImage;"
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
]
|
|
996
|
+
};
|
|
997
|
+
var image_default = imageModule;
|
|
998
|
+
|
|
999
|
+
// src/modules/video.ts
|
|
1000
|
+
var videoModule = {
|
|
1001
|
+
id: "video",
|
|
1002
|
+
name: "\u89C6\u9891",
|
|
1003
|
+
description: "expo-av \u5C01\u88C5",
|
|
1004
|
+
defaultChecked: false,
|
|
1005
|
+
dependencies: {
|
|
1006
|
+
"expo-av": "~15.0.0"
|
|
1007
|
+
},
|
|
1008
|
+
devDependencies: {},
|
|
1009
|
+
files: [
|
|
1010
|
+
{
|
|
1011
|
+
path: "src/modules/media/VideoPlayer.tsx",
|
|
1012
|
+
content: lines(
|
|
1013
|
+
'import React, { useCallback, useRef, useState } from "react";',
|
|
1014
|
+
"import {",
|
|
1015
|
+
" Video,",
|
|
1016
|
+
" ResizeMode,",
|
|
1017
|
+
" type AVPlaybackStatus,",
|
|
1018
|
+
" type VideoProps,",
|
|
1019
|
+
'} from "expo-av";',
|
|
1020
|
+
'import { StyleSheet, View, TouchableOpacity, ActivityIndicator } from "react-native";',
|
|
1021
|
+
"",
|
|
1022
|
+
'interface VideoPlayerProps extends Omit<VideoProps, "source"> {',
|
|
1023
|
+
" uri: string;",
|
|
1024
|
+
" autoPlay?: boolean;",
|
|
1025
|
+
" loop?: boolean;",
|
|
1026
|
+
" showControls?: boolean;",
|
|
1027
|
+
" width?: number;",
|
|
1028
|
+
" height?: number;",
|
|
1029
|
+
"}",
|
|
1030
|
+
"",
|
|
1031
|
+
"export function VideoPlayer({",
|
|
1032
|
+
" uri,",
|
|
1033
|
+
" autoPlay = false,",
|
|
1034
|
+
" loop = false,",
|
|
1035
|
+
" showControls = true,",
|
|
1036
|
+
" width,",
|
|
1037
|
+
" height,",
|
|
1038
|
+
" style,",
|
|
1039
|
+
" ...rest",
|
|
1040
|
+
"}: VideoPlayerProps) {",
|
|
1041
|
+
" const videoRef = useRef<Video>(null);",
|
|
1042
|
+
" const [isPlaying, setIsPlaying] = useState(autoPlay);",
|
|
1043
|
+
" const [isLoading, setIsLoading] = useState(true);",
|
|
1044
|
+
"",
|
|
1045
|
+
" const handlePlaybackStatusUpdate = useCallback(",
|
|
1046
|
+
" (status: AVPlaybackStatus) => {",
|
|
1047
|
+
" if (status.isLoaded) {",
|
|
1048
|
+
" setIsLoading(status.isBuffering);",
|
|
1049
|
+
" setIsPlaying(status.isPlaying);",
|
|
1050
|
+
" }",
|
|
1051
|
+
" },",
|
|
1052
|
+
" []",
|
|
1053
|
+
" );",
|
|
1054
|
+
"",
|
|
1055
|
+
" const togglePlayback = useCallback(async () => {",
|
|
1056
|
+
" if (!videoRef.current) return;",
|
|
1057
|
+
" if (isPlaying) {",
|
|
1058
|
+
" await videoRef.current.pauseAsync();",
|
|
1059
|
+
" } else {",
|
|
1060
|
+
" await videoRef.current.playAsync();",
|
|
1061
|
+
" }",
|
|
1062
|
+
" }, [isPlaying]);",
|
|
1063
|
+
"",
|
|
1064
|
+
" const containerStyle = StyleSheet.compose(",
|
|
1065
|
+
' { width, height, backgroundColor: "#000", borderRadius: 8, overflow: "hidden" },',
|
|
1066
|
+
" style as any",
|
|
1067
|
+
" );",
|
|
1068
|
+
"",
|
|
1069
|
+
" return (",
|
|
1070
|
+
" <View style={containerStyle}>",
|
|
1071
|
+
" <Video",
|
|
1072
|
+
" ref={videoRef}",
|
|
1073
|
+
" source={{ uri }}",
|
|
1074
|
+
" style={StyleSheet.absoluteFillObject}",
|
|
1075
|
+
" resizeMode={ResizeMode.CONTAIN}",
|
|
1076
|
+
" shouldPlay={autoPlay}",
|
|
1077
|
+
" isLooping={loop}",
|
|
1078
|
+
" onPlaybackStatusUpdate={handlePlaybackStatusUpdate}",
|
|
1079
|
+
" {...rest}",
|
|
1080
|
+
" />",
|
|
1081
|
+
"",
|
|
1082
|
+
" {isLoading && (",
|
|
1083
|
+
" <View style={styles.overlay}>",
|
|
1084
|
+
' <ActivityIndicator color="#fff" size="large" />',
|
|
1085
|
+
" </View>",
|
|
1086
|
+
" )}",
|
|
1087
|
+
"",
|
|
1088
|
+
" {showControls && !isLoading && (",
|
|
1089
|
+
" <TouchableOpacity",
|
|
1090
|
+
" style={styles.overlay}",
|
|
1091
|
+
" onPress={togglePlayback}",
|
|
1092
|
+
" activeOpacity={0.7}",
|
|
1093
|
+
" />",
|
|
1094
|
+
" )}",
|
|
1095
|
+
" </View>",
|
|
1096
|
+
" );",
|
|
1097
|
+
"}",
|
|
1098
|
+
"",
|
|
1099
|
+
"const styles = StyleSheet.create({",
|
|
1100
|
+
" overlay: {",
|
|
1101
|
+
" ...StyleSheet.absoluteFillObject,",
|
|
1102
|
+
' justifyContent: "center",',
|
|
1103
|
+
' alignItems: "center",',
|
|
1104
|
+
' backgroundColor: "rgba(0, 0, 0, 0.3)",',
|
|
1105
|
+
" },",
|
|
1106
|
+
"});",
|
|
1107
|
+
"",
|
|
1108
|
+
"export default VideoPlayer;"
|
|
1109
|
+
)
|
|
1110
|
+
}
|
|
1111
|
+
]
|
|
1112
|
+
};
|
|
1113
|
+
var video_default = videoModule;
|
|
1114
|
+
|
|
1115
|
+
// src/modules/auth-google.ts
|
|
1116
|
+
var authGoogleModule = {
|
|
1117
|
+
id: "auth-google",
|
|
1118
|
+
name: "Google \u767B\u5F55",
|
|
1119
|
+
description: "@react-native-google-signin/google-signin",
|
|
1120
|
+
defaultChecked: false,
|
|
1121
|
+
dependencies: {
|
|
1122
|
+
"@react-native-google-signin/google-signin": "^13.1.0"
|
|
1123
|
+
},
|
|
1124
|
+
devDependencies: {},
|
|
1125
|
+
files: [
|
|
1126
|
+
{
|
|
1127
|
+
path: "src/modules/auth/google.ts",
|
|
1128
|
+
content: lines(
|
|
1129
|
+
"import {",
|
|
1130
|
+
" GoogleSignin,",
|
|
1131
|
+
" statusCodes,",
|
|
1132
|
+
" type User,",
|
|
1133
|
+
'} from "@react-native-google-signin/google-signin";',
|
|
1134
|
+
"",
|
|
1135
|
+
"export interface GoogleAuthConfig {",
|
|
1136
|
+
" iosClientId?: string;",
|
|
1137
|
+
" webClientId?: string;",
|
|
1138
|
+
" offlineAccess?: boolean;",
|
|
1139
|
+
" forceCodeForRefreshToken?: boolean;",
|
|
1140
|
+
"}",
|
|
1141
|
+
"",
|
|
1142
|
+
"export function setupGoogleAuth(config: GoogleAuthConfig): void {",
|
|
1143
|
+
" GoogleSignin.configure({",
|
|
1144
|
+
" iosClientId: config.iosClientId,",
|
|
1145
|
+
" webClientId: config.webClientId,",
|
|
1146
|
+
" offlineAccess: config.offlineAccess ?? false,",
|
|
1147
|
+
" forceCodeForRefreshToken: config.forceCodeForRefreshToken ?? true,",
|
|
1148
|
+
" });",
|
|
1149
|
+
"}",
|
|
1150
|
+
"",
|
|
1151
|
+
"export async function signInWithGoogle(): Promise<{",
|
|
1152
|
+
" user: User | null;",
|
|
1153
|
+
" idToken: string | null;",
|
|
1154
|
+
" error: string | null;",
|
|
1155
|
+
"}> {",
|
|
1156
|
+
" try {",
|
|
1157
|
+
" await GoogleSignin.hasPlayServices({",
|
|
1158
|
+
" showPlayServicesUpdateDialog: true,",
|
|
1159
|
+
" });",
|
|
1160
|
+
"",
|
|
1161
|
+
" const userInfo = await GoogleSignin.signIn();",
|
|
1162
|
+
" const idToken = userInfo.data?.idToken ?? null;",
|
|
1163
|
+
"",
|
|
1164
|
+
" return {",
|
|
1165
|
+
" user: userInfo.data ?? null,",
|
|
1166
|
+
" idToken,",
|
|
1167
|
+
" error: null,",
|
|
1168
|
+
" };",
|
|
1169
|
+
" } catch (error: any) {",
|
|
1170
|
+
' let errorMessage = "Google \u767B\u5F55\u5931\u8D25";',
|
|
1171
|
+
"",
|
|
1172
|
+
" if (error.code === statusCodes.SIGN_IN_CANCELLED) {",
|
|
1173
|
+
' errorMessage = "\u7528\u6237\u53D6\u6D88\u4E86\u767B\u5F55";',
|
|
1174
|
+
" } else if (error.code === statusCodes.IN_PROGRESS) {",
|
|
1175
|
+
' errorMessage = "\u767B\u5F55\u6B63\u5728\u8FDB\u884C\u4E2D";',
|
|
1176
|
+
" } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {",
|
|
1177
|
+
' errorMessage = "\u8BBE\u5907\u4E0D\u652F\u6301 Google Play \u670D\u52A1";',
|
|
1178
|
+
" }",
|
|
1179
|
+
"",
|
|
1180
|
+
' console.error("[GoogleAuth] Error:", errorMessage, error);',
|
|
1181
|
+
" return { user: null, idToken: null, error: errorMessage };",
|
|
1182
|
+
" }",
|
|
1183
|
+
"}",
|
|
1184
|
+
"",
|
|
1185
|
+
"export async function signOutGoogle(): Promise<void> {",
|
|
1186
|
+
" try {",
|
|
1187
|
+
" await GoogleSignin.signOut();",
|
|
1188
|
+
" } catch (error) {",
|
|
1189
|
+
' console.error("[GoogleAuth] Sign out error:", error);',
|
|
1190
|
+
" }",
|
|
1191
|
+
"}",
|
|
1192
|
+
"",
|
|
1193
|
+
"export async function getCurrentGoogleUser(): Promise<User | null> {",
|
|
1194
|
+
" try {",
|
|
1195
|
+
" const user = await GoogleSignin.getCurrentUser();",
|
|
1196
|
+
" return user;",
|
|
1197
|
+
" } catch {",
|
|
1198
|
+
" return null;",
|
|
1199
|
+
" }",
|
|
1200
|
+
"}",
|
|
1201
|
+
"",
|
|
1202
|
+
"export async function isGoogleSignedIn(): Promise<boolean> {",
|
|
1203
|
+
" try {",
|
|
1204
|
+
" return GoogleSignin.hasPreviousSignIn();",
|
|
1205
|
+
" } catch {",
|
|
1206
|
+
" return false;",
|
|
1207
|
+
" }",
|
|
1208
|
+
"}"
|
|
1209
|
+
)
|
|
1210
|
+
}
|
|
1211
|
+
]
|
|
1212
|
+
};
|
|
1213
|
+
var auth_google_default = authGoogleModule;
|
|
1214
|
+
|
|
1215
|
+
// src/modules/auth-facebook.ts
|
|
1216
|
+
var authFacebookModule = {
|
|
1217
|
+
id: "auth-facebook",
|
|
1218
|
+
name: "Facebook \u767B\u5F55",
|
|
1219
|
+
description: "expo-facebook",
|
|
1220
|
+
defaultChecked: false,
|
|
1221
|
+
dependencies: {
|
|
1222
|
+
"expo-facebook": "~13.0.0"
|
|
1223
|
+
},
|
|
1224
|
+
devDependencies: {},
|
|
1225
|
+
files: [
|
|
1226
|
+
{
|
|
1227
|
+
path: "src/modules/auth/facebook.ts",
|
|
1228
|
+
content: lines(
|
|
1229
|
+
'import * as Facebook from "expo-facebook";',
|
|
1230
|
+
"",
|
|
1231
|
+
"export interface FacebookAuthConfig {",
|
|
1232
|
+
" appId: string;",
|
|
1233
|
+
" appName: string;",
|
|
1234
|
+
"}",
|
|
1235
|
+
"",
|
|
1236
|
+
"export async function setupFacebookAuth(config: FacebookAuthConfig): Promise<void> {",
|
|
1237
|
+
" await Facebook.initializeAsync({",
|
|
1238
|
+
" appId: config.appId,",
|
|
1239
|
+
" appName: config.appName,",
|
|
1240
|
+
" });",
|
|
1241
|
+
"}",
|
|
1242
|
+
"",
|
|
1243
|
+
"export async function signInWithFacebook(): Promise<{",
|
|
1244
|
+
" token: string | null;",
|
|
1245
|
+
" userId: string | null;",
|
|
1246
|
+
" userName: string | null;",
|
|
1247
|
+
" error: string | null;",
|
|
1248
|
+
"}> {",
|
|
1249
|
+
" try {",
|
|
1250
|
+
" const result = await Facebook.logInWithReadPermissionsAsync({",
|
|
1251
|
+
' permissions: ["public_profile", "email"],',
|
|
1252
|
+
" });",
|
|
1253
|
+
"",
|
|
1254
|
+
' if (result.type === "success") {',
|
|
1255
|
+
" const response = await fetch(",
|
|
1256
|
+
" `https://graph.facebook.com/me?access_token=${result.token}&fields=id,name,email`",
|
|
1257
|
+
" );",
|
|
1258
|
+
" const userData = await response.json();",
|
|
1259
|
+
"",
|
|
1260
|
+
" return {",
|
|
1261
|
+
" token: result.token,",
|
|
1262
|
+
" userId: userData.id,",
|
|
1263
|
+
" userName: userData.name,",
|
|
1264
|
+
" error: null,",
|
|
1265
|
+
" };",
|
|
1266
|
+
" } else {",
|
|
1267
|
+
" return {",
|
|
1268
|
+
" token: null,",
|
|
1269
|
+
" userId: null,",
|
|
1270
|
+
" userName: null,",
|
|
1271
|
+
' error: "\u7528\u6237\u53D6\u6D88\u4E86 Facebook \u767B\u5F55",',
|
|
1272
|
+
" };",
|
|
1273
|
+
" }",
|
|
1274
|
+
" } catch (error) {",
|
|
1275
|
+
' console.error("[FacebookAuth] Error:", error);',
|
|
1276
|
+
" return {",
|
|
1277
|
+
" token: null,",
|
|
1278
|
+
" userId: null,",
|
|
1279
|
+
" userName: null,",
|
|
1280
|
+
' error: "Facebook \u767B\u5F55\u5931\u8D25",',
|
|
1281
|
+
" };",
|
|
1282
|
+
" }",
|
|
1283
|
+
"}",
|
|
1284
|
+
"",
|
|
1285
|
+
"export async function signOutFacebook(): Promise<void> {",
|
|
1286
|
+
" try {",
|
|
1287
|
+
" await Facebook.logOutAsync();",
|
|
1288
|
+
" } catch (error) {",
|
|
1289
|
+
' console.error("[FacebookAuth] Sign out error:", error);',
|
|
1290
|
+
" }",
|
|
1291
|
+
"}"
|
|
1292
|
+
)
|
|
1293
|
+
}
|
|
1294
|
+
],
|
|
1295
|
+
appConfig: {
|
|
1296
|
+
plugins: [
|
|
1297
|
+
[
|
|
1298
|
+
"expo-facebook",
|
|
1299
|
+
{
|
|
1300
|
+
appId: "YOUR_FACEBOOK_APP_ID"
|
|
1301
|
+
}
|
|
1302
|
+
]
|
|
1303
|
+
]
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
var auth_facebook_default = authFacebookModule;
|
|
1307
|
+
|
|
1308
|
+
// src/modules/auth-apple.ts
|
|
1309
|
+
var authAppleModule = {
|
|
1310
|
+
id: "auth-apple",
|
|
1311
|
+
name: "Apple \u767B\u5F55",
|
|
1312
|
+
description: "expo-apple-authentication",
|
|
1313
|
+
defaultChecked: false,
|
|
1314
|
+
dependencies: {
|
|
1315
|
+
"expo-apple-authentication": "~8.0.0"
|
|
1316
|
+
},
|
|
1317
|
+
devDependencies: {},
|
|
1318
|
+
files: [
|
|
1319
|
+
{
|
|
1320
|
+
path: "src/modules/auth/apple.ts",
|
|
1321
|
+
content: lines(
|
|
1322
|
+
'import * as AppleAuthentication from "expo-apple-authentication";',
|
|
1323
|
+
'import { Platform } from "react-native";',
|
|
1324
|
+
"",
|
|
1325
|
+
"export interface AppleAuthResult {",
|
|
1326
|
+
" user: string;",
|
|
1327
|
+
" email: string | null;",
|
|
1328
|
+
" fullName: AppleAuthentication.AppleAuthenticationFullName | null;",
|
|
1329
|
+
" identityToken: string | null;",
|
|
1330
|
+
" authorizationCode: string | null;",
|
|
1331
|
+
" error: string | null;",
|
|
1332
|
+
"}",
|
|
1333
|
+
"",
|
|
1334
|
+
"export async function isAppleAuthAvailable(): Promise<boolean> {",
|
|
1335
|
+
' if (Platform.OS !== "ios") return false;',
|
|
1336
|
+
" return AppleAuthentication.isAvailableAsync();",
|
|
1337
|
+
"}",
|
|
1338
|
+
"",
|
|
1339
|
+
"export async function signInWithApple(): Promise<AppleAuthResult> {",
|
|
1340
|
+
" try {",
|
|
1341
|
+
" const credential = await AppleAuthentication.signInAsync({",
|
|
1342
|
+
" requestedScopes: [",
|
|
1343
|
+
" AppleAuthentication.AppleAuthenticationScope.FULL_NAME,",
|
|
1344
|
+
" AppleAuthentication.AppleAuthenticationScope.EMAIL,",
|
|
1345
|
+
" ],",
|
|
1346
|
+
" });",
|
|
1347
|
+
"",
|
|
1348
|
+
" return {",
|
|
1349
|
+
" user: credential.user,",
|
|
1350
|
+
" email: credential.email ?? null,",
|
|
1351
|
+
" fullName: credential.fullName ?? null,",
|
|
1352
|
+
" identityToken: credential.identityToken ?? null,",
|
|
1353
|
+
" authorizationCode: credential.authorizationCode ?? null,",
|
|
1354
|
+
" error: null,",
|
|
1355
|
+
" };",
|
|
1356
|
+
" } catch (error: any) {",
|
|
1357
|
+
' if (error.code === "ERR_CANCELED") {',
|
|
1358
|
+
" return {",
|
|
1359
|
+
' user: "",',
|
|
1360
|
+
" email: null,",
|
|
1361
|
+
" fullName: null,",
|
|
1362
|
+
" identityToken: null,",
|
|
1363
|
+
" authorizationCode: null,",
|
|
1364
|
+
' error: "\u7528\u6237\u53D6\u6D88\u4E86 Apple \u767B\u5F55",',
|
|
1365
|
+
" };",
|
|
1366
|
+
" }",
|
|
1367
|
+
"",
|
|
1368
|
+
' console.error("[AppleAuth] Error:", error);',
|
|
1369
|
+
" return {",
|
|
1370
|
+
' user: "",',
|
|
1371
|
+
" email: null,",
|
|
1372
|
+
" fullName: null,",
|
|
1373
|
+
" identityToken: null,",
|
|
1374
|
+
" authorizationCode: null,",
|
|
1375
|
+
' error: "Apple \u767B\u5F55\u5931\u8D25",',
|
|
1376
|
+
" };",
|
|
1377
|
+
" }",
|
|
1378
|
+
"}",
|
|
1379
|
+
"",
|
|
1380
|
+
"export async function getAppleCredentialState(",
|
|
1381
|
+
" userId: string",
|
|
1382
|
+
"): Promise<AppleAuthentication.AppleAuthenticationCredentialState> {",
|
|
1383
|
+
" return AppleAuthentication.getCredentialStateAsync(userId);",
|
|
1384
|
+
"}",
|
|
1385
|
+
"",
|
|
1386
|
+
"export async function signOutApple(userId: string): Promise<boolean> {",
|
|
1387
|
+
" try {",
|
|
1388
|
+
" const state = await getAppleCredentialState(userId);",
|
|
1389
|
+
" return state === AppleAuthentication.AppleAuthenticationCredentialState.REVOKED;",
|
|
1390
|
+
" } catch {",
|
|
1391
|
+
" return false;",
|
|
1392
|
+
" }",
|
|
1393
|
+
"}"
|
|
1394
|
+
)
|
|
1395
|
+
}
|
|
1396
|
+
],
|
|
1397
|
+
appConfig: {
|
|
1398
|
+
plugins: ["expo-apple-authentication"]
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
var auth_apple_default = authAppleModule;
|
|
1402
|
+
|
|
1403
|
+
// src/modules/webview.ts
|
|
1404
|
+
var webviewModule = {
|
|
1405
|
+
id: "webview",
|
|
1406
|
+
name: "WebView \u5BB9\u5668",
|
|
1407
|
+
description: "react-native-webview + JS Bridge",
|
|
1408
|
+
defaultChecked: false,
|
|
1409
|
+
dependencies: {
|
|
1410
|
+
"react-native-webview": "^13.12.0"
|
|
1411
|
+
},
|
|
1412
|
+
devDependencies: {},
|
|
1413
|
+
files: [
|
|
1414
|
+
{
|
|
1415
|
+
path: "src/modules/webview/WebViewContainer.tsx",
|
|
1416
|
+
content: lines(
|
|
1417
|
+
'import React, { useCallback, useRef } from "react";',
|
|
1418
|
+
'import { View, StyleSheet, ActivityIndicator } from "react-native";',
|
|
1419
|
+
"import WebView, {",
|
|
1420
|
+
" type WebViewMessageEvent,",
|
|
1421
|
+
" type WebViewProps,",
|
|
1422
|
+
'} from "react-native-webview";',
|
|
1423
|
+
'import { bridge, type BridgeMessage } from "./bridge";',
|
|
1424
|
+
"",
|
|
1425
|
+
'interface WebViewContainerProps extends Omit<WebViewProps, "source"> {',
|
|
1426
|
+
" uri: string;",
|
|
1427
|
+
" showLoading?: boolean;",
|
|
1428
|
+
" onBridgeMessage?: (message: BridgeMessage) => void;",
|
|
1429
|
+
"}",
|
|
1430
|
+
"",
|
|
1431
|
+
"export function WebViewContainer({",
|
|
1432
|
+
" uri,",
|
|
1433
|
+
" showLoading = true,",
|
|
1434
|
+
" onBridgeMessage,",
|
|
1435
|
+
" style,",
|
|
1436
|
+
" ...rest",
|
|
1437
|
+
"}: WebViewContainerProps) {",
|
|
1438
|
+
" const webViewRef = useRef<WebView>(null);",
|
|
1439
|
+
"",
|
|
1440
|
+
" const handleMessage = useCallback(",
|
|
1441
|
+
" (event: WebViewMessageEvent) => {",
|
|
1442
|
+
" try {",
|
|
1443
|
+
" const data = JSON.parse(event.nativeEvent.data) as BridgeMessage;",
|
|
1444
|
+
" const processed = bridge.receive(data);",
|
|
1445
|
+
" if (onBridgeMessage) {",
|
|
1446
|
+
" onBridgeMessage(processed);",
|
|
1447
|
+
" }",
|
|
1448
|
+
" } catch (error) {",
|
|
1449
|
+
' console.warn("[WebView] Failed to parse message:", error);',
|
|
1450
|
+
" }",
|
|
1451
|
+
" },",
|
|
1452
|
+
" [onBridgeMessage]",
|
|
1453
|
+
" );",
|
|
1454
|
+
"",
|
|
1455
|
+
" const sendToWebView = useCallback(",
|
|
1456
|
+
" (message: BridgeMessage) => {",
|
|
1457
|
+
" const script = bridge.buildSendScript(message);",
|
|
1458
|
+
" webViewRef.current?.injectJavaScript(script);",
|
|
1459
|
+
" },",
|
|
1460
|
+
" []",
|
|
1461
|
+
" );",
|
|
1462
|
+
"",
|
|
1463
|
+
" const injectedJavaScript = [",
|
|
1464
|
+
' "(function() {",',
|
|
1465
|
+
' " window.ReactNativeWebView = window.ReactNativeWebView || {};",',
|
|
1466
|
+
' " window.NativeBridge = {",',
|
|
1467
|
+
' " send: function(type, payload) {",',
|
|
1468
|
+
' " window.ReactNativeWebView.postMessage(JSON.stringify({ type: type, payload: payload }));",',
|
|
1469
|
+
' " }",,',
|
|
1470
|
+
' " };,',
|
|
1471
|
+
' "})();,',
|
|
1472
|
+
' "true;,',
|
|
1473
|
+
' ].join("\\\\n");',
|
|
1474
|
+
"",
|
|
1475
|
+
" return (",
|
|
1476
|
+
" <View style={styles.container}>",
|
|
1477
|
+
" <WebView",
|
|
1478
|
+
" ref={webViewRef}",
|
|
1479
|
+
" source={{ uri }}",
|
|
1480
|
+
" style={[styles.webview, style]}",
|
|
1481
|
+
' originWhitelist={["*"]}',
|
|
1482
|
+
" javaScriptEnabled",
|
|
1483
|
+
" domStorageEnabled",
|
|
1484
|
+
" injectedJavaScript={injectedJavaScript}",
|
|
1485
|
+
" onMessage={handleMessage}",
|
|
1486
|
+
" startInLoadingState={showLoading}",
|
|
1487
|
+
" renderLoading={() =>",
|
|
1488
|
+
" showLoading ? (",
|
|
1489
|
+
" <View style={styles.loadingContainer}>",
|
|
1490
|
+
' <ActivityIndicator size="large" color="#007AFF" />',
|
|
1491
|
+
" </View>",
|
|
1492
|
+
" ) : null",
|
|
1493
|
+
" }",
|
|
1494
|
+
" {...rest}",
|
|
1495
|
+
" />",
|
|
1496
|
+
" </View>",
|
|
1497
|
+
" );",
|
|
1498
|
+
"}",
|
|
1499
|
+
"",
|
|
1500
|
+
"const styles = StyleSheet.create({",
|
|
1501
|
+
" container: {",
|
|
1502
|
+
" flex: 1,",
|
|
1503
|
+
" },",
|
|
1504
|
+
" webview: {",
|
|
1505
|
+
" flex: 1,",
|
|
1506
|
+
" },",
|
|
1507
|
+
" loadingContainer: {",
|
|
1508
|
+
' position: "absolute",',
|
|
1509
|
+
" top: 0,",
|
|
1510
|
+
" left: 0,",
|
|
1511
|
+
" right: 0,",
|
|
1512
|
+
" bottom: 0,",
|
|
1513
|
+
' justifyContent: "center",',
|
|
1514
|
+
' alignItems: "center",',
|
|
1515
|
+
' backgroundColor: "rgba(255, 255, 255, 0.8)",',
|
|
1516
|
+
" },",
|
|
1517
|
+
"});",
|
|
1518
|
+
"",
|
|
1519
|
+
"export default WebViewContainer;"
|
|
1520
|
+
)
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
path: "src/modules/webview/bridge.ts",
|
|
1524
|
+
content: lines(
|
|
1525
|
+
"export interface BridgeMessage {",
|
|
1526
|
+
" type: string;",
|
|
1527
|
+
" payload: Record<string, unknown>;",
|
|
1528
|
+
"}",
|
|
1529
|
+
"",
|
|
1530
|
+
"export const BridgeMessageType = {",
|
|
1531
|
+
' NAVIGATE: "NAVIGATE",',
|
|
1532
|
+
' SHARE: "SHARE",',
|
|
1533
|
+
' PAYMENT: "PAYMENT",',
|
|
1534
|
+
' AUTH: "AUTH",',
|
|
1535
|
+
' TOAST: "TOAST",',
|
|
1536
|
+
' CLOSE: "CLOSE",',
|
|
1537
|
+
' CUSTOM: "CUSTOM",',
|
|
1538
|
+
"} as const;",
|
|
1539
|
+
"",
|
|
1540
|
+
"export type BridgeMessageTypeValue =",
|
|
1541
|
+
" (typeof BridgeMessageType)[keyof typeof BridgeMessageType];",
|
|
1542
|
+
"",
|
|
1543
|
+
"export const bridge = {",
|
|
1544
|
+
" receive(message: BridgeMessage): BridgeMessage {",
|
|
1545
|
+
" if (!message.type) {",
|
|
1546
|
+
` throw new Error("Bridge message must have a 'type' field");`,
|
|
1547
|
+
" }",
|
|
1548
|
+
" return {",
|
|
1549
|
+
" type: message.type,",
|
|
1550
|
+
" payload: message.payload || {},",
|
|
1551
|
+
" };",
|
|
1552
|
+
" },",
|
|
1553
|
+
"",
|
|
1554
|
+
" buildSendScript(message: BridgeMessage): string {",
|
|
1555
|
+
" const payload = JSON.stringify(message.payload);",
|
|
1556
|
+
" return [",
|
|
1557
|
+
' "(function() {",',
|
|
1558
|
+
` " if (typeof window.onNativeMessage === 'function') {",`,
|
|
1559
|
+
` " window.onNativeMessage('" + message.type + "', " + payload + ");",`,
|
|
1560
|
+
' " }",',
|
|
1561
|
+
' "})();",',
|
|
1562
|
+
' "true;",',
|
|
1563
|
+
' ].join("\\\\n");',
|
|
1564
|
+
" },",
|
|
1565
|
+
"",
|
|
1566
|
+
" createMessage(",
|
|
1567
|
+
" type: BridgeMessageTypeValue | string,",
|
|
1568
|
+
" payload: Record<string, unknown> = {}",
|
|
1569
|
+
" ): BridgeMessage {",
|
|
1570
|
+
" return { type, payload };",
|
|
1571
|
+
" },",
|
|
1572
|
+
"};"
|
|
1573
|
+
)
|
|
1574
|
+
}
|
|
1575
|
+
]
|
|
1576
|
+
};
|
|
1577
|
+
var webview_default = webviewModule;
|
|
1578
|
+
|
|
1579
|
+
// src/modules/i18n.ts
|
|
1580
|
+
var i18nModule = {
|
|
1581
|
+
id: "i18n",
|
|
1582
|
+
name: "\u591A\u8BED\u8A00",
|
|
1583
|
+
description: "i18next + react-i18next",
|
|
1584
|
+
defaultChecked: false,
|
|
1585
|
+
dependencies: {
|
|
1586
|
+
i18next: "^24.2.0",
|
|
1587
|
+
"react-i18next": "^15.2.0",
|
|
1588
|
+
"expo-localization": "~16.0.0"
|
|
1589
|
+
},
|
|
1590
|
+
devDependencies: {},
|
|
1591
|
+
files: [
|
|
1592
|
+
{
|
|
1593
|
+
path: "src/modules/i18n/index.ts",
|
|
1594
|
+
content: lines(
|
|
1595
|
+
'import i18n from "i18next";',
|
|
1596
|
+
'import { initReactI18next } from "react-i18next";',
|
|
1597
|
+
'import { getLocales } from "expo-localization";',
|
|
1598
|
+
'import en from "./locales/en.json";',
|
|
1599
|
+
'import zh from "./locales/zh.json";',
|
|
1600
|
+
"",
|
|
1601
|
+
"const resources = {",
|
|
1602
|
+
" en: { translation: en },",
|
|
1603
|
+
" zh: { translation: zh },",
|
|
1604
|
+
"};",
|
|
1605
|
+
"",
|
|
1606
|
+
"function getDeviceLanguage(): string {",
|
|
1607
|
+
" const locales = getLocales();",
|
|
1608
|
+
' const deviceLang = locales[0]?.languageCode ?? "en";',
|
|
1609
|
+
' return deviceLang in resources ? deviceLang : "en";',
|
|
1610
|
+
"}",
|
|
1611
|
+
"",
|
|
1612
|
+
"i18n.use(initReactI18next).init({",
|
|
1613
|
+
" resources,",
|
|
1614
|
+
" lng: getDeviceLanguage(),",
|
|
1615
|
+
' fallbackLng: "en",',
|
|
1616
|
+
" interpolation: {",
|
|
1617
|
+
" escapeValue: false,",
|
|
1618
|
+
" },",
|
|
1619
|
+
' compatibilityJSON: "v4",',
|
|
1620
|
+
"});",
|
|
1621
|
+
"",
|
|
1622
|
+
"export default i18n;",
|
|
1623
|
+
"",
|
|
1624
|
+
'export { useTranslation } from "react-i18next";'
|
|
1625
|
+
)
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
path: "src/modules/i18n/locales/en.json",
|
|
1629
|
+
content: [
|
|
1630
|
+
"{",
|
|
1631
|
+
' "common": {',
|
|
1632
|
+
' "ok": "OK",',
|
|
1633
|
+
' "cancel": "Cancel",',
|
|
1634
|
+
' "confirm": "Confirm",',
|
|
1635
|
+
' "save": "Save",',
|
|
1636
|
+
' "delete": "Delete",',
|
|
1637
|
+
' "edit": "Edit",',
|
|
1638
|
+
' "loading": "Loading...",',
|
|
1639
|
+
' "error": "Something went wrong",',
|
|
1640
|
+
' "retry": "Retry",',
|
|
1641
|
+
' "search": "Search"',
|
|
1642
|
+
" },",
|
|
1643
|
+
' "auth": {',
|
|
1644
|
+
' "login": "Sign In",',
|
|
1645
|
+
' "register": "Sign Up",',
|
|
1646
|
+
' "logout": "Sign Out",',
|
|
1647
|
+
' "email": "Email",',
|
|
1648
|
+
' "password": "Password",',
|
|
1649
|
+
' "forgotPassword": "Forgot Password?"',
|
|
1650
|
+
" },",
|
|
1651
|
+
' "home": {',
|
|
1652
|
+
' "title": "Home",',
|
|
1653
|
+
' "welcome": "Welcome"',
|
|
1654
|
+
" },",
|
|
1655
|
+
' "settings": {',
|
|
1656
|
+
' "title": "Settings",',
|
|
1657
|
+
' "language": "Language",',
|
|
1658
|
+
' "theme": "Theme",',
|
|
1659
|
+
' "about": "About"',
|
|
1660
|
+
" },",
|
|
1661
|
+
' "errors": {',
|
|
1662
|
+
' "network": "Network error. Please check your connection.",',
|
|
1663
|
+
' "unauthorized": "Session expired. Please sign in again.",',
|
|
1664
|
+
` "forbidden": "You don't have permission to do this.",`,
|
|
1665
|
+
' "notFound": "The requested resource was not found.",',
|
|
1666
|
+
' "server": "Server error. Please try again later."',
|
|
1667
|
+
" }",
|
|
1668
|
+
"}"
|
|
1669
|
+
].join("\n")
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
path: "src/modules/i18n/locales/zh.json",
|
|
1673
|
+
content: [
|
|
1674
|
+
"{",
|
|
1675
|
+
' "common": {',
|
|
1676
|
+
' "ok": "\u786E\u5B9A",',
|
|
1677
|
+
' "cancel": "\u53D6\u6D88",',
|
|
1678
|
+
' "confirm": "\u786E\u8BA4",',
|
|
1679
|
+
' "save": "\u4FDD\u5B58",',
|
|
1680
|
+
' "delete": "\u5220\u9664",',
|
|
1681
|
+
' "edit": "\u7F16\u8F91",',
|
|
1682
|
+
' "loading": "\u52A0\u8F7D\u4E2D...",',
|
|
1683
|
+
' "error": "\u51FA\u4E86\u70B9\u95EE\u9898",',
|
|
1684
|
+
' "retry": "\u91CD\u8BD5",',
|
|
1685
|
+
' "search": "\u641C\u7D22"',
|
|
1686
|
+
" },",
|
|
1687
|
+
' "auth": {',
|
|
1688
|
+
' "login": "\u767B\u5F55",',
|
|
1689
|
+
' "register": "\u6CE8\u518C",',
|
|
1690
|
+
' "logout": "\u9000\u51FA\u767B\u5F55",',
|
|
1691
|
+
' "email": "\u90AE\u7BB1",',
|
|
1692
|
+
' "password": "\u5BC6\u7801",',
|
|
1693
|
+
' "forgotPassword": "\u5FD8\u8BB0\u5BC6\u7801\uFF1F"',
|
|
1694
|
+
" },",
|
|
1695
|
+
' "home": {',
|
|
1696
|
+
' "title": "\u9996\u9875",',
|
|
1697
|
+
' "welcome": "\u6B22\u8FCE"',
|
|
1698
|
+
" },",
|
|
1699
|
+
' "settings": {',
|
|
1700
|
+
' "title": "\u8BBE\u7F6E",',
|
|
1701
|
+
' "language": "\u8BED\u8A00",',
|
|
1702
|
+
' "theme": "\u4E3B\u9898",',
|
|
1703
|
+
' "about": "\u5173\u4E8E"',
|
|
1704
|
+
" },",
|
|
1705
|
+
' "errors": {',
|
|
1706
|
+
' "network": "\u7F51\u7EDC\u9519\u8BEF\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002",',
|
|
1707
|
+
' "unauthorized": "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u3002",',
|
|
1708
|
+
' "forbidden": "\u60A8\u6CA1\u6709\u6743\u9650\u6267\u884C\u6B64\u64CD\u4F5C\u3002",',
|
|
1709
|
+
' "notFound": "\u672A\u627E\u5230\u8BF7\u6C42\u7684\u8D44\u6E90\u3002",',
|
|
1710
|
+
' "server": "\u670D\u52A1\u5668\u9519\u8BEF\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"',
|
|
1711
|
+
" }",
|
|
1712
|
+
"}"
|
|
1713
|
+
].join("\n")
|
|
1714
|
+
}
|
|
1715
|
+
]
|
|
1716
|
+
};
|
|
1717
|
+
var i18n_default = i18nModule;
|
|
1718
|
+
|
|
1719
|
+
// src/modules/animation.ts
|
|
1720
|
+
var animationModule = {
|
|
1721
|
+
id: "animation",
|
|
1722
|
+
name: "\u52A8\u753B/\u624B\u52BF",
|
|
1723
|
+
description: "Reanimated 3 + Gesture Handler",
|
|
1724
|
+
defaultChecked: false,
|
|
1725
|
+
dependencies: {
|
|
1726
|
+
"react-native-reanimated": "~3.17.0",
|
|
1727
|
+
"react-native-gesture-handler": "~2.22.0"
|
|
1728
|
+
},
|
|
1729
|
+
devDependencies: {},
|
|
1730
|
+
files: [
|
|
1731
|
+
{
|
|
1732
|
+
path: "src/modules/animation/index.ts",
|
|
1733
|
+
content: lines(
|
|
1734
|
+
'export { FadeIn, FadeOut, SlideIn, SlideOut, ScaleIn, ScaleOut } from "./transitions";'
|
|
1735
|
+
)
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
path: "src/modules/animation/transitions.ts",
|
|
1739
|
+
content: lines(
|
|
1740
|
+
"import Animated, {",
|
|
1741
|
+
" FadeIn as ReanimatedFadeIn,",
|
|
1742
|
+
" FadeOut as ReanimatedFadeOut,",
|
|
1743
|
+
" SlideInUp as ReanimatedSlideInUp,",
|
|
1744
|
+
" SlideInDown as ReanimatedSlideInDown,",
|
|
1745
|
+
" SlideInLeft as ReanimatedSlideInLeft,",
|
|
1746
|
+
" SlideInRight as ReanimatedSlideInRight,",
|
|
1747
|
+
" SlideOutUp as ReanimatedSlideOutUp,",
|
|
1748
|
+
" SlideOutDown as ReanimatedSlideOutDown,",
|
|
1749
|
+
" SlideOutLeft as ReanimatedSlideOutLeft,",
|
|
1750
|
+
" SlideOutRight as ReanimatedSlideOutRight,",
|
|
1751
|
+
" ZoomIn as ReanimatedZoomIn,",
|
|
1752
|
+
" ZoomOut as ReanimatedZoomOut,",
|
|
1753
|
+
" type EntryExitAnimationFunction,",
|
|
1754
|
+
'} from "react-native-reanimated";',
|
|
1755
|
+
"",
|
|
1756
|
+
"export const FadeIn = ReanimatedFadeIn.duration(300);",
|
|
1757
|
+
"export const FadeOut = ReanimatedFadeOut.duration(300);",
|
|
1758
|
+
"export const SlideIn = ReanimatedSlideInDown.duration(300).springify();",
|
|
1759
|
+
"export const SlideInTop = ReanimatedSlideInUp.duration(300).springify();",
|
|
1760
|
+
"export const SlideInLeft = ReanimatedSlideInLeft.duration(300).springify();",
|
|
1761
|
+
"export const SlideInRight = ReanimatedSlideInRight.duration(300).springify();",
|
|
1762
|
+
"export const SlideOut = ReanimatedSlideOutDown.duration(300).springify();",
|
|
1763
|
+
"export const SlideOutTop = ReanimatedSlideOutUp.duration(300).springify();",
|
|
1764
|
+
"export const SlideOutLeft = ReanimatedSlideOutLeft.duration(300).springify();",
|
|
1765
|
+
"export const SlideOutRight = ReanimatedSlideOutRight.duration(300).springify();",
|
|
1766
|
+
"export const ScaleIn = ReanimatedZoomIn.duration(200).springify();",
|
|
1767
|
+
"export const ScaleOut = ReanimatedZoomOut.duration(200).springify();",
|
|
1768
|
+
"",
|
|
1769
|
+
"export function staggerFadeIn(",
|
|
1770
|
+
" index: number,",
|
|
1771
|
+
" baseDelay: number = 50",
|
|
1772
|
+
"): EntryExitAnimationFunction {",
|
|
1773
|
+
" return ReanimatedFadeIn.duration(300).delay(index * baseDelay);",
|
|
1774
|
+
"}",
|
|
1775
|
+
"",
|
|
1776
|
+
"export function staggerSlideIn(",
|
|
1777
|
+
" index: number,",
|
|
1778
|
+
" baseDelay: number = 80",
|
|
1779
|
+
"): EntryExitAnimationFunction {",
|
|
1780
|
+
" return ReanimatedSlideInDown.duration(400).springify().delay(index * baseDelay);",
|
|
1781
|
+
"}",
|
|
1782
|
+
"",
|
|
1783
|
+
"export { Animated };"
|
|
1784
|
+
)
|
|
1785
|
+
}
|
|
1786
|
+
],
|
|
1787
|
+
babelPlugins: ["react-native-reanimated/plugin"]
|
|
1788
|
+
};
|
|
1789
|
+
var animation_default = animationModule;
|
|
1790
|
+
|
|
1791
|
+
// src/modules/ota.ts
|
|
1792
|
+
var otaModule = {
|
|
1793
|
+
id: "ota",
|
|
1794
|
+
name: "OTA \u66F4\u65B0",
|
|
1795
|
+
description: "expo-updates",
|
|
1796
|
+
defaultChecked: false,
|
|
1797
|
+
dependencies: {
|
|
1798
|
+
"expo-updates": "~7.0.0"
|
|
1799
|
+
},
|
|
1800
|
+
devDependencies: {},
|
|
1801
|
+
files: [
|
|
1802
|
+
{
|
|
1803
|
+
path: "src/modules/ota/useOTAUpdate.ts",
|
|
1804
|
+
content: lines(
|
|
1805
|
+
'import { useCallback, useEffect, useState } from "react";',
|
|
1806
|
+
'import * as Updates from "expo-updates";',
|
|
1807
|
+
'import { Alert, Platform } from "react-native";',
|
|
1808
|
+
"",
|
|
1809
|
+
"interface OTAUpdateState {",
|
|
1810
|
+
" isUpdateAvailable: boolean;",
|
|
1811
|
+
" isDownloading: boolean;",
|
|
1812
|
+
" progress: number;",
|
|
1813
|
+
" error: string | null;",
|
|
1814
|
+
"}",
|
|
1815
|
+
"",
|
|
1816
|
+
"export function useOTAUpdate() {",
|
|
1817
|
+
" const [state, setState] = useState<OTAUpdateState>({",
|
|
1818
|
+
" isUpdateAvailable: false,",
|
|
1819
|
+
" isDownloading: false,",
|
|
1820
|
+
" progress: 0,",
|
|
1821
|
+
" error: null,",
|
|
1822
|
+
" });",
|
|
1823
|
+
"",
|
|
1824
|
+
" const checkForUpdate = useCallback(async () => {",
|
|
1825
|
+
' if (__DEV__ || Platform.OS === "web") {',
|
|
1826
|
+
" return;",
|
|
1827
|
+
" }",
|
|
1828
|
+
"",
|
|
1829
|
+
" try {",
|
|
1830
|
+
" const update = await Updates.checkForUpdateAsync();",
|
|
1831
|
+
" setState((prev) => ({",
|
|
1832
|
+
" ...prev,",
|
|
1833
|
+
" isUpdateAvailable: update.isAvailable,",
|
|
1834
|
+
" error: null,",
|
|
1835
|
+
" }));",
|
|
1836
|
+
" return update.isAvailable;",
|
|
1837
|
+
" } catch (error) {",
|
|
1838
|
+
' const message = error instanceof Error ? error.message : "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25";',
|
|
1839
|
+
" setState((prev) => ({ ...prev, error: message }));",
|
|
1840
|
+
" return false;",
|
|
1841
|
+
" }",
|
|
1842
|
+
" }, []);",
|
|
1843
|
+
"",
|
|
1844
|
+
" const downloadUpdate = useCallback(async () => {",
|
|
1845
|
+
' if (__DEV__ || Platform.OS === "web") return null;',
|
|
1846
|
+
"",
|
|
1847
|
+
" try {",
|
|
1848
|
+
" setState((prev) => ({ ...prev, isDownloading: true, progress: 0 }));",
|
|
1849
|
+
" const result = await Updates.fetchUpdateAsync();",
|
|
1850
|
+
" setState((prev) => ({",
|
|
1851
|
+
" ...prev,",
|
|
1852
|
+
" isDownloading: false,",
|
|
1853
|
+
" isUpdateAvailable: false,",
|
|
1854
|
+
" progress: 1,",
|
|
1855
|
+
" }));",
|
|
1856
|
+
" return result;",
|
|
1857
|
+
" } catch (error) {",
|
|
1858
|
+
' const message = error instanceof Error ? error.message : "\u4E0B\u8F7D\u66F4\u65B0\u5931\u8D25";',
|
|
1859
|
+
" setState((prev) => ({",
|
|
1860
|
+
" ...prev,",
|
|
1861
|
+
" isDownloading: false,",
|
|
1862
|
+
" error: message,",
|
|
1863
|
+
" }));",
|
|
1864
|
+
" return null;",
|
|
1865
|
+
" }",
|
|
1866
|
+
" }, []);",
|
|
1867
|
+
"",
|
|
1868
|
+
" const downloadAndRestart = useCallback(async () => {",
|
|
1869
|
+
" const result = await downloadUpdate();",
|
|
1870
|
+
" if (result?.isNew) {",
|
|
1871
|
+
" Alert.alert(",
|
|
1872
|
+
' "\u66F4\u65B0\u5DF2\u4E0B\u8F7D",',
|
|
1873
|
+
' "\u5E94\u7528\u9700\u8981\u91CD\u542F\u4EE5\u5B8C\u6210\u66F4\u65B0\uFF0C\u662F\u5426\u7ACB\u5373\u91CD\u542F\uFF1F",',
|
|
1874
|
+
" [",
|
|
1875
|
+
' { text: "\u7A0D\u540E", style: "cancel" },',
|
|
1876
|
+
" {",
|
|
1877
|
+
' text: "\u7ACB\u5373\u91CD\u542F",',
|
|
1878
|
+
' style: "default",',
|
|
1879
|
+
" onPress: async () => {",
|
|
1880
|
+
" await Updates.reloadAsync();",
|
|
1881
|
+
" },",
|
|
1882
|
+
" },",
|
|
1883
|
+
" ]",
|
|
1884
|
+
" );",
|
|
1885
|
+
" }",
|
|
1886
|
+
" }, [downloadUpdate]);",
|
|
1887
|
+
"",
|
|
1888
|
+
" const restartApp = useCallback(async () => {",
|
|
1889
|
+
" await Updates.reloadAsync();",
|
|
1890
|
+
" }, []);",
|
|
1891
|
+
"",
|
|
1892
|
+
" useEffect(() => {",
|
|
1893
|
+
" checkForUpdate();",
|
|
1894
|
+
" }, [checkForUpdate]);",
|
|
1895
|
+
"",
|
|
1896
|
+
" return {",
|
|
1897
|
+
" ...state,",
|
|
1898
|
+
" checkForUpdate,",
|
|
1899
|
+
" downloadUpdate,",
|
|
1900
|
+
" downloadAndRestart,",
|
|
1901
|
+
" restartApp,",
|
|
1902
|
+
" };",
|
|
1903
|
+
"}"
|
|
1904
|
+
)
|
|
1905
|
+
}
|
|
1906
|
+
]
|
|
1907
|
+
};
|
|
1908
|
+
var ota_default = otaModule;
|
|
1909
|
+
|
|
1910
|
+
// src/modules/notification.ts
|
|
1911
|
+
var notificationModule = {
|
|
1912
|
+
id: "notification",
|
|
1913
|
+
name: "\u5E94\u7528\u5185\u901A\u77E5",
|
|
1914
|
+
description: "expo-notifications + \u81EA\u5B9A\u4E49\u7EC4\u4EF6",
|
|
1915
|
+
defaultChecked: false,
|
|
1916
|
+
dependencies: {
|
|
1917
|
+
"expo-notifications": "~0.30.0"
|
|
1918
|
+
},
|
|
1919
|
+
devDependencies: {},
|
|
1920
|
+
files: [
|
|
1921
|
+
{
|
|
1922
|
+
path: "src/modules/notification/index.ts",
|
|
1923
|
+
content: lines(
|
|
1924
|
+
'export { InAppNotification, useInAppNotification } from "./InAppNotification";',
|
|
1925
|
+
'export { setupNotificationHandlers } from "./InAppNotification";'
|
|
1926
|
+
)
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
path: "src/modules/notification/InAppNotification.tsx",
|
|
1930
|
+
content: lines(
|
|
1931
|
+
"import React, {",
|
|
1932
|
+
" createContext,",
|
|
1933
|
+
" useCallback,",
|
|
1934
|
+
" useContext,",
|
|
1935
|
+
" useEffect,",
|
|
1936
|
+
" useMemo,",
|
|
1937
|
+
" useRef,",
|
|
1938
|
+
" useState,",
|
|
1939
|
+
'} from "react";',
|
|
1940
|
+
'import { Animated, StyleSheet, Text, TouchableOpacity, View } from "react-native";',
|
|
1941
|
+
'import * as Notifications from "expo-notifications";',
|
|
1942
|
+
'import type { Subscription } from "expo-notifications";',
|
|
1943
|
+
"",
|
|
1944
|
+
"export function setupNotificationHandlers(): void {",
|
|
1945
|
+
" Notifications.setNotificationHandler({",
|
|
1946
|
+
" handleNotification: async () => ({",
|
|
1947
|
+
" shouldShowAlert: true,",
|
|
1948
|
+
" shouldPlaySound: true,",
|
|
1949
|
+
" shouldSetBadge: true,",
|
|
1950
|
+
" }),",
|
|
1951
|
+
" });",
|
|
1952
|
+
"}",
|
|
1953
|
+
"",
|
|
1954
|
+
"interface NotificationData {",
|
|
1955
|
+
" id: string;",
|
|
1956
|
+
" title: string;",
|
|
1957
|
+
" message: string;",
|
|
1958
|
+
' type: "info" | "success" | "warning" | "error";',
|
|
1959
|
+
" duration?: number;",
|
|
1960
|
+
"}",
|
|
1961
|
+
"",
|
|
1962
|
+
"interface InAppNotificationContextType {",
|
|
1963
|
+
' showNotification: (data: Omit<NotificationData, "id">) => void;',
|
|
1964
|
+
"}",
|
|
1965
|
+
"",
|
|
1966
|
+
"const InAppNotificationContext = createContext<InAppNotificationContextType>({",
|
|
1967
|
+
" showNotification: () => {},",
|
|
1968
|
+
"});",
|
|
1969
|
+
"",
|
|
1970
|
+
"export function useInAppNotification(): InAppNotificationContextType {",
|
|
1971
|
+
" return useContext(InAppNotificationContext);",
|
|
1972
|
+
"}",
|
|
1973
|
+
"",
|
|
1974
|
+
"let notificationIdCounter = 0;",
|
|
1975
|
+
"",
|
|
1976
|
+
"export function InAppNotificationProvider({",
|
|
1977
|
+
" children,",
|
|
1978
|
+
"}: {",
|
|
1979
|
+
" children: React.ReactNode;",
|
|
1980
|
+
"}) {",
|
|
1981
|
+
" const [notifications, setNotifications] = useState<NotificationData[]>([]);",
|
|
1982
|
+
" const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());",
|
|
1983
|
+
"",
|
|
1984
|
+
" const removeNotification = useCallback((id: string) => {",
|
|
1985
|
+
" setNotifications((prev) => prev.filter((n) => n.id !== id));",
|
|
1986
|
+
" const timer = timersRef.current.get(id);",
|
|
1987
|
+
" if (timer) {",
|
|
1988
|
+
" clearTimeout(timer);",
|
|
1989
|
+
" timersRef.current.delete(id);",
|
|
1990
|
+
" }",
|
|
1991
|
+
" }, []);",
|
|
1992
|
+
"",
|
|
1993
|
+
" const showNotification = useCallback(",
|
|
1994
|
+
' (data: Omit<NotificationData, "id">) => {',
|
|
1995
|
+
" const id = `notification-${++notificationIdCounter}`;",
|
|
1996
|
+
" const notification: NotificationData = { ...data, id };",
|
|
1997
|
+
" setNotifications((prev) => [...prev, notification]);",
|
|
1998
|
+
"",
|
|
1999
|
+
" const duration = data.duration ?? 3000;",
|
|
2000
|
+
" const timer = setTimeout(() => {",
|
|
2001
|
+
" removeNotification(id);",
|
|
2002
|
+
" }, duration);",
|
|
2003
|
+
" timersRef.current.set(id, timer);",
|
|
2004
|
+
" },",
|
|
2005
|
+
" [removeNotification]",
|
|
2006
|
+
" );",
|
|
2007
|
+
"",
|
|
2008
|
+
" const contextValue = useMemo(",
|
|
2009
|
+
" () => ({ showNotification }),",
|
|
2010
|
+
" [showNotification]",
|
|
2011
|
+
" );",
|
|
2012
|
+
"",
|
|
2013
|
+
" return (",
|
|
2014
|
+
" <InAppNotificationContext.Provider value={contextValue}>",
|
|
2015
|
+
" {children}",
|
|
2016
|
+
' <View style={styles.container} pointerEvents="box-none">',
|
|
2017
|
+
" {notifications.map((notification) => (",
|
|
2018
|
+
" <NotificationCard",
|
|
2019
|
+
" key={notification.id}",
|
|
2020
|
+
" notification={notification}",
|
|
2021
|
+
" onDismiss={() => removeNotification(notification.id)}",
|
|
2022
|
+
" />",
|
|
2023
|
+
" ))}",
|
|
2024
|
+
" </View>",
|
|
2025
|
+
" </InAppNotificationContext.Provider>",
|
|
2026
|
+
" );",
|
|
2027
|
+
"}",
|
|
2028
|
+
"",
|
|
2029
|
+
"interface NotificationCardProps {",
|
|
2030
|
+
" notification: NotificationData;",
|
|
2031
|
+
" onDismiss: () => void;",
|
|
2032
|
+
"}",
|
|
2033
|
+
"",
|
|
2034
|
+
'const typeColors: Record<NotificationData["type"], string> = {',
|
|
2035
|
+
' info: "#007AFF",',
|
|
2036
|
+
' success: "#34C759",',
|
|
2037
|
+
' warning: "#FF9500",',
|
|
2038
|
+
' error: "#FF3B30",',
|
|
2039
|
+
"};",
|
|
2040
|
+
"",
|
|
2041
|
+
"function NotificationCard({ notification, onDismiss }: NotificationCardProps) {",
|
|
2042
|
+
" const opacity = useRef(new Animated.Value(0)).current;",
|
|
2043
|
+
"",
|
|
2044
|
+
" useEffect(() => {",
|
|
2045
|
+
" Animated.timing(opacity, {",
|
|
2046
|
+
" toValue: 1,",
|
|
2047
|
+
" duration: 300,",
|
|
2048
|
+
" useNativeDriver: true,",
|
|
2049
|
+
" }).start();",
|
|
2050
|
+
" }, [opacity]);",
|
|
2051
|
+
"",
|
|
2052
|
+
" const handleDismiss = useCallback(() => {",
|
|
2053
|
+
" Animated.timing(opacity, {",
|
|
2054
|
+
" toValue: 0,",
|
|
2055
|
+
" duration: 200,",
|
|
2056
|
+
" useNativeDriver: true,",
|
|
2057
|
+
" }).start(() => onDismiss());",
|
|
2058
|
+
" }, [opacity, onDismiss]);",
|
|
2059
|
+
"",
|
|
2060
|
+
" return (",
|
|
2061
|
+
" <Animated.View style={[styles.card, { opacity }]}>",
|
|
2062
|
+
" <View",
|
|
2063
|
+
" style={[styles.accent, { backgroundColor: typeColors[notification.type] }]}",
|
|
2064
|
+
" />",
|
|
2065
|
+
" <View style={styles.content}>",
|
|
2066
|
+
" <Text style={styles.title}>{notification.title}</Text>",
|
|
2067
|
+
" <Text style={styles.message}>{notification.message}</Text>",
|
|
2068
|
+
" </View>",
|
|
2069
|
+
" <TouchableOpacity onPress={handleDismiss} style={styles.closeButton}>",
|
|
2070
|
+
" <Text style={styles.closeButtonText}>X</Text>",
|
|
2071
|
+
" </TouchableOpacity>",
|
|
2072
|
+
" </Animated.View>",
|
|
2073
|
+
" );",
|
|
2074
|
+
"}",
|
|
2075
|
+
"",
|
|
2076
|
+
"const styles = StyleSheet.create({",
|
|
2077
|
+
" container: {",
|
|
2078
|
+
' position: "absolute",',
|
|
2079
|
+
" top: 60,",
|
|
2080
|
+
" left: 16,",
|
|
2081
|
+
" right: 16,",
|
|
2082
|
+
" zIndex: 9999,",
|
|
2083
|
+
" gap: 8,",
|
|
2084
|
+
" },",
|
|
2085
|
+
" card: {",
|
|
2086
|
+
' flexDirection: "row",',
|
|
2087
|
+
' alignItems: "center",',
|
|
2088
|
+
' backgroundColor: "#fff",',
|
|
2089
|
+
" borderRadius: 12,",
|
|
2090
|
+
' shadowColor: "#000",',
|
|
2091
|
+
" shadowOffset: { width: 0, height: 2 },",
|
|
2092
|
+
" shadowOpacity: 0.15,",
|
|
2093
|
+
" shadowRadius: 8,",
|
|
2094
|
+
" elevation: 5,",
|
|
2095
|
+
' overflow: "hidden",',
|
|
2096
|
+
" },",
|
|
2097
|
+
" accent: {",
|
|
2098
|
+
" width: 4,",
|
|
2099
|
+
' height: "100%",',
|
|
2100
|
+
" minHeight: 48,",
|
|
2101
|
+
" },",
|
|
2102
|
+
" content: {",
|
|
2103
|
+
" flex: 1,",
|
|
2104
|
+
" paddingVertical: 12,",
|
|
2105
|
+
" paddingHorizontal: 12,",
|
|
2106
|
+
" },",
|
|
2107
|
+
" title: {",
|
|
2108
|
+
" fontSize: 14,",
|
|
2109
|
+
' fontWeight: "600",',
|
|
2110
|
+
' color: "#1a1a1a",',
|
|
2111
|
+
" },",
|
|
2112
|
+
" message: {",
|
|
2113
|
+
" fontSize: 13,",
|
|
2114
|
+
' color: "#666",',
|
|
2115
|
+
" marginTop: 2,",
|
|
2116
|
+
" },",
|
|
2117
|
+
" closeButton: {",
|
|
2118
|
+
" padding: 12,",
|
|
2119
|
+
" },",
|
|
2120
|
+
" closeButtonText: {",
|
|
2121
|
+
" fontSize: 14,",
|
|
2122
|
+
' color: "#999",',
|
|
2123
|
+
" },",
|
|
2124
|
+
"});",
|
|
2125
|
+
"",
|
|
2126
|
+
"export const InAppNotification = InAppNotificationProvider;"
|
|
2127
|
+
)
|
|
2128
|
+
}
|
|
2129
|
+
],
|
|
2130
|
+
layoutProviders: ["<InAppNotification>"],
|
|
2131
|
+
layoutImports: [
|
|
2132
|
+
'import { InAppNotification } from "../modules/notification/InAppNotification";'
|
|
2133
|
+
]
|
|
2134
|
+
};
|
|
2135
|
+
var notification_default = notificationModule;
|
|
2136
|
+
|
|
2137
|
+
// src/modules/permission.ts
|
|
2138
|
+
var permissionModule = {
|
|
2139
|
+
id: "permission",
|
|
2140
|
+
name: "\u7528\u6237\u6743\u9650\u7BA1\u7406",
|
|
2141
|
+
description: "\u6743\u9650\u8BF7\u6C42/\u68C0\u67E5\u5C01\u88C5",
|
|
2142
|
+
defaultChecked: false,
|
|
2143
|
+
dependencies: {
|
|
2144
|
+
"expo-image-picker": "~16.0.0",
|
|
2145
|
+
"expo-camera": "~16.0.0",
|
|
2146
|
+
"expo-location": "~18.0.0"
|
|
2147
|
+
},
|
|
2148
|
+
devDependencies: {},
|
|
2149
|
+
files: [
|
|
2150
|
+
{
|
|
2151
|
+
path: "src/modules/permission/index.ts",
|
|
2152
|
+
content: lines(
|
|
2153
|
+
'import * as ImagePicker from "expo-image-picker";',
|
|
2154
|
+
'import * as Camera from "expo-camera";',
|
|
2155
|
+
'import * as Location from "expo-location";',
|
|
2156
|
+
'import { Alert, Platform } from "react-native";',
|
|
2157
|
+
"",
|
|
2158
|
+
'export type PermissionStatus = "granted" | "denied" | "undetermined";',
|
|
2159
|
+
"",
|
|
2160
|
+
"export interface PermissionResult {",
|
|
2161
|
+
" status: PermissionStatus;",
|
|
2162
|
+
" canAskAgain: boolean;",
|
|
2163
|
+
"}",
|
|
2164
|
+
"",
|
|
2165
|
+
"export async function checkCameraPermission(): Promise<PermissionResult> {",
|
|
2166
|
+
" const { status, canAskAgain } = await Camera.getCameraPermissionsAsync();",
|
|
2167
|
+
" return { status: status as PermissionStatus, canAskAgain };",
|
|
2168
|
+
"}",
|
|
2169
|
+
"",
|
|
2170
|
+
"export async function requestCameraPermission(): Promise<PermissionResult> {",
|
|
2171
|
+
" const { status, canAskAgain } = await Camera.requestCameraPermissionsAsync();",
|
|
2172
|
+
" return { status: status as PermissionStatus, canAskAgain };",
|
|
2173
|
+
"}",
|
|
2174
|
+
"",
|
|
2175
|
+
"export async function checkPhotoLibraryPermission(): Promise<PermissionResult> {",
|
|
2176
|
+
" const { status, canAskAgain } =",
|
|
2177
|
+
" await ImagePicker.getMediaLibraryPermissionsAsync();",
|
|
2178
|
+
" return { status: status as PermissionStatus, canAskAgain };",
|
|
2179
|
+
"}",
|
|
2180
|
+
"",
|
|
2181
|
+
"export async function requestPhotoLibraryPermission(): Promise<PermissionResult> {",
|
|
2182
|
+
" const { status, canAskAgain } =",
|
|
2183
|
+
" await ImagePicker.requestMediaLibraryPermissionsAsync();",
|
|
2184
|
+
" return { status: status as PermissionStatus, canAskAgain };",
|
|
2185
|
+
"}",
|
|
2186
|
+
"",
|
|
2187
|
+
"export async function checkLocationPermission(): Promise<PermissionResult> {",
|
|
2188
|
+
" const { status, canAskAgain } = await Location.getForegroundPermissionsAsync();",
|
|
2189
|
+
" return { status: status as PermissionStatus, canAskAgain };",
|
|
2190
|
+
"}",
|
|
2191
|
+
"",
|
|
2192
|
+
"export async function requestLocationPermission(): Promise<PermissionResult> {",
|
|
2193
|
+
" const { status, canAskAgain } =",
|
|
2194
|
+
" await Location.requestForegroundPermissionsAsync();",
|
|
2195
|
+
" return { status: status as PermissionStatus, canAskAgain };",
|
|
2196
|
+
"}",
|
|
2197
|
+
"",
|
|
2198
|
+
"export async function requestBackgroundLocationPermission(): Promise<PermissionResult> {",
|
|
2199
|
+
" const { status, canAskAgain } =",
|
|
2200
|
+
" await Location.requestBackgroundPermissionsAsync();",
|
|
2201
|
+
" return { status: status as PermissionStatus, canAskAgain };",
|
|
2202
|
+
"}",
|
|
2203
|
+
"",
|
|
2204
|
+
"export async function requestPermissionWithAlert(",
|
|
2205
|
+
" permissionName: string,",
|
|
2206
|
+
" requestFn: () => Promise<PermissionResult>,",
|
|
2207
|
+
" onGranted?: () => void,",
|
|
2208
|
+
" onDenied?: () => void",
|
|
2209
|
+
"): Promise<PermissionResult> {",
|
|
2210
|
+
" const result = await requestFn();",
|
|
2211
|
+
"",
|
|
2212
|
+
' if (result.status === "granted") {',
|
|
2213
|
+
" onGranted?.();",
|
|
2214
|
+
" } else {",
|
|
2215
|
+
" const message = result.canAskAgain",
|
|
2216
|
+
" ? `\u60A8\u9700\u8981\u6388\u4E88${permissionName}\u6743\u9650\u624D\u80FD\u4F7F\u7528\u6B64\u529F\u80FD`",
|
|
2217
|
+
" : `${permissionName}\u6743\u9650\u5DF2\u88AB\u62D2\u7EDD\uFF0C\u8BF7\u5728\u7CFB\u7EDF\u8BBE\u7F6E\u4E2D\u624B\u52A8\u5F00\u542F`;",
|
|
2218
|
+
"",
|
|
2219
|
+
' Alert.alert("\u9700\u8981\u6743\u9650", message, [',
|
|
2220
|
+
' { text: "\u53D6\u6D88", style: "cancel", onPress: onDenied },',
|
|
2221
|
+
" ...(result.canAskAgain",
|
|
2222
|
+
" ? [",
|
|
2223
|
+
" {",
|
|
2224
|
+
' text: "\u518D\u6B21\u8BF7\u6C42",',
|
|
2225
|
+
' style: "default" as const,',
|
|
2226
|
+
" onPress: () => requestFn(),",
|
|
2227
|
+
" },",
|
|
2228
|
+
" ]",
|
|
2229
|
+
" : []),",
|
|
2230
|
+
" ]);",
|
|
2231
|
+
" }",
|
|
2232
|
+
"",
|
|
2233
|
+
" return result;",
|
|
2234
|
+
"}",
|
|
2235
|
+
"",
|
|
2236
|
+
"export async function pickImageWithPermission(): Promise<ImagePicker.ImagePickerResult | null> {",
|
|
2237
|
+
" const permission = await requestPhotoLibraryPermission();",
|
|
2238
|
+
' if (permission.status !== "granted") {',
|
|
2239
|
+
' Alert.alert("\u9700\u8981\u6743\u9650", "\u8BF7\u6388\u4E88\u76F8\u518C\u8BBF\u95EE\u6743\u9650");',
|
|
2240
|
+
" return null;",
|
|
2241
|
+
" }",
|
|
2242
|
+
"",
|
|
2243
|
+
" const result = await ImagePicker.launchImageLibraryAsync({",
|
|
2244
|
+
" mediaTypes: ImagePicker.MediaTypeOptions.Images,",
|
|
2245
|
+
" allowsEditing: true,",
|
|
2246
|
+
" aspect: [1, 1],",
|
|
2247
|
+
" quality: 0.8,",
|
|
2248
|
+
" });",
|
|
2249
|
+
"",
|
|
2250
|
+
" return result;",
|
|
2251
|
+
"}",
|
|
2252
|
+
"",
|
|
2253
|
+
"export async function takePhotoWithPermission(): Promise<ImagePicker.ImagePickerResult | null> {",
|
|
2254
|
+
" const permission = await requestCameraPermission();",
|
|
2255
|
+
' if (permission.status !== "granted") {',
|
|
2256
|
+
' Alert.alert("\u9700\u8981\u6743\u9650", "\u8BF7\u6388\u4E88\u76F8\u673A\u8BBF\u95EE\u6743\u9650");',
|
|
2257
|
+
" return null;",
|
|
2258
|
+
" }",
|
|
2259
|
+
"",
|
|
2260
|
+
" const result = await ImagePicker.launchCameraAsync({",
|
|
2261
|
+
" allowsEditing: true,",
|
|
2262
|
+
" aspect: [1, 1],",
|
|
2263
|
+
" quality: 0.8,",
|
|
2264
|
+
" });",
|
|
2265
|
+
"",
|
|
2266
|
+
" return result;",
|
|
2267
|
+
"}"
|
|
2268
|
+
)
|
|
2269
|
+
}
|
|
2270
|
+
],
|
|
2271
|
+
appConfig: {
|
|
2272
|
+
plugins: [
|
|
2273
|
+
[
|
|
2274
|
+
"expo-camera",
|
|
2275
|
+
{
|
|
2276
|
+
cameraPermission: "Allow $(PRODUCT_NAME) to access your camera"
|
|
2277
|
+
}
|
|
2278
|
+
],
|
|
2279
|
+
[
|
|
2280
|
+
"expo-image-picker",
|
|
2281
|
+
{
|
|
2282
|
+
photosPermission: "Allow $(PRODUCT_NAME) to access your photos"
|
|
2283
|
+
}
|
|
2284
|
+
],
|
|
2285
|
+
[
|
|
2286
|
+
"expo-location",
|
|
2287
|
+
{
|
|
2288
|
+
locationAlwaysAndWhenInUsePermission: "Allow $(PRODUCT_NAME) to use your location"
|
|
2289
|
+
}
|
|
2290
|
+
]
|
|
2291
|
+
]
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
var permission_default = permissionModule;
|
|
2295
|
+
|
|
2296
|
+
// src/modules/bottom-sheet.ts
|
|
2297
|
+
var bottomSheetModule = {
|
|
2298
|
+
id: "bottom-sheet",
|
|
2299
|
+
name: "Bottom Sheet",
|
|
2300
|
+
description: "@gorhom/bottom-sheet",
|
|
2301
|
+
defaultChecked: false,
|
|
2302
|
+
dependencies: {
|
|
2303
|
+
"@gorhom/bottom-sheet": "^5.1.0"
|
|
2304
|
+
},
|
|
2305
|
+
devDependencies: {},
|
|
2306
|
+
files: [
|
|
2307
|
+
{
|
|
2308
|
+
path: "src/components/AppBottomSheet.tsx",
|
|
2309
|
+
content: lines(
|
|
2310
|
+
'import React, { useCallback, useRef } from "react";',
|
|
2311
|
+
'import { View, Text, StyleSheet } from "react-native";',
|
|
2312
|
+
"import BottomSheet, {",
|
|
2313
|
+
" BottomSheetBackdrop,",
|
|
2314
|
+
" BottomSheetView,",
|
|
2315
|
+
" type BottomSheetBackdropProps,",
|
|
2316
|
+
" type BottomSheetProps,",
|
|
2317
|
+
'} from "@gorhom/bottom-sheet";',
|
|
2318
|
+
"",
|
|
2319
|
+
'interface AppBottomSheetProps extends Omit<BottomSheetProps, "children"> {',
|
|
2320
|
+
" title?: string;",
|
|
2321
|
+
" children: React.ReactNode;",
|
|
2322
|
+
" onClose?: () => void;",
|
|
2323
|
+
"}",
|
|
2324
|
+
"",
|
|
2325
|
+
"export const AppBottomSheet = React.forwardRef<BottomSheet, AppBottomSheetProps>(",
|
|
2326
|
+
' ({ title, children, onClose, snapPoints = ["50%"], ...rest }, ref) => {',
|
|
2327
|
+
" const localRef = useRef<BottomSheet>(null);",
|
|
2328
|
+
" const sheetRef = (ref as React.RefObject<BottomSheet>) || localRef;",
|
|
2329
|
+
"",
|
|
2330
|
+
" const renderBackdrop = useCallback(",
|
|
2331
|
+
" (props: BottomSheetBackdropProps) => (",
|
|
2332
|
+
" <BottomSheetBackdrop",
|
|
2333
|
+
" {...props}",
|
|
2334
|
+
" disappearsOnIndex={-1}",
|
|
2335
|
+
" appearsOnIndex={0}",
|
|
2336
|
+
' pressBehavior="close"',
|
|
2337
|
+
" />",
|
|
2338
|
+
" ),",
|
|
2339
|
+
" []",
|
|
2340
|
+
" );",
|
|
2341
|
+
"",
|
|
2342
|
+
" const handleSheetChange = useCallback(",
|
|
2343
|
+
" (index: number) => {",
|
|
2344
|
+
" if (index === -1 && onClose) {",
|
|
2345
|
+
" onClose();",
|
|
2346
|
+
" }",
|
|
2347
|
+
" },",
|
|
2348
|
+
" [onClose]",
|
|
2349
|
+
" );",
|
|
2350
|
+
"",
|
|
2351
|
+
" return (",
|
|
2352
|
+
" <BottomSheet",
|
|
2353
|
+
" ref={sheetRef}",
|
|
2354
|
+
" index={-1}",
|
|
2355
|
+
" snapPoints={snapPoints}",
|
|
2356
|
+
" backdropComponent={renderBackdrop}",
|
|
2357
|
+
" onChange={handleSheetChange}",
|
|
2358
|
+
" enablePanDownToClose",
|
|
2359
|
+
" backgroundStyle={styles.background}",
|
|
2360
|
+
" handleIndicatorStyle={styles.handleIndicator}",
|
|
2361
|
+
" {...rest}",
|
|
2362
|
+
" >",
|
|
2363
|
+
" <BottomSheetView style={styles.content}>",
|
|
2364
|
+
" {title && <Text style={styles.title}>{title}</Text>}",
|
|
2365
|
+
" {children}",
|
|
2366
|
+
" </BottomSheetView>",
|
|
2367
|
+
" </BottomSheet>",
|
|
2368
|
+
" );",
|
|
2369
|
+
" }",
|
|
2370
|
+
");",
|
|
2371
|
+
"",
|
|
2372
|
+
'AppBottomSheet.displayName = "AppBottomSheet";',
|
|
2373
|
+
"",
|
|
2374
|
+
"const styles = StyleSheet.create({",
|
|
2375
|
+
" background: {",
|
|
2376
|
+
" borderRadius: 16,",
|
|
2377
|
+
" },",
|
|
2378
|
+
" handleIndicator: {",
|
|
2379
|
+
' backgroundColor: "#d1d5db",',
|
|
2380
|
+
" width: 40,",
|
|
2381
|
+
" },",
|
|
2382
|
+
" content: {",
|
|
2383
|
+
" paddingHorizontal: 16,",
|
|
2384
|
+
" paddingBottom: 24,",
|
|
2385
|
+
" },",
|
|
2386
|
+
" title: {",
|
|
2387
|
+
" fontSize: 18,",
|
|
2388
|
+
' fontWeight: "600",',
|
|
2389
|
+
' color: "#1a1a1a",',
|
|
2390
|
+
" marginBottom: 16,",
|
|
2391
|
+
" },",
|
|
2392
|
+
"});",
|
|
2393
|
+
"",
|
|
2394
|
+
"export default AppBottomSheet;"
|
|
2395
|
+
)
|
|
2396
|
+
}
|
|
2397
|
+
],
|
|
2398
|
+
layoutProviders: ["<BottomSheetModalProvider>"],
|
|
2399
|
+
layoutImports: [
|
|
2400
|
+
'import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";'
|
|
2401
|
+
]
|
|
2402
|
+
};
|
|
2403
|
+
var bottom_sheet_default = bottomSheetModule;
|
|
2404
|
+
|
|
2405
|
+
// src/modules/flashlist.ts
|
|
2406
|
+
var flashlistModule = {
|
|
2407
|
+
id: "flashlist",
|
|
2408
|
+
name: "FlashList",
|
|
2409
|
+
description: "@shopify/flash-list",
|
|
2410
|
+
defaultChecked: false,
|
|
2411
|
+
dependencies: {
|
|
2412
|
+
"@shopify/flash-list": "^1.7.0"
|
|
2413
|
+
},
|
|
2414
|
+
devDependencies: {},
|
|
2415
|
+
files: [
|
|
2416
|
+
{
|
|
2417
|
+
path: "src/components/AppFlashList.tsx",
|
|
2418
|
+
content: lines(
|
|
2419
|
+
'import React, { useCallback } from "react";',
|
|
2420
|
+
'import { View, Text, StyleSheet, type ListRenderItemInfo } from "react-native";',
|
|
2421
|
+
"import {",
|
|
2422
|
+
" FlashList,",
|
|
2423
|
+
" type FlashListProps,",
|
|
2424
|
+
'} from "@shopify/flash-list";',
|
|
2425
|
+
"",
|
|
2426
|
+
"export interface ListItem {",
|
|
2427
|
+
" id: string;",
|
|
2428
|
+
" [key: string]: unknown;",
|
|
2429
|
+
"}",
|
|
2430
|
+
"",
|
|
2431
|
+
"interface AppFlashListProps<T extends ListItem>",
|
|
2432
|
+
' extends Omit<FlashListProps<T>, "renderItem" | "estimatedItemSize"> {',
|
|
2433
|
+
" renderItem: (info: ListRenderItemInfo<T>) => React.ReactElement | null;",
|
|
2434
|
+
" estimatedItemSize?: number;",
|
|
2435
|
+
" ListEmptyComponent?: React.ReactElement;",
|
|
2436
|
+
" isLoading?: boolean;",
|
|
2437
|
+
" LoadingComponent?: React.ReactElement;",
|
|
2438
|
+
"}",
|
|
2439
|
+
"",
|
|
2440
|
+
"export function AppFlashList<T extends ListItem>({",
|
|
2441
|
+
" data,",
|
|
2442
|
+
" renderItem,",
|
|
2443
|
+
" estimatedItemSize = 50,",
|
|
2444
|
+
" ListEmptyComponent,",
|
|
2445
|
+
" isLoading,",
|
|
2446
|
+
" LoadingComponent,",
|
|
2447
|
+
" ...rest",
|
|
2448
|
+
"}: AppFlashListProps<T>) {",
|
|
2449
|
+
" const defaultEmptyComponent = React.useMemo(",
|
|
2450
|
+
" () => (",
|
|
2451
|
+
" <View style={styles.emptyContainer}>",
|
|
2452
|
+
" <Text style={styles.emptyText}>\u6682\u65E0\u6570\u636E</Text>",
|
|
2453
|
+
" </View>",
|
|
2454
|
+
" ),",
|
|
2455
|
+
" []",
|
|
2456
|
+
" );",
|
|
2457
|
+
"",
|
|
2458
|
+
" const defaultLoadingComponent = React.useMemo(",
|
|
2459
|
+
" () => (",
|
|
2460
|
+
" <View style={styles.loadingContainer}>",
|
|
2461
|
+
" <Text style={styles.loadingText}>\u52A0\u8F7D\u4E2D...</Text>",
|
|
2462
|
+
" </View>",
|
|
2463
|
+
" ),",
|
|
2464
|
+
" []",
|
|
2465
|
+
" );",
|
|
2466
|
+
"",
|
|
2467
|
+
" if (isLoading) {",
|
|
2468
|
+
" return LoadingComponent ?? defaultLoadingComponent;",
|
|
2469
|
+
" }",
|
|
2470
|
+
"",
|
|
2471
|
+
" return (",
|
|
2472
|
+
" <FlashList",
|
|
2473
|
+
" data={data}",
|
|
2474
|
+
" renderItem={renderItem}",
|
|
2475
|
+
" estimatedItemSize={estimatedItemSize}",
|
|
2476
|
+
" ListEmptyComponent={ListEmptyComponent ?? defaultEmptyComponent}",
|
|
2477
|
+
" drawDistance={200}",
|
|
2478
|
+
" {...rest}",
|
|
2479
|
+
" />",
|
|
2480
|
+
" );",
|
|
2481
|
+
"}",
|
|
2482
|
+
"",
|
|
2483
|
+
"const styles = StyleSheet.create({",
|
|
2484
|
+
" emptyContainer: {",
|
|
2485
|
+
" flex: 1,",
|
|
2486
|
+
' justifyContent: "center",',
|
|
2487
|
+
' alignItems: "center",',
|
|
2488
|
+
" paddingVertical: 40,",
|
|
2489
|
+
" },",
|
|
2490
|
+
" emptyText: {",
|
|
2491
|
+
" fontSize: 16,",
|
|
2492
|
+
' color: "#999",',
|
|
2493
|
+
" },",
|
|
2494
|
+
" loadingContainer: {",
|
|
2495
|
+
" flex: 1,",
|
|
2496
|
+
' justifyContent: "center",',
|
|
2497
|
+
' alignItems: "center",',
|
|
2498
|
+
" paddingVertical: 40,",
|
|
2499
|
+
" },",
|
|
2500
|
+
" loadingText: {",
|
|
2501
|
+
" fontSize: 16,",
|
|
2502
|
+
' color: "#999",',
|
|
2503
|
+
" },",
|
|
2504
|
+
"});",
|
|
2505
|
+
"",
|
|
2506
|
+
"export default AppFlashList;"
|
|
2507
|
+
)
|
|
2508
|
+
}
|
|
2509
|
+
]
|
|
2510
|
+
};
|
|
2511
|
+
var flashlist_default = flashlistModule;
|
|
2512
|
+
|
|
2513
|
+
// src/modules/ui-reusables.ts
|
|
2514
|
+
var uiReusablesModule = {
|
|
2515
|
+
id: "ui-reusables",
|
|
2516
|
+
name: "reactnative.reusables UI",
|
|
2517
|
+
description: "\u9884\u7F6E UI \u7EC4\u4EF6",
|
|
2518
|
+
defaultChecked: false,
|
|
2519
|
+
dependencies: {
|
|
2520
|
+
"reactnative.reusables": "^0.1.0",
|
|
2521
|
+
"react-native-svg": "^15.8.0",
|
|
2522
|
+
"@rn-primitives/slot": "^1.1.0",
|
|
2523
|
+
"@rn-primitives/types": "^1.1.0"
|
|
2524
|
+
},
|
|
2525
|
+
devDependencies: {},
|
|
2526
|
+
files: [
|
|
2527
|
+
{
|
|
2528
|
+
path: "src/components/ui/button.tsx",
|
|
2529
|
+
content: lines(
|
|
2530
|
+
'import React from "react";',
|
|
2531
|
+
'import { Text, Pressable, type PressableProps, type StyleProp, type ViewStyle, type TextStyle } from "react-native";',
|
|
2532
|
+
'import { Slot } from "@rn-primitives/slot";',
|
|
2533
|
+
"",
|
|
2534
|
+
'type ButtonVariant = "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";',
|
|
2535
|
+
'type ButtonSize = "default" | "sm" | "lg" | "icon";',
|
|
2536
|
+
"",
|
|
2537
|
+
"interface ButtonProps extends PressableProps {",
|
|
2538
|
+
" variant?: ButtonVariant;",
|
|
2539
|
+
" size?: ButtonSize;",
|
|
2540
|
+
" asChild?: boolean;",
|
|
2541
|
+
" style?: StyleProp<ViewStyle>;",
|
|
2542
|
+
" textStyle?: StyleProp<TextStyle>;",
|
|
2543
|
+
" children: React.ReactNode;",
|
|
2544
|
+
"}",
|
|
2545
|
+
"",
|
|
2546
|
+
"const variantStyles: Record<ButtonVariant, ViewStyle> = {",
|
|
2547
|
+
' default: { backgroundColor: "#0f172a" },',
|
|
2548
|
+
' destructive: { backgroundColor: "#ef4444" },',
|
|
2549
|
+
' outline: { backgroundColor: "transparent", borderWidth: 1, borderColor: "#d4d4d8" },',
|
|
2550
|
+
' secondary: { backgroundColor: "#f4f4f5" },',
|
|
2551
|
+
' ghost: { backgroundColor: "transparent" },',
|
|
2552
|
+
' link: { backgroundColor: "transparent" },',
|
|
2553
|
+
"};",
|
|
2554
|
+
"",
|
|
2555
|
+
"const variantTextStyles: Record<ButtonVariant, TextStyle> = {",
|
|
2556
|
+
' default: { color: "#fafafa" },',
|
|
2557
|
+
' destructive: { color: "#fafafa" },',
|
|
2558
|
+
' outline: { color: "#18181b" },',
|
|
2559
|
+
' secondary: { color: "#18181b" },',
|
|
2560
|
+
' ghost: { color: "#18181b" },',
|
|
2561
|
+
' link: { color: "#2563eb", textDecorationLine: "underline" },',
|
|
2562
|
+
"};",
|
|
2563
|
+
"",
|
|
2564
|
+
"const sizeStyles: Record<ButtonSize, ViewStyle> = {",
|
|
2565
|
+
" default: { height: 44, paddingHorizontal: 16 },",
|
|
2566
|
+
" sm: { height: 36, paddingHorizontal: 12 },",
|
|
2567
|
+
" lg: { height: 52, paddingHorizontal: 24 },",
|
|
2568
|
+
" icon: { height: 44, width: 44 },",
|
|
2569
|
+
"};",
|
|
2570
|
+
"",
|
|
2571
|
+
"const sizeTextStyles: Record<ButtonSize, TextStyle> = {",
|
|
2572
|
+
" default: { fontSize: 16 },",
|
|
2573
|
+
" sm: { fontSize: 14 },",
|
|
2574
|
+
" lg: { fontSize: 18 },",
|
|
2575
|
+
" icon: { fontSize: 16 },",
|
|
2576
|
+
"};",
|
|
2577
|
+
"",
|
|
2578
|
+
"export function Button({",
|
|
2579
|
+
' variant = "default",',
|
|
2580
|
+
' size = "default",',
|
|
2581
|
+
" asChild = false,",
|
|
2582
|
+
" style,",
|
|
2583
|
+
" textStyle,",
|
|
2584
|
+
" children,",
|
|
2585
|
+
" ...rest",
|
|
2586
|
+
"}: ButtonProps) {",
|
|
2587
|
+
" const containerStyle: StyleProp<ViewStyle> = [",
|
|
2588
|
+
" {",
|
|
2589
|
+
" borderRadius: 8,",
|
|
2590
|
+
' flexDirection: "row",',
|
|
2591
|
+
' alignItems: "center",',
|
|
2592
|
+
' justifyContent: "center",',
|
|
2593
|
+
" },",
|
|
2594
|
+
" variantStyles[variant],",
|
|
2595
|
+
" sizeStyles[size],",
|
|
2596
|
+
" style,",
|
|
2597
|
+
" ];",
|
|
2598
|
+
"",
|
|
2599
|
+
" const textStyling: StyleProp<TextStyle> = [",
|
|
2600
|
+
" {",
|
|
2601
|
+
' fontWeight: "600",',
|
|
2602
|
+
' textAlign: "center",',
|
|
2603
|
+
" },",
|
|
2604
|
+
" variantTextStyles[variant],",
|
|
2605
|
+
" sizeTextStyles[size],",
|
|
2606
|
+
" textStyle,",
|
|
2607
|
+
" ];",
|
|
2608
|
+
"",
|
|
2609
|
+
" if (asChild && React.isValidElement(children)) {",
|
|
2610
|
+
" return (",
|
|
2611
|
+
" <Pressable style={containerStyle} {...rest}>",
|
|
2612
|
+
" <Slot>{children}</Slot>",
|
|
2613
|
+
" </Pressable>",
|
|
2614
|
+
" );",
|
|
2615
|
+
" }",
|
|
2616
|
+
"",
|
|
2617
|
+
" return (",
|
|
2618
|
+
" <Pressable style={containerStyle} {...rest}>",
|
|
2619
|
+
' {typeof children === "string" ? (',
|
|
2620
|
+
" <Text style={textStyling}>{children}</Text>",
|
|
2621
|
+
" ) : (",
|
|
2622
|
+
" children",
|
|
2623
|
+
" )}",
|
|
2624
|
+
" </Pressable>",
|
|
2625
|
+
" );",
|
|
2626
|
+
"}",
|
|
2627
|
+
"",
|
|
2628
|
+
"export default Button;"
|
|
2629
|
+
)
|
|
2630
|
+
},
|
|
2631
|
+
{
|
|
2632
|
+
path: "src/components/ui/input.tsx",
|
|
2633
|
+
content: lines(
|
|
2634
|
+
'import React, { forwardRef } from "react";',
|
|
2635
|
+
"import {",
|
|
2636
|
+
" TextInput,",
|
|
2637
|
+
" type TextInputProps,",
|
|
2638
|
+
" type StyleProp,",
|
|
2639
|
+
" type ViewStyle,",
|
|
2640
|
+
" type TextStyle,",
|
|
2641
|
+
" View,",
|
|
2642
|
+
" Text,",
|
|
2643
|
+
'} from "react-native";',
|
|
2644
|
+
"",
|
|
2645
|
+
"interface InputProps extends TextInputProps {",
|
|
2646
|
+
" label?: string;",
|
|
2647
|
+
" error?: string;",
|
|
2648
|
+
" containerStyle?: StyleProp<ViewStyle>;",
|
|
2649
|
+
" inputStyle?: StyleProp<TextStyle>;",
|
|
2650
|
+
"}",
|
|
2651
|
+
"",
|
|
2652
|
+
"export const Input = forwardRef<TextInput, InputProps>(",
|
|
2653
|
+
" ({ label, error, containerStyle, inputStyle, ...rest }, ref) => {",
|
|
2654
|
+
" return (",
|
|
2655
|
+
" <View style={containerStyle}>",
|
|
2656
|
+
" {label && <Text style={styles.label}>{label}</Text>}",
|
|
2657
|
+
" <TextInput",
|
|
2658
|
+
" ref={ref}",
|
|
2659
|
+
" style={[",
|
|
2660
|
+
" styles.input,",
|
|
2661
|
+
" error && styles.inputError,",
|
|
2662
|
+
" inputStyle,",
|
|
2663
|
+
" ]}",
|
|
2664
|
+
' placeholderTextColor="#a1a1aa"',
|
|
2665
|
+
" {...rest}",
|
|
2666
|
+
" />",
|
|
2667
|
+
" {error && <Text style={styles.errorText}>{error}</Text>}",
|
|
2668
|
+
" </View>",
|
|
2669
|
+
" );",
|
|
2670
|
+
" }",
|
|
2671
|
+
");",
|
|
2672
|
+
"",
|
|
2673
|
+
'Input.displayName = "Input";',
|
|
2674
|
+
"",
|
|
2675
|
+
"const styles = {",
|
|
2676
|
+
" label: {",
|
|
2677
|
+
" fontSize: 14,",
|
|
2678
|
+
' fontWeight: "500",',
|
|
2679
|
+
' color: "#18181b",',
|
|
2680
|
+
" marginBottom: 6,",
|
|
2681
|
+
" } as TextStyle,",
|
|
2682
|
+
" input: {",
|
|
2683
|
+
" height: 44,",
|
|
2684
|
+
" borderWidth: 1,",
|
|
2685
|
+
' borderColor: "#d4d4d8",',
|
|
2686
|
+
" borderRadius: 8,",
|
|
2687
|
+
" paddingHorizontal: 12,",
|
|
2688
|
+
" fontSize: 16,",
|
|
2689
|
+
' color: "#18181b",',
|
|
2690
|
+
' backgroundColor: "#ffffff",',
|
|
2691
|
+
" } as TextStyle,",
|
|
2692
|
+
" inputError: {",
|
|
2693
|
+
' borderColor: "#ef4444",',
|
|
2694
|
+
" } as TextStyle,",
|
|
2695
|
+
" errorText: {",
|
|
2696
|
+
" fontSize: 12,",
|
|
2697
|
+
' color: "#ef4444",',
|
|
2698
|
+
" marginTop: 4,",
|
|
2699
|
+
" } as TextStyle,",
|
|
2700
|
+
"};",
|
|
2701
|
+
"",
|
|
2702
|
+
"export default Input;"
|
|
2703
|
+
)
|
|
2704
|
+
},
|
|
2705
|
+
{
|
|
2706
|
+
path: "src/components/ui/card.tsx",
|
|
2707
|
+
content: lines(
|
|
2708
|
+
'import React from "react";',
|
|
2709
|
+
'import { View, Text, type StyleProp, type ViewStyle, type TextStyle } from "react-native";',
|
|
2710
|
+
"",
|
|
2711
|
+
"interface CardProps {",
|
|
2712
|
+
" title?: string;",
|
|
2713
|
+
" description?: string;",
|
|
2714
|
+
" children: React.ReactNode;",
|
|
2715
|
+
" style?: StyleProp<ViewStyle>;",
|
|
2716
|
+
" titleStyle?: StyleProp<TextStyle>;",
|
|
2717
|
+
" descriptionStyle?: StyleProp<TextStyle>;",
|
|
2718
|
+
' padding?: "none" | "sm" | "md" | "lg";',
|
|
2719
|
+
"}",
|
|
2720
|
+
"",
|
|
2721
|
+
'const paddingMap: Record<NonNullable<CardProps["padding"]>, number> = {',
|
|
2722
|
+
" none: 0,",
|
|
2723
|
+
" sm: 8,",
|
|
2724
|
+
" md: 16,",
|
|
2725
|
+
" lg: 24,",
|
|
2726
|
+
"};",
|
|
2727
|
+
"",
|
|
2728
|
+
"export function Card({",
|
|
2729
|
+
" title,",
|
|
2730
|
+
" description,",
|
|
2731
|
+
" children,",
|
|
2732
|
+
" style,",
|
|
2733
|
+
" titleStyle,",
|
|
2734
|
+
" descriptionStyle,",
|
|
2735
|
+
' padding = "md",',
|
|
2736
|
+
"}: CardProps) {",
|
|
2737
|
+
" return (",
|
|
2738
|
+
" <View",
|
|
2739
|
+
" style={[",
|
|
2740
|
+
" {",
|
|
2741
|
+
' backgroundColor: "#ffffff",',
|
|
2742
|
+
" borderRadius: 12,",
|
|
2743
|
+
" borderWidth: 1,",
|
|
2744
|
+
' borderColor: "#e4e4e7",',
|
|
2745
|
+
" padding: paddingMap[padding],",
|
|
2746
|
+
" },",
|
|
2747
|
+
" style,",
|
|
2748
|
+
" ]}",
|
|
2749
|
+
" >",
|
|
2750
|
+
" {title && (",
|
|
2751
|
+
" <Text style={[styles.title, titleStyle]}>{title}</Text>",
|
|
2752
|
+
" )}",
|
|
2753
|
+
" {description && (",
|
|
2754
|
+
" <Text style={[styles.description, descriptionStyle]}>",
|
|
2755
|
+
" {description}",
|
|
2756
|
+
" </Text>",
|
|
2757
|
+
" )}",
|
|
2758
|
+
" {children}",
|
|
2759
|
+
" </View>",
|
|
2760
|
+
" );",
|
|
2761
|
+
"}",
|
|
2762
|
+
"",
|
|
2763
|
+
"const styles = {",
|
|
2764
|
+
" title: {",
|
|
2765
|
+
" fontSize: 18,",
|
|
2766
|
+
' fontWeight: "600",',
|
|
2767
|
+
' color: "#18181b",',
|
|
2768
|
+
" marginBottom: 4,",
|
|
2769
|
+
" } as TextStyle,",
|
|
2770
|
+
" description: {",
|
|
2771
|
+
" fontSize: 14,",
|
|
2772
|
+
' color: "#71717a",',
|
|
2773
|
+
" marginBottom: 12,",
|
|
2774
|
+
" } as TextStyle,",
|
|
2775
|
+
"};",
|
|
2776
|
+
"",
|
|
2777
|
+
"export default Card;"
|
|
2778
|
+
)
|
|
2779
|
+
}
|
|
2780
|
+
]
|
|
2781
|
+
};
|
|
2782
|
+
var ui_reusables_default = uiReusablesModule;
|
|
2783
|
+
|
|
2784
|
+
// src/modules/index.ts
|
|
2785
|
+
var modules = [
|
|
2786
|
+
// P0 — Core (default checked)
|
|
2787
|
+
network_default,
|
|
2788
|
+
state_default,
|
|
2789
|
+
storage_default,
|
|
2790
|
+
// Note: expo-router and nativewind are always included in the base template
|
|
2791
|
+
// P1 — Feature modules
|
|
2792
|
+
payment_default,
|
|
2793
|
+
form_default,
|
|
2794
|
+
image_default,
|
|
2795
|
+
video_default,
|
|
2796
|
+
auth_google_default,
|
|
2797
|
+
auth_facebook_default,
|
|
2798
|
+
auth_apple_default,
|
|
2799
|
+
webview_default,
|
|
2800
|
+
i18n_default,
|
|
2801
|
+
animation_default,
|
|
2802
|
+
// P2 — Additional modules
|
|
2803
|
+
ota_default,
|
|
2804
|
+
notification_default,
|
|
2805
|
+
permission_default,
|
|
2806
|
+
bottom_sheet_default,
|
|
2807
|
+
flashlist_default,
|
|
2808
|
+
ui_reusables_default
|
|
2809
|
+
];
|
|
2810
|
+
function getModuleById(id) {
|
|
2811
|
+
return modules.find((m) => m.id === id);
|
|
2812
|
+
}
|
|
2813
|
+
function getModulesByIds(ids) {
|
|
2814
|
+
return ids.map((id) => getModuleById(id)).filter((m) => m !== void 0);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/templates/base.ts
|
|
2818
|
+
function generateBaseTemplates(projectName) {
|
|
2819
|
+
return [
|
|
2820
|
+
// ─── app/_layout.tsx ────────────────────────────────────────────────
|
|
2821
|
+
{
|
|
2822
|
+
path: "app/_layout.tsx",
|
|
2823
|
+
content: `import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
|
|
2824
|
+
import { useFonts } from "expo-font";
|
|
2825
|
+
import { Stack } from "expo-router";
|
|
2826
|
+
import * as SplashScreen from "expo-splash-screen";
|
|
2827
|
+
import { useEffect } from "react";
|
|
2828
|
+
import { useColorScheme } from "react-native";
|
|
2829
|
+
|
|
2830
|
+
import { Colors } from "@/src/constants/Colors";
|
|
2831
|
+
|
|
2832
|
+
SplashScreen.preventAutoHideAsync();
|
|
2833
|
+
|
|
2834
|
+
export default function RootLayout() {
|
|
2835
|
+
const colorScheme = useColorScheme();
|
|
2836
|
+
const [loaded] = useFonts({
|
|
2837
|
+
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
|
2838
|
+
});
|
|
2839
|
+
|
|
2840
|
+
useEffect(() => {
|
|
2841
|
+
if (loaded) {
|
|
2842
|
+
SplashScreen.hideAsync();
|
|
2843
|
+
}
|
|
2844
|
+
}, [loaded]);
|
|
2845
|
+
|
|
2846
|
+
if (!loaded) {
|
|
2847
|
+
return null;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
return (
|
|
2851
|
+
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
|
2852
|
+
<Stack>
|
|
2853
|
+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
2854
|
+
<Stack.Screen name="+not-found" />
|
|
2855
|
+
</Stack>
|
|
2856
|
+
</ThemeProvider>
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
`
|
|
2860
|
+
},
|
|
2861
|
+
// ─── app/(tabs)/_layout.tsx ─────────────────────────────────────────
|
|
2862
|
+
{
|
|
2863
|
+
path: "app/(tabs)/_layout.tsx",
|
|
2864
|
+
content: `import { Tabs } from "expo-router";
|
|
2865
|
+
import { Platform } from "react-native";
|
|
2866
|
+
|
|
2867
|
+
import { Colors } from "@/src/constants/Colors";
|
|
2868
|
+
import { useColorScheme } from "@/src/hooks/useColorScheme";
|
|
2869
|
+
|
|
2870
|
+
export default function TabLayout() {
|
|
2871
|
+
const colorScheme = useColorScheme();
|
|
2872
|
+
|
|
2873
|
+
return (
|
|
2874
|
+
<Tabs
|
|
2875
|
+
screenOptions={{
|
|
2876
|
+
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
|
|
2877
|
+
headerStyle: {
|
|
2878
|
+
backgroundColor: Colors[colorScheme ?? "light"].background,
|
|
2879
|
+
},
|
|
2880
|
+
headerShadowVisible: false,
|
|
2881
|
+
tabBarStyle: Platform.select({
|
|
2882
|
+
ios: {
|
|
2883
|
+
position: "absolute",
|
|
2884
|
+
},
|
|
2885
|
+
default: {},
|
|
2886
|
+
}),
|
|
2887
|
+
}}
|
|
2888
|
+
>
|
|
2889
|
+
<Tabs.Screen
|
|
2890
|
+
name="index"
|
|
2891
|
+
options={{
|
|
2892
|
+
title: "Home",
|
|
2893
|
+
tabBarIcon: () => null,
|
|
2894
|
+
}}
|
|
2895
|
+
/>
|
|
2896
|
+
<Tabs.Screen
|
|
2897
|
+
name="explore"
|
|
2898
|
+
options={{
|
|
2899
|
+
title: "Explore",
|
|
2900
|
+
tabBarIcon: () => null,
|
|
2901
|
+
}}
|
|
2902
|
+
/>
|
|
2903
|
+
</Tabs>
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
`
|
|
2907
|
+
},
|
|
2908
|
+
// ─── app/(tabs)/index.tsx ───────────────────────────────────────────
|
|
2909
|
+
{
|
|
2910
|
+
path: "app/(tabs)/index.tsx",
|
|
2911
|
+
content: `import { Image, StyleSheet, Platform } from "react-native";
|
|
2912
|
+
import { HelloWave } from "@/src/components/HelloWave";
|
|
2913
|
+
import { ThemedText } from "@/src/components/Themed";
|
|
2914
|
+
import { ThemedView } from "@/src/components/Themed";
|
|
2915
|
+
import { Link } from "expo-router";
|
|
2916
|
+
|
|
2917
|
+
export default function HomeScreen() {
|
|
2918
|
+
return (
|
|
2919
|
+
<ThemedView style={styles.container}>
|
|
2920
|
+
<ThemedView style={styles.titleContainer}>
|
|
2921
|
+
<ThemedText type="title">${projectName}</ThemedText>
|
|
2922
|
+
<HelloWave />
|
|
2923
|
+
</ThemedView>
|
|
2924
|
+
<ThemedView style={styles.stepContainer}>
|
|
2925
|
+
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
|
|
2926
|
+
<ThemedText>
|
|
2927
|
+
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
|
|
2928
|
+
Press{" "}
|
|
2929
|
+
<ThemedText type="defaultSemiBold">
|
|
2930
|
+
{Platform.select({ ios: "cmd + d", android: "cmd + m" })}
|
|
2931
|
+
</ThemedText>{" "}
|
|
2932
|
+
to open developer tools.
|
|
2933
|
+
</ThemedText>
|
|
2934
|
+
</ThemedView>
|
|
2935
|
+
<ThemedView style={styles.stepContainer}>
|
|
2936
|
+
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
|
|
2937
|
+
<ThemedText>
|
|
2938
|
+
Tap the Explore tab to learn more about what's included in this starter.
|
|
2939
|
+
</ThemedText>
|
|
2940
|
+
<Link href="/explore">
|
|
2941
|
+
<ThemedText type="link">Go to Explore \u2192</ThemedText>
|
|
2942
|
+
</Link>
|
|
2943
|
+
</ThemedView>
|
|
2944
|
+
</ThemedView>
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
const styles = StyleSheet.create({
|
|
2949
|
+
container: {
|
|
2950
|
+
flex: 1,
|
|
2951
|
+
alignItems: "center",
|
|
2952
|
+
justifyContent: "center",
|
|
2953
|
+
},
|
|
2954
|
+
titleContainer: {
|
|
2955
|
+
flexDirection: "row",
|
|
2956
|
+
alignItems: "center",
|
|
2957
|
+
gap: 8,
|
|
2958
|
+
},
|
|
2959
|
+
stepContainer: {
|
|
2960
|
+
gap: 8,
|
|
2961
|
+
marginBottom: 8,
|
|
2962
|
+
},
|
|
2963
|
+
});
|
|
2964
|
+
`
|
|
2965
|
+
},
|
|
2966
|
+
// ─── app/(tabs)/explore.tsx ─────────────────────────────────────────
|
|
2967
|
+
{
|
|
2968
|
+
path: "app/(tabs)/explore.tsx",
|
|
2969
|
+
content: `import { StyleSheet, Image, Platform } from "react-native";
|
|
2970
|
+
import { ThemedText } from "@/src/components/Themed";
|
|
2971
|
+
import { ThemedView } from "@/src/components/Themed";
|
|
2972
|
+
|
|
2973
|
+
export default function ExploreScreen() {
|
|
2974
|
+
return (
|
|
2975
|
+
<ThemedView style={styles.container}>
|
|
2976
|
+
<ThemedText type="title">Explore</ThemedText>
|
|
2977
|
+
<ThemedText style={styles.subtitle}>
|
|
2978
|
+
This screen shows what you can do with this scaffolded project.
|
|
2979
|
+
</ThemedText>
|
|
2980
|
+
</ThemedView>
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
const styles = StyleSheet.create({
|
|
2985
|
+
container: {
|
|
2986
|
+
flex: 1,
|
|
2987
|
+
alignItems: "center",
|
|
2988
|
+
justifyContent: "center",
|
|
2989
|
+
},
|
|
2990
|
+
subtitle: {
|
|
2991
|
+
fontSize: 16,
|
|
2992
|
+
textAlign: "center",
|
|
2993
|
+
marginTop: 8,
|
|
2994
|
+
},
|
|
2995
|
+
});
|
|
2996
|
+
`
|
|
2997
|
+
},
|
|
2998
|
+
// ─── app/+not-found.tsx ─────────────────────────────────────────────
|
|
2999
|
+
{
|
|
3000
|
+
path: "app/+not-found.tsx",
|
|
3001
|
+
content: `import { Link, Stack } from "expo-router";
|
|
3002
|
+
import { StyleSheet } from "react-native";
|
|
3003
|
+
import { ThemedText } from "@/src/components/Themed";
|
|
3004
|
+
import { ThemedView } from "@/src/components/Themed";
|
|
3005
|
+
|
|
3006
|
+
export default function NotFoundScreen() {
|
|
3007
|
+
return (
|
|
3008
|
+
<>
|
|
3009
|
+
<Stack.Screen options={{ title: "Oops!" }} />
|
|
3010
|
+
<ThemedView style={styles.container}>
|
|
3011
|
+
<ThemedText type="title">This screen doesn't exist.</ThemedText>
|
|
3012
|
+
<Link href="/" style={styles.link}>
|
|
3013
|
+
<ThemedText type="link">Go to home screen!</ThemedText>
|
|
3014
|
+
</Link>
|
|
3015
|
+
</ThemedView>
|
|
3016
|
+
</>
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
const styles = StyleSheet.create({
|
|
3021
|
+
container: {
|
|
3022
|
+
flex: 1,
|
|
3023
|
+
alignItems: "center",
|
|
3024
|
+
justifyContent: "center",
|
|
3025
|
+
padding: 20,
|
|
3026
|
+
},
|
|
3027
|
+
link: {
|
|
3028
|
+
marginTop: 15,
|
|
3029
|
+
paddingVertical: 15,
|
|
3030
|
+
},
|
|
3031
|
+
});
|
|
3032
|
+
`
|
|
3033
|
+
},
|
|
3034
|
+
// ─── src/components/Themed.tsx ───────────────────────────────────────
|
|
3035
|
+
{
|
|
3036
|
+
path: "src/components/Themed.tsx",
|
|
3037
|
+
content: `import { Text, type TextProps, View, type ViewProps } from "react-native";
|
|
3038
|
+
import { useColorScheme } from "@/src/hooks/useColorScheme";
|
|
3039
|
+
import { Colors } from "@/src/constants/Colors";
|
|
3040
|
+
|
|
3041
|
+
/** Themed text component that adapts to light/dark mode */
|
|
3042
|
+
export function ThemedText({
|
|
3043
|
+
style,
|
|
3044
|
+
type = "default",
|
|
3045
|
+
...rest
|
|
3046
|
+
}: TextProps & { type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link" }) {
|
|
3047
|
+
const colorScheme = useColorScheme();
|
|
3048
|
+
const color = Colors[colorScheme ?? "light"].text;
|
|
3049
|
+
|
|
3050
|
+
return (
|
|
3051
|
+
<Text
|
|
3052
|
+
style={[
|
|
3053
|
+
{ color },
|
|
3054
|
+
type === "default" ? { fontSize: 16, lineHeight: 24 } : undefined,
|
|
3055
|
+
type === "title" ? { fontSize: 28, fontWeight: "bold", lineHeight: 32 } : undefined,
|
|
3056
|
+
type === "defaultSemiBold" ? { fontSize: 16, lineHeight: 24, fontWeight: "600" } : undefined,
|
|
3057
|
+
type === "subtitle" ? { fontSize: 20, fontWeight: "bold" } : undefined,
|
|
3058
|
+
type === "link" ? { fontSize: 16, lineHeight: 24, color: Colors[colorScheme ?? "light"].tint } : undefined,
|
|
3059
|
+
style,
|
|
3060
|
+
]}
|
|
3061
|
+
{...rest}
|
|
3062
|
+
/>
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
/** Themed view component that adapts to light/dark mode */
|
|
3067
|
+
export function ThemedView({ style, ...rest }: ViewProps) {
|
|
3068
|
+
const colorScheme = useColorScheme();
|
|
3069
|
+
const backgroundColor = Colors[colorScheme ?? "light"].background;
|
|
3070
|
+
|
|
3071
|
+
return <View style={[{ backgroundColor }, style]} {...rest} />;
|
|
3072
|
+
}
|
|
3073
|
+
`
|
|
3074
|
+
},
|
|
3075
|
+
// ─── src/components/HelloWave.tsx ────────────────────────────────────
|
|
3076
|
+
{
|
|
3077
|
+
path: "src/components/HelloWave.tsx",
|
|
3078
|
+
content: `import { useEffect } from "react";
|
|
3079
|
+
import { Animated, Easing } from "react-native";
|
|
3080
|
+
import { ThemedText } from "./Themed";
|
|
3081
|
+
|
|
3082
|
+
export function HelloWave() {
|
|
3083
|
+
const rotationAnim = new Animated.Value(0);
|
|
3084
|
+
|
|
3085
|
+
useEffect(() => {
|
|
3086
|
+
Animated.loop(
|
|
3087
|
+
Animated.sequence([
|
|
3088
|
+
Animated.timing(rotationAnim, {
|
|
3089
|
+
toValue: 1,
|
|
3090
|
+
duration: 150,
|
|
3091
|
+
easing: Easing.linear,
|
|
3092
|
+
useNativeDriver: true,
|
|
3093
|
+
}),
|
|
3094
|
+
Animated.timing(rotationAnim, {
|
|
3095
|
+
toValue: 0,
|
|
3096
|
+
duration: 150,
|
|
3097
|
+
easing: Easing.linear,
|
|
3098
|
+
useNativeDriver: true,
|
|
3099
|
+
}),
|
|
3100
|
+
])
|
|
3101
|
+
).start();
|
|
3102
|
+
}, [rotationAnim]);
|
|
3103
|
+
|
|
3104
|
+
return (
|
|
3105
|
+
<Animated.View
|
|
3106
|
+
style={{
|
|
3107
|
+
transform: [
|
|
3108
|
+
{
|
|
3109
|
+
rotate: rotationAnim.interpolate({
|
|
3110
|
+
inputRange: [0, 1],
|
|
3111
|
+
outputRange: ["0deg", "14deg"],
|
|
3112
|
+
}),
|
|
3113
|
+
},
|
|
3114
|
+
],
|
|
3115
|
+
}}
|
|
3116
|
+
>
|
|
3117
|
+
<ThemedText style={{ fontSize: 28 }}>\u{1F44B}</ThemedText>
|
|
3118
|
+
</Animated.View>
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
`
|
|
3122
|
+
},
|
|
3123
|
+
// ─── src/hooks/useColorScheme.ts ─────────────────────────────────────
|
|
3124
|
+
{
|
|
3125
|
+
path: "src/hooks/useColorScheme.ts",
|
|
3126
|
+
content: `import { useColorScheme as useRNColorScheme } from "react-native";
|
|
3127
|
+
|
|
3128
|
+
/**
|
|
3129
|
+
* Returns the current color scheme (light or dark).
|
|
3130
|
+
* Defaults to "light" if the system preference is not available.
|
|
3131
|
+
*/
|
|
3132
|
+
export function useColorScheme(): "light" | "dark" {
|
|
3133
|
+
return useRNColorScheme() ?? "light";
|
|
3134
|
+
}
|
|
3135
|
+
`
|
|
3136
|
+
},
|
|
3137
|
+
// ─── src/constants/Colors.ts ─────────────────────────────────────────
|
|
3138
|
+
{
|
|
3139
|
+
path: "src/constants/Colors.ts",
|
|
3140
|
+
content: `/**
|
|
3141
|
+
* Color tokens for light and dark themes.
|
|
3142
|
+
* Used by Themed components and navigation theming.
|
|
3143
|
+
*/
|
|
3144
|
+
export const Colors = {
|
|
3145
|
+
light: {
|
|
3146
|
+
text: "#11181C",
|
|
3147
|
+
background: "#fff",
|
|
3148
|
+
tint: "#0a7ea4",
|
|
3149
|
+
tabIconDefault: "#687076",
|
|
3150
|
+
tabIconSelected: "#0a7ea4",
|
|
3151
|
+
},
|
|
3152
|
+
dark: {
|
|
3153
|
+
text: "#ECEDEE",
|
|
3154
|
+
background: "#151718",
|
|
3155
|
+
tint: "#fff",
|
|
3156
|
+
tabIconDefault: "#9BA1A6",
|
|
3157
|
+
tabIconSelected: "#fff",
|
|
3158
|
+
},
|
|
3159
|
+
};
|
|
3160
|
+
`
|
|
3161
|
+
},
|
|
3162
|
+
// ─── src/types/index.ts ─────────────────────────────────────────────
|
|
3163
|
+
{
|
|
3164
|
+
path: "src/types/index.ts",
|
|
3165
|
+
content: `/** Global type definitions */
|
|
3166
|
+
|
|
3167
|
+
/** Extend this to declare module-specific types */
|
|
3168
|
+
declare global {
|
|
3169
|
+
// Add global type augmentations here
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
export {};
|
|
3173
|
+
`
|
|
3174
|
+
},
|
|
3175
|
+
// ─── app.json ────────────────────────────────────────────────────────
|
|
3176
|
+
{
|
|
3177
|
+
path: "app.json",
|
|
3178
|
+
content: `{
|
|
3179
|
+
"expo": {
|
|
3180
|
+
"name": "${projectName}",
|
|
3181
|
+
"slug": "${projectName}",
|
|
3182
|
+
"version": "1.0.0",
|
|
3183
|
+
"orientation": "portrait",
|
|
3184
|
+
"icon": "./assets/icon.png",
|
|
3185
|
+
"userInterfaceStyle": "light",
|
|
3186
|
+
"splash": {
|
|
3187
|
+
"image": "./assets/splash.png",
|
|
3188
|
+
"resizeMode": "contain",
|
|
3189
|
+
"backgroundColor": "#ffffff"
|
|
3190
|
+
},
|
|
3191
|
+
"ios": {
|
|
3192
|
+
"supportsTablet": true,
|
|
3193
|
+
"bundleIdentifier": "com.${projectName}.app"
|
|
3194
|
+
},
|
|
3195
|
+
"android": {
|
|
3196
|
+
"adaptiveIcon": {
|
|
3197
|
+
"foregroundImage": "./assets/adaptive-icon.png",
|
|
3198
|
+
"backgroundColor": "#ffffff"
|
|
3199
|
+
},
|
|
3200
|
+
"package": "com.${projectName}.app"
|
|
3201
|
+
},
|
|
3202
|
+
"web": {
|
|
3203
|
+
"favicon": "./assets/favicon.png"
|
|
3204
|
+
},
|
|
3205
|
+
"plugins": [
|
|
3206
|
+
"expo-router",
|
|
3207
|
+
"expo-splash-screen"
|
|
3208
|
+
]
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
`
|
|
3212
|
+
},
|
|
3213
|
+
// ─── tsconfig.json ───────────────────────────────────────────────────
|
|
3214
|
+
{
|
|
3215
|
+
path: "tsconfig.json",
|
|
3216
|
+
content: `{
|
|
3217
|
+
"compilerOptions": {
|
|
3218
|
+
"target": "ESNext",
|
|
3219
|
+
"module": "ESNext",
|
|
3220
|
+
"moduleResolution": "bundler",
|
|
3221
|
+
"lib": ["ESNext"],
|
|
3222
|
+
"strict": true,
|
|
3223
|
+
"jsx": "react-jsx",
|
|
3224
|
+
"esModuleInterop": true,
|
|
3225
|
+
"skipLibCheck": true,
|
|
3226
|
+
"forceConsistentCasingInFileNames": true,
|
|
3227
|
+
"resolveJsonModule": true,
|
|
3228
|
+
"isolatedModules": true,
|
|
3229
|
+
"noEmit": true,
|
|
3230
|
+
"paths": {
|
|
3231
|
+
"@/*": ["./*"]
|
|
3232
|
+
}
|
|
3233
|
+
},
|
|
3234
|
+
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
|
|
3235
|
+
"exclude": ["node_modules"]
|
|
3236
|
+
}
|
|
3237
|
+
`
|
|
3238
|
+
},
|
|
3239
|
+
// ─── tailwind.config.js ──────────────────────────────────────────────
|
|
3240
|
+
{
|
|
3241
|
+
path: "tailwind.config.js",
|
|
3242
|
+
content: `/** @type {import('tailwindcss').Config} */
|
|
3243
|
+
module.exports = {
|
|
3244
|
+
content: [
|
|
3245
|
+
"./app/**/*.{js,jsx,ts,tsx}",
|
|
3246
|
+
"./src/**/*.{js,jsx,ts,tsx}",
|
|
3247
|
+
],
|
|
3248
|
+
theme: {
|
|
3249
|
+
extend: {},
|
|
3250
|
+
},
|
|
3251
|
+
plugins: [],
|
|
3252
|
+
};
|
|
3253
|
+
`
|
|
3254
|
+
},
|
|
3255
|
+
// ─── metro.config.js ─────────────────────────────────────────────────
|
|
3256
|
+
{
|
|
3257
|
+
path: "metro.config.js",
|
|
3258
|
+
content: `const { getDefaultConfig } = require("expo/metro-config");
|
|
3259
|
+
const { withTailwind } = require("@expo/metro-config/tailwind");
|
|
3260
|
+
|
|
3261
|
+
const config = getDefaultConfig(__dirname);
|
|
3262
|
+
|
|
3263
|
+
module.exports = withTailwind(config);
|
|
3264
|
+
`
|
|
3265
|
+
},
|
|
3266
|
+
// ─── babel.config.js ─────────────────────────────────────────────────
|
|
3267
|
+
{
|
|
3268
|
+
path: "babel.config.js",
|
|
3269
|
+
content: `module.exports = function (api) {
|
|
3270
|
+
api.cache(true);
|
|
3271
|
+
return {
|
|
3272
|
+
presets: ["babel-preset-expo"],
|
|
3273
|
+
plugins: ["nativewind/babel"],
|
|
3274
|
+
};
|
|
3275
|
+
};
|
|
3276
|
+
`
|
|
3277
|
+
},
|
|
3278
|
+
// ─── .gitignore ─────────────────────────────────────────────────────
|
|
3279
|
+
{
|
|
3280
|
+
path: ".gitignore",
|
|
3281
|
+
content: `# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
|
3282
|
+
|
|
3283
|
+
# dependencies
|
|
3284
|
+
node_modules/
|
|
3285
|
+
|
|
3286
|
+
# Expo
|
|
3287
|
+
.expo/
|
|
3288
|
+
dist/
|
|
3289
|
+
web-build/
|
|
3290
|
+
|
|
3291
|
+
# Native
|
|
3292
|
+
*.orig.*
|
|
3293
|
+
*.jks
|
|
3294
|
+
*.p8
|
|
3295
|
+
*.p12
|
|
3296
|
+
*.key
|
|
3297
|
+
*.mobileprovision
|
|
3298
|
+
|
|
3299
|
+
# Metro
|
|
3300
|
+
.metro-health-check*
|
|
3301
|
+
|
|
3302
|
+
# debug
|
|
3303
|
+
npm-debug.*
|
|
3304
|
+
yarn-debug.*
|
|
3305
|
+
yarn-error.*
|
|
3306
|
+
|
|
3307
|
+
# macOS
|
|
3308
|
+
.DS_Store
|
|
3309
|
+
*.pem
|
|
3310
|
+
|
|
3311
|
+
# local env files
|
|
3312
|
+
.env*.local
|
|
3313
|
+
|
|
3314
|
+
# typescript
|
|
3315
|
+
*.tsbuildinfo
|
|
3316
|
+
|
|
3317
|
+
# generated files
|
|
3318
|
+
expo-env.d.ts
|
|
3319
|
+
`
|
|
3320
|
+
}
|
|
3321
|
+
];
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// src/utils/file.ts
|
|
3325
|
+
var import_fs_extra = __toESM(require("fs-extra"));
|
|
3326
|
+
var import_path = __toESM(require("path"));
|
|
3327
|
+
async function writeFile(filePath, content) {
|
|
3328
|
+
const dir = import_path.default.dirname(filePath);
|
|
3329
|
+
await import_fs_extra.default.ensureDir(dir);
|
|
3330
|
+
await import_fs_extra.default.writeFile(filePath, content, "utf-8");
|
|
3331
|
+
}
|
|
3332
|
+
async function writeJson(filePath, data, spaces = 2) {
|
|
3333
|
+
const dir = import_path.default.dirname(filePath);
|
|
3334
|
+
await import_fs_extra.default.ensureDir(dir);
|
|
3335
|
+
await import_fs_extra.default.writeJson(filePath, data, { spaces });
|
|
3336
|
+
}
|
|
3337
|
+
function replaceTemplateVars(content, vars) {
|
|
3338
|
+
let result = content;
|
|
3339
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
3340
|
+
const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g");
|
|
3341
|
+
result = result.replace(regex, value);
|
|
3342
|
+
}
|
|
3343
|
+
return result;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/utils/package.ts
|
|
3347
|
+
function generateBasePackageJson(projectName) {
|
|
3348
|
+
return {
|
|
3349
|
+
name: projectName,
|
|
3350
|
+
version: "1.0.0",
|
|
3351
|
+
main: "expo-router/entry",
|
|
3352
|
+
scripts: {
|
|
3353
|
+
start: "expo start",
|
|
3354
|
+
android: "expo start --android",
|
|
3355
|
+
ios: "expo start --ios",
|
|
3356
|
+
web: "expo start --web",
|
|
3357
|
+
lint: "eslint ."
|
|
3358
|
+
},
|
|
3359
|
+
dependencies: {
|
|
3360
|
+
expo: "~54.0.0",
|
|
3361
|
+
"expo-router": "~5.0.0",
|
|
3362
|
+
"expo-linking": "~7.0.0",
|
|
3363
|
+
"expo-constants": "~18.0.0",
|
|
3364
|
+
"expo-status-bar": "~2.0.0",
|
|
3365
|
+
"expo-splash-screen": "~1.0.0",
|
|
3366
|
+
react: "19.0.0",
|
|
3367
|
+
"react-native": "0.79.0",
|
|
3368
|
+
"react-native-safe-area-context": "5.4.0",
|
|
3369
|
+
"react-native-screens": "~4.6.0",
|
|
3370
|
+
nativewind: "^4.1.0",
|
|
3371
|
+
tailwindcss: "^3.4.0"
|
|
3372
|
+
},
|
|
3373
|
+
devDependencies: {
|
|
3374
|
+
"@types/react": "~19.0.0",
|
|
3375
|
+
typescript: "^5.5.0"
|
|
3376
|
+
}
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// src/index.ts
|
|
3381
|
+
var import_execa = require("execa");
|
|
3382
|
+
async function run() {
|
|
3383
|
+
const program = new import_commander.Command();
|
|
3384
|
+
program.name("expo-bbase").description("Expo SDK 54+ \u811A\u624B\u67B6 CLI \u5DE5\u5177").version("1.0.0");
|
|
3385
|
+
program.argument("[project-name]", "Name of the project to create").action(async (projectName) => {
|
|
3386
|
+
if (!projectName) {
|
|
3387
|
+
console.error(import_chalk.default.red("Error: Please provide a project name."));
|
|
3388
|
+
console.log(import_chalk.default.gray("Usage: npx expo-bbase <project-name>"));
|
|
3389
|
+
process.exit(1);
|
|
3390
|
+
}
|
|
3391
|
+
await createProject(projectName);
|
|
3392
|
+
});
|
|
3393
|
+
registerCreateCommand(program);
|
|
3394
|
+
await program.parseAsync(process.argv);
|
|
3395
|
+
}
|
|
3396
|
+
async function createProject(projectName) {
|
|
3397
|
+
console.log();
|
|
3398
|
+
console.log(
|
|
3399
|
+
import_chalk.default.bold.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")
|
|
3400
|
+
);
|
|
3401
|
+
console.log(
|
|
3402
|
+
import_chalk.default.bold.cyan(" \u2551 expo-bbase \u2014 Expo \u811A\u624B\u67B6\u5DE5\u5177 \u2551")
|
|
3403
|
+
);
|
|
3404
|
+
console.log(
|
|
3405
|
+
import_chalk.default.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")
|
|
3406
|
+
);
|
|
3407
|
+
console.log();
|
|
3408
|
+
const choices = modules.map((m) => ({
|
|
3409
|
+
title: `${import_chalk.default.bold(m.name)} \u2014 ${import_chalk.default.gray(m.description)}`,
|
|
3410
|
+
value: m.id,
|
|
3411
|
+
selected: m.defaultChecked
|
|
3412
|
+
}));
|
|
3413
|
+
const { selectedModules } = await (0, import_prompts.default)({
|
|
3414
|
+
type: "multiselect",
|
|
3415
|
+
name: "selectedModules",
|
|
3416
|
+
message: "\u9009\u62E9\u9700\u8981\u7684\u529F\u80FD\u6A21\u5757\uFF08\u7A7A\u683C\u5207\u6362\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09",
|
|
3417
|
+
choices,
|
|
3418
|
+
hint: "- \u7A7A\u683C\u5207\u6362\u9009\u62E9 \xB7 a \u5168\u9009/\u53D6\u6D88 \xB7 \u56DE\u8F66\u786E\u8BA4",
|
|
3419
|
+
instructions: false
|
|
3420
|
+
});
|
|
3421
|
+
if (selectedModules === void 0) {
|
|
3422
|
+
console.log(import_chalk.default.yellow("\n\u5DF2\u53D6\u6D88\u521B\u5EFA\u9879\u76EE\u3002"));
|
|
3423
|
+
process.exit(0);
|
|
3424
|
+
}
|
|
3425
|
+
const selectedModuleDefs = getModulesByIds(selectedModules);
|
|
3426
|
+
const targetDir = import_path2.default.resolve(process.cwd(), projectName);
|
|
3427
|
+
console.log();
|
|
3428
|
+
console.log(import_chalk.default.white(` \u{1F4E6} \u9879\u76EE\u540D\u79F0: ${import_chalk.default.bold(projectName)}`));
|
|
3429
|
+
console.log(
|
|
3430
|
+
import_chalk.default.white(` \u{1F4C2} \u76EE\u6807\u8DEF\u5F84: ${import_chalk.default.gray(targetDir)}`)
|
|
3431
|
+
);
|
|
3432
|
+
console.log(
|
|
3433
|
+
import_chalk.default.white(
|
|
3434
|
+
` \u{1F9E9} \u9009\u62E9\u6A21\u5757: ${import_chalk.default.green(selectedModuleDefs.map((m) => m.name).join(", ") || "\u65E0")}`
|
|
3435
|
+
)
|
|
3436
|
+
);
|
|
3437
|
+
console.log();
|
|
3438
|
+
const spinner = (0, import_ora.default)("\u6B63\u5728\u521B\u5EFA\u9879\u76EE...").start();
|
|
3439
|
+
try {
|
|
3440
|
+
const baseTemplates = generateBaseTemplates(projectName);
|
|
3441
|
+
for (const file of baseTemplates) {
|
|
3442
|
+
const filePath = import_path2.default.join(targetDir, file.path);
|
|
3443
|
+
const content = replaceTemplateVars(file.content, {
|
|
3444
|
+
projectName
|
|
3445
|
+
});
|
|
3446
|
+
await writeFile(filePath, content);
|
|
3447
|
+
}
|
|
3448
|
+
spinner.text = "\u6B63\u5728\u5199\u5165\u6A21\u5757\u6587\u4EF6...";
|
|
3449
|
+
for (const mod of selectedModuleDefs) {
|
|
3450
|
+
for (const file of mod.files) {
|
|
3451
|
+
const filePath = import_path2.default.join(targetDir, file.path);
|
|
3452
|
+
const content = replaceTemplateVars(file.content, {
|
|
3453
|
+
projectName
|
|
3454
|
+
});
|
|
3455
|
+
await writeFile(filePath, content);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
spinner.text = "\u6B63\u5728\u751F\u6210 package.json...";
|
|
3459
|
+
const pkgJson = generateBasePackageJson(projectName);
|
|
3460
|
+
const allDeps = {};
|
|
3461
|
+
const allDevDeps = {};
|
|
3462
|
+
for (const mod of selectedModuleDefs) {
|
|
3463
|
+
Object.assign(allDeps, mod.dependencies);
|
|
3464
|
+
Object.assign(allDevDeps, mod.devDependencies);
|
|
3465
|
+
}
|
|
3466
|
+
Object.assign(pkgJson.dependencies, allDeps);
|
|
3467
|
+
Object.assign(
|
|
3468
|
+
pkgJson.devDependencies,
|
|
3469
|
+
allDevDeps
|
|
3470
|
+
);
|
|
3471
|
+
const pkgPath = import_path2.default.join(targetDir, "package.json");
|
|
3472
|
+
await writeJson(pkgPath, pkgJson);
|
|
3473
|
+
spinner.text = "\u6B63\u5728\u914D\u7F6E app.json...";
|
|
3474
|
+
await updateAppJson(targetDir, selectedModuleDefs, projectName);
|
|
3475
|
+
spinner.text = "\u6B63\u5728\u914D\u7F6E Babel...";
|
|
3476
|
+
await updateBabelConfig(targetDir, selectedModuleDefs);
|
|
3477
|
+
spinner.text = "\u6B63\u5728\u914D\u7F6E\u5165\u53E3\u6587\u4EF6...";
|
|
3478
|
+
await updateLayoutFile(targetDir, selectedModuleDefs);
|
|
3479
|
+
spinner.text = "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56 (npm install)...";
|
|
3480
|
+
try {
|
|
3481
|
+
await (0, import_execa.execa)("npm", ["install"], {
|
|
3482
|
+
cwd: targetDir,
|
|
3483
|
+
timeout: 3e5
|
|
3484
|
+
// 5 minute timeout
|
|
3485
|
+
});
|
|
3486
|
+
} catch (installError) {
|
|
3487
|
+
spinner.warn("npm install \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u8FD0\u884C npm install");
|
|
3488
|
+
console.log(
|
|
3489
|
+
import_chalk.default.gray(` cd ${projectName} && npm install`)
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
spinner.succeed(import_chalk.default.green("\u9879\u76EE\u521B\u5EFA\u6210\u529F\uFF01"));
|
|
3493
|
+
console.log();
|
|
3494
|
+
console.log(import_chalk.default.bold(" \u{1F389} \u4E0B\u4E00\u6B65\uFF1A"));
|
|
3495
|
+
console.log(import_chalk.default.white(` cd ${projectName}`));
|
|
3496
|
+
console.log(import_chalk.default.white(" npx expo start"));
|
|
3497
|
+
console.log();
|
|
3498
|
+
if (selectedModuleDefs.length > 0) {
|
|
3499
|
+
console.log(import_chalk.default.bold(" \u{1F4CB} \u5DF2\u9009\u6A21\u5757\uFF1A"));
|
|
3500
|
+
for (const mod of selectedModuleDefs) {
|
|
3501
|
+
console.log(import_chalk.default.white(` \u2713 ${mod.name}`));
|
|
3502
|
+
}
|
|
3503
|
+
console.log();
|
|
3504
|
+
}
|
|
3505
|
+
} catch (error) {
|
|
3506
|
+
spinner.fail(import_chalk.default.red("\u9879\u76EE\u521B\u5EFA\u5931\u8D25"));
|
|
3507
|
+
console.error(error);
|
|
3508
|
+
process.exit(1);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
async function updateAppJson(targetDir, selectedModules, projectName) {
|
|
3512
|
+
const appJsonPath = import_path2.default.join(targetDir, "app.json");
|
|
3513
|
+
const appJson = await import("fs-extra").then(
|
|
3514
|
+
(fs2) => fs2.readJson(appJsonPath)
|
|
3515
|
+
);
|
|
3516
|
+
const existingPlugins = appJson.expo?.plugins || [];
|
|
3517
|
+
for (const mod of selectedModules) {
|
|
3518
|
+
if (mod.appConfig?.plugins) {
|
|
3519
|
+
const modulePlugins = mod.appConfig.plugins;
|
|
3520
|
+
for (const plugin of modulePlugins) {
|
|
3521
|
+
const pluginName = typeof plugin === "string" ? plugin : plugin[0];
|
|
3522
|
+
const exists = existingPlugins.some(
|
|
3523
|
+
(p) => typeof p === "string" ? p === pluginName : p[0] === pluginName
|
|
3524
|
+
);
|
|
3525
|
+
if (!exists) {
|
|
3526
|
+
existingPlugins.push(plugin);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
appJson.expo.plugins = existingPlugins;
|
|
3532
|
+
await writeJson(appJsonPath, appJson);
|
|
3533
|
+
}
|
|
3534
|
+
async function updateBabelConfig(targetDir, selectedModules) {
|
|
3535
|
+
const babelPath = import_path2.default.join(targetDir, "babel.config.js");
|
|
3536
|
+
const fs2 = await import("fs-extra");
|
|
3537
|
+
let content = await fs2.readFile(babelPath, "utf-8");
|
|
3538
|
+
const extraPlugins = [];
|
|
3539
|
+
for (const mod of selectedModules) {
|
|
3540
|
+
if (mod.babelPlugins) {
|
|
3541
|
+
extraPlugins.push(...mod.babelPlugins);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
if (extraPlugins.length > 0) {
|
|
3545
|
+
const pluginStrings = extraPlugins.map((p) => ` "${p}"`).join(",\n");
|
|
3546
|
+
content = content.replace(
|
|
3547
|
+
/plugins:\s*\[([^\]]*)\]/,
|
|
3548
|
+
`plugins: [$1${pluginStrings ? ",\n" + pluginStrings : ""}]`
|
|
3549
|
+
);
|
|
3550
|
+
await fs2.writeFile(babelPath, content, "utf-8");
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
async function updateLayoutFile(targetDir, selectedModules) {
|
|
3554
|
+
const layoutPath = import_path2.default.join(targetDir, "app/_layout.tsx");
|
|
3555
|
+
const fs2 = await import("fs-extra");
|
|
3556
|
+
let content = await fs2.readFile(layoutPath, "utf-8");
|
|
3557
|
+
const extraImports = [];
|
|
3558
|
+
const extraProviderPairs = [];
|
|
3559
|
+
for (const mod of selectedModules) {
|
|
3560
|
+
if (mod.layoutImports) {
|
|
3561
|
+
extraImports.push(...mod.layoutImports);
|
|
3562
|
+
}
|
|
3563
|
+
if (mod.layoutProviders) {
|
|
3564
|
+
for (const provider of mod.layoutProviders) {
|
|
3565
|
+
const match = provider.match(/^<(\w+)/);
|
|
3566
|
+
if (match) {
|
|
3567
|
+
const tagName = match[1];
|
|
3568
|
+
extraProviderPairs.push({
|
|
3569
|
+
open: ` ${provider}`,
|
|
3570
|
+
close: ` </${tagName}>`
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
if (extraImports.length === 0 && extraProviderPairs.length === 0) {
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
if (extraImports.length > 0) {
|
|
3580
|
+
const lastImportIndex = content.lastIndexOf("import ");
|
|
3581
|
+
const lineEndIndex = content.indexOf("\n", lastImportIndex);
|
|
3582
|
+
const importBlock = "\n" + extraImports.join("\n");
|
|
3583
|
+
content = content.slice(0, lineEndIndex + 1) + importBlock + content.slice(lineEndIndex + 1);
|
|
3584
|
+
}
|
|
3585
|
+
if (extraProviderPairs.length > 0) {
|
|
3586
|
+
const returnMatch = content.indexOf("return (");
|
|
3587
|
+
if (returnMatch !== -1) {
|
|
3588
|
+
const openingProviders = extraProviderPairs.map((p) => p.open).join("\n");
|
|
3589
|
+
const closingProviders = extraProviderPairs.reverse().map((p) => p.close).join("\n");
|
|
3590
|
+
const themeProviderOpen = content.indexOf("<ThemeProvider");
|
|
3591
|
+
const themeProviderClose = content.lastIndexOf("</ThemeProvider>");
|
|
3592
|
+
if (themeProviderOpen !== -1 && themeProviderClose !== -1) {
|
|
3593
|
+
content = content.slice(0, themeProviderOpen) + openingProviders + "\n" + content.slice(themeProviderOpen);
|
|
3594
|
+
const newThemeProviderClose = content.lastIndexOf("</ThemeProvider>");
|
|
3595
|
+
const afterClose = newThemeProviderClose + "</ThemeProvider>".length;
|
|
3596
|
+
content = content.slice(0, afterClose) + "\n" + closingProviders + content.slice(afterClose);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
await fs2.writeFile(layoutPath, content, "utf-8");
|
|
3601
|
+
}
|
|
3602
|
+
run().catch((error) => {
|
|
3603
|
+
console.error(import_chalk.default.red("Fatal error:"), error);
|
|
3604
|
+
process.exit(1);
|
|
3605
|
+
});
|
|
3606
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3607
|
+
0 && (module.exports = {
|
|
3608
|
+
createProject,
|
|
3609
|
+
run
|
|
3610
|
+
});
|
|
3611
|
+
//# sourceMappingURL=index.js.map
|