@better-auth-ui/heroui 1.6.23 → 1.6.25
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 +530 -447
- package/dist/plugins.js +340 -346
- package/package.json +5 -5
- 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.25",
|
|
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",
|
|
@@ -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"
|