diff --git a/src/backend/accounts/meta.ts b/src/backend/accounts/meta.ts index ec6b94be..0886dc11 100644 --- a/src/backend/accounts/meta.ts +++ b/src/backend/accounts/meta.ts @@ -5,6 +5,7 @@ export interface MetaResponse { name: string; description?: string; hasCaptcha: boolean; + captchaClientKey?: string; } export async function getBackendMeta(url: string): Promise { diff --git a/src/components/PassphraseDisplay.tsx b/src/components/PassphraseDisplay.tsx index c0567854..660418d1 100644 --- a/src/components/PassphraseDisplay.tsx +++ b/src/components/PassphraseDisplay.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from "react"; -import { useCopyToClipboard } from "react-use"; +import { useCopyToClipboard, useMountedState } from "react-use"; import { Icon, Icons } from "./Icon"; @@ -9,6 +9,7 @@ export function PassphaseDisplay(props: { mnemonic: string }) { const [, copy] = useCopyToClipboard(); const [hasCopied, setHasCopied] = useState(false); + const isMounted = useMountedState(); const timeout = useRef>(); @@ -16,7 +17,7 @@ export function PassphaseDisplay(props: { mnemonic: string }) { copy(props.mnemonic); setHasCopied(true); if (timeout.current) clearTimeout(timeout.current); - timeout.current = setTimeout(() => setHasCopied(false), 500); + timeout.current = setTimeout(() => isMounted() && setHasCopied(false), 500); } return ( diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts index 2024b4d5..8fb184c0 100644 --- a/src/hooks/auth/useAuth.ts +++ b/src/hooks/auth/useAuth.ts @@ -19,7 +19,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useAuthStore } from "@/stores/auth"; export interface RegistrationData { - recaptchaToken: string; + recaptchaToken?: string; mnemonic: string; userData: { device: string; diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index e7458f0a..f257f12b 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; +import { MetaResponse } from "@/backend/accounts/meta"; import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; import { AccountCreatePart, @@ -9,20 +10,38 @@ import { import { PassphraseGeneratePart } from "@/pages/parts/auth/PassphraseGeneratePart"; import { TrustBackendPart } from "@/pages/parts/auth/TrustBackendPart"; import { VerifyPassphrase } from "@/pages/parts/auth/VerifyPassphrasePart"; -import { conf } from "@/setup/config"; + +function CaptchaProvider(props: { + siteKey: string | null; + children: JSX.Element; +}) { + if (!props.siteKey) return props.children; + return ( + + {props.children} + + ); +} export function RegisterPage() { const [step, setStep] = useState(0); const [mnemonic, setMnemonic] = useState(null); const [account, setAccount] = useState(null); - const reCaptchaKey = conf().RECAPTCHA_SITE_KEY; + const [siteKey, setSiteKey] = useState(null); + + // TODO because of user data loading (in useAuthRestore()), the register page gets unmounted before finishing the register flow return ( - + {step === 0 ? ( { + onNext={(meta: MetaResponse) => { + setSiteKey( + meta.hasCaptcha && meta.captchaClientKey + ? meta.captchaClientKey + : null + ); setStep(1); }} /> @@ -45,6 +64,7 @@ export function RegisterPage() { ) : null} {step === 3 ? ( { @@ -54,6 +74,6 @@ export function RegisterPage() { ) : null} {step === 4 ?

Success, account now exists

: null}
-
+ ); } diff --git a/src/pages/parts/auth/VerifyPassphrasePart.tsx b/src/pages/parts/auth/VerifyPassphrasePart.tsx index 270be190..34c3a52b 100644 --- a/src/pages/parts/auth/VerifyPassphrasePart.tsx +++ b/src/pages/parts/auth/VerifyPassphrasePart.tsx @@ -15,6 +15,7 @@ import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart"; interface VerifyPassphraseProps { mnemonic: string | null; + hasCaptcha?: boolean; userData: AccountProfile | null; onNext?: () => void; } @@ -27,18 +28,20 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) { const [result, execute] = useAsyncFn( async (inputMnemonic: string) => { - const recaptchaToken = executeRecaptcha - ? await executeRecaptcha() - : undefined; - if (!props.mnemonic || !props.userData) throw new Error("Data is not valid"); - if (!recaptchaToken) throw new Error("ReCaptcha validation failed"); + + let recaptchaToken: string | undefined; + if (props.hasCaptcha) { + recaptchaToken = executeRecaptcha + ? await executeRecaptcha() + : undefined; + if (!recaptchaToken) throw new Error("ReCaptcha validation failed"); + } + if (inputMnemonic !== props.mnemonic) throw new Error("Passphrase doesn't match"); - // TODO captcha? - await register({ mnemonic: inputMnemonic, userData: props.userData, diff --git a/src/setup/config.ts b/src/setup/config.ts index ffc57ffa..36b63c2c 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -8,7 +8,6 @@ interface Config { CORS_PROXY_URL: string; NORMAL_ROUTER: boolean; BACKEND_URL: string; - RECAPTCHA_SITE_KEY: string; } export interface RuntimeConfig { @@ -19,7 +18,6 @@ export interface RuntimeConfig { NORMAL_ROUTER: boolean; PROXY_URLS: string[]; BACKEND_URL: string; - RECAPTCHA_SITE_KEY: string; } const env: Record = { @@ -30,7 +28,6 @@ const env: Record = { CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, BACKEND_URL: import.meta.env.VITE_BACKEND_URL, - RECAPTCHA_SITE_KEY: import.meta.env.VITE_RECAPTCHA_SITE_KEY, }; // loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js) @@ -56,6 +53,5 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", - RECAPTCHA_SITE_KEY: getKey("RECAPTCHA_SITE_KEY"), }; }