@better-auth-ui/heroui 1.6.27 → 1.6.29

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 (31) hide show
  1. package/dist/components/auth/forgot-password.d.ts +3 -1
  2. package/dist/components/auth/last-login-method/last-used-badge.d.ts +13 -0
  3. package/dist/components/auth/magic-link/magic-link-sent.d.ts +20 -0
  4. package/dist/components/auth/provider-button.d.ts +3 -1
  5. package/dist/components/auth/provider-buttons.d.ts +3 -1
  6. package/dist/components/auth/reset-link-sent.d.ts +20 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +280 -220
  9. package/dist/lib/auth/last-login-method-plugin.d.ts +12 -0
  10. package/dist/lib/auth/magic-link-plugin.d.ts +4 -0
  11. package/dist/plugins.d.ts +3 -0
  12. package/dist/plugins.js +1070 -1005
  13. package/dist/user-view-D9ial2fQ.js +150 -0
  14. package/package.json +1 -1
  15. package/src/components/auth/auth.tsx +8 -1
  16. package/src/components/auth/forgot-password.tsx +9 -6
  17. package/src/components/auth/last-login-method/last-used-badge.tsx +40 -0
  18. package/src/components/auth/magic-link/magic-link-sent.tsx +84 -0
  19. package/src/components/auth/magic-link/magic-link.tsx +12 -8
  20. package/src/components/auth/provider-button.tsx +17 -3
  21. package/src/components/auth/provider-buttons.tsx +5 -1
  22. package/src/components/auth/reset-link-sent.tsx +80 -0
  23. package/src/components/auth/settings/security/change-password.tsx +32 -14
  24. package/src/components/auth/sign-in.tsx +10 -3
  25. package/src/components/auth/sign-up.tsx +2 -2
  26. package/src/components/auth/username/sign-in-username.tsx +6 -3
  27. package/src/index.tsx +1 -0
  28. package/src/lib/auth/last-login-method-plugin.ts +12 -0
  29. package/src/lib/auth/magic-link-plugin.ts +2 -1
  30. package/src/plugins.ts +3 -0
  31. package/dist/user-view--47tVtJ3.js +0 -113
@@ -0,0 +1,150 @@
1
+ import { authMutationKeys as e, getEmailProviderLink as t, getProviderName as n } from "@better-auth-ui/core";
2
+ import { providerIcons as r, useAuth as i, useLastLoginMethod as a, useSession as o, useSignInSocial as s } from "@better-auth-ui/react";
3
+ import { ArrowUpRightFromSquare as c, Person as l } from "@gravity-ui/icons";
4
+ import { Avatar as u, Button as d, Chip as f, Link as p, Separator as m, Skeleton as h, Spinner as g, cn as _ } from "@heroui/react";
5
+ import { useMemo as v } from "react";
6
+ import { jsx as y, jsxs as b } from "react/jsx-runtime";
7
+ import { buttonVariants as x } from "@heroui/styles";
8
+ import { useIsMutating as S } from "@tanstack/react-query";
9
+ //#region src/components/auth/open-email-button.tsx
10
+ function C({ email: e, className: n }) {
11
+ let { localization: r } = i(), a = t(e);
12
+ return a ? /* @__PURE__ */ b(p, {
13
+ href: a.loginUrl,
14
+ target: "_blank",
15
+ rel: "noopener noreferrer",
16
+ className: _(x({ variant: "primary" }), "w-full gap-2", n),
17
+ children: [r.auth.openEmailProvider.replace("{{provider}}", a.companyProvider), /* @__PURE__ */ y(c, { className: "size-3" })]
18
+ }) : null;
19
+ }
20
+ //#endregion
21
+ //#region src/components/auth/field-separator.tsx
22
+ function w({ children: e }) {
23
+ return /* @__PURE__ */ b("div", {
24
+ className: "flex items-center gap-4",
25
+ children: [
26
+ /* @__PURE__ */ y(m, { className: "flex-1" }),
27
+ /* @__PURE__ */ y("p", {
28
+ className: "text-xs text-muted shrink-0",
29
+ children: e
30
+ }),
31
+ /* @__PURE__ */ y(m, { className: "flex-1" })
32
+ ]
33
+ });
34
+ }
35
+ //#endregion
36
+ //#region src/components/auth/last-login-method/last-used-badge.tsx
37
+ function T({ method: e, compact: t, floating: n }) {
38
+ let { method: r, localization: i } = a();
39
+ return !r || !(Array.isArray(e) ? e : [e]).includes(r) ? null : /* @__PURE__ */ y(f, {
40
+ className: _(n && "pointer-events-none absolute top-0 right-0 z-10 translate-x-1/4 -translate-y-1/2 shadow-sm"),
41
+ color: n ? "accent" : "default",
42
+ size: "sm",
43
+ variant: n ? "primary" : "secondary",
44
+ children: t || n ? i.lastUsedShort : i.lastUsed
45
+ });
46
+ }
47
+ //#endregion
48
+ //#region src/components/auth/provider-button.tsx
49
+ function E({ provider: t, display: a = "full", view: o = "signIn", variant: c = "tertiary", className: l, ...u }) {
50
+ let { authClient: f, baseURL: p, localization: m, redirectTo: h } = i(), v = `${p}${h}`, { mutate: x, isPending: C } = s(f), w = r[t];
51
+ return /* @__PURE__ */ b(d, {
52
+ variant: c,
53
+ isPending: S({ mutationKey: e.signIn.all }) + S({ mutationKey: e.signUp.all }) > 0,
54
+ onPress: () => x({
55
+ provider: t,
56
+ callbackURL: v
57
+ }),
58
+ className: _("relative overflow-visible", l),
59
+ ...u,
60
+ children: [
61
+ C ? /* @__PURE__ */ y(g, {
62
+ color: "current",
63
+ size: "sm"
64
+ }) : /* @__PURE__ */ y(w, {}),
65
+ a === "full" ? m.auth.continueWith.replace("{{provider}}", n(t)) : a === "name" ? n(t) : null,
66
+ a === "icon" && /* @__PURE__ */ y("span", {
67
+ className: "sr-only",
68
+ children: n(t)
69
+ }),
70
+ o !== "signUp" && /* @__PURE__ */ y(T, {
71
+ method: t,
72
+ floating: !0
73
+ })
74
+ ]
75
+ });
76
+ }
77
+ //#endregion
78
+ //#region src/components/auth/provider-buttons.tsx
79
+ function D({ socialLayout: e = "auto", view: t = "signIn" }) {
80
+ let { socialProviders: n } = i(), r = v(() => e === "auto" ? n?.length && n.length >= 4 ? "horizontal" : "vertical" : e, [e, n?.length]);
81
+ return /* @__PURE__ */ y("div", {
82
+ className: _("gap-3", r === "grid" && "grid grid-cols-2", r === "vertical" && "flex flex-col", r === "horizontal" && "flex flex-wrap"),
83
+ children: n?.map((e) => /* @__PURE__ */ y(E, {
84
+ provider: e,
85
+ view: t,
86
+ display: r === "vertical" ? "full" : r === "grid" ? "name" : "icon",
87
+ className: _("w-full", r === "horizontal" && "flex-1")
88
+ }, e))
89
+ });
90
+ }
91
+ //#endregion
92
+ //#region src/components/auth/user/user-avatar.tsx
93
+ function O({ className: e, fallback: t, isPending: n, user: r, size: a = "sm", style: s, ...c }) {
94
+ let { authClient: d } = i(), { data: f, isPending: p } = o(d, { enabled: !r && !n });
95
+ if ((n || p) && !r) return /* @__PURE__ */ y(h, {
96
+ className: _("rounded-full", a === "sm" ? "size-8" : a === "md" ? "size-10" : "size-12", e),
97
+ style: s
98
+ });
99
+ let m = r ?? f?.user, g = (m?.username || m?.name || m?.email)?.slice(0, 2).toUpperCase();
100
+ return /* @__PURE__ */ b(u, {
101
+ size: a,
102
+ className: _("rounded-full", e),
103
+ style: s,
104
+ ...c,
105
+ children: [/* @__PURE__ */ y(u.Image, {
106
+ alt: m?.displayUsername || m?.name || m?.email,
107
+ src: m?.image ?? void 0
108
+ }), /* @__PURE__ */ y(u.Fallback, {
109
+ className: _(a === "lg" ? "text-xl" : a === "md" ? "text-base" : "text-sm"),
110
+ delayMs: m?.image ? 600 : void 0,
111
+ children: t || g || /* @__PURE__ */ y(l, { className: "size-4" })
112
+ })]
113
+ });
114
+ }
115
+ //#endregion
116
+ //#region src/components/auth/user/user-view.tsx
117
+ function k({ className: e, isPending: t, size: n = "md", hideSubtitle: r = !1, user: a, ...s }) {
118
+ let { authClient: c } = i(), { data: l, isPending: u } = o(c, { enabled: !a && !t }), d = a ?? l?.user;
119
+ return (t || u) && !a ? /* @__PURE__ */ b("div", {
120
+ className: _("flex items-center gap-2 min-w-0", e),
121
+ ...s,
122
+ children: [/* @__PURE__ */ y(O, {
123
+ isPending: !0,
124
+ className: n === "sm" ? "size-5 [&>span]:text-xs" : void 0,
125
+ size: n === "lg" ? "md" : "sm"
126
+ }), /* @__PURE__ */ b("div", {
127
+ className: "flex flex-col gap-1 min-w-0",
128
+ children: [/* @__PURE__ */ y(h, { className: "h-3.5 w-24 rounded-lg" }), r ? null : /* @__PURE__ */ y(h, { className: "h-3 w-32 rounded-lg" })]
129
+ })]
130
+ }) : /* @__PURE__ */ b("div", {
131
+ className: _("flex items-center gap-2 min-w-0", e),
132
+ ...s,
133
+ children: [/* @__PURE__ */ y(O, {
134
+ className: n === "sm" ? "size-5 [&>span]:text-xs" : void 0,
135
+ user: d,
136
+ size: n === "lg" ? "md" : "sm"
137
+ }), /* @__PURE__ */ b("div", {
138
+ className: "flex flex-col min-w-0",
139
+ children: [/* @__PURE__ */ y("p", {
140
+ className: "text-foreground text-sm font-medium leading-tight truncate",
141
+ children: d?.displayUsername || d?.name || d?.email
142
+ }), !r && (d?.displayUsername || d?.name) ? /* @__PURE__ */ y("p", {
143
+ className: "text-muted text-xs leading-tight truncate overflow-x-hidden",
144
+ children: d?.email
145
+ }) : null]
146
+ })]
147
+ });
148
+ }
149
+ //#endregion
150
+ export { T as a, E as i, O as n, w as o, D as r, C as s, k as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth-ui/heroui",
3
- "version": "1.6.27",
3
+ "version": "1.6.29",
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,40 @@
1
+ import { useLastLoginMethod } from "@better-auth-ui/react"
2
+ import { Chip, cn } from "@heroui/react"
3
+
4
+ export type LastUsedBadgeProps = {
5
+ /** Login method IDs that should display the indicator. */
6
+ method: string | string[]
7
+ /** Use the shorter label in constrained layouts. */
8
+ compact?: boolean
9
+ /** Float the compact indicator over the top-right edge of its container. */
10
+ floating?: boolean
11
+ }
12
+
13
+ /**
14
+ * Displays an indicator when one of the supplied method IDs matches Better
15
+ * Auth's stored last login method.
16
+ */
17
+ export function LastUsedBadge({
18
+ method,
19
+ compact,
20
+ floating
21
+ }: LastUsedBadgeProps) {
22
+ const { method: lastLoginMethod, localization } = useLastLoginMethod()
23
+ const methods = Array.isArray(method) ? method : [method]
24
+
25
+ if (!lastLoginMethod || !methods.includes(lastLoginMethod)) return null
26
+
27
+ return (
28
+ <Chip
29
+ className={cn(
30
+ floating &&
31
+ "pointer-events-none absolute top-0 right-0 z-10 translate-x-1/4 -translate-y-1/2 shadow-sm"
32
+ )}
33
+ color={floating ? "accent" : "default"}
34
+ size="sm"
35
+ variant={floating ? "primary" : "secondary"}
36
+ >
37
+ {compact || floating ? localization.lastUsedShort : localization.lastUsed}
38
+ </Chip>
39
+ )
40
+ }
@@ -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
 
@@ -100,7 +104,7 @@ export function MagicLink({
100
104
  {socialPosition === "top" && (
101
105
  <>
102
106
  {!!socialProviders?.length && (
103
- <ProviderButtons socialLayout={socialLayout} />
107
+ <ProviderButtons socialLayout={socialLayout} view="magicLink" />
104
108
  )}
105
109
 
106
110
  {showSeparator && (
@@ -154,7 +158,7 @@ export function MagicLink({
154
158
  )}
155
159
 
156
160
  {!!socialProviders?.length && (
157
- <ProviderButtons socialLayout={socialLayout} />
161
+ <ProviderButtons socialLayout={socialLayout} view="magicLink" />
158
162
  )}
159
163
  </>
160
164
  )}
@@ -1,12 +1,18 @@
1
- import { authMutationKeys, getProviderName } from "@better-auth-ui/core"
1
+ import {
2
+ type AuthView,
3
+ authMutationKeys,
4
+ getProviderName
5
+ } from "@better-auth-ui/core"
2
6
  import { providerIcons, useAuth, useSignInSocial } from "@better-auth-ui/react"
3
- import { Button, type ButtonProps, Spinner } from "@heroui/react"
7
+ import { Button, type ButtonProps, cn, Spinner } from "@heroui/react"
4
8
  import { useIsMutating } from "@tanstack/react-query"
5
9
  import type { SocialProvider } from "better-auth/social-providers"
10
+ import { LastUsedBadge } from "./last-login-method/last-used-badge"
6
11
 
7
12
  export type ProviderButtonProps = {
8
13
  provider: SocialProvider
9
14
  display?: "full" | "name" | "icon"
15
+ view?: AuthView
10
16
  } & Omit<ButtonProps, "children" | "onPress" | "isPending" | "isDisabled">
11
17
 
12
18
  /**
@@ -18,7 +24,9 @@ export type ProviderButtonProps = {
18
24
  export function ProviderButton({
19
25
  provider,
20
26
  display = "full",
27
+ view = "signIn",
21
28
  variant = "tertiary",
29
+ className,
22
30
  ...props
23
31
  }: ProviderButtonProps) {
24
32
  const { authClient, baseURL, localization, redirectTo } = useAuth()
@@ -43,8 +51,8 @@ export function ProviderButton({
43
51
  variant={variant}
44
52
  isPending={isPending}
45
53
  onPress={() => signInSocial({ provider, callbackURL })}
54
+ className={cn("relative overflow-visible", className)}
46
55
  {...props}
47
- aria-label={getProviderName(provider)}
48
56
  >
49
57
  {signInSocialPending ? (
50
58
  <Spinner color="current" size="sm" />
@@ -60,6 +68,12 @@ export function ProviderButton({
60
68
  : display === "name"
61
69
  ? getProviderName(provider)
62
70
  : null}
71
+
72
+ {display === "icon" && (
73
+ <span className="sr-only">{getProviderName(provider)}</span>
74
+ )}
75
+
76
+ {view !== "signUp" && <LastUsedBadge method={provider} floating />}
63
77
  </Button>
64
78
  )
65
79
  }
@@ -1,9 +1,11 @@
1
+ import type { AuthView } from "@better-auth-ui/core"
1
2
  import { useAuth } from "@better-auth-ui/react"
2
3
  import { cn } from "@heroui/react"
3
4
  import { useMemo } from "react"
4
5
  import { ProviderButton } from "./provider-button"
5
6
  export type ProviderButtonsProps = {
6
7
  socialLayout?: SocialLayout
8
+ view?: AuthView
7
9
  }
8
10
 
9
11
  export type SocialLayout = "auto" | "horizontal" | "vertical" | "grid"
@@ -16,7 +18,8 @@ export type SocialLayout = "auto" | "horizontal" | "vertical" | "grid"
16
18
  * @returns The JSX element that renders the configured social provider buttons.
17
19
  */
18
20
  export function ProviderButtons({
19
- socialLayout = "auto"
21
+ socialLayout = "auto",
22
+ view = "signIn"
20
23
  }: ProviderButtonsProps) {
21
24
  const { socialProviders } = useAuth()
22
25
 
@@ -45,6 +48,7 @@ export function ProviderButtons({
45
48
  <ProviderButton
46
49
  key={provider}
47
50
  provider={provider}
51
+ view={view}
48
52
  display={
49
53
  resolvedSocialLayout === "vertical"
50
54
  ? "full"
@@ -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>