@erikey/react 0.4.26 → 0.4.28
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.
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/auth-client.test.ts +105 -0
- package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
- package/src/auth-client.ts +158 -0
- package/src/dashboard-client.ts +60 -0
- package/src/index.ts +88 -0
- package/src/kv-client.ts +316 -0
- package/src/lib/cross-origin-auth.ts +99 -0
- package/src/stubs/captcha.ts +24 -0
- package/src/stubs/hashes.ts +16 -0
- package/src/stubs/index.ts +17 -0
- package/src/stubs/passkey.ts +12 -0
- package/src/stubs/qr-code.ts +10 -0
- package/src/stubs/query.ts +16 -0
- package/src/stubs/realtime.ts +17 -0
- package/src/stubs/use-sync-external-store.ts +12 -0
- package/src/styles.css +141 -0
- package/src/types.ts +14 -0
- package/src/ui/components/auth/auth-callback.tsx +36 -0
- package/src/ui/components/auth/auth-form.tsx +310 -0
- package/src/ui/components/auth/auth-view.tsx +435 -0
- package/src/ui/components/auth/email-otp-button.tsx +53 -0
- package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
- package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
- package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
- package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
- package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
- package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
- package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
- package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
- package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
- package/src/ui/components/auth/magic-link-button.tsx +54 -0
- package/src/ui/components/auth/one-tap.tsx +53 -0
- package/src/ui/components/auth/otp-input-group.tsx +65 -0
- package/src/ui/components/auth/passkey-button.tsx +91 -0
- package/src/ui/components/auth/provider-button.tsx +155 -0
- package/src/ui/components/auth/sign-out.tsx +25 -0
- package/src/ui/components/auth/wallet-button.tsx +192 -0
- package/src/ui/components/auth-loading.tsx +21 -0
- package/src/ui/components/captcha/captcha.tsx +91 -0
- package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
- package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
- package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
- package/src/ui/components/email/email-template.tsx +216 -0
- package/src/ui/components/form-error.tsx +27 -0
- package/src/ui/components/password-input.tsx +56 -0
- package/src/ui/components/provider-icons.tsx +404 -0
- package/src/ui/components/redirect-to-sign-in.tsx +16 -0
- package/src/ui/components/redirect-to-sign-up.tsx +16 -0
- package/src/ui/components/signed-in.tsx +20 -0
- package/src/ui/components/signed-out.tsx +20 -0
- package/src/ui/components/ui/alert.tsx +66 -0
- package/src/ui/components/ui/button.tsx +70 -0
- package/src/ui/components/ui/card.tsx +92 -0
- package/src/ui/components/ui/checkbox.tsx +66 -0
- package/src/ui/components/ui/field.tsx +248 -0
- package/src/ui/components/ui/form.tsx +165 -0
- package/src/ui/components/ui/input-otp.tsx +77 -0
- package/src/ui/components/ui/input.tsx +21 -0
- package/src/ui/components/ui/label.tsx +23 -0
- package/src/ui/components/ui/separator.tsx +34 -0
- package/src/ui/components/ui/skeleton.tsx +13 -0
- package/src/ui/components/ui/textarea.tsx +18 -0
- package/src/ui/components/user-avatar.tsx +151 -0
- package/src/ui/hooks/use-auth-data.ts +193 -0
- package/src/ui/hooks/use-authenticate.ts +64 -0
- package/src/ui/hooks/use-captcha.tsx +151 -0
- package/src/ui/hooks/use-hydrated.ts +13 -0
- package/src/ui/hooks/use-lang.ts +32 -0
- package/src/ui/hooks/use-success-transition.ts +41 -0
- package/src/ui/hooks/use-theme.ts +39 -0
- package/src/ui/index.ts +46 -0
- package/src/ui/instantdb.ts +1 -0
- package/src/ui/lib/auth-data-cache.ts +90 -0
- package/src/ui/lib/auth-ui-provider.tsx +769 -0
- package/src/ui/lib/gravatar-utils.ts +58 -0
- package/src/ui/lib/image-utils.ts +55 -0
- package/src/ui/lib/instantdb/model-names.ts +24 -0
- package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
- package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
- package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
- package/src/ui/lib/instantdb/use-session.ts +55 -0
- package/src/ui/lib/social-providers.ts +150 -0
- package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
- package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
- package/src/ui/lib/triplit/model-names.ts +24 -0
- package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
- package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
- package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
- package/src/ui/lib/triplit/use-session.ts +42 -0
- package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
- package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
- package/src/ui/lib/utils.ts +119 -0
- package/src/ui/lib/view-paths.ts +61 -0
- package/src/ui/lib/wallet.ts +129 -0
- package/src/ui/localization/admin-error-codes.ts +20 -0
- package/src/ui/localization/anonymous-error-codes.ts +6 -0
- package/src/ui/localization/api-key-error-codes.ts +32 -0
- package/src/ui/localization/auth-localization.ts +865 -0
- package/src/ui/localization/base-error-codes.ts +27 -0
- package/src/ui/localization/captcha-error-codes.ts +17 -0
- package/src/ui/localization/email-otp-error-codes.ts +7 -0
- package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
- package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
- package/src/ui/localization/multi-session-error-codes.ts +3 -0
- package/src/ui/localization/organization-error-codes.ts +57 -0
- package/src/ui/localization/passkey-error-codes.ts +10 -0
- package/src/ui/localization/phone-number-error-codes.ts +10 -0
- package/src/ui/localization/stripe-localization.ts +12 -0
- package/src/ui/localization/team-error-codes.ts +12 -0
- package/src/ui/localization/two-factor-error-codes.ts +12 -0
- package/src/ui/localization/username-error-codes.ts +9 -0
- package/src/ui/server.ts +4 -0
- package/src/ui/style.css +146 -0
- package/src/ui/tanstack.ts +1 -0
- package/src/ui/triplit.ts +1 -0
- package/src/ui/types/account-options.ts +35 -0
- package/src/ui/types/additional-fields.ts +21 -0
- package/src/ui/types/any-auth-client.ts +6 -0
- package/src/ui/types/api-key.ts +9 -0
- package/src/ui/types/auth-client.ts +41 -0
- package/src/ui/types/auth-hooks.ts +81 -0
- package/src/ui/types/auth-mutators.ts +21 -0
- package/src/ui/types/avatar-options.ts +29 -0
- package/src/ui/types/captcha-options.ts +32 -0
- package/src/ui/types/captcha-provider.ts +7 -0
- package/src/ui/types/credentials-options.ts +38 -0
- package/src/ui/types/delete-user-options.ts +7 -0
- package/src/ui/types/email-verification-options.ts +7 -0
- package/src/ui/types/fetch-error.ts +6 -0
- package/src/ui/types/generic-oauth-options.ts +16 -0
- package/src/ui/types/gravatar-options.ts +21 -0
- package/src/ui/types/image.ts +7 -0
- package/src/ui/types/invitation.ts +10 -0
- package/src/ui/types/link.ts +7 -0
- package/src/ui/types/organization-options.ts +106 -0
- package/src/ui/types/password-validation.ts +16 -0
- package/src/ui/types/profile.ts +15 -0
- package/src/ui/types/refetch.ts +1 -0
- package/src/ui/types/render-toast.ts +9 -0
- package/src/ui/types/sign-up-options.ts +7 -0
- package/src/ui/types/social-options.ts +16 -0
- package/src/ui/types/team-options.ts +47 -0
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
onError?: (error: string) => void
|
|
18
|
+
other?: boolean
|
|
19
|
+
provider: Provider
|
|
20
|
+
redirectTo?: string
|
|
21
|
+
socialLayout: "auto" | "horizontal" | "grid" | "vertical"
|
|
22
|
+
setIsSubmitting: (isSubmitting: boolean) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ProviderButton({
|
|
26
|
+
className,
|
|
27
|
+
classNames,
|
|
28
|
+
callbackURL: callbackURLProp,
|
|
29
|
+
isSubmitting,
|
|
30
|
+
localization,
|
|
31
|
+
onError,
|
|
32
|
+
other,
|
|
33
|
+
provider,
|
|
34
|
+
redirectTo: redirectToProp,
|
|
35
|
+
socialLayout,
|
|
36
|
+
setIsSubmitting
|
|
37
|
+
}: ProviderButtonProps) {
|
|
38
|
+
const {
|
|
39
|
+
authClient,
|
|
40
|
+
basePath,
|
|
41
|
+
baseURL,
|
|
42
|
+
persistClient,
|
|
43
|
+
redirectTo: contextRedirectTo,
|
|
44
|
+
viewPaths,
|
|
45
|
+
social,
|
|
46
|
+
genericOAuth,
|
|
47
|
+
toast,
|
|
48
|
+
localizeErrors
|
|
49
|
+
} = useContext(AuthUIContext)
|
|
50
|
+
|
|
51
|
+
const getRedirectTo = useCallback(
|
|
52
|
+
() =>
|
|
53
|
+
redirectToProp || getSearchParam("redirectTo") || contextRedirectTo,
|
|
54
|
+
[redirectToProp, contextRedirectTo]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const getCallbackURL = useCallback(
|
|
58
|
+
() =>
|
|
59
|
+
`${baseURL}${
|
|
60
|
+
callbackURLProp ||
|
|
61
|
+
(persistClient
|
|
62
|
+
? `${basePath}/${viewPaths.CALLBACK}?redirectTo=${encodeURIComponent(getRedirectTo())}`
|
|
63
|
+
: getRedirectTo())
|
|
64
|
+
}`,
|
|
65
|
+
[
|
|
66
|
+
callbackURLProp,
|
|
67
|
+
persistClient,
|
|
68
|
+
basePath,
|
|
69
|
+
viewPaths,
|
|
70
|
+
baseURL,
|
|
71
|
+
getRedirectTo
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const doSignInSocial = async () => {
|
|
76
|
+
setIsSubmitting(true)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
if (other) {
|
|
80
|
+
const oauth2Params = {
|
|
81
|
+
providerId: provider.provider,
|
|
82
|
+
callbackURL: getCallbackURL(),
|
|
83
|
+
fetchOptions: { throw: true }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (genericOAuth?.signIn) {
|
|
87
|
+
await genericOAuth.signIn(oauth2Params)
|
|
88
|
+
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
setIsSubmitting(false)
|
|
91
|
+
}, 10000)
|
|
92
|
+
} else {
|
|
93
|
+
await authClient.signIn.oauth2(oauth2Params)
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const socialParams = {
|
|
97
|
+
provider: provider.provider as SocialProvider,
|
|
98
|
+
callbackURL: getCallbackURL(),
|
|
99
|
+
fetchOptions: { throw: true }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (social?.signIn) {
|
|
103
|
+
await social.signIn(socialParams)
|
|
104
|
+
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
setIsSubmitting(false)
|
|
107
|
+
}, 10000)
|
|
108
|
+
} else {
|
|
109
|
+
await authClient.signIn.social(socialParams)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const errorMessage = getLocalizedError({
|
|
114
|
+
error,
|
|
115
|
+
localization,
|
|
116
|
+
localizeErrors
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Call onError callback if provided (for inline display in parent)
|
|
120
|
+
onError?.(errorMessage)
|
|
121
|
+
|
|
122
|
+
// Also call toast for users who provide custom toast handler
|
|
123
|
+
toast({
|
|
124
|
+
variant: "error",
|
|
125
|
+
message: errorMessage
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
setIsSubmitting(false)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Button
|
|
134
|
+
className={cn(
|
|
135
|
+
socialLayout === "vertical" && "w-full",
|
|
136
|
+
socialLayout === "horizontal" && "flex-1 min-w-0",
|
|
137
|
+
className,
|
|
138
|
+
classNames?.form?.button,
|
|
139
|
+
classNames?.form?.outlineButton,
|
|
140
|
+
classNames?.form?.providerButton
|
|
141
|
+
)}
|
|
142
|
+
disabled={isSubmitting}
|
|
143
|
+
variant="outline"
|
|
144
|
+
onClick={doSignInSocial}
|
|
145
|
+
>
|
|
146
|
+
{provider.icon && (
|
|
147
|
+
<provider.icon className={classNames?.form?.icon} />
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{socialLayout === "grid" && provider.name}
|
|
151
|
+
{socialLayout === "vertical" &&
|
|
152
|
+
`${localization.SIGN_IN_WITH} ${provider.name}`}
|
|
153
|
+
</Button>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -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({ redirectTo }: { redirectTo?: string }) {
|
|
10
|
+
const signingOut = useRef(false)
|
|
11
|
+
|
|
12
|
+
const { authClient, basePath, viewPaths } = useContext(AuthUIContext)
|
|
13
|
+
const { onSuccess } = useOnSuccessTransition({
|
|
14
|
+
redirectTo: 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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet Button - Sign-In with Ethereum (SIWE)
|
|
3
|
+
*
|
|
4
|
+
* Flow: Connect wallet → Get nonce → Sign message → Verify → Success
|
|
5
|
+
*/
|
|
6
|
+
import { WalletIcon } from 'lucide-react';
|
|
7
|
+
import { useContext, useState, useEffect, useCallback } from 'react';
|
|
8
|
+
|
|
9
|
+
import { useOnSuccessTransition } from '../../hooks/use-success-transition';
|
|
10
|
+
import { AuthUIContext } from '../../lib/auth-ui-provider';
|
|
11
|
+
import { cn, getLocalizedError } from '../../lib/utils';
|
|
12
|
+
import type { AuthLocalization } from '../../localization/auth-localization';
|
|
13
|
+
import { Button } from '../ui/button';
|
|
14
|
+
import type { AuthViewClassNames } from './auth-view';
|
|
15
|
+
import {
|
|
16
|
+
hasInjectedWallet,
|
|
17
|
+
connectWallet,
|
|
18
|
+
signMessage,
|
|
19
|
+
buildSiweMessage,
|
|
20
|
+
} from '../../lib/wallet';
|
|
21
|
+
|
|
22
|
+
interface WalletButtonProps {
|
|
23
|
+
classNames?: AuthViewClassNames;
|
|
24
|
+
isSubmitting?: boolean;
|
|
25
|
+
localization?: Partial<AuthLocalization>;
|
|
26
|
+
redirectTo?: string;
|
|
27
|
+
setIsSubmitting?: (isSubmitting: boolean) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function WalletButton({
|
|
31
|
+
classNames,
|
|
32
|
+
isSubmitting,
|
|
33
|
+
localization,
|
|
34
|
+
redirectTo,
|
|
35
|
+
setIsSubmitting,
|
|
36
|
+
}: WalletButtonProps) {
|
|
37
|
+
const {
|
|
38
|
+
authClient,
|
|
39
|
+
localization: contextLocalization,
|
|
40
|
+
toast,
|
|
41
|
+
localizeErrors,
|
|
42
|
+
} = useContext(AuthUIContext);
|
|
43
|
+
|
|
44
|
+
localization = { ...contextLocalization, ...localization };
|
|
45
|
+
|
|
46
|
+
const { onSuccess } = useOnSuccessTransition({ redirectTo });
|
|
47
|
+
|
|
48
|
+
// Track mounted state to avoid hydration mismatch
|
|
49
|
+
const [mounted, setMounted] = useState(false);
|
|
50
|
+
const [walletAddress, setWalletAddress] = useState<string | null>(null);
|
|
51
|
+
const [chainId, setChainId] = useState<number | null>(null);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setMounted(true);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const handleWalletAuth = useCallback(async () => {
|
|
58
|
+
setIsSubmitting?.(true);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Step 1: Connect wallet if not connected
|
|
62
|
+
let address = walletAddress;
|
|
63
|
+
let chain = chainId;
|
|
64
|
+
|
|
65
|
+
if (!address || !chain) {
|
|
66
|
+
const connection = await connectWallet();
|
|
67
|
+
address = connection.address;
|
|
68
|
+
chain = connection.chainId;
|
|
69
|
+
setWalletAddress(address);
|
|
70
|
+
setChainId(chain);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 2: Get nonce from server
|
|
74
|
+
const nonceResult = await (authClient as any).siwe.nonce({
|
|
75
|
+
walletAddress: address,
|
|
76
|
+
chainId: chain,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (nonceResult.error) {
|
|
80
|
+
throw new Error(nonceResult.error.message || 'Failed to get nonce');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const nonce = nonceResult.data?.nonce;
|
|
84
|
+
if (!nonce) {
|
|
85
|
+
throw new Error('No nonce received from server');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Step 3: Build SIWE message
|
|
89
|
+
const message = buildSiweMessage({
|
|
90
|
+
domain: window.location.host,
|
|
91
|
+
address: address,
|
|
92
|
+
statement: 'Sign in with Ethereum',
|
|
93
|
+
uri: window.location.origin,
|
|
94
|
+
version: '1',
|
|
95
|
+
chainId: chain,
|
|
96
|
+
nonce,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Step 4: Sign with wallet
|
|
100
|
+
const signature = await signMessage(message, address);
|
|
101
|
+
|
|
102
|
+
// Step 5: Verify with server
|
|
103
|
+
const verifyResult = await (authClient as any).siwe.verify({
|
|
104
|
+
message,
|
|
105
|
+
signature,
|
|
106
|
+
walletAddress: address,
|
|
107
|
+
chainId: chain,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (verifyResult.error) {
|
|
111
|
+
throw new Error(verifyResult.error.message || 'Verification failed');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Success!
|
|
115
|
+
onSuccess();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
toast({
|
|
118
|
+
variant: 'error',
|
|
119
|
+
message: getLocalizedError({
|
|
120
|
+
error,
|
|
121
|
+
localization,
|
|
122
|
+
localizeErrors,
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
setIsSubmitting?.(false);
|
|
126
|
+
}
|
|
127
|
+
}, [
|
|
128
|
+
authClient,
|
|
129
|
+
walletAddress,
|
|
130
|
+
chainId,
|
|
131
|
+
localization,
|
|
132
|
+
localizeErrors,
|
|
133
|
+
onSuccess,
|
|
134
|
+
setIsSubmitting,
|
|
135
|
+
toast,
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// Don't render during SSR to avoid hydration mismatch
|
|
139
|
+
if (!mounted) {
|
|
140
|
+
return (
|
|
141
|
+
<Button
|
|
142
|
+
className={cn(
|
|
143
|
+
'w-full',
|
|
144
|
+
classNames?.form?.button,
|
|
145
|
+
classNames?.form?.secondaryButton
|
|
146
|
+
)}
|
|
147
|
+
disabled
|
|
148
|
+
variant="secondary"
|
|
149
|
+
>
|
|
150
|
+
<WalletIcon className="h-4 w-4" />
|
|
151
|
+
Connect Wallet
|
|
152
|
+
</Button>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// No wallet available
|
|
157
|
+
if (!hasInjectedWallet()) {
|
|
158
|
+
return (
|
|
159
|
+
<Button
|
|
160
|
+
className={cn(
|
|
161
|
+
'w-full',
|
|
162
|
+
classNames?.form?.button,
|
|
163
|
+
classNames?.form?.secondaryButton
|
|
164
|
+
)}
|
|
165
|
+
disabled
|
|
166
|
+
variant="secondary"
|
|
167
|
+
title="Install MetaMask or Phantom to use wallet sign-in"
|
|
168
|
+
>
|
|
169
|
+
<WalletIcon className="h-4 w-4" />
|
|
170
|
+
No Wallet Found
|
|
171
|
+
</Button>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Button
|
|
177
|
+
className={cn(
|
|
178
|
+
'w-full',
|
|
179
|
+
classNames?.form?.button,
|
|
180
|
+
classNames?.form?.secondaryButton
|
|
181
|
+
)}
|
|
182
|
+
disabled={isSubmitting}
|
|
183
|
+
variant="secondary"
|
|
184
|
+
onClick={handleWalletAuth}
|
|
185
|
+
>
|
|
186
|
+
<WalletIcon className="h-4 w-4" />
|
|
187
|
+
{walletAddress
|
|
188
|
+
? `Sign in with ${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`
|
|
189
|
+
: 'Sign in with Wallet'}
|
|
190
|
+
</Button>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -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,91 @@
|
|
|
1
|
+
import { CaptchaFox } from "@captchafox/react"
|
|
2
|
+
import HCaptcha from "@hcaptcha/react-hcaptcha"
|
|
3
|
+
import { Turnstile } from "@marsidev/react-turnstile"
|
|
4
|
+
import { type RefObject, useContext } from "react"
|
|
5
|
+
|
|
6
|
+
import { useTheme } from "../../hooks/use-theme"
|
|
7
|
+
import { AuthUIContext } from "../../lib/auth-ui-provider"
|
|
8
|
+
import type { AuthLocalization } from "../../localization/auth-localization"
|
|
9
|
+
import { RecaptchaBadge } from "./recaptcha-badge"
|
|
10
|
+
import { RecaptchaV2 } from "./recaptcha-v2"
|
|
11
|
+
|
|
12
|
+
// Default captcha endpoints
|
|
13
|
+
const DEFAULT_CAPTCHA_ENDPOINTS = [
|
|
14
|
+
"/sign-up/email",
|
|
15
|
+
"/sign-in/email",
|
|
16
|
+
"/forget-password"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
interface CaptchaProps {
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
|
21
|
+
ref: RefObject<any>
|
|
22
|
+
localization: Partial<AuthLocalization>
|
|
23
|
+
action?: string // Optional action to check if it's in the endpoints list
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Captcha({ ref, localization, action }: CaptchaProps) {
|
|
27
|
+
const { captcha } = useContext(AuthUIContext)
|
|
28
|
+
if (!captcha) return null
|
|
29
|
+
|
|
30
|
+
// If action is provided, check if it's in the list of captcha-enabled endpoints
|
|
31
|
+
if (action) {
|
|
32
|
+
const endpoints = captcha.endpoints || DEFAULT_CAPTCHA_ENDPOINTS
|
|
33
|
+
if (!endpoints.includes(action)) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { theme } = useTheme()
|
|
39
|
+
|
|
40
|
+
const showRecaptchaV2 =
|
|
41
|
+
captcha.provider === "google-recaptcha-v2-checkbox" ||
|
|
42
|
+
captcha.provider === "google-recaptcha-v2-invisible"
|
|
43
|
+
|
|
44
|
+
const showRecaptchaBadge =
|
|
45
|
+
captcha.provider === "google-recaptcha-v3" ||
|
|
46
|
+
captcha.provider === "google-recaptcha-v2-invisible"
|
|
47
|
+
|
|
48
|
+
const showTurnstile = captcha.provider === "cloudflare-turnstile"
|
|
49
|
+
|
|
50
|
+
const showHCaptcha = captcha.provider === "hcaptcha"
|
|
51
|
+
|
|
52
|
+
const showCaptchaFox = captcha.provider === "captchafox"
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
{showRecaptchaV2 && <RecaptchaV2 ref={ref} />}
|
|
57
|
+
{showRecaptchaBadge && (
|
|
58
|
+
<RecaptchaBadge localization={localization} />
|
|
59
|
+
)}
|
|
60
|
+
{showTurnstile && (
|
|
61
|
+
<Turnstile
|
|
62
|
+
className="mx-auto"
|
|
63
|
+
ref={ref}
|
|
64
|
+
siteKey={captcha.siteKey}
|
|
65
|
+
options={{
|
|
66
|
+
theme: theme,
|
|
67
|
+
size: "flexible"
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
{showHCaptcha && (
|
|
72
|
+
<div className="mx-auto">
|
|
73
|
+
<HCaptcha
|
|
74
|
+
ref={ref}
|
|
75
|
+
sitekey={captcha.siteKey}
|
|
76
|
+
theme={theme}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
{showCaptchaFox && (
|
|
81
|
+
<div className="mx-auto">
|
|
82
|
+
<CaptchaFox
|
|
83
|
+
ref={ref}
|
|
84
|
+
sitekey={captcha.siteKey}
|
|
85
|
+
theme={theme}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -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
|
+
}
|