@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.
- package/dist/components/auth/forgot-password.d.ts +3 -1
- package/dist/components/auth/last-login-method/last-used-badge.d.ts +13 -0
- package/dist/components/auth/magic-link/magic-link-sent.d.ts +20 -0
- package/dist/components/auth/provider-button.d.ts +3 -1
- package/dist/components/auth/provider-buttons.d.ts +3 -1
- package/dist/components/auth/reset-link-sent.d.ts +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +280 -220
- package/dist/lib/auth/last-login-method-plugin.d.ts +12 -0
- package/dist/lib/auth/magic-link-plugin.d.ts +4 -0
- package/dist/plugins.d.ts +3 -0
- package/dist/plugins.js +1070 -1005
- package/dist/user-view-D9ial2fQ.js +150 -0
- package/package.json +1 -1
- package/src/components/auth/auth.tsx +8 -1
- package/src/components/auth/forgot-password.tsx +9 -6
- package/src/components/auth/last-login-method/last-used-badge.tsx +40 -0
- package/src/components/auth/magic-link/magic-link-sent.tsx +84 -0
- package/src/components/auth/magic-link/magic-link.tsx +12 -8
- package/src/components/auth/provider-button.tsx +17 -3
- package/src/components/auth/provider-buttons.tsx +5 -1
- package/src/components/auth/reset-link-sent.tsx +80 -0
- package/src/components/auth/settings/security/change-password.tsx +32 -14
- package/src/components/auth/sign-in.tsx +10 -3
- package/src/components/auth/sign-up.tsx +2 -2
- package/src/components/auth/username/sign-in-username.tsx +6 -3
- package/src/index.tsx +1 -0
- package/src/lib/auth/last-login-method-plugin.ts +12 -0
- package/src/lib/auth/magic-link-plugin.ts +2 -1
- package/src/plugins.ts +3 -0
- 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
|
@@ -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 = [
|
|
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
|
-
*
|
|
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
|
-
|
|
58
|
-
navigate({ to: `${basePaths.auth}/${viewPaths.auth.
|
|
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 } =
|
|
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
|
-
|
|
69
|
-
|
|
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 {
|
|
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: () =>
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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>
|