better-auth-ui 3.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/auth-hooks-IOEvlYYv.d.cts +6966 -0
  4. package/dist/auth-hooks-IOEvlYYv.d.ts +6966 -0
  5. package/dist/auth-mutators-DdqOmQ32.d.cts +29 -0
  6. package/dist/auth-mutators-DdqOmQ32.d.ts +29 -0
  7. package/dist/auth-ui-provider-BsH3xJDw.d.ts +697 -0
  8. package/dist/auth-ui-provider-DhZfncd3.d.cts +697 -0
  9. package/dist/chunk-BDFQSFBU.js +750 -0
  10. package/dist/chunk-CRAHKL2C.cjs +801 -0
  11. package/dist/chunk-MJPOA6PK.js +801 -0
  12. package/dist/chunk-SV64DXGW.cjs +750 -0
  13. package/dist/index.cjs +12618 -0
  14. package/dist/index.d.cts +771 -0
  15. package/dist/index.d.ts +771 -0
  16. package/dist/index.js +12618 -0
  17. package/dist/instantdb.cjs +189 -0
  18. package/dist/instantdb.d.cts +36 -0
  19. package/dist/instantdb.d.ts +36 -0
  20. package/dist/instantdb.js +189 -0
  21. package/dist/metafile-cjs.json +1 -0
  22. package/dist/metafile-esm.json +1 -0
  23. package/dist/server.cjs +194 -0
  24. package/dist/server.d.cts +35 -0
  25. package/dist/server.d.ts +35 -0
  26. package/dist/server.js +194 -0
  27. package/dist/style.css +1 -0
  28. package/dist/tanstack.cjs +153 -0
  29. package/dist/tanstack.d.cts +18 -0
  30. package/dist/tanstack.d.ts +18 -0
  31. package/dist/tanstack.js +153 -0
  32. package/dist/triplit.cjs +201 -0
  33. package/dist/triplit.d.cts +31 -0
  34. package/dist/triplit.d.ts +31 -0
  35. package/dist/triplit.js +201 -0
  36. package/dist/utils-C5R37WDe.d.cts +3 -0
  37. package/dist/utils-C5R37WDe.d.ts +3 -0
  38. package/dist/view-paths-CHSJf5dv.d.cts +645 -0
  39. package/dist/view-paths-CHSJf5dv.d.ts +645 -0
  40. package/package.json +156 -0
  41. package/src/components/account/account-view.tsx +220 -0
  42. package/src/components/auth/auth-callback.tsx +36 -0
  43. package/src/components/auth/auth-form.tsx +277 -0
  44. package/src/components/auth/auth-view.tsx +389 -0
  45. package/src/components/auth/email-otp-button.tsx +53 -0
  46. package/src/components/auth/forms/email-otp-form.tsx +288 -0
  47. package/src/components/auth/forms/forgot-password-form.tsx +168 -0
  48. package/src/components/auth/forms/magic-link-form.tsx +191 -0
  49. package/src/components/auth/forms/recover-account-form.tsx +138 -0
  50. package/src/components/auth/forms/reset-password-form.tsx +215 -0
  51. package/src/components/auth/forms/sign-in-form.tsx +289 -0
  52. package/src/components/auth/forms/sign-up-form.tsx +788 -0
  53. package/src/components/auth/forms/two-factor-form.tsx +372 -0
  54. package/src/components/auth/magic-link-button.tsx +54 -0
  55. package/src/components/auth/one-tap.tsx +48 -0
  56. package/src/components/auth/otp-input-group.tsx +65 -0
  57. package/src/components/auth/passkey-button.tsx +85 -0
  58. package/src/components/auth/provider-button.tsx +141 -0
  59. package/src/components/auth/sign-out.tsx +25 -0
  60. package/src/components/auth-loading.tsx +21 -0
  61. package/src/components/captcha/captcha.tsx +79 -0
  62. package/src/components/captcha/recaptcha-badge.tsx +61 -0
  63. package/src/components/captcha/recaptcha-v2.tsx +58 -0
  64. package/src/components/captcha/recaptcha-v3.tsx +73 -0
  65. package/src/components/email/email-template.tsx +216 -0
  66. package/src/components/form-error.tsx +27 -0
  67. package/src/components/organization/accept-invitation-card.tsx +362 -0
  68. package/src/components/organization/create-organization-dialog.tsx +395 -0
  69. package/src/components/organization/delete-organization-card.tsx +101 -0
  70. package/src/components/organization/delete-organization-dialog.tsx +209 -0
  71. package/src/components/organization/invitation-cell.tsx +156 -0
  72. package/src/components/organization/invite-member-dialog.tsx +258 -0
  73. package/src/components/organization/leave-organization-dialog.tsx +150 -0
  74. package/src/components/organization/member-cell.tsx +187 -0
  75. package/src/components/organization/organization-cell-view.tsx +122 -0
  76. package/src/components/organization/organization-cell.tsx +154 -0
  77. package/src/components/organization/organization-invitations-card.tsx +94 -0
  78. package/src/components/organization/organization-logo-card.tsx +308 -0
  79. package/src/components/organization/organization-logo.tsx +120 -0
  80. package/src/components/organization/organization-members-card.tsx +155 -0
  81. package/src/components/organization/organization-name-card.tsx +204 -0
  82. package/src/components/organization/organization-settings-cards.tsx +67 -0
  83. package/src/components/organization/organization-slug-card.tsx +223 -0
  84. package/src/components/organization/organization-switcher.tsx +512 -0
  85. package/src/components/organization/organization-view.tsx +228 -0
  86. package/src/components/organization/organizations-card.tsx +72 -0
  87. package/src/components/organization/personal-account-view.tsx +115 -0
  88. package/src/components/organization/remove-member-dialog.tsx +144 -0
  89. package/src/components/organization/update-member-role-dialog.tsx +213 -0
  90. package/src/components/organization/user-invitations-card.tsx +238 -0
  91. package/src/components/password-input.tsx +56 -0
  92. package/src/components/provider-icons.tsx +385 -0
  93. package/src/components/redirect-to-sign-in.tsx +16 -0
  94. package/src/components/redirect-to-sign-up.tsx +16 -0
  95. package/src/components/settings/account/account-cell.tsx +158 -0
  96. package/src/components/settings/account/accounts-card.tsx +75 -0
  97. package/src/components/settings/account/delete-account-card.tsx +65 -0
  98. package/src/components/settings/account/delete-account-dialog.tsx +231 -0
  99. package/src/components/settings/account/update-avatar-card.tsx +198 -0
  100. package/src/components/settings/account/update-field-card.tsx +282 -0
  101. package/src/components/settings/account/update-name-card.tsx +39 -0
  102. package/src/components/settings/account/update-username-card.tsx +42 -0
  103. package/src/components/settings/account-settings-cards.tsx +123 -0
  104. package/src/components/settings/api-key/api-key-cell.tsx +108 -0
  105. package/src/components/settings/api-key/api-key-delete-dialog.tsx +162 -0
  106. package/src/components/settings/api-key/api-key-display-dialog.tsx +110 -0
  107. package/src/components/settings/api-key/api-keys-card.tsx +98 -0
  108. package/src/components/settings/api-key/create-api-key-dialog.tsx +376 -0
  109. package/src/components/settings/passkey/passkey-cell.tsx +113 -0
  110. package/src/components/settings/passkey/passkeys-card.tsx +111 -0
  111. package/src/components/settings/providers/provider-cell.tsx +152 -0
  112. package/src/components/settings/providers/providers-card.tsx +108 -0
  113. package/src/components/settings/security/change-email-card.tsx +200 -0
  114. package/src/components/settings/security/change-password-card.tsx +326 -0
  115. package/src/components/settings/security/session-cell.tsx +120 -0
  116. package/src/components/settings/security/sessions-card.tsx +58 -0
  117. package/src/components/settings/security-settings-cards.tsx +111 -0
  118. package/src/components/settings/shared/session-freshness-dialog.tsx +97 -0
  119. package/src/components/settings/shared/settings-action-button.tsx +51 -0
  120. package/src/components/settings/shared/settings-card-footer.tsx +94 -0
  121. package/src/components/settings/shared/settings-card-header.tsx +67 -0
  122. package/src/components/settings/shared/settings-card.tsx +106 -0
  123. package/src/components/settings/skeletons/input-field-skeleton.tsx +18 -0
  124. package/src/components/settings/skeletons/settings-cell-skeleton.tsx +37 -0
  125. package/src/components/settings/two-factor/backup-codes-dialog.tsx +113 -0
  126. package/src/components/settings/two-factor/two-factor-card.tsx +63 -0
  127. package/src/components/settings/two-factor/two-factor-password-dialog.tsx +226 -0
  128. package/src/components/signed-in.tsx +20 -0
  129. package/src/components/signed-out.tsx +20 -0
  130. package/src/components/ui/alert.tsx +66 -0
  131. package/src/components/ui/avatar.tsx +53 -0
  132. package/src/components/ui/button.tsx +59 -0
  133. package/src/components/ui/card.tsx +92 -0
  134. package/src/components/ui/checkbox.tsx +32 -0
  135. package/src/components/ui/dialog.tsx +143 -0
  136. package/src/components/ui/drawer.tsx +135 -0
  137. package/src/components/ui/dropdown-menu.tsx +257 -0
  138. package/src/components/ui/form.tsx +167 -0
  139. package/src/components/ui/input-otp.tsx +77 -0
  140. package/src/components/ui/input.tsx +21 -0
  141. package/src/components/ui/label.tsx +24 -0
  142. package/src/components/ui/select.tsx +185 -0
  143. package/src/components/ui/separator.tsx +28 -0
  144. package/src/components/ui/skeleton.tsx +13 -0
  145. package/src/components/ui/tabs.tsx +66 -0
  146. package/src/components/ui/textarea.tsx +18 -0
  147. package/src/components/user-avatar.tsx +147 -0
  148. package/src/components/user-button.tsx +409 -0
  149. package/src/components/user-view.tsx +138 -0
  150. package/src/hooks/use-auth-data.ts +184 -0
  151. package/src/hooks/use-authenticate.ts +62 -0
  152. package/src/hooks/use-captcha.tsx +138 -0
  153. package/src/hooks/use-current-organization.ts +59 -0
  154. package/src/hooks/use-hydrated.ts +13 -0
  155. package/src/hooks/use-lang.ts +32 -0
  156. package/src/hooks/use-success-transition.ts +51 -0
  157. package/src/hooks/use-theme.ts +39 -0
  158. package/src/index.ts +65 -0
  159. package/src/instantdb.ts +1 -0
  160. package/src/lib/auth-data-cache.ts +90 -0
  161. package/src/lib/auth-ui-provider.tsx +658 -0
  162. package/src/lib/gravatar-utils.ts +58 -0
  163. package/src/lib/image-utils.ts +55 -0
  164. package/src/lib/instantdb/model-names.ts +24 -0
  165. package/src/lib/instantdb/use-instant-options.ts +98 -0
  166. package/src/lib/instantdb/use-list-accounts.ts +38 -0
  167. package/src/lib/instantdb/use-list-sessions.ts +53 -0
  168. package/src/lib/instantdb/use-session.ts +55 -0
  169. package/src/lib/organization-refetcher.tsx +56 -0
  170. package/src/lib/social-providers.ts +144 -0
  171. package/src/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  172. package/src/lib/tanstack/use-tanstack-options.ts +112 -0
  173. package/src/lib/triplit/model-names.ts +24 -0
  174. package/src/lib/triplit/use-conditional-query.ts +82 -0
  175. package/src/lib/triplit/use-list-accounts.ts +31 -0
  176. package/src/lib/triplit/use-list-sessions.ts +33 -0
  177. package/src/lib/triplit/use-session.ts +42 -0
  178. package/src/lib/triplit/use-triplit-hooks.ts +68 -0
  179. package/src/lib/triplit/use-triplit-token.ts +44 -0
  180. package/src/lib/utils.ts +105 -0
  181. package/src/lib/view-paths.ts +55 -0
  182. package/src/localization/admin-error-codes.ts +20 -0
  183. package/src/localization/anonymous-error-codes.ts +6 -0
  184. package/src/localization/api-key-error-codes.ts +32 -0
  185. package/src/localization/auth-localization.ts +740 -0
  186. package/src/localization/base-error-codes.ts +27 -0
  187. package/src/localization/captcha-error-codes.ts +17 -0
  188. package/src/localization/email-otp-error-codes.ts +7 -0
  189. package/src/localization/generic-oauth-error-codes.ts +3 -0
  190. package/src/localization/haveibeenpwned-error-codes.ts +4 -0
  191. package/src/localization/multi-session-error-codes.ts +3 -0
  192. package/src/localization/organization-error-codes.ts +57 -0
  193. package/src/localization/passkey-error-codes.ts +10 -0
  194. package/src/localization/phone-number-error-codes.ts +10 -0
  195. package/src/localization/stripe-localization.ts +12 -0
  196. package/src/localization/two-factor-error-codes.ts +12 -0
  197. package/src/localization/username-error-codes.ts +9 -0
  198. package/src/server.ts +4 -0
  199. package/src/style.css +1 -0
  200. package/src/tanstack.ts +1 -0
  201. package/src/triplit.ts +1 -0
  202. package/src/types/account-options.ts +35 -0
  203. package/src/types/additional-fields.ts +21 -0
  204. package/src/types/any-auth-client.ts +6 -0
  205. package/src/types/api-key.ts +9 -0
  206. package/src/types/auth-client.ts +37 -0
  207. package/src/types/auth-hooks.ts +61 -0
  208. package/src/types/auth-mutators.ts +17 -0
  209. package/src/types/avatar-options.ts +29 -0
  210. package/src/types/captcha-options.ts +32 -0
  211. package/src/types/captcha-provider.ts +6 -0
  212. package/src/types/credentials-options.ts +32 -0
  213. package/src/types/delete-user-options.ts +7 -0
  214. package/src/types/fetch-error.ts +6 -0
  215. package/src/types/generic-oauth-options.ts +16 -0
  216. package/src/types/gravatar-options.ts +21 -0
  217. package/src/types/image.ts +7 -0
  218. package/src/types/invitation.ts +10 -0
  219. package/src/types/link.ts +7 -0
  220. package/src/types/organization-options.ts +106 -0
  221. package/src/types/password-validation.ts +16 -0
  222. package/src/types/profile.ts +15 -0
  223. package/src/types/refetch.ts +1 -0
  224. package/src/types/render-toast.ts +9 -0
  225. package/src/types/sign-up-options.ts +7 -0
  226. package/src/types/social-options.ts +16 -0
@@ -0,0 +1,184 @@
1
+ import {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ useSyncExternalStore
8
+ } from "react"
9
+
10
+ import { authDataCache } from "../lib/auth-data-cache"
11
+ import { AuthUIContext } from "../lib/auth-ui-provider"
12
+ import { getLocalizedError } from "../lib/utils"
13
+ import type { FetchError } from "../types/fetch-error"
14
+
15
+ export function useAuthData<T>({
16
+ queryFn,
17
+ cacheKey,
18
+ staleTime = 10000 // Default 10 seconds
19
+ }: {
20
+ queryFn: () => Promise<{ data: T | null; error?: FetchError | null }>
21
+ cacheKey?: string
22
+ staleTime?: number
23
+ }) {
24
+ const {
25
+ hooks: { useSession },
26
+ toast,
27
+ localization
28
+ } = useContext(AuthUIContext)
29
+ const { data: sessionData, isPending: sessionPending } = useSession()
30
+
31
+ // Generate a stable cache key based on the queryFn if not provided
32
+ const queryFnRef = useRef(queryFn)
33
+ queryFnRef.current = queryFn
34
+
35
+ const stableCacheKey = cacheKey || queryFn.toString()
36
+
37
+ // Subscribe to cache updates for this key
38
+ const cacheEntry = useSyncExternalStore(
39
+ useCallback(
40
+ (callback) => authDataCache.subscribe(stableCacheKey, callback),
41
+ [stableCacheKey]
42
+ ),
43
+ useCallback(
44
+ () => authDataCache.get<T>(stableCacheKey),
45
+ [stableCacheKey]
46
+ ),
47
+ useCallback(
48
+ () => authDataCache.get<T>(stableCacheKey),
49
+ [stableCacheKey]
50
+ )
51
+ )
52
+
53
+ const initialized = useRef(false)
54
+ const previousUserId = useRef<string | undefined>(undefined)
55
+ const [error, setError] = useState<FetchError | null>(null)
56
+
57
+ const refetch = useCallback(async () => {
58
+ // Check if there's already an in-flight request for this key
59
+ const existingRequest = authDataCache.getInFlightRequest<{
60
+ data: T | null
61
+ error?: FetchError | null
62
+ }>(stableCacheKey)
63
+ if (existingRequest) {
64
+ // Wait for the existing request to complete
65
+ try {
66
+ const result = await existingRequest
67
+ if (result.error) {
68
+ setError(result.error)
69
+ } else {
70
+ setError(null)
71
+ }
72
+ } catch (err) {
73
+ setError(err as FetchError)
74
+ }
75
+ return
76
+ }
77
+
78
+ // Mark as refetching if we have cached data
79
+ if (cacheEntry?.data !== undefined) {
80
+ authDataCache.setRefetching(stableCacheKey, true)
81
+ }
82
+
83
+ // Create the fetch promise
84
+ const fetchPromise = queryFnRef.current()
85
+
86
+ // Store the promise as in-flight
87
+ authDataCache.setInFlightRequest(stableCacheKey, fetchPromise)
88
+
89
+ try {
90
+ const { data, error } = await fetchPromise
91
+
92
+ if (error) {
93
+ setError(error)
94
+ toast({
95
+ variant: "error",
96
+ message: getLocalizedError({ error, localization })
97
+ })
98
+ } else {
99
+ setError(null)
100
+ }
101
+
102
+ // Update cache with new data
103
+ authDataCache.set(stableCacheKey, data)
104
+ } catch (err) {
105
+ const error = err as FetchError
106
+ setError(error)
107
+ toast({
108
+ variant: "error",
109
+ message: getLocalizedError({ error, localization })
110
+ })
111
+ } finally {
112
+ authDataCache.setRefetching(stableCacheKey, false)
113
+ authDataCache.removeInFlightRequest(stableCacheKey)
114
+ }
115
+ }, [stableCacheKey, toast, localization, cacheEntry])
116
+
117
+ useEffect(() => {
118
+ const currentUserId = sessionData?.user?.id
119
+
120
+ if (!sessionData) {
121
+ // Clear cache when session is lost
122
+ authDataCache.setRefetching(stableCacheKey, false)
123
+ authDataCache.clear(stableCacheKey)
124
+ initialized.current = false
125
+ previousUserId.current = undefined
126
+ return
127
+ }
128
+
129
+ // Check if user ID has changed
130
+ const userIdChanged =
131
+ previousUserId.current !== undefined &&
132
+ previousUserId.current !== currentUserId
133
+
134
+ // If user changed, clear cache to ensure isPending becomes true
135
+ if (userIdChanged) {
136
+ authDataCache.clear(stableCacheKey)
137
+ }
138
+
139
+ // If we have cached data, we're not pending anymore
140
+ const hasCachedData = cacheEntry?.data !== undefined
141
+
142
+ // Check if data is stale
143
+ const isStale =
144
+ !cacheEntry || Date.now() - cacheEntry.timestamp > staleTime
145
+
146
+ if (
147
+ !initialized.current ||
148
+ !hasCachedData ||
149
+ userIdChanged ||
150
+ (hasCachedData && isStale)
151
+ ) {
152
+ // Only fetch if we don't have data or if the data is stale
153
+ if (!hasCachedData || isStale) {
154
+ initialized.current = true
155
+ refetch()
156
+ }
157
+ }
158
+
159
+ // Update the previous user ID
160
+ previousUserId.current = currentUserId
161
+ }, [
162
+ sessionData,
163
+ sessionData?.user?.id,
164
+ stableCacheKey,
165
+ refetch,
166
+ cacheEntry,
167
+ staleTime
168
+ ])
169
+
170
+ // Determine if we're in a pending state
171
+ // We're only pending if:
172
+ // 1. Session is still loading, OR
173
+ // 2. We have no cached data and no error
174
+ const isPending =
175
+ sessionPending || (cacheEntry?.data === undefined && !error)
176
+
177
+ return {
178
+ data: cacheEntry?.data ?? null,
179
+ isPending,
180
+ isRefetching: cacheEntry?.isRefetching ?? false,
181
+ error,
182
+ refetch
183
+ }
184
+ }
@@ -0,0 +1,62 @@
1
+ import { useContext, useEffect } from "react"
2
+ import { AuthUIContext } from "../lib/auth-ui-provider"
3
+ import type { AuthViewPath } from "../server"
4
+ import type { AnyAuthClient } from "../types/any-auth-client"
5
+
6
+ interface AuthenticateOptions<TAuthClient extends AnyAuthClient> {
7
+ authClient?: TAuthClient
8
+ authView?: AuthViewPath
9
+ enabled?: boolean
10
+ }
11
+
12
+ export function useAuthenticate<TAuthClient extends AnyAuthClient>(
13
+ options?: AuthenticateOptions<TAuthClient>
14
+ ) {
15
+ type Session = TAuthClient["$Infer"]["Session"]["session"]
16
+ type User = TAuthClient["$Infer"]["Session"]["user"]
17
+
18
+ const { authView = "SIGN_IN", enabled = true } = options ?? {}
19
+
20
+ const {
21
+ hooks: { useSession },
22
+ basePath,
23
+ viewPaths,
24
+ replace
25
+ } = useContext(AuthUIContext)
26
+
27
+ const { data, isPending, error, refetch } = useSession()
28
+ const sessionData = data as
29
+ | {
30
+ session: Session
31
+ user: User
32
+ }
33
+ | null
34
+ | undefined
35
+
36
+ useEffect(() => {
37
+ if (!enabled || isPending || sessionData) return
38
+
39
+ const currentUrl = new URL(window.location.href)
40
+ const redirectTo =
41
+ currentUrl.searchParams.get("redirectTo") ||
42
+ window.location.href.replace(window.location.origin, "")
43
+
44
+ replace(`${basePath}/${viewPaths[authView]}?redirectTo=${redirectTo}`)
45
+ }, [
46
+ isPending,
47
+ sessionData,
48
+ basePath,
49
+ viewPaths,
50
+ replace,
51
+ authView,
52
+ enabled
53
+ ])
54
+
55
+ return {
56
+ data: sessionData,
57
+ user: sessionData?.user,
58
+ isPending,
59
+ error,
60
+ refetch
61
+ }
62
+ }
@@ -0,0 +1,138 @@
1
+ import type HCaptcha from "@hcaptcha/react-hcaptcha"
2
+ import type { TurnstileInstance } from "@marsidev/react-turnstile"
3
+ import { useGoogleReCaptcha } from "@wojtekmaj/react-recaptcha-v3"
4
+ import { type RefObject, useContext, useRef } from "react"
5
+ import type ReCAPTCHA from "react-google-recaptcha"
6
+
7
+ import { AuthUIContext } from "../lib/auth-ui-provider"
8
+ import type { AuthLocalization } from "../localization/auth-localization"
9
+
10
+ // Default captcha endpoints
11
+ const DEFAULT_CAPTCHA_ENDPOINTS = [
12
+ "/sign-up/email",
13
+ "/sign-in/email",
14
+ "/forget-password"
15
+ ]
16
+
17
+ // Sanitize action name for reCAPTCHA
18
+ // Google reCAPTCHA only allows A-Za-z/_ in action names
19
+ const sanitizeActionName = (action: string): string => {
20
+ // First remove leading slash if present
21
+ let result = action.startsWith("/") ? action.substring(1) : action
22
+
23
+ // Convert both kebab-case and path separators to camelCase
24
+ // Example: "/sign-in/email" becomes "signInEmail"
25
+ result = result
26
+ .replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
27
+ .replace(/\/([a-z])/g, (_, letter) => letter.toUpperCase())
28
+ .replace(/\//g, "")
29
+ .replace(/[^A-Za-z0-9_]/g, "")
30
+
31
+ return result
32
+ }
33
+
34
+ export function useCaptcha({
35
+ localization
36
+ }: {
37
+ localization: Partial<AuthLocalization>
38
+ }) {
39
+ const { captcha, localization: contextLocalization } =
40
+ useContext(AuthUIContext)
41
+
42
+ localization = { ...contextLocalization, ...localization }
43
+
44
+ // biome-ignore lint/suspicious/noExplicitAny: ignore
45
+ const captchaRef = useRef<any>(null)
46
+ const { executeRecaptcha } = useGoogleReCaptcha()
47
+
48
+ const executeCaptcha = async (action: string) => {
49
+ if (!captcha) throw new Error(localization.MISSING_RESPONSE)
50
+
51
+ // Sanitize the action name for reCAPTCHA
52
+ let response: string | undefined | null
53
+
54
+ switch (captcha.provider) {
55
+ case "google-recaptcha-v3": {
56
+ const sanitizedAction = sanitizeActionName(action)
57
+ response = await executeRecaptcha?.(sanitizedAction)
58
+ break
59
+ }
60
+ case "google-recaptcha-v2-checkbox": {
61
+ const recaptchaRef = captchaRef as RefObject<ReCAPTCHA>
62
+ response = recaptchaRef.current.getValue()
63
+ break
64
+ }
65
+ case "google-recaptcha-v2-invisible": {
66
+ const recaptchaRef = captchaRef as RefObject<ReCAPTCHA>
67
+ response = await recaptchaRef.current.executeAsync()
68
+ break
69
+ }
70
+ case "cloudflare-turnstile": {
71
+ const turnstileRef = captchaRef as RefObject<TurnstileInstance>
72
+ response = turnstileRef.current.getResponse()
73
+ break
74
+ }
75
+ case "hcaptcha": {
76
+ const hcaptchaRef = captchaRef as RefObject<HCaptcha>
77
+ response = hcaptchaRef.current.getResponse()
78
+ break
79
+ }
80
+ }
81
+
82
+ if (!response) {
83
+ throw new Error(localization.MISSING_RESPONSE)
84
+ }
85
+
86
+ return response
87
+ }
88
+
89
+ const getCaptchaHeaders = async (action: string) => {
90
+ if (!captcha) return undefined
91
+
92
+ // Use custom endpoints if provided, otherwise use defaults
93
+ const endpoints = captcha.endpoints || DEFAULT_CAPTCHA_ENDPOINTS
94
+
95
+ // Only execute captcha if the action is in the endpoints list
96
+ if (endpoints.includes(action)) {
97
+ return { "x-captcha-response": await executeCaptcha(action) }
98
+ }
99
+
100
+ return undefined
101
+ }
102
+
103
+ const resetCaptcha = () => {
104
+ if (!captcha) return
105
+
106
+ switch (captcha.provider) {
107
+ case "google-recaptcha-v3": {
108
+ // No widget to reset; token is generated per execute call
109
+ break
110
+ }
111
+ case "google-recaptcha-v2-checkbox":
112
+ case "google-recaptcha-v2-invisible": {
113
+ const recaptchaRef = captchaRef as RefObject<ReCAPTCHA>
114
+ recaptchaRef.current?.reset?.()
115
+ break
116
+ }
117
+ case "cloudflare-turnstile": {
118
+ const turnstileRef = captchaRef as RefObject<TurnstileInstance>
119
+ // Some versions expose reset on the instance
120
+ // biome-ignore lint/suspicious/noExplicitAny: defensive
121
+ ;(turnstileRef.current as any)?.reset?.()
122
+ break
123
+ }
124
+ case "hcaptcha": {
125
+ const hcaptchaRef = captchaRef as RefObject<HCaptcha>
126
+ // HCaptcha uses resetCaptcha()
127
+ hcaptchaRef.current?.resetCaptcha?.()
128
+ break
129
+ }
130
+ }
131
+ }
132
+
133
+ return {
134
+ captchaRef,
135
+ getCaptchaHeaders,
136
+ resetCaptcha
137
+ }
138
+ }
@@ -0,0 +1,59 @@
1
+ import type { Organization } from "better-auth/plugins/organization"
2
+ import { useContext, useMemo } from "react"
3
+ import { AuthUIContext } from "../lib/auth-ui-provider"
4
+
5
+ export function useCurrentOrganization({
6
+ slug: slugProp
7
+ }: {
8
+ slug?: string
9
+ } = {}) {
10
+ const {
11
+ organization: organizationOptions,
12
+ hooks: { useActiveOrganization, useListOrganizations }
13
+ } = useContext(AuthUIContext)
14
+
15
+ const { pathMode, slug: contextSlug } = organizationOptions || {}
16
+
17
+ let data: Organization | null | undefined
18
+ let isPending: boolean | undefined
19
+ let isRefetching: boolean | undefined
20
+
21
+ let refetch: (() => void) | undefined
22
+
23
+ const {
24
+ data: organizations,
25
+ isPending: organizationsPending,
26
+ isRefetching: organizationsRefetching
27
+ } = useListOrganizations()
28
+
29
+ if (pathMode === "slug") {
30
+ const slug = slugProp || contextSlug
31
+
32
+ data = organizations?.find((organization) => organization.slug === slug)
33
+ isPending = organizationsPending
34
+ isRefetching = organizationsRefetching
35
+ } else {
36
+ const {
37
+ data: activeOrganization,
38
+ isPending: organizationPending,
39
+ isRefetching: organizationRefetching,
40
+ refetch: refetchOrganization
41
+ } = useActiveOrganization()
42
+
43
+ refetch = refetchOrganization
44
+
45
+ data = activeOrganization
46
+ isPending = organizationPending
47
+ isRefetching = organizationRefetching
48
+ }
49
+
50
+ return useMemo(
51
+ () => ({
52
+ data,
53
+ isPending,
54
+ isRefetching,
55
+ refetch
56
+ }),
57
+ [data, isPending, isRefetching, refetch]
58
+ )
59
+ }
@@ -0,0 +1,13 @@
1
+ import { useSyncExternalStore } from "react"
2
+
3
+ function subscribe() {
4
+ return () => {}
5
+ }
6
+
7
+ export function useIsHydrated() {
8
+ return useSyncExternalStore(
9
+ subscribe,
10
+ () => true,
11
+ () => false
12
+ )
13
+ }
@@ -0,0 +1,32 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ export function useLang() {
4
+ const [lang, setLang] = useState<string>()
5
+
6
+ useEffect(() => {
7
+ const checkLang = () => {
8
+ const currentLang = document.documentElement.getAttribute("lang")
9
+ setLang(currentLang ?? undefined)
10
+ }
11
+
12
+ // Initial check
13
+ checkLang()
14
+
15
+ // Listen for changes to lang attribute on html tag
16
+ const observer = new MutationObserver((mutations) => {
17
+ for (const mutation of mutations) {
18
+ if (mutation.attributeName === "lang") {
19
+ checkLang()
20
+ }
21
+ }
22
+ })
23
+
24
+ observer.observe(document.documentElement, { attributes: true })
25
+
26
+ return () => {
27
+ observer.disconnect()
28
+ }
29
+ }, [])
30
+
31
+ return { lang }
32
+ }
@@ -0,0 +1,51 @@
1
+ import {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ useTransition
7
+ } from "react"
8
+ import { AuthUIContext } from "../lib/auth-ui-provider"
9
+ import { getSearchParam } from "../lib/utils"
10
+
11
+ export function useOnSuccessTransition({
12
+ redirectTo: redirectToProp
13
+ }: {
14
+ redirectTo?: string
15
+ }) {
16
+ const { redirectTo: contextRedirectTo } = useContext(AuthUIContext)
17
+
18
+ const getRedirectTo = useCallback(
19
+ () =>
20
+ redirectToProp || getSearchParam("redirectTo") || contextRedirectTo,
21
+ [redirectToProp, contextRedirectTo]
22
+ )
23
+
24
+ const [isPending, startTransition] = useTransition()
25
+ const [success, setSuccess] = useState(false)
26
+
27
+ const {
28
+ navigate,
29
+ hooks: { useSession },
30
+ onSessionChange
31
+ } = useContext(AuthUIContext)
32
+
33
+ const { refetch: refetchSession } = useSession()
34
+
35
+ useEffect(() => {
36
+ if (!success || isPending) return
37
+
38
+ startTransition(() => {
39
+ navigate(getRedirectTo())
40
+ })
41
+ }, [success, isPending, navigate, getRedirectTo])
42
+
43
+ const onSuccess = useCallback(async () => {
44
+ await refetchSession?.()
45
+ setSuccess(true)
46
+
47
+ if (onSessionChange) startTransition(onSessionChange)
48
+ }, [refetchSession, onSessionChange])
49
+
50
+ return { onSuccess, isPending }
51
+ }
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ export function useTheme() {
4
+ const [theme, setTheme] = useState<"light" | "dark">("light")
5
+
6
+ useEffect(() => {
7
+ const checkTheme = () => {
8
+ const isDark =
9
+ document.documentElement.classList.contains("dark") ||
10
+ document.documentElement
11
+ .getAttribute("style")
12
+ ?.includes("color-scheme: dark")
13
+ setTheme(isDark ? "dark" : "light")
14
+ }
15
+
16
+ // Initial check
17
+ checkTheme()
18
+
19
+ // Listen for changes to html tag
20
+ const observer = new MutationObserver((mutations) => {
21
+ for (const mutation of mutations) {
22
+ if (
23
+ mutation.attributeName === "style" ||
24
+ mutation.attributeName === "class"
25
+ ) {
26
+ checkTheme()
27
+ }
28
+ }
29
+ })
30
+
31
+ observer.observe(document.documentElement, { attributes: true })
32
+
33
+ return () => {
34
+ observer.disconnect()
35
+ }
36
+ }, [])
37
+
38
+ return { theme }
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ export * from "./components/account/account-view"
2
+ export * from "./components/auth/auth-callback"
3
+ export * from "./components/auth/auth-form"
4
+ export * from "./components/auth/auth-view"
5
+ export * from "./components/auth/forms/forgot-password-form"
6
+ export * from "./components/auth/forms/magic-link-form"
7
+ export * from "./components/auth/forms/recover-account-form"
8
+ export * from "./components/auth/forms/reset-password-form"
9
+ export * from "./components/auth/forms/sign-in-form"
10
+ export * from "./components/auth/forms/sign-up-form"
11
+ export * from "./components/auth/forms/two-factor-form"
12
+ export * from "./components/auth/sign-out"
13
+ export * from "./components/auth-loading"
14
+ export * from "./components/organization/accept-invitation-card"
15
+ export * from "./components/organization/create-organization-dialog"
16
+ export * from "./components/organization/delete-organization-card"
17
+ export * from "./components/organization/organization-cell-view"
18
+ export * from "./components/organization/organization-invitations-card"
19
+ export * from "./components/organization/organization-logo"
20
+ export * from "./components/organization/organization-logo-card"
21
+ export * from "./components/organization/organization-members-card"
22
+ export * from "./components/organization/organization-name-card"
23
+ export * from "./components/organization/organization-settings-cards"
24
+ export * from "./components/organization/organization-slug-card"
25
+ export * from "./components/organization/organization-switcher"
26
+ export * from "./components/organization/organization-view"
27
+ export * from "./components/organization/organizations-card"
28
+ export * from "./components/organization/user-invitations-card"
29
+ export * from "./components/password-input"
30
+ export * from "./components/provider-icons"
31
+ export * from "./components/redirect-to-sign-in"
32
+ export * from "./components/redirect-to-sign-up"
33
+ export * from "./components/settings/account/accounts-card"
34
+ export * from "./components/settings/account/delete-account-card"
35
+ export * from "./components/settings/account/update-avatar-card"
36
+ export * from "./components/settings/account/update-field-card"
37
+ export * from "./components/settings/account/update-name-card"
38
+ export * from "./components/settings/account/update-username-card"
39
+ export * from "./components/settings/account-settings-cards"
40
+ export * from "./components/settings/api-key/api-keys-card"
41
+ export * from "./components/settings/passkey/passkeys-card"
42
+ export * from "./components/settings/providers/providers-card"
43
+ export * from "./components/settings/security/change-email-card"
44
+ export * from "./components/settings/security/change-password-card"
45
+ export * from "./components/settings/security/sessions-card"
46
+ export * from "./components/settings/security-settings-cards"
47
+ export * from "./components/settings/shared/settings-card"
48
+ export * from "./components/settings/skeletons/input-field-skeleton"
49
+ export * from "./components/settings/skeletons/settings-cell-skeleton"
50
+ export * from "./components/settings/two-factor/two-factor-card"
51
+ export * from "./components/signed-in"
52
+ export * from "./components/signed-out"
53
+ export * from "./components/user-avatar"
54
+ export * from "./components/user-button"
55
+ export * from "./components/user-view"
56
+ export * from "./hooks/use-auth-data"
57
+ export * from "./hooks/use-authenticate"
58
+ export * from "./hooks/use-current-organization"
59
+ export * from "./lib/auth-ui-provider"
60
+ export * from "./lib/social-providers"
61
+ export { getViewByPath } from "./lib/utils"
62
+ export * from "./lib/view-paths"
63
+ export * from "./localization/auth-localization"
64
+ export * from "./types/auth-hooks"
65
+ export * from "./types/auth-mutators"
@@ -0,0 +1 @@
1
+ export * from "./lib/instantdb/use-instant-options"