@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
@@ -0,0 +1,196 @@
1
+ import {
2
+ type PasskeyAuthClient,
3
+ useAddPasskey,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useListPasskeys
7
+ } from "@better-auth-ui/react"
8
+ import { Fingerprint } from "@gravity-ui/icons"
9
+ import {
10
+ AlertDialog,
11
+ Button,
12
+ Card,
13
+ type CardProps,
14
+ cn,
15
+ FieldError,
16
+ Form,
17
+ Input,
18
+ Label,
19
+ Skeleton,
20
+ Spinner,
21
+ TextField
22
+ } from "@heroui/react"
23
+ import { type SyntheticEvent, useState } from "react"
24
+
25
+ import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
26
+ import { Passkey } from "./passkey"
27
+
28
+ export type PasskeysProps = {
29
+ className?: string
30
+ variant?: CardProps["variant"]
31
+ }
32
+
33
+ export function Passkeys({
34
+ className,
35
+ variant,
36
+ ...props
37
+ }: PasskeysProps & Omit<CardProps, "children">) {
38
+ const { authClient, localization } = useAuth()
39
+ const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
40
+
41
+ const { data: passkeys, isPending } = useListPasskeys(
42
+ authClient as PasskeyAuthClient
43
+ )
44
+ const { mutate: addPasskey, isPending: isAdding } = useAddPasskey(
45
+ authClient as PasskeyAuthClient
46
+ )
47
+
48
+ const [nameOpen, setNameOpen] = useState(false)
49
+ const [name, setName] = useState("")
50
+
51
+ const handleDialogOpenChange = (open: boolean) => {
52
+ setNameOpen(open)
53
+ setName("")
54
+ }
55
+
56
+ const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
57
+ e.preventDefault()
58
+
59
+ addPasskey(
60
+ { name: name.trim() || undefined },
61
+ {
62
+ onSuccess: () => {
63
+ setNameOpen(false)
64
+ setName("")
65
+ }
66
+ }
67
+ )
68
+ }
69
+
70
+ return (
71
+ <div>
72
+ <h2 className={cn("text-sm font-semibold mb-3")}>
73
+ {passkeyLocalization.passkeys}
74
+ </h2>
75
+
76
+ <Card className={cn(className)} variant={variant} {...props}>
77
+ <Card.Content className="gap-0">
78
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
79
+ <div>
80
+ <p className="text-sm font-medium leading-tight">
81
+ {passkeyLocalization.passkeysDescription}
82
+ </p>
83
+
84
+ <p className="text-muted text-xs mt-0.5">
85
+ {passkeyLocalization.passkeysInstructions}
86
+ </p>
87
+ </div>
88
+
89
+ <AlertDialog>
90
+ <Button
91
+ className="shrink-0"
92
+ size="sm"
93
+ isDisabled={isPending}
94
+ onPress={() => setNameOpen(true)}
95
+ >
96
+ {passkeyLocalization.addPasskey}
97
+ </Button>
98
+
99
+ <AlertDialog.Backdrop
100
+ isOpen={nameOpen}
101
+ onOpenChange={handleDialogOpenChange}
102
+ >
103
+ <AlertDialog.Container>
104
+ <AlertDialog.Dialog>
105
+ <Form onSubmit={handleSubmit}>
106
+ <AlertDialog.CloseTrigger />
107
+
108
+ <AlertDialog.Header>
109
+ <AlertDialog.Icon status="default">
110
+ <Fingerprint />
111
+ </AlertDialog.Icon>
112
+
113
+ <AlertDialog.Heading>
114
+ {passkeyLocalization.addPasskey}
115
+ </AlertDialog.Heading>
116
+ </AlertDialog.Header>
117
+
118
+ <AlertDialog.Body className="overflow-visible">
119
+ <p className="text-muted text-sm">
120
+ {passkeyLocalization.passkeysInstructions}
121
+ </p>
122
+
123
+ <TextField
124
+ className="mt-4"
125
+ name="passkey-name"
126
+ isDisabled={isAdding}
127
+ value={name}
128
+ onChange={setName}
129
+ >
130
+ <Label>{passkeyLocalization.passkey}</Label>
131
+
132
+ <Input
133
+ autoFocus
134
+ placeholder={localization.settings.optional}
135
+ variant="secondary"
136
+ />
137
+
138
+ <FieldError />
139
+ </TextField>
140
+ </AlertDialog.Body>
141
+
142
+ <AlertDialog.Footer>
143
+ <Button
144
+ slot="close"
145
+ variant="tertiary"
146
+ isDisabled={isAdding}
147
+ >
148
+ {localization.settings.cancel}
149
+ </Button>
150
+
151
+ <Button type="submit" isPending={isAdding}>
152
+ {isAdding && <Spinner color="current" size="sm" />}
153
+
154
+ {passkeyLocalization.addPasskey}
155
+ </Button>
156
+ </AlertDialog.Footer>
157
+ </Form>
158
+ </AlertDialog.Dialog>
159
+ </AlertDialog.Container>
160
+ </AlertDialog.Backdrop>
161
+ </AlertDialog>
162
+ </div>
163
+
164
+ {isPending ? (
165
+ <>
166
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
167
+ <PasskeySkeleton />
168
+ </>
169
+ ) : (
170
+ passkeys?.map((passkey) => (
171
+ <div key={passkey.id}>
172
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
173
+ <Passkey passkey={passkey} />
174
+ </div>
175
+ ))
176
+ )}
177
+ </Card.Content>
178
+ </Card>
179
+ </div>
180
+ )
181
+ }
182
+
183
+ function PasskeySkeleton() {
184
+ return (
185
+ <div className="flex items-center justify-between">
186
+ <div className="flex items-center gap-3">
187
+ <Skeleton className="size-10 rounded-xl" />
188
+
189
+ <div className="flex flex-col gap-1">
190
+ <Skeleton className="h-4 w-24 rounded-lg" />
191
+ <Skeleton className="h-3 w-20 rounded-lg" />
192
+ </div>
193
+ </div>
194
+ </div>
195
+ )
196
+ }
@@ -1,65 +1,63 @@
1
- import { getProviderName } from "@better-auth-ui/core"
1
+ import { authMutationKeys, getProviderName } from "@better-auth-ui/core"
2
2
  import { providerIcons, useAuth, useSignInSocial } from "@better-auth-ui/react"
3
3
  import { Button, type ButtonProps, Spinner } from "@heroui/react"
4
+ import { useIsMutating } from "@tanstack/react-query"
4
5
  import type { SocialProvider } from "better-auth/social-providers"
5
- import { useState } from "react"
6
6
 
7
7
  export type ProviderButtonProps = {
8
8
  provider: SocialProvider
9
- label?: "continueWith" | "providerName" | "none"
10
- isDisabled?: boolean
11
- } & Omit<ButtonProps, "children" | "onPress" | "isPending">
9
+ display?: "full" | "name" | "icon"
10
+ } & Omit<ButtonProps, "children" | "onPress" | "isPending" | "isDisabled">
12
11
 
13
12
  /**
14
- * Render a single social provider sign-in button with its own mutation and pending state.
13
+ * Social provider sign-in button.
15
14
  *
16
- * @param provider - The social provider this button signs in with.
17
- * @param label - Label style: `"continueWith"` (e.g. "Continue with Google"), `"providerName"` (e.g. "Google"), or `"none"` (icon only).
18
- * @param isDisabled - External disabled state (e.g. a sibling provider is mid-redirect).
15
+ * @param provider - Provider to sign in with.
16
+ * @param display - `"full"` (e.g. "Continue with Google"), `"name"` (just the provider name), or `"icon"` (icon only).
19
17
  */
20
18
  export function ProviderButton({
21
19
  provider,
22
- label = "continueWith",
23
- isDisabled,
20
+ display = "full",
24
21
  variant = "tertiary",
25
22
  ...props
26
23
  }: ProviderButtonProps) {
27
- const { baseURL, localization, redirectTo } = useAuth()
24
+ const { authClient, baseURL, localization, redirectTo } = useAuth()
28
25
 
29
26
  const callbackURL = `${baseURL}${redirectTo}`
30
27
 
31
- const [redirecting, setRedirecting] = useState(false)
32
-
33
- const { mutate: signInSocial, isPending } = useSignInSocial({
34
- onSuccess: () => {
35
- setRedirecting(true)
36
-
37
- setTimeout(() => {
38
- setRedirecting(false)
39
- }, 5000)
40
- }
41
- })
28
+ const { mutate: signInSocial, isPending: signInSocialPending } =
29
+ useSignInSocial(authClient)
42
30
 
43
31
  const ProviderIcon = providerIcons[provider]
44
32
 
45
- const pending = isPending || redirecting
33
+ const signInMutating = useIsMutating({
34
+ mutationKey: authMutationKeys.signIn.all
35
+ })
36
+ const signUpMutating = useIsMutating({
37
+ mutationKey: authMutationKeys.signUp.all
38
+ })
39
+ const isPending = signInMutating + signUpMutating > 0
46
40
 
47
41
  return (
48
42
  <Button
49
43
  variant={variant}
50
- isDisabled={isDisabled}
51
- isPending={pending}
44
+ isPending={isPending}
52
45
  onPress={() => signInSocial({ provider, callbackURL })}
53
46
  {...props}
47
+ aria-label={getProviderName(provider)}
54
48
  >
55
- {pending ? <Spinner color="current" size="sm" /> : <ProviderIcon />}
49
+ {signInSocialPending ? (
50
+ <Spinner color="current" size="sm" />
51
+ ) : (
52
+ <ProviderIcon />
53
+ )}
56
54
 
57
- {label === "continueWith"
55
+ {display === "full"
58
56
  ? localization.auth.continueWith.replace(
59
57
  "{{provider}}",
60
58
  getProviderName(provider)
61
59
  )
62
- : label === "providerName"
60
+ : display === "name"
63
61
  ? getProviderName(provider)
64
62
  : null}
65
63
  </Button>
@@ -1,25 +1,21 @@
1
1
  import { useAuth } from "@better-auth-ui/react"
2
2
  import { cn } from "@heroui/react"
3
3
  import { useMemo } from "react"
4
-
5
4
  import { ProviderButton } from "./provider-button"
6
-
7
5
  export type ProviderButtonsProps = {
8
- isPending?: boolean
9
6
  socialLayout?: SocialLayout
10
7
  }
11
8
 
12
9
  export type SocialLayout = "auto" | "horizontal" | "vertical" | "grid"
13
10
 
14
11
  /**
15
- * Render social provider sign-in buttons. Each button owns its own sign-in mutation.
12
+ * Render social provider sign-in buttons. Each button owns its own sign-in
13
+ * mutation and reads shared auth pending state from React Query.
16
14
  *
17
- * @param isPending - External pending state (e.g. parent form submitting) that disables all buttons.
18
15
  * @param socialLayout - Preferred layout for the buttons; `"auto"` picks `"horizontal"` when there are four or more providers, otherwise `"vertical"`.
19
16
  * @returns The JSX element that renders the configured social provider buttons.
20
17
  */
21
18
  export function ProviderButtons({
22
- isPending,
23
19
  socialLayout = "auto"
24
20
  }: ProviderButtonsProps) {
25
21
  const { socialProviders } = useAuth()
@@ -49,13 +45,12 @@ export function ProviderButtons({
49
45
  <ProviderButton
50
46
  key={provider}
51
47
  provider={provider}
52
- isDisabled={isPending}
53
- label={
48
+ display={
54
49
  resolvedSocialLayout === "vertical"
55
- ? "continueWith"
50
+ ? "full"
56
51
  : resolvedSocialLayout === "grid"
57
- ? "providerName"
58
- : "none"
52
+ ? "name"
53
+ : "icon"
59
54
  }
60
55
  className={cn(
61
56
  "w-full",
@@ -32,10 +32,16 @@ export function ResetPassword({
32
32
  variant,
33
33
  ...props
34
34
  }: ResetPasswordProps & Omit<CardProps, "children">) {
35
- const { basePaths, emailAndPassword, localization, viewPaths, navigate } =
36
- useAuth()
35
+ const {
36
+ authClient,
37
+ basePaths,
38
+ emailAndPassword,
39
+ localization,
40
+ viewPaths,
41
+ navigate
42
+ } = useAuth()
37
43
 
38
- const { mutate: resetPassword, isPending } = useResetPassword({
44
+ const { mutate: resetPassword, isPending } = useResetPassword(authClient, {
39
45
  onSuccess: () => {
40
46
  toast.success(localization.auth.passwordResetSuccess)
41
47
  navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
@@ -87,7 +93,7 @@ export function ResetPassword({
87
93
 
88
94
  return (
89
95
  <Card
90
- className={cn("w-full max-w-sm p-4 md:p-6", className)}
96
+ className={cn("w-full max-w-sm gap-4 md:p-6", className)}
91
97
  variant={variant}
92
98
  {...props}
93
99
  >
@@ -97,7 +103,7 @@ export function ResetPassword({
97
103
  </Card.Title>
98
104
  </Card.Header>
99
105
 
100
- <Card.Content>
106
+ <Card.Content className="gap-4">
101
107
  <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
102
108
  <TextField
103
109
  minLength={emailAndPassword?.minPasswordLength}
@@ -193,12 +199,12 @@ export function ResetPassword({
193
199
  </Form>
194
200
  </Card.Content>
195
201
 
196
- <Card.Footer className="flex-col">
202
+ <Card.Footer className="flex-col gap-3">
197
203
  <Description className="text-sm">
198
204
  {localization.auth.rememberYourPassword}{" "}
199
205
  <Link
200
206
  href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
201
- className="text-accent decoration-accent no-underline hover:underline"
207
+ className="text-accent no-underline hover:underline decoration-accent-hover"
202
208
  >
203
209
  {localization.auth.signIn}
204
210
  </Link>
@@ -1,9 +1,8 @@
1
1
  import { useAuth } from "@better-auth-ui/react"
2
2
  import { type CardProps, cn } from "@heroui/react"
3
3
  import type { ComponentProps } from "react"
4
- import { Appearance } from "./appearance"
4
+
5
5
  import { ChangeEmail } from "./change-email"
6
- import { ManageAccounts } from "./manage-accounts"
7
6
  import { UserProfile } from "./user-profile"
8
7
 
9
8
  export type AccountSettingsProps = {
@@ -12,9 +11,12 @@ export type AccountSettingsProps = {
12
11
  }
13
12
 
14
13
  /**
15
- * Renders the account settings layout including user profile, change email, appearance, and accounts management.
14
+ * Renders the account settings layout.
16
15
  *
17
- * UserProfile, ChangeEmail, and Appearance are always rendered; Accounts is rendered when `multiSession` is enabled.
16
+ * `UserProfile` always renders. `ChangeEmail` renders when password auth is
17
+ * enabled or the `magicLink` plugin is registered. Plugin-contributed account
18
+ * cards (e.g. `Appearance` from the theme plugin, multi-session accounts) are
19
+ * rendered via the plugins array.
18
20
  *
19
21
  * @param className - Optional additional CSS class names for the outer container.
20
22
  * @param variant - Card variant forwarded to each account settings card.
@@ -25,12 +27,9 @@ export function AccountSettings({
25
27
  variant,
26
28
  ...props
27
29
  }: AccountSettingsProps & ComponentProps<"div">) {
28
- const {
29
- multiSession,
30
- emailAndPassword,
31
- magicLink,
32
- appearance: { setTheme }
33
- } = useAuth()
30
+ const { emailAndPassword, plugins } = useAuth()
31
+
32
+ const hasMagicLink = plugins.some((plugin) => plugin.id === "magicLink")
34
33
 
35
34
  return (
36
35
  <div
@@ -38,11 +37,14 @@ export function AccountSettings({
38
37
  {...props}
39
38
  >
40
39
  <UserProfile variant={variant} />
41
- {(emailAndPassword?.enabled || magicLink) && (
40
+ {(emailAndPassword?.enabled || hasMagicLink) && (
42
41
  <ChangeEmail variant={variant} />
43
42
  )}
44
- {setTheme && <Appearance variant={variant} />}
45
- {multiSession && <ManageAccounts variant={variant} />}
43
+ {plugins.flatMap((plugin) =>
44
+ plugin.accountCards?.map((Card, index) => (
45
+ <Card key={`${plugin.id}-${index.toString()}`} variant={variant} />
46
+ ))
47
+ )}
46
48
  </div>
47
49
  )
48
50
  }
@@ -11,10 +11,11 @@ export type ChangeAvatarProps = {
11
11
  }
12
12
 
13
13
  export function ChangeAvatar({ className }: ChangeAvatarProps) {
14
- const { localization, avatar } = useAuth()
15
- const { data: session } = useSession()
14
+ const { authClient, localization, avatar } = useAuth()
15
+ const { data: session } = useSession(authClient)
16
16
 
17
- const { mutate: updateUser, isPending: updatePending } = useUpdateUser()
17
+ const { mutate: updateUser, isPending: updatePending } =
18
+ useUpdateUser(authClient)
18
19
 
19
20
  const fileInputRef = useRef<HTMLInputElement>(null)
20
21
  const [isUploading, setIsUploading] = useState(false)
@@ -35,10 +35,10 @@ export function ChangeEmail({
35
35
  variant,
36
36
  ...props
37
37
  }: ChangeEmailProps & Omit<CardProps, "children">) {
38
- const { localization, baseURL, viewPaths } = useAuth()
39
- const { data: session } = useSession()
38
+ const { authClient, localization, baseURL, viewPaths } = useAuth()
39
+ const { data: session } = useSession(authClient)
40
40
 
41
- const { mutate: changeEmail, isPending } = useChangeEmail({
41
+ const { mutate: changeEmail, isPending } = useChangeEmail(authClient, {
42
42
  onSuccess: () => toast.success(localization.settings.changeEmailSuccess)
43
43
  })
44
44
 
@@ -0,0 +1,171 @@
1
+ import {
2
+ type AdditionalFieldValue,
3
+ parseAdditionalFieldValue
4
+ } from "@better-auth-ui/core"
5
+ import { useAuth, useSession, useUpdateUser } from "@better-auth-ui/react"
6
+ import {
7
+ Button,
8
+ Card,
9
+ type CardProps,
10
+ cn,
11
+ FieldError,
12
+ Form,
13
+ Input,
14
+ Label,
15
+ Skeleton,
16
+ Spinner,
17
+ TextField,
18
+ toast
19
+ } from "@heroui/react"
20
+ import type { SyntheticEvent } from "react"
21
+
22
+ import { AdditionalField } from "../../additional-field"
23
+ import { ChangeAvatar } from "./change-avatar"
24
+
25
+ export type UserProfileProps = {
26
+ className?: string
27
+ variant?: CardProps["variant"]
28
+ }
29
+
30
+ /**
31
+ * Render a profile card that lets the authenticated user view and update their
32
+ * display name, avatar, and any plugin- or user-supplied additional fields.
33
+ */
34
+ export function UserProfile({
35
+ className,
36
+ variant,
37
+ ...props
38
+ }: UserProfileProps & Omit<CardProps, "children">) {
39
+ const { additionalFields, authClient, localization } = useAuth()
40
+ const { data: session } = useSession(authClient)
41
+
42
+ const { mutate: updateUser, isPending } = useUpdateUser(authClient, {
43
+ onSuccess: () => toast.success(localization.settings.profileUpdatedSuccess)
44
+ })
45
+
46
+ async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
47
+ e.preventDefault()
48
+ const formData = new FormData(e.currentTarget)
49
+ const name = formData.get("name") as string
50
+
51
+ const additionalFieldValues: Record<string, unknown> = {}
52
+
53
+ for (const field of additionalFields ?? []) {
54
+ if (field.profile === false || field.readOnly) continue
55
+ const value = parseAdditionalFieldValue(
56
+ field,
57
+ formData.get(field.name) as string | null
58
+ )
59
+
60
+ if (field.validate) {
61
+ try {
62
+ await field.validate(value)
63
+ } catch (error) {
64
+ toast.danger(error instanceof Error ? error.message : String(error))
65
+ return
66
+ }
67
+ }
68
+
69
+ // `null` = explicit clear (forward to backend); `undefined` = omitted.
70
+ if (value !== undefined) {
71
+ additionalFieldValues[field.name] = value
72
+ }
73
+ }
74
+
75
+ updateUser({
76
+ name,
77
+ ...additionalFieldValues
78
+ })
79
+ }
80
+
81
+ return (
82
+ <div>
83
+ <h2 className={cn("text-sm font-semibold mb-3")}>
84
+ {localization.settings.profile}
85
+ </h2>
86
+
87
+ <Card className={cn("p-4 gap-4", className)} variant={variant} {...props}>
88
+ <Card.Content>
89
+ <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
90
+ <ChangeAvatar />
91
+
92
+ <TextField
93
+ key={session?.user?.name}
94
+ name="name"
95
+ defaultValue={session?.user.name}
96
+ isDisabled={isPending || !session}
97
+ >
98
+ <Label>{localization.auth.name}</Label>
99
+
100
+ <Input
101
+ className={cn(!session && "hidden")}
102
+ autoComplete="name"
103
+ placeholder={localization.auth.name}
104
+ variant={variant === "transparent" ? "primary" : "secondary"}
105
+ />
106
+
107
+ {!session && (
108
+ <Skeleton className="h-10 md:h-9 w-full rounded-xl" />
109
+ )}
110
+
111
+ <FieldError />
112
+ </TextField>
113
+
114
+ {additionalFields
115
+ ?.filter((field) => field.profile !== false)
116
+ .map((field) => {
117
+ if (!session) {
118
+ if (field.inputType === "hidden") {
119
+ return null
120
+ }
121
+
122
+ return (
123
+ <Skeleton
124
+ key={field.name}
125
+ className="h-10 md:h-9 w-full rounded-xl"
126
+ />
127
+ )
128
+ }
129
+
130
+ const value = (session.user as Record<string, unknown>)[
131
+ field.name
132
+ ]
133
+
134
+ // Re-mount when the session value loads so the field's
135
+ // uncontrolled `defaultValue` reflects the latest data.
136
+ const key = `${field.name}:${
137
+ value instanceof Date
138
+ ? value.toISOString()
139
+ : String(value ?? "")
140
+ }`
141
+
142
+ return (
143
+ <AdditionalField
144
+ key={key}
145
+ name={field.name}
146
+ field={{
147
+ ...field,
148
+ defaultValue: value as AdditionalFieldValue | null
149
+ }}
150
+ isPending={isPending}
151
+ variant={variant}
152
+ />
153
+ )
154
+ })}
155
+
156
+ <Button
157
+ type="submit"
158
+ isPending={isPending}
159
+ isDisabled={!session}
160
+ size="sm"
161
+ className="self-start mt-1"
162
+ >
163
+ {isPending && <Spinner color="current" size="sm" />}
164
+ {localization.settings.saveChanges}
165
+ </Button>
166
+ </Form>
167
+ </Card.Content>
168
+ </Card>
169
+ </div>
170
+ )
171
+ }
@@ -46,12 +46,15 @@ export type ActiveSessionProps = {
46
46
  * @returns A JSX element containing the active session row
47
47
  */
48
48
  export function ActiveSession({ activeSession }: ActiveSessionProps) {
49
- const { basePaths, localization, viewPaths, navigate } = useAuth()
50
- const { data: session } = useSession({ refetchOnMount: false })
49
+ const { authClient, basePaths, localization, viewPaths, navigate } = useAuth()
50
+ const { data: session } = useSession(authClient, { refetchOnMount: false })
51
51
 
52
- const { mutate: revokeSession, isPending: isRevoking } = useRevokeSession({
53
- onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)
54
- })
52
+ const { mutate: revokeSession, isPending: isRevoking } = useRevokeSession(
53
+ authClient,
54
+ {
55
+ onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)
56
+ }
57
+ )
55
58
 
56
59
  const isCurrentSession = activeSession.token === session?.session.token
57
60
  const ua = Bowser.parse(activeSession.userAgent || "")
@@ -20,10 +20,10 @@ export function ActiveSessions({
20
20
  variant,
21
21
  ...props
22
22
  }: ActiveSessionsProps & Omit<CardProps, "children">) {
23
- const { localization } = useAuth()
24
- const { data: session } = useSession()
23
+ const { authClient, localization } = useAuth()
24
+ const { data: session } = useSession(authClient)
25
25
 
26
- const { data: sessions, isPending } = useListSessions()
26
+ const { data: sessions, isPending } = useListSessions(authClient)
27
27
 
28
28
  const activeSessions = sessions?.toSorted((activeSession) =>
29
29
  activeSession.id === session?.session.id ? -1 : 1