@better-auth-ui/heroui 1.6.3 → 1.6.5

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 (204) 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.d.ts +1 -1
  5. package/dist/components/auth/auth.js +54 -28
  6. package/dist/components/{settings/security → auth/delete-user}/danger-zone.d.ts +1 -1
  7. package/dist/components/{settings/security → auth/delete-user}/danger-zone.js +1 -1
  8. package/dist/components/auth/delete-user/delete-user.js +95 -0
  9. package/dist/components/{email → auth/email}/email-changed-email.d.ts +2 -2
  10. package/dist/components/{email → auth/email}/email-changed-email.js +7 -7
  11. package/dist/components/{email → auth/email}/email-verification-email.d.ts +2 -2
  12. package/dist/components/{email → auth/email}/email-verification-email.js +7 -7
  13. package/dist/components/auth/email/magic-link-email.d.ts +3 -0
  14. package/dist/components/{email → auth/email}/magic-link-email.js +7 -7
  15. package/dist/components/auth/email/new-device-email.d.ts +3 -0
  16. package/dist/components/{email → auth/email}/new-device-email.js +7 -7
  17. package/dist/components/auth/email/otp-email.d.ts +3 -0
  18. package/dist/components/{email → auth/email}/otp-email.js +6 -6
  19. package/dist/components/{email → auth/email}/password-changed-email.d.ts +2 -2
  20. package/dist/components/{email → auth/email}/password-changed-email.js +7 -7
  21. package/dist/components/{email → auth/email}/reset-password-email.d.ts +2 -2
  22. package/dist/components/{email → auth/email}/reset-password-email.js +7 -7
  23. package/dist/components/auth/forgot-password.d.ts +1 -1
  24. package/dist/components/auth/forgot-password.js +70 -48
  25. package/dist/components/auth/magic-link/magic-link-button.d.ts +11 -0
  26. package/dist/components/auth/magic-link/magic-link-button.js +20 -0
  27. package/dist/components/auth/magic-link/magic-link.d.ts +16 -0
  28. package/dist/components/auth/magic-link/magic-link.js +87 -0
  29. package/dist/components/{settings/account → auth/multi-session}/manage-account.d.ts +2 -6
  30. package/dist/components/auth/multi-session/manage-account.js +49 -0
  31. package/dist/components/auth/multi-session/manage-accounts.js +34 -0
  32. package/dist/components/{user/switch-account-menu.d.ts → auth/multi-session/switch-account-submenu-content.d.ts} +3 -3
  33. package/dist/components/auth/multi-session/switch-account-submenu-content.js +29 -0
  34. package/dist/components/{user/switch-account-item.d.ts → auth/multi-session/switch-account-submenu-item.d.ts} +2 -2
  35. package/dist/components/auth/multi-session/switch-account-submenu-item.js +20 -0
  36. package/dist/components/auth/multi-session/switch-account-submenu.d.ts +13 -0
  37. package/dist/components/auth/multi-session/switch-account-submenu.js +24 -0
  38. package/dist/components/auth/passkey/passkey-button.d.ts +6 -0
  39. package/dist/components/auth/passkey/passkey-button.js +24 -0
  40. package/dist/components/auth/passkey/passkey.js +45 -0
  41. package/dist/components/auth/passkey/passkeys.js +106 -0
  42. package/dist/components/auth/provider-button.d.ts +6 -8
  43. package/dist/components/auth/provider-button.js +16 -20
  44. package/dist/components/auth/provider-buttons.d.ts +3 -4
  45. package/dist/components/auth/provider-buttons.js +9 -10
  46. package/dist/components/auth/reset-password.js +110 -107
  47. package/dist/components/{settings → auth/settings}/account/account-settings.d.ts +5 -2
  48. package/dist/components/auth/settings/account/account-settings.js +20 -0
  49. package/dist/components/auth/settings/account/change-avatar.js +89 -0
  50. package/dist/components/{settings → auth/settings}/account/change-email.js +17 -17
  51. package/dist/components/{settings → auth/settings}/account/user-profile.d.ts +2 -3
  52. package/dist/components/auth/settings/account/user-profile.js +86 -0
  53. package/dist/components/auth/settings/security/active-session.js +59 -0
  54. package/dist/components/{settings → auth/settings}/security/active-sessions.js +4 -4
  55. package/dist/components/auth/settings/security/change-password.js +189 -0
  56. package/dist/components/auth/settings/security/linked-account.js +56 -0
  57. package/dist/components/{settings → auth/settings}/security/linked-accounts.js +5 -5
  58. package/dist/components/{settings → auth/settings}/security/security-settings.d.ts +1 -1
  59. package/dist/components/auth/settings/security/security-settings.js +22 -0
  60. package/dist/components/auth/settings/settings.js +45 -0
  61. package/dist/components/auth/sign-in.js +66 -85
  62. package/dist/components/auth/sign-out.js +11 -11
  63. package/dist/components/auth/sign-up.js +128 -148
  64. package/dist/components/{settings/account → auth/theme}/appearance.d.ts +3 -6
  65. package/dist/components/auth/theme/appearance.js +72 -0
  66. package/dist/components/auth/theme/theme-toggle-item.d.ts +5 -0
  67. package/dist/components/auth/theme/theme-toggle-item.js +58 -0
  68. package/dist/components/auth/user/user-avatar.js +29 -0
  69. package/dist/components/{user → auth/user}/user-button.d.ts +2 -6
  70. package/dist/components/auth/user/user-button.js +55 -0
  71. package/dist/components/auth/user/user-view.js +37 -0
  72. package/dist/components/auth/username/sign-in-username.d.ts +14 -0
  73. package/dist/components/auth/username/sign-in-username.js +148 -0
  74. package/dist/components/auth/username/username-field.d.ts +8 -0
  75. package/dist/components/auth/username/username-field.js +53 -0
  76. package/dist/email.d.ts +7 -0
  77. package/dist/email.js +9 -0
  78. package/dist/index.d.ts +12 -27
  79. package/dist/index.js +20 -35
  80. package/dist/lib/auth/auth-plugin.d.ts +44 -0
  81. package/dist/lib/auth/delete-user-plugin.d.ts +17 -0
  82. package/dist/lib/auth/delete-user-plugin.js +10 -0
  83. package/dist/lib/auth/magic-link-plugin.d.ts +31 -0
  84. package/dist/lib/auth/magic-link-plugin.js +13 -0
  85. package/dist/lib/auth/multi-session-plugin.d.ts +18 -0
  86. package/dist/lib/auth/multi-session-plugin.js +12 -0
  87. package/dist/lib/auth/passkey-plugin.d.ts +20 -0
  88. package/dist/lib/auth/passkey-plugin.js +12 -0
  89. package/dist/lib/auth/theme-plugin.d.ts +68 -0
  90. package/dist/lib/auth/theme-plugin.js +23 -0
  91. package/dist/lib/auth/username-plugin.d.ts +63 -0
  92. package/dist/lib/auth/username-plugin.js +18 -0
  93. package/dist/plugins.d.ts +20 -0
  94. package/dist/plugins.js +21 -0
  95. package/package.json +19 -21
  96. package/src/components/auth/additional-field.tsx +554 -0
  97. package/src/components/auth/auth-provider.tsx +5 -6
  98. package/src/components/auth/auth.tsx +99 -35
  99. package/src/components/{settings/security → auth/delete-user}/danger-zone.tsx +1 -1
  100. package/src/components/{settings/security → auth/delete-user}/delete-user.tsx +30 -20
  101. package/src/components/{email → auth/email}/email-changed-email.tsx +2 -2
  102. package/src/components/{email → auth/email}/email-verification-email.tsx +2 -2
  103. package/src/components/{email → auth/email}/magic-link-email.tsx +2 -2
  104. package/src/components/{email → auth/email}/new-device-email.tsx +2 -2
  105. package/src/components/{email → auth/email}/otp-email.tsx +2 -2
  106. package/src/components/{email → auth/email}/password-changed-email.tsx +2 -2
  107. package/src/components/{email → auth/email}/reset-password-email.tsx +2 -2
  108. package/src/components/auth/forgot-password.tsx +42 -17
  109. package/src/components/auth/magic-link/magic-link-button.tsx +61 -0
  110. package/src/components/auth/{magic-link.tsx → magic-link/magic-link.tsx} +55 -40
  111. package/src/components/{settings/account → auth/multi-session}/manage-account.tsx +16 -13
  112. package/src/components/{settings/account → auth/multi-session}/manage-accounts.tsx +14 -5
  113. package/src/components/{user/switch-account-menu.tsx → auth/multi-session/switch-account-submenu-content.tsx} +18 -11
  114. package/src/components/{user/switch-account-item.tsx → auth/multi-session/switch-account-submenu-item.tsx} +17 -5
  115. package/src/components/auth/multi-session/switch-account-submenu.tsx +50 -0
  116. package/src/components/auth/passkey/passkey-button.tsx +56 -0
  117. package/src/components/{settings/security → auth/passkey}/passkey.tsx +20 -6
  118. package/src/components/auth/passkey/passkeys.tsx +196 -0
  119. package/src/components/auth/provider-button.tsx +27 -29
  120. package/src/components/auth/provider-buttons.tsx +6 -11
  121. package/src/components/auth/reset-password.tsx +13 -7
  122. package/src/components/{settings → auth/settings}/account/account-settings.tsx +15 -13
  123. package/src/components/{settings → auth/settings}/account/change-avatar.tsx +4 -3
  124. package/src/components/{settings → auth/settings}/account/change-email.tsx +3 -3
  125. package/src/components/auth/settings/account/user-profile.tsx +171 -0
  126. package/src/components/{settings → auth/settings}/security/active-session.tsx +8 -5
  127. package/src/components/{settings → auth/settings}/security/active-sessions.tsx +3 -3
  128. package/src/components/{settings → auth/settings}/security/change-password.tsx +14 -9
  129. package/src/components/{settings → auth/settings}/security/linked-account.tsx +12 -8
  130. package/src/components/{settings → auth/settings}/security/linked-accounts.tsx +2 -2
  131. package/src/components/{settings → auth/settings}/security/security-settings.tsx +7 -6
  132. package/src/components/{settings → auth/settings}/settings.tsx +2 -2
  133. package/src/components/auth/sign-in.tsx +50 -61
  134. package/src/components/auth/sign-out.tsx +2 -2
  135. package/src/components/auth/sign-up.tsx +119 -131
  136. package/src/components/{settings/account → auth/theme}/appearance.tsx +17 -24
  137. package/src/components/auth/theme/theme-toggle-item.tsx +110 -0
  138. package/src/components/{user → auth/user}/user-avatar.tsx +12 -4
  139. package/src/components/{user → auth/user}/user-button.tsx +15 -88
  140. package/src/components/{user → auth/user}/user-view.tsx +12 -4
  141. package/src/components/auth/username/sign-in-username.tsx +299 -0
  142. package/src/components/auth/username/username-field.tsx +124 -0
  143. package/src/email.ts +9 -0
  144. package/src/index.tsx +12 -27
  145. package/src/lib/auth/auth-plugin.ts +67 -0
  146. package/src/lib/auth/delete-user-plugin.ts +15 -0
  147. package/src/lib/auth/magic-link-plugin.ts +26 -0
  148. package/src/lib/auth/multi-session-plugin.ts +17 -0
  149. package/src/lib/auth/passkey-plugin.ts +17 -0
  150. package/src/lib/auth/theme-plugin.ts +86 -0
  151. package/src/lib/auth/username-plugin.tsx +30 -0
  152. package/src/plugins.ts +23 -0
  153. package/dist/components/auth/magic-link-button.d.ts +0 -13
  154. package/dist/components/auth/magic-link-button.js +0 -16
  155. package/dist/components/auth/magic-link.d.ts +0 -17
  156. package/dist/components/auth/magic-link.js +0 -99
  157. package/dist/components/auth/passkey-button.d.ts +0 -4
  158. package/dist/components/auth/passkey-button.js +0 -21
  159. package/dist/components/email/magic-link-email.d.ts +0 -3
  160. package/dist/components/email/new-device-email.d.ts +0 -3
  161. package/dist/components/email/otp-email.d.ts +0 -3
  162. package/dist/components/settings/account/account-settings.js +0 -23
  163. package/dist/components/settings/account/appearance.js +0 -71
  164. package/dist/components/settings/account/change-avatar.js +0 -89
  165. package/dist/components/settings/account/manage-account.js +0 -48
  166. package/dist/components/settings/account/manage-accounts.js +0 -33
  167. package/dist/components/settings/account/user-profile.js +0 -103
  168. package/dist/components/settings/security/active-session.js +0 -59
  169. package/dist/components/settings/security/change-password.js +0 -189
  170. package/dist/components/settings/security/delete-user.js +0 -92
  171. package/dist/components/settings/security/linked-account.js +0 -56
  172. package/dist/components/settings/security/passkey.js +0 -43
  173. package/dist/components/settings/security/passkeys.js +0 -53
  174. package/dist/components/settings/security/security-settings.js +0 -25
  175. package/dist/components/settings/settings.js +0 -45
  176. package/dist/components/user/switch-account-item.js +0 -20
  177. package/dist/components/user/switch-account-menu.js +0 -28
  178. package/dist/components/user/user-avatar.js +0 -29
  179. package/dist/components/user/user-button.js +0 -91
  180. package/dist/components/user/user-view.js +0 -37
  181. package/dist/core.d.ts +0 -1
  182. package/dist/core.js +0 -1
  183. package/dist/react-exports.d.ts +0 -1
  184. package/dist/react-exports.js +0 -1
  185. package/src/components/auth/magic-link-button.tsx +0 -43
  186. package/src/components/auth/passkey-button.tsx +0 -35
  187. package/src/components/settings/account/user-profile.tsx +0 -217
  188. package/src/components/settings/security/passkeys.tsx +0 -96
  189. package/src/core.ts +0 -1
  190. package/src/react-exports.ts +0 -1
  191. /package/dist/components/{settings/security → auth/delete-user}/delete-user.d.ts +0 -0
  192. /package/dist/components/{settings/account → auth/multi-session}/manage-accounts.d.ts +0 -0
  193. /package/dist/components/{settings/security → auth/passkey}/passkey.d.ts +0 -0
  194. /package/dist/components/{settings/security → auth/passkey}/passkeys.d.ts +0 -0
  195. /package/dist/components/{settings → auth/settings}/account/change-avatar.d.ts +0 -0
  196. /package/dist/components/{settings → auth/settings}/account/change-email.d.ts +0 -0
  197. /package/dist/components/{settings → auth/settings}/security/active-session.d.ts +0 -0
  198. /package/dist/components/{settings → auth/settings}/security/active-sessions.d.ts +0 -0
  199. /package/dist/components/{settings → auth/settings}/security/change-password.d.ts +0 -0
  200. /package/dist/components/{settings → auth/settings}/security/linked-account.d.ts +0 -0
  201. /package/dist/components/{settings → auth/settings}/security/linked-accounts.d.ts +0 -0
  202. /package/dist/components/{settings → auth/settings}/settings.d.ts +0 -0
  203. /package/dist/components/{user → auth/user}/user-avatar.d.ts +0 -0
  204. /package/dist/components/{user → auth/user}/user-view.d.ts +0 -0
@@ -1,4 +1,10 @@
1
- import { useAuth, useSignInMagicLink } from "@better-auth-ui/react"
1
+ import { authMutationKeys } from "@better-auth-ui/core"
2
+ import {
3
+ type MagicLinkAuthClient,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useSignInMagicLink
7
+ } from "@better-auth-ui/react"
2
8
  import {
3
9
  Button,
4
10
  Card,
@@ -14,12 +20,12 @@ import {
14
20
  TextField,
15
21
  toast
16
22
  } from "@heroui/react"
23
+ import { useIsMutating } from "@tanstack/react-query"
17
24
  import { type SyntheticEvent, useState } from "react"
18
25
 
19
- import { FieldSeparator } from "./field-separator"
20
- import { MagicLinkButton } from "./magic-link-button"
21
- import { PasskeyButton } from "./passkey-button"
22
- 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"
23
29
 
24
30
  export type MagicLinkProps = {
25
31
  className?: string
@@ -29,12 +35,11 @@ export type MagicLinkProps = {
29
35
  }
30
36
 
31
37
  /**
32
- * 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.
33
39
  *
34
- * @param className - Additional CSS class names applied to the card container
35
- * @param socialLayout - Layout style for social provider buttons
36
- * @param socialPosition - Position of social provider buttons; `"top"` or `"bottom"`. Defaults to `"bottom"`.
37
- * @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.
38
43
  */
39
44
  export function MagicLink({
40
45
  className,
@@ -44,26 +49,35 @@ export function MagicLink({
44
49
  ...props
45
50
  }: MagicLinkProps & Omit<CardProps, "children">) {
46
51
  const {
52
+ authClient,
47
53
  basePaths,
48
54
  baseURL,
55
+ emailAndPassword,
49
56
  localization,
50
- passkey,
57
+ plugins,
51
58
  redirectTo,
52
59
  socialProviders,
53
60
  viewPaths
54
61
  } = useAuth()
62
+ const { localization: magicLinkLocalization } = useAuthPlugin(magicLinkPlugin)
55
63
 
56
64
  const [email, setEmail] = useState("")
57
65
 
58
- const { mutate: signInMagicLink, isPending: magicLinkPending } =
59
- useSignInMagicLink({
66
+ const { mutate: signInMagicLink, isPending: signInMagicLinkPending } =
67
+ useSignInMagicLink(authClient as MagicLinkAuthClient, {
60
68
  onSuccess: () => {
61
69
  setEmail("")
62
- toast.success(localization.auth.magicLinkSent)
70
+ toast.success(magicLinkLocalization.magicLinkSent)
63
71
  }
64
72
  })
65
73
 
66
- const isPending = magicLinkPending
74
+ const signInMutating = useIsMutating({
75
+ mutationKey: authMutationKeys.signIn.all
76
+ })
77
+ const signUpMutating = useIsMutating({
78
+ mutationKey: authMutationKeys.signUp.all
79
+ })
80
+ const isPending = signInMutating + signUpMutating > 0
67
81
 
68
82
  const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
69
83
  e.preventDefault()
@@ -74,7 +88,7 @@ export function MagicLink({
74
88
 
75
89
  return (
76
90
  <Card
77
- 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)}
78
92
  variant={variant}
79
93
  {...props}
80
94
  >
@@ -88,10 +102,7 @@ export function MagicLink({
88
102
  {socialPosition === "top" && (
89
103
  <>
90
104
  {!!socialProviders?.length && (
91
- <ProviderButtons
92
- socialLayout={socialLayout}
93
- isPending={isPending}
94
- />
105
+ <ProviderButtons socialLayout={socialLayout} />
95
106
  )}
96
107
 
97
108
  {showSeparator && (
@@ -122,14 +133,19 @@ export function MagicLink({
122
133
 
123
134
  <div className="flex flex-col gap-3">
124
135
  <Button type="submit" className="w-full" isPending={isPending}>
125
- {isPending && <Spinner color="current" size="sm" />}
136
+ {signInMagicLinkPending && <Spinner color="current" size="sm" />}
126
137
 
127
- {localization.auth.sendMagicLink}
138
+ {magicLinkLocalization.sendMagicLink}
128
139
  </Button>
129
140
 
130
- <MagicLinkButton view="magicLink" isPending={isPending} />
131
-
132
- {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
+ )}
133
149
  </div>
134
150
  </Form>
135
151
 
@@ -140,26 +156,25 @@ export function MagicLink({
140
156
  )}
141
157
 
142
158
  {!!socialProviders?.length && (
143
- <ProviderButtons
144
- socialLayout={socialLayout}
145
- isPending={isPending}
146
- />
159
+ <ProviderButtons socialLayout={socialLayout} />
147
160
  )}
148
161
  </>
149
162
  )}
150
163
  </Card.Content>
151
164
 
152
- <Card.Footer className="flex-col">
153
- <Description className="text-sm">
154
- {localization.auth.needToCreateAnAccount}{" "}
155
- <Link
156
- href={`${basePaths.auth}/${viewPaths.auth.signUp}`}
157
- className="text-accent decoration-accent no-underline hover:underline"
158
- >
159
- {localization.auth.signUp}
160
- </Link>
161
- </Description>
162
- </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
+ )}
163
178
  </Card>
164
179
  )
165
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,14 +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
+ useSetActiveSession(authClient as MultiSessionAuthClient, {
46
+ onSuccess: () => window.scrollTo({ top: 0 })
47
+ })
45
48
 
46
49
  const { mutate: revokeSession, isPending: isRevoking } =
47
- useRevokeMultiSession({
50
+ useRevokeMultiSession(authClient as MultiSessionAuthClient, {
48
51
  onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)
49
52
  })
50
53
 
@@ -89,7 +92,7 @@ export function ManageAccount({
89
92
  <Dropdown.Popover>
90
93
  <Dropdown.Menu>
91
94
  <Dropdown.Item
92
- textValue={localization.auth.switchAccount}
95
+ textValue={multiSessionLocalization.switchAccount}
93
96
  onAction={() =>
94
97
  setActiveSession({
95
98
  sessionToken: deviceSession.session.token
@@ -97,7 +100,7 @@ export function ManageAccount({
97
100
  }
98
101
  >
99
102
  <ArrowRightArrowLeft className="text-muted" />
100
- {localization.auth.switchAccount}
103
+ {multiSessionLocalization.switchAccount}
101
104
  </Dropdown.Item>
102
105
 
103
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
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 = {
@@ -24,10 +29,14 @@ export function ManageAccounts({
24
29
  variant,
25
30
  ...props
26
31
  }: ManageAccountsProps & Omit<CardProps, "children">) {
27
- const { localization } = useAuth()
28
- const { data: session } = useSession()
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()
37
+ const { data: deviceSessions, isPending } = useListDeviceSessions(
38
+ authClient as MultiSessionAuthClient
39
+ )
31
40
 
32
41
  const otherSessions = deviceSessions?.filter(
33
42
  (deviceSession) => deviceSession.session.id !== session?.session.id
@@ -35,7 +44,7 @@ export function ManageAccounts({
35
44
 
36
45
  const allRows = [
37
46
  {
38
- key: "current",
47
+ key: session?.session.id ?? "current",
39
48
  deviceSession: !isPending ? session : null,
40
49
  isPending
41
50
  },
@@ -49,7 +58,7 @@ export function ManageAccounts({
49
58
  return (
50
59
  <div>
51
60
  <h2 className={cn("text-sm font-semibold mb-3")}>
52
- {localization.settings.manageAccounts}
61
+ {multiSessionLocalization.manageAccounts}
53
62
  </h2>
54
63
 
55
64
  <Card className={cn(className)} variant={variant} {...props}>
@@ -1,27 +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
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()
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
+ )
25
32
 
26
33
  return (
27
34
  <Dropdown.Popover className="min-w-40 md:min-w-56 max-w-[48svw]">
@@ -37,7 +44,7 @@ export function SwitchAccountMenu() {
37
44
  (deviceSession) => deviceSession.session.id !== session?.session?.id
38
45
  )
39
46
  .map((deviceSession) => (
40
- <SwitchAccountItem
47
+ <SwitchAccountSubmenuItem
41
48
  key={deviceSession.session.id}
42
49
  deviceSession={deviceSession}
43
50
  />
@@ -46,12 +53,12 @@ export function SwitchAccountMenu() {
46
53
  <Separator />
47
54
 
48
55
  <Dropdown.Item
49
- textValue={localization.auth.addAccount}
56
+ textValue={multiSessionLocalization.addAccount}
50
57
  href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
51
58
  >
52
59
  <CirclePlus className="text-muted" />
53
60
 
54
- <Label>{localization.auth.addAccount}</Label>
61
+ <Label>{multiSessionLocalization.addAccount}</Label>
55
62
  </Dropdown.Item>
56
63
  </Dropdown.Menu>
57
64
  </Dropdown.Popover>
@@ -1,14 +1,18 @@
1
- import { useSetActiveSession } from "@better-auth-ui/react"
1
+ import {
2
+ type MultiSessionAuthClient,
3
+ useAuth,
4
+ useSetActiveSession
5
+ } from "@better-auth-ui/react"
2
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,8 +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()
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
+ )
23
35
 
24
36
  return (
25
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,7 +1,14 @@
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
8
  import { Button, Spinner } from "@heroui/react"
4
9
 
10
+ import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
11
+
5
12
  export type PasskeyProps = {
6
13
  passkey: {
7
14
  id: string
@@ -11,9 +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()
28
+ const passkeyName = passkey.name || passkeyLocalization.passkey
17
29
 
18
30
  return (
19
31
  <div className="flex items-center gap-3">
@@ -22,9 +34,7 @@ export function Passkey({ passkey }: PasskeyProps) {
22
34
  </div>
23
35
 
24
36
  <div className="flex flex-col min-w-0">
25
- <span className="text-sm font-medium leading-tight">
26
- {passkey.name || localization.auth.passkey}
27
- </span>
37
+ <span className="text-sm font-medium leading-tight">{passkeyName}</span>
28
38
 
29
39
  <span className="text-xs text-muted">
30
40
  {new Date(passkey.createdAt).toLocaleString(undefined, {
@@ -40,6 +50,10 @@ export function Passkey({ passkey }: PasskeyProps) {
40
50
  size="sm"
41
51
  isPending={isPending}
42
52
  onPress={() => deletePasskey({ id: passkey.id })}
53
+ aria-label={passkeyLocalization.deletePasskey.replace(
54
+ "{{name}}",
55
+ passkeyName
56
+ )}
43
57
  >
44
58
  {isPending ? <Spinner color="current" size="sm" /> : <Xmark />}
45
59
  {localization.settings.delete}