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,141 @@
1
+ import type { SocialProvider } from "better-auth/social-providers"
2
+ import { useCallback, useContext } from "react"
3
+
4
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
5
+ import type { Provider } from "../../lib/social-providers"
6
+ import { cn, getLocalizedError, getSearchParam } from "../../lib/utils"
7
+ import type { AuthLocalization } from "../../localization/auth-localization"
8
+ import { Button } from "../ui/button"
9
+ import type { AuthViewClassNames } from "./auth-view"
10
+
11
+ interface ProviderButtonProps {
12
+ className?: string
13
+ classNames?: AuthViewClassNames
14
+ callbackURL?: string
15
+ isSubmitting: boolean
16
+ localization: Partial<AuthLocalization>
17
+ other?: boolean
18
+ provider: Provider
19
+ redirectTo?: string
20
+ socialLayout: "auto" | "horizontal" | "grid" | "vertical"
21
+ setIsSubmitting: (isSubmitting: boolean) => void
22
+ }
23
+
24
+ export function ProviderButton({
25
+ className,
26
+ classNames,
27
+ callbackURL: callbackURLProp,
28
+ isSubmitting,
29
+ localization,
30
+ other,
31
+ provider,
32
+ redirectTo: redirectToProp,
33
+ socialLayout,
34
+ setIsSubmitting
35
+ }: ProviderButtonProps) {
36
+ const {
37
+ authClient,
38
+ basePath,
39
+ baseURL,
40
+ persistClient,
41
+ redirectTo: contextRedirectTo,
42
+ viewPaths,
43
+ social,
44
+ genericOAuth,
45
+ toast
46
+ } = useContext(AuthUIContext)
47
+
48
+ const getRedirectTo = useCallback(
49
+ () =>
50
+ redirectToProp || getSearchParam("redirectTo") || contextRedirectTo,
51
+ [redirectToProp, contextRedirectTo]
52
+ )
53
+
54
+ const getCallbackURL = useCallback(
55
+ () =>
56
+ `${baseURL}${
57
+ callbackURLProp ||
58
+ (persistClient
59
+ ? `${basePath}/${viewPaths.CALLBACK}?redirectTo=${getRedirectTo()}`
60
+ : getRedirectTo())
61
+ }`,
62
+ [
63
+ callbackURLProp,
64
+ persistClient,
65
+ basePath,
66
+ viewPaths,
67
+ baseURL,
68
+ getRedirectTo
69
+ ]
70
+ )
71
+
72
+ const doSignInSocial = async () => {
73
+ setIsSubmitting(true)
74
+
75
+ try {
76
+ if (other) {
77
+ const oauth2Params = {
78
+ providerId: provider.provider,
79
+ callbackURL: getCallbackURL(),
80
+ fetchOptions: { throw: true }
81
+ }
82
+
83
+ if (genericOAuth?.signIn) {
84
+ await genericOAuth.signIn(oauth2Params)
85
+
86
+ setTimeout(() => {
87
+ setIsSubmitting(false)
88
+ }, 10000)
89
+ } else {
90
+ await authClient.signIn.oauth2(oauth2Params)
91
+ }
92
+ } else {
93
+ const socialParams = {
94
+ provider: provider.provider as SocialProvider,
95
+ callbackURL: getCallbackURL(),
96
+ fetchOptions: { throw: true }
97
+ }
98
+
99
+ if (social?.signIn) {
100
+ await social.signIn(socialParams)
101
+
102
+ setTimeout(() => {
103
+ setIsSubmitting(false)
104
+ }, 10000)
105
+ } else {
106
+ await authClient.signIn.social(socialParams)
107
+ }
108
+ }
109
+ } catch (error) {
110
+ toast({
111
+ variant: "error",
112
+ message: getLocalizedError({ error, localization })
113
+ })
114
+
115
+ setIsSubmitting(false)
116
+ }
117
+ }
118
+
119
+ return (
120
+ <Button
121
+ className={cn(
122
+ socialLayout === "vertical" ? "w-full" : "grow",
123
+ className,
124
+ classNames?.form?.button,
125
+ classNames?.form?.outlineButton,
126
+ classNames?.form?.providerButton
127
+ )}
128
+ disabled={isSubmitting}
129
+ variant="outline"
130
+ onClick={doSignInSocial}
131
+ >
132
+ {provider.icon && (
133
+ <provider.icon className={classNames?.form?.icon} />
134
+ )}
135
+
136
+ {socialLayout === "grid" && provider.name}
137
+ {socialLayout === "vertical" &&
138
+ `${localization.SIGN_IN_WITH} ${provider.name}`}
139
+ </Button>
140
+ )
141
+ }
@@ -0,0 +1,25 @@
1
+ "use client"
2
+
3
+ import { Loader2 } from "lucide-react"
4
+ import { useContext, useEffect, useRef } from "react"
5
+
6
+ import { useOnSuccessTransition } from "../../hooks/use-success-transition"
7
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
8
+
9
+ export function SignOut() {
10
+ const signingOut = useRef(false)
11
+
12
+ const { authClient, basePath, viewPaths } = useContext(AuthUIContext)
13
+ const { onSuccess } = useOnSuccessTransition({
14
+ redirectTo: `${basePath}/${viewPaths.SIGN_IN}`
15
+ })
16
+
17
+ useEffect(() => {
18
+ if (signingOut.current) return
19
+ signingOut.current = true
20
+
21
+ authClient.signOut().finally(onSuccess)
22
+ }, [authClient, onSuccess])
23
+
24
+ return <Loader2 className="animate-spin" />
25
+ }
@@ -0,0 +1,21 @@
1
+ "use client"
2
+
3
+ import { type ReactNode, useContext } from "react"
4
+ import { AuthUIContext } from "../lib/auth-ui-provider"
5
+
6
+ /**
7
+ * Conditionally renders content during authentication loading state
8
+ *
9
+ * Renders its children only when the authentication state is being determined
10
+ * (during the loading/pending phase). Once the authentication state is resolved,
11
+ * nothing is rendered. Useful for displaying loading indicators or temporary
12
+ * content while waiting for the authentication check to complete.
13
+ */
14
+ export function AuthLoading({ children }: { children: ReactNode }) {
15
+ const {
16
+ hooks: { useSession }
17
+ } = useContext(AuthUIContext)
18
+ const { isPending } = useSession()
19
+
20
+ return isPending ? children : null
21
+ }
@@ -0,0 +1,79 @@
1
+ import HCaptcha from "@hcaptcha/react-hcaptcha"
2
+ import { Turnstile } from "@marsidev/react-turnstile"
3
+ import { type RefObject, useContext } from "react"
4
+
5
+ import { useTheme } from "../../hooks/use-theme"
6
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
7
+ import type { AuthLocalization } from "../../localization/auth-localization"
8
+ import { RecaptchaBadge } from "./recaptcha-badge"
9
+ import { RecaptchaV2 } from "./recaptcha-v2"
10
+
11
+ // Default captcha endpoints
12
+ const DEFAULT_CAPTCHA_ENDPOINTS = [
13
+ "/sign-up/email",
14
+ "/sign-in/email",
15
+ "/forget-password"
16
+ ]
17
+
18
+ interface CaptchaProps {
19
+ // biome-ignore lint/suspicious/noExplicitAny: ignore
20
+ ref: RefObject<any>
21
+ localization: Partial<AuthLocalization>
22
+ action?: string // Optional action to check if it's in the endpoints list
23
+ }
24
+
25
+ export function Captcha({ ref, localization, action }: CaptchaProps) {
26
+ const { captcha } = useContext(AuthUIContext)
27
+ if (!captcha) return null
28
+
29
+ // If action is provided, check if it's in the list of captcha-enabled endpoints
30
+ if (action) {
31
+ const endpoints = captcha.endpoints || DEFAULT_CAPTCHA_ENDPOINTS
32
+ if (!endpoints.includes(action)) {
33
+ return null
34
+ }
35
+ }
36
+
37
+ const { theme } = useTheme()
38
+
39
+ const showRecaptchaV2 =
40
+ captcha.provider === "google-recaptcha-v2-checkbox" ||
41
+ captcha.provider === "google-recaptcha-v2-invisible"
42
+
43
+ const showRecaptchaBadge =
44
+ captcha.provider === "google-recaptcha-v3" ||
45
+ captcha.provider === "google-recaptcha-v2-invisible"
46
+
47
+ const showTurnstile = captcha.provider === "cloudflare-turnstile"
48
+
49
+ const showHCaptcha = captcha.provider === "hcaptcha"
50
+
51
+ return (
52
+ <>
53
+ {showRecaptchaV2 && <RecaptchaV2 ref={ref} />}
54
+ {showRecaptchaBadge && (
55
+ <RecaptchaBadge localization={localization} />
56
+ )}
57
+ {showTurnstile && (
58
+ <Turnstile
59
+ className="mx-auto"
60
+ ref={ref}
61
+ siteKey={captcha.siteKey}
62
+ options={{
63
+ theme: theme,
64
+ size: "flexible"
65
+ }}
66
+ />
67
+ )}
68
+ {showHCaptcha && (
69
+ <div className="mx-auto">
70
+ <HCaptcha
71
+ ref={ref}
72
+ sitekey={captcha.siteKey}
73
+ theme={theme}
74
+ />
75
+ </div>
76
+ )}
77
+ </>
78
+ )
79
+ }
@@ -0,0 +1,61 @@
1
+ import { useContext } from "react"
2
+ import { useIsHydrated } from "../../hooks/use-hydrated"
3
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
4
+ import { cn } from "../../lib/utils"
5
+ import type { AuthLocalization } from "../../localization/auth-localization"
6
+
7
+ export interface RecaptchaBadgeProps {
8
+ className?: string
9
+ localization?: Partial<AuthLocalization>
10
+ }
11
+
12
+ export function RecaptchaBadge({
13
+ className,
14
+ localization: propLocalization
15
+ }: RecaptchaBadgeProps) {
16
+ const isHydrated = useIsHydrated()
17
+ const { captcha, localization: contextLocalization } =
18
+ useContext(AuthUIContext)
19
+ const localization = { ...contextLocalization, ...propLocalization }
20
+
21
+ if (!captcha) return null
22
+
23
+ if (!captcha.hideBadge) {
24
+ return isHydrated ? (
25
+ <style>{`
26
+ .grecaptcha-badge { visibility: visible !important; }
27
+ `}</style>
28
+ ) : null
29
+ }
30
+
31
+ return (
32
+ <>
33
+ <style>{`
34
+ .grecaptcha-badge { visibility: hidden; }
35
+ `}</style>
36
+
37
+ <p className={cn("text-muted-foreground text-xs", className)}>
38
+ {localization.PROTECTED_BY_RECAPTCHA}{" "}
39
+ {localization.BY_CONTINUING_YOU_AGREE} Google{" "}
40
+ <a
41
+ className="text-foreground hover:underline"
42
+ href="https://policies.google.com/privacy"
43
+ target="_blank"
44
+ rel="noreferrer"
45
+ >
46
+ {localization.PRIVACY_POLICY}
47
+ </a>{" "}
48
+ &{" "}
49
+ <a
50
+ className="text-foreground hover:underline"
51
+ href="https://policies.google.com/terms"
52
+ target="_blank"
53
+ rel="noreferrer"
54
+ >
55
+ {localization.TERMS_OF_SERVICE}
56
+ </a>
57
+ .
58
+ </p>
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,58 @@
1
+ import { type RefObject, useContext, useEffect } from "react"
2
+ import ReCAPTCHA from "react-google-recaptcha"
3
+ import { useLang } from "../../hooks/use-lang"
4
+ import { useTheme } from "../../hooks/use-theme"
5
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
6
+ import { cn } from "../../lib/utils"
7
+
8
+ export function RecaptchaV2({ ref }: { ref: RefObject<ReCAPTCHA | null> }) {
9
+ const { captcha } = useContext(AuthUIContext)
10
+ const { theme } = useTheme()
11
+ const { lang } = useLang()
12
+
13
+ useEffect(() => {
14
+ // biome-ignore lint/suspicious/noExplicitAny: ignore
15
+ ;(window as any).recaptchaOptions = {
16
+ useRecaptchaNet: captcha?.recaptchaNet,
17
+ enterprise: captcha?.enterprise
18
+ }
19
+ }, [captcha])
20
+
21
+ if (!captcha) return null
22
+
23
+ return (
24
+ <>
25
+ <style>{`
26
+ .grecaptcha-badge {
27
+ border-radius: var(--radius) !important;
28
+ --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, #0000000d);
29
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow) !important;
30
+ border-style: var(--tw-border-style) !important;
31
+ border-width: 1px;
32
+ }
33
+
34
+ .dark .grecaptcha-badge {
35
+ border-color: var(--input) !important;
36
+ }
37
+ `}</style>
38
+
39
+ <ReCAPTCHA
40
+ ref={ref}
41
+ key={`${theme}-${lang}-${captcha.provider}`}
42
+ sitekey={captcha.siteKey}
43
+ theme={theme}
44
+ hl={lang}
45
+ size={
46
+ captcha.provider === "google-recaptcha-v2-invisible"
47
+ ? "invisible"
48
+ : "normal"
49
+ }
50
+ className={cn(
51
+ captcha.provider === "google-recaptcha-v2-invisible"
52
+ ? "absolute"
53
+ : "mx-auto h-[76px] w-[302px] overflow-hidden rounded bg-muted"
54
+ )}
55
+ />
56
+ </>
57
+ )
58
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ GoogleReCaptchaProvider,
3
+ useGoogleReCaptcha
4
+ } from "@wojtekmaj/react-recaptcha-v3"
5
+ import { type ReactNode, useContext, useEffect } from "react"
6
+
7
+ import { useIsHydrated } from "../../hooks/use-hydrated"
8
+ import { useLang } from "../../hooks/use-lang"
9
+ import { useTheme } from "../../hooks/use-theme"
10
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
11
+
12
+ export function RecaptchaV3({ children }: { children: ReactNode }) {
13
+ const isHydrated = useIsHydrated()
14
+ const { captcha } = useContext(AuthUIContext)
15
+
16
+ if (captcha?.provider !== "google-recaptcha-v3") return children
17
+
18
+ return (
19
+ <GoogleReCaptchaProvider
20
+ reCaptchaKey={captcha.siteKey}
21
+ useEnterprise={captcha.enterprise}
22
+ useRecaptchaNet={captcha.recaptchaNet}
23
+ >
24
+ {isHydrated && (
25
+ <style>{`
26
+ .grecaptcha-badge {
27
+ visibility: hidden;
28
+ border-radius: var(--radius) !important;
29
+ --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, #0000000d);
30
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow) !important;
31
+ border-style: var(--tw-border-style) !important;
32
+ border-width: 1px;
33
+ }
34
+
35
+ .dark .grecaptcha-badge {
36
+ border-color: var(--input) !important;
37
+ }
38
+ `}</style>
39
+ )}
40
+
41
+ <RecaptchaV3Style />
42
+
43
+ {children}
44
+ </GoogleReCaptchaProvider>
45
+ )
46
+ }
47
+
48
+ function RecaptchaV3Style() {
49
+ const { executeRecaptcha } = useGoogleReCaptcha()
50
+ const { theme } = useTheme()
51
+ const { lang } = useLang()
52
+
53
+ useEffect(() => {
54
+ if (!executeRecaptcha) return
55
+
56
+ const updateRecaptcha = async () => {
57
+ // find iframe with title "reCAPTCHA"
58
+ const iframe = document.querySelector(
59
+ "iframe[title='reCAPTCHA']"
60
+ ) as HTMLIFrameElement
61
+ if (iframe) {
62
+ const iframeSrcUrl = new URL(iframe.src)
63
+ iframeSrcUrl.searchParams.set("theme", theme)
64
+ if (lang) iframeSrcUrl.searchParams.set("hl", lang)
65
+ iframe.src = iframeSrcUrl.toString()
66
+ }
67
+ }
68
+
69
+ updateRecaptcha()
70
+ }, [executeRecaptcha, theme, lang])
71
+
72
+ return null
73
+ }
@@ -0,0 +1,216 @@
1
+ import {
2
+ Body,
3
+ Button,
4
+ Container,
5
+ Head,
6
+ Heading,
7
+ Hr,
8
+ Html,
9
+ Img,
10
+ Link,
11
+ Preview,
12
+ Section,
13
+ Tailwind,
14
+ Text
15
+ } from "@react-email/components"
16
+ import type { ReactNode } from "react"
17
+
18
+ import { cn } from "../../lib/utils"
19
+
20
+ export interface EmailTemplateClassNames {
21
+ body?: string
22
+ button?: string
23
+ container?: string
24
+ image?: string
25
+ content?: string
26
+ footer?: string
27
+ heading?: string
28
+ hr?: string
29
+ link?: string
30
+ }
31
+
32
+ export interface EmailTemplateProps {
33
+ classNames?: EmailTemplateClassNames
34
+ action?: string
35
+ /** @default process.env.BASE_URL || process.env.NEXT_PUBLIC_BASE_URL */
36
+ baseUrl?: string
37
+ content: ReactNode
38
+ heading: ReactNode
39
+ /** @default `${baseUrl}/apple-touch-icon.png` */
40
+ imageUrl?: string
41
+ preview?: string
42
+ /** @default process.env.SITE_NAME || process.env.NEXT_PUBLIC_SITE_NAME */
43
+ siteName?: string
44
+ url?: string
45
+ /** @default "vercel" */
46
+ variant?: "vercel"
47
+ }
48
+
49
+ export const EmailTemplate = ({
50
+ classNames,
51
+ action,
52
+ baseUrl,
53
+ content,
54
+ heading,
55
+ imageUrl,
56
+ preview,
57
+ siteName,
58
+ variant = "vercel",
59
+ url
60
+ }: EmailTemplateProps) => {
61
+ baseUrl =
62
+ baseUrl || process.env.BASE_URL || process.env.NEXT_PUBLIC_BASE_URL
63
+ imageUrl = imageUrl || `${baseUrl}/apple-touch-icon.png`
64
+ siteName =
65
+ siteName || process.env.SITE_NAME || process.env.NEXT_PUBLIC_SITE_NAME
66
+ preview = preview || (typeof heading === "string" ? heading : undefined)
67
+
68
+ return (
69
+ <Html>
70
+ <Head>
71
+ <meta name="x-apple-disable-message-reformatting" />
72
+ <meta content="light dark" name="color-scheme" />
73
+ <meta content="light dark" name="supported-color-schemes" />
74
+
75
+ <style type="text/css">
76
+ {`
77
+ :root {
78
+ color-scheme: light dark;
79
+ supported-color-schemes: light dark;
80
+ }
81
+ `}
82
+ </style>
83
+
84
+ <style type="text/css">
85
+ {`
86
+ html, body {
87
+ background-color: #ffffff;
88
+ color: #000000;
89
+ }
90
+
91
+ a {
92
+ color: #000000;
93
+ }
94
+
95
+ .border-color {
96
+ border-color: #eaeaea;
97
+ }
98
+
99
+ .action-button {
100
+ background-color: #000000 !important;
101
+ color: #ffffff !important;
102
+ }
103
+
104
+ @media (prefers-color-scheme: dark) {
105
+ html, body {
106
+ background-color: #000000 !important;
107
+ color: #ffffff !important;
108
+ }
109
+
110
+ a {
111
+ color: #ffffff;
112
+ }
113
+
114
+ .border-color {
115
+ border-color: #333333 !important;
116
+ }
117
+
118
+ .action-button {
119
+ background-color: rgb(38, 38, 38) !important;
120
+ color: #ffffff !important;
121
+ }
122
+ }
123
+ `}
124
+ </style>
125
+ </Head>
126
+
127
+ {preview && <Preview>{preview}</Preview>}
128
+
129
+ <Tailwind>
130
+ <Body
131
+ className={cn(
132
+ "mx-auto my-auto px-2 font-sans",
133
+ classNames?.body
134
+ )}
135
+ >
136
+ <Container className="mx-auto my-[40px] max-w-[465px] rounded border border-color border-solid p-[20px]">
137
+ <Section className="mt-[32px]">
138
+ <Img
139
+ alt={siteName}
140
+ className={cn(
141
+ "mx-auto my-0 rounded-full",
142
+ classNames?.image
143
+ )}
144
+ height="40"
145
+ src={imageUrl}
146
+ width="40"
147
+ />
148
+ </Section>
149
+
150
+ <Heading
151
+ className={cn(
152
+ "mx-0 my-[30px] p-0 text-center font-bold text-[24px]",
153
+ classNames?.heading
154
+ )}
155
+ >
156
+ {heading}
157
+ </Heading>
158
+
159
+ <Text
160
+ className={cn(
161
+ "text-[14px] leading-[24px]",
162
+ classNames?.content
163
+ )}
164
+ >
165
+ {content}
166
+ </Text>
167
+
168
+ {action && url && (
169
+ <Section className="mt-[32px] mb-[32px] text-center">
170
+ <Button
171
+ className={cn(
172
+ "action-button rounded px-5 py-3 text-center font-semibold text-[12px] no-underline",
173
+ classNames?.button
174
+ )}
175
+ href={url}
176
+ >
177
+ {action}
178
+ </Button>
179
+ </Section>
180
+ )}
181
+
182
+ <Hr
183
+ className={cn(
184
+ "mx-0 my-[26px] w-full border border-color border-solid",
185
+ classNames?.hr
186
+ )}
187
+ />
188
+
189
+ <Text
190
+ className={cn(
191
+ "text-[#666666] text-[12px] leading-[24px]",
192
+ classNames?.footer
193
+ )}
194
+ >
195
+ {siteName && <>{siteName} </>}
196
+
197
+ {baseUrl && (
198
+ <Link
199
+ className={cn(
200
+ "no-underline",
201
+ classNames?.link
202
+ )}
203
+ href={baseUrl}
204
+ >
205
+ {baseUrl
206
+ ?.replace("https://", "")
207
+ .replace("http://", "")}
208
+ </Link>
209
+ )}
210
+ </Text>
211
+ </Container>
212
+ </Body>
213
+ </Tailwind>
214
+ </Html>
215
+ )
216
+ }