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.
- package/README.md +24 -0
- package/package.json +21 -0
- package/src/index.mjs +103 -0
- package/template/.agents/skills/convex-best-practices/SKILL.md +333 -0
- package/template/.agents/skills/convex-file-storage/SKILL.md +466 -0
- package/template/.agents/skills/convex-security-audit/SKILL.md +538 -0
- package/template/.agents/skills/convex-security-check/SKILL.md +377 -0
- package/template/.github/workflows/ci.yml +130 -0
- package/template/.prettierignore +8 -0
- package/template/.prettierrc.json +6 -0
- package/template/AGENTS.md +156 -0
- package/template/Justfile +48 -0
- package/template/README.md +94 -0
- package/template/client/.env.example +3 -0
- package/template/client/.vscode/extensions.json +1 -0
- package/template/client/.vscode/settings.json +7 -0
- package/template/client/README.md +33 -0
- package/template/client/app/(app)/_layout.tsx +34 -0
- package/template/client/app/(app)/index.tsx +66 -0
- package/template/client/app/(app)/push.tsx +75 -0
- package/template/client/app/(app)/settings.tsx +36 -0
- package/template/client/app/(auth)/_layout.tsx +22 -0
- package/template/client/app/(auth)/sign-in.tsx +358 -0
- package/template/client/app/(auth)/sign-up.tsx +237 -0
- package/template/client/app/_layout.tsx +30 -0
- package/template/client/app/index.tsx +127 -0
- package/template/client/app.config.ts +30 -0
- package/template/client/assets/images/android-icon-background.png +0 -0
- package/template/client/assets/images/android-icon-foreground.png +0 -0
- package/template/client/assets/images/android-icon-monochrome.png +0 -0
- package/template/client/assets/images/favicon.png +0 -0
- package/template/client/assets/images/icon.png +0 -0
- package/template/client/assets/images/partial-react-logo.png +0 -0
- package/template/client/assets/images/react-logo.png +0 -0
- package/template/client/assets/images/react-logo@2x.png +0 -0
- package/template/client/assets/images/react-logo@3x.png +0 -0
- package/template/client/assets/images/splash-icon.png +0 -0
- package/template/client/eslint.config.js +10 -0
- package/template/client/global.css +2 -0
- package/template/client/metro.config.js +9 -0
- package/template/client/package.json +51 -0
- package/template/client/src/components/auth-shell.tsx +63 -0
- package/template/client/src/components/form-input.tsx +62 -0
- package/template/client/src/components/primary-button.tsx +37 -0
- package/template/client/src/components/screen.tsx +17 -0
- package/template/client/src/components/sign-out-button.tsx +32 -0
- package/template/client/src/hooks/use-theme-sync.ts +11 -0
- package/template/client/src/lib/convex.ts +6 -0
- package/template/client/src/lib/env-schema.ts +13 -0
- package/template/client/src/lib/env.test.ts +24 -0
- package/template/client/src/lib/env.ts +19 -0
- package/template/client/src/lib/notifications.ts +47 -0
- package/template/client/src/store/preferences-store.ts +42 -0
- package/template/client/src/types/theme.ts +1 -0
- package/template/client/tsconfig.json +18 -0
- package/template/client/uniwind-types.d.ts +10 -0
- package/template/client/vitest.config.ts +7 -0
- package/template/package.json +22 -0
- package/template/server/.env.example +8 -0
- package/template/server/README.md +31 -0
- package/template/server/convex/_generated/api.d.ts +55 -0
- package/template/server/convex/_generated/api.js +23 -0
- package/template/server/convex/_generated/dataModel.d.ts +60 -0
- package/template/server/convex/_generated/server.d.ts +143 -0
- package/template/server/convex/_generated/server.js +93 -0
- package/template/server/convex/auth.config.ts +11 -0
- package/template/server/convex/env.ts +18 -0
- package/template/server/convex/lib.ts +12 -0
- package/template/server/convex/push.ts +148 -0
- package/template/server/convex/schema.ts +22 -0
- package/template/server/convex/users.ts +54 -0
- package/template/server/convex.json +3 -0
- package/template/server/eslint.config.js +51 -0
- package/template/server/package.json +29 -0
- package/template/server/tests/convex.test.ts +52 -0
- package/template/server/tests/import-meta.d.ts +3 -0
- package/template/server/tsconfig.json +15 -0
- 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'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
|
+
}
|