@erikey/react 0.4.26 → 0.4.27

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 (143) hide show
  1. package/package.json +2 -1
  2. package/src/__tests__/auth-client.test.ts +105 -0
  3. package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
  4. package/src/auth-client.ts +158 -0
  5. package/src/dashboard-client.ts +60 -0
  6. package/src/index.ts +88 -0
  7. package/src/kv-client.ts +316 -0
  8. package/src/lib/cross-origin-auth.ts +99 -0
  9. package/src/stubs/captcha.ts +24 -0
  10. package/src/stubs/hashes.ts +16 -0
  11. package/src/stubs/index.ts +17 -0
  12. package/src/stubs/passkey.ts +12 -0
  13. package/src/stubs/qr-code.ts +10 -0
  14. package/src/stubs/query.ts +16 -0
  15. package/src/stubs/realtime.ts +17 -0
  16. package/src/stubs/use-sync-external-store.ts +12 -0
  17. package/src/styles.css +141 -0
  18. package/src/types.ts +14 -0
  19. package/src/ui/components/auth/auth-callback.tsx +36 -0
  20. package/src/ui/components/auth/auth-form.tsx +310 -0
  21. package/src/ui/components/auth/auth-view.tsx +435 -0
  22. package/src/ui/components/auth/email-otp-button.tsx +53 -0
  23. package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
  24. package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
  25. package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
  26. package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
  27. package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
  28. package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
  29. package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
  30. package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
  31. package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
  32. package/src/ui/components/auth/magic-link-button.tsx +54 -0
  33. package/src/ui/components/auth/one-tap.tsx +53 -0
  34. package/src/ui/components/auth/otp-input-group.tsx +65 -0
  35. package/src/ui/components/auth/passkey-button.tsx +91 -0
  36. package/src/ui/components/auth/provider-button.tsx +155 -0
  37. package/src/ui/components/auth/sign-out.tsx +25 -0
  38. package/src/ui/components/auth/wallet-button.tsx +192 -0
  39. package/src/ui/components/auth-loading.tsx +21 -0
  40. package/src/ui/components/captcha/captcha.tsx +91 -0
  41. package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
  42. package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
  43. package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
  44. package/src/ui/components/email/email-template.tsx +216 -0
  45. package/src/ui/components/form-error.tsx +27 -0
  46. package/src/ui/components/password-input.tsx +56 -0
  47. package/src/ui/components/provider-icons.tsx +404 -0
  48. package/src/ui/components/redirect-to-sign-in.tsx +16 -0
  49. package/src/ui/components/redirect-to-sign-up.tsx +16 -0
  50. package/src/ui/components/signed-in.tsx +20 -0
  51. package/src/ui/components/signed-out.tsx +20 -0
  52. package/src/ui/components/ui/alert.tsx +66 -0
  53. package/src/ui/components/ui/button.tsx +70 -0
  54. package/src/ui/components/ui/card.tsx +92 -0
  55. package/src/ui/components/ui/checkbox.tsx +66 -0
  56. package/src/ui/components/ui/field.tsx +248 -0
  57. package/src/ui/components/ui/form.tsx +165 -0
  58. package/src/ui/components/ui/input-otp.tsx +77 -0
  59. package/src/ui/components/ui/input.tsx +21 -0
  60. package/src/ui/components/ui/label.tsx +23 -0
  61. package/src/ui/components/ui/separator.tsx +34 -0
  62. package/src/ui/components/ui/skeleton.tsx +13 -0
  63. package/src/ui/components/ui/textarea.tsx +18 -0
  64. package/src/ui/components/user-avatar.tsx +151 -0
  65. package/src/ui/hooks/use-auth-data.ts +193 -0
  66. package/src/ui/hooks/use-authenticate.ts +64 -0
  67. package/src/ui/hooks/use-captcha.tsx +151 -0
  68. package/src/ui/hooks/use-hydrated.ts +13 -0
  69. package/src/ui/hooks/use-lang.ts +32 -0
  70. package/src/ui/hooks/use-success-transition.ts +41 -0
  71. package/src/ui/hooks/use-theme.ts +39 -0
  72. package/src/ui/index.ts +46 -0
  73. package/src/ui/instantdb.ts +1 -0
  74. package/src/ui/lib/auth-data-cache.ts +90 -0
  75. package/src/ui/lib/auth-ui-provider.tsx +769 -0
  76. package/src/ui/lib/gravatar-utils.ts +58 -0
  77. package/src/ui/lib/image-utils.ts +55 -0
  78. package/src/ui/lib/instantdb/model-names.ts +24 -0
  79. package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
  80. package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
  81. package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
  82. package/src/ui/lib/instantdb/use-session.ts +55 -0
  83. package/src/ui/lib/social-providers.ts +150 -0
  84. package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  85. package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
  86. package/src/ui/lib/triplit/model-names.ts +24 -0
  87. package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
  88. package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
  89. package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
  90. package/src/ui/lib/triplit/use-session.ts +42 -0
  91. package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
  92. package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
  93. package/src/ui/lib/utils.ts +119 -0
  94. package/src/ui/lib/view-paths.ts +61 -0
  95. package/src/ui/lib/wallet.ts +129 -0
  96. package/src/ui/localization/admin-error-codes.ts +20 -0
  97. package/src/ui/localization/anonymous-error-codes.ts +6 -0
  98. package/src/ui/localization/api-key-error-codes.ts +32 -0
  99. package/src/ui/localization/auth-localization.ts +865 -0
  100. package/src/ui/localization/base-error-codes.ts +27 -0
  101. package/src/ui/localization/captcha-error-codes.ts +17 -0
  102. package/src/ui/localization/email-otp-error-codes.ts +7 -0
  103. package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
  104. package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
  105. package/src/ui/localization/multi-session-error-codes.ts +3 -0
  106. package/src/ui/localization/organization-error-codes.ts +57 -0
  107. package/src/ui/localization/passkey-error-codes.ts +10 -0
  108. package/src/ui/localization/phone-number-error-codes.ts +10 -0
  109. package/src/ui/localization/stripe-localization.ts +12 -0
  110. package/src/ui/localization/team-error-codes.ts +12 -0
  111. package/src/ui/localization/two-factor-error-codes.ts +12 -0
  112. package/src/ui/localization/username-error-codes.ts +9 -0
  113. package/src/ui/server.ts +4 -0
  114. package/src/ui/style.css +146 -0
  115. package/src/ui/tanstack.ts +1 -0
  116. package/src/ui/triplit.ts +1 -0
  117. package/src/ui/types/account-options.ts +35 -0
  118. package/src/ui/types/additional-fields.ts +21 -0
  119. package/src/ui/types/any-auth-client.ts +6 -0
  120. package/src/ui/types/api-key.ts +9 -0
  121. package/src/ui/types/auth-client.ts +41 -0
  122. package/src/ui/types/auth-hooks.ts +81 -0
  123. package/src/ui/types/auth-mutators.ts +21 -0
  124. package/src/ui/types/avatar-options.ts +29 -0
  125. package/src/ui/types/captcha-options.ts +32 -0
  126. package/src/ui/types/captcha-provider.ts +7 -0
  127. package/src/ui/types/credentials-options.ts +38 -0
  128. package/src/ui/types/delete-user-options.ts +7 -0
  129. package/src/ui/types/email-verification-options.ts +7 -0
  130. package/src/ui/types/fetch-error.ts +6 -0
  131. package/src/ui/types/generic-oauth-options.ts +16 -0
  132. package/src/ui/types/gravatar-options.ts +21 -0
  133. package/src/ui/types/image.ts +7 -0
  134. package/src/ui/types/invitation.ts +10 -0
  135. package/src/ui/types/link.ts +7 -0
  136. package/src/ui/types/organization-options.ts +106 -0
  137. package/src/ui/types/password-validation.ts +16 -0
  138. package/src/ui/types/profile.ts +15 -0
  139. package/src/ui/types/refetch.ts +1 -0
  140. package/src/ui/types/render-toast.ts +9 -0
  141. package/src/ui/types/sign-up-options.ts +7 -0
  142. package/src/ui/types/social-options.ts +16 -0
  143. package/src/ui/types/team-options.ts +47 -0
@@ -0,0 +1,381 @@
1
+ "use client"
2
+
3
+ import type { BetterFetchError } from "@better-fetch/fetch"
4
+ import { zodResolver } from "@hookform/resolvers/zod"
5
+ import { Loader2, QrCodeIcon, SendIcon } from "lucide-react"
6
+ import { useContext, useEffect, useRef, useState } from "react"
7
+ import { useForm } from "react-hook-form"
8
+ import QRCode from "react-qr-code"
9
+ import * as z from "zod"
10
+
11
+ import { useIsHydrated } from "../../../hooks/use-hydrated"
12
+ import { useOnSuccessTransition } from "../../../hooks/use-success-transition"
13
+ import { AuthUIContext } from "../../../lib/auth-ui-provider"
14
+ import { cn, getLocalizedError, getSearchParam } from "../../../lib/utils"
15
+ import type { AuthLocalization } from "../../../localization/auth-localization"
16
+ import type { User } from "../../../types/auth-client"
17
+ import { Button } from "../../ui/button"
18
+ import { Checkbox } from "../../ui/checkbox"
19
+ import {
20
+ Form,
21
+ FormControl,
22
+ FormField,
23
+ FormItem,
24
+ FormLabel,
25
+ FormMessage
26
+ } from "../../ui/form"
27
+ import { InputOTP } from "../../ui/input-otp"
28
+ import { Label } from "../../ui/label"
29
+ import type { AuthFormClassNames } from "../auth-form"
30
+ import { OTPInputGroup } from "../otp-input-group"
31
+
32
+ export interface TwoFactorFormProps {
33
+ className?: string
34
+ classNames?: AuthFormClassNames
35
+ isSubmitting?: boolean
36
+ localization?: Partial<AuthLocalization>
37
+ otpSeparators?: 0 | 1 | 2
38
+ redirectTo?: string
39
+ setIsSubmitting?: (value: boolean) => void
40
+ }
41
+
42
+ export function TwoFactorForm({
43
+ className,
44
+ classNames,
45
+ isSubmitting,
46
+ localization,
47
+ otpSeparators = 0,
48
+ redirectTo,
49
+ setIsSubmitting
50
+ }: TwoFactorFormProps) {
51
+ const isHydrated = useIsHydrated()
52
+ const totpURI = isHydrated ? getSearchParam("totpURI") : null
53
+ const initialSendRef = useRef(false)
54
+
55
+ const {
56
+ authClient,
57
+ basePath,
58
+ hooks: { useSession },
59
+ localization: contextLocalization,
60
+ twoFactor,
61
+ viewPaths,
62
+ toast,
63
+ Link,
64
+ localizeErrors
65
+ } = useContext(AuthUIContext)
66
+
67
+ localization = { ...contextLocalization, ...localization }
68
+
69
+ const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({
70
+ redirectTo
71
+ })
72
+
73
+ const { data: sessionData } = useSession()
74
+ const isTwoFactorEnabled = (sessionData?.user as User)?.twoFactorEnabled
75
+
76
+ const [method, setMethod] = useState<"totp" | "otp" | null>(
77
+ twoFactor?.length === 1 ? twoFactor[0] : null
78
+ )
79
+
80
+ const [isSendingOtp, setIsSendingOtp] = useState(false)
81
+ const [cooldownSeconds, setCooldownSeconds] = useState(0)
82
+
83
+ const formSchema = z.object({
84
+ code: z
85
+ .string()
86
+ .min(1, {
87
+ message: `${localization.ONE_TIME_PASSWORD} ${localization.IS_REQUIRED}`
88
+ })
89
+ .min(6, {
90
+ message: `${localization.ONE_TIME_PASSWORD} ${localization.IS_INVALID}`
91
+ }),
92
+ trustDevice: z.boolean().optional()
93
+ })
94
+
95
+ const form = useForm({
96
+ resolver: zodResolver(formSchema),
97
+ defaultValues: {
98
+ code: ""
99
+ }
100
+ })
101
+
102
+ isSubmitting =
103
+ isSubmitting || form.formState.isSubmitting || transitionPending
104
+
105
+ useEffect(() => {
106
+ setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
107
+ }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
108
+
109
+ // biome-ignore lint/correctness/useExhaustiveDependencies: ignore
110
+ useEffect(() => {
111
+ if (
112
+ method === "otp" &&
113
+ cooldownSeconds <= 0 &&
114
+ !initialSendRef.current
115
+ ) {
116
+ initialSendRef.current = true
117
+ sendOtp()
118
+ }
119
+ }, [method])
120
+
121
+ useEffect(() => {
122
+ if (cooldownSeconds <= 0) return
123
+
124
+ const timer = setTimeout(() => {
125
+ setCooldownSeconds((prev) => prev - 1)
126
+ }, 1000)
127
+ return () => clearTimeout(timer)
128
+ }, [cooldownSeconds])
129
+
130
+ const sendOtp = async () => {
131
+ if (isSendingOtp || cooldownSeconds > 0) return
132
+
133
+ try {
134
+ setIsSendingOtp(true)
135
+ await authClient.twoFactor.sendOtp({
136
+ fetchOptions: { throw: true }
137
+ })
138
+ setCooldownSeconds(60)
139
+ } catch (error) {
140
+ toast({
141
+ variant: "error",
142
+ message: getLocalizedError({
143
+ error,
144
+ localization,
145
+ localizeErrors
146
+ })
147
+ })
148
+
149
+ if (
150
+ (error as BetterFetchError).error.code ===
151
+ "INVALID_TWO_FACTOR_COOKIE"
152
+ ) {
153
+ history.back()
154
+ }
155
+ }
156
+
157
+ initialSendRef.current = false
158
+ setIsSendingOtp(false)
159
+ }
160
+
161
+ async function verifyCode({
162
+ code,
163
+ trustDevice
164
+ }: z.infer<typeof formSchema>) {
165
+ try {
166
+ const verifyMethod =
167
+ method === "totp"
168
+ ? authClient.twoFactor.verifyTotp
169
+ : authClient.twoFactor.verifyOtp
170
+
171
+ await verifyMethod({
172
+ code,
173
+ trustDevice,
174
+ fetchOptions: { throw: true }
175
+ })
176
+
177
+ await onSuccess()
178
+
179
+ if (sessionData && !isTwoFactorEnabled) {
180
+ toast({
181
+ variant: "success",
182
+ message: localization?.TWO_FACTOR_ENABLED
183
+ })
184
+ }
185
+ } catch (error) {
186
+ toast({
187
+ variant: "error",
188
+ message: getLocalizedError({
189
+ error,
190
+ localization,
191
+ localizeErrors
192
+ })
193
+ })
194
+
195
+ form.reset()
196
+ }
197
+ }
198
+
199
+ return (
200
+ <Form {...form}>
201
+ <form
202
+ onSubmit={form.handleSubmit(verifyCode)}
203
+ className={cn("grid w-full gap-6", className, classNames?.base)}
204
+ >
205
+ {twoFactor?.includes("totp") &&
206
+ totpURI &&
207
+ method === "totp" && (
208
+ <div className="space-y-3">
209
+ <Label className={classNames?.label}>
210
+ {localization.TWO_FACTOR_TOTP_LABEL}
211
+ </Label>
212
+
213
+ <QRCode
214
+ className={cn(
215
+ "border shadow-xs",
216
+ classNames?.qrCode
217
+ )}
218
+ value={totpURI}
219
+ />
220
+ </div>
221
+ )}
222
+
223
+ {method !== null && (
224
+ <>
225
+ <FormField
226
+ control={form.control}
227
+ name="code"
228
+ render={({ field }) => (
229
+ <FormItem>
230
+ <div className="flex items-center justify-between">
231
+ <FormLabel
232
+ className={classNames?.label}
233
+ >
234
+ {localization.ONE_TIME_PASSWORD}
235
+ </FormLabel>
236
+
237
+ <Link
238
+ className={cn(
239
+ "text-sm hover:underline",
240
+ classNames?.forgotPasswordLink
241
+ )}
242
+ href={`${basePath}/${viewPaths.RECOVER_ACCOUNT}${isHydrated ? window.location.search : ""}`}
243
+ >
244
+ {localization.FORGOT_AUTHENTICATOR}
245
+ </Link>
246
+ </div>
247
+
248
+ <FormControl>
249
+ <InputOTP
250
+ {...field}
251
+ maxLength={6}
252
+ onChange={(value) => {
253
+ field.onChange(value)
254
+
255
+ if (value.length === 6) {
256
+ form.handleSubmit(
257
+ verifyCode
258
+ )()
259
+ }
260
+ }}
261
+ containerClassName={
262
+ classNames?.otpInputContainer
263
+ }
264
+ className={classNames?.otpInput}
265
+ disabled={isSubmitting}
266
+ >
267
+ <OTPInputGroup
268
+ otpSeparators={otpSeparators}
269
+ />
270
+ </InputOTP>
271
+ </FormControl>
272
+
273
+ <FormMessage
274
+ className={classNames?.error}
275
+ />
276
+ </FormItem>
277
+ )}
278
+ />
279
+
280
+ <FormField
281
+ control={form.control}
282
+ name="trustDevice"
283
+ render={({ field }) => (
284
+ <FormItem className="flex">
285
+ <FormControl>
286
+ <Checkbox
287
+ checked={field.value}
288
+ onCheckedChange={field.onChange}
289
+ disabled={isSubmitting}
290
+ className={classNames?.checkbox}
291
+ />
292
+ </FormControl>
293
+
294
+ <FormLabel className={classNames?.label}>
295
+ {localization.TRUST_DEVICE}
296
+ </FormLabel>
297
+ </FormItem>
298
+ )}
299
+ />
300
+ </>
301
+ )}
302
+
303
+ <div className="grid gap-4">
304
+ {method !== null && (
305
+ <Button
306
+ type="submit"
307
+ disabled={isSubmitting}
308
+ className={cn(
309
+ classNames?.button,
310
+ classNames?.primaryButton
311
+ )}
312
+ >
313
+ {isSubmitting && (
314
+ <Loader2 className="animate-spin" />
315
+ )}
316
+ {localization.TWO_FACTOR_ACTION}
317
+ </Button>
318
+ )}
319
+
320
+ {method === "otp" && twoFactor?.includes("otp") && (
321
+ <Button
322
+ type="button"
323
+ variant="outline"
324
+ onClick={sendOtp}
325
+ disabled={
326
+ cooldownSeconds > 0 ||
327
+ isSendingOtp ||
328
+ isSubmitting
329
+ }
330
+ className={cn(
331
+ classNames?.button,
332
+ classNames?.outlineButton
333
+ )}
334
+ >
335
+ {isSendingOtp ? (
336
+ <Loader2 className="animate-spin" />
337
+ ) : (
338
+ <SendIcon className={classNames?.icon} />
339
+ )}
340
+
341
+ {localization.RESEND_CODE}
342
+ {cooldownSeconds > 0 && ` (${cooldownSeconds})`}
343
+ </Button>
344
+ )}
345
+
346
+ {method !== "otp" && twoFactor?.includes("otp") && (
347
+ <Button
348
+ type="button"
349
+ variant="secondary"
350
+ className={cn(
351
+ classNames?.button,
352
+ classNames?.secondaryButton
353
+ )}
354
+ onClick={() => setMethod("otp")}
355
+ disabled={isSubmitting}
356
+ >
357
+ <SendIcon className={classNames?.icon} />
358
+ {localization.SEND_VERIFICATION_CODE}
359
+ </Button>
360
+ )}
361
+
362
+ {method !== "totp" && twoFactor?.includes("totp") && (
363
+ <Button
364
+ type="button"
365
+ variant="secondary"
366
+ className={cn(
367
+ classNames?.button,
368
+ classNames?.secondaryButton
369
+ )}
370
+ onClick={() => setMethod("totp")}
371
+ disabled={isSubmitting}
372
+ >
373
+ <QrCodeIcon className={classNames?.icon} />
374
+ {localization.CONTINUE_WITH_AUTHENTICATOR}
375
+ </Button>
376
+ )}
377
+ </div>
378
+ </form>
379
+ </Form>
380
+ )
381
+ }
@@ -0,0 +1,54 @@
1
+ import { LockIcon, MailIcon } from "lucide-react"
2
+ import { useContext } from "react"
3
+
4
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
5
+ import { cn } from "../../lib/utils"
6
+ import type { AuthViewPath } from "../../lib/view-paths"
7
+ import type { AuthLocalization } from "../../localization/auth-localization"
8
+ import { Button } from "../ui/button"
9
+ import type { AuthViewClassNames } from "./auth-view"
10
+
11
+ interface MagicLinkButtonProps {
12
+ classNames?: AuthViewClassNames
13
+ isSubmitting?: boolean
14
+ localization: Partial<AuthLocalization>
15
+ view: AuthViewPath
16
+ }
17
+
18
+ export function MagicLinkButton({
19
+ classNames,
20
+ isSubmitting,
21
+ localization,
22
+ view
23
+ }: MagicLinkButtonProps) {
24
+ const { viewPaths, navigate, basePath, credentials } =
25
+ useContext(AuthUIContext)
26
+
27
+ return (
28
+ <Button
29
+ className={cn(
30
+ "w-full",
31
+ classNames?.form?.button,
32
+ classNames?.form?.secondaryButton
33
+ )}
34
+ disabled={isSubmitting}
35
+ type="button"
36
+ variant="secondary"
37
+ onClick={() =>
38
+ navigate(
39
+ `${basePath}/${view === "MAGIC_LINK" || !credentials ? viewPaths.SIGN_IN : viewPaths.MAGIC_LINK}${window.location.search}`
40
+ )
41
+ }
42
+ >
43
+ {view === "MAGIC_LINK" ? (
44
+ <LockIcon className={classNames?.form?.icon} />
45
+ ) : (
46
+ <MailIcon className={classNames?.form?.icon} />
47
+ )}
48
+ {localization.SIGN_IN_WITH}{" "}
49
+ {view === "MAGIC_LINK"
50
+ ? localization.PASSWORD
51
+ : localization.MAGIC_LINK}
52
+ </Button>
53
+ )
54
+ }
@@ -0,0 +1,53 @@
1
+ import { useContext, useEffect, useMemo, useRef } from "react"
2
+
3
+ import { useOnSuccessTransition } from "../../hooks/use-success-transition"
4
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
5
+ import { getLocalizedError } from "../../lib/utils"
6
+ import type { AuthLocalization } from "../../localization/auth-localization"
7
+
8
+ interface OneTapProps {
9
+ localization: Partial<AuthLocalization>
10
+ redirectTo?: string
11
+ }
12
+
13
+ export function OneTap({ localization, redirectTo }: OneTapProps) {
14
+ const {
15
+ authClient,
16
+ localization: contextLocalization,
17
+ toast,
18
+ localizeErrors
19
+ } = useContext(AuthUIContext)
20
+ const oneTapFetched = useRef(false)
21
+
22
+ localization = useMemo(
23
+ () => ({ ...contextLocalization, ...localization }),
24
+ [contextLocalization, localization]
25
+ )
26
+
27
+ const { onSuccess } = useOnSuccessTransition({ redirectTo })
28
+
29
+ useEffect(() => {
30
+ if (oneTapFetched.current) return
31
+ oneTapFetched.current = true
32
+
33
+ try {
34
+ authClient.oneTap({
35
+ fetchOptions: {
36
+ throw: true,
37
+ onSuccess
38
+ }
39
+ })
40
+ } catch (error) {
41
+ toast({
42
+ variant: "error",
43
+ message: getLocalizedError({
44
+ error,
45
+ localization,
46
+ localizeErrors
47
+ })
48
+ })
49
+ }
50
+ }, [authClient, localization, localizeErrors, onSuccess, toast])
51
+
52
+ return null
53
+ }
@@ -0,0 +1,65 @@
1
+ "use client"
2
+
3
+ import { InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "../ui/input-otp"
4
+
5
+ export function OTPInputGroup({
6
+ otpSeparators = 0
7
+ }: {
8
+ otpSeparators?: 0 | 1 | 2
9
+ }) {
10
+ if (otpSeparators === 0) {
11
+ return (
12
+ <InputOTPGroup>
13
+ <InputOTPSlot index={0} />
14
+ <InputOTPSlot index={1} />
15
+ <InputOTPSlot index={2} />
16
+ <InputOTPSlot index={3} />
17
+ <InputOTPSlot index={4} />
18
+ <InputOTPSlot index={5} />
19
+ </InputOTPGroup>
20
+ )
21
+ }
22
+
23
+ if (otpSeparators === 1) {
24
+ return (
25
+ <>
26
+ <InputOTPGroup>
27
+ <InputOTPSlot index={0} />
28
+ <InputOTPSlot index={1} />
29
+ <InputOTPSlot index={2} />
30
+ </InputOTPGroup>
31
+
32
+ <InputOTPSeparator />
33
+
34
+ <InputOTPGroup>
35
+ <InputOTPSlot index={3} />
36
+ <InputOTPSlot index={4} />
37
+ <InputOTPSlot index={5} />
38
+ </InputOTPGroup>
39
+ </>
40
+ )
41
+ }
42
+
43
+ return (
44
+ <>
45
+ <InputOTPGroup>
46
+ <InputOTPSlot index={0} />
47
+ <InputOTPSlot index={1} />
48
+ </InputOTPGroup>
49
+
50
+ <InputOTPSeparator />
51
+
52
+ <InputOTPGroup>
53
+ <InputOTPSlot index={2} />
54
+ <InputOTPSlot index={3} />
55
+ </InputOTPGroup>
56
+
57
+ <InputOTPSeparator />
58
+
59
+ <InputOTPGroup>
60
+ <InputOTPSlot index={4} />
61
+ <InputOTPSlot index={5} />
62
+ </InputOTPGroup>
63
+ </>
64
+ )
65
+ }
@@ -0,0 +1,91 @@
1
+ import { FingerprintIcon } from "lucide-react"
2
+ import { useContext } from "react"
3
+
4
+ import { useOnSuccessTransition } from "../../hooks/use-success-transition"
5
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
6
+ import { cn, getLocalizedError } from "../../lib/utils"
7
+ import type { AuthLocalization } from "../../localization/auth-localization"
8
+ import { Button } from "../ui/button"
9
+ import type { AuthViewClassNames } from "./auth-view"
10
+
11
+ interface PasskeyButtonProps {
12
+ classNames?: AuthViewClassNames
13
+ isSubmitting?: boolean
14
+ localization: Partial<AuthLocalization>
15
+ redirectTo?: string
16
+ setIsSubmitting?: (isSubmitting: boolean) => void
17
+ }
18
+
19
+ export function PasskeyButton({
20
+ classNames,
21
+ isSubmitting,
22
+ localization,
23
+ redirectTo,
24
+ setIsSubmitting
25
+ }: PasskeyButtonProps) {
26
+ const {
27
+ authClient,
28
+ localization: contextLocalization,
29
+ toast,
30
+ localizeErrors
31
+ } = useContext(AuthUIContext)
32
+
33
+ localization = { ...contextLocalization, ...localization }
34
+
35
+ const { onSuccess } = useOnSuccessTransition({ redirectTo })
36
+
37
+ const signInPassKey = async () => {
38
+ setIsSubmitting?.(true)
39
+
40
+ try {
41
+ const response = await authClient.signIn.passkey({
42
+ fetchOptions: { throw: true }
43
+ })
44
+
45
+ if (response?.error) {
46
+ toast({
47
+ variant: "error",
48
+ message: getLocalizedError({
49
+ error: response.error,
50
+ localization,
51
+ localizeErrors
52
+ })
53
+ })
54
+
55
+ setIsSubmitting?.(false)
56
+ } else {
57
+ onSuccess()
58
+ }
59
+ } catch (error) {
60
+ toast({
61
+ variant: "error",
62
+ message: getLocalizedError({
63
+ error,
64
+ localization,
65
+ localizeErrors
66
+ })
67
+ })
68
+
69
+ setIsSubmitting?.(false)
70
+ }
71
+ }
72
+
73
+ return (
74
+ <Button
75
+ className={cn(
76
+ "w-full",
77
+ classNames?.form?.button,
78
+ classNames?.form?.secondaryButton
79
+ )}
80
+ disabled={isSubmitting}
81
+ formNoValidate
82
+ name="passkey"
83
+ value="true"
84
+ variant="secondary"
85
+ onClick={signInPassKey}
86
+ >
87
+ <FingerprintIcon />
88
+ {localization.SIGN_IN_WITH} {localization.PASSKEY}
89
+ </Button>
90
+ )
91
+ }