@better-auth-ui/heroui 1.6.22 → 1.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/auth/open-email-button.d.ts +18 -0
- package/dist/components/auth/verify-email.d.ts +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +556 -462
- package/dist/plugins.js +340 -346
- package/package.json +5 -5
- package/src/components/auth/additional-field.tsx +29 -9
- package/src/components/auth/auth.tsx +3 -1
- package/src/components/auth/open-email-button.tsx +50 -0
- package/src/components/auth/sign-in.tsx +5 -25
- package/src/components/auth/sign-up.tsx +5 -3
- package/src/components/auth/username/sign-in-username.tsx +4 -20
- package/src/components/auth/verify-email.tsx +144 -0
- package/src/index.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth-ui/heroui",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "vite build",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@gravity-ui/icons": "^2.18.0",
|
|
36
|
-
"@heroui/react": "^3.2.
|
|
37
|
-
"@heroui/styles": "^3.2.
|
|
36
|
+
"@heroui/react": "^3.2.1",
|
|
37
|
+
"@heroui/styles": "^3.2.1",
|
|
38
38
|
"@internationalized/date": "^3.12.2",
|
|
39
39
|
"@tanstack/react-pacer": "^0.22.1",
|
|
40
40
|
"@tanstack/react-query": "^5.100.14",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"@better-auth-ui/core": "*",
|
|
55
55
|
"@better-auth-ui/react": "*",
|
|
56
56
|
"@gravity-ui/icons": ">=2.18.0",
|
|
57
|
-
"@heroui/react": ">=3.2.
|
|
58
|
-
"@heroui/styles": ">=3.2.
|
|
57
|
+
"@heroui/react": ">=3.2.1",
|
|
58
|
+
"@heroui/styles": ">=3.2.1",
|
|
59
59
|
"@internationalized/date": ">=3.12.2",
|
|
60
60
|
"@tanstack/react-pacer": ">=0.22.1",
|
|
61
61
|
"@tanstack/react-query": ">=5.100.14",
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
toCalendarDate,
|
|
35
35
|
toCalendarDateTime
|
|
36
36
|
} from "@internationalized/date"
|
|
37
|
-
import { useRef, useState } from "react"
|
|
37
|
+
import { type ComponentType, useRef, useState } from "react"
|
|
38
38
|
|
|
39
39
|
import type { AdditionalFieldProps } from "../../lib/auth/auth-plugin"
|
|
40
40
|
|
|
@@ -128,12 +128,17 @@ export function AdditionalField({
|
|
|
128
128
|
const { localization } = useAuth()
|
|
129
129
|
const inputType = resolveInputType(field)
|
|
130
130
|
const inputVariant = variant === "transparent" ? "primary" : "secondary"
|
|
131
|
-
// Used by `inputType: "input"` with `copyable: true` so the copy button
|
|
132
|
-
// reads the input's *live* value rather than a stale `defaultValue`.
|
|
133
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
134
131
|
|
|
135
132
|
if (field.render) {
|
|
136
|
-
|
|
133
|
+
const FieldRenderer = field.render as ComponentType<AdditionalFieldProps>
|
|
134
|
+
return (
|
|
135
|
+
<FieldRenderer
|
|
136
|
+
name={name}
|
|
137
|
+
field={field}
|
|
138
|
+
isPending={isPending}
|
|
139
|
+
variant={variant}
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
if (inputType === "hidden") {
|
|
@@ -471,13 +476,28 @@ export function AdditionalField({
|
|
|
471
476
|
)
|
|
472
477
|
}
|
|
473
478
|
|
|
474
|
-
|
|
479
|
+
return (
|
|
480
|
+
<HeroInputField
|
|
481
|
+
name={name}
|
|
482
|
+
field={field}
|
|
483
|
+
isPending={isPending}
|
|
484
|
+
variant={variant}
|
|
485
|
+
/>
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function HeroInputField({
|
|
490
|
+
name,
|
|
491
|
+
field,
|
|
492
|
+
isPending,
|
|
493
|
+
variant
|
|
494
|
+
}: AdditionalFieldProps) {
|
|
495
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
496
|
+
const inputVariant = variant === "transparent" ? "primary" : "secondary"
|
|
497
|
+
|
|
475
498
|
const hasPrefix = field.prefix != null
|
|
476
499
|
const hasSuffix = field.suffix != null || field.copyable
|
|
477
500
|
|
|
478
|
-
// When `inputType: "input"` is paired with `type: "number"`, restrict the
|
|
479
|
-
// native input to numbers. `formatOptions.maximumFractionDigits` enables
|
|
480
|
-
// fractional input via `step`.
|
|
481
501
|
const isNumeric = field.type === "number"
|
|
482
502
|
const maxFractionDigits = field.formatOptions?.maximumFractionDigits
|
|
483
503
|
const nativeInputType = isNumeric ? "number" : undefined
|
|
@@ -8,6 +8,7 @@ import { ResetPassword } from "./reset-password"
|
|
|
8
8
|
import { SignIn } from "./sign-in"
|
|
9
9
|
import { SignOut } from "./sign-out"
|
|
10
10
|
import { SignUp } from "./sign-up"
|
|
11
|
+
import { VerifyEmail } from "./verify-email"
|
|
11
12
|
|
|
12
13
|
export type AuthProps = {
|
|
13
14
|
className?: string
|
|
@@ -31,7 +32,8 @@ const AUTH_VIEWS: Partial<Record<AuthView, ComponentType<AuthProps>>> = {
|
|
|
31
32
|
signOut: SignOut,
|
|
32
33
|
signUp: SignUp,
|
|
33
34
|
forgotPassword: ForgotPassword,
|
|
34
|
-
resetPassword: ResetPassword
|
|
35
|
+
resetPassword: ResetPassword,
|
|
36
|
+
verifyEmail: VerifyEmail
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getEmailProviderLink } from "@better-auth-ui/core"
|
|
2
|
+
import { useAuth } from "@better-auth-ui/react"
|
|
3
|
+
import { ArrowUpRightFromSquare } from "@gravity-ui/icons"
|
|
4
|
+
import { cn, Link } from "@heroui/react"
|
|
5
|
+
import { buttonVariants } from "@heroui/styles"
|
|
6
|
+
|
|
7
|
+
export type OpenEmailButtonProps = {
|
|
8
|
+
/** Email address used to detect the provider, e.g. from the verify-email flow. */
|
|
9
|
+
email: string
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a link styled as a button that opens the user's email provider login
|
|
15
|
+
* page in a new tab.
|
|
16
|
+
*
|
|
17
|
+
* The provider is resolved from the email domain via the curated
|
|
18
|
+
* `@mikkelscheike/email-provider-links` dataset (Gmail, Outlook, GMX, etc.).
|
|
19
|
+
* Renders nothing when the domain is empty or not a known provider.
|
|
20
|
+
*
|
|
21
|
+
* @param email - Email address to resolve the provider from.
|
|
22
|
+
* @param className - Additional CSS classes applied to the link.
|
|
23
|
+
* @returns The open-email link element, or `null` when no provider matches.
|
|
24
|
+
*/
|
|
25
|
+
export function OpenEmailButton({ email, className }: OpenEmailButtonProps) {
|
|
26
|
+
const { localization } = useAuth()
|
|
27
|
+
|
|
28
|
+
const provider = getEmailProviderLink(email)
|
|
29
|
+
if (!provider) return null
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Link
|
|
33
|
+
href={provider.loginUrl}
|
|
34
|
+
target="_blank"
|
|
35
|
+
rel="noopener noreferrer"
|
|
36
|
+
className={cn(
|
|
37
|
+
buttonVariants({ variant: "primary" }),
|
|
38
|
+
"w-full gap-2",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{localization.auth.openEmailProvider.replace(
|
|
43
|
+
"{{provider}}",
|
|
44
|
+
provider.companyProvider
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
<ArrowUpRightFromSquare className="size-3" />
|
|
48
|
+
</Link>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { authMutationKeys } from "@better-auth-ui/core"
|
|
2
|
-
import {
|
|
3
|
-
useAuth,
|
|
4
|
-
useFetchOptions,
|
|
5
|
-
useSendVerificationEmail,
|
|
6
|
-
useSignInEmail
|
|
7
|
-
} from "@better-auth-ui/react"
|
|
2
|
+
import { useAuth, useFetchOptions, useSignInEmail } from "@better-auth-ui/react"
|
|
8
3
|
import {
|
|
9
4
|
Button,
|
|
10
5
|
Card,
|
|
@@ -18,8 +13,7 @@ import {
|
|
|
18
13
|
Label,
|
|
19
14
|
Link,
|
|
20
15
|
Spinner,
|
|
21
|
-
TextField
|
|
22
|
-
toast
|
|
16
|
+
TextField
|
|
23
17
|
} from "@heroui/react"
|
|
24
18
|
import { useIsMutating } from "@tanstack/react-query"
|
|
25
19
|
import { type SyntheticEvent, useState } from "react"
|
|
@@ -48,7 +42,6 @@ export function SignIn({
|
|
|
48
42
|
const {
|
|
49
43
|
authClient,
|
|
50
44
|
basePaths,
|
|
51
|
-
baseURL,
|
|
52
45
|
emailAndPassword,
|
|
53
46
|
localization,
|
|
54
47
|
plugins,
|
|
@@ -62,13 +55,6 @@ export function SignIn({
|
|
|
62
55
|
|
|
63
56
|
const [password, setPassword] = useState("")
|
|
64
57
|
|
|
65
|
-
const { mutate: sendVerificationEmail } = useSendVerificationEmail(
|
|
66
|
-
authClient,
|
|
67
|
-
{
|
|
68
|
-
onSuccess: () => toast.success(localization.auth.verificationEmailSent)
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
|
|
72
58
|
const { mutate: signInEmail, isPending: signInEmailPending } = useSignInEmail(
|
|
73
59
|
authClient,
|
|
74
60
|
{
|
|
@@ -76,15 +62,9 @@ export function SignIn({
|
|
|
76
62
|
setPassword("")
|
|
77
63
|
|
|
78
64
|
if (error.error?.code === "EMAIL_NOT_VERIFIED") {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
onClick: () =>
|
|
83
|
-
sendVerificationEmail({
|
|
84
|
-
email,
|
|
85
|
-
callbackURL: `${baseURL}${redirectTo}`
|
|
86
|
-
})
|
|
87
|
-
}
|
|
65
|
+
sessionStorage.setItem("better-auth-ui.verify-email", email)
|
|
66
|
+
navigate({
|
|
67
|
+
to: `${basePaths.auth}/${viewPaths.auth.verifyEmail}`
|
|
88
68
|
})
|
|
89
69
|
}
|
|
90
70
|
|
|
@@ -75,10 +75,12 @@ export function SignUp({
|
|
|
75
75
|
setConfirmPassword("")
|
|
76
76
|
resetFetchOptions()
|
|
77
77
|
},
|
|
78
|
-
onSuccess: () => {
|
|
78
|
+
onSuccess: (_data, { email }) => {
|
|
79
79
|
if (emailAndPassword?.requireEmailVerification) {
|
|
80
|
-
|
|
81
|
-
navigate({
|
|
80
|
+
sessionStorage.setItem("better-auth-ui.verify-email", email)
|
|
81
|
+
navigate({
|
|
82
|
+
to: `${basePaths.auth}/${viewPaths.auth.verifyEmail}`
|
|
83
|
+
})
|
|
82
84
|
} else {
|
|
83
85
|
navigate({ to: redirectTo })
|
|
84
86
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
useAuth,
|
|
5
5
|
useAuthPlugin,
|
|
6
6
|
useFetchOptions,
|
|
7
|
-
useSendVerificationEmail,
|
|
8
7
|
useSignInEmail,
|
|
9
8
|
useSignInUsername
|
|
10
9
|
} from "@better-auth-ui/react"
|
|
@@ -21,8 +20,7 @@ import {
|
|
|
21
20
|
Label,
|
|
22
21
|
Link,
|
|
23
22
|
Spinner,
|
|
24
|
-
TextField
|
|
25
|
-
toast
|
|
23
|
+
TextField
|
|
26
24
|
} from "@heroui/react"
|
|
27
25
|
import { useIsMutating } from "@tanstack/react-query"
|
|
28
26
|
import { type SyntheticEvent, useState } from "react"
|
|
@@ -51,7 +49,6 @@ export function SignInUsername({
|
|
|
51
49
|
const {
|
|
52
50
|
authClient,
|
|
53
51
|
basePaths,
|
|
54
|
-
baseURL,
|
|
55
52
|
emailAndPassword,
|
|
56
53
|
localization,
|
|
57
54
|
plugins,
|
|
@@ -67,13 +64,6 @@ export function SignInUsername({
|
|
|
67
64
|
|
|
68
65
|
const [password, setPassword] = useState("")
|
|
69
66
|
|
|
70
|
-
const { mutate: sendVerificationEmail } = useSendVerificationEmail(
|
|
71
|
-
authClient,
|
|
72
|
-
{
|
|
73
|
-
onSuccess: () => toast.success(localization.auth.verificationEmailSent)
|
|
74
|
-
}
|
|
75
|
-
)
|
|
76
|
-
|
|
77
67
|
function isEmail(value: string): boolean {
|
|
78
68
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
79
69
|
}
|
|
@@ -84,15 +74,9 @@ export function SignInUsername({
|
|
|
84
74
|
setPassword("")
|
|
85
75
|
|
|
86
76
|
if (error.error?.code === "EMAIL_NOT_VERIFIED") {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
onClick: () =>
|
|
91
|
-
sendVerificationEmail({
|
|
92
|
-
email,
|
|
93
|
-
callbackURL: `${baseURL}${redirectTo}`
|
|
94
|
-
})
|
|
95
|
-
}
|
|
77
|
+
sessionStorage.setItem("better-auth-ui.verify-email", email)
|
|
78
|
+
navigate({
|
|
79
|
+
to: `${basePaths.auth}/${viewPaths.auth.verifyEmail}`
|
|
96
80
|
})
|
|
97
81
|
}
|
|
98
82
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useAuth, useSendVerificationEmail } from "@better-auth-ui/react"
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
type CardProps,
|
|
6
|
+
cn,
|
|
7
|
+
Description,
|
|
8
|
+
Link,
|
|
9
|
+
Spinner,
|
|
10
|
+
toast,
|
|
11
|
+
useIsHydrated
|
|
12
|
+
} from "@heroui/react"
|
|
13
|
+
import { useEffect, useState } from "react"
|
|
14
|
+
|
|
15
|
+
import { OpenEmailButton } from "./open-email-button"
|
|
16
|
+
|
|
17
|
+
export type VerifyEmailProps = {
|
|
18
|
+
className?: string
|
|
19
|
+
variant?: CardProps["variant"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Seconds the resend button stays disabled to prevent spamming the endpoint. */
|
|
23
|
+
const RESEND_COOLDOWN_SECONDS = 60
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render a card prompting the user to verify their email, with a resend button
|
|
27
|
+
* that is rate-limited by a cooldown timer.
|
|
28
|
+
*
|
|
29
|
+
* The target email is read from `sessionStorage` (set when sign-up or sign-in
|
|
30
|
+
* redirects here); if none is stored the user is redirected back to sign-in.
|
|
31
|
+
* The resend button is disabled while a cooldown is active and shows the
|
|
32
|
+
* remaining seconds.
|
|
33
|
+
*
|
|
34
|
+
* @param className - Additional CSS classes applied to the outer card container
|
|
35
|
+
* @param variant - Variant to apply to the card
|
|
36
|
+
* @returns The verify-email card React element
|
|
37
|
+
*/
|
|
38
|
+
export function VerifyEmail({ className, variant }: VerifyEmailProps) {
|
|
39
|
+
const {
|
|
40
|
+
authClient,
|
|
41
|
+
basePaths,
|
|
42
|
+
baseURL,
|
|
43
|
+
localization,
|
|
44
|
+
navigate,
|
|
45
|
+
redirectTo,
|
|
46
|
+
viewPaths
|
|
47
|
+
} = useAuth()
|
|
48
|
+
|
|
49
|
+
const isHydrated = useIsHydrated()
|
|
50
|
+
const [email, setEmail] = useState(
|
|
51
|
+
(isHydrated && sessionStorage.getItem("better-auth-ui.verify-email")) || ""
|
|
52
|
+
)
|
|
53
|
+
const [cooldown, setCooldown] = useState(RESEND_COOLDOWN_SECONDS)
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const storedEmail = sessionStorage.getItem("better-auth-ui.verify-email")
|
|
57
|
+
|
|
58
|
+
if (!storedEmail) {
|
|
59
|
+
navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setEmail(storedEmail)
|
|
64
|
+
}, [basePaths.auth, navigate, viewPaths.auth.signIn])
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (cooldown <= 0) return
|
|
68
|
+
|
|
69
|
+
const interval = setInterval(() => {
|
|
70
|
+
setCooldown((current) => (current > 0 ? current - 1 : 0))
|
|
71
|
+
}, 1000)
|
|
72
|
+
|
|
73
|
+
return () => clearInterval(interval)
|
|
74
|
+
}, [cooldown])
|
|
75
|
+
|
|
76
|
+
const { mutate: sendVerificationEmail, isPending } = useSendVerificationEmail(
|
|
77
|
+
authClient,
|
|
78
|
+
{
|
|
79
|
+
onSuccess: () => {
|
|
80
|
+
toast.success(localization.auth.verificationEmailSent)
|
|
81
|
+
setCooldown(RESEND_COOLDOWN_SECONDS)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const isCoolingDown = cooldown > 0
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Card
|
|
90
|
+
className={cn("w-full max-w-sm gap-4 md:p-6", className)}
|
|
91
|
+
variant={variant}
|
|
92
|
+
>
|
|
93
|
+
<Card.Header>
|
|
94
|
+
<Card.Title className="text-xl font-semibold mb-1">
|
|
95
|
+
{localization.auth.verifyEmail}
|
|
96
|
+
</Card.Title>
|
|
97
|
+
</Card.Header>
|
|
98
|
+
|
|
99
|
+
<Card.Content className="gap-4">
|
|
100
|
+
<Description className="text-sm">
|
|
101
|
+
{localization.auth.checkYourEmail}
|
|
102
|
+
</Description>
|
|
103
|
+
|
|
104
|
+
<div className="flex flex-col gap-3">
|
|
105
|
+
<OpenEmailButton email={email} />
|
|
106
|
+
|
|
107
|
+
<Button
|
|
108
|
+
className="w-full"
|
|
109
|
+
variant="tertiary"
|
|
110
|
+
isDisabled={!email || isCoolingDown || isPending}
|
|
111
|
+
isPending={isPending}
|
|
112
|
+
onPress={() =>
|
|
113
|
+
sendVerificationEmail({
|
|
114
|
+
email,
|
|
115
|
+
callbackURL: `${baseURL}${redirectTo}`
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
>
|
|
119
|
+
{isPending && <Spinner color="current" size="sm" />}
|
|
120
|
+
|
|
121
|
+
{isCoolingDown
|
|
122
|
+
? localization.auth.resendIn.replace(
|
|
123
|
+
"{{seconds}}",
|
|
124
|
+
String(cooldown)
|
|
125
|
+
)
|
|
126
|
+
: localization.auth.resend}
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
</Card.Content>
|
|
130
|
+
|
|
131
|
+
<Card.Footer className="flex-col gap-3">
|
|
132
|
+
<Description className="text-sm">
|
|
133
|
+
{localization.auth.alreadyVerifiedYourEmail}{" "}
|
|
134
|
+
<Link
|
|
135
|
+
href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
|
|
136
|
+
className="text-accent no-underline hover:underline decoration-accent-hover"
|
|
137
|
+
>
|
|
138
|
+
{localization.auth.signIn}
|
|
139
|
+
</Link>
|
|
140
|
+
</Description>
|
|
141
|
+
</Card.Footer>
|
|
142
|
+
</Card>
|
|
143
|
+
)
|
|
144
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./components/auth/additional-field"
|
|
|
4
4
|
export * from "./components/auth/auth"
|
|
5
5
|
export * from "./components/auth/auth-provider"
|
|
6
6
|
export * from "./components/auth/forgot-password"
|
|
7
|
+
export * from "./components/auth/open-email-button"
|
|
7
8
|
export * from "./components/auth/provider-button"
|
|
8
9
|
export * from "./components/auth/provider-buttons"
|
|
9
10
|
export * from "./components/auth/reset-password"
|
|
@@ -21,3 +22,4 @@ export * from "./components/auth/sign-up"
|
|
|
21
22
|
export * from "./components/auth/user/user-avatar"
|
|
22
23
|
export * from "./components/auth/user/user-button"
|
|
23
24
|
export * from "./components/auth/user/user-view"
|
|
25
|
+
export * from "./components/auth/verify-email"
|