@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.
- package/dist/components/auth/forgot-password.d.ts +3 -1
- package/dist/components/auth/magic-link/magic-link-sent.d.ts +20 -0
- package/dist/components/auth/reset-link-sent.d.ts +20 -0
- package/dist/components/auth/verify-email.d.ts +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +265 -233
- package/dist/lib/auth/magic-link-plugin.d.ts +4 -0
- package/dist/plugins.d.ts +1 -0
- package/dist/plugins.js +431 -384
- package/dist/user-view-CxSDufx_.js +126 -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/magic-link/magic-link-sent.tsx +84 -0
- package/src/components/auth/magic-link/magic-link.tsx +10 -6
- 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/username/sign-in-username.tsx +18 -3
- package/src/components/auth/verify-email.tsx +34 -40
- package/src/index.tsx +1 -0
- package/src/lib/auth/magic-link-plugin.ts +2 -1
- package/src/plugins.ts +1 -0
- package/dist/user-view--47tVtJ3.js +0 -113
|
@@ -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
|
@@ -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,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
|
|
|
@@ -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>
|
|
@@ -82,16 +82,31 @@ export function SignInUsername({
|
|
|
82
82
|
|
|
83
83
|
resetFetchOptions()
|
|
84
84
|
},
|
|
85
|
-
onSuccess: () =>
|
|
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: () =>
|
|
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);
|
|
31
|
-
* The resend button is disabled while a cooldown is active and shows
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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"
|