create-100x-mobile 0.4.6 → 0.4.8
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 +32 -9
- package/dist/templates/app/signIn.js +1 -1
- 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 +230 -0
- package/dist/templates/instant/rootLayout.js +4 -2
- package/dist/templates/instant/settingsScreen.js +193 -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,14 @@ 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" };
|
|
176
|
+
if (backend === "instantdb" && instant.appId && instant.clerkPublishableKey) {
|
|
177
|
+
prompts_1.log.info("");
|
|
178
|
+
prompts_1.log.info(picocolors_1.default.bold("Instant Auth setup required"));
|
|
179
|
+
prompts_1.log.info(picocolors_1.default.dim("In Instant Dashboard → Auth tab, add a Clerk auth client before running the app."));
|
|
180
|
+
prompts_1.log.info(picocolors_1.default.dim(`Client name: ${instant.clerkClientName}`));
|
|
181
|
+
prompts_1.log.info(picocolors_1.default.dim(`Clerk publishable key: ${instant.clerkPublishableKey}`));
|
|
182
|
+
}
|
|
172
183
|
const totalSteps = 1 + // project generation
|
|
173
184
|
(options.installDependencies ? 2 : 0) +
|
|
174
185
|
(backend === "convex" ? 2 + (clerk.domain ? 1 : 0) : 1) +
|
|
@@ -192,11 +203,11 @@ async function cmdNew(rawArgs) {
|
|
|
192
203
|
else {
|
|
193
204
|
prompts_1.log.step(`Configuring InstantDB ${stepLabel()}`);
|
|
194
205
|
await (0, steps_1.writeInstantEnvironment)(projectDir, instant);
|
|
195
|
-
if (instant.appId) {
|
|
196
|
-
prompts_1.log.success("InstantDB environment configured.");
|
|
206
|
+
if (instant.appId && instant.clerkPublishableKey) {
|
|
207
|
+
prompts_1.log.success("InstantDB and Clerk environment configured.");
|
|
197
208
|
}
|
|
198
209
|
else {
|
|
199
|
-
prompts_1.log.info(picocolors_1.default.dim("Set EXPO_PUBLIC_INSTANT_APP_ID in .env.local
|
|
210
|
+
prompts_1.log.info(picocolors_1.default.dim("Set EXPO_PUBLIC_INSTANT_APP_ID and EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in .env.local."));
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
if (options.installDependencies) {
|
|
@@ -272,7 +283,13 @@ async function cmdNew(rawArgs) {
|
|
|
272
283
|
}
|
|
273
284
|
prompts_1.log.info("");
|
|
274
285
|
if (backend === "instantdb") {
|
|
275
|
-
|
|
286
|
+
const instantReady = instant.appId && instant.clerkPublishableKey;
|
|
287
|
+
if (instantReady) {
|
|
288
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Your app is ready!")));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded with InstantDB!")));
|
|
292
|
+
}
|
|
276
293
|
prompts_1.log.info("");
|
|
277
294
|
prompts_1.log.info(" Next steps:");
|
|
278
295
|
prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
|
|
@@ -283,11 +300,17 @@ async function cmdNew(rawArgs) {
|
|
|
283
300
|
prompts_1.log.info(` ${picocolors_1.default.cyan("bun install")} ${picocolors_1.default.dim("# or npm install")}`);
|
|
284
301
|
prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
|
|
285
302
|
}
|
|
286
|
-
if (!
|
|
303
|
+
if (!instantReady) {
|
|
287
304
|
prompts_1.log.info("");
|
|
288
|
-
prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID to .env.local
|
|
305
|
+
prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID and EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to .env.local."));
|
|
289
306
|
prompts_1.log.info(picocolors_1.default.dim(" Create one with: npx instant-cli init-without-files --title " +
|
|
290
307
|
projectName));
|
|
308
|
+
prompts_1.log.info(picocolors_1.default.dim(" In Instant Auth tab, add your Clerk app and set EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME."));
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
prompts_1.log.info("");
|
|
312
|
+
prompts_1.log.info(picocolors_1.default.dim(" In Instant Dashboard → Auth tab, add a Clerk auth client using your publishable key."));
|
|
313
|
+
prompts_1.log.info(picocolors_1.default.dim(` Use client name "${instant.clerkClientName}" (EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME).`));
|
|
291
314
|
}
|
|
292
315
|
}
|
|
293
316
|
else if (clerk.publishableKey && clerk.domain) {
|
|
@@ -44,7 +44,7 @@ export default function SignInScreen() {
|
|
|
44
44
|
|
|
45
45
|
if (createdSessionId && setActive) {
|
|
46
46
|
await setActive({ session: createdSessionId });
|
|
47
|
-
router.replace("/
|
|
47
|
+
router.replace("/");
|
|
48
48
|
}
|
|
49
49
|
} catch (err: any) {
|
|
50
50
|
console.error(\`\${provider} OAuth error:\`, err);
|
|
@@ -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,230 @@
|
|
|
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
|
+
function isSignedOutError(error: unknown): boolean {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
17
|
+
return message.toLowerCase().includes("signed out");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tokenCache = {
|
|
21
|
+
async getToken(key: string) {
|
|
22
|
+
try {
|
|
23
|
+
return await SecureStore.getItemAsync(key);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("SecureStore getToken error:", error);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async saveToken(key: string, value: string) {
|
|
30
|
+
try {
|
|
31
|
+
await SecureStore.setItemAsync(key, value);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("SecureStore saveToken error:", error);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
async deleteToken(key: string) {
|
|
37
|
+
try {
|
|
38
|
+
await SecureStore.deleteItemAsync(key);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("SecureStore deleteToken error:", error);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function SetupRequired() {
|
|
46
|
+
return (
|
|
47
|
+
<View style={styles.setupContainer}>
|
|
48
|
+
<View style={styles.setupCard}>
|
|
49
|
+
<Text style={styles.setupTitle}>Setup Required</Text>
|
|
50
|
+
<Text style={styles.setupText}>
|
|
51
|
+
Configure InstantDB and Clerk to use this app.
|
|
52
|
+
</Text>
|
|
53
|
+
<Text style={styles.setupStep}>
|
|
54
|
+
1. Set EXPO_PUBLIC_INSTANT_APP_ID{"\\n"}
|
|
55
|
+
2. Set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY{"\\n"}
|
|
56
|
+
3. Register Clerk in Instant Auth tab{"\\n"}
|
|
57
|
+
4. Set EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME
|
|
58
|
+
</Text>
|
|
59
|
+
</View>
|
|
60
|
+
</View>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function InstantSessionBridge({ children }: { children: React.ReactNode }) {
|
|
65
|
+
const { isLoaded, isSignedIn, getToken } = useAuth();
|
|
66
|
+
const instantAuth = instantDb?.useAuth();
|
|
67
|
+
const [syncError, setSyncError] = React.useState<string | null>(null);
|
|
68
|
+
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
let active = true;
|
|
71
|
+
|
|
72
|
+
async function syncAuth() {
|
|
73
|
+
if (!instantDb || !isLoaded) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!isSignedIn) {
|
|
78
|
+
setSyncError(null);
|
|
79
|
+
try {
|
|
80
|
+
await instantDb.auth.signOut();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (!isSignedOutError(error)) {
|
|
83
|
+
console.warn("Instant signOut warning:", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const idToken = await getToken();
|
|
91
|
+
if (!idToken) {
|
|
92
|
+
if (active) {
|
|
93
|
+
setSyncError("Could not get Clerk token.");
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await instantDb.auth.signInWithIdToken({
|
|
98
|
+
clientName: clerkClientName,
|
|
99
|
+
idToken,
|
|
100
|
+
});
|
|
101
|
+
if (active) {
|
|
102
|
+
setSyncError(null);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (isSignedOutError(error)) {
|
|
106
|
+
if (active) {
|
|
107
|
+
setSyncError(null);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.error("Instant Clerk auth error:", error);
|
|
112
|
+
if (active) {
|
|
113
|
+
setSyncError(
|
|
114
|
+
error instanceof Error ? error.message : "Could not sign in to Instant."
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
syncAuth();
|
|
121
|
+
return () => {
|
|
122
|
+
active = false;
|
|
123
|
+
};
|
|
124
|
+
}, [getToken, isLoaded, isSignedIn]);
|
|
125
|
+
|
|
126
|
+
if (!instantDb) {
|
|
127
|
+
return <SetupRequired />;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!isLoaded || instantAuth?.isLoading) {
|
|
131
|
+
return (
|
|
132
|
+
<View style={styles.loadingContainer}>
|
|
133
|
+
<ActivityIndicator size="large" color="#4B5563" />
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (isSignedIn && !instantAuth?.user) {
|
|
139
|
+
return (
|
|
140
|
+
<View style={styles.loadingContainer}>
|
|
141
|
+
<ActivityIndicator size="large" color="#4B5563" />
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (syncError || instantAuth?.error) {
|
|
147
|
+
return (
|
|
148
|
+
<View style={styles.setupContainer}>
|
|
149
|
+
<View style={styles.setupCard}>
|
|
150
|
+
<Text style={styles.setupTitle}>Authentication Error</Text>
|
|
151
|
+
<Text style={styles.setupText}>
|
|
152
|
+
{syncError || instantAuth?.error?.message}
|
|
153
|
+
</Text>
|
|
154
|
+
<Text style={styles.setupStep}>
|
|
155
|
+
Ensure Clerk session token includes email and email_verified claims.
|
|
156
|
+
</Text>
|
|
157
|
+
</View>
|
|
158
|
+
</View>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return <>{children}</>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
166
|
+
if (!publishableKey) {
|
|
167
|
+
return <SetupRequired />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
|
|
172
|
+
<ClerkLoaded>
|
|
173
|
+
<InstantSessionBridge>{children}</InstantSessionBridge>
|
|
174
|
+
</ClerkLoaded>
|
|
175
|
+
</ClerkProvider>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const styles = StyleSheet.create({
|
|
180
|
+
setupContainer: {
|
|
181
|
+
flex: 1,
|
|
182
|
+
backgroundColor: "#f5f5f5",
|
|
183
|
+
justifyContent: "center",
|
|
184
|
+
alignItems: "center",
|
|
185
|
+
padding: 20,
|
|
186
|
+
},
|
|
187
|
+
setupCard: {
|
|
188
|
+
backgroundColor: "#fff",
|
|
189
|
+
borderRadius: 12,
|
|
190
|
+
padding: 24,
|
|
191
|
+
maxWidth: 420,
|
|
192
|
+
width: "100%",
|
|
193
|
+
shadowColor: "#000",
|
|
194
|
+
shadowOffset: { width: 0, height: 2 },
|
|
195
|
+
shadowOpacity: 0.1,
|
|
196
|
+
shadowRadius: 8,
|
|
197
|
+
elevation: 3,
|
|
198
|
+
},
|
|
199
|
+
setupTitle: {
|
|
200
|
+
fontSize: 24,
|
|
201
|
+
fontWeight: "700",
|
|
202
|
+
color: "#333",
|
|
203
|
+
marginBottom: 16,
|
|
204
|
+
textAlign: "center",
|
|
205
|
+
},
|
|
206
|
+
setupText: {
|
|
207
|
+
fontSize: 16,
|
|
208
|
+
color: "#666",
|
|
209
|
+
marginBottom: 12,
|
|
210
|
+
lineHeight: 22,
|
|
211
|
+
textAlign: "center",
|
|
212
|
+
},
|
|
213
|
+
setupStep: {
|
|
214
|
+
fontSize: 14,
|
|
215
|
+
color: "#888",
|
|
216
|
+
marginTop: 16,
|
|
217
|
+
padding: 16,
|
|
218
|
+
backgroundColor: "#f8f9fa",
|
|
219
|
+
borderRadius: 8,
|
|
220
|
+
lineHeight: 20,
|
|
221
|
+
},
|
|
222
|
+
loadingContainer: {
|
|
223
|
+
flex: 1,
|
|
224
|
+
alignItems: "center",
|
|
225
|
+
justifyContent: "center",
|
|
226
|
+
backgroundColor: "#FFFFFF",
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
@@ -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,93 @@ 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";
|
|
22
|
+
|
|
23
|
+
function isSignedOutError(error: unknown): boolean {
|
|
24
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
25
|
+
return message.toLowerCase().includes("signed out");
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
export default function SettingsScreen() {
|
|
29
|
+
const { signOut } = useAuth();
|
|
30
|
+
const { user, isLoaded } = useUser();
|
|
31
|
+
const [isSigningOut, setIsSigningOut] = React.useState(false);
|
|
32
|
+
const [imageLoadError, setImageLoadError] = React.useState(false);
|
|
10
33
|
const instantAppId = process.env.EXPO_PUBLIC_INSTANT_APP_ID;
|
|
11
34
|
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
setImageLoadError(false);
|
|
37
|
+
}, [user?.imageUrl]);
|
|
38
|
+
|
|
39
|
+
const performSignOut = async () => {
|
|
40
|
+
setIsSigningOut(true);
|
|
41
|
+
try {
|
|
42
|
+
try {
|
|
43
|
+
await instantDb?.auth.signOut();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!isSignedOutError(error)) {
|
|
46
|
+
console.warn("Instant signOut warning:", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await Promise.all([
|
|
52
|
+
SecureStore.deleteItemAsync("__clerk_client_jwt"),
|
|
53
|
+
SecureStore.deleteItemAsync("__clerk_db_jwt"),
|
|
54
|
+
SecureStore.deleteItemAsync("__clerk_session_jwt"),
|
|
55
|
+
]);
|
|
56
|
+
} catch (storageError) {
|
|
57
|
+
console.warn("SecureStore cleanup failed:", storageError);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await signOut();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!isSignedOutError(error)) {
|
|
63
|
+
console.warn("Sign out flow warning:", error);
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
setIsSigningOut(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleSignOut = async () => {
|
|
71
|
+
if (Platform.OS === "web") {
|
|
72
|
+
const confirmed = window.confirm("Are you sure you want to sign out?");
|
|
73
|
+
if (confirmed) await performSignOut();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
|
|
78
|
+
{ text: "Cancel", style: "cancel" },
|
|
79
|
+
{ text: "Sign Out", style: "destructive", onPress: performSignOut },
|
|
80
|
+
]);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (!isLoaded) {
|
|
84
|
+
return (
|
|
85
|
+
<SafeAreaView style={styles.container}>
|
|
86
|
+
<View style={styles.loadingContainer}>
|
|
87
|
+
<ActivityIndicator size="large" color="#4B5563" />
|
|
88
|
+
</View>
|
|
89
|
+
</SafeAreaView>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
12
93
|
return (
|
|
13
94
|
<SafeAreaView style={styles.container}>
|
|
14
95
|
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
|
@@ -17,9 +98,40 @@ export default function SettingsScreen() {
|
|
|
17
98
|
</View>
|
|
18
99
|
|
|
19
100
|
<View style={styles.content}>
|
|
101
|
+
<View style={styles.card}>
|
|
102
|
+
<Text style={styles.sectionTitle}>Profile</Text>
|
|
103
|
+
<View style={styles.profileRow}>
|
|
104
|
+
<View style={styles.avatarWrap}>
|
|
105
|
+
{user?.imageUrl && !imageLoadError ? (
|
|
106
|
+
<Image
|
|
107
|
+
source={{ uri: user.imageUrl }}
|
|
108
|
+
style={styles.avatar}
|
|
109
|
+
onError={() => setImageLoadError(true)}
|
|
110
|
+
/>
|
|
111
|
+
) : (
|
|
112
|
+
<User size={28} color="#6B7280" />
|
|
113
|
+
)}
|
|
114
|
+
</View>
|
|
115
|
+
<View style={styles.profileTextWrap}>
|
|
116
|
+
<Text style={styles.profileName}>
|
|
117
|
+
{user?.fullName ||
|
|
118
|
+
\`\${user?.firstName || ""} \${user?.lastName || ""}\`.trim() ||
|
|
119
|
+
user?.username ||
|
|
120
|
+
"User"}
|
|
121
|
+
</Text>
|
|
122
|
+
<View style={styles.emailRow}>
|
|
123
|
+
<Mail size={14} color="#9CA3AF" />
|
|
124
|
+
<Text style={styles.profileEmail}>
|
|
125
|
+
{user?.primaryEmailAddress?.emailAddress || "No email"}
|
|
126
|
+
</Text>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
|
|
20
132
|
<View style={styles.card}>
|
|
21
133
|
<Text style={styles.sectionTitle}>Backend</Text>
|
|
22
|
-
<Text style={styles.sectionText}>InstantDB</Text>
|
|
134
|
+
<Text style={styles.sectionText}>InstantDB + Clerk</Text>
|
|
23
135
|
</View>
|
|
24
136
|
|
|
25
137
|
<View style={styles.card}>
|
|
@@ -32,8 +144,25 @@ export default function SettingsScreen() {
|
|
|
32
144
|
<View style={styles.card}>
|
|
33
145
|
<Text style={styles.sectionTitle}>About</Text>
|
|
34
146
|
<Text style={styles.sectionText}>Version 1.0.0</Text>
|
|
35
|
-
<Text style={styles.sectionText}>Built with Expo + InstantDB</Text>
|
|
147
|
+
<Text style={styles.sectionText}>Built with Expo + InstantDB + Clerk</Text>
|
|
36
148
|
</View>
|
|
149
|
+
|
|
150
|
+
<TouchableOpacity
|
|
151
|
+
style={[styles.signOutButton, isSigningOut && styles.signOutButtonDisabled]}
|
|
152
|
+
onPress={() => {
|
|
153
|
+
if (!isSigningOut) handleSignOut();
|
|
154
|
+
}}
|
|
155
|
+
activeOpacity={0.75}
|
|
156
|
+
>
|
|
157
|
+
{isSigningOut ? (
|
|
158
|
+
<ActivityIndicator size={18} color="#EF4444" />
|
|
159
|
+
) : (
|
|
160
|
+
<LogOut size={18} color="#EF4444" />
|
|
161
|
+
)}
|
|
162
|
+
<Text style={styles.signOutText}>
|
|
163
|
+
{isSigningOut ? "Signing Out..." : "Sign Out"}
|
|
164
|
+
</Text>
|
|
165
|
+
</TouchableOpacity>
|
|
37
166
|
</View>
|
|
38
167
|
</SafeAreaView>
|
|
39
168
|
);
|
|
@@ -44,6 +173,11 @@ const styles = StyleSheet.create({
|
|
|
44
173
|
flex: 1,
|
|
45
174
|
backgroundColor: "#FFFFFF",
|
|
46
175
|
},
|
|
176
|
+
loadingContainer: {
|
|
177
|
+
flex: 1,
|
|
178
|
+
justifyContent: "center",
|
|
179
|
+
alignItems: "center",
|
|
180
|
+
},
|
|
47
181
|
header: {
|
|
48
182
|
paddingHorizontal: 16,
|
|
49
183
|
paddingTop: 24,
|
|
@@ -78,6 +212,62 @@ const styles = StyleSheet.create({
|
|
|
78
212
|
marginBottom: 4,
|
|
79
213
|
lineHeight: 22,
|
|
80
214
|
},
|
|
215
|
+
profileRow: {
|
|
216
|
+
flexDirection: "row",
|
|
217
|
+
alignItems: "center",
|
|
218
|
+
},
|
|
219
|
+
avatarWrap: {
|
|
220
|
+
width: 54,
|
|
221
|
+
height: 54,
|
|
222
|
+
borderRadius: 27,
|
|
223
|
+
backgroundColor: "#E5E7EB",
|
|
224
|
+
alignItems: "center",
|
|
225
|
+
justifyContent: "center",
|
|
226
|
+
marginRight: 12,
|
|
227
|
+
},
|
|
228
|
+
avatar: {
|
|
229
|
+
width: 54,
|
|
230
|
+
height: 54,
|
|
231
|
+
borderRadius: 27,
|
|
232
|
+
},
|
|
233
|
+
profileTextWrap: {
|
|
234
|
+
flex: 1,
|
|
235
|
+
},
|
|
236
|
+
profileName: {
|
|
237
|
+
fontSize: 17,
|
|
238
|
+
fontWeight: "600",
|
|
239
|
+
color: "#111827",
|
|
240
|
+
marginBottom: 4,
|
|
241
|
+
},
|
|
242
|
+
emailRow: {
|
|
243
|
+
flexDirection: "row",
|
|
244
|
+
alignItems: "center",
|
|
245
|
+
},
|
|
246
|
+
profileEmail: {
|
|
247
|
+
marginLeft: 6,
|
|
248
|
+
color: "#6B7280",
|
|
249
|
+
fontSize: 14,
|
|
250
|
+
},
|
|
251
|
+
signOutButton: {
|
|
252
|
+
marginTop: 4,
|
|
253
|
+
flexDirection: "row",
|
|
254
|
+
alignItems: "center",
|
|
255
|
+
justifyContent: "center",
|
|
256
|
+
minHeight: 48,
|
|
257
|
+
borderRadius: 12,
|
|
258
|
+
backgroundColor: "#FEF2F2",
|
|
259
|
+
borderWidth: 1,
|
|
260
|
+
borderColor: "#FECACA",
|
|
261
|
+
gap: 8,
|
|
262
|
+
},
|
|
263
|
+
signOutButtonDisabled: {
|
|
264
|
+
opacity: 0.65,
|
|
265
|
+
},
|
|
266
|
+
signOutText: {
|
|
267
|
+
color: "#EF4444",
|
|
268
|
+
fontSize: 15,
|
|
269
|
+
fontWeight: "600",
|
|
270
|
+
},
|
|
81
271
|
});
|
|
82
272
|
`;
|
|
83
273
|
}
|
|
@@ -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={{
|