From 43239e9fb380a172464f90337ef6b883798e967c Mon Sep 17 00:00:00 2001 From: Privilege Mendes Date: Wed, 6 Dec 2023 19:56:16 +0100 Subject: [PATCH 01/24] added recaptcha detection text --- src/assets/text/homepage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/assets/text/homepage.ts b/src/assets/text/homepage.ts index 8b9e7165..e3673522 100644 --- a/src/assets/text/homepage.ts +++ b/src/assets/text/homepage.ts @@ -8,4 +8,5 @@ export const homepage = { "The daily top 100 gets Aleo credits transferred to their wallets. Start playing now!", topRanked: (position: number, username: string, score: number) => `${position + 1}. ${username} ${score} points`, + botDetected: "Recaptcha detected a bot. Please try again.", }; From 732ea847d26755deddd15fd9f5d03970f22ab584 Mon Sep 17 00:00:00 2001 From: Privilege Mendes Date: Wed, 6 Dec 2023 19:57:40 +0100 Subject: [PATCH 02/24] added home button to menu dialog for ease of navigation --- src/components/menu-dialog/menu-dialog.tsx | 11 +++++++++++ src/components/menu-dialog/styles.ts | 3 +++ 2 files changed, 14 insertions(+) diff --git a/src/components/menu-dialog/menu-dialog.tsx b/src/components/menu-dialog/menu-dialog.tsx index 444cfb68..4c8b0b46 100644 --- a/src/components/menu-dialog/menu-dialog.tsx +++ b/src/components/menu-dialog/menu-dialog.tsx @@ -7,6 +7,8 @@ import { text } from "~/assets/text"; import { styles } from "./styles.js"; import { Footer } from "./footer"; import { MenuOptions } from "./menu-options"; +import { useRouter } from "next/router"; +import { pages } from "~/router"; const Transition = forwardRef(function Transition( props: TransitionProps & { @@ -24,6 +26,12 @@ interface Props { } export const MenuDialog: FC = ({ open, setOpen }) => { + const { push } = useRouter(); + + const goHome = () => { + void push(pages.home); + setOpen(false); + }; const handleClose = () => { setOpen(false); }; @@ -37,6 +45,9 @@ export const MenuDialog: FC = ({ open, setOpen }) => { sx={styles.dialog} > + diff --git a/src/components/menu-dialog/styles.ts b/src/components/menu-dialog/styles.ts index e89d3abf..8fc63ef6 100644 --- a/src/components/menu-dialog/styles.ts +++ b/src/components/menu-dialog/styles.ts @@ -9,8 +9,11 @@ export const styles = { }, buttonWrapper: { alignItems: "flex-end", + flexDirection: "row", + justifyContent: "space-between", pt: 3, pr: 3, + pl: 3, width: "100vw", }, button: { From c7e36467066c73465a421705d3341e650a1bafd4 Mon Sep 17 00:00:00 2001 From: Privilege Mendes Date: Wed, 6 Dec 2023 20:02:40 +0100 Subject: [PATCH 03/24] added recaptcha provider, hooks and utils --- src/containers/reCaptcha/index.ts | 2 + src/containers/reCaptcha/reCaptcha.tsx | 152 +++++++++++++++++++++++++ src/containers/reCaptcha/utils.ts | 32 ++++++ src/hooks/use-reCaptcha.ts | 58 ++++++++++ src/pages/index.tsx | 7 +- 5 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/containers/reCaptcha/index.ts create mode 100644 src/containers/reCaptcha/reCaptcha.tsx create mode 100644 src/containers/reCaptcha/utils.ts create mode 100644 src/hooks/use-reCaptcha.ts diff --git a/src/containers/reCaptcha/index.ts b/src/containers/reCaptcha/index.ts new file mode 100644 index 00000000..f084af1c --- /dev/null +++ b/src/containers/reCaptcha/index.ts @@ -0,0 +1,2 @@ +export * from "./reCaptcha"; +export * from "./utils.js"; diff --git a/src/containers/reCaptcha/reCaptcha.tsx b/src/containers/reCaptcha/reCaptcha.tsx new file mode 100644 index 00000000..de504c73 --- /dev/null +++ b/src/containers/reCaptcha/reCaptcha.tsx @@ -0,0 +1,152 @@ +"use client"; + +import React, { + createContext, + useCallback, + useContext, + useDebugValue, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import Script, { type ScriptProps } from "next/script.js"; + +import { getRecaptchaScriptSrc } from "./utils.js"; +import { type IReCaptcha } from "~/types/recaptcha"; +import { env } from "~/env.mjs"; + +interface ReCaptchaContextProps { + /** reCAPTCHA_site_key */ + readonly reCaptchaKey: string | null; + /** Global ReCaptcha object */ + readonly grecaptcha: IReCaptcha | null; + /** Is ReCaptcha script loaded */ + readonly loaded: boolean; + /** Is ReCaptcha failed to load */ + readonly error: boolean; +} + +const ReCaptchaContext = createContext({ + reCaptchaKey: null, + grecaptcha: null, + loaded: false, + error: false, +}); + +const useReCaptchaContext = () => { + const values = useContext(ReCaptchaContext); + useDebugValue(`grecaptcha available: ${values?.loaded ? "Yes" : "No"}`); + useDebugValue( + `ReCaptcha Script: ${values?.loaded ? "Loaded" : "Not Loaded"}`, + ); + useDebugValue(`Failed to load Script: ${values?.error ? "Yes" : "No"}`); + return values; +}; + +interface ReCaptchaProviderProps extends Partial> { + reCaptchaKey?: string; + language?: string; + useRecaptchaNet?: boolean; + useEnterprise?: boolean; + children?: React.ReactNode; + onLoad?: (grecaptcha: IReCaptcha, e: undefined) => void; +} + +const ReCaptchaProvider: React.FC = ({ + reCaptchaKey: passedReCaptchaKey, + + useEnterprise = false, + useRecaptchaNet = false, + language, + children, + + id = "google-recaptcha-v3", + strategy = "beforeInteractive", + + src: passedSrc, + onLoad: passedOnLoad, + onError: passedOnError, + + ...props +}) => { + const [grecaptcha, setGreCaptcha] = useState(null); + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(false); + + const reCaptchaKey = + (passedReCaptchaKey ?? env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY) || ""; + + const src = + passedSrc ?? + getRecaptchaScriptSrc({ + reCaptchaKey, + language, + useRecaptchaNet, + useEnterprise, + }); + + // Reset state when script src is changed + const mounted = useRef(false); + + useEffect(() => { + if (mounted.current) { + setLoaded(false); + setError(false); + } + mounted.current = true; + }, [src]); + + // Handle script load + const onLoad = useCallback( + (e?: never) => { + const grecaptcha = useEnterprise + ? window?.grecaptcha?.enterprise + : window?.grecaptcha; + + if (grecaptcha) { + grecaptcha.ready(() => { + setGreCaptcha(grecaptcha); + setLoaded(true); + passedOnLoad?.(grecaptcha, e); + }); + } + }, + [passedOnLoad, useEnterprise], + ); + + // Run 'onLoad' function once just in case if grecaptcha is already globally available in window + useEffect(() => onLoad(), [onLoad]); + + // Handle script error + const onError = useCallback( + (e: ErrorEvent) => { + setError(true); + passedOnError?.(e); + }, + [passedOnError], + ); + + // Prevent unnecessary rerenders + const value = useMemo( + () => ({ reCaptchaKey, grecaptcha, loaded, error }), + [reCaptchaKey, grecaptcha, loaded, error], + ); + + return ( + + {children} +