@yackey-labs/yauth-ui-solidjs 0.1.0

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.
@@ -0,0 +1,406 @@
1
+ import { type Component, createResource, createSignal, For } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export const ProfileSettings: Component = () => {
6
+ const { client, user, loading: userLoading } = useYAuth();
7
+
8
+ // Passkeys
9
+ const [passkeys, { refetch: refetchPasskeys }] = createResource(async () => {
10
+ try {
11
+ return await client.passkey.list();
12
+ } catch {
13
+ return [];
14
+ }
15
+ });
16
+ const [passkeyError, setPasskeyError] = createSignal<string | null>(null);
17
+ const [deletingPasskey, setDeletingPasskey] = createSignal<string | null>(
18
+ null,
19
+ );
20
+
21
+ // OAuth accounts
22
+ const [oauthAccounts, { refetch: refetchOAuth }] = createResource(
23
+ async () => {
24
+ try {
25
+ return await client.oauth.accounts();
26
+ } catch {
27
+ return [];
28
+ }
29
+ },
30
+ );
31
+ const [oauthError, setOauthError] = createSignal<string | null>(null);
32
+ const [unlinkingOAuth, setUnlinkingOAuth] = createSignal<string | null>(null);
33
+
34
+ // MFA state
35
+ const [mfaUri, setMfaUri] = createSignal("");
36
+ const [mfaSecret, setMfaSecret] = createSignal("");
37
+ const [mfaCode, setMfaCode] = createSignal("");
38
+ const [mfaBackupCodes, setMfaBackupCodes] = createSignal<string[]>([]);
39
+ const [mfaStep, setMfaStep] = createSignal<
40
+ "idle" | "setup" | "confirm" | "done"
41
+ >("idle");
42
+ const [mfaError, setMfaError] = createSignal<string | null>(null);
43
+ const [mfaLoading, setMfaLoading] = createSignal(false);
44
+
45
+ const handleDeletePasskey = async (id: string) => {
46
+ setPasskeyError(null);
47
+ setDeletingPasskey(id);
48
+
49
+ try {
50
+ await client.passkey.delete(id);
51
+ refetchPasskeys();
52
+ } catch (err) {
53
+ const error = err instanceof Error ? err : new Error(String(err));
54
+ setPasskeyError(error.message);
55
+ } finally {
56
+ setDeletingPasskey(null);
57
+ }
58
+ };
59
+
60
+ const handleUnlinkOAuth = async (provider: string) => {
61
+ setOauthError(null);
62
+ setUnlinkingOAuth(provider);
63
+
64
+ try {
65
+ await client.oauth.unlink(provider);
66
+ refetchOAuth();
67
+ } catch (err) {
68
+ const error = err instanceof Error ? err : new Error(String(err));
69
+ setOauthError(error.message);
70
+ } finally {
71
+ setUnlinkingOAuth(null);
72
+ }
73
+ };
74
+
75
+ const handleMfaBegin = async () => {
76
+ setMfaError(null);
77
+ setMfaLoading(true);
78
+
79
+ try {
80
+ const result = await client.mfa.setup();
81
+ setMfaUri(result.otpauth_url);
82
+ setMfaSecret(result.secret);
83
+ setMfaBackupCodes(result.backup_codes);
84
+ setMfaStep("confirm");
85
+ } catch (err) {
86
+ const error = err instanceof Error ? err : new Error(String(err));
87
+ setMfaError(error.message);
88
+ } finally {
89
+ setMfaLoading(false);
90
+ }
91
+ };
92
+
93
+ const handleMfaConfirm = async (e: SubmitEvent) => {
94
+ e.preventDefault();
95
+ setMfaError(null);
96
+ setMfaLoading(true);
97
+
98
+ try {
99
+ const form = e.currentTarget as HTMLFormElement;
100
+ const formData = new FormData(form);
101
+ await client.mfa.confirm(
102
+ (formData.get("mfa_code") as string) || mfaCode(),
103
+ );
104
+ setMfaStep("done");
105
+ } catch (err) {
106
+ const error = err instanceof Error ? err : new Error(String(err));
107
+ setMfaError(error.message);
108
+ } finally {
109
+ setMfaLoading(false);
110
+ }
111
+ };
112
+
113
+ const handleMfaDisable = async () => {
114
+ setMfaError(null);
115
+ setMfaLoading(true);
116
+
117
+ try {
118
+ await client.mfa.disable();
119
+ setMfaStep("idle");
120
+ setMfaUri("");
121
+ setMfaSecret("");
122
+ setMfaCode("");
123
+ setMfaBackupCodes([]);
124
+ } catch (err) {
125
+ const error = err instanceof Error ? err : new Error(String(err));
126
+ setMfaError(error.message);
127
+ } finally {
128
+ setMfaLoading(false);
129
+ }
130
+ };
131
+
132
+ return (
133
+ <div class="space-y-8">
134
+ <Show when={userLoading()}>
135
+ <div class="text-sm text-muted-foreground">Loading profile...</div>
136
+ </Show>
137
+
138
+ <Show when={user()}>
139
+ {(currentUser) => (
140
+ <>
141
+ {/* User info */}
142
+ <section class="space-y-4">
143
+ <h2 class="text-lg font-semibold tracking-tight">Profile</h2>
144
+ <dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
145
+ <dt class="font-medium text-muted-foreground">Email</dt>
146
+ <dd>{currentUser().email}</dd>
147
+
148
+ <Show when={currentUser().display_name}>
149
+ <dt class="font-medium text-muted-foreground">
150
+ Display name
151
+ </dt>
152
+ <dd>{currentUser().display_name}</dd>
153
+ </Show>
154
+
155
+ <dt class="font-medium text-muted-foreground">
156
+ Email verified
157
+ </dt>
158
+ <dd>{currentUser().email_verified ? "Yes" : "No"}</dd>
159
+
160
+ <dt class="font-medium text-muted-foreground">Role</dt>
161
+ <dd>{currentUser().role}</dd>
162
+ </dl>
163
+ </section>
164
+
165
+ {/* Passkeys */}
166
+ <section class="space-y-4">
167
+ <h2 class="text-lg font-semibold tracking-tight">Passkeys</h2>
168
+
169
+ <Show when={passkeyError()}>
170
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
171
+ {passkeyError()}
172
+ </div>
173
+ </Show>
174
+
175
+ <Show when={passkeys.loading}>
176
+ <div class="text-sm text-muted-foreground">
177
+ Loading passkeys...
178
+ </div>
179
+ </Show>
180
+
181
+ <Show when={passkeys()}>
182
+ {(passkeyList) => (
183
+ <Show
184
+ when={passkeyList().length > 0}
185
+ fallback={
186
+ <p class="text-sm text-muted-foreground">
187
+ No passkeys registered.
188
+ </p>
189
+ }
190
+ >
191
+ <ul class="space-y-2">
192
+ <For each={passkeyList()}>
193
+ {(passkey) => (
194
+ <li class="flex items-center justify-between rounded-md border border-input px-3 py-2">
195
+ <div class="space-y-0.5">
196
+ <span class="text-sm font-medium">
197
+ {passkey.name ?? "Unnamed passkey"}
198
+ </span>
199
+ <span class="block text-xs text-muted-foreground">
200
+ Added{" "}
201
+ {new Date(
202
+ passkey.created_at,
203
+ ).toLocaleDateString()}
204
+ </span>
205
+ </div>
206
+ <button
207
+ class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
208
+ type="button"
209
+ onClick={() => handleDeletePasskey(passkey.id)}
210
+ disabled={deletingPasskey() === passkey.id}
211
+ >
212
+ {deletingPasskey() === passkey.id
213
+ ? "Deleting..."
214
+ : "Delete"}
215
+ </button>
216
+ </li>
217
+ )}
218
+ </For>
219
+ </ul>
220
+ </Show>
221
+ )}
222
+ </Show>
223
+ </section>
224
+
225
+ {/* OAuth accounts */}
226
+ <section class="space-y-4">
227
+ <h2 class="text-lg font-semibold tracking-tight">
228
+ Connected accounts
229
+ </h2>
230
+
231
+ <Show when={oauthError()}>
232
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
233
+ {oauthError()}
234
+ </div>
235
+ </Show>
236
+
237
+ <Show when={oauthAccounts.loading}>
238
+ <div class="text-sm text-muted-foreground">
239
+ Loading accounts...
240
+ </div>
241
+ </Show>
242
+
243
+ <Show when={oauthAccounts()}>
244
+ {(accountList) => (
245
+ <Show
246
+ when={accountList().length > 0}
247
+ fallback={
248
+ <p class="text-sm text-muted-foreground">
249
+ No connected accounts.
250
+ </p>
251
+ }
252
+ >
253
+ <ul class="space-y-2">
254
+ <For each={accountList()}>
255
+ {(account) => (
256
+ <li class="flex items-center justify-between rounded-md border border-input px-3 py-2">
257
+ <div class="space-y-0.5">
258
+ <span class="text-sm font-medium">
259
+ {account.provider.charAt(0).toUpperCase() +
260
+ account.provider.slice(1)}
261
+ </span>
262
+ <span class="block text-xs text-muted-foreground">
263
+ Connected{" "}
264
+ {new Date(
265
+ account.created_at,
266
+ ).toLocaleDateString()}
267
+ </span>
268
+ </div>
269
+ <button
270
+ class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
271
+ type="button"
272
+ onClick={() =>
273
+ handleUnlinkOAuth(account.provider)
274
+ }
275
+ disabled={unlinkingOAuth() === account.provider}
276
+ >
277
+ {unlinkingOAuth() === account.provider
278
+ ? "Unlinking..."
279
+ : "Unlink"}
280
+ </button>
281
+ </li>
282
+ )}
283
+ </For>
284
+ </ul>
285
+ </Show>
286
+ )}
287
+ </Show>
288
+ </section>
289
+
290
+ {/* MFA setup */}
291
+ <section class="space-y-4">
292
+ <h2 class="text-lg font-semibold tracking-tight">
293
+ Two-factor authentication
294
+ </h2>
295
+
296
+ <Show when={mfaError()}>
297
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
298
+ {mfaError()}
299
+ </div>
300
+ </Show>
301
+
302
+ <Show when={mfaStep() === "idle"}>
303
+ <div class="flex gap-2">
304
+ <button
305
+ class="inline-flex h-9 cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
306
+ type="button"
307
+ onClick={handleMfaBegin}
308
+ disabled={mfaLoading()}
309
+ >
310
+ {mfaLoading() ? "Setting up..." : "Set up 2FA"}
311
+ </button>
312
+
313
+ <button
314
+ class="inline-flex h-9 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
315
+ type="button"
316
+ onClick={handleMfaDisable}
317
+ disabled={mfaLoading()}
318
+ >
319
+ {mfaLoading() ? "Disabling..." : "Disable 2FA"}
320
+ </button>
321
+ </div>
322
+ </Show>
323
+
324
+ <Show when={mfaStep() === "confirm"}>
325
+ <div class="space-y-4">
326
+ <p class="text-sm text-muted-foreground">
327
+ Add this account to your authenticator app, then enter the
328
+ verification code.
329
+ </p>
330
+
331
+ <div class="space-y-1">
332
+ <span class="text-sm font-medium leading-none">
333
+ OTP Auth URI
334
+ </span>
335
+ <code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs">
336
+ {mfaUri()}
337
+ </code>
338
+ </div>
339
+
340
+ <div class="space-y-1">
341
+ <span class="text-sm font-medium leading-none">
342
+ Manual entry key
343
+ </span>
344
+ <code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs font-mono tracking-wider">
345
+ {mfaSecret()}
346
+ </code>
347
+ </div>
348
+
349
+ <form class="space-y-4" onSubmit={handleMfaConfirm}>
350
+ <div class="space-y-2">
351
+ <label
352
+ class="text-sm font-medium leading-none"
353
+ for="yauth-profile-mfa-code"
354
+ >
355
+ Verification code
356
+ </label>
357
+ <input
358
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
359
+ id="yauth-profile-mfa-code"
360
+ name="mfa_code"
361
+ type="text"
362
+ inputmode="numeric"
363
+ autocomplete="one-time-code"
364
+ value={mfaCode()}
365
+ onInput={(e) => setMfaCode(e.currentTarget.value)}
366
+ required
367
+ disabled={mfaLoading()}
368
+ />
369
+ </div>
370
+
371
+ <button
372
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
373
+ type="submit"
374
+ disabled={mfaLoading()}
375
+ >
376
+ {mfaLoading() ? "Verifying..." : "Verify and enable"}
377
+ </button>
378
+ </form>
379
+ </div>
380
+ </Show>
381
+
382
+ <Show when={mfaStep() === "done"}>
383
+ <div class="space-y-4">
384
+ <div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
385
+ Two-factor authentication is enabled. Save these backup
386
+ codes in a safe place.
387
+ </div>
388
+
389
+ <ul class="space-y-1">
390
+ <For each={mfaBackupCodes()}>
391
+ {(code) => (
392
+ <li class="rounded-md border border-input bg-muted px-3 py-1.5 text-center font-mono text-sm tracking-wider">
393
+ {code}
394
+ </li>
395
+ )}
396
+ </For>
397
+ </ul>
398
+ </div>
399
+ </Show>
400
+ </section>
401
+ </>
402
+ )}
403
+ </Show>
404
+ </div>
405
+ );
406
+ };
@@ -0,0 +1,119 @@
1
+ import { type Component, createSignal } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export interface RegisterFormProps {
6
+ onSuccess?: (message: string) => void;
7
+ onError?: (error: Error) => void;
8
+ }
9
+
10
+ export const RegisterForm: Component<RegisterFormProps> = (props) => {
11
+ const { client } = useYAuth();
12
+ const [email, setEmail] = createSignal("");
13
+ const [password, setPassword] = createSignal("");
14
+ const [displayName, setDisplayName] = createSignal("");
15
+ const [error, setError] = createSignal<string | null>(null);
16
+ const [loading, setLoading] = createSignal(false);
17
+
18
+ const handleSubmit = async (e: SubmitEvent) => {
19
+ e.preventDefault();
20
+ setError(null);
21
+ setLoading(true);
22
+
23
+ try {
24
+ const form = e.currentTarget as HTMLFormElement;
25
+ const formData = new FormData(form);
26
+ const formDisplayName =
27
+ (formData.get("display_name") as string) || displayName();
28
+ const result = await client.emailPassword.register({
29
+ email: (formData.get("email") as string) || email(),
30
+ password: (formData.get("password") as string) || password(),
31
+ display_name: formDisplayName || undefined,
32
+ });
33
+ props.onSuccess?.(result.message);
34
+ } catch (err) {
35
+ const error = err instanceof Error ? err : new Error(String(err));
36
+ setError(error.message);
37
+ props.onError?.(error);
38
+ } finally {
39
+ setLoading(false);
40
+ }
41
+ };
42
+
43
+ return (
44
+ <form class="space-y-6" onSubmit={handleSubmit}>
45
+ <Show when={error()}>
46
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
47
+ {error()}
48
+ </div>
49
+ </Show>
50
+
51
+ <div class="space-y-2">
52
+ <label
53
+ class="text-sm font-medium leading-none"
54
+ for="yauth-register-email"
55
+ >
56
+ Email
57
+ </label>
58
+ <input
59
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
60
+ id="yauth-register-email"
61
+ name="email"
62
+ type="email"
63
+ value={email()}
64
+ onInput={(e) => setEmail(e.currentTarget.value)}
65
+ required
66
+ autocomplete="email"
67
+ disabled={loading()}
68
+ />
69
+ </div>
70
+
71
+ <div class="space-y-2">
72
+ <label
73
+ class="text-sm font-medium leading-none"
74
+ for="yauth-register-password"
75
+ >
76
+ Password
77
+ </label>
78
+ <input
79
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
80
+ id="yauth-register-password"
81
+ name="password"
82
+ type="password"
83
+ value={password()}
84
+ onInput={(e) => setPassword(e.currentTarget.value)}
85
+ required
86
+ autocomplete="new-password"
87
+ disabled={loading()}
88
+ />
89
+ </div>
90
+
91
+ <div class="space-y-2">
92
+ <label
93
+ class="text-sm font-medium leading-none"
94
+ for="yauth-register-display-name"
95
+ >
96
+ Display name (optional)
97
+ </label>
98
+ <input
99
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
100
+ id="yauth-register-display-name"
101
+ name="display_name"
102
+ type="text"
103
+ value={displayName()}
104
+ onInput={(e) => setDisplayName(e.currentTarget.value)}
105
+ autocomplete="name"
106
+ disabled={loading()}
107
+ />
108
+ </div>
109
+
110
+ <button
111
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
112
+ type="submit"
113
+ disabled={loading()}
114
+ >
115
+ {loading() ? "Creating account..." : "Create account"}
116
+ </button>
117
+ </form>
118
+ );
119
+ };
@@ -0,0 +1,83 @@
1
+ import { type Component, createSignal } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export interface ResetPasswordFormProps {
6
+ token: string;
7
+ onSuccess?: (message: string) => void;
8
+ }
9
+
10
+ export const ResetPasswordForm: Component<ResetPasswordFormProps> = (props) => {
11
+ const { client } = useYAuth();
12
+ const [password, setPassword] = createSignal("");
13
+ const [error, setError] = createSignal<string | null>(null);
14
+ const [success, setSuccess] = createSignal<string | null>(null);
15
+ const [loading, setLoading] = createSignal(false);
16
+
17
+ const handleSubmit = async (e: SubmitEvent) => {
18
+ e.preventDefault();
19
+ setError(null);
20
+ setSuccess(null);
21
+ setLoading(true);
22
+
23
+ try {
24
+ const form = e.currentTarget as HTMLFormElement;
25
+ const formData = new FormData(form);
26
+ const result = await client.emailPassword.resetPassword(
27
+ props.token,
28
+ (formData.get("password") as string) || password(),
29
+ );
30
+ setSuccess(result.message);
31
+ props.onSuccess?.(result.message);
32
+ } catch (err) {
33
+ const error = err instanceof Error ? err : new Error(String(err));
34
+ setError(error.message);
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <form class="space-y-4" onSubmit={handleSubmit}>
42
+ <Show when={error()}>
43
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
44
+ {error()}
45
+ </div>
46
+ </Show>
47
+
48
+ <Show when={success()}>
49
+ <div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
50
+ {success()}
51
+ </div>
52
+ </Show>
53
+
54
+ <div class="space-y-2">
55
+ <label
56
+ class="text-sm font-medium leading-none"
57
+ for="yauth-reset-password-input"
58
+ >
59
+ New password
60
+ </label>
61
+ <input
62
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
63
+ id="yauth-reset-password-input"
64
+ name="password"
65
+ type="password"
66
+ value={password()}
67
+ onInput={(e) => setPassword(e.currentTarget.value)}
68
+ required
69
+ autocomplete="new-password"
70
+ disabled={loading()}
71
+ />
72
+ </div>
73
+
74
+ <button
75
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
76
+ type="submit"
77
+ disabled={loading()}
78
+ >
79
+ {loading() ? "Resetting..." : "Reset password"}
80
+ </button>
81
+ </form>
82
+ );
83
+ };
@@ -0,0 +1,54 @@
1
+ import { type Component, createEffect, createSignal } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export interface VerifyEmailProps {
6
+ token: string;
7
+ onSuccess?: () => void;
8
+ onError?: (error: Error) => void;
9
+ }
10
+
11
+ type VerifyState = "loading" | "success" | "error";
12
+
13
+ export const VerifyEmail: Component<VerifyEmailProps> = (props) => {
14
+ const { client } = useYAuth();
15
+ const [state, setState] = createSignal<VerifyState>("loading");
16
+ const [message, setMessage] = createSignal("");
17
+ const [errorMessage, setErrorMessage] = createSignal("");
18
+
19
+ createEffect(async () => {
20
+ try {
21
+ const result = await client.emailPassword.verify(props.token);
22
+ setMessage(result.message);
23
+ setState("success");
24
+ props.onSuccess?.();
25
+ } catch (err) {
26
+ const error = err instanceof Error ? err : new Error(String(err));
27
+ setErrorMessage(error.message);
28
+ setState("error");
29
+ props.onError?.(error);
30
+ }
31
+ });
32
+
33
+ return (
34
+ <div class="space-y-4">
35
+ <Show when={state() === "loading"}>
36
+ <div class="text-sm text-muted-foreground">
37
+ Verifying your email address...
38
+ </div>
39
+ </Show>
40
+
41
+ <Show when={state() === "success"}>
42
+ <div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
43
+ {message()}
44
+ </div>
45
+ </Show>
46
+
47
+ <Show when={state() === "error"}>
48
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
49
+ {errorMessage()}
50
+ </div>
51
+ </Show>
52
+ </div>
53
+ );
54
+ };
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { ChangePasswordForm } from "./components/change-password-form";
2
+ export { ConsentScreen } from "./components/consent-screen";
3
+ export { ForgotPasswordForm } from "./components/forgot-password-form";
4
+ export { LoginForm } from "./components/login-form";
5
+ export { MagicLinkForm } from "./components/magic-link-form";
6
+ export { MfaChallenge } from "./components/mfa-challenge";
7
+ export { MfaSetup } from "./components/mfa-setup";
8
+ export { OAuthButtons } from "./components/oauth-buttons";
9
+ export { PasskeyButton } from "./components/passkey-button";
10
+ export { ProfileSettings } from "./components/profile-settings";
11
+ export { RegisterForm } from "./components/register-form";
12
+ export { ResetPasswordForm } from "./components/reset-password-form";
13
+ export { VerifyEmail } from "./components/verify-email";
14
+ export { useYAuth, YAuthProvider } from "./provider";