@better-auth-ui/heroui 1.6.0 → 1.6.2

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 (221) hide show
  1. package/dist/components/auth/auth-provider.d.ts +1 -1
  2. package/dist/components/auth/auth-provider.js +15 -11
  3. package/dist/components/auth/auth.d.ts +3 -3
  4. package/dist/components/auth/auth.js +36 -41
  5. package/dist/components/auth/field-separator.d.ts +1 -1
  6. package/dist/components/auth/field-separator.js +17 -4
  7. package/dist/components/auth/forgot-password.d.ts +1 -1
  8. package/dist/components/auth/forgot-password.js +69 -27
  9. package/dist/components/auth/magic-link-button.d.ts +1 -1
  10. package/dist/components/auth/magic-link-button.js +15 -18
  11. package/dist/components/auth/magic-link.d.ts +2 -2
  12. package/dist/components/auth/magic-link.js +110 -42
  13. package/dist/components/auth/passkey-button.d.ts +4 -0
  14. package/dist/components/auth/passkey-button.js +24 -0
  15. package/dist/components/auth/provider-buttons.d.ts +1 -1
  16. package/dist/components/auth/provider-buttons.js +26 -33
  17. package/dist/components/auth/reset-password.d.ts +1 -1
  18. package/dist/components/auth/reset-password.js +146 -57
  19. package/dist/components/auth/sign-in.d.ts +2 -2
  20. package/dist/components/auth/sign-in.js +163 -63
  21. package/dist/components/auth/sign-out.js +26 -32
  22. package/dist/components/auth/sign-up.d.ts +2 -2
  23. package/dist/components/auth/sign-up.js +240 -70
  24. package/dist/components/email/email-changed-email.d.ts +2 -2
  25. package/dist/components/email/email-changed-email.js +29 -22
  26. package/dist/components/email/email-verification-email.d.ts +2 -2
  27. package/dist/components/email/email-verification-email.js +29 -22
  28. package/dist/components/email/magic-link-email.d.ts +2 -2
  29. package/dist/components/email/magic-link-email.js +29 -22
  30. package/dist/components/email/new-device-email.d.ts +2 -2
  31. package/dist/components/email/new-device-email.js +29 -22
  32. package/dist/components/email/otp-email.d.ts +2 -2
  33. package/dist/components/email/otp-email.js +28 -21
  34. package/dist/components/email/password-changed-email.d.ts +2 -2
  35. package/dist/components/email/password-changed-email.js +29 -22
  36. package/dist/components/email/reset-password-email.d.ts +2 -2
  37. package/dist/components/email/reset-password-email.js +29 -22
  38. package/dist/components/settings/account/account-settings.d.ts +2 -2
  39. package/dist/components/settings/account/account-settings.js +22 -19
  40. package/dist/components/settings/account/appearance.d.ts +1 -1
  41. package/dist/components/settings/account/appearance.js +70 -27
  42. package/dist/components/settings/account/change-avatar.js +88 -55
  43. package/dist/components/settings/account/change-email.d.ts +1 -1
  44. package/dist/components/settings/account/change-email.js +56 -28
  45. package/dist/components/settings/account/manage-account.d.ts +1 -1
  46. package/dist/components/settings/account/manage-account.js +50 -31
  47. package/dist/components/settings/account/manage-accounts.d.ts +1 -1
  48. package/dist/components/settings/account/manage-accounts.js +32 -36
  49. package/dist/components/settings/account/user-profile.d.ts +3 -3
  50. package/dist/components/settings/account/user-profile.js +105 -22
  51. package/dist/components/settings/security/active-session.d.ts +1 -1
  52. package/dist/components/settings/security/active-session.js +60 -51
  53. package/dist/components/settings/security/active-sessions.d.ts +1 -1
  54. package/dist/components/settings/security/active-sessions.js +32 -26
  55. package/dist/components/settings/security/change-password.d.ts +1 -1
  56. package/dist/components/settings/security/change-password.js +189 -76
  57. package/dist/components/settings/security/danger-zone.d.ts +2 -2
  58. package/dist/components/settings/security/danger-zone.js +17 -11
  59. package/dist/components/settings/security/delete-user.d.ts +1 -1
  60. package/dist/components/settings/security/delete-user.js +96 -46
  61. package/dist/components/settings/security/linked-account.d.ts +1 -1
  62. package/dist/components/settings/security/linked-account.js +61 -47
  63. package/dist/components/settings/security/linked-accounts.d.ts +1 -1
  64. package/dist/components/settings/security/linked-accounts.js +43 -40
  65. package/dist/components/settings/security/passkey.d.ts +8 -0
  66. package/dist/components/settings/security/passkey.js +43 -0
  67. package/dist/components/settings/security/passkeys.d.ts +6 -0
  68. package/dist/components/settings/security/passkeys.js +53 -0
  69. package/dist/components/settings/security/security-settings.d.ts +2 -2
  70. package/dist/components/settings/security/security-settings.js +24 -20
  71. package/dist/components/settings/settings.d.ts +3 -3
  72. package/dist/components/settings/settings.js +44 -27
  73. package/dist/components/user/switch-account-item.d.ts +1 -1
  74. package/dist/components/user/switch-account-item.js +19 -15
  75. package/dist/components/user/switch-account-menu.js +27 -28
  76. package/dist/components/user/user-avatar.d.ts +3 -3
  77. package/dist/components/user/user-avatar.js +28 -30
  78. package/dist/components/user/user-button.d.ts +1 -1
  79. package/dist/components/user/user-button.js +90 -23
  80. package/dist/components/user/user-view.d.ts +3 -3
  81. package/dist/components/user/user-view.js +36 -23
  82. package/dist/core.d.ts +1 -1
  83. package/dist/index.d.ts +35 -33
  84. package/dist/index.js +36 -33
  85. package/dist/react-exports.d.ts +1 -1
  86. package/package.json +17 -10
  87. package/src/components/auth/auth.tsx +1 -1
  88. package/src/components/auth/magic-link.tsx +4 -0
  89. package/src/components/auth/passkey-button.tsx +36 -0
  90. package/src/components/auth/sign-in.tsx +67 -30
  91. package/src/components/auth/sign-up.tsx +97 -3
  92. package/src/components/settings/account/user-profile.tsx +121 -6
  93. package/src/components/settings/security/passkey.tsx +51 -0
  94. package/src/components/settings/security/passkeys.tsx +104 -0
  95. package/src/components/settings/security/security-settings.tsx +3 -1
  96. package/src/components/settings/settings.tsx +1 -1
  97. package/src/index.tsx +2 -0
  98. package/dist/components/account/account.d.ts +0 -18
  99. package/dist/components/account/account.js +0 -24
  100. package/dist/components/account/settings/account-settings.d.ts +0 -5
  101. package/dist/components/account/settings/account-settings.js +0 -6
  102. package/dist/components/account/settings/settings.d.ts +0 -5
  103. package/dist/components/account/settings/settings.js +0 -5
  104. package/dist/components/account/settings/user-profile.d.ts +0 -5
  105. package/dist/components/account/settings/user-profile.js +0 -20
  106. package/dist/components/settings/account/accounts.d.ts +0 -14
  107. package/dist/components/settings/account/accounts.js +0 -26
  108. package/dist/components/settings/account/theme-selector.d.ts +0 -16
  109. package/dist/components/settings/account/theme-selector.js +0 -33
  110. package/dist/components/settings/account/theme-settings.d.ts +0 -16
  111. package/dist/components/settings/account/theme-settings.js +0 -31
  112. package/dist/components/settings/account-settings.d.ts +0 -5
  113. package/dist/components/settings/account-settings.js +0 -6
  114. package/dist/components/settings/account.d.ts +0 -18
  115. package/dist/components/settings/account.js +0 -24
  116. package/dist/components/settings/security/connected-account.d.ts +0 -16
  117. package/dist/components/settings/security/connected-account.js +0 -46
  118. package/dist/components/settings/security/connected-accounts.d.ts +0 -15
  119. package/dist/components/settings/security/connected-accounts.js +0 -31
  120. package/dist/components/settings/security/sessions.d.ts +0 -14
  121. package/dist/components/settings/security/sessions.js +0 -40
  122. package/dist/components/settings/settings/account-settings.d.ts +0 -5
  123. package/dist/components/settings/settings/account-settings.js +0 -6
  124. package/dist/components/settings/settings/user-profile.d.ts +0 -5
  125. package/dist/components/settings/settings/user-profile.js +0 -20
  126. package/dist/components/settings/user-profile.d.ts +0 -5
  127. package/dist/components/settings/user-profile.js +0 -20
  128. package/dist/components/user/switch-account-popover.d.ts +0 -10
  129. package/dist/components/user/switch-account-popover.js +0 -25
  130. package/dist/components/user/switch-account-submenu.d.ts +0 -10
  131. package/dist/components/user/switch-account-submenu.js +0 -25
  132. package/dist/hooks/use-auth.d.ts +0 -2
  133. package/dist/hooks/use-auth.js +0 -8
  134. package/dist/hooks/use-redirect-to.d.ts +0 -8
  135. package/dist/hooks/use-redirect-to.js +0 -19
  136. package/dist/hooks/use-sign-in.d.ts +0 -8
  137. package/dist/hooks/use-sign-in.js +0 -53
  138. package/dist/lib/utils.d.ts +0 -2
  139. package/dist/lib/utils.js +0 -5
  140. package/dist/server.d.ts +0 -1
  141. package/dist/server.js +0 -1
  142. package/dist/src/components/auth/auth-provider.d.ts +0 -8
  143. package/dist/src/components/auth/auth-provider.js +0 -13
  144. package/dist/src/components/auth/auth.d.ts +0 -22
  145. package/dist/src/components/auth/auth.js +0 -42
  146. package/dist/src/components/auth/field-separator.d.ts +0 -4
  147. package/dist/src/components/auth/field-separator.js +0 -5
  148. package/dist/src/components/auth/forgot-password.d.ts +0 -15
  149. package/dist/src/components/auth/forgot-password.js +0 -29
  150. package/dist/src/components/auth/magic-link-button.d.ts +0 -13
  151. package/dist/src/components/auth/magic-link-button.js +0 -20
  152. package/dist/src/components/auth/magic-link.d.ts +0 -17
  153. package/dist/src/components/auth/magic-link.js +0 -37
  154. package/dist/src/components/auth/provider-buttons.d.ts +0 -19
  155. package/dist/src/components/auth/provider-buttons.js +0 -35
  156. package/dist/src/components/auth/reset-password.d.ts +0 -11
  157. package/dist/src/components/auth/reset-password.js +0 -59
  158. package/dist/src/components/auth/sign-in.d.ts +0 -14
  159. package/dist/src/components/auth/sign-in.js +0 -58
  160. package/dist/src/components/auth/sign-out.d.ts +0 -11
  161. package/dist/src/components/auth/sign-out.js +0 -34
  162. package/dist/src/components/auth/sign-up.d.ts +0 -19
  163. package/dist/src/components/auth/sign-up.js +0 -65
  164. package/dist/src/components/email/email-changed-email.d.ts +0 -3
  165. package/dist/src/components/email/email-changed-email.js +0 -23
  166. package/dist/src/components/email/email-verification-email.d.ts +0 -3
  167. package/dist/src/components/email/email-verification-email.js +0 -23
  168. package/dist/src/components/email/magic-link-email.d.ts +0 -3
  169. package/dist/src/components/email/magic-link-email.js +0 -23
  170. package/dist/src/components/email/new-device-email.d.ts +0 -3
  171. package/dist/src/components/email/new-device-email.js +0 -23
  172. package/dist/src/components/email/otp-email.d.ts +0 -3
  173. package/dist/src/components/email/otp-email.js +0 -22
  174. package/dist/src/components/email/password-changed-email.d.ts +0 -3
  175. package/dist/src/components/email/password-changed-email.js +0 -23
  176. package/dist/src/components/email/reset-password-email.d.ts +0 -3
  177. package/dist/src/components/email/reset-password-email.js +0 -23
  178. package/dist/src/components/settings/account/account-settings.d.ts +0 -13
  179. package/dist/src/components/settings/account/account-settings.js +0 -19
  180. package/dist/src/components/settings/account/appearance.d.ts +0 -16
  181. package/dist/src/components/settings/account/appearance.js +0 -28
  182. package/dist/src/components/settings/account/change-email.d.ts +0 -15
  183. package/dist/src/components/settings/account/change-email.js +0 -35
  184. package/dist/src/components/settings/account/manage-account.d.ts +0 -19
  185. package/dist/src/components/settings/account/manage-account.js +0 -27
  186. package/dist/src/components/settings/account/manage-accounts.d.ts +0 -14
  187. package/dist/src/components/settings/account/manage-accounts.js +0 -27
  188. package/dist/src/components/settings/account/user-profile.d.ts +0 -13
  189. package/dist/src/components/settings/account/user-profile.js +0 -29
  190. package/dist/src/components/settings/security/active-session.d.ts +0 -14
  191. package/dist/src/components/settings/security/active-session.js +0 -35
  192. package/dist/src/components/settings/security/active-sessions.d.ts +0 -14
  193. package/dist/src/components/settings/security/active-sessions.js +0 -29
  194. package/dist/src/components/settings/security/change-password.d.ts +0 -15
  195. package/dist/src/components/settings/security/change-password.js +0 -57
  196. package/dist/src/components/settings/security/linked-account.d.ts +0 -16
  197. package/dist/src/components/settings/security/linked-account.js +0 -46
  198. package/dist/src/components/settings/security/linked-accounts.d.ts +0 -15
  199. package/dist/src/components/settings/security/linked-accounts.js +0 -31
  200. package/dist/src/components/settings/security/security-settings.d.ts +0 -15
  201. package/dist/src/components/settings/security/security-settings.js +0 -18
  202. package/dist/src/components/settings/settings.d.ts +0 -29
  203. package/dist/src/components/settings/settings.js +0 -39
  204. package/dist/src/components/user/switch-account-item.d.ts +0 -16
  205. package/dist/src/components/user/switch-account-item.js +0 -14
  206. package/dist/src/components/user/switch-account-menu.d.ts +0 -10
  207. package/dist/src/components/user/switch-account-menu.js +0 -23
  208. package/dist/src/components/user/user-avatar.d.ts +0 -24
  209. package/dist/src/components/user/user-avatar.js +0 -31
  210. package/dist/src/components/user/user-button.d.ts +0 -21
  211. package/dist/src/components/user/user-button.js +0 -24
  212. package/dist/src/components/user/user-view.d.ts +0 -21
  213. package/dist/src/components/user/user-view.js +0 -24
  214. package/dist/src/core.d.ts +0 -1
  215. package/dist/src/core.js +0 -1
  216. package/dist/src/index.d.ts +0 -31
  217. package/dist/src/index.js +0 -32
  218. package/dist/src/lib/utils.d.ts +0 -2
  219. package/dist/src/lib/utils.js +0 -5
  220. package/dist/src/react-exports.d.ts +0 -1
  221. package/dist/src/react-exports.js +0 -1
@@ -2,7 +2,8 @@ import {
2
2
  useAuth,
3
3
  useSendVerificationEmail,
4
4
  useSignInEmail,
5
- useSignInSocial
5
+ useSignInSocial,
6
+ useSignInUsername
6
7
  } from "@better-auth-ui/react"
7
8
  import {
8
9
  Button,
@@ -24,6 +25,7 @@ import { type SyntheticEvent, useState } from "react"
24
25
 
25
26
  import { FieldSeparator } from "./field-separator"
26
27
  import { MagicLinkButton } from "./magic-link-button"
28
+ import { PasskeyButton } from "./passkey-button"
27
29
  import { ProviderButtons, type SocialLayout } from "./provider-buttons"
28
30
 
29
31
  export interface SignInProps {
@@ -33,6 +35,10 @@ export interface SignInProps {
33
35
  variant?: CardProps["variant"]
34
36
  }
35
37
 
38
+ function isEmail(value: string): boolean {
39
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
40
+ }
41
+
36
42
  /**
37
43
  * Render the sign-in UI using auth context for configuration and localization.
38
44
  *
@@ -51,8 +57,10 @@ export function SignIn({
51
57
  emailAndPassword,
52
58
  localization,
53
59
  magicLink,
60
+ passkey,
54
61
  redirectTo,
55
62
  socialProviders,
63
+ username: usernameConfig,
56
64
  viewPaths,
57
65
  navigate
58
66
  } = useAuth()
@@ -64,27 +72,38 @@ export function SignIn({
64
72
  onSuccess: () => toast.success(localization.auth.verificationEmailSent)
65
73
  })
66
74
 
67
- const { mutate: signInEmail, isPending: signInPending } = useSignInEmail({
68
- onError: (error, { email }) => {
69
- setPassword("")
70
-
71
- if (error.error?.code === "EMAIL_NOT_VERIFIED") {
72
- toast.danger(error.error?.message || error.message, {
73
- actionProps: {
74
- children: localization.auth.resend,
75
- onClick: () =>
76
- sendVerificationEmail({
77
- email,
78
- callbackURL: `${baseURL}${redirectTo}`
79
- })
80
- }
81
- })
82
- } else {
75
+ const { mutate: signInEmail, isPending: signInEmailPending } = useSignInEmail(
76
+ {
77
+ onError: (error, { email }) => {
78
+ setPassword("")
79
+
80
+ if (error.error?.code === "EMAIL_NOT_VERIFIED") {
81
+ toast.danger(error.error?.message || error.message, {
82
+ actionProps: {
83
+ children: localization.auth.resend,
84
+ onClick: () =>
85
+ sendVerificationEmail({
86
+ email,
87
+ callbackURL: `${baseURL}${redirectTo}`
88
+ })
89
+ }
90
+ })
91
+ } else {
92
+ toast.danger(error.error?.message || error.message)
93
+ }
94
+ },
95
+ onSuccess: () => navigate({ to: redirectTo })
96
+ }
97
+ )
98
+
99
+ const { mutate: signInUsername, isPending: signInUsernamePending } =
100
+ useSignInUsername({
101
+ onError: (error) => {
102
+ setPassword("")
83
103
  toast.danger(error.error?.message || error.message)
84
- }
85
- },
86
- onSuccess: () => navigate({ to: redirectTo })
87
- })
104
+ },
105
+ onSuccess: () => navigate({ to: redirectTo })
106
+ })
88
107
 
89
108
  const [socialRedirecting, setSocialRedirecting] = useState(false)
90
109
 
@@ -106,13 +125,21 @@ export function SignIn({
106
125
  const email = formData.get("email") as string
107
126
  const rememberMe = formData.get("rememberMe") === "on"
108
127
 
109
- signInEmail({
110
- email,
111
- password,
112
- ...(emailAndPassword?.rememberMe ? { rememberMe } : {})
113
- })
128
+ if (usernameConfig?.enabled && !isEmail(email)) {
129
+ signInUsername({
130
+ username: email,
131
+ password
132
+ })
133
+ } else {
134
+ signInEmail({
135
+ email,
136
+ password,
137
+ ...(emailAndPassword?.rememberMe ? { rememberMe } : {})
138
+ })
139
+ }
114
140
  }
115
141
 
142
+ const signInPending = signInEmailPending || signInUsernamePending
116
143
  const isPending = signInPending || socialPending || socialRedirecting
117
144
 
118
145
  const showSeparator = emailAndPassword?.enabled && !!socialProviders?.length
@@ -150,14 +177,22 @@ export function SignIn({
150
177
  <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
151
178
  <TextField
152
179
  name="email"
153
- type="email"
154
- autoComplete="email"
180
+ type={usernameConfig?.enabled ? "text" : "email"}
181
+ autoComplete={usernameConfig?.enabled ? "username" : "email"}
155
182
  isDisabled={isPending}
156
183
  >
157
- <Label>{localization.auth.email}</Label>
184
+ <Label>
185
+ {usernameConfig?.enabled
186
+ ? localization.auth.username
187
+ : localization.auth.email}
188
+ </Label>
158
189
 
159
190
  <Input
160
- placeholder={localization.auth.emailPlaceholder}
191
+ placeholder={
192
+ usernameConfig?.enabled
193
+ ? localization.auth.usernameOrEmailPlaceholder
194
+ : localization.auth.emailPlaceholder
195
+ }
161
196
  variant={variant === "transparent" ? "primary" : "secondary"}
162
197
  required
163
198
  />
@@ -212,6 +247,8 @@ export function SignIn({
212
247
  {magicLink && (
213
248
  <MagicLinkButton view="signIn" isPending={isPending} />
214
249
  )}
250
+
251
+ {passkey && <PasskeyButton isPending={isPending} />}
215
252
  </div>
216
253
  </Form>
217
254
  )}
@@ -1,5 +1,10 @@
1
- import { useAuth, useSignInSocial, useSignUpEmail } from "@better-auth-ui/react"
2
- import { Eye, EyeSlash } from "@gravity-ui/icons"
1
+ import {
2
+ useAuth,
3
+ useIsUsernameAvailable,
4
+ useSignInSocial,
5
+ useSignUpEmail
6
+ } from "@better-auth-ui/react"
7
+ import { Check, Eye, EyeSlash, Xmark } from "@gravity-ui/icons"
3
8
  import {
4
9
  Button,
5
10
  Card,
@@ -16,6 +21,7 @@ import {
16
21
  TextField,
17
22
  toast
18
23
  } from "@heroui/react"
24
+ import { useDebouncer } from "@tanstack/react-pacer"
19
25
  import { type SyntheticEvent, useState } from "react"
20
26
 
21
27
  import { FieldSeparator } from "./field-separator"
@@ -53,12 +59,42 @@ export function SignUp({
53
59
  magicLink,
54
60
  redirectTo,
55
61
  socialProviders,
62
+ username: usernameConfig,
56
63
  viewPaths,
57
64
  navigate
58
65
  } = useAuth()
59
66
 
60
67
  const [password, setPassword] = useState("")
61
68
  const [confirmPassword, setConfirmPassword] = useState("")
69
+ const [username, setUsername] = useState("")
70
+
71
+ const {
72
+ mutate: isUsernameAvailable,
73
+ data: usernameData,
74
+ error: usernameError,
75
+ reset: resetUsername
76
+ } = useIsUsernameAvailable()
77
+
78
+ const usernameDebouncer = useDebouncer(
79
+ (value: string) => {
80
+ if (!value.trim()) {
81
+ resetUsername()
82
+ return
83
+ }
84
+
85
+ isUsernameAvailable({ username: value.trim() })
86
+ },
87
+ { wait: 500 }
88
+ )
89
+
90
+ function handleUsernameChange(value: string) {
91
+ setUsername(value)
92
+ resetUsername()
93
+
94
+ if (usernameConfig?.isUsernameAvailable) {
95
+ usernameDebouncer.maybeExecute(value)
96
+ }
97
+ }
62
98
 
63
99
  const { mutate: signUpEmail, isPending: signUpPending } = useSignUpEmail({
64
100
  onError: (error) => {
@@ -109,7 +145,19 @@ export function SignUp({
109
145
  return
110
146
  }
111
147
 
112
- signUpEmail({ name, email, password })
148
+ signUpEmail({
149
+ name,
150
+ email,
151
+ password,
152
+ ...(usernameConfig?.enabled
153
+ ? {
154
+ username: username.trim(),
155
+ ...(usernameConfig.displayUsername
156
+ ? { displayUsername: username.trim() }
157
+ : {})
158
+ }
159
+ : {})
160
+ })
113
161
  }
114
162
 
115
163
  const showSeparator = emailAndPassword?.enabled && !!socialProviders?.length
@@ -162,6 +210,52 @@ export function SignUp({
162
210
  <FieldError />
163
211
  </TextField>
164
212
 
213
+ {usernameConfig?.enabled && (
214
+ <TextField
215
+ name="username"
216
+ type="text"
217
+ autoComplete="username"
218
+ minLength={usernameConfig.minUsernameLength}
219
+ maxLength={usernameConfig.maxUsernameLength}
220
+ isDisabled={isPending}
221
+ value={username}
222
+ onChange={handleUsernameChange}
223
+ isInvalid={
224
+ !!usernameError || (usernameData && !usernameData.available)
225
+ }
226
+ >
227
+ <Label>{localization.auth.username}</Label>
228
+
229
+ <InputGroup
230
+ variant={variant === "transparent" ? "primary" : "secondary"}
231
+ >
232
+ <InputGroup.Input
233
+ placeholder={localization.auth.usernamePlaceholder}
234
+ required
235
+ />
236
+
237
+ {usernameConfig.isUsernameAvailable && username.trim() && (
238
+ <InputGroup.Suffix className="px-2">
239
+ {usernameData?.available ? (
240
+ <Check className="text-success" />
241
+ ) : usernameError || usernameData?.available === false ? (
242
+ <Xmark className="text-danger" />
243
+ ) : (
244
+ <Spinner size="sm" color="current" />
245
+ )}
246
+ </InputGroup.Suffix>
247
+ )}
248
+ </InputGroup>
249
+
250
+ <FieldError>
251
+ {usernameError?.error?.message ||
252
+ usernameError?.message ||
253
+ (usernameData?.available === false &&
254
+ localization.auth.usernameTaken)}
255
+ </FieldError>
256
+ </TextField>
257
+ )}
258
+
165
259
  <TextField
166
260
  name="email"
167
261
  type="email"
@@ -1,4 +1,10 @@
1
- import { useAuth, useSession, useUpdateUser } from "@better-auth-ui/react"
1
+ import {
2
+ useAuth,
3
+ useIsUsernameAvailable,
4
+ useSession,
5
+ useUpdateUser
6
+ } from "@better-auth-ui/react"
7
+ import { Check, Xmark } from "@gravity-ui/icons"
2
8
  import {
3
9
  Button,
4
10
  Card,
@@ -8,13 +14,16 @@ import {
8
14
  Fieldset,
9
15
  Form,
10
16
  Input,
17
+ InputGroup,
11
18
  Label,
12
19
  Skeleton,
13
20
  Spinner,
14
21
  TextField,
15
22
  toast
16
23
  } from "@heroui/react"
17
- import type { SyntheticEvent } from "react"
24
+ import { useDebouncer } from "@tanstack/react-pacer"
25
+ import { type SyntheticEvent, useEffect, useState } from "react"
26
+
18
27
  import { ChangeAvatar } from "./change-avatar"
19
28
 
20
29
  export type UserProfileProps = {
@@ -23,18 +32,57 @@ export type UserProfileProps = {
23
32
  }
24
33
 
25
34
  /**
26
- * Render a profile card that lets the authenticated user view and update their display name and avatar.
35
+ * Render a profile card that lets the authenticated user view and update their display name, username, and avatar.
27
36
  *
28
- * @returns A JSX element containing the user profile card with avatar upload and an editable name form
37
+ * @returns A JSX element containing the user profile card with avatar upload and editable form fields
29
38
  */
30
39
  export function UserProfile({
31
40
  className,
32
41
  variant,
33
42
  ...props
34
43
  }: UserProfileProps & CardProps) {
35
- const { localization } = useAuth()
44
+ const { localization, username: usernameConfig } = useAuth()
36
45
  const { data: session } = useSession()
37
46
 
47
+ const currentUsername =
48
+ (usernameConfig?.displayUsername
49
+ ? session?.user?.displayUsername
50
+ : session?.user?.username) || ""
51
+
52
+ const [username, setUsername] = useState(currentUsername)
53
+
54
+ useEffect(() => {
55
+ setUsername(currentUsername)
56
+ }, [currentUsername])
57
+
58
+ const {
59
+ mutate: isUsernameAvailable,
60
+ data: usernameData,
61
+ error: usernameError,
62
+ reset: resetUsername
63
+ } = useIsUsernameAvailable()
64
+
65
+ const usernameDebouncer = useDebouncer(
66
+ (value: string) => {
67
+ if (!value.trim() || value.trim() === currentUsername) {
68
+ resetUsername()
69
+ return
70
+ }
71
+
72
+ isUsernameAvailable({ username: value.trim() })
73
+ },
74
+ { wait: 500 }
75
+ )
76
+
77
+ function handleUsernameChange(value: string) {
78
+ setUsername(value)
79
+ resetUsername()
80
+
81
+ if (usernameConfig?.isUsernameAvailable) {
82
+ usernameDebouncer.maybeExecute(value)
83
+ }
84
+ }
85
+
38
86
  const { mutate: updateUser, isPending } = useUpdateUser({
39
87
  onError: (error) => toast.danger(error.error?.message || error.message),
40
88
  onSuccess: () => toast.success(localization.settings.profileUpdatedSuccess)
@@ -43,7 +91,19 @@ export function UserProfile({
43
91
  function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
44
92
  e.preventDefault()
45
93
  const formData = new FormData(e.currentTarget)
46
- updateUser({ name: formData.get("name") as string })
94
+ const name = formData.get("name") as string
95
+
96
+ updateUser({
97
+ name,
98
+ ...(usernameConfig?.enabled
99
+ ? {
100
+ username: username.trim(),
101
+ ...(usernameConfig.displayUsername
102
+ ? { displayUsername: username.trim() }
103
+ : {})
104
+ }
105
+ : {})
106
+ })
47
107
  }
48
108
 
49
109
  return (
@@ -59,6 +119,61 @@ export function UserProfile({
59
119
  <ChangeAvatar />
60
120
 
61
121
  <Fieldset.Group>
122
+ {usernameConfig?.enabled && (
123
+ <TextField
124
+ type="text"
125
+ autoComplete="username"
126
+ minLength={usernameConfig.minUsernameLength}
127
+ maxLength={usernameConfig.maxUsernameLength}
128
+ isDisabled={isPending || !session}
129
+ value={username}
130
+ onChange={handleUsernameChange}
131
+ isInvalid={
132
+ !!usernameError ||
133
+ (usernameData && !usernameData.available)
134
+ }
135
+ >
136
+ <Label>{localization.auth.username}</Label>
137
+
138
+ {session ? (
139
+ <InputGroup
140
+ variant={
141
+ variant === "transparent" ? "primary" : "secondary"
142
+ }
143
+ >
144
+ <InputGroup.Input
145
+ placeholder={localization.auth.usernamePlaceholder}
146
+ name="username"
147
+ />
148
+
149
+ {usernameConfig.isUsernameAvailable &&
150
+ username.trim() &&
151
+ username.trim() !== currentUsername && (
152
+ <InputGroup.Suffix className="px-2">
153
+ {usernameData?.available ? (
154
+ <Check className="text-success" />
155
+ ) : usernameError ||
156
+ usernameData?.available === false ? (
157
+ <Xmark className="text-danger" />
158
+ ) : (
159
+ <Spinner size="sm" color="current" />
160
+ )}
161
+ </InputGroup.Suffix>
162
+ )}
163
+ </InputGroup>
164
+ ) : (
165
+ <Skeleton className="h-10 md:h-9 w-full rounded-xl" />
166
+ )}
167
+
168
+ <FieldError>
169
+ {usernameError?.error?.message ||
170
+ usernameError?.message ||
171
+ (usernameData?.available === false &&
172
+ localization.auth.usernameTaken)}
173
+ </FieldError>
174
+ </TextField>
175
+ )}
176
+
62
177
  <TextField
63
178
  key={session?.user?.name}
64
179
  name="name"
@@ -0,0 +1,51 @@
1
+ import { useAuth, useDeletePasskey } from "@better-auth-ui/react"
2
+ import { Fingerprint, Xmark } from "@gravity-ui/icons"
3
+ import { Button, Spinner, toast } from "@heroui/react"
4
+
5
+ export type PasskeyProps = {
6
+ passkey: {
7
+ id: string
8
+ name?: string | null
9
+ createdAt: Date
10
+ }
11
+ }
12
+
13
+ export function Passkey({ passkey }: PasskeyProps) {
14
+ const { localization } = useAuth()
15
+
16
+ const { mutate: deletePasskey, isPending } = useDeletePasskey({
17
+ onError: (error) => toast.danger(error.error?.message || error.message)
18
+ })
19
+
20
+ return (
21
+ <div className="flex items-center gap-3">
22
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-surface-secondary">
23
+ <Fingerprint className="size-4.5" />
24
+ </div>
25
+
26
+ <div className="flex flex-col min-w-0">
27
+ <span className="text-sm font-medium leading-tight">
28
+ {passkey.name || localization.auth.passkey}
29
+ </span>
30
+
31
+ <span className="text-xs text-muted">
32
+ {new Date(passkey.createdAt).toLocaleString(undefined, {
33
+ dateStyle: "medium",
34
+ timeStyle: "short"
35
+ })}
36
+ </span>
37
+ </div>
38
+
39
+ <Button
40
+ className="ml-auto shrink-0"
41
+ variant="outline"
42
+ size="sm"
43
+ isPending={isPending}
44
+ onPress={() => deletePasskey({ id: passkey.id })}
45
+ >
46
+ {isPending ? <Spinner color="current" size="sm" /> : <Xmark />}
47
+ {localization.settings.delete}
48
+ </Button>
49
+ </div>
50
+ )
51
+ }
@@ -0,0 +1,104 @@
1
+ import {
2
+ useAddPasskey,
3
+ useAuth,
4
+ useListUserPasskeys
5
+ } from "@better-auth-ui/react"
6
+ import {
7
+ Button,
8
+ Card,
9
+ type CardProps,
10
+ cn,
11
+ Skeleton,
12
+ Spinner,
13
+ toast
14
+ } from "@heroui/react"
15
+
16
+ import { Passkey } from "./passkey"
17
+
18
+ export type PasskeysProps = {
19
+ className?: string
20
+ variant?: CardProps["variant"]
21
+ }
22
+
23
+ export function Passkeys({
24
+ className,
25
+ variant,
26
+ ...props
27
+ }: PasskeysProps & CardProps) {
28
+ const { localization } = useAuth()
29
+
30
+ const { data: passkeys, isPending } = useListUserPasskeys({
31
+ throwOnError: (error) => {
32
+ if (error.error) toast.danger(error.error.message)
33
+ return false
34
+ }
35
+ })
36
+
37
+ const { mutate: addPasskey, isPending: isAdding } = useAddPasskey({
38
+ onError: (error) => toast.danger(error.error?.message || error.message)
39
+ })
40
+
41
+ return (
42
+ <div>
43
+ <h2 className={cn("text-sm font-semibold mb-3")}>
44
+ {localization.settings.passkeys}
45
+ </h2>
46
+
47
+ <Card className={cn(className)} variant={variant} {...props}>
48
+ <Card.Content className="gap-0">
49
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
50
+ <div>
51
+ <p className="text-sm font-medium leading-tight">
52
+ {localization.settings.passkeysDescription}
53
+ </p>
54
+
55
+ <p className="text-muted text-xs mt-0.5">
56
+ {localization.settings.passkeysInstructions}
57
+ </p>
58
+ </div>
59
+
60
+ <Button
61
+ className="shrink-0"
62
+ size="sm"
63
+ isPending={isAdding}
64
+ isDisabled={isPending}
65
+ onPress={() => addPasskey()}
66
+ >
67
+ {isAdding && <Spinner color="current" size="sm" />}
68
+ {localization.settings.addPasskey}
69
+ </Button>
70
+ </div>
71
+
72
+ {isPending ? (
73
+ <>
74
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
75
+ <PasskeySkeleton />
76
+ </>
77
+ ) : (
78
+ passkeys?.map((passkey) => (
79
+ <div key={passkey.id}>
80
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
81
+ <Passkey passkey={passkey} />
82
+ </div>
83
+ ))
84
+ )}
85
+ </Card.Content>
86
+ </Card>
87
+ </div>
88
+ )
89
+ }
90
+
91
+ function PasskeySkeleton() {
92
+ return (
93
+ <div className="flex items-center justify-between">
94
+ <div className="flex items-center gap-3">
95
+ <Skeleton className="size-10 rounded-xl" />
96
+
97
+ <div className="flex flex-col gap-1">
98
+ <Skeleton className="h-4 w-24 rounded-lg" />
99
+ <Skeleton className="h-3 w-20 rounded-lg" />
100
+ </div>
101
+ </div>
102
+ </div>
103
+ )
104
+ }
@@ -6,6 +6,7 @@ import { ActiveSessions } from "./active-sessions"
6
6
  import { ChangePassword } from "./change-password"
7
7
  import { DangerZone } from "./danger-zone"
8
8
  import { LinkedAccounts } from "./linked-accounts"
9
+ import { Passkeys } from "./passkeys"
9
10
 
10
11
  export type SecuritySettingsProps = {
11
12
  className?: string
@@ -27,7 +28,7 @@ export function SecuritySettings({
27
28
  variant,
28
29
  ...props
29
30
  }: SecuritySettingsProps & ComponentProps<"div">) {
30
- const { deleteUser, emailAndPassword, socialProviders } = useAuth()
31
+ const { deleteUser, emailAndPassword, passkey, socialProviders } = useAuth()
31
32
 
32
33
  return (
33
34
  <div
@@ -36,6 +37,7 @@ export function SecuritySettings({
36
37
  >
37
38
  {emailAndPassword?.enabled && <ChangePassword variant={variant} />}
38
39
  {!!socialProviders?.length && <LinkedAccounts variant={variant} />}
40
+ {passkey && <Passkeys variant={variant} />}
39
41
  <ActiveSessions variant={variant} />
40
42
  {deleteUser?.enabled && <DangerZone variant={variant} />}
41
43
  </div>
@@ -1,5 +1,5 @@
1
+ import type { SettingsView } from "@better-auth-ui/core"
1
2
  import { useAuth, useAuthenticate } from "@better-auth-ui/react"
2
- import type { SettingsView } from "@better-auth-ui/react/core"
3
3
  import { type CardProps, cn, Tabs } from "@heroui/react"
4
4
  import { type ComponentProps, useMemo } from "react"
5
5
 
package/src/index.tsx CHANGED
@@ -5,6 +5,7 @@ export * from "./components/auth/auth-provider"
5
5
  export * from "./components/auth/forgot-password"
6
6
  export * from "./components/auth/magic-link"
7
7
  export * from "./components/auth/magic-link-button"
8
+ export * from "./components/auth/passkey-button"
8
9
  export * from "./components/auth/provider-buttons"
9
10
  export * from "./components/auth/reset-password"
10
11
  export * from "./components/auth/sign-in"
@@ -28,6 +29,7 @@ export * from "./components/settings/security/change-password"
28
29
  export * from "./components/settings/security/danger-zone"
29
30
  export * from "./components/settings/security/delete-user"
30
31
  export * from "./components/settings/security/linked-accounts"
32
+ export * from "./components/settings/security/passkeys"
31
33
  export * from "./components/settings/security/security-settings"
32
34
  export * from "./components/settings/settings"
33
35
  export * from "./components/user/user-avatar"
@@ -1,18 +0,0 @@
1
- import type { AnyAuthConfig } from "@better-auth-ui/react";
2
- import type { AccountView } from "@better-auth-ui/react/core";
3
- import { type TabsProps } from "@heroui/react";
4
- export type AccountProps = AnyAuthConfig & {
5
- className?: string;
6
- path?: string;
7
- view?: AccountView;
8
- hideNav?: boolean;
9
- };
10
- /**
11
- * Selects and renders the appropriate account view component.
12
- *
13
- * @param path - Route path used to resolve an account view when `view` is not provided
14
- * @param view - Explicit auth view to render (e.g., "userProfile")
15
- * @param hideNav - Hide the navigation tabs
16
- */
17
- export declare function Account({ className, view, path, hideNav, ...config }: AccountProps): import("react/jsx-runtime").JSX.Element;
18
- export declare function ResponsiveTabs({ className, ...props }: TabsProps): import("react/jsx-runtime").JSX.Element;