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 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
- : ["@react-native-async-storage/async-storage", "@react-native-community/netinfo"];
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
- if (!appId && options.interactive) {
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
- prompts_1.log.info(picocolors_1.default.dim("Press Enter to skip and configure later."));
282
- const appIdInput = await (0, prompts_1.text)({
283
- message: "InstantDB App ID",
284
- placeholder: "your-instant-app-id",
285
- defaultValue: "",
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)(appIdInput) && appIdInput) {
288
- appId = appIdInput.trim();
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
- envContents = (0, dotenv_1.upsertDotenvVar)(envContents, "EXPO_PUBLIC_INSTANT_APP_ID", instant.appId);
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({
@@ -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 to connect InstantDB."));
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
- prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded with InstantDB!")));
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 (!instant.appId) {
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 first."));
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("/(tabs)");
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
  },
@@ -5,6 +5,10 @@ function envExampleTemplate(backend) {
5
5
  if (backend === "instantdb") {
6
6
  return `# InstantDB
7
7
  EXPO_PUBLIC_INSTANT_APP_ID=
8
+ EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME=clerk
9
+
10
+ # Clerk
11
+ EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=
8
12
  `;
9
13
  }
10
14
  return `# Convex
@@ -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 **InstantDB**.
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 \`EXPO_PUBLIC_INSTANT_APP_ID\` in \`.env.local\`. You can create an Instant app id via:
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 { View, Text, StyleSheet, StatusBar } from "react-native";
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={{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-100x-mobile",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Scaffold a full-stack mobile app with Expo + Convex + Clerk in seconds",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {