@better-auth-ui/heroui 1.6.2 → 1.6.4

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 (213) hide show
  1. package/dist/components/auth/additional-field.d.ts +4 -0
  2. package/dist/components/auth/additional-field.js +270 -0
  3. package/dist/components/auth/auth-provider.d.ts +4 -6
  4. package/dist/components/auth/auth-provider.js +12 -11
  5. package/dist/components/auth/auth.d.ts +1 -1
  6. package/dist/components/auth/auth.js +54 -28
  7. package/dist/components/{settings/security → auth/delete-user}/danger-zone.d.ts +3 -4
  8. package/dist/components/{settings/security → auth/delete-user}/danger-zone.js +1 -1
  9. package/dist/components/{settings/security → auth/delete-user}/delete-user.d.ts +1 -1
  10. package/dist/components/auth/delete-user/delete-user.js +95 -0
  11. package/dist/components/{email → auth/email}/email-changed-email.d.ts +2 -2
  12. package/dist/components/{email → auth/email}/email-changed-email.js +7 -7
  13. package/dist/components/{email → auth/email}/email-verification-email.d.ts +2 -2
  14. package/dist/components/{email → auth/email}/email-verification-email.js +7 -7
  15. package/dist/components/auth/email/magic-link-email.d.ts +3 -0
  16. package/dist/components/{email → auth/email}/magic-link-email.js +7 -7
  17. package/dist/components/auth/email/new-device-email.d.ts +3 -0
  18. package/dist/components/{email → auth/email}/new-device-email.js +7 -7
  19. package/dist/components/auth/email/otp-email.d.ts +3 -0
  20. package/dist/components/{email → auth/email}/otp-email.js +6 -6
  21. package/dist/components/{email → auth/email}/password-changed-email.d.ts +2 -2
  22. package/dist/components/{email → auth/email}/password-changed-email.js +7 -7
  23. package/dist/components/{email → auth/email}/reset-password-email.d.ts +2 -2
  24. package/dist/components/{email → auth/email}/reset-password-email.js +7 -7
  25. package/dist/components/auth/error-toaster.d.ts +1 -0
  26. package/dist/components/auth/error-toaster.js +17 -0
  27. package/dist/components/auth/forgot-password.d.ts +2 -2
  28. package/dist/components/auth/forgot-password.js +42 -42
  29. package/dist/components/auth/magic-link/magic-link-button.d.ts +11 -0
  30. package/dist/components/auth/magic-link/magic-link-button.js +20 -0
  31. package/dist/components/auth/magic-link/magic-link.d.ts +16 -0
  32. package/dist/components/auth/magic-link/magic-link.js +87 -0
  33. package/dist/components/{settings/account → auth/multi-session}/manage-account.d.ts +2 -6
  34. package/dist/components/auth/multi-session/manage-account.js +49 -0
  35. package/dist/components/{settings/account → auth/multi-session}/manage-accounts.d.ts +1 -1
  36. package/dist/components/auth/multi-session/manage-accounts.js +34 -0
  37. package/dist/components/{user/switch-account-menu.d.ts → auth/multi-session/switch-account-submenu-content.d.ts} +3 -3
  38. package/dist/components/auth/multi-session/switch-account-submenu-content.js +29 -0
  39. package/dist/components/{user/switch-account-item.d.ts → auth/multi-session/switch-account-submenu-item.d.ts} +2 -2
  40. package/dist/components/auth/multi-session/switch-account-submenu-item.js +20 -0
  41. package/dist/components/auth/multi-session/switch-account-submenu.d.ts +13 -0
  42. package/dist/components/auth/multi-session/switch-account-submenu.js +24 -0
  43. package/dist/components/auth/passkey/passkey-button.d.ts +6 -0
  44. package/dist/components/auth/passkey/passkey-button.js +24 -0
  45. package/dist/components/auth/passkey/passkey.js +45 -0
  46. package/dist/components/{settings/security → auth/passkey}/passkeys.d.ts +1 -1
  47. package/dist/components/auth/passkey/passkeys.js +106 -0
  48. package/dist/components/auth/provider-button.d.ts +13 -0
  49. package/dist/components/auth/provider-button.js +25 -0
  50. package/dist/components/auth/provider-buttons.d.ts +3 -10
  51. package/dist/components/auth/provider-buttons.js +14 -22
  52. package/dist/components/auth/reset-password.d.ts +1 -1
  53. package/dist/components/auth/reset-password.js +110 -110
  54. package/dist/components/{settings → auth/settings}/account/account-settings.d.ts +5 -2
  55. package/dist/components/auth/settings/account/account-settings.js +20 -0
  56. package/dist/components/auth/settings/account/change-avatar.js +89 -0
  57. package/dist/components/{settings → auth/settings}/account/change-email.d.ts +1 -1
  58. package/dist/components/{settings → auth/settings}/account/change-email.js +17 -20
  59. package/dist/components/{settings → auth/settings}/account/user-profile.d.ts +3 -4
  60. package/dist/components/auth/settings/account/user-profile.js +86 -0
  61. package/dist/components/auth/settings/security/active-session.js +59 -0
  62. package/dist/components/{settings → auth/settings}/security/active-sessions.d.ts +1 -1
  63. package/dist/components/auth/settings/security/active-sessions.js +34 -0
  64. package/dist/components/{settings → auth/settings}/security/change-password.d.ts +1 -1
  65. package/dist/components/auth/settings/security/change-password.js +189 -0
  66. package/dist/components/auth/settings/security/linked-account.js +56 -0
  67. package/dist/components/{settings → auth/settings}/security/linked-accounts.d.ts +1 -1
  68. package/dist/components/auth/settings/security/linked-accounts.js +45 -0
  69. package/dist/components/{settings → auth/settings}/security/security-settings.d.ts +1 -1
  70. package/dist/components/auth/settings/security/security-settings.js +22 -0
  71. package/dist/components/auth/settings/settings.js +45 -0
  72. package/dist/components/auth/sign-in.d.ts +1 -1
  73. package/dist/components/auth/sign-in.js +64 -100
  74. package/dist/components/auth/sign-out.js +11 -11
  75. package/dist/components/auth/sign-up.d.ts +1 -1
  76. package/dist/components/auth/sign-up.js +123 -157
  77. package/dist/components/{settings/account → auth/theme}/appearance.d.ts +3 -6
  78. package/dist/components/auth/theme/appearance.js +72 -0
  79. package/dist/components/auth/theme/theme-toggle-item.d.ts +5 -0
  80. package/dist/components/auth/theme/theme-toggle-item.js +58 -0
  81. package/dist/components/auth/user/user-avatar.js +29 -0
  82. package/dist/components/{user → auth/user}/user-button.d.ts +2 -6
  83. package/dist/components/auth/user/user-button.js +55 -0
  84. package/dist/components/auth/user/user-view.js +37 -0
  85. package/dist/components/auth/username/sign-in-username.d.ts +14 -0
  86. package/dist/components/auth/username/sign-in-username.js +142 -0
  87. package/dist/components/auth/username/username-field.d.ts +8 -0
  88. package/dist/components/auth/username/username-field.js +51 -0
  89. package/dist/email.d.ts +7 -0
  90. package/dist/email.js +9 -0
  91. package/dist/index.d.ts +13 -27
  92. package/dist/index.js +22 -36
  93. package/dist/lib/auth/auth-plugin.d.ts +44 -0
  94. package/dist/lib/auth/delete-user-plugin.d.ts +17 -0
  95. package/dist/lib/auth/delete-user-plugin.js +10 -0
  96. package/dist/lib/auth/magic-link-plugin.d.ts +31 -0
  97. package/dist/lib/auth/magic-link-plugin.js +13 -0
  98. package/dist/lib/auth/multi-session-plugin.d.ts +18 -0
  99. package/dist/lib/auth/multi-session-plugin.js +12 -0
  100. package/dist/lib/auth/passkey-plugin.d.ts +20 -0
  101. package/dist/lib/auth/passkey-plugin.js +12 -0
  102. package/dist/lib/auth/theme-plugin.d.ts +68 -0
  103. package/dist/lib/auth/theme-plugin.js +23 -0
  104. package/dist/lib/auth/username-plugin.d.ts +63 -0
  105. package/dist/lib/auth/username-plugin.js +18 -0
  106. package/dist/plugins.d.ts +20 -0
  107. package/dist/plugins.js +21 -0
  108. package/package.json +22 -22
  109. package/src/components/auth/additional-field.tsx +554 -0
  110. package/src/components/auth/auth-provider.tsx +8 -5
  111. package/src/components/auth/auth.tsx +100 -36
  112. package/src/components/{settings/security → auth/delete-user}/danger-zone.tsx +3 -10
  113. package/src/components/{settings/security → auth/delete-user}/delete-user.tsx +31 -24
  114. package/src/components/{email → auth/email}/email-changed-email.tsx +2 -2
  115. package/src/components/{email → auth/email}/email-verification-email.tsx +2 -2
  116. package/src/components/{email → auth/email}/magic-link-email.tsx +2 -2
  117. package/src/components/{email → auth/email}/new-device-email.tsx +2 -2
  118. package/src/components/{email → auth/email}/otp-email.tsx +2 -2
  119. package/src/components/{email → auth/email}/password-changed-email.tsx +2 -2
  120. package/src/components/{email → auth/email}/reset-password-email.tsx +2 -2
  121. package/src/components/auth/error-toaster.tsx +25 -0
  122. package/src/components/auth/forgot-password.tsx +15 -13
  123. package/src/components/auth/magic-link/magic-link-button.tsx +61 -0
  124. package/src/components/auth/{magic-link.tsx → magic-link/magic-link.tsx} +52 -57
  125. package/src/components/{settings/account → auth/multi-session}/manage-account.tsx +15 -15
  126. package/src/components/{settings/account → auth/multi-session}/manage-accounts.tsx +16 -12
  127. package/src/components/{user/switch-account-menu.tsx → auth/multi-session/switch-account-submenu-content.tsx} +19 -17
  128. package/src/components/{user/switch-account-item.tsx → auth/multi-session/switch-account-submenu-item.tsx} +18 -8
  129. package/src/components/auth/multi-session/switch-account-submenu.tsx +50 -0
  130. package/src/components/auth/passkey/passkey-button.tsx +56 -0
  131. package/src/components/{settings/security → auth/passkey}/passkey.tsx +21 -9
  132. package/src/components/auth/passkey/passkeys.tsx +196 -0
  133. package/src/components/auth/provider-button.tsx +65 -0
  134. package/src/components/auth/provider-buttons.tsx +23 -45
  135. package/src/components/auth/reset-password.tsx +14 -9
  136. package/src/components/{settings → auth/settings}/account/account-settings.tsx +15 -13
  137. package/src/components/{settings → auth/settings}/account/change-avatar.tsx +4 -5
  138. package/src/components/{settings → auth/settings}/account/change-email.tsx +5 -6
  139. package/src/components/auth/settings/account/user-profile.tsx +171 -0
  140. package/src/components/{settings → auth/settings}/security/active-session.tsx +8 -6
  141. package/src/components/{settings → auth/settings}/security/active-sessions.tsx +5 -10
  142. package/src/components/{settings → auth/settings}/security/change-password.tsx +22 -14
  143. package/src/components/{settings → auth/settings}/security/linked-account.tsx +10 -16
  144. package/src/components/{settings → auth/settings}/security/linked-accounts.tsx +4 -9
  145. package/src/components/{settings → auth/settings}/security/security-settings.tsx +7 -6
  146. package/src/components/{settings → auth/settings}/settings.tsx +2 -2
  147. package/src/components/auth/sign-in.tsx +39 -80
  148. package/src/components/auth/sign-out.tsx +2 -2
  149. package/src/components/auth/sign-up.tsx +110 -147
  150. package/src/components/{settings/account → auth/theme}/appearance.tsx +18 -25
  151. package/src/components/auth/theme/theme-toggle-item.tsx +110 -0
  152. package/src/components/{user → auth/user}/user-avatar.tsx +12 -4
  153. package/src/components/{user → auth/user}/user-button.tsx +15 -88
  154. package/src/components/{user → auth/user}/user-view.tsx +12 -4
  155. package/src/components/auth/username/sign-in-username.tsx +283 -0
  156. package/src/components/auth/username/username-field.tsx +122 -0
  157. package/src/email.ts +9 -0
  158. package/src/index.tsx +13 -27
  159. package/src/lib/auth/auth-plugin.ts +67 -0
  160. package/src/lib/auth/delete-user-plugin.ts +15 -0
  161. package/src/lib/auth/magic-link-plugin.ts +26 -0
  162. package/src/lib/auth/multi-session-plugin.ts +17 -0
  163. package/src/lib/auth/passkey-plugin.ts +17 -0
  164. package/src/lib/auth/theme-plugin.ts +86 -0
  165. package/src/lib/auth/username-plugin.tsx +30 -0
  166. package/src/plugins.ts +23 -0
  167. package/dist/components/auth/magic-link-button.d.ts +0 -13
  168. package/dist/components/auth/magic-link-button.js +0 -16
  169. package/dist/components/auth/magic-link.d.ts +0 -17
  170. package/dist/components/auth/magic-link.js +0 -111
  171. package/dist/components/auth/passkey-button.d.ts +0 -4
  172. package/dist/components/auth/passkey-button.js +0 -24
  173. package/dist/components/email/magic-link-email.d.ts +0 -3
  174. package/dist/components/email/new-device-email.d.ts +0 -3
  175. package/dist/components/email/otp-email.d.ts +0 -3
  176. package/dist/components/settings/account/account-settings.js +0 -23
  177. package/dist/components/settings/account/appearance.js +0 -71
  178. package/dist/components/settings/account/change-avatar.js +0 -89
  179. package/dist/components/settings/account/manage-account.js +0 -51
  180. package/dist/components/settings/account/manage-accounts.js +0 -33
  181. package/dist/components/settings/account/user-profile.js +0 -106
  182. package/dist/components/settings/security/active-session.js +0 -62
  183. package/dist/components/settings/security/active-sessions.js +0 -34
  184. package/dist/components/settings/security/change-password.js +0 -192
  185. package/dist/components/settings/security/delete-user.js +0 -97
  186. package/dist/components/settings/security/linked-account.js +0 -62
  187. package/dist/components/settings/security/linked-accounts.js +0 -45
  188. package/dist/components/settings/security/passkey.js +0 -43
  189. package/dist/components/settings/security/passkeys.js +0 -53
  190. package/dist/components/settings/security/security-settings.js +0 -25
  191. package/dist/components/settings/settings.js +0 -45
  192. package/dist/components/user/switch-account-item.js +0 -20
  193. package/dist/components/user/switch-account-menu.js +0 -28
  194. package/dist/components/user/user-avatar.js +0 -29
  195. package/dist/components/user/user-button.js +0 -91
  196. package/dist/components/user/user-view.js +0 -37
  197. package/dist/core.d.ts +0 -1
  198. package/dist/core.js +0 -1
  199. package/dist/react-exports.d.ts +0 -1
  200. package/dist/react-exports.js +0 -1
  201. package/src/components/auth/magic-link-button.tsx +0 -43
  202. package/src/components/auth/passkey-button.tsx +0 -36
  203. package/src/components/settings/account/user-profile.tsx +0 -218
  204. package/src/components/settings/security/passkeys.tsx +0 -104
  205. package/src/core.ts +0 -1
  206. package/src/react-exports.ts +0 -1
  207. /package/dist/components/{settings/security → auth/passkey}/passkey.d.ts +0 -0
  208. /package/dist/components/{settings → auth/settings}/account/change-avatar.d.ts +0 -0
  209. /package/dist/components/{settings → auth/settings}/security/active-session.d.ts +0 -0
  210. /package/dist/components/{settings → auth/settings}/security/linked-account.d.ts +0 -0
  211. /package/dist/components/{settings → auth/settings}/settings.d.ts +0 -0
  212. /package/dist/components/{user → auth/user}/user-avatar.d.ts +0 -0
  213. /package/dist/components/{user → auth/user}/user-view.d.ts +0 -0
@@ -1,7 +1,9 @@
1
+ import { authMutationKeys } from "@better-auth-ui/core"
1
2
  import {
3
+ type MagicLinkAuthClient,
2
4
  useAuth,
3
- useSignInMagicLink,
4
- useSignInSocial
5
+ useAuthPlugin,
6
+ useSignInMagicLink
5
7
  } from "@better-auth-ui/react"
6
8
  import {
7
9
  Button,
@@ -18,12 +20,12 @@ import {
18
20
  TextField,
19
21
  toast
20
22
  } from "@heroui/react"
23
+ import { useIsMutating } from "@tanstack/react-query"
21
24
  import { type SyntheticEvent, useState } from "react"
22
25
 
23
- import { FieldSeparator } from "./field-separator"
24
- import { MagicLinkButton } from "./magic-link-button"
25
- import { PasskeyButton } from "./passkey-button"
26
- import { ProviderButtons, type SocialLayout } from "./provider-buttons"
26
+ import { magicLinkPlugin } from "../../../lib/auth/magic-link-plugin"
27
+ import { FieldSeparator } from "../field-separator"
28
+ import { ProviderButtons, type SocialLayout } from "../provider-buttons"
27
29
 
28
30
  export type MagicLinkProps = {
29
31
  className?: string
@@ -33,12 +35,11 @@ export type MagicLinkProps = {
33
35
  }
34
36
 
35
37
  /**
36
- * Render a card-based sign-in form that sends an email magic link and optionally shows social provider buttons.
38
+ * Magic-link sign-in form.
37
39
  *
38
- * @param className - Additional CSS class names applied to the card container
39
- * @param socialLayout - Layout style for social provider buttons
40
- * @param socialPosition - Position of social provider buttons; `"top"` or `"bottom"`. Defaults to `"bottom"`.
41
- * @returns The magic-link sign-in UI as a JSX element
40
+ * @param socialLayout - Provider button layout.
41
+ * @param socialPosition - `"top"` or `"bottom"`. Defaults to `"bottom"`.
42
+ * @param variant - Card variant.
42
43
  */
43
44
  export function MagicLink({
44
45
  className,
@@ -46,42 +47,37 @@ export function MagicLink({
46
47
  socialPosition = "bottom",
47
48
  variant,
48
49
  ...props
49
- }: MagicLinkProps & CardProps) {
50
+ }: MagicLinkProps & Omit<CardProps, "children">) {
50
51
  const {
52
+ authClient,
51
53
  basePaths,
52
54
  baseURL,
55
+ emailAndPassword,
53
56
  localization,
54
- passkey,
57
+ plugins,
55
58
  redirectTo,
56
59
  socialProviders,
57
60
  viewPaths
58
61
  } = useAuth()
62
+ const { localization: magicLinkLocalization } = useAuthPlugin(magicLinkPlugin)
59
63
 
60
64
  const [email, setEmail] = useState("")
61
65
 
62
- const { mutate: signInMagicLink, isPending: magicLinkPending } =
63
- useSignInMagicLink({
64
- onError: (error) => toast.danger(error.error?.message || error.message),
66
+ const { mutate: signInMagicLink, isPending: signInMagicLinkPending } =
67
+ useSignInMagicLink(authClient as MagicLinkAuthClient, {
65
68
  onSuccess: () => {
66
69
  setEmail("")
67
- toast.success(localization.auth.magicLinkSent)
70
+ toast.success(magicLinkLocalization.magicLinkSent)
68
71
  }
69
72
  })
70
73
 
71
- const [socialRedirecting, setSocialRedirecting] = useState(false)
72
-
73
- const { mutate: signInSocial, isPending: socialPending } = useSignInSocial({
74
- onError: (error) => toast.danger(error.error?.message || error.message),
75
- onSuccess: () => {
76
- setSocialRedirecting(true)
77
-
78
- setTimeout(() => {
79
- setSocialRedirecting(false)
80
- }, 5000)
81
- }
74
+ const signInMutating = useIsMutating({
75
+ mutationKey: authMutationKeys.signIn.all
82
76
  })
83
-
84
- const isPending = magicLinkPending || socialPending || socialRedirecting
77
+ const signUpMutating = useIsMutating({
78
+ mutationKey: authMutationKeys.signUp.all
79
+ })
80
+ const isPending = signInMutating + signUpMutating > 0
85
81
 
86
82
  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
87
83
  e.preventDefault()
@@ -92,7 +88,7 @@ export function MagicLink({
92
88
 
93
89
  return (
94
90
  <Card
95
- className={cn("w-full max-w-sm p-4 md:p-6", className)}
91
+ className={cn("w-full max-w-sm gap-4 md:p-6", className)}
96
92
  variant={variant}
97
93
  {...props}
98
94
  >
@@ -106,11 +102,7 @@ export function MagicLink({
106
102
  {socialPosition === "top" && (
107
103
  <>
108
104
  {!!socialProviders?.length && (
109
- <ProviderButtons
110
- socialLayout={socialLayout}
111
- signInSocial={signInSocial}
112
- isPending={isPending}
113
- />
105
+ <ProviderButtons socialLayout={socialLayout} />
114
106
  )}
115
107
 
116
108
  {showSeparator && (
@@ -141,14 +133,19 @@ export function MagicLink({
141
133
 
142
134
  <div className="flex flex-col gap-3">
143
135
  <Button type="submit" className="w-full" isPending={isPending}>
144
- {isPending && <Spinner color="current" size="sm" />}
136
+ {signInMagicLinkPending && <Spinner color="current" size="sm" />}
145
137
 
146
- {localization.auth.sendMagicLink}
138
+ {magicLinkLocalization.sendMagicLink}
147
139
  </Button>
148
140
 
149
- <MagicLinkButton view="magicLink" isPending={isPending} />
150
-
151
- {passkey && <PasskeyButton isPending={isPending} />}
141
+ {plugins.flatMap((plugin) =>
142
+ (plugin.authButtons ?? []).map((AuthButton, index) => (
143
+ <AuthButton
144
+ key={`${plugin.id}-${index.toString()}`}
145
+ view="magicLink"
146
+ />
147
+ ))
148
+ )}
152
149
  </div>
153
150
  </Form>
154
151
 
@@ -159,27 +156,25 @@ export function MagicLink({
159
156
  )}
160
157
 
161
158
  {!!socialProviders?.length && (
162
- <ProviderButtons
163
- socialLayout={socialLayout}
164
- signInSocial={signInSocial}
165
- isPending={isPending}
166
- />
159
+ <ProviderButtons socialLayout={socialLayout} />
167
160
  )}
168
161
  </>
169
162
  )}
170
163
  </Card.Content>
171
164
 
172
- <Card.Footer className="flex-col">
173
- <Description className="text-sm">
174
- {localization.auth.needToCreateAnAccount}{" "}
175
- <Link
176
- href={`${basePaths.auth}/${viewPaths.auth.signUp}`}
177
- className="text-accent decoration-accent no-underline hover:underline"
178
- >
179
- {localization.auth.signUp}
180
- </Link>
181
- </Description>
182
- </Card.Footer>
165
+ {emailAndPassword?.enabled && (
166
+ <Card.Footer className="flex-col gap-3">
167
+ <Description className="text-sm">
168
+ {localization.auth.needToCreateAnAccount}{" "}
169
+ <Link
170
+ href={`${basePaths.auth}/${viewPaths.auth.signUp}`}
171
+ className="text-accent no-underline hover:underline decoration-accent-hover"
172
+ >
173
+ {localization.auth.signUp}
174
+ </Link>
175
+ </Description>
176
+ </Card.Footer>
177
+ )}
183
178
  </Card>
184
179
  )
185
180
  }
@@ -1,5 +1,8 @@
1
1
  import {
2
+ type ListDeviceSession,
3
+ type MultiSessionAuthClient,
2
4
  useAuth,
5
+ useAuthPlugin,
3
6
  useRevokeMultiSession,
4
7
  useSetActiveSession,
5
8
  useUser
@@ -10,17 +13,13 @@ import {
10
13
  Ellipsis
11
14
  } from "@gravity-ui/icons"
12
15
  import { Button, Dropdown, Spinner, toast } from "@heroui/react"
13
- import type { Session, User } from "better-auth"
14
16
 
15
- import { UserView } from "../../user/user-view"
17
+ import { multiSessionPlugin } from "../../../lib/auth/multi-session-plugin"
16
18
 
17
- export type DeviceSession = {
18
- session: Session
19
- user: User
20
- }
19
+ import { UserView } from "../user/user-view"
21
20
 
22
21
  export type ManageAccountProps = {
23
- deviceSession?: DeviceSession | null
22
+ deviceSession?: ListDeviceSession | null
24
23
  isPending?: boolean
25
24
  }
26
25
 
@@ -37,17 +36,18 @@ export function ManageAccount({
37
36
  deviceSession,
38
37
  isPending
39
38
  }: ManageAccountProps) {
40
- const { localization } = useAuth()
41
- const { data: user } = useUser()
39
+ const { authClient, localization } = useAuth()
40
+ const { localization: multiSessionLocalization } =
41
+ useAuthPlugin(multiSessionPlugin)
42
+ const { data: user } = useUser(authClient)
42
43
 
43
44
  const { mutate: setActiveSession, isPending: isSwitching } =
44
- useSetActiveSession({
45
- onError: (error) => toast.danger(error.error?.message || error.message)
45
+ useSetActiveSession(authClient as MultiSessionAuthClient, {
46
+ onSuccess: () => window.scrollTo({ top: 0 })
46
47
  })
47
48
 
48
49
  const { mutate: revokeSession, isPending: isRevoking } =
49
- useRevokeMultiSession({
50
- onError: (error) => toast.danger(error.error?.message || error.message),
50
+ useRevokeMultiSession(authClient as MultiSessionAuthClient, {
51
51
  onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)
52
52
  })
53
53
 
@@ -92,7 +92,7 @@ export function ManageAccount({
92
92
  <Dropdown.Popover>
93
93
  <Dropdown.Menu>
94
94
  <Dropdown.Item
95
- textValue={localization.auth.switchAccount}
95
+ textValue={multiSessionLocalization.switchAccount}
96
96
  onAction={() =>
97
97
  setActiveSession({
98
98
  sessionToken: deviceSession.session.token
@@ -100,7 +100,7 @@ export function ManageAccount({
100
100
  }
101
101
  >
102
102
  <ArrowRightArrowLeft className="text-muted" />
103
- {localization.auth.switchAccount}
103
+ {multiSessionLocalization.switchAccount}
104
104
  </Dropdown.Item>
105
105
 
106
106
  <Dropdown.Item
@@ -1,9 +1,14 @@
1
1
  import {
2
+ type MultiSessionAuthClient,
2
3
  useAuth,
4
+ useAuthPlugin,
3
5
  useListDeviceSessions,
4
6
  useSession
5
7
  } from "@better-auth-ui/react"
6
- import { Card, type CardProps, cn, toast } from "@heroui/react"
8
+ import { Card, type CardProps, cn } from "@heroui/react"
9
+
10
+ import { multiSessionPlugin } from "../../../lib/auth/multi-session-plugin"
11
+
7
12
  import { ManageAccount } from "./manage-account"
8
13
 
9
14
  export type ManageAccountsProps = {
@@ -23,16 +28,15 @@ export function ManageAccounts({
23
28
  className,
24
29
  variant,
25
30
  ...props
26
- }: ManageAccountsProps & CardProps) {
27
- const { localization } = useAuth()
28
- const { data: session } = useSession()
31
+ }: ManageAccountsProps & Omit<CardProps, "children">) {
32
+ const { authClient } = useAuth()
33
+ const { localization: multiSessionLocalization } =
34
+ useAuthPlugin(multiSessionPlugin)
35
+ const { data: session } = useSession(authClient)
29
36
 
30
- const { data: deviceSessions, isPending } = useListDeviceSessions({
31
- throwOnError: (error) => {
32
- if (error.error) toast.danger(error.error.message)
33
- return false
34
- }
35
- })
37
+ const { data: deviceSessions, isPending } = useListDeviceSessions(
38
+ authClient as MultiSessionAuthClient
39
+ )
36
40
 
37
41
  const otherSessions = deviceSessions?.filter(
38
42
  (deviceSession) => deviceSession.session.id !== session?.session.id
@@ -40,7 +44,7 @@ export function ManageAccounts({
40
44
 
41
45
  const allRows = [
42
46
  {
43
- key: "current",
47
+ key: session?.session.id ?? "current",
44
48
  deviceSession: !isPending ? session : null,
45
49
  isPending
46
50
  },
@@ -54,7 +58,7 @@ export function ManageAccounts({
54
58
  return (
55
59
  <div>
56
60
  <h2 className={cn("text-sm font-semibold mb-3")}>
57
- {localization.settings.manageAccounts}
61
+ {multiSessionLocalization.manageAccounts}
58
62
  </h2>
59
63
 
60
64
  <Card className={cn(className)} variant={variant} {...props}>
@@ -1,32 +1,34 @@
1
1
  import {
2
+ type MultiSessionAuthClient,
2
3
  useAuth,
4
+ useAuthPlugin,
3
5
  useListDeviceSessions,
4
6
  useSession
5
7
  } from "@better-auth-ui/react"
6
8
  import { Check, CirclePlus } from "@gravity-ui/icons"
7
- import { Dropdown, Label, Separator, toast } from "@heroui/react"
9
+ import { Dropdown, Label, Separator } from "@heroui/react"
8
10
 
9
- import { SwitchAccountItem } from "./switch-account-item"
10
- import { UserView } from "./user-view"
11
+ import { multiSessionPlugin } from "../../../lib/auth/multi-session-plugin"
12
+ import { UserView } from "../user/user-view"
13
+ import { SwitchAccountSubmenuItem } from "./switch-account-submenu-item"
11
14
 
12
15
  /**
13
- * Render the menu content for switching between multiple authenticated sessions.
16
+ * Render the submenu content for switching between multiple authenticated sessions.
14
17
  *
15
18
  * Shows the current session with a checkmark, lists other device sessions that can be activated,
16
19
  * and provides an option to add a new account. This component should be rendered inside a
17
20
  * Dropdown.SubmenuTrigger to defer the useListDeviceSessions query until the submenu is opened.
18
21
  *
19
- * @returns The switch account menu content as a JSX element
22
+ * @returns The switch account submenu content as a JSX element
20
23
  */
21
- export function SwitchAccountMenu() {
22
- const { basePaths, viewPaths, localization } = useAuth()
23
- const { data: session } = useSession()
24
- const { data: deviceSessions, isPending } = useListDeviceSessions({
25
- throwOnError: (error) => {
26
- if (error.error) toast.danger(error.error.message)
27
- return false
28
- }
29
- })
24
+ export function SwitchAccountSubmenuContent() {
25
+ const { authClient, basePaths, viewPaths } = useAuth()
26
+ const { localization: multiSessionLocalization } =
27
+ useAuthPlugin(multiSessionPlugin)
28
+ const { data: session } = useSession(authClient)
29
+ const { data: deviceSessions, isPending } = useListDeviceSessions(
30
+ authClient as MultiSessionAuthClient
31
+ )
30
32
 
31
33
  return (
32
34
  <Dropdown.Popover className="min-w-40 md:min-w-56 max-w-[48svw]">
@@ -42,7 +44,7 @@ export function SwitchAccountMenu() {
42
44
  (deviceSession) => deviceSession.session.id !== session?.session?.id
43
45
  )
44
46
  .map((deviceSession) => (
45
- <SwitchAccountItem
47
+ <SwitchAccountSubmenuItem
46
48
  key={deviceSession.session.id}
47
49
  deviceSession={deviceSession}
48
50
  />
@@ -51,12 +53,12 @@ export function SwitchAccountMenu() {
51
53
  <Separator />
52
54
 
53
55
  <Dropdown.Item
54
- textValue={localization.auth.addAccount}
56
+ textValue={multiSessionLocalization.addAccount}
55
57
  href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
56
58
  >
57
59
  <CirclePlus className="text-muted" />
58
60
 
59
- <Label>{localization.auth.addAccount}</Label>
61
+ <Label>{multiSessionLocalization.addAccount}</Label>
60
62
  </Dropdown.Item>
61
63
  </Dropdown.Menu>
62
64
  </Dropdown.Popover>
@@ -1,14 +1,18 @@
1
- import { useSetActiveSession } from "@better-auth-ui/react"
2
- import { Dropdown, Spinner, toast } from "@heroui/react"
1
+ import {
2
+ type MultiSessionAuthClient,
3
+ useAuth,
4
+ useSetActiveSession
5
+ } from "@better-auth-ui/react"
6
+ import { Dropdown, Spinner } from "@heroui/react"
3
7
  import type { Session, User } from "better-auth"
4
- import { UserView } from "./user-view"
8
+ import { UserView } from "../user/user-view"
5
9
 
6
10
  type DeviceSession = {
7
11
  session: Session
8
12
  user: User
9
13
  }
10
14
 
11
- export type SwitchAccountItemProps = {
15
+ export type SwitchAccountSubmenuItemProps = {
12
16
  deviceSession: DeviceSession
13
17
  }
14
18
 
@@ -18,10 +22,16 @@ export type SwitchAccountItemProps = {
18
22
  * @param deviceSession - The device session to display and switch to when pressed
19
23
  * @returns The switch account dropdown item as a JSX element
20
24
  */
21
- export function SwitchAccountItem({ deviceSession }: SwitchAccountItemProps) {
22
- const { mutate: setActiveSession, isPending } = useSetActiveSession({
23
- onError: (error) => toast.danger(error.error?.message || error.message)
24
- })
25
+ export function SwitchAccountSubmenuItem({
26
+ deviceSession
27
+ }: SwitchAccountSubmenuItemProps) {
28
+ const { authClient } = useAuth()
29
+ const { mutate: setActiveSession, isPending } = useSetActiveSession(
30
+ authClient as MultiSessionAuthClient,
31
+ {
32
+ onSuccess: () => window.scrollTo({ top: 0 })
33
+ }
34
+ )
25
35
 
26
36
  return (
27
37
  <Dropdown.Item
@@ -0,0 +1,50 @@
1
+ import { useAuth, useAuthPlugin, useSession } from "@better-auth-ui/react"
2
+ import { Persons } from "@gravity-ui/icons"
3
+ import { Dropdown, Label } from "@heroui/react"
4
+
5
+ import { multiSessionPlugin } from "../../../lib/auth/multi-session-plugin"
6
+
7
+ import { SwitchAccountSubmenuContent } from "./switch-account-submenu-content"
8
+
9
+ export type SwitchAccountSubmenuProps = {
10
+ className?: string
11
+ }
12
+
13
+ /**
14
+ * Render a dropdown submenu for switching between multiple authenticated sessions.
15
+ *
16
+ * This component renders as a submenu trigger that opens the switch account menu.
17
+ * It should be rendered inside a Dropdown.Menu.
18
+ *
19
+ * @param className - Optional additional CSS class names
20
+ * @returns The switch account submenu as a JSX element
21
+ */
22
+ export function SwitchAccountSubmenu({ className }: SwitchAccountSubmenuProps) {
23
+ const { authClient } = useAuth()
24
+ const { data: session } = useSession(authClient)
25
+ const { localization: multiSessionLocalization } =
26
+ useAuthPlugin(multiSessionPlugin)
27
+
28
+ if (!session) {
29
+ return null
30
+ }
31
+
32
+ return (
33
+ <Dropdown.SubmenuTrigger>
34
+ <Dropdown.Item
35
+ className={className}
36
+ textValue={multiSessionLocalization.switchAccount}
37
+ >
38
+ <Persons className="text-muted" />
39
+
40
+ <Label>{multiSessionLocalization.switchAccount}</Label>
41
+
42
+ <Dropdown.SubmenuIndicator />
43
+ </Dropdown.Item>
44
+
45
+ <Dropdown.Popover className="min-w-40 md:min-w-56 max-w-[48svw]">
46
+ <SwitchAccountSubmenuContent />
47
+ </Dropdown.Popover>
48
+ </Dropdown.SubmenuTrigger>
49
+ )
50
+ }
@@ -0,0 +1,56 @@
1
+ import { type AuthView, authMutationKeys } from "@better-auth-ui/core"
2
+ import {
3
+ type PasskeyAuthClient,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useSignInPasskey
7
+ } from "@better-auth-ui/react"
8
+ import { Fingerprint } from "@gravity-ui/icons"
9
+ import { Button, Spinner } from "@heroui/react"
10
+ import { useIsMutating } from "@tanstack/react-query"
11
+
12
+ import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
13
+
14
+ export type PasskeyButtonProps = {
15
+ /** @remarks `AuthView` */
16
+ view?: AuthView
17
+ }
18
+
19
+ export function PasskeyButton({ view }: PasskeyButtonProps) {
20
+ const { authClient, localization, redirectTo, navigate } = useAuth()
21
+ const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
22
+
23
+ const { mutate: signInPasskey, isPending: passkeyPending } = useSignInPasskey(
24
+ authClient as PasskeyAuthClient,
25
+ {
26
+ onSuccess: () => navigate({ to: redirectTo })
27
+ }
28
+ )
29
+
30
+ const signInMutating = useIsMutating({
31
+ mutationKey: authMutationKeys.signIn.all
32
+ })
33
+ const signUpMutating = useIsMutating({
34
+ mutationKey: authMutationKeys.signUp.all
35
+ })
36
+ const isPending = signInMutating + signUpMutating > 0
37
+
38
+ // Passkey sign-in isn't relevant on the sign-up flow.
39
+ if (view === "signUp") return null
40
+
41
+ return (
42
+ <Button
43
+ className="w-full"
44
+ variant="tertiary"
45
+ isDisabled={isPending}
46
+ isPending={passkeyPending}
47
+ onPress={() => signInPasskey()}
48
+ >
49
+ {passkeyPending ? <Spinner color="current" size="sm" /> : <Fingerprint />}
50
+ {localization.auth.continueWith.replace(
51
+ "{{provider}}",
52
+ passkeyLocalization.passkey
53
+ )}
54
+ </Button>
55
+ )
56
+ }
@@ -1,6 +1,13 @@
1
- import { useAuth, useDeletePasskey } from "@better-auth-ui/react"
1
+ import {
2
+ type PasskeyAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useDeletePasskey
6
+ } from "@better-auth-ui/react"
2
7
  import { Fingerprint, Xmark } from "@gravity-ui/icons"
3
- import { Button, Spinner, toast } from "@heroui/react"
8
+ import { Button, Spinner } from "@heroui/react"
9
+
10
+ import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
4
11
 
5
12
  export type PasskeyProps = {
6
13
  passkey: {
@@ -11,11 +18,14 @@ export type PasskeyProps = {
11
18
  }
12
19
 
13
20
  export function Passkey({ passkey }: PasskeyProps) {
14
- const { localization } = useAuth()
21
+ const { authClient, localization } = useAuth()
22
+ const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
23
+
24
+ const { mutate: deletePasskey, isPending } = useDeletePasskey(
25
+ authClient as PasskeyAuthClient
26
+ )
15
27
 
16
- const { mutate: deletePasskey, isPending } = useDeletePasskey({
17
- onError: (error) => toast.danger(error.error?.message || error.message)
18
- })
28
+ const passkeyName = passkey.name || passkeyLocalization.passkey
19
29
 
20
30
  return (
21
31
  <div className="flex items-center gap-3">
@@ -24,9 +34,7 @@ export function Passkey({ passkey }: PasskeyProps) {
24
34
  </div>
25
35
 
26
36
  <div className="flex flex-col min-w-0">
27
- <span className="text-sm font-medium leading-tight">
28
- {passkey.name || localization.auth.passkey}
29
- </span>
37
+ <span className="text-sm font-medium leading-tight">{passkeyName}</span>
30
38
 
31
39
  <span className="text-xs text-muted">
32
40
  {new Date(passkey.createdAt).toLocaleString(undefined, {
@@ -42,6 +50,10 @@ export function Passkey({ passkey }: PasskeyProps) {
42
50
  size="sm"
43
51
  isPending={isPending}
44
52
  onPress={() => deletePasskey({ id: passkey.id })}
53
+ aria-label={passkeyLocalization.deletePasskey.replace(
54
+ "{{name}}",
55
+ passkeyName
56
+ )}
45
57
  >
46
58
  {isPending ? <Spinner color="current" size="sm" /> : <Xmark />}
47
59
  {localization.settings.delete}