@better-auth-ui/heroui 1.6.26 → 1.6.28

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,126 @@
1
+ import { authMutationKeys as e, getEmailProviderLink as t, getProviderName as n } from "@better-auth-ui/core";
2
+ import { providerIcons as r, useAuth as i, useSession as a, useSignInSocial as o } from "@better-auth-ui/react";
3
+ import { ArrowUpRightFromSquare as s, Person as c } from "@gravity-ui/icons";
4
+ import { Avatar as l, Button as u, Link as d, Separator as f, Skeleton as p, Spinner as m, cn as h } from "@heroui/react";
5
+ import { useMemo as g } from "react";
6
+ import { jsx as _, jsxs as v } from "react/jsx-runtime";
7
+ import { buttonVariants as y } from "@heroui/styles";
8
+ import { useIsMutating as b } from "@tanstack/react-query";
9
+ //#region src/components/auth/open-email-button.tsx
10
+ function x({ email: e, className: n }) {
11
+ let { localization: r } = i(), a = t(e);
12
+ return a ? /* @__PURE__ */ v(d, {
13
+ href: a.loginUrl,
14
+ target: "_blank",
15
+ rel: "noopener noreferrer",
16
+ className: h(y({ variant: "primary" }), "w-full gap-2", n),
17
+ children: [r.auth.openEmailProvider.replace("{{provider}}", a.companyProvider), /* @__PURE__ */ _(s, { className: "size-3" })]
18
+ }) : null;
19
+ }
20
+ //#endregion
21
+ //#region src/components/auth/field-separator.tsx
22
+ function S({ children: e }) {
23
+ return /* @__PURE__ */ v("div", {
24
+ className: "flex items-center gap-4",
25
+ children: [
26
+ /* @__PURE__ */ _(f, { className: "flex-1" }),
27
+ /* @__PURE__ */ _("p", {
28
+ className: "text-xs text-muted shrink-0",
29
+ children: e
30
+ }),
31
+ /* @__PURE__ */ _(f, { className: "flex-1" })
32
+ ]
33
+ });
34
+ }
35
+ //#endregion
36
+ //#region src/components/auth/provider-button.tsx
37
+ function C({ provider: t, display: a = "full", variant: s = "tertiary", ...c }) {
38
+ let { authClient: l, baseURL: d, localization: f, redirectTo: p } = i(), h = `${d}${p}`, { mutate: g, isPending: y } = o(l), x = r[t];
39
+ return /* @__PURE__ */ v(u, {
40
+ variant: s,
41
+ isPending: b({ mutationKey: e.signIn.all }) + b({ mutationKey: e.signUp.all }) > 0,
42
+ onPress: () => g({
43
+ provider: t,
44
+ callbackURL: h
45
+ }),
46
+ ...c,
47
+ "aria-label": n(t),
48
+ children: [y ? /* @__PURE__ */ _(m, {
49
+ color: "current",
50
+ size: "sm"
51
+ }) : /* @__PURE__ */ _(x, {}), a === "full" ? f.auth.continueWith.replace("{{provider}}", n(t)) : a === "name" ? n(t) : null]
52
+ });
53
+ }
54
+ //#endregion
55
+ //#region src/components/auth/provider-buttons.tsx
56
+ function w({ socialLayout: e = "auto" }) {
57
+ let { socialProviders: t } = i(), n = g(() => e === "auto" ? t?.length && t.length >= 4 ? "horizontal" : "vertical" : e, [e, t?.length]);
58
+ return /* @__PURE__ */ _("div", {
59
+ className: h("gap-3", n === "grid" && "grid grid-cols-2", n === "vertical" && "flex flex-col", n === "horizontal" && "flex flex-wrap"),
60
+ children: t?.map((e) => /* @__PURE__ */ _(C, {
61
+ provider: e,
62
+ display: n === "vertical" ? "full" : n === "grid" ? "name" : "icon",
63
+ className: h("w-full", n === "horizontal" && "flex-1")
64
+ }, e))
65
+ });
66
+ }
67
+ //#endregion
68
+ //#region src/components/auth/user/user-avatar.tsx
69
+ function T({ className: e, fallback: t, isPending: n, user: r, size: o = "sm", style: s, ...u }) {
70
+ let { authClient: d } = i(), { data: f, isPending: m } = a(d, { enabled: !r && !n });
71
+ if ((n || m) && !r) return /* @__PURE__ */ _(p, {
72
+ className: h("rounded-full", o === "sm" ? "size-8" : o === "md" ? "size-10" : "size-12", e),
73
+ style: s
74
+ });
75
+ let g = r ?? f?.user, y = (g?.username || g?.name || g?.email)?.slice(0, 2).toUpperCase();
76
+ return /* @__PURE__ */ v(l, {
77
+ size: o,
78
+ className: h("rounded-full", e),
79
+ style: s,
80
+ ...u,
81
+ children: [/* @__PURE__ */ _(l.Image, {
82
+ alt: g?.displayUsername || g?.name || g?.email,
83
+ src: g?.image ?? void 0
84
+ }), /* @__PURE__ */ _(l.Fallback, {
85
+ className: h(o === "lg" ? "text-xl" : o === "md" ? "text-base" : "text-sm"),
86
+ delayMs: g?.image ? 600 : void 0,
87
+ children: t || y || /* @__PURE__ */ _(c, { className: "size-4" })
88
+ })]
89
+ });
90
+ }
91
+ //#endregion
92
+ //#region src/components/auth/user/user-view.tsx
93
+ function E({ className: e, isPending: t, size: n = "md", hideSubtitle: r = !1, user: o, ...s }) {
94
+ let { authClient: c } = i(), { data: l, isPending: u } = a(c, { enabled: !o && !t }), d = o ?? l?.user;
95
+ return (t || u) && !o ? /* @__PURE__ */ v("div", {
96
+ className: h("flex items-center gap-2 min-w-0", e),
97
+ ...s,
98
+ children: [/* @__PURE__ */ _(T, {
99
+ isPending: !0,
100
+ className: n === "sm" ? "size-5 [&>span]:text-xs" : void 0,
101
+ size: n === "lg" ? "md" : "sm"
102
+ }), /* @__PURE__ */ v("div", {
103
+ className: "flex flex-col gap-1 min-w-0",
104
+ children: [/* @__PURE__ */ _(p, { className: "h-3.5 w-24 rounded-lg" }), r ? null : /* @__PURE__ */ _(p, { className: "h-3 w-32 rounded-lg" })]
105
+ })]
106
+ }) : /* @__PURE__ */ v("div", {
107
+ className: h("flex items-center gap-2 min-w-0", e),
108
+ ...s,
109
+ children: [/* @__PURE__ */ _(T, {
110
+ className: n === "sm" ? "size-5 [&>span]:text-xs" : void 0,
111
+ user: d,
112
+ size: n === "lg" ? "md" : "sm"
113
+ }), /* @__PURE__ */ v("div", {
114
+ className: "flex flex-col min-w-0",
115
+ children: [/* @__PURE__ */ _("p", {
116
+ className: "text-foreground text-sm font-medium leading-tight truncate",
117
+ children: d?.displayUsername || d?.name || d?.email
118
+ }), !r && (d?.displayUsername || d?.name) ? /* @__PURE__ */ _("p", {
119
+ className: "text-muted text-xs leading-tight truncate overflow-x-hidden",
120
+ children: d?.email
121
+ }) : null]
122
+ })]
123
+ });
124
+ }
125
+ //#endregion
126
+ export { S as a, C as i, T as n, x as o, w as r, E as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth-ui/heroui",
3
- "version": "1.6.26",
3
+ "version": "1.6.28",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "vite build",
@@ -4,6 +4,7 @@ import type { CardProps } from "@heroui/react"
4
4
  import { type ComponentType, useEffect } from "react"
5
5
  import { ForgotPassword } from "./forgot-password"
6
6
  import type { SocialLayout } from "./provider-buttons"
7
+ import { ResetLinkSent } from "./reset-link-sent"
7
8
  import { ResetPassword } from "./reset-password"
8
9
  import { SignIn } from "./sign-in"
9
10
  import { SignOut } from "./sign-out"
@@ -25,7 +26,12 @@ export type AuthProps = {
25
26
  * When it's disabled, the `<Auth>` router redirects these to `signIn` so a
26
27
  * plugin's `fallbackViews.auth.signIn` (e.g. magic link) takes over.
27
28
  */
28
- const PASSWORD_ONLY_VIEWS = ["signUp", "forgotPassword", "resetPassword"]
29
+ const PASSWORD_ONLY_VIEWS = [
30
+ "signUp",
31
+ "forgotPassword",
32
+ "resetPassword",
33
+ "resetLinkSent"
34
+ ]
29
35
 
30
36
  const AUTH_VIEWS: Partial<Record<AuthView, ComponentType<AuthProps>>> = {
31
37
  signIn: SignIn,
@@ -33,6 +39,7 @@ const AUTH_VIEWS: Partial<Record<AuthView, ComponentType<AuthProps>>> = {
33
39
  signUp: SignUp,
34
40
  forgotPassword: ForgotPassword,
35
41
  resetPassword: ResetPassword,
42
+ resetLinkSent: ResetLinkSent,
36
43
  verifyEmail: VerifyEmail
37
44
  }
38
45
 
@@ -15,11 +15,12 @@ import {
15
15
  Label,
16
16
  Link,
17
17
  Spinner,
18
- TextField,
19
- toast
18
+ TextField
20
19
  } from "@heroui/react"
21
20
  import type { SyntheticEvent } from "react"
22
21
 
22
+ import { RESET_LINK_SENT_STORAGE_KEY } from "./reset-link-sent"
23
+
23
24
  export type ForgotPasswordProps = {
24
25
  className?: string
25
26
  variant?: CardProps["variant"]
@@ -29,7 +30,9 @@ export type ForgotPasswordProps = {
29
30
  * Render a card-based "Forgot Password" form that sends a password-reset email.
30
31
  *
31
32
  * The form displays an email input, submit button, and a link back to sign-in.
32
- * Success toasts are shown via `useRequestPasswordReset`; errors are handled globally by `ErrorToaster`.
33
+ * After a successful request the submitted email is stored in `sessionStorage`
34
+ * and the user is redirected to the reset-link-sent view, which offers to open
35
+ * their email provider. Errors are handled globally by `ErrorToaster`.
33
36
  *
34
37
  * @param className - Optional additional CSS class names applied to the card
35
38
  * @returns The forgot-password form UI as a JSX element
@@ -53,9 +56,9 @@ export function ForgotPassword({ className, variant }: ForgotPasswordProps) {
53
56
  onError: () => {
54
57
  resetFetchOptions()
55
58
  },
56
- onSuccess: () => {
57
- toast.success(localization.auth.passwordResetEmailSent)
58
- navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
59
+ onSuccess: (_data, { email }) => {
60
+ sessionStorage.setItem(RESET_LINK_SENT_STORAGE_KEY, email)
61
+ navigate({ to: `${basePaths.auth}/${viewPaths.auth.resetLinkSent}` })
59
62
  }
60
63
  }
61
64
  )
@@ -0,0 +1,84 @@
1
+ import { useAuth, useAuthPlugin } from "@better-auth-ui/react"
2
+ import {
3
+ Card,
4
+ type CardProps,
5
+ cn,
6
+ Description,
7
+ Link,
8
+ useIsHydrated
9
+ } from "@heroui/react"
10
+ import { useEffect, useState } from "react"
11
+
12
+ import { magicLinkPlugin } from "../../../lib/auth/magic-link-plugin"
13
+ import { OpenEmailButton } from "../open-email-button"
14
+
15
+ /** `sessionStorage` key the magic-link form stores the submitted email under. */
16
+ export const MAGIC_LINK_SENT_STORAGE_KEY = "better-auth-ui.magic-link-sent"
17
+
18
+ export type MagicLinkSentProps = {
19
+ className?: string
20
+ variant?: CardProps["variant"]
21
+ }
22
+
23
+ /**
24
+ * Render a card confirming that a magic-link email was sent, with a button
25
+ * to open the user's email provider.
26
+ *
27
+ * The target email is read from `sessionStorage` (set when the magic-link
28
+ * form redirects here); the OpenEmail button is only shown when an email is
29
+ * stored and resolves to a known provider.
30
+ *
31
+ * @param className - Additional CSS classes applied to the outer card container
32
+ * @param variant - Variant to apply to the card
33
+ * @returns The magic-link-sent card React element
34
+ */
35
+ export function MagicLinkSent({ className, variant }: MagicLinkSentProps) {
36
+ const { basePaths, emailAndPassword, localization, viewPaths } = useAuth()
37
+ const { localization: magicLinkLocalization } = useAuthPlugin(magicLinkPlugin)
38
+
39
+ const isHydrated = useIsHydrated()
40
+ const [email, setEmail] = useState(
41
+ (isHydrated && sessionStorage.getItem(MAGIC_LINK_SENT_STORAGE_KEY)) || ""
42
+ )
43
+
44
+ useEffect(() => {
45
+ setEmail(sessionStorage.getItem(MAGIC_LINK_SENT_STORAGE_KEY) ?? "")
46
+ }, [])
47
+
48
+ return (
49
+ <Card
50
+ className={cn("w-full max-w-sm gap-4 md:p-6", className)}
51
+ variant={variant}
52
+ >
53
+ <Card.Header>
54
+ <Card.Title className="text-xl font-semibold mb-1">
55
+ {localization.auth.checkYourEmailTitle}
56
+ </Card.Title>
57
+ </Card.Header>
58
+
59
+ <Card.Content className="gap-4">
60
+ <Description className="text-sm">
61
+ {email
62
+ ? magicLinkLocalization.magicLinkSentTo.replace("{{email}}", email)
63
+ : magicLinkLocalization.magicLinkSent}
64
+ </Description>
65
+
66
+ {email && <OpenEmailButton email={email} />}
67
+ </Card.Content>
68
+
69
+ {emailAndPassword?.enabled && (
70
+ <Card.Footer className="flex-col gap-3">
71
+ <Description className="text-sm">
72
+ {localization.auth.needToCreateAnAccount}{" "}
73
+ <Link
74
+ href={`${basePaths.auth}/${viewPaths.auth.signUp}`}
75
+ className="text-accent no-underline hover:underline decoration-accent-hover"
76
+ >
77
+ {localization.auth.signUp}
78
+ </Link>
79
+ </Description>
80
+ </Card.Footer>
81
+ )}
82
+ </Card>
83
+ )
84
+ }
@@ -17,8 +17,7 @@ import {
17
17
  Label,
18
18
  Link,
19
19
  Spinner,
20
- TextField,
21
- toast
20
+ TextField
22
21
  } from "@heroui/react"
23
22
  import { useIsMutating } from "@tanstack/react-query"
24
23
  import { type SyntheticEvent, useState } from "react"
@@ -26,6 +25,7 @@ import { type SyntheticEvent, useState } from "react"
26
25
  import { magicLinkPlugin } from "../../../lib/auth/magic-link-plugin"
27
26
  import { FieldSeparator } from "../field-separator"
28
27
  import { ProviderButtons, type SocialLayout } from "../provider-buttons"
28
+ import { MAGIC_LINK_SENT_STORAGE_KEY } from "./magic-link-sent"
29
29
 
30
30
  export type MagicLinkProps = {
31
31
  className?: string
@@ -53,20 +53,24 @@ export function MagicLink({
53
53
  baseURL,
54
54
  emailAndPassword,
55
55
  localization,
56
+ navigate,
56
57
  plugins,
57
58
  redirectTo,
58
59
  socialProviders,
59
60
  viewPaths
60
61
  } = useAuth()
61
- const { localization: magicLinkLocalization } = useAuthPlugin(magicLinkPlugin)
62
+ const { localization: magicLinkLocalization, viewPaths: magicLinkViewPaths } =
63
+ useAuthPlugin(magicLinkPlugin)
62
64
 
63
65
  const [email, setEmail] = useState("")
64
66
 
65
67
  const { mutate: signInMagicLink, isPending: signInMagicLinkPending } =
66
68
  useSignInMagicLink(authClient as MagicLinkAuthClient, {
67
- onSuccess: () => {
68
- setEmail("")
69
- toast.success(magicLinkLocalization.magicLinkSent)
69
+ onSuccess: (_data, variables) => {
70
+ sessionStorage.setItem(MAGIC_LINK_SENT_STORAGE_KEY, variables.email)
71
+ navigate({
72
+ to: `${basePaths.auth}/${magicLinkViewPaths.auth.magicLinkSent}`
73
+ })
70
74
  }
71
75
  })
72
76
 
@@ -0,0 +1,80 @@
1
+ import { useAuth } from "@better-auth-ui/react"
2
+ import {
3
+ Card,
4
+ type CardProps,
5
+ cn,
6
+ Description,
7
+ Link,
8
+ useIsHydrated
9
+ } from "@heroui/react"
10
+ import { useEffect, useState } from "react"
11
+
12
+ import { OpenEmailButton } from "./open-email-button"
13
+
14
+ /** `sessionStorage` key the forgot-password form stores the submitted email under. */
15
+ export const RESET_LINK_SENT_STORAGE_KEY = "better-auth-ui.reset-link-sent"
16
+
17
+ export type ResetLinkSentProps = {
18
+ className?: string
19
+ variant?: CardProps["variant"]
20
+ }
21
+
22
+ /**
23
+ * Render a card confirming that a password-reset email was sent, with a
24
+ * button to open the user's email provider.
25
+ *
26
+ * The target email is read from `sessionStorage` (set when the forgot-password
27
+ * form redirects here); the OpenEmail button is only shown when an email is
28
+ * stored and resolves to a known provider.
29
+ *
30
+ * @param className - Additional CSS classes applied to the outer card container
31
+ * @param variant - Variant to apply to the card
32
+ * @returns The reset-link-sent card React element
33
+ */
34
+ export function ResetLinkSent({ className, variant }: ResetLinkSentProps) {
35
+ const { basePaths, localization, viewPaths } = useAuth()
36
+
37
+ const isHydrated = useIsHydrated()
38
+ const [email, setEmail] = useState(
39
+ (isHydrated && sessionStorage.getItem(RESET_LINK_SENT_STORAGE_KEY)) || ""
40
+ )
41
+
42
+ useEffect(() => {
43
+ setEmail(sessionStorage.getItem(RESET_LINK_SENT_STORAGE_KEY) ?? "")
44
+ }, [])
45
+
46
+ return (
47
+ <Card
48
+ className={cn("w-full max-w-sm gap-4 md:p-6", className)}
49
+ variant={variant}
50
+ >
51
+ <Card.Header>
52
+ <Card.Title className="text-xl font-semibold mb-1">
53
+ {localization.auth.checkYourEmailTitle}
54
+ </Card.Title>
55
+ </Card.Header>
56
+
57
+ <Card.Content className="gap-4">
58
+ <Description className="text-sm">
59
+ {email
60
+ ? localization.auth.resetLinkSentTo.replace("{{email}}", email)
61
+ : localization.auth.passwordResetEmailSent}
62
+ </Description>
63
+
64
+ {email && <OpenEmailButton email={email} />}
65
+ </Card.Content>
66
+
67
+ <Card.Footer className="flex-col gap-3">
68
+ <Description className="text-sm">
69
+ {localization.auth.rememberYourPassword}{" "}
70
+ <Link
71
+ href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
72
+ className="text-accent no-underline hover:underline decoration-accent-hover"
73
+ >
74
+ {localization.auth.signIn}
75
+ </Link>
76
+ </Description>
77
+ </Card.Footer>
78
+ </Card>
79
+ )
80
+ }
@@ -25,6 +25,8 @@ import {
25
25
  } from "@heroui/react"
26
26
  import { type SyntheticEvent, useState } from "react"
27
27
 
28
+ import { OpenEmailButton } from "../../open-email-button"
29
+
28
30
  export type ChangePasswordProps = {
29
31
  className?: string
30
32
  variant?: CardProps["variant"]
@@ -77,6 +79,7 @@ function SetPassword({
77
79
  const { authClient, localization, plugins } = useAuth()
78
80
  const { data: session } = useSession(authClient)
79
81
  const { fetchOptions, resetFetchOptions } = useFetchOptions()
82
+ const [sentEmail, setSentEmail] = useState("")
80
83
 
81
84
  const { mutate: requestPasswordReset, isPending } = useRequestPasswordReset(
82
85
  authClient,
@@ -84,7 +87,9 @@ function SetPassword({
84
87
  onError: () => {
85
88
  resetFetchOptions()
86
89
  },
87
- onSuccess: () => toast.success(localization.auth.passwordResetEmailSent)
90
+ onSuccess: (_data, { email }) => {
91
+ setSentEmail(email)
92
+ }
88
93
  }
89
94
  )
90
95
 
@@ -115,19 +120,32 @@ function SetPassword({
115
120
  </p>
116
121
  </div>
117
122
 
118
- <div className="flex flex-col gap-3 items-start sm:items-end">
119
- {Captcha && <div>{Captcha}</div>}
120
-
121
- <Button
122
- size="sm"
123
- isPending={isPending}
124
- isDisabled={!session?.user.email}
125
- onPress={handleSetPassword}
126
- >
127
- {isPending && <Spinner color="current" size="sm" />}
128
- {localization.auth.sendResetLink}
129
- </Button>
130
- </div>
123
+ {sentEmail ? (
124
+ <div className="flex flex-col gap-3 items-start sm:items-end">
125
+ <p className="text-sm" role="status">
126
+ {localization.auth.resetLinkSentTo.replace(
127
+ "{{email}}",
128
+ sentEmail
129
+ )}
130
+ </p>
131
+
132
+ <OpenEmailButton email={sentEmail} className="w-auto" />
133
+ </div>
134
+ ) : (
135
+ <div className="flex flex-col gap-3 items-start sm:items-end">
136
+ {Captcha && <div>{Captcha}</div>}
137
+
138
+ <Button
139
+ size="sm"
140
+ isPending={isPending}
141
+ isDisabled={!session?.user.email}
142
+ onPress={handleSetPassword}
143
+ >
144
+ {isPending && <Spinner color="current" size="sm" />}
145
+ {localization.auth.sendResetLink}
146
+ </Button>
147
+ </div>
148
+ )}
131
149
  </Card.Content>
132
150
  </Card>
133
151
  </div>
@@ -82,16 +82,31 @@ export function SignInUsername({
82
82
 
83
83
  resetFetchOptions()
84
84
  },
85
- onSuccess: () => navigate({ to: redirectTo })
85
+ onSuccess: () => {
86
+ sessionStorage.removeItem("better-auth-ui.verify-email")
87
+ navigate({ to: redirectTo })
88
+ }
86
89
  })
87
90
 
88
91
  const { mutate: signInUsername, isPending: isSignInUsernamePending } =
89
92
  useSignInUsername(authClient as UsernameAuthClient, {
90
- onError: () => {
93
+ onError: (error) => {
91
94
  setPassword("")
95
+
96
+ if (error.error?.code === "EMAIL_NOT_VERIFIED") {
97
+ sessionStorage.removeItem("better-auth-ui.verify-email")
98
+
99
+ navigate({
100
+ to: `${basePaths.auth}/${viewPaths.auth.verifyEmail}`
101
+ })
102
+ }
103
+
92
104
  resetFetchOptions()
93
105
  },
94
- onSuccess: () => navigate({ to: redirectTo })
106
+ onSuccess: () => {
107
+ sessionStorage.removeItem("better-auth-ui.verify-email")
108
+ navigate({ to: redirectTo })
109
+ }
95
110
  })
96
111
 
97
112
  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
@@ -27,9 +27,9 @@ const RESEND_COOLDOWN_SECONDS = 60
27
27
  * that is rate-limited by a cooldown timer.
28
28
  *
29
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.
30
+ * redirects here); the OpenEmail/Resend controls are only shown when an email
31
+ * is stored. The resend button is disabled while a cooldown is active and shows
32
+ * the remaining seconds.
33
33
  *
34
34
  * @param className - Additional CSS classes applied to the outer card container
35
35
  * @param variant - Variant to apply to the card
@@ -41,7 +41,6 @@ export function VerifyEmail({ className, variant }: VerifyEmailProps) {
41
41
  basePaths,
42
42
  baseURL,
43
43
  localization,
44
- navigate,
45
44
  redirectTo,
46
45
  viewPaths
47
46
  } = useAuth()
@@ -53,25 +52,18 @@ export function VerifyEmail({ className, variant }: VerifyEmailProps) {
53
52
  const [cooldown, setCooldown] = useState(RESEND_COOLDOWN_SECONDS)
54
53
 
55
54
  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])
55
+ setEmail(sessionStorage.getItem("better-auth-ui.verify-email") ?? "")
56
+ }, [])
65
57
 
66
58
  useEffect(() => {
67
- if (cooldown <= 0) return
59
+ if (cooldown <= 0 || !email) return
68
60
 
69
61
  const interval = setInterval(() => {
70
62
  setCooldown((current) => (current > 0 ? current - 1 : 0))
71
63
  }, 1000)
72
64
 
73
65
  return () => clearInterval(interval)
74
- }, [cooldown])
66
+ }, [cooldown, email])
75
67
 
76
68
  const { mutate: sendVerificationEmail, isPending } = useSendVerificationEmail(
77
69
  authClient,
@@ -101,31 +93,33 @@ export function VerifyEmail({ className, variant }: VerifyEmailProps) {
101
93
  {localization.auth.checkYourEmail}
102
94
  </Description>
103
95
 
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>
96
+ {email && (
97
+ <div className="flex flex-col gap-3">
98
+ <OpenEmailButton email={email} />
99
+
100
+ <Button
101
+ className="w-full"
102
+ variant="tertiary"
103
+ isDisabled={!email || isCoolingDown || isPending}
104
+ isPending={isPending}
105
+ onPress={() =>
106
+ sendVerificationEmail({
107
+ email,
108
+ callbackURL: `${baseURL}${redirectTo}`
109
+ })
110
+ }
111
+ >
112
+ {isPending && <Spinner color="current" size="sm" />}
113
+
114
+ {isCoolingDown
115
+ ? localization.auth.resendIn.replace(
116
+ "{{seconds}}",
117
+ String(cooldown)
118
+ )
119
+ : localization.auth.resend}
120
+ </Button>
121
+ </div>
122
+ )}
129
123
  </Card.Content>
130
124
 
131
125
  <Card.Footer className="flex-col gap-3">
package/src/index.tsx CHANGED
@@ -7,6 +7,7 @@ export * from "./components/auth/forgot-password"
7
7
  export * from "./components/auth/open-email-button"
8
8
  export * from "./components/auth/provider-button"
9
9
  export * from "./components/auth/provider-buttons"
10
+ export * from "./components/auth/reset-link-sent"
10
11
  export * from "./components/auth/reset-password"
11
12
  export * from "./components/auth/settings/account/account-settings"
12
13
  export * from "./components/auth/settings/account/change-email"
@@ -6,6 +6,7 @@ import {
6
6
 
7
7
  import { MagicLink } from "../../components/auth/magic-link/magic-link"
8
8
  import { MagicLinkButton } from "../../components/auth/magic-link/magic-link-button"
9
+ import { MagicLinkSent } from "../../components/auth/magic-link/magic-link-sent"
9
10
 
10
11
  export const magicLinkPlugin = createAuthPlugin(
11
12
  coreMagicLinkPlugin.id,
@@ -13,7 +14,7 @@ export const magicLinkPlugin = createAuthPlugin(
13
14
  ...coreMagicLinkPlugin(options),
14
15
  authButtons: [MagicLinkButton],
15
16
  views: {
16
- auth: { magicLink: MagicLink }
17
+ auth: { magicLink: MagicLink, magicLinkSent: MagicLinkSent }
17
18
  },
18
19
  // Conditional, not an override: when `emailAndPassword.enabled === false`
19
20
  // the `<Auth>` router renders this at `/auth/sign-in` instead of the
package/src/plugins.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./components/auth/delete-user/danger-zone"
6
6
  export * from "./components/auth/delete-user/delete-account"
7
7
  // Plugin-contributed components that may be used standalone
8
8
  export * from "./components/auth/magic-link/magic-link"
9
+ export * from "./components/auth/magic-link/magic-link-sent"
9
10
  export * from "./components/auth/multi-session/manage-account"
10
11
  export * from "./components/auth/multi-session/manage-accounts"
11
12
  export * from "./components/auth/multi-session/switch-account-submenu"