Skip to content

Commit

Permalink
feat: only shows turnstile widget when cf rejects (#722)
Browse files Browse the repository at this point in the history
  • Loading branch information
ygrishajev authored Jan 28, 2025
1 parent 62198a8 commit 0537f2c
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 49 deletions.
5 changes: 3 additions & 2 deletions apps/deploy-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"dev-nodc": "next",
"format": "prettier --write ./*.{ts,js,json} **/*.{ts,tsx,js,json}",
"lint": "eslint .",
"release": "release-it",
"start": "next start",
"type-check": "tsc",
"release": "release-it"
"type-check": "tsc"
},
"dependencies": {
"@akashnetwork/akash-api": "^1.3.0",
Expand Down Expand Up @@ -65,6 +65,7 @@
"date-fns": "^2.29.3",
"dotenv": "^16.4.5",
"file-saver": "^2.0.5",
"framer-motion": "^12.0.5",
"geist": "^1.3.0",
"http-proxy": "^1.18.1",
"iconoir-react": "^7.3.0",
Expand Down
110 changes: 64 additions & 46 deletions apps/deploy-web/src/components/turnstile/Turnstile.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use client";

import { FC, useEffect, useMemo, useRef, useState } from "react";
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { MdInfo } from "react-icons/md";
import { Button } from "@akashnetwork/ui/components";
import { Turnstile as ReactTurnstile, TurnstileInstance } from "@marsidev/react-turnstile";
import classnames from "classnames";
import axios, { AxiosError, AxiosHeaders } from "axios";
import { motion } from "framer-motion";
import { firstValueFrom, Subject } from "rxjs";

import { browserEnvConfig } from "@src/config/browser-env.config";
import { useWhen } from "@src/hooks/useWhen";
import { managedWalletHttpService } from "@src/services/managed-wallet-http/managed-wallet-http.service";

type TurnstileStatus = "uninitialized" | "solved" | "interactive" | "expired" | "error" | "dismissed" | "retrying";

Expand All @@ -16,44 +18,51 @@ const VISIBILITY_STATUSES: TurnstileStatus[] = ["interactive", "error", "retryin
export const Turnstile: FC = () => {
const turnstileRef = useRef<TurnstileInstance>();
const [status, setStatus] = useState<TurnstileStatus>("uninitialized");
const [isTimingOut, setIsTimingOut] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const [shouldHide, setShouldHide] = useState(true);
const hasActions = useMemo(() => isTimingOut || status === "error", [isTimingOut, status]);
const isVisible = useMemo(() => !!browserEnvConfig.NEXT_PUBLIC_TURNSTILE_ENABLED && VISIBILITY_STATUSES.includes(status), [status]);
const dismissedSubject = useRef(new Subject<void>());

useEffect(() => {
if (!browserEnvConfig.NEXT_PUBLIC_TURNSTILE_ENABLED) {
setIsVisible(false);
} else if (VISIBILITY_STATUSES.includes(status)) {
setIsVisible(true);
setShouldHide(false);
} else if (isVisible && status === "dismissed") {
setShouldHide(true);
} else if (isVisible) {
setTimeout(() => setShouldHide(true), 1000);
}
}, [isVisible, status]);
const interceptorId = managedWalletHttpService.interceptors.response.use(
response => response,
async (error: AxiosError) => {
const headers = error.response?.headers;

useWhen(isVisible, () => {
setTimeout(() => setIsTimingOut(true), 5000);
});
if (headers instanceof AxiosHeaders && headers.get("cf-mitigated") === "challenge" && turnstileRef.current) {
turnstileRef.current?.render();
turnstileRef.current.execute();
const response = await Promise.race([turnstileRef.current.getResponsePromise(), firstValueFrom(dismissedSubject.current.asObservable())]);

useWhen(shouldHide, () => {
setTimeout(() => setIsVisible(false), 400);
});
if (response) {
return axios(error.config!);
}
}

return Promise.reject(error);
}
);

return () => {
managedWalletHttpService.interceptors.response.eject(interceptorId);
};
}, []);

const resetWidget = useCallback(() => {
turnstileRef.current?.remove();
turnstileRef.current?.render();
turnstileRef.current?.execute();
}, []);

return browserEnvConfig.NEXT_PUBLIC_TURNSTILE_ENABLED ? (
<>
<div
className={classnames(
{
"opacity-0": shouldHide,
"opacity-100": !shouldHide,
visible: isVisible,
invisible: !isVisible
},
"fixed inset-0 z-[101] flex content-center items-center justify-center bg-white bg-opacity-90 transition-opacity duration-300"
)}
<motion.div
className="fixed inset-0 z-[101] flex content-center items-center justify-center bg-white bg-opacity-90"
initial={{ opacity: 0 }}
animate={{ opacity: isVisible ? 1 : 0 }}
style={{ pointerEvents: isVisible ? "auto" : "none" }}
transition={{
duration: 0.3,
delay: isVisible ? 0 : status === "dismissed" ? 0 : 1
}}
>
<div className="flex flex-col items-center">
<h3 className="mb-2 text-2xl font-bold">We are verifying you are a human. This may take a few seconds</h3>
Expand All @@ -62,6 +71,7 @@ export const Turnstile: FC = () => {
<ReactTurnstile
ref={turnstileRef}
siteKey={browserEnvConfig.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
options={{ execution: "execute" }}
onError={() =>
setStatus(prevStatus => {
if (prevStatus === "retrying") {
Expand All @@ -79,20 +89,28 @@ export const Turnstile: FC = () => {
/>
</div>
{status === "error" && <p className="text-red-600">Some error occurred</p>}
<div
className={classnames(
{
"invisible opacity-0": !hasActions,
"visible opacity-100": hasActions
},
"flex flex-col items-center transition-opacity duration-200"
)}
<motion.div
className="flex flex-col items-center"
initial={{ opacity: 0 }}
animate={{ opacity: isVisible ? 1 : 0 }}
style={{ pointerEvents: isVisible ? "auto" : "none" }}
transition={{
duration: 0.3,
delay: isVisible ? (status === "error" ? 0 : 5) : 1
}}
>
<div className="my-8">
<Button onClick={turnstileRef.current?.reset} className="mr-4">
<Button onClick={resetWidget} className="mr-4">
Retry
</Button>
<Button onClick={() => setStatus("dismissed")} variant="link">
<Button
onClick={() => {
setStatus("dismissed");
dismissedSubject.current.next();
turnstileRef.current?.remove();
}}
variant="link"
>
Dismiss
</Button>
</div>
Expand All @@ -101,9 +119,9 @@ export const Turnstile: FC = () => {
<MdInfo className="mr-1 inline text-xl text-muted-foreground" />
<small>dismissing the check might result into some features not working properly</small>
</p>
</div>
</motion.div>
</div>
</div>
</motion.div>
</>
) : null;
};
42 changes: 41 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0537f2c

Please sign in to comment.