@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
@@ -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
+ }
@@ -0,0 +1,65 @@
1
+ import { authMutationKeys, getProviderName } from "@better-auth-ui/core"
2
+ import { providerIcons, useAuth, useSignInSocial } from "@better-auth-ui/react"
3
+ import { Button, type ButtonProps, Spinner } from "@heroui/react"
4
+ import { useIsMutating } from "@tanstack/react-query"
5
+ import type { SocialProvider } from "better-auth/social-providers"
6
+
7
+ export type ProviderButtonProps = {
8
+ provider: SocialProvider
9
+ display?: "full" | "name" | "icon"
10
+ } & Omit<ButtonProps, "children" | "onPress" | "isPending" | "isDisabled">
11
+
12
+ /**
13
+ * Social provider sign-in button.
14
+ *
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).
17
+ */
18
+ export function ProviderButton({
19
+ provider,
20
+ display = "full",
21
+ variant = "tertiary",
22
+ ...props
23
+ }: ProviderButtonProps) {
24
+ const { authClient, baseURL, localization, redirectTo } = useAuth()
25
+
26
+ const callbackURL = `${baseURL}${redirectTo}`
27
+
28
+ const { mutate: signInSocial, isPending: signInSocialPending } =
29
+ useSignInSocial(authClient)
30
+
31
+ const ProviderIcon = providerIcons[provider]
32
+
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
40
+
41
+ return (
42
+ <Button
43
+ variant={variant}
44
+ isPending={isPending}
45
+ onPress={() => signInSocial({ provider, callbackURL })}
46
+ {...props}
47
+ aria-label={getProviderName(provider)}
48
+ >
49
+ {signInSocialPending ? (
50
+ <Spinner color="current" size="sm" />
51
+ ) : (
52
+ <ProviderIcon />
53
+ )}
54
+
55
+ {display === "full"
56
+ ? localization.auth.continueWith.replace(
57
+ "{{provider}}",
58
+ getProviderName(provider)
59
+ )
60
+ : display === "name"
61
+ ? getProviderName(provider)
62
+ : null}
63
+ </Button>
64
+ )
65
+ }
@@ -1,36 +1,24 @@
1
- import { getProviderName } from "@better-auth-ui/core"
2
- import { providerIcons, useAuth } from "@better-auth-ui/react"
3
- import { Button, cn } from "@heroui/react"
4
- import type { SocialProvider } from "better-auth/social-providers"
1
+ import { useAuth } from "@better-auth-ui/react"
2
+ import { cn } from "@heroui/react"
5
3
  import { useMemo } from "react"
6
-
4
+ import { ProviderButton } from "./provider-button"
7
5
  export type ProviderButtonsProps = {
8
- isPending: boolean
9
6
  socialLayout?: SocialLayout
10
- signInSocial: (params: {
11
- provider: SocialProvider
12
- callbackURL: string
13
- }) => void
14
7
  }
15
8
 
16
9
  export type SocialLayout = "auto" | "horizontal" | "vertical" | "grid"
17
10
 
18
11
  /**
19
- * Render social provider sign-in buttons and handle initiation and pending state.
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.
20
14
  *
21
- * @param isPending - Disables all provider buttons when true.
22
15
  * @param socialLayout - Preferred layout for the buttons; `"auto"` picks `"horizontal"` when there are four or more providers, otherwise `"vertical"`.
23
- * @param signInSocial - Callback invoked with the provider and callbackURL when a button is clicked.
24
16
  * @returns The JSX element that renders the configured social provider buttons.
25
17
  */
26
18
  export function ProviderButtons({
27
- isPending,
28
- socialLayout = "auto",
29
- signInSocial
19
+ socialLayout = "auto"
30
20
  }: ProviderButtonsProps) {
31
- const { baseURL, localization, redirectTo, socialProviders } = useAuth()
32
-
33
- const callbackURL = `${baseURL}${redirectTo}`
21
+ const { socialProviders } = useAuth()
34
22
 
35
23
  const resolvedSocialLayout = useMemo(() => {
36
24
  if (socialLayout === "auto") {
@@ -53,33 +41,23 @@ export function ProviderButtons({
53
41
  resolvedSocialLayout === "horizontal" && "flex flex-wrap"
54
42
  )}
55
43
  >
56
- {socialProviders?.map((provider) => {
57
- const ProviderIcon = providerIcons[provider]
58
-
59
- return (
60
- <Button
61
- key={provider}
62
- className={cn(
63
- "w-full",
64
- resolvedSocialLayout === "horizontal" && "flex-1"
65
- )}
66
- variant="tertiary"
67
- isPending={isPending}
68
- onPress={() => signInSocial({ provider, callbackURL })}
69
- >
70
- <ProviderIcon />
71
-
72
- {resolvedSocialLayout === "vertical"
73
- ? localization.auth.continueWith.replace(
74
- "{{provider}}",
75
- getProviderName(provider)
76
- )
44
+ {socialProviders?.map((provider) => (
45
+ <ProviderButton
46
+ key={provider}
47
+ provider={provider}
48
+ display={
49
+ resolvedSocialLayout === "vertical"
50
+ ? "full"
77
51
  : resolvedSocialLayout === "grid"
78
- ? getProviderName(provider)
79
- : null}
80
- </Button>
81
- )
82
- })}
52
+ ? "name"
53
+ : "icon"
54
+ }
55
+ className={cn(
56
+ "w-full",
57
+ resolvedSocialLayout === "horizontal" && "flex-1"
58
+ )}
59
+ />
60
+ ))}
83
61
  </div>
84
62
  )
85
63
  }
@@ -31,12 +31,17 @@ export function ResetPassword({
31
31
  className,
32
32
  variant,
33
33
  ...props
34
- }: ResetPasswordProps & CardProps) {
35
- const { basePaths, emailAndPassword, localization, viewPaths, navigate } =
36
- useAuth()
34
+ }: ResetPasswordProps & Omit<CardProps, "children">) {
35
+ const {
36
+ authClient,
37
+ basePaths,
38
+ emailAndPassword,
39
+ localization,
40
+ viewPaths,
41
+ navigate
42
+ } = useAuth()
37
43
 
38
- const { mutate: resetPassword, isPending } = useResetPassword({
39
- onError: (error) => toast.danger(error.error?.message || error.message),
44
+ const { mutate: resetPassword, isPending } = useResetPassword(authClient, {
40
45
  onSuccess: () => {
41
46
  toast.success(localization.auth.passwordResetSuccess)
42
47
  navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
@@ -88,7 +93,7 @@ export function ResetPassword({
88
93
 
89
94
  return (
90
95
  <Card
91
- 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)}
92
97
  variant={variant}
93
98
  {...props}
94
99
  >
@@ -98,7 +103,7 @@ export function ResetPassword({
98
103
  </Card.Title>
99
104
  </Card.Header>
100
105
 
101
- <Card.Content>
106
+ <Card.Content className="gap-4">
102
107
  <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
103
108
  <TextField
104
109
  minLength={emailAndPassword?.minPasswordLength}
@@ -194,12 +199,12 @@ export function ResetPassword({
194
199
  </Form>
195
200
  </Card.Content>
196
201
 
197
- <Card.Footer className="flex-col">
202
+ <Card.Footer className="flex-col gap-3">
198
203
  <Description className="text-sm">
199
204
  {localization.auth.rememberYourPassword}{" "}
200
205
  <Link
201
206
  href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
202
- className="text-accent decoration-accent no-underline hover:underline"
207
+ className="text-accent no-underline hover:underline decoration-accent-hover"
203
208
  >
204
209
  {localization.auth.signIn}
205
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,12 +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({
18
- onError: (error) => toast.danger(error.error?.message || error.message)
19
- })
17
+ const { mutate: updateUser, isPending: updatePending } =
18
+ useUpdateUser(authClient)
20
19
 
21
20
  const fileInputRef = useRef<HTMLInputElement>(null)
22
21
  const [isUploading, setIsUploading] = useState(false)
@@ -34,13 +34,12 @@ export function ChangeEmail({
34
34
  className,
35
35
  variant,
36
36
  ...props
37
- }: ChangeEmailProps & CardProps) {
38
- const { localization, baseURL, viewPaths } = useAuth()
39
- const { data: session } = useSession()
37
+ }: ChangeEmailProps & Omit<CardProps, "children">) {
38
+ const { authClient, localization, baseURL, viewPaths } = useAuth()
39
+ const { data: session } = useSession(authClient)
40
40
 
41
- const { mutate: changeEmail, isPending } = useChangeEmail({
42
- onSuccess: () => toast.success(localization.settings.changeEmailSuccess),
43
- onError: (error) => toast.danger(error.error?.message || error.message)
41
+ const { mutate: changeEmail, isPending } = useChangeEmail(authClient, {
42
+ onSuccess: () => toast.success(localization.settings.changeEmailSuccess)
44
43
  })
45
44
 
46
45
  function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
@@ -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
+ }