@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth-ui/heroui",
3
- "version": "1.6.23",
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.0",
37
- "@heroui/styles": "^3.2.0",
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.0",
58
- "@heroui/styles": ">=3.2.0",
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
- toast.danger(error.error?.message || error.message, {
80
- actionProps: {
81
- children: localization.auth.resend,
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
- toast.success(localization.auth.verifyYourEmail)
81
- navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
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
- toast.danger(error.error?.message || error.message, {
88
- actionProps: {
89
- children: localization.auth.resend,
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"