create-100x-mobile 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -0
- package/dist/commands/new/args.js +8 -0
- package/dist/commands/new/scaffold.js +1 -1
- package/dist/commands/new/steps.js +65 -12
- package/dist/commands/new.js +20 -9
- package/dist/templates/config/appJson.js +5 -7
- package/dist/templates/config/envExample.js +4 -0
- package/dist/templates/config/packageJson.js +1 -0
- package/dist/templates/config/readme.js +16 -2
- package/dist/templates/instant/authProvider.js +217 -0
- package/dist/templates/instant/rootLayout.js +4 -2
- package/dist/templates/instant/settingsScreen.js +184 -3
- package/dist/templates/instant/tabsLayout.js +12 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ Options:
|
|
|
21
21
|
--clerk-pk <key> Pre-fill Clerk publishable key
|
|
22
22
|
--clerk-domain <domain> Pre-fill Clerk issuer domain
|
|
23
23
|
--instant-app-id <id> Pre-fill InstantDB app id
|
|
24
|
+
--instant-clerk-client-name <name> Client name configured in Instant Auth tab
|
|
24
25
|
|
|
25
26
|
Scaffolds a mobile app with Expo + Convex/Clerk or Expo + InstantDB.
|
|
26
27
|
`);
|
|
@@ -26,6 +26,7 @@ function parseNewCommandArgs(args) {
|
|
|
26
26
|
clerkPublishableKey: null,
|
|
27
27
|
clerkDomain: null,
|
|
28
28
|
instantAppId: null,
|
|
29
|
+
instantClerkClientName: null,
|
|
29
30
|
};
|
|
30
31
|
let projectName = null;
|
|
31
32
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -85,6 +86,13 @@ function parseNewCommandArgs(args) {
|
|
|
85
86
|
i = nextIndex;
|
|
86
87
|
continue;
|
|
87
88
|
}
|
|
89
|
+
if (arg === "--instant-clerk-client-name" ||
|
|
90
|
+
arg.startsWith("--instant-clerk-client-name=")) {
|
|
91
|
+
const { value, nextIndex } = getInlineOrNextValue(arg, args, i);
|
|
92
|
+
options.instantClerkClientName = value;
|
|
93
|
+
i = nextIndex;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
88
96
|
throw new Error(`Unknown flag: ${arg}`);
|
|
89
97
|
}
|
|
90
98
|
if (!projectName && !options.interactive) {
|
|
@@ -15,7 +15,7 @@ async function createProjectDirectories(projectDir, backend) {
|
|
|
15
15
|
];
|
|
16
16
|
const backendDirs = backend === "convex"
|
|
17
17
|
? ["app/(auth)", "app/providers", "components", "convex"]
|
|
18
|
-
: ["lib"];
|
|
18
|
+
: ["app/(auth)", "app/providers", "lib"];
|
|
19
19
|
const dirs = [...commonDirs, ...backendDirs];
|
|
20
20
|
for (const dir of dirs) {
|
|
21
21
|
await (0, fs_1.ensureDir)((0, node_path_1.join)(projectDir, dir));
|
|
@@ -199,7 +199,13 @@ async function resolveExpoDependencies(projectDir, backend) {
|
|
|
199
199
|
];
|
|
200
200
|
const backendExpoPackages = backend === "convex"
|
|
201
201
|
? ["expo-auth-session", "expo-secure-store", "expo-web-browser"]
|
|
202
|
-
: [
|
|
202
|
+
: [
|
|
203
|
+
"expo-auth-session",
|
|
204
|
+
"expo-secure-store",
|
|
205
|
+
"expo-web-browser",
|
|
206
|
+
"@react-native-async-storage/async-storage",
|
|
207
|
+
"@react-native-community/netinfo",
|
|
208
|
+
];
|
|
203
209
|
const expoPackages = [...commonExpoPackages, ...backendExpoPackages];
|
|
204
210
|
try {
|
|
205
211
|
await (0, run_1.run)("npx", ["expo", "install", ...expoPackages], { cwd: projectDir });
|
|
@@ -274,21 +280,46 @@ async function promptClerkSetup(options) {
|
|
|
274
280
|
}
|
|
275
281
|
async function promptInstantSetup(options) {
|
|
276
282
|
let appId = options.instantAppId?.trim() ?? "";
|
|
277
|
-
|
|
283
|
+
let clerkPublishableKey = options.clerkPublishableKey?.trim() ?? "";
|
|
284
|
+
let clerkClientName = options.instantClerkClientName?.trim() || "clerk";
|
|
285
|
+
if (options.interactive) {
|
|
278
286
|
prompts_1.log.info("");
|
|
279
287
|
prompts_1.log.info(picocolors_1.default.bold("InstantDB Setup"));
|
|
280
288
|
prompts_1.log.info(picocolors_1.default.dim("Create an app id with: npx instant-cli init-without-files --title my-app"));
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
289
|
+
if (!appId) {
|
|
290
|
+
prompts_1.log.info(picocolors_1.default.dim("Press Enter to skip and configure later."));
|
|
291
|
+
const appIdInput = await (0, prompts_1.text)({
|
|
292
|
+
message: "InstantDB App ID",
|
|
293
|
+
placeholder: "your-instant-app-id",
|
|
294
|
+
defaultValue: "",
|
|
295
|
+
});
|
|
296
|
+
if (!(0, prompts_1.isCancel)(appIdInput) && appIdInput) {
|
|
297
|
+
appId = appIdInput.trim();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
prompts_1.log.info("");
|
|
301
|
+
prompts_1.log.info(picocolors_1.default.bold("Clerk Setup for InstantDB"));
|
|
302
|
+
prompts_1.log.info(picocolors_1.default.dim("In Clerk Sessions → Customize session token, include `email` and `email_verified` claims."));
|
|
303
|
+
if (!clerkPublishableKey) {
|
|
304
|
+
const clerkKeyInput = await (0, prompts_1.text)({
|
|
305
|
+
message: "Clerk Publishable Key",
|
|
306
|
+
placeholder: "pk_test_...",
|
|
307
|
+
defaultValue: "",
|
|
308
|
+
});
|
|
309
|
+
if (!(0, prompts_1.isCancel)(clerkKeyInput) && clerkKeyInput) {
|
|
310
|
+
clerkPublishableKey = clerkKeyInput.trim();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const clientNameInput = await (0, prompts_1.text)({
|
|
314
|
+
message: "Instant Clerk Client Name",
|
|
315
|
+
placeholder: "clerk",
|
|
316
|
+
defaultValue: clerkClientName,
|
|
286
317
|
});
|
|
287
|
-
if (!(0, prompts_1.isCancel)(
|
|
288
|
-
|
|
318
|
+
if (!(0, prompts_1.isCancel)(clientNameInput) && clientNameInput?.trim()) {
|
|
319
|
+
clerkClientName = clientNameInput.trim();
|
|
289
320
|
}
|
|
290
321
|
}
|
|
291
|
-
return { appId };
|
|
322
|
+
return { appId, clerkPublishableKey, clerkClientName };
|
|
292
323
|
}
|
|
293
324
|
async function writeAuthEnvironment(projectDir, clerk) {
|
|
294
325
|
const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
|
|
@@ -316,7 +347,7 @@ async function writeAuthEnvironment(projectDir, clerk) {
|
|
|
316
347
|
}
|
|
317
348
|
}
|
|
318
349
|
async function writeInstantEnvironment(projectDir, instant) {
|
|
319
|
-
if (!instant.appId) {
|
|
350
|
+
if (!instant.appId && !instant.clerkPublishableKey && !instant.clerkClientName) {
|
|
320
351
|
return;
|
|
321
352
|
}
|
|
322
353
|
const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
|
|
@@ -329,7 +360,13 @@ async function writeInstantEnvironment(projectDir, instant) {
|
|
|
329
360
|
prompts_1.log.warn(`Could not read .env.local: ${toErrorMessage(error)}`);
|
|
330
361
|
}
|
|
331
362
|
}
|
|
332
|
-
|
|
363
|
+
if (instant.appId) {
|
|
364
|
+
envContents = (0, dotenv_1.upsertDotenvVar)(envContents, "EXPO_PUBLIC_INSTANT_APP_ID", instant.appId);
|
|
365
|
+
}
|
|
366
|
+
if (instant.clerkPublishableKey) {
|
|
367
|
+
envContents = (0, dotenv_1.upsertDotenvVar)(envContents, "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY", instant.clerkPublishableKey);
|
|
368
|
+
}
|
|
369
|
+
envContents = (0, dotenv_1.upsertDotenvVar)(envContents, "EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME", instant.clerkClientName || "clerk");
|
|
333
370
|
await (0, fs_1.writeTextFile)(envLocalPath, envContents);
|
|
334
371
|
}
|
|
335
372
|
async function initializeConvex(projectDir) {
|
|
@@ -418,6 +455,14 @@ async function runHealthChecks(projectDir, backend, clerkPublishableKey) {
|
|
|
418
455
|
if (!envExists) {
|
|
419
456
|
if (backend === "instantdb") {
|
|
420
457
|
checks.push({ label: "EXPO_PUBLIC_INSTANT_APP_ID is set", ok: false });
|
|
458
|
+
checks.push({
|
|
459
|
+
label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
|
|
460
|
+
ok: false,
|
|
461
|
+
});
|
|
462
|
+
checks.push({
|
|
463
|
+
label: "EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME is set",
|
|
464
|
+
ok: false,
|
|
465
|
+
});
|
|
421
466
|
}
|
|
422
467
|
else {
|
|
423
468
|
checks.push({ label: "EXPO_PUBLIC_CONVEX_URL is set", ok: false });
|
|
@@ -442,6 +487,14 @@ async function runHealthChecks(projectDir, backend, clerkPublishableKey) {
|
|
|
442
487
|
label: "EXPO_PUBLIC_INSTANT_APP_ID is set",
|
|
443
488
|
ok: envContent.includes("EXPO_PUBLIC_INSTANT_APP_ID"),
|
|
444
489
|
});
|
|
490
|
+
checks.push({
|
|
491
|
+
label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
|
|
492
|
+
ok: envContent.includes("EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY"),
|
|
493
|
+
});
|
|
494
|
+
checks.push({
|
|
495
|
+
label: "EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME is set",
|
|
496
|
+
ok: envContent.includes("EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME"),
|
|
497
|
+
});
|
|
445
498
|
}
|
|
446
499
|
else {
|
|
447
500
|
checks.push({
|
package/dist/commands/new.js
CHANGED
|
@@ -38,6 +38,7 @@ const tabsLayout_1 = require("../templates/app/tabsLayout");
|
|
|
38
38
|
const todosScreen_1 = require("../templates/app/todosScreen");
|
|
39
39
|
// Instant templates
|
|
40
40
|
const instantLib_1 = require("../templates/instant/instantLib");
|
|
41
|
+
const authProvider_2 = require("../templates/instant/authProvider");
|
|
41
42
|
const rootLayout_2 = require("../templates/instant/rootLayout");
|
|
42
43
|
const settingsScreen_2 = require("../templates/instant/settingsScreen");
|
|
43
44
|
const tabsLayout_2 = require("../templates/instant/tabsLayout");
|
|
@@ -54,7 +55,7 @@ function buildTemplateFiles(projectName, expoVersion, backend) {
|
|
|
54
55
|
const files = [
|
|
55
56
|
// Config
|
|
56
57
|
["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend)],
|
|
57
|
-
["app.json", (0, appJson_1.appJsonTemplate)(projectName)],
|
|
58
|
+
["app.json", (0, appJson_1.appJsonTemplate)(projectName, backend)],
|
|
58
59
|
["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
|
|
59
60
|
[".gitignore", (0, gitignore_1.gitignoreTemplate)()],
|
|
60
61
|
[".env.example", (0, envExample_1.envExampleTemplate)(backend)],
|
|
@@ -65,6 +66,9 @@ function buildTemplateFiles(projectName, expoVersion, backend) {
|
|
|
65
66
|
// Instant app
|
|
66
67
|
["app/_layout.tsx", (0, rootLayout_2.instantRootLayoutTemplate)()],
|
|
67
68
|
["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
|
|
69
|
+
["app/providers/AuthProvider.tsx", (0, authProvider_2.instantAuthProviderTemplate)()],
|
|
70
|
+
["app/(auth)/_layout.tsx", (0, authLayout_1.authLayoutTemplate)()],
|
|
71
|
+
["app/(auth)/sign-in.tsx", (0, signIn_1.signInTemplate)()],
|
|
68
72
|
["app/(tabs)/_layout.tsx", (0, tabsLayout_2.instantTabsLayoutTemplate)()],
|
|
69
73
|
["app/(tabs)/index.tsx", (0, todosScreen_2.instantTodosScreenTemplate)()],
|
|
70
74
|
["app/(tabs)/settings.tsx", (0, settingsScreen_2.instantSettingsScreenTemplate)()],
|
|
@@ -77,7 +81,7 @@ function buildTemplateFiles(projectName, expoVersion, backend) {
|
|
|
77
81
|
const files = [
|
|
78
82
|
// Config
|
|
79
83
|
["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend)],
|
|
80
|
-
["app.json", (0, appJson_1.appJsonTemplate)(projectName)],
|
|
84
|
+
["app.json", (0, appJson_1.appJsonTemplate)(projectName, backend)],
|
|
81
85
|
["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
|
|
82
86
|
[".gitignore", (0, gitignore_1.gitignoreTemplate)()],
|
|
83
87
|
[".env.example", (0, envExample_1.envExampleTemplate)(backend)],
|
|
@@ -168,7 +172,7 @@ async function cmdNew(rawArgs) {
|
|
|
168
172
|
: { publishableKey: "", domain: "", jwtCreated: false };
|
|
169
173
|
const instant = backend === "instantdb"
|
|
170
174
|
? await (0, steps_1.promptInstantSetup)(options)
|
|
171
|
-
: { appId: "" };
|
|
175
|
+
: { appId: "", clerkPublishableKey: "", clerkClientName: "clerk" };
|
|
172
176
|
const totalSteps = 1 + // project generation
|
|
173
177
|
(options.installDependencies ? 2 : 0) +
|
|
174
178
|
(backend === "convex" ? 2 + (clerk.domain ? 1 : 0) : 1) +
|
|
@@ -192,11 +196,11 @@ async function cmdNew(rawArgs) {
|
|
|
192
196
|
else {
|
|
193
197
|
prompts_1.log.step(`Configuring InstantDB ${stepLabel()}`);
|
|
194
198
|
await (0, steps_1.writeInstantEnvironment)(projectDir, instant);
|
|
195
|
-
if (instant.appId) {
|
|
196
|
-
prompts_1.log.success("InstantDB environment configured.");
|
|
199
|
+
if (instant.appId && instant.clerkPublishableKey) {
|
|
200
|
+
prompts_1.log.success("InstantDB and Clerk environment configured.");
|
|
197
201
|
}
|
|
198
202
|
else {
|
|
199
|
-
prompts_1.log.info(picocolors_1.default.dim("Set EXPO_PUBLIC_INSTANT_APP_ID in .env.local
|
|
203
|
+
prompts_1.log.info(picocolors_1.default.dim("Set EXPO_PUBLIC_INSTANT_APP_ID and EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in .env.local."));
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
if (options.installDependencies) {
|
|
@@ -272,7 +276,13 @@ async function cmdNew(rawArgs) {
|
|
|
272
276
|
}
|
|
273
277
|
prompts_1.log.info("");
|
|
274
278
|
if (backend === "instantdb") {
|
|
275
|
-
|
|
279
|
+
const instantReady = instant.appId && instant.clerkPublishableKey;
|
|
280
|
+
if (instantReady) {
|
|
281
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Your app is ready!")));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded with InstantDB!")));
|
|
285
|
+
}
|
|
276
286
|
prompts_1.log.info("");
|
|
277
287
|
prompts_1.log.info(" Next steps:");
|
|
278
288
|
prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
|
|
@@ -283,11 +293,12 @@ async function cmdNew(rawArgs) {
|
|
|
283
293
|
prompts_1.log.info(` ${picocolors_1.default.cyan("bun install")} ${picocolors_1.default.dim("# or npm install")}`);
|
|
284
294
|
prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
|
|
285
295
|
}
|
|
286
|
-
if (!
|
|
296
|
+
if (!instantReady) {
|
|
287
297
|
prompts_1.log.info("");
|
|
288
|
-
prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID to .env.local
|
|
298
|
+
prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID and EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to .env.local."));
|
|
289
299
|
prompts_1.log.info(picocolors_1.default.dim(" Create one with: npx instant-cli init-without-files --title " +
|
|
290
300
|
projectName));
|
|
301
|
+
prompts_1.log.info(picocolors_1.default.dim(" In Instant Auth tab, add your Clerk app and set EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME."));
|
|
291
302
|
}
|
|
292
303
|
}
|
|
293
304
|
else if (clerk.publishableKey && clerk.domain) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.appJsonTemplate = appJsonTemplate;
|
|
4
|
-
function appJsonTemplate(appName) {
|
|
4
|
+
function appJsonTemplate(appName, backend) {
|
|
5
|
+
const plugins = backend === "convex"
|
|
6
|
+
? ["expo-router", "expo-font", "expo-web-browser", "expo-secure-store"]
|
|
7
|
+
: ["expo-router", "expo-font", "expo-web-browser", "expo-secure-store"];
|
|
5
8
|
const config = {
|
|
6
9
|
expo: {
|
|
7
10
|
name: appName,
|
|
@@ -20,12 +23,7 @@ function appJsonTemplate(appName) {
|
|
|
20
23
|
output: "single",
|
|
21
24
|
favicon: "./assets/images/favicon.png",
|
|
22
25
|
},
|
|
23
|
-
plugins
|
|
24
|
-
"expo-router",
|
|
25
|
-
"expo-font",
|
|
26
|
-
"expo-web-browser",
|
|
27
|
-
"expo-secure-store",
|
|
28
|
-
],
|
|
26
|
+
plugins,
|
|
29
27
|
experiments: {
|
|
30
28
|
typedRoutes: true,
|
|
31
29
|
},
|
|
@@ -4,6 +4,7 @@ exports.packageJsonTemplate = packageJsonTemplate;
|
|
|
4
4
|
function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex") {
|
|
5
5
|
const backendDependencies = backend === "instantdb"
|
|
6
6
|
? {
|
|
7
|
+
"@clerk/clerk-expo": "^2.14.24",
|
|
7
8
|
"@instantdb/react-native": "^0.20.0",
|
|
8
9
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
9
10
|
"@react-native-community/netinfo": "^11.4.1",
|
|
@@ -5,12 +5,13 @@ function readmeTemplate(projectName, backend) {
|
|
|
5
5
|
if (backend === "instantdb") {
|
|
6
6
|
return `# ${projectName}
|
|
7
7
|
|
|
8
|
-
A mobile app built with **Expo** and **
|
|
8
|
+
A mobile app built with **Expo**, **InstantDB**, and **Clerk**.
|
|
9
9
|
|
|
10
10
|
## Tech Stack
|
|
11
11
|
|
|
12
12
|
- **[Expo](https://expo.dev)** — React Native framework
|
|
13
13
|
- **[InstantDB](https://instantdb.com)** — Real-time backend and sync
|
|
14
|
+
- **[Clerk](https://clerk.com)** — Authentication
|
|
14
15
|
- **[Expo Router](https://docs.expo.dev/router/introduction/)** — File-based navigation
|
|
15
16
|
|
|
16
17
|
## Getting Started
|
|
@@ -26,12 +27,24 @@ cp .env.example .env.local
|
|
|
26
27
|
bunx expo start
|
|
27
28
|
\`\`\`
|
|
28
29
|
|
|
29
|
-
Set
|
|
30
|
+
Set these in \`.env.local\`:
|
|
31
|
+
- \`EXPO_PUBLIC_INSTANT_APP_ID\`
|
|
32
|
+
- \`EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY\`
|
|
33
|
+
- \`EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME\` (default \`clerk\`)
|
|
34
|
+
|
|
35
|
+
You can create an Instant app id via:
|
|
30
36
|
|
|
31
37
|
\`\`\`bash
|
|
32
38
|
npx instant-cli init-without-files --title ${projectName}
|
|
33
39
|
\`\`\`
|
|
34
40
|
|
|
41
|
+
Clerk setup required for Instant:
|
|
42
|
+
1. In Clerk dashboard, Sessions → Customize session token.
|
|
43
|
+
2. Add claims:
|
|
44
|
+
- \`"email": "{{user.primary_email_address}}"\`
|
|
45
|
+
- \`"email_verified": "{{user.email_verified}}"\`
|
|
46
|
+
3. In Instant dashboard Auth tab, register your Clerk publishable key and set the client name.
|
|
47
|
+
|
|
35
48
|
## Scripts
|
|
36
49
|
|
|
37
50
|
| Command | Description |
|
|
@@ -44,6 +57,7 @@ npx instant-cli init-without-files --title ${projectName}
|
|
|
44
57
|
|
|
45
58
|
- [Expo Docs](https://docs.expo.dev)
|
|
46
59
|
- [InstantDB Docs](https://instantdb.com/docs)
|
|
60
|
+
- [Clerk Docs](https://clerk.com/docs)
|
|
47
61
|
`;
|
|
48
62
|
}
|
|
49
63
|
return `# ${projectName}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instantAuthProviderTemplate = instantAuthProviderTemplate;
|
|
4
|
+
function instantAuthProviderTemplate() {
|
|
5
|
+
return `import React from "react";
|
|
6
|
+
import { ClerkProvider, ClerkLoaded, useAuth } from "@clerk/clerk-expo";
|
|
7
|
+
import * as SecureStore from "expo-secure-store";
|
|
8
|
+
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
|
9
|
+
import { instantDb } from "@/lib/instant";
|
|
10
|
+
|
|
11
|
+
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
|
12
|
+
const clerkClientName =
|
|
13
|
+
process.env.EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME || "clerk";
|
|
14
|
+
|
|
15
|
+
const tokenCache = {
|
|
16
|
+
async getToken(key: string) {
|
|
17
|
+
try {
|
|
18
|
+
return await SecureStore.getItemAsync(key);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("SecureStore getToken error:", error);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
async saveToken(key: string, value: string) {
|
|
25
|
+
try {
|
|
26
|
+
await SecureStore.setItemAsync(key, value);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("SecureStore saveToken error:", error);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
async deleteToken(key: string) {
|
|
32
|
+
try {
|
|
33
|
+
await SecureStore.deleteItemAsync(key);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("SecureStore deleteToken error:", error);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function SetupRequired() {
|
|
41
|
+
return (
|
|
42
|
+
<View style={styles.setupContainer}>
|
|
43
|
+
<View style={styles.setupCard}>
|
|
44
|
+
<Text style={styles.setupTitle}>Setup Required</Text>
|
|
45
|
+
<Text style={styles.setupText}>
|
|
46
|
+
Configure InstantDB and Clerk to use this app.
|
|
47
|
+
</Text>
|
|
48
|
+
<Text style={styles.setupStep}>
|
|
49
|
+
1. Set EXPO_PUBLIC_INSTANT_APP_ID{"\\n"}
|
|
50
|
+
2. Set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY{"\\n"}
|
|
51
|
+
3. Register Clerk in Instant Auth tab{"\\n"}
|
|
52
|
+
4. Set EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME
|
|
53
|
+
</Text>
|
|
54
|
+
</View>
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function InstantSessionBridge({ children }: { children: React.ReactNode }) {
|
|
60
|
+
const { isLoaded, isSignedIn, getToken } = useAuth();
|
|
61
|
+
const instantAuth = instantDb?.useAuth();
|
|
62
|
+
const [syncError, setSyncError] = React.useState<string | null>(null);
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
let active = true;
|
|
66
|
+
|
|
67
|
+
async function syncAuth() {
|
|
68
|
+
if (!instantDb || !isLoaded) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isSignedIn) {
|
|
73
|
+
setSyncError(null);
|
|
74
|
+
try {
|
|
75
|
+
await instantDb.auth.signOut();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn("Instant signOut warning:", error);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const idToken = await getToken();
|
|
84
|
+
if (!idToken) {
|
|
85
|
+
if (active) {
|
|
86
|
+
setSyncError("Could not get Clerk token.");
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await instantDb.auth.signInWithIdToken({
|
|
91
|
+
clientName: clerkClientName,
|
|
92
|
+
idToken,
|
|
93
|
+
});
|
|
94
|
+
if (active) {
|
|
95
|
+
setSyncError(null);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Instant Clerk auth error:", error);
|
|
99
|
+
if (active) {
|
|
100
|
+
setSyncError(
|
|
101
|
+
error instanceof Error ? error.message : "Could not sign in to Instant."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
syncAuth();
|
|
108
|
+
return () => {
|
|
109
|
+
active = false;
|
|
110
|
+
};
|
|
111
|
+
}, [getToken, isLoaded, isSignedIn]);
|
|
112
|
+
|
|
113
|
+
if (!instantDb) {
|
|
114
|
+
return <SetupRequired />;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!isLoaded || instantAuth?.isLoading) {
|
|
118
|
+
return (
|
|
119
|
+
<View style={styles.loadingContainer}>
|
|
120
|
+
<ActivityIndicator size="large" color="#4B5563" />
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isSignedIn && !instantAuth?.user) {
|
|
126
|
+
return (
|
|
127
|
+
<View style={styles.loadingContainer}>
|
|
128
|
+
<ActivityIndicator size="large" color="#4B5563" />
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (syncError || instantAuth?.error) {
|
|
134
|
+
return (
|
|
135
|
+
<View style={styles.setupContainer}>
|
|
136
|
+
<View style={styles.setupCard}>
|
|
137
|
+
<Text style={styles.setupTitle}>Authentication Error</Text>
|
|
138
|
+
<Text style={styles.setupText}>
|
|
139
|
+
{syncError || instantAuth?.error?.message}
|
|
140
|
+
</Text>
|
|
141
|
+
<Text style={styles.setupStep}>
|
|
142
|
+
Ensure Clerk session token includes email and email_verified claims.
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return <>{children}</>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
153
|
+
if (!publishableKey) {
|
|
154
|
+
return <SetupRequired />;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
|
|
159
|
+
<ClerkLoaded>
|
|
160
|
+
<InstantSessionBridge>{children}</InstantSessionBridge>
|
|
161
|
+
</ClerkLoaded>
|
|
162
|
+
</ClerkProvider>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const styles = StyleSheet.create({
|
|
167
|
+
setupContainer: {
|
|
168
|
+
flex: 1,
|
|
169
|
+
backgroundColor: "#f5f5f5",
|
|
170
|
+
justifyContent: "center",
|
|
171
|
+
alignItems: "center",
|
|
172
|
+
padding: 20,
|
|
173
|
+
},
|
|
174
|
+
setupCard: {
|
|
175
|
+
backgroundColor: "#fff",
|
|
176
|
+
borderRadius: 12,
|
|
177
|
+
padding: 24,
|
|
178
|
+
maxWidth: 420,
|
|
179
|
+
width: "100%",
|
|
180
|
+
shadowColor: "#000",
|
|
181
|
+
shadowOffset: { width: 0, height: 2 },
|
|
182
|
+
shadowOpacity: 0.1,
|
|
183
|
+
shadowRadius: 8,
|
|
184
|
+
elevation: 3,
|
|
185
|
+
},
|
|
186
|
+
setupTitle: {
|
|
187
|
+
fontSize: 24,
|
|
188
|
+
fontWeight: "700",
|
|
189
|
+
color: "#333",
|
|
190
|
+
marginBottom: 16,
|
|
191
|
+
textAlign: "center",
|
|
192
|
+
},
|
|
193
|
+
setupText: {
|
|
194
|
+
fontSize: 16,
|
|
195
|
+
color: "#666",
|
|
196
|
+
marginBottom: 12,
|
|
197
|
+
lineHeight: 22,
|
|
198
|
+
textAlign: "center",
|
|
199
|
+
},
|
|
200
|
+
setupStep: {
|
|
201
|
+
fontSize: 14,
|
|
202
|
+
color: "#888",
|
|
203
|
+
marginTop: 16,
|
|
204
|
+
padding: 16,
|
|
205
|
+
backgroundColor: "#f8f9fa",
|
|
206
|
+
borderRadius: 8,
|
|
207
|
+
lineHeight: 20,
|
|
208
|
+
},
|
|
209
|
+
loadingContainer: {
|
|
210
|
+
flex: 1,
|
|
211
|
+
alignItems: "center",
|
|
212
|
+
justifyContent: "center",
|
|
213
|
+
backgroundColor: "#FFFFFF",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
@@ -5,18 +5,20 @@ function instantRootLayoutTemplate() {
|
|
|
5
5
|
return `import { Stack } from "expo-router";
|
|
6
6
|
import { StatusBar } from "expo-status-bar";
|
|
7
7
|
import { useFrameworkReady } from "@/hooks/useFrameworkReady";
|
|
8
|
+
import { AuthProvider } from "./providers/AuthProvider";
|
|
8
9
|
|
|
9
10
|
export default function RootLayout() {
|
|
10
11
|
useFrameworkReady();
|
|
11
12
|
|
|
12
13
|
return (
|
|
13
|
-
|
|
14
|
+
<AuthProvider>
|
|
14
15
|
<Stack screenOptions={{ headerShown: false }}>
|
|
15
16
|
<Stack.Screen name="(tabs)" />
|
|
17
|
+
<Stack.Screen name="(auth)" />
|
|
16
18
|
<Stack.Screen name="+not-found" />
|
|
17
19
|
</Stack>
|
|
18
20
|
<StatusBar style="auto" />
|
|
19
|
-
|
|
21
|
+
</AuthProvider>
|
|
20
22
|
);
|
|
21
23
|
}
|
|
22
24
|
`;
|
|
@@ -3,12 +3,84 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.instantSettingsScreenTemplate = instantSettingsScreenTemplate;
|
|
4
4
|
function instantSettingsScreenTemplate() {
|
|
5
5
|
return `import React from "react";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Alert,
|
|
8
|
+
ActivityIndicator,
|
|
9
|
+
Image,
|
|
10
|
+
Platform,
|
|
11
|
+
StatusBar,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Text,
|
|
14
|
+
TouchableOpacity,
|
|
15
|
+
View,
|
|
16
|
+
} from "react-native";
|
|
7
17
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
18
|
+
import { LogOut, Mail, User } from "lucide-react-native";
|
|
19
|
+
import * as SecureStore from "expo-secure-store";
|
|
20
|
+
import { useAuth, useUser } from "@clerk/clerk-expo";
|
|
21
|
+
import { instantDb } from "@/lib/instant";
|
|
8
22
|
|
|
9
23
|
export default function SettingsScreen() {
|
|
24
|
+
const { signOut } = useAuth();
|
|
25
|
+
const { user, isLoaded } = useUser();
|
|
26
|
+
const [isSigningOut, setIsSigningOut] = React.useState(false);
|
|
27
|
+
const [imageLoadError, setImageLoadError] = React.useState(false);
|
|
10
28
|
const instantAppId = process.env.EXPO_PUBLIC_INSTANT_APP_ID;
|
|
11
29
|
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
setImageLoadError(false);
|
|
32
|
+
}, [user?.imageUrl]);
|
|
33
|
+
|
|
34
|
+
const performSignOut = async () => {
|
|
35
|
+
setIsSigningOut(true);
|
|
36
|
+
try {
|
|
37
|
+
try {
|
|
38
|
+
await instantDb?.auth.signOut();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn("Instant signOut warning:", error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await Promise.all([
|
|
45
|
+
SecureStore.deleteItemAsync("__clerk_client_jwt"),
|
|
46
|
+
SecureStore.deleteItemAsync("__clerk_db_jwt"),
|
|
47
|
+
SecureStore.deleteItemAsync("__clerk_session_jwt"),
|
|
48
|
+
]);
|
|
49
|
+
} catch (storageError) {
|
|
50
|
+
console.warn("SecureStore cleanup failed:", storageError);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await signOut();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn("Sign out flow warning:", error);
|
|
56
|
+
} finally {
|
|
57
|
+
setIsSigningOut(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleSignOut = async () => {
|
|
62
|
+
if (Platform.OS === "web") {
|
|
63
|
+
const confirmed = window.confirm("Are you sure you want to sign out?");
|
|
64
|
+
if (confirmed) await performSignOut();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
|
|
69
|
+
{ text: "Cancel", style: "cancel" },
|
|
70
|
+
{ text: "Sign Out", style: "destructive", onPress: performSignOut },
|
|
71
|
+
]);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (!isLoaded) {
|
|
75
|
+
return (
|
|
76
|
+
<SafeAreaView style={styles.container}>
|
|
77
|
+
<View style={styles.loadingContainer}>
|
|
78
|
+
<ActivityIndicator size="large" color="#4B5563" />
|
|
79
|
+
</View>
|
|
80
|
+
</SafeAreaView>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
12
84
|
return (
|
|
13
85
|
<SafeAreaView style={styles.container}>
|
|
14
86
|
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
|
@@ -17,9 +89,40 @@ export default function SettingsScreen() {
|
|
|
17
89
|
</View>
|
|
18
90
|
|
|
19
91
|
<View style={styles.content}>
|
|
92
|
+
<View style={styles.card}>
|
|
93
|
+
<Text style={styles.sectionTitle}>Profile</Text>
|
|
94
|
+
<View style={styles.profileRow}>
|
|
95
|
+
<View style={styles.avatarWrap}>
|
|
96
|
+
{user?.imageUrl && !imageLoadError ? (
|
|
97
|
+
<Image
|
|
98
|
+
source={{ uri: user.imageUrl }}
|
|
99
|
+
style={styles.avatar}
|
|
100
|
+
onError={() => setImageLoadError(true)}
|
|
101
|
+
/>
|
|
102
|
+
) : (
|
|
103
|
+
<User size={28} color="#6B7280" />
|
|
104
|
+
)}
|
|
105
|
+
</View>
|
|
106
|
+
<View style={styles.profileTextWrap}>
|
|
107
|
+
<Text style={styles.profileName}>
|
|
108
|
+
{user?.fullName ||
|
|
109
|
+
\`\${user?.firstName || ""} \${user?.lastName || ""}\`.trim() ||
|
|
110
|
+
user?.username ||
|
|
111
|
+
"User"}
|
|
112
|
+
</Text>
|
|
113
|
+
<View style={styles.emailRow}>
|
|
114
|
+
<Mail size={14} color="#9CA3AF" />
|
|
115
|
+
<Text style={styles.profileEmail}>
|
|
116
|
+
{user?.primaryEmailAddress?.emailAddress || "No email"}
|
|
117
|
+
</Text>
|
|
118
|
+
</View>
|
|
119
|
+
</View>
|
|
120
|
+
</View>
|
|
121
|
+
</View>
|
|
122
|
+
|
|
20
123
|
<View style={styles.card}>
|
|
21
124
|
<Text style={styles.sectionTitle}>Backend</Text>
|
|
22
|
-
<Text style={styles.sectionText}>InstantDB</Text>
|
|
125
|
+
<Text style={styles.sectionText}>InstantDB + Clerk</Text>
|
|
23
126
|
</View>
|
|
24
127
|
|
|
25
128
|
<View style={styles.card}>
|
|
@@ -32,8 +135,25 @@ export default function SettingsScreen() {
|
|
|
32
135
|
<View style={styles.card}>
|
|
33
136
|
<Text style={styles.sectionTitle}>About</Text>
|
|
34
137
|
<Text style={styles.sectionText}>Version 1.0.0</Text>
|
|
35
|
-
<Text style={styles.sectionText}>Built with Expo + InstantDB</Text>
|
|
138
|
+
<Text style={styles.sectionText}>Built with Expo + InstantDB + Clerk</Text>
|
|
36
139
|
</View>
|
|
140
|
+
|
|
141
|
+
<TouchableOpacity
|
|
142
|
+
style={[styles.signOutButton, isSigningOut && styles.signOutButtonDisabled]}
|
|
143
|
+
onPress={() => {
|
|
144
|
+
if (!isSigningOut) handleSignOut();
|
|
145
|
+
}}
|
|
146
|
+
activeOpacity={0.75}
|
|
147
|
+
>
|
|
148
|
+
{isSigningOut ? (
|
|
149
|
+
<ActivityIndicator size={18} color="#EF4444" />
|
|
150
|
+
) : (
|
|
151
|
+
<LogOut size={18} color="#EF4444" />
|
|
152
|
+
)}
|
|
153
|
+
<Text style={styles.signOutText}>
|
|
154
|
+
{isSigningOut ? "Signing Out..." : "Sign Out"}
|
|
155
|
+
</Text>
|
|
156
|
+
</TouchableOpacity>
|
|
37
157
|
</View>
|
|
38
158
|
</SafeAreaView>
|
|
39
159
|
);
|
|
@@ -44,6 +164,11 @@ const styles = StyleSheet.create({
|
|
|
44
164
|
flex: 1,
|
|
45
165
|
backgroundColor: "#FFFFFF",
|
|
46
166
|
},
|
|
167
|
+
loadingContainer: {
|
|
168
|
+
flex: 1,
|
|
169
|
+
justifyContent: "center",
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
},
|
|
47
172
|
header: {
|
|
48
173
|
paddingHorizontal: 16,
|
|
49
174
|
paddingTop: 24,
|
|
@@ -78,6 +203,62 @@ const styles = StyleSheet.create({
|
|
|
78
203
|
marginBottom: 4,
|
|
79
204
|
lineHeight: 22,
|
|
80
205
|
},
|
|
206
|
+
profileRow: {
|
|
207
|
+
flexDirection: "row",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
},
|
|
210
|
+
avatarWrap: {
|
|
211
|
+
width: 54,
|
|
212
|
+
height: 54,
|
|
213
|
+
borderRadius: 27,
|
|
214
|
+
backgroundColor: "#E5E7EB",
|
|
215
|
+
alignItems: "center",
|
|
216
|
+
justifyContent: "center",
|
|
217
|
+
marginRight: 12,
|
|
218
|
+
},
|
|
219
|
+
avatar: {
|
|
220
|
+
width: 54,
|
|
221
|
+
height: 54,
|
|
222
|
+
borderRadius: 27,
|
|
223
|
+
},
|
|
224
|
+
profileTextWrap: {
|
|
225
|
+
flex: 1,
|
|
226
|
+
},
|
|
227
|
+
profileName: {
|
|
228
|
+
fontSize: 17,
|
|
229
|
+
fontWeight: "600",
|
|
230
|
+
color: "#111827",
|
|
231
|
+
marginBottom: 4,
|
|
232
|
+
},
|
|
233
|
+
emailRow: {
|
|
234
|
+
flexDirection: "row",
|
|
235
|
+
alignItems: "center",
|
|
236
|
+
},
|
|
237
|
+
profileEmail: {
|
|
238
|
+
marginLeft: 6,
|
|
239
|
+
color: "#6B7280",
|
|
240
|
+
fontSize: 14,
|
|
241
|
+
},
|
|
242
|
+
signOutButton: {
|
|
243
|
+
marginTop: 4,
|
|
244
|
+
flexDirection: "row",
|
|
245
|
+
alignItems: "center",
|
|
246
|
+
justifyContent: "center",
|
|
247
|
+
minHeight: 48,
|
|
248
|
+
borderRadius: 12,
|
|
249
|
+
backgroundColor: "#FEF2F2",
|
|
250
|
+
borderWidth: 1,
|
|
251
|
+
borderColor: "#FECACA",
|
|
252
|
+
gap: 8,
|
|
253
|
+
},
|
|
254
|
+
signOutButtonDisabled: {
|
|
255
|
+
opacity: 0.65,
|
|
256
|
+
},
|
|
257
|
+
signOutText: {
|
|
258
|
+
color: "#EF4444",
|
|
259
|
+
fontSize: 15,
|
|
260
|
+
fontWeight: "600",
|
|
261
|
+
},
|
|
81
262
|
});
|
|
82
263
|
`;
|
|
83
264
|
}
|
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.instantTabsLayoutTemplate = instantTabsLayoutTemplate;
|
|
4
4
|
function instantTabsLayoutTemplate() {
|
|
5
|
-
return `import { Tabs } from "expo-router";
|
|
5
|
+
return `import { Tabs, Redirect } from "expo-router";
|
|
6
6
|
import { SquareCheck as CheckSquare, Settings } from "lucide-react-native";
|
|
7
7
|
import { Platform } from "react-native";
|
|
8
|
+
import { useAuth } from "@clerk/clerk-expo";
|
|
8
9
|
|
|
9
10
|
export default function TabLayout() {
|
|
11
|
+
const { isSignedIn, isLoaded } = useAuth();
|
|
12
|
+
|
|
13
|
+
if (!isLoaded) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!isSignedIn) {
|
|
18
|
+
return <Redirect href="/(auth)/sign-in" />;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
return (
|
|
11
22
|
<Tabs
|
|
12
23
|
screenOptions={{
|