@better-auth-ui/heroui 1.6.0

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 (253) hide show
  1. package/dist/components/account/account.d.ts +18 -0
  2. package/dist/components/account/account.js +24 -0
  3. package/dist/components/account/settings/account-settings.d.ts +5 -0
  4. package/dist/components/account/settings/account-settings.js +6 -0
  5. package/dist/components/account/settings/settings.d.ts +5 -0
  6. package/dist/components/account/settings/settings.js +5 -0
  7. package/dist/components/account/settings/user-profile.d.ts +5 -0
  8. package/dist/components/account/settings/user-profile.js +20 -0
  9. package/dist/components/auth/auth-provider.d.ts +9 -0
  10. package/dist/components/auth/auth-provider.js +12 -0
  11. package/dist/components/auth/auth.d.ts +23 -0
  12. package/dist/components/auth/auth.js +42 -0
  13. package/dist/components/auth/field-separator.d.ts +4 -0
  14. package/dist/components/auth/field-separator.js +5 -0
  15. package/dist/components/auth/forgot-password.d.ts +15 -0
  16. package/dist/components/auth/forgot-password.js +28 -0
  17. package/dist/components/auth/magic-link-button.d.ts +13 -0
  18. package/dist/components/auth/magic-link-button.js +19 -0
  19. package/dist/components/auth/magic-link.d.ts +17 -0
  20. package/dist/components/auth/magic-link.js +43 -0
  21. package/dist/components/auth/provider-buttons.d.ts +19 -0
  22. package/dist/components/auth/provider-buttons.js +34 -0
  23. package/dist/components/auth/reset-password.d.ts +11 -0
  24. package/dist/components/auth/reset-password.js +58 -0
  25. package/dist/components/auth/sign-in.d.ts +14 -0
  26. package/dist/components/auth/sign-in.js +64 -0
  27. package/dist/components/auth/sign-out.d.ts +9 -0
  28. package/dist/components/auth/sign-out.js +33 -0
  29. package/dist/components/auth/sign-up.d.ts +19 -0
  30. package/dist/components/auth/sign-up.js +71 -0
  31. package/dist/components/email/email-changed-email.d.ts +3 -0
  32. package/dist/components/email/email-changed-email.js +23 -0
  33. package/dist/components/email/email-verification-email.d.ts +3 -0
  34. package/dist/components/email/email-verification-email.js +23 -0
  35. package/dist/components/email/magic-link-email.d.ts +3 -0
  36. package/dist/components/email/magic-link-email.js +23 -0
  37. package/dist/components/email/new-device-email.d.ts +3 -0
  38. package/dist/components/email/new-device-email.js +23 -0
  39. package/dist/components/email/otp-email.d.ts +3 -0
  40. package/dist/components/email/otp-email.js +22 -0
  41. package/dist/components/email/password-changed-email.d.ts +3 -0
  42. package/dist/components/email/password-changed-email.js +23 -0
  43. package/dist/components/email/reset-password-email.d.ts +3 -0
  44. package/dist/components/email/reset-password-email.js +23 -0
  45. package/dist/components/settings/account/account-settings.d.ts +16 -0
  46. package/dist/components/settings/account/account-settings.js +20 -0
  47. package/dist/components/settings/account/accounts.d.ts +14 -0
  48. package/dist/components/settings/account/accounts.js +26 -0
  49. package/dist/components/settings/account/appearance.d.ts +16 -0
  50. package/dist/components/settings/account/appearance.js +28 -0
  51. package/dist/components/settings/account/change-avatar.d.ts +4 -0
  52. package/dist/components/settings/account/change-avatar.js +56 -0
  53. package/dist/components/settings/account/change-email.d.ts +15 -0
  54. package/dist/components/settings/account/change-email.js +29 -0
  55. package/dist/components/settings/account/manage-account.d.ts +19 -0
  56. package/dist/components/settings/account/manage-account.js +32 -0
  57. package/dist/components/settings/account/manage-accounts.d.ts +14 -0
  58. package/dist/components/settings/account/manage-accounts.js +37 -0
  59. package/dist/components/settings/account/theme-selector.d.ts +16 -0
  60. package/dist/components/settings/account/theme-selector.js +33 -0
  61. package/dist/components/settings/account/theme-settings.d.ts +16 -0
  62. package/dist/components/settings/account/theme-settings.js +31 -0
  63. package/dist/components/settings/account/user-profile.d.ts +11 -0
  64. package/dist/components/settings/account/user-profile.js +23 -0
  65. package/dist/components/settings/account-settings.d.ts +5 -0
  66. package/dist/components/settings/account-settings.js +6 -0
  67. package/dist/components/settings/account.d.ts +18 -0
  68. package/dist/components/settings/account.js +24 -0
  69. package/dist/components/settings/security/active-session.d.ts +14 -0
  70. package/dist/components/settings/security/active-session.js +53 -0
  71. package/dist/components/settings/security/active-sessions.d.ts +14 -0
  72. package/dist/components/settings/security/active-sessions.js +28 -0
  73. package/dist/components/settings/security/change-password.d.ts +15 -0
  74. package/dist/components/settings/security/change-password.js +79 -0
  75. package/dist/components/settings/security/connected-account.d.ts +16 -0
  76. package/dist/components/settings/security/connected-account.js +46 -0
  77. package/dist/components/settings/security/connected-accounts.d.ts +15 -0
  78. package/dist/components/settings/security/connected-accounts.js +31 -0
  79. package/dist/components/settings/security/danger-zone.d.ts +11 -0
  80. package/dist/components/settings/security/danger-zone.js +12 -0
  81. package/dist/components/settings/security/delete-user.d.ts +9 -0
  82. package/dist/components/settings/security/delete-user.js +47 -0
  83. package/dist/components/settings/security/linked-account.d.ts +16 -0
  84. package/dist/components/settings/security/linked-account.js +48 -0
  85. package/dist/components/settings/security/linked-accounts.d.ts +15 -0
  86. package/dist/components/settings/security/linked-accounts.js +42 -0
  87. package/dist/components/settings/security/security-settings.d.ts +17 -0
  88. package/dist/components/settings/security/security-settings.js +21 -0
  89. package/dist/components/settings/security/sessions.d.ts +14 -0
  90. package/dist/components/settings/security/sessions.js +40 -0
  91. package/dist/components/settings/settings/account-settings.d.ts +5 -0
  92. package/dist/components/settings/settings/account-settings.js +6 -0
  93. package/dist/components/settings/settings/user-profile.d.ts +5 -0
  94. package/dist/components/settings/settings/user-profile.js +20 -0
  95. package/dist/components/settings/settings.d.ts +24 -0
  96. package/dist/components/settings/settings.js +28 -0
  97. package/dist/components/settings/user-profile.d.ts +5 -0
  98. package/dist/components/settings/user-profile.js +20 -0
  99. package/dist/components/user/switch-account-item.d.ts +16 -0
  100. package/dist/components/user/switch-account-item.js +16 -0
  101. package/dist/components/user/switch-account-menu.d.ts +10 -0
  102. package/dist/components/user/switch-account-menu.js +29 -0
  103. package/dist/components/user/switch-account-popover.d.ts +10 -0
  104. package/dist/components/user/switch-account-popover.js +25 -0
  105. package/dist/components/user/switch-account-submenu.d.ts +10 -0
  106. package/dist/components/user/switch-account-submenu.js +25 -0
  107. package/dist/components/user/user-avatar.d.ts +25 -0
  108. package/dist/components/user/user-avatar.js +31 -0
  109. package/dist/components/user/user-button.d.ts +25 -0
  110. package/dist/components/user/user-button.js +24 -0
  111. package/dist/components/user/user-view.d.ts +22 -0
  112. package/dist/components/user/user-view.js +24 -0
  113. package/dist/core.d.ts +1 -0
  114. package/dist/core.js +1 -0
  115. package/dist/hooks/use-auth.d.ts +2 -0
  116. package/dist/hooks/use-auth.js +8 -0
  117. package/dist/hooks/use-redirect-to.d.ts +8 -0
  118. package/dist/hooks/use-redirect-to.js +19 -0
  119. package/dist/hooks/use-sign-in.d.ts +8 -0
  120. package/dist/hooks/use-sign-in.js +53 -0
  121. package/dist/index.d.ts +33 -0
  122. package/dist/index.js +34 -0
  123. package/dist/lib/utils.d.ts +2 -0
  124. package/dist/lib/utils.js +5 -0
  125. package/dist/react-exports.d.ts +1 -0
  126. package/dist/react-exports.js +1 -0
  127. package/dist/server.d.ts +1 -0
  128. package/dist/server.js +1 -0
  129. package/dist/src/components/auth/auth-provider.d.ts +8 -0
  130. package/dist/src/components/auth/auth-provider.js +13 -0
  131. package/dist/src/components/auth/auth.d.ts +22 -0
  132. package/dist/src/components/auth/auth.js +42 -0
  133. package/dist/src/components/auth/field-separator.d.ts +4 -0
  134. package/dist/src/components/auth/field-separator.js +5 -0
  135. package/dist/src/components/auth/forgot-password.d.ts +15 -0
  136. package/dist/src/components/auth/forgot-password.js +29 -0
  137. package/dist/src/components/auth/magic-link-button.d.ts +13 -0
  138. package/dist/src/components/auth/magic-link-button.js +20 -0
  139. package/dist/src/components/auth/magic-link.d.ts +17 -0
  140. package/dist/src/components/auth/magic-link.js +37 -0
  141. package/dist/src/components/auth/provider-buttons.d.ts +19 -0
  142. package/dist/src/components/auth/provider-buttons.js +35 -0
  143. package/dist/src/components/auth/reset-password.d.ts +11 -0
  144. package/dist/src/components/auth/reset-password.js +59 -0
  145. package/dist/src/components/auth/sign-in.d.ts +14 -0
  146. package/dist/src/components/auth/sign-in.js +58 -0
  147. package/dist/src/components/auth/sign-out.d.ts +11 -0
  148. package/dist/src/components/auth/sign-out.js +34 -0
  149. package/dist/src/components/auth/sign-up.d.ts +19 -0
  150. package/dist/src/components/auth/sign-up.js +65 -0
  151. package/dist/src/components/email/email-changed-email.d.ts +3 -0
  152. package/dist/src/components/email/email-changed-email.js +23 -0
  153. package/dist/src/components/email/email-verification-email.d.ts +3 -0
  154. package/dist/src/components/email/email-verification-email.js +23 -0
  155. package/dist/src/components/email/magic-link-email.d.ts +3 -0
  156. package/dist/src/components/email/magic-link-email.js +23 -0
  157. package/dist/src/components/email/new-device-email.d.ts +3 -0
  158. package/dist/src/components/email/new-device-email.js +23 -0
  159. package/dist/src/components/email/otp-email.d.ts +3 -0
  160. package/dist/src/components/email/otp-email.js +22 -0
  161. package/dist/src/components/email/password-changed-email.d.ts +3 -0
  162. package/dist/src/components/email/password-changed-email.js +23 -0
  163. package/dist/src/components/email/reset-password-email.d.ts +3 -0
  164. package/dist/src/components/email/reset-password-email.js +23 -0
  165. package/dist/src/components/settings/account/account-settings.d.ts +13 -0
  166. package/dist/src/components/settings/account/account-settings.js +19 -0
  167. package/dist/src/components/settings/account/appearance.d.ts +16 -0
  168. package/dist/src/components/settings/account/appearance.js +28 -0
  169. package/dist/src/components/settings/account/change-email.d.ts +15 -0
  170. package/dist/src/components/settings/account/change-email.js +35 -0
  171. package/dist/src/components/settings/account/manage-account.d.ts +19 -0
  172. package/dist/src/components/settings/account/manage-account.js +27 -0
  173. package/dist/src/components/settings/account/manage-accounts.d.ts +14 -0
  174. package/dist/src/components/settings/account/manage-accounts.js +27 -0
  175. package/dist/src/components/settings/account/user-profile.d.ts +13 -0
  176. package/dist/src/components/settings/account/user-profile.js +29 -0
  177. package/dist/src/components/settings/security/active-session.d.ts +14 -0
  178. package/dist/src/components/settings/security/active-session.js +35 -0
  179. package/dist/src/components/settings/security/active-sessions.d.ts +14 -0
  180. package/dist/src/components/settings/security/active-sessions.js +29 -0
  181. package/dist/src/components/settings/security/change-password.d.ts +15 -0
  182. package/dist/src/components/settings/security/change-password.js +57 -0
  183. package/dist/src/components/settings/security/linked-account.d.ts +16 -0
  184. package/dist/src/components/settings/security/linked-account.js +46 -0
  185. package/dist/src/components/settings/security/linked-accounts.d.ts +15 -0
  186. package/dist/src/components/settings/security/linked-accounts.js +31 -0
  187. package/dist/src/components/settings/security/security-settings.d.ts +15 -0
  188. package/dist/src/components/settings/security/security-settings.js +18 -0
  189. package/dist/src/components/settings/settings.d.ts +29 -0
  190. package/dist/src/components/settings/settings.js +39 -0
  191. package/dist/src/components/user/switch-account-item.d.ts +16 -0
  192. package/dist/src/components/user/switch-account-item.js +14 -0
  193. package/dist/src/components/user/switch-account-menu.d.ts +10 -0
  194. package/dist/src/components/user/switch-account-menu.js +23 -0
  195. package/dist/src/components/user/user-avatar.d.ts +24 -0
  196. package/dist/src/components/user/user-avatar.js +31 -0
  197. package/dist/src/components/user/user-button.d.ts +21 -0
  198. package/dist/src/components/user/user-button.js +24 -0
  199. package/dist/src/components/user/user-view.d.ts +21 -0
  200. package/dist/src/components/user/user-view.js +24 -0
  201. package/dist/src/core.d.ts +1 -0
  202. package/dist/src/core.js +1 -0
  203. package/dist/src/index.d.ts +31 -0
  204. package/dist/src/index.js +32 -0
  205. package/dist/src/lib/utils.d.ts +2 -0
  206. package/dist/src/lib/utils.js +5 -0
  207. package/dist/src/react-exports.d.ts +1 -0
  208. package/dist/src/react-exports.js +1 -0
  209. package/dist/styles.css +1 -0
  210. package/package.json +63 -0
  211. package/src/components/auth/auth-provider.tsx +27 -0
  212. package/src/components/auth/auth.tsx +87 -0
  213. package/src/components/auth/field-separator.tsx +14 -0
  214. package/src/components/auth/forgot-password.tsx +107 -0
  215. package/src/components/auth/magic-link-button.tsx +43 -0
  216. package/src/components/auth/magic-link.tsx +181 -0
  217. package/src/components/auth/provider-buttons.tsx +85 -0
  218. package/src/components/auth/reset-password.tsx +210 -0
  219. package/src/components/auth/sign-in.tsx +260 -0
  220. package/src/components/auth/sign-out.tsx +44 -0
  221. package/src/components/auth/sign-up.tsx +314 -0
  222. package/src/components/email/email-changed-email.tsx +38 -0
  223. package/src/components/email/email-verification-email.tsx +38 -0
  224. package/src/components/email/magic-link-email.tsx +38 -0
  225. package/src/components/email/new-device-email.tsx +38 -0
  226. package/src/components/email/otp-email.tsx +33 -0
  227. package/src/components/email/password-changed-email.tsx +38 -0
  228. package/src/components/email/reset-password-email.tsx +38 -0
  229. package/src/components/settings/account/account-settings.tsx +48 -0
  230. package/src/components/settings/account/appearance.tsx +157 -0
  231. package/src/components/settings/account/change-avatar.tsx +142 -0
  232. package/src/components/settings/account/change-email.tsx +111 -0
  233. package/src/components/settings/account/manage-account.tsx +123 -0
  234. package/src/components/settings/account/manage-accounts.tsx +78 -0
  235. package/src/components/settings/account/user-profile.tsx +103 -0
  236. package/src/components/settings/security/active-session.tsx +123 -0
  237. package/src/components/settings/security/active-sessions.tsx +77 -0
  238. package/src/components/settings/security/change-password.tsx +335 -0
  239. package/src/components/settings/security/danger-zone.tsx +32 -0
  240. package/src/components/settings/security/delete-user.tsx +187 -0
  241. package/src/components/settings/security/linked-account.tsx +136 -0
  242. package/src/components/settings/security/linked-accounts.tsx +98 -0
  243. package/src/components/settings/security/security-settings.tsx +43 -0
  244. package/src/components/settings/settings.tsx +96 -0
  245. package/src/components/user/switch-account-item.tsx +39 -0
  246. package/src/components/user/switch-account-menu.tsx +64 -0
  247. package/src/components/user/user-avatar.tsx +88 -0
  248. package/src/components/user/user-button.tsx +221 -0
  249. package/src/components/user/user-view.tsx +74 -0
  250. package/src/core.ts +1 -0
  251. package/src/index.tsx +35 -0
  252. package/src/react-exports.ts +1 -0
  253. package/src/styles.css +1 -0
@@ -0,0 +1,335 @@
1
+ import {
2
+ useAuth,
3
+ useChangePassword,
4
+ useListAccounts,
5
+ useRequestPasswordReset,
6
+ useSession
7
+ } from "@better-auth-ui/react"
8
+ import { Eye, EyeSlash } from "@gravity-ui/icons"
9
+ import {
10
+ Button,
11
+ Card,
12
+ type CardProps,
13
+ cn,
14
+ FieldError,
15
+ Fieldset,
16
+ Form,
17
+ Input,
18
+ InputGroup,
19
+ Label,
20
+ Skeleton,
21
+ Spinner,
22
+ TextField,
23
+ toast
24
+ } from "@heroui/react"
25
+ import { type SyntheticEvent, useState } from "react"
26
+
27
+ export type ChangePasswordProps = {
28
+ className?: string
29
+ variant?: CardProps["variant"]
30
+ }
31
+
32
+ /**
33
+ * Render a card form for changing the authenticated user's password.
34
+ *
35
+ * When the user has a credential account, displays fields for current password,
36
+ * new password, and optionally confirm password. When the user only has social
37
+ * accounts, displays a prompt to set a password via the reset flow.
38
+ *
39
+ * @returns A JSX element containing the change-password or set-password card
40
+ */
41
+ export function ChangePassword({
42
+ className,
43
+ variant,
44
+ ...props
45
+ }: ChangePasswordProps & CardProps) {
46
+ const { emailAndPassword, localization } = useAuth()
47
+ const { data: session } = useSession()
48
+ const { data: accounts, isPending: isAccountsPending } = useListAccounts()
49
+
50
+ const hasCredentialAccount = accounts?.some(
51
+ (account) => account.providerId === "credential"
52
+ )
53
+
54
+ if (!isAccountsPending && !hasCredentialAccount) {
55
+ return <SetPassword className={className} variant={variant} {...props} />
56
+ }
57
+
58
+ return (
59
+ <ChangePasswordForm
60
+ className={className}
61
+ variant={variant}
62
+ emailAndPassword={emailAndPassword}
63
+ localization={localization}
64
+ session={isAccountsPending ? undefined : session}
65
+ {...props}
66
+ />
67
+ )
68
+ }
69
+
70
+ function SetPassword({ className, variant, ...props }: CardProps) {
71
+ const { localization } = useAuth()
72
+ const { data: session } = useSession()
73
+
74
+ const { mutate: requestPasswordReset, isPending } = useRequestPasswordReset({
75
+ onError: (error) => toast.danger(error.error?.message || error.message),
76
+ onSuccess: () => toast.success(localization.auth.passwordResetEmailSent)
77
+ })
78
+
79
+ const handleSetPassword = () => {
80
+ if (!session?.user.email) return
81
+ requestPasswordReset({ email: session.user.email })
82
+ }
83
+
84
+ return (
85
+ <div>
86
+ <h2 className={cn("text-sm font-semibold mb-3")}>
87
+ {localization.settings.changePassword}
88
+ </h2>
89
+
90
+ <Card className={cn(className)} variant={variant} {...props}>
91
+ <Card.Content className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
92
+ <div>
93
+ <p className="text-sm font-medium leading-tight">
94
+ {localization.settings.setPassword}
95
+ </p>
96
+
97
+ <p className="text-muted text-xs mt-0.5">
98
+ {localization.settings.setPasswordDescription}
99
+ </p>
100
+ </div>
101
+
102
+ <Button
103
+ size="sm"
104
+ isPending={isPending}
105
+ isDisabled={!session}
106
+ onPress={handleSetPassword}
107
+ >
108
+ {isPending && <Spinner color="current" size="sm" />}
109
+ {localization.auth.sendResetLink}
110
+ </Button>
111
+ </Card.Content>
112
+ </Card>
113
+ </div>
114
+ )
115
+ }
116
+
117
+ function ChangePasswordForm({
118
+ className,
119
+ variant,
120
+ emailAndPassword,
121
+ localization,
122
+ session,
123
+ ...props
124
+ }: {
125
+ emailAndPassword: ReturnType<typeof useAuth>["emailAndPassword"]
126
+ localization: ReturnType<typeof useAuth>["localization"]
127
+ session: ReturnType<typeof useSession>["data"]
128
+ } & CardProps) {
129
+ const [currentPassword, setCurrentPassword] = useState("")
130
+ const [newPassword, setNewPassword] = useState("")
131
+ const [confirmPassword, setConfirmPassword] = useState("")
132
+
133
+ const { mutate: changePassword, isPending } = useChangePassword({
134
+ onError: (error) => {
135
+ setCurrentPassword("")
136
+ setNewPassword("")
137
+ setConfirmPassword("")
138
+ toast.danger(error.error?.message || error.message)
139
+ },
140
+ onSuccess: () => {
141
+ setCurrentPassword("")
142
+ setNewPassword("")
143
+ setConfirmPassword("")
144
+ toast.success(localization.settings.changePasswordSuccess)
145
+ }
146
+ })
147
+
148
+ const [isNewPasswordVisible, setIsNewPasswordVisible] = useState(false)
149
+ const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =
150
+ useState(false)
151
+
152
+ const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
153
+ e.preventDefault()
154
+
155
+ if (emailAndPassword?.confirmPassword && newPassword !== confirmPassword) {
156
+ setCurrentPassword("")
157
+ setNewPassword("")
158
+ setConfirmPassword("")
159
+ toast.danger(localization.auth.passwordsDoNotMatch)
160
+ return
161
+ }
162
+
163
+ changePassword({
164
+ currentPassword,
165
+ newPassword,
166
+ revokeOtherSessions: true
167
+ })
168
+ }
169
+
170
+ return (
171
+ <div>
172
+ <h2 className={cn("text-sm font-semibold mb-3")}>
173
+ {localization.settings.changePassword}
174
+ </h2>
175
+
176
+ <Card className={cn("p-4 gap-4", className)} variant={variant} {...props}>
177
+ <Card.Content>
178
+ <Form onSubmit={handleSubmit}>
179
+ <Fieldset className="w-full gap-4">
180
+ <Fieldset.Group>
181
+ <TextField
182
+ name="currentPassword"
183
+ type="password"
184
+ isDisabled={isPending || !session}
185
+ defaultValue=""
186
+ value={currentPassword}
187
+ onChange={setCurrentPassword}
188
+ >
189
+ <Label>{localization.settings.currentPassword}</Label>
190
+
191
+ <Input
192
+ className={cn(!session && "hidden")}
193
+ autoComplete="current-password"
194
+ placeholder={
195
+ localization.settings.currentPasswordPlaceholder
196
+ }
197
+ required
198
+ variant={
199
+ variant === "transparent" ? "primary" : "secondary"
200
+ }
201
+ />
202
+
203
+ {!session && (
204
+ <Skeleton className="h-10 md:h-9 w-full rounded-xl" />
205
+ )}
206
+
207
+ <FieldError />
208
+ </TextField>
209
+
210
+ <TextField
211
+ minLength={emailAndPassword?.minPasswordLength}
212
+ maxLength={emailAndPassword?.maxPasswordLength}
213
+ isDisabled={isPending || !session}
214
+ value={newPassword}
215
+ onChange={setNewPassword}
216
+ >
217
+ <Label>{localization.auth.newPassword}</Label>
218
+
219
+ <InputGroup
220
+ className={cn(!session && "hidden")}
221
+ variant={
222
+ variant === "transparent" ? "primary" : "secondary"
223
+ }
224
+ >
225
+ <InputGroup.Input
226
+ name="newPassword"
227
+ type={isNewPasswordVisible ? "text" : "password"}
228
+ autoComplete="new-password"
229
+ placeholder={localization.auth.newPasswordPlaceholder}
230
+ required
231
+ />
232
+
233
+ <InputGroup.Suffix className="px-0">
234
+ <Button
235
+ isIconOnly
236
+ aria-label={
237
+ isNewPasswordVisible
238
+ ? localization.auth.hidePassword
239
+ : localization.auth.showPassword
240
+ }
241
+ size="sm"
242
+ variant="ghost"
243
+ onPress={() =>
244
+ setIsNewPasswordVisible(!isNewPasswordVisible)
245
+ }
246
+ isDisabled={isPending}
247
+ >
248
+ {isNewPasswordVisible ? <EyeSlash /> : <Eye />}
249
+ </Button>
250
+ </InputGroup.Suffix>
251
+ </InputGroup>
252
+
253
+ {!session && (
254
+ <Skeleton className="h-10 md:h-9 w-full rounded-xl" />
255
+ )}
256
+
257
+ <FieldError />
258
+ </TextField>
259
+
260
+ {emailAndPassword?.confirmPassword && (
261
+ <TextField
262
+ minLength={emailAndPassword?.minPasswordLength}
263
+ maxLength={emailAndPassword?.maxPasswordLength}
264
+ isDisabled={isPending || !session}
265
+ isRequired
266
+ value={confirmPassword}
267
+ onChange={setConfirmPassword}
268
+ >
269
+ <Label>{localization.auth.confirmPassword}</Label>
270
+
271
+ <InputGroup
272
+ className={cn(!session && "hidden")}
273
+ variant={
274
+ variant === "transparent" ? "primary" : "secondary"
275
+ }
276
+ >
277
+ <InputGroup.Input
278
+ name="confirmPassword"
279
+ type={isConfirmPasswordVisible ? "text" : "password"}
280
+ autoComplete="new-password"
281
+ placeholder={
282
+ localization.auth.confirmPasswordPlaceholder
283
+ }
284
+ required
285
+ />
286
+
287
+ <InputGroup.Suffix className="px-0">
288
+ <Button
289
+ isIconOnly
290
+ aria-label={
291
+ isConfirmPasswordVisible
292
+ ? localization.auth.hidePassword
293
+ : localization.auth.showPassword
294
+ }
295
+ size="sm"
296
+ variant="ghost"
297
+ onPress={() =>
298
+ setIsConfirmPasswordVisible(
299
+ !isConfirmPasswordVisible
300
+ )
301
+ }
302
+ isDisabled={isPending}
303
+ >
304
+ {isConfirmPasswordVisible ? <EyeSlash /> : <Eye />}
305
+ </Button>
306
+ </InputGroup.Suffix>
307
+ </InputGroup>
308
+
309
+ {!session && (
310
+ <Skeleton className="h-10 md:h-9 w-full rounded-xl" />
311
+ )}
312
+
313
+ <FieldError />
314
+ </TextField>
315
+ )}
316
+ </Fieldset.Group>
317
+
318
+ <Fieldset.Actions>
319
+ <Button
320
+ type="submit"
321
+ isPending={isPending}
322
+ isDisabled={!session}
323
+ size="sm"
324
+ >
325
+ {isPending && <Spinner color="current" size="sm" />}
326
+ {localization.settings.updatePassword}
327
+ </Button>
328
+ </Fieldset.Actions>
329
+ </Fieldset>
330
+ </Form>
331
+ </Card.Content>
332
+ </Card>
333
+ </div>
334
+ )
335
+ }
@@ -0,0 +1,32 @@
1
+ import { useAuth } from "@better-auth-ui/react"
2
+ import { type CardProps, cn } from "@heroui/react"
3
+ import type { ComponentProps } from "react"
4
+
5
+ import { DeleteUser } from "./delete-user"
6
+
7
+ export type DangerZoneProps = {
8
+ className?: string
9
+ variant?: CardProps["variant"]
10
+ }
11
+
12
+ /**
13
+ * Renders the danger zone heading and {@link DeleteUser}.
14
+ * Gate with `deleteUser.enabled` at the call site (e.g. {@link SecuritySettings}).
15
+ */
16
+ export function DangerZone({
17
+ className,
18
+ variant,
19
+ ...props
20
+ }: DangerZoneProps & ComponentProps<"div">) {
21
+ const { localization } = useAuth()
22
+
23
+ return (
24
+ <div className={cn("flex w-full flex-col", className)} {...props}>
25
+ <h2 className={cn("text-sm font-semibold mb-3")}>
26
+ {localization.settings.dangerZone}
27
+ </h2>
28
+
29
+ <DeleteUser variant={variant} />
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,187 @@
1
+ import { useAuth, useDeleteUser, useListAccounts } from "@better-auth-ui/react"
2
+ import { TriangleExclamation } from "@gravity-ui/icons"
3
+ import {
4
+ AlertDialog,
5
+ Button,
6
+ Card,
7
+ type CardProps,
8
+ cn,
9
+ FieldError,
10
+ Form,
11
+ Input,
12
+ Label,
13
+ Spinner,
14
+ TextField,
15
+ toast
16
+ } from "@heroui/react"
17
+ import { type SyntheticEvent, useState } from "react"
18
+
19
+ export type DeleteUserProps = {
20
+ className?: string
21
+ variant?: CardProps["variant"]
22
+ }
23
+
24
+ /**
25
+ * Danger-zone card to delete the authenticated account, with a confirmation dialog and toasts.
26
+ */
27
+ export function DeleteUser({
28
+ className,
29
+ variant,
30
+ ...props
31
+ }: DeleteUserProps & CardProps) {
32
+ const {
33
+ basePaths,
34
+ deleteUser: deleteUserConfig,
35
+ localization,
36
+ navigate,
37
+ viewPaths
38
+ } = useAuth()
39
+
40
+ const { data: accounts } = useListAccounts()
41
+
42
+ const [confirmOpen, setConfirmOpen] = useState(false)
43
+ const [password, setPassword] = useState("")
44
+
45
+ const hasCredentialAccount = accounts?.some(
46
+ (account) => account.providerId === "credential"
47
+ )
48
+ const needsPassword =
49
+ !deleteUserConfig?.sendDeleteAccountVerification && hasCredentialAccount
50
+
51
+ const { mutate: deleteUser, isPending } = useDeleteUser()
52
+
53
+ const handleDialogOpenChange = (open: boolean) => {
54
+ setConfirmOpen(open)
55
+ setPassword("")
56
+ }
57
+
58
+ const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
59
+ e.preventDefault()
60
+
61
+ const params = {
62
+ ...(needsPassword ? { password } : {})
63
+ }
64
+
65
+ deleteUser(params, {
66
+ onSuccess: () => {
67
+ setConfirmOpen(false)
68
+ setPassword("")
69
+
70
+ if (deleteUserConfig?.sendDeleteAccountVerification) {
71
+ toast.success(localization.settings.deleteUserVerificationSent)
72
+ } else {
73
+ toast.success(localization.settings.deleteUserSuccess)
74
+ navigate({
75
+ to: `${basePaths.auth}/${viewPaths.auth.signIn}`,
76
+ replace: true
77
+ })
78
+ }
79
+ },
80
+ onError: (error) => {
81
+ toast.danger(error.error?.message || error.message)
82
+ }
83
+ })
84
+ }
85
+
86
+ return (
87
+ <Card
88
+ className={cn("border border-danger", className)}
89
+ variant={variant}
90
+ {...props}
91
+ >
92
+ <Card.Content className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
93
+ <div>
94
+ <p className="text-sm font-medium leading-tight">
95
+ {localization.settings.deleteUser}
96
+ </p>
97
+
98
+ <p className="text-muted text-xs mt-0.5">
99
+ {localization.settings.deleteUserDescription}
100
+ </p>
101
+ </div>
102
+
103
+ <AlertDialog>
104
+ <Button
105
+ isDisabled={!accounts}
106
+ size="sm"
107
+ variant="danger"
108
+ onPress={() => setConfirmOpen(true)}
109
+ >
110
+ {localization.settings.deleteUser}
111
+ </Button>
112
+
113
+ <AlertDialog.Backdrop
114
+ isOpen={confirmOpen}
115
+ onOpenChange={handleDialogOpenChange}
116
+ >
117
+ <AlertDialog.Container>
118
+ <AlertDialog.Dialog>
119
+ <Form onSubmit={handleSubmit}>
120
+ <AlertDialog.CloseTrigger />
121
+
122
+ <AlertDialog.Header>
123
+ <AlertDialog.Icon status="danger">
124
+ <TriangleExclamation />
125
+ </AlertDialog.Icon>
126
+
127
+ <AlertDialog.Heading>
128
+ {localization.settings.deleteUser}
129
+ </AlertDialog.Heading>
130
+ </AlertDialog.Header>
131
+
132
+ <AlertDialog.Body className="overflow-visible">
133
+ <p className="text-muted text-sm">
134
+ {localization.settings.deleteUserDescription}
135
+ </p>
136
+
137
+ {needsPassword && (
138
+ <TextField
139
+ className="mt-4"
140
+ name="password"
141
+ type="password"
142
+ isDisabled={isPending}
143
+ value={password}
144
+ onChange={setPassword}
145
+ >
146
+ <Label>{localization.auth.password}</Label>
147
+
148
+ <Input
149
+ autoComplete="current-password"
150
+ placeholder={localization.auth.passwordPlaceholder}
151
+ required
152
+ variant="secondary"
153
+ />
154
+
155
+ <FieldError />
156
+ </TextField>
157
+ )}
158
+ </AlertDialog.Body>
159
+
160
+ <AlertDialog.Footer>
161
+ <Button
162
+ slot="close"
163
+ variant="tertiary"
164
+ isDisabled={isPending}
165
+ >
166
+ {localization.settings.cancel}
167
+ </Button>
168
+
169
+ <Button
170
+ type="submit"
171
+ variant="danger"
172
+ isPending={isPending}
173
+ >
174
+ {isPending && <Spinner color="current" size="sm" />}
175
+
176
+ {localization.settings.deleteUser}
177
+ </Button>
178
+ </AlertDialog.Footer>
179
+ </Form>
180
+ </AlertDialog.Dialog>
181
+ </AlertDialog.Container>
182
+ </AlertDialog.Backdrop>
183
+ </AlertDialog>
184
+ </Card.Content>
185
+ </Card>
186
+ )
187
+ }
@@ -0,0 +1,136 @@
1
+ import { getProviderName } from "@better-auth-ui/core"
2
+ import {
3
+ providerIcons,
4
+ useAccountInfo,
5
+ useAuth,
6
+ useLinkSocial,
7
+ useUnlinkAccount
8
+ } from "@better-auth-ui/react"
9
+ import { Link, LinkSlash, PlugConnection } from "@gravity-ui/icons"
10
+ import { Button, cn, Skeleton, Spinner, toast } from "@heroui/react"
11
+ import type { Account, SocialProvider } from "better-auth"
12
+
13
+ export type LinkedAccountProps = {
14
+ account?: Account
15
+ provider: SocialProvider
16
+ }
17
+
18
+ /**
19
+ * Render a single linked social account row with provider info and link/unlink control.
20
+ *
21
+ * Fetches additional account information from the provider using the accountInfo API
22
+ * and displays the provider name, account details, and a link/unlink button.
23
+ *
24
+ * @param account - The account object containing id, accountId, and providerId
25
+ * @param provider - The provider id
26
+ * @returns A JSX element containing the linked account row
27
+ */
28
+ export function LinkedAccount({ account, provider }: LinkedAccountProps) {
29
+ const { baseURL, localization } = useAuth()
30
+
31
+ const { data: accountInfo, isPending: isLoadingInfo } = useAccountInfo(
32
+ account?.accountId,
33
+ {
34
+ enabled: !!account,
35
+ throwOnError: (error) => {
36
+ if (error.error) toast.danger(error.error.message)
37
+ return false
38
+ }
39
+ }
40
+ )
41
+
42
+ const { mutate: linkSocial, isPending: isLinking } = useLinkSocial({
43
+ onError: (error) => toast.danger(error.error?.message || error.message)
44
+ })
45
+
46
+ const { mutate: unlinkAccount, isPending: isUnlinking } = useUnlinkAccount({
47
+ onError: (error) => toast.danger(error.error?.message || error.message),
48
+ onSuccess: () => toast.success(localization.settings.accountUnlinked)
49
+ })
50
+
51
+ const ProviderIcon = providerIcons[provider]
52
+ const providerName = getProviderName(provider)
53
+
54
+ const displayName =
55
+ accountInfo?.data?.login ||
56
+ accountInfo?.data?.username ||
57
+ accountInfo?.user?.email ||
58
+ accountInfo?.user?.name ||
59
+ account?.accountId
60
+
61
+ return (
62
+ <div className="flex items-center gap-3">
63
+ <div
64
+ className={cn(
65
+ "flex size-10 shrink-0 items-center justify-center rounded-xl bg-surface-secondary"
66
+ )}
67
+ >
68
+ {ProviderIcon ? (
69
+ <ProviderIcon className={cn("size-4.5", !account && "opacity-50")} />
70
+ ) : (
71
+ <PlugConnection
72
+ className={cn("size-4.5", !account && "opacity-50")}
73
+ />
74
+ )}
75
+ </div>
76
+
77
+ <div className="flex flex-col min-w-0">
78
+ <span className="text-sm font-medium leading-tight">
79
+ {providerName}
80
+ </span>
81
+
82
+ {account && isLoadingInfo ? (
83
+ <Skeleton className="h-3 w-24 my-0.5 rounded-lg" />
84
+ ) : (
85
+ <span className="text-xs text-muted truncate">
86
+ {account
87
+ ? displayName
88
+ : localization.settings.linkProvider.replace(
89
+ "{{provider}}",
90
+ providerName
91
+ )}
92
+ </span>
93
+ )}
94
+ </div>
95
+
96
+ {account ? (
97
+ <Button
98
+ className="ml-auto shrink-0"
99
+ variant="outline"
100
+ size="sm"
101
+ onPress={() => unlinkAccount({ providerId: account.providerId })}
102
+ isPending={isUnlinking}
103
+ aria-label={localization.settings.unlinkProvider.replace(
104
+ "{{provider}}",
105
+ providerName
106
+ )}
107
+ >
108
+ {isUnlinking ? <Spinner color="current" size="sm" /> : <LinkSlash />}
109
+ {localization.settings.unlinkProvider
110
+ .replace("{{provider}}", "")
111
+ .trim()}
112
+ </Button>
113
+ ) : (
114
+ <Button
115
+ className="ml-auto shrink-0"
116
+ variant="outline"
117
+ size="sm"
118
+ onPress={() =>
119
+ linkSocial({
120
+ provider,
121
+ callbackURL: `${baseURL}${window.location.pathname}`
122
+ })
123
+ }
124
+ isPending={isLinking}
125
+ aria-label={localization.settings.linkProvider.replace(
126
+ "{{provider}}",
127
+ providerName
128
+ )}
129
+ >
130
+ {isLinking ? <Spinner color="current" size="sm" /> : <Link />}
131
+ {localization.settings.link}
132
+ </Button>
133
+ )}
134
+ </div>
135
+ )
136
+ }