create-react-native-airborne 0.0.1

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.
Files changed (78) hide show
  1. package/README.md +24 -0
  2. package/package.json +21 -0
  3. package/src/index.mjs +103 -0
  4. package/template/.agents/skills/convex-best-practices/SKILL.md +333 -0
  5. package/template/.agents/skills/convex-file-storage/SKILL.md +466 -0
  6. package/template/.agents/skills/convex-security-audit/SKILL.md +538 -0
  7. package/template/.agents/skills/convex-security-check/SKILL.md +377 -0
  8. package/template/.github/workflows/ci.yml +130 -0
  9. package/template/.prettierignore +8 -0
  10. package/template/.prettierrc.json +6 -0
  11. package/template/AGENTS.md +156 -0
  12. package/template/Justfile +48 -0
  13. package/template/README.md +94 -0
  14. package/template/client/.env.example +3 -0
  15. package/template/client/.vscode/extensions.json +1 -0
  16. package/template/client/.vscode/settings.json +7 -0
  17. package/template/client/README.md +33 -0
  18. package/template/client/app/(app)/_layout.tsx +34 -0
  19. package/template/client/app/(app)/index.tsx +66 -0
  20. package/template/client/app/(app)/push.tsx +75 -0
  21. package/template/client/app/(app)/settings.tsx +36 -0
  22. package/template/client/app/(auth)/_layout.tsx +22 -0
  23. package/template/client/app/(auth)/sign-in.tsx +358 -0
  24. package/template/client/app/(auth)/sign-up.tsx +237 -0
  25. package/template/client/app/_layout.tsx +30 -0
  26. package/template/client/app/index.tsx +127 -0
  27. package/template/client/app.config.ts +30 -0
  28. package/template/client/assets/images/android-icon-background.png +0 -0
  29. package/template/client/assets/images/android-icon-foreground.png +0 -0
  30. package/template/client/assets/images/android-icon-monochrome.png +0 -0
  31. package/template/client/assets/images/favicon.png +0 -0
  32. package/template/client/assets/images/icon.png +0 -0
  33. package/template/client/assets/images/partial-react-logo.png +0 -0
  34. package/template/client/assets/images/react-logo.png +0 -0
  35. package/template/client/assets/images/react-logo@2x.png +0 -0
  36. package/template/client/assets/images/react-logo@3x.png +0 -0
  37. package/template/client/assets/images/splash-icon.png +0 -0
  38. package/template/client/eslint.config.js +10 -0
  39. package/template/client/global.css +2 -0
  40. package/template/client/metro.config.js +9 -0
  41. package/template/client/package.json +51 -0
  42. package/template/client/src/components/auth-shell.tsx +63 -0
  43. package/template/client/src/components/form-input.tsx +62 -0
  44. package/template/client/src/components/primary-button.tsx +37 -0
  45. package/template/client/src/components/screen.tsx +17 -0
  46. package/template/client/src/components/sign-out-button.tsx +32 -0
  47. package/template/client/src/hooks/use-theme-sync.ts +11 -0
  48. package/template/client/src/lib/convex.ts +6 -0
  49. package/template/client/src/lib/env-schema.ts +13 -0
  50. package/template/client/src/lib/env.test.ts +24 -0
  51. package/template/client/src/lib/env.ts +19 -0
  52. package/template/client/src/lib/notifications.ts +47 -0
  53. package/template/client/src/store/preferences-store.ts +42 -0
  54. package/template/client/src/types/theme.ts +1 -0
  55. package/template/client/tsconfig.json +18 -0
  56. package/template/client/uniwind-types.d.ts +10 -0
  57. package/template/client/vitest.config.ts +7 -0
  58. package/template/package.json +22 -0
  59. package/template/server/.env.example +8 -0
  60. package/template/server/README.md +31 -0
  61. package/template/server/convex/_generated/api.d.ts +55 -0
  62. package/template/server/convex/_generated/api.js +23 -0
  63. package/template/server/convex/_generated/dataModel.d.ts +60 -0
  64. package/template/server/convex/_generated/server.d.ts +143 -0
  65. package/template/server/convex/_generated/server.js +93 -0
  66. package/template/server/convex/auth.config.ts +11 -0
  67. package/template/server/convex/env.ts +18 -0
  68. package/template/server/convex/lib.ts +12 -0
  69. package/template/server/convex/push.ts +148 -0
  70. package/template/server/convex/schema.ts +22 -0
  71. package/template/server/convex/users.ts +54 -0
  72. package/template/server/convex.json +3 -0
  73. package/template/server/eslint.config.js +51 -0
  74. package/template/server/package.json +29 -0
  75. package/template/server/tests/convex.test.ts +52 -0
  76. package/template/server/tests/import-meta.d.ts +3 -0
  77. package/template/server/tsconfig.json +15 -0
  78. package/template/server/vitest.config.ts +13 -0
@@ -0,0 +1,358 @@
1
+ import { useAuth, useSignIn } from "@clerk/clerk-expo";
2
+ import { Link, Redirect, useRouter } from "expo-router";
3
+ import { useState } from "react";
4
+ import { Text, View } from "react-native";
5
+ import { AuthShell } from "@/src/components/auth-shell";
6
+ import { FormInput } from "@/src/components/form-input";
7
+ import { PrimaryButton } from "@/src/components/primary-button";
8
+
9
+ type ClerkError = {
10
+ errors?: { code?: string; longMessage?: string; message?: string }[];
11
+ };
12
+
13
+ function getClerkErrorMessage(error: unknown, fallback: string) {
14
+ const clerkError = error as ClerkError;
15
+ return clerkError.errors?.[0]?.longMessage ?? clerkError.errors?.[0]?.message ?? fallback;
16
+ }
17
+
18
+ function getClerkErrorCode(error: unknown) {
19
+ const clerkError = error as ClerkError;
20
+ return clerkError.errors?.[0]?.code?.toLowerCase() ?? null;
21
+ }
22
+
23
+ function messageIndicatesSignedIn(message: string) {
24
+ return message.toLowerCase().includes("already signed in");
25
+ }
26
+
27
+ export default function SignInScreen() {
28
+ const { isLoaded: isAuthLoaded, isSignedIn } = useAuth();
29
+ const { signIn, setActive, isLoaded } = useSignIn();
30
+ const router = useRouter();
31
+
32
+ const [emailAddress, setEmailAddress] = useState("");
33
+ const [password, setPassword] = useState("");
34
+ const [code, setCode] = useState("");
35
+ const [showEmailCode, setShowEmailCode] = useState(false);
36
+ const [error, setError] = useState<string | null>(null);
37
+ const [submitting, setSubmitting] = useState(false);
38
+
39
+ if (!isAuthLoaded) {
40
+ return null;
41
+ }
42
+
43
+ if (isAuthLoaded && isSignedIn) {
44
+ return <Redirect href="/" />;
45
+ }
46
+
47
+ const activateSession = async (createdSessionId: string | null) => {
48
+ if (!setActive || !createdSessionId) {
49
+ setError("Sign-in completed, but no session could be activated.");
50
+ return;
51
+ }
52
+
53
+ await setActive({ session: createdSessionId });
54
+ router.replace("/");
55
+ };
56
+
57
+ const beginEmailCodeSecondFactor = async (
58
+ supportedSecondFactors:
59
+ | {
60
+ strategy: string;
61
+ emailAddressId?: string;
62
+ }[]
63
+ | null
64
+ | undefined,
65
+ ) => {
66
+ if (!signIn) {
67
+ return false;
68
+ }
69
+
70
+ const emailCodeFactor = supportedSecondFactors?.find(
71
+ (factor) => factor.strategy === "email_code" && Boolean(factor.emailAddressId),
72
+ );
73
+
74
+ if (!emailCodeFactor?.emailAddressId) {
75
+ return false;
76
+ }
77
+
78
+ await signIn.prepareSecondFactor({
79
+ strategy: "email_code",
80
+ emailAddressId: emailCodeFactor.emailAddressId,
81
+ });
82
+ setShowEmailCode(true);
83
+ return true;
84
+ };
85
+
86
+ const onSignInPress = async () => {
87
+ if (!isLoaded || !setActive || !signIn) {
88
+ return;
89
+ }
90
+
91
+ if (isSignedIn) {
92
+ router.replace("/");
93
+ return;
94
+ }
95
+
96
+ setSubmitting(true);
97
+ setError(null);
98
+
99
+ try {
100
+ const identifier = emailAddress.trim();
101
+ let signInAttempt: Awaited<ReturnType<typeof signIn.create>>;
102
+
103
+ try {
104
+ signInAttempt = await signIn.create({
105
+ strategy: "password",
106
+ identifier,
107
+ password,
108
+ });
109
+ } catch (createErr) {
110
+ const createCode = getClerkErrorCode(createErr);
111
+ getClerkErrorMessage(createErr, "Unable to start password sign-in.");
112
+
113
+ if (createCode !== "form_param_format_invalid") {
114
+ throw createErr;
115
+ }
116
+
117
+ signInAttempt = await signIn.create({
118
+ identifier,
119
+ });
120
+ }
121
+
122
+ const firstFactorStrategies = signInAttempt.supportedFirstFactors?.map(
123
+ (factor) => factor.strategy,
124
+ );
125
+ const secondFactors = signInAttempt.supportedSecondFactors?.map((factor) => ({
126
+ strategy: factor.strategy,
127
+ emailAddressId: "emailAddressId" in factor ? factor.emailAddressId : undefined,
128
+ }));
129
+
130
+ if (signInAttempt.status === "complete") {
131
+ await activateSession(signInAttempt.createdSessionId);
132
+ return;
133
+ }
134
+
135
+ if (signInAttempt.status === "needs_first_factor") {
136
+ const hasPasswordFactor = firstFactorStrategies?.includes("password");
137
+
138
+ if (!hasPasswordFactor) {
139
+ setError(
140
+ "Password sign-in is not enabled for this Clerk app. Enable Password sign-in in Clerk Dashboard.",
141
+ );
142
+ return;
143
+ }
144
+
145
+ const passwordAttempt = await signIn.attemptFirstFactor({
146
+ strategy: "password",
147
+ password,
148
+ });
149
+ const passwordAttemptSecondFactors = passwordAttempt.supportedSecondFactors?.map(
150
+ (factor) => ({
151
+ strategy: factor.strategy,
152
+ emailAddressId: "emailAddressId" in factor ? factor.emailAddressId : undefined,
153
+ }),
154
+ );
155
+
156
+ if (passwordAttempt.status === "complete") {
157
+ await activateSession(passwordAttempt.createdSessionId);
158
+ return;
159
+ }
160
+
161
+ if (passwordAttempt.status === "needs_second_factor") {
162
+ const startedSecondFactor = await beginEmailCodeSecondFactor(
163
+ passwordAttemptSecondFactors,
164
+ );
165
+
166
+ if (startedSecondFactor) {
167
+ return;
168
+ }
169
+
170
+ setError(
171
+ "Second-factor authentication is required, but email code is not enabled for this account.",
172
+ );
173
+ return;
174
+ }
175
+
176
+ setError(
177
+ `Password verification did not complete sign-in (${passwordAttempt.status ?? "unknown"}).`,
178
+ );
179
+ return;
180
+ }
181
+
182
+ if (signInAttempt.status === "needs_second_factor") {
183
+ const startedSecondFactor = await beginEmailCodeSecondFactor(secondFactors);
184
+
185
+ if (startedSecondFactor) {
186
+ return;
187
+ }
188
+
189
+ setError(
190
+ "Second-factor authentication is required, but email code is not enabled for this account.",
191
+ );
192
+ return;
193
+ }
194
+
195
+ if (signInAttempt.status === "needs_new_password") {
196
+ setError("This account requires a password reset before sign-in.");
197
+ return;
198
+ }
199
+
200
+ setError(`Sign-in requires an unsupported step: ${signInAttempt.status ?? "unknown"}.`);
201
+ } catch (err) {
202
+ const code = getClerkErrorCode(err);
203
+ const message = getClerkErrorMessage(
204
+ err,
205
+ "Unable to sign in. Check your credentials and Clerk setup.",
206
+ );
207
+
208
+ if (messageIndicatesSignedIn(message)) {
209
+ router.replace("/");
210
+ return;
211
+ }
212
+
213
+ if (code?.includes("identifier") || message.toLowerCase().includes("invalid identifier")) {
214
+ setError(
215
+ "Invalid identifier. Verify you are using the exact email used at sign-up, and ensure Email is enabled as a sign-in identifier in Clerk Dashboard.",
216
+ );
217
+ return;
218
+ }
219
+
220
+ setError(message);
221
+ } finally {
222
+ setSubmitting(false);
223
+ }
224
+ };
225
+
226
+ const onVerifyPress = async () => {
227
+ if (!isLoaded || !setActive || !signIn) {
228
+ return;
229
+ }
230
+
231
+ if (isSignedIn) {
232
+ router.replace("/");
233
+ return;
234
+ }
235
+
236
+ setSubmitting(true);
237
+ setError(null);
238
+
239
+ try {
240
+ const signInAttempt = await signIn.attemptSecondFactor({
241
+ strategy: "email_code",
242
+ code: code.trim(),
243
+ });
244
+
245
+ if (signInAttempt.status === "complete") {
246
+ await activateSession(signInAttempt.createdSessionId);
247
+ return;
248
+ }
249
+
250
+ setError(`Verification is not complete yet (${signInAttempt.status ?? "unknown"}).`);
251
+ } catch (err) {
252
+ const message = getClerkErrorMessage(err, "Invalid verification code.");
253
+ setError(message);
254
+ } finally {
255
+ setSubmitting(false);
256
+ }
257
+ };
258
+
259
+ if (showEmailCode) {
260
+ return (
261
+ <AuthShell
262
+ badge="Two-step verification"
263
+ title="Check your inbox"
264
+ subtitle="Enter the 6-digit code we sent to your email to finish signing in."
265
+ footer={
266
+ <View className="flex-row items-center gap-1">
267
+ <Text className="text-sm text-zinc-600 dark:text-zinc-300">
268
+ Need a different account?
269
+ </Text>
270
+ <Link
271
+ href="/(auth)/sign-in"
272
+ className="text-sm font-semibold text-sky-600 dark:text-sky-400"
273
+ >
274
+ Start over
275
+ </Link>
276
+ </View>
277
+ }
278
+ >
279
+ <FormInput
280
+ label="Verification code"
281
+ value={code}
282
+ onChangeText={setCode}
283
+ placeholder="123456"
284
+ keyboardType="number-pad"
285
+ autoComplete="one-time-code"
286
+ textContentType="oneTimeCode"
287
+ />
288
+
289
+ {error ? (
290
+ <Text className="rounded-xl bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-200">
291
+ {error}
292
+ </Text>
293
+ ) : null}
294
+
295
+ <PrimaryButton
296
+ onPress={onVerifyPress}
297
+ disabled={submitting || !isLoaded || code.length === 0}
298
+ >
299
+ {submitting ? "Verifying..." : "Verify and continue"}
300
+ </PrimaryButton>
301
+ </AuthShell>
302
+ );
303
+ }
304
+
305
+ return (
306
+ <AuthShell
307
+ badge="Welcome back"
308
+ title="Sign in"
309
+ subtitle="Continue building with your existing account."
310
+ footer={
311
+ <View className="flex-row items-center gap-1">
312
+ <Text className="text-sm text-zinc-600 dark:text-zinc-300">
313
+ Don&apos;t have an account?
314
+ </Text>
315
+ <Link
316
+ href="/(auth)/sign-up"
317
+ className="text-sm font-semibold text-sky-600 dark:text-sky-400"
318
+ >
319
+ Create one
320
+ </Link>
321
+ </View>
322
+ }
323
+ >
324
+ <FormInput
325
+ label="Email address"
326
+ value={emailAddress}
327
+ onChangeText={setEmailAddress}
328
+ placeholder="you@example.com"
329
+ keyboardType="email-address"
330
+ autoComplete="email"
331
+ textContentType="emailAddress"
332
+ />
333
+
334
+ <FormInput
335
+ label="Password"
336
+ value={password}
337
+ onChangeText={setPassword}
338
+ secureTextEntry
339
+ placeholder="••••••••"
340
+ autoComplete="password"
341
+ textContentType="password"
342
+ />
343
+
344
+ {error ? (
345
+ <Text className="rounded-xl bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-200">
346
+ {error}
347
+ </Text>
348
+ ) : null}
349
+
350
+ <PrimaryButton
351
+ onPress={onSignInPress}
352
+ disabled={submitting || !isLoaded || !emailAddress || !password}
353
+ >
354
+ {submitting ? "Signing in..." : "Sign in"}
355
+ </PrimaryButton>
356
+ </AuthShell>
357
+ );
358
+ }
@@ -0,0 +1,237 @@
1
+ import { useAuth, useSignUp } from "@clerk/clerk-expo";
2
+ import { Link, Redirect, useRouter } from "expo-router";
3
+ import { useState } from "react";
4
+ import { Text, View } from "react-native";
5
+ import { AuthShell } from "@/src/components/auth-shell";
6
+ import { FormInput } from "@/src/components/form-input";
7
+ import { PrimaryButton } from "@/src/components/primary-button";
8
+
9
+ type ClerkError = {
10
+ errors?: { longMessage?: string; message?: string }[];
11
+ };
12
+
13
+ function getClerkErrorMessage(error: unknown, fallback: string) {
14
+ const clerkError = error as ClerkError;
15
+ return clerkError.errors?.[0]?.longMessage ?? clerkError.errors?.[0]?.message ?? fallback;
16
+ }
17
+
18
+ function messageIndicatesSignedIn(message: string) {
19
+ return message.toLowerCase().includes("already signed in");
20
+ }
21
+
22
+ export default function SignUpScreen() {
23
+ const { isLoaded: isAuthLoaded, isSignedIn } = useAuth();
24
+ const { isLoaded, signUp, setActive } = useSignUp();
25
+ const router = useRouter();
26
+
27
+ const [emailAddress, setEmailAddress] = useState("");
28
+ const [password, setPassword] = useState("");
29
+ const [pendingVerification, setPendingVerification] = useState(false);
30
+ const [code, setCode] = useState("");
31
+ const [submitting, setSubmitting] = useState(false);
32
+ const [error, setError] = useState<string | null>(null);
33
+
34
+ if (!isAuthLoaded) {
35
+ return null;
36
+ }
37
+
38
+ if (isAuthLoaded && isSignedIn) {
39
+ return <Redirect href="/" />;
40
+ }
41
+
42
+ const activateSession = async (createdSessionId: string | null) => {
43
+ if (!setActive || !createdSessionId) {
44
+ router.replace("/(auth)/sign-in");
45
+ return;
46
+ }
47
+
48
+ await setActive({ session: createdSessionId });
49
+ router.replace("/");
50
+ };
51
+
52
+ const onSignUpPress = async () => {
53
+ if (!isLoaded || !signUp) {
54
+ return;
55
+ }
56
+
57
+ if (isSignedIn) {
58
+ router.replace("/");
59
+ return;
60
+ }
61
+
62
+ setSubmitting(true);
63
+ setError(null);
64
+
65
+ try {
66
+ const signUpAttempt = await signUp.create({
67
+ emailAddress: emailAddress.trim(),
68
+ password,
69
+ });
70
+
71
+ if (signUpAttempt.status === "complete") {
72
+ await activateSession(signUpAttempt.createdSessionId);
73
+ return;
74
+ }
75
+
76
+ if (signUpAttempt.status === "missing_requirements") {
77
+ const needsEmailVerification = signUpAttempt.unverifiedFields.includes("email_address");
78
+
79
+ if (needsEmailVerification) {
80
+ await signUpAttempt.prepareEmailAddressVerification({ strategy: "email_code" });
81
+ setPendingVerification(true);
82
+ return;
83
+ }
84
+
85
+ setError(
86
+ "Account created, but additional sign-up requirements are enabled in Clerk. Check your Clerk settings.",
87
+ );
88
+ return;
89
+ }
90
+
91
+ setError("Sign-up was abandoned. Please try again.");
92
+ } catch (err) {
93
+ const message = getClerkErrorMessage(
94
+ err,
95
+ "Unable to sign up. Check your Clerk configuration.",
96
+ );
97
+
98
+ if (messageIndicatesSignedIn(message)) {
99
+ router.replace("/");
100
+ return;
101
+ }
102
+
103
+ setError(message);
104
+ } finally {
105
+ setSubmitting(false);
106
+ }
107
+ };
108
+
109
+ const onVerifyPress = async () => {
110
+ if (!isLoaded || !signUp) {
111
+ return;
112
+ }
113
+
114
+ if (isSignedIn) {
115
+ router.replace("/");
116
+ return;
117
+ }
118
+
119
+ setSubmitting(true);
120
+ setError(null);
121
+
122
+ try {
123
+ const signUpAttempt = await signUp.attemptEmailAddressVerification({
124
+ code: code.trim(),
125
+ });
126
+
127
+ if (signUpAttempt.status === "complete") {
128
+ await activateSession(signUpAttempt.createdSessionId);
129
+ return;
130
+ }
131
+
132
+ setError("Verification is not complete yet. Check the code and try again.");
133
+ } catch (err) {
134
+ const message = getClerkErrorMessage(err, "Invalid verification code.");
135
+ setError(message);
136
+ } finally {
137
+ setSubmitting(false);
138
+ }
139
+ };
140
+
141
+ if (pendingVerification) {
142
+ return (
143
+ <AuthShell
144
+ badge="Almost done"
145
+ title="Verify your email"
146
+ subtitle="Enter the code from your inbox to activate your account."
147
+ footer={
148
+ <View className="flex-row items-center gap-1">
149
+ <Text className="text-sm text-zinc-600 dark:text-zinc-300">Already verified?</Text>
150
+ <Link
151
+ href="/(auth)/sign-in"
152
+ className="text-sm font-semibold text-sky-600 dark:text-sky-400"
153
+ >
154
+ Sign in
155
+ </Link>
156
+ </View>
157
+ }
158
+ >
159
+ <FormInput
160
+ label="Verification code"
161
+ value={code}
162
+ onChangeText={setCode}
163
+ placeholder="123456"
164
+ keyboardType="number-pad"
165
+ autoComplete="one-time-code"
166
+ textContentType="oneTimeCode"
167
+ />
168
+
169
+ {error ? (
170
+ <Text className="rounded-xl bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-200">
171
+ {error}
172
+ </Text>
173
+ ) : null}
174
+
175
+ <PrimaryButton
176
+ onPress={onVerifyPress}
177
+ disabled={submitting || !isLoaded || code.length === 0}
178
+ >
179
+ {submitting ? "Verifying..." : "Verify and continue"}
180
+ </PrimaryButton>
181
+ </AuthShell>
182
+ );
183
+ }
184
+
185
+ return (
186
+ <AuthShell
187
+ badge="Get started"
188
+ title="Create your account"
189
+ subtitle="Use your email and a password. We'll send a verification code next."
190
+ footer={
191
+ <View className="flex-row items-center gap-1">
192
+ <Text className="text-sm text-zinc-600 dark:text-zinc-300">Already have an account?</Text>
193
+ <Link
194
+ href="/(auth)/sign-in"
195
+ className="text-sm font-semibold text-sky-600 dark:text-sky-400"
196
+ >
197
+ Sign in
198
+ </Link>
199
+ </View>
200
+ }
201
+ >
202
+ <FormInput
203
+ label="Email address"
204
+ value={emailAddress}
205
+ onChangeText={setEmailAddress}
206
+ placeholder="you@example.com"
207
+ keyboardType="email-address"
208
+ autoComplete="email"
209
+ textContentType="emailAddress"
210
+ />
211
+
212
+ <FormInput
213
+ label="Password"
214
+ value={password}
215
+ onChangeText={setPassword}
216
+ secureTextEntry
217
+ placeholder="••••••••"
218
+ autoComplete="password"
219
+ textContentType="password"
220
+ hint="Use at least 8 characters for stronger security."
221
+ />
222
+
223
+ {error ? (
224
+ <Text className="rounded-xl bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-200">
225
+ {error}
226
+ </Text>
227
+ ) : null}
228
+
229
+ <PrimaryButton
230
+ onPress={onSignUpPress}
231
+ disabled={submitting || !isLoaded || !emailAddress || !password}
232
+ >
233
+ {submitting ? "Creating account..." : "Continue"}
234
+ </PrimaryButton>
235
+ </AuthShell>
236
+ );
237
+ }
@@ -0,0 +1,30 @@
1
+ import "../global.css";
2
+ import "react-native-reanimated";
3
+ import { ClerkProvider, useAuth } from "@clerk/clerk-expo";
4
+ import { tokenCache } from "@clerk/clerk-expo/token-cache";
5
+ import { ConvexProviderWithClerk } from "convex/react-clerk";
6
+ import { Slot } from "expo-router";
7
+ import { SafeAreaProvider } from "react-native-safe-area-context";
8
+ import { useThemeSync } from "@/src/hooks/use-theme-sync";
9
+ import { convexClient } from "@/src/lib/convex";
10
+ import { getClientEnv } from "@/src/lib/env";
11
+
12
+ const env = getClientEnv();
13
+
14
+ function ThemeBootstrap() {
15
+ useThemeSync();
16
+ return null;
17
+ }
18
+
19
+ export default function RootLayout() {
20
+ return (
21
+ <ClerkProvider publishableKey={env.clerkPublishableKey} tokenCache={tokenCache}>
22
+ <ConvexProviderWithClerk client={convexClient} useAuth={useAuth}>
23
+ <SafeAreaProvider>
24
+ <ThemeBootstrap />
25
+ <Slot />
26
+ </SafeAreaProvider>
27
+ </ConvexProviderWithClerk>
28
+ </ClerkProvider>
29
+ );
30
+ }