Skip to content

Commit

Permalink
feat(CaptchaRequirement): use Tailwind CSS and Radix UI
Browse files Browse the repository at this point in the history
  • Loading branch information
BrickheadJohnny committed Oct 17, 2024
1 parent 5c7a734 commit a82d243
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useWeb3ConnectionManager } from "@/components/Web3ConnectionManager/hoo
import { useDisclosure } from "@/hooks/useDisclosure"
import { Robot } from "@phosphor-icons/react/dist/ssr"
import useSWRWithOptionalAuth from "hooks/useSWRWithOptionalAuth"
import { CompleteCaptchaModal } from "requirements/Captcha/components/CompleteCaptcha"
import { CompleteCaptchaDialog } from "requirements/Captcha/components/CompleteCaptcha"
import { JoinStep } from "./JoinStep"

const CompleteCaptchaJoinStep = (): JSX.Element => {
Expand All @@ -14,7 +14,7 @@ const CompleteCaptchaJoinStep = (): JSX.Element => {
mutate,
} = useSWRWithOptionalAuth(`/v2/util/gate-proof-existence/CAPTCHA`)

const { onOpen, onClose, isOpen } = useDisclosure()
const { isOpen, onOpen, setValue } = useDisclosure()

return (
<>
Expand All @@ -34,9 +34,9 @@ const CompleteCaptchaJoinStep = (): JSX.Element => {
}}
/>

<CompleteCaptchaModal
isOpen={isOpen}
onClose={onClose}
<CompleteCaptchaDialog
open={isOpen}
onOpenChange={setValue}
onComplete={() => mutate(() => true, { revalidate: false })}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const RequirementAccessIndicator = () => {
</p>
<div className="mt-2 flex justify-end">
{type === "CAPTCHA" ? (
<DynamicCompleteCaptcha size="sm" iconSpacing={2} />
<DynamicCompleteCaptcha size="sm" />
) : type.startsWith("GITCOIN_") ? (
<DynamicSetupPassport size="sm" />
) : (
Expand Down
11 changes: 5 additions & 6 deletions src/requirements/Captcha/CaptchaRequirement.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Icon } from "@chakra-ui/react"
import { Robot } from "@phosphor-icons/react"
import { Robot } from "@phosphor-icons/react/dist/ssr"
import {
Requirement,
RequirementProps,
Expand All @@ -17,16 +16,16 @@ const CaptchaRequirement = (props: RequirementProps): JSX.Element => {

return (
<Requirement
image={<Icon as={Robot} boxSize={6} />}
image={<Robot weight="bold" className="size-6" />}
footer={<CompleteCaptcha />}
{...props}
>
{"Complete a CAPTCHA"}
<span>Complete a CAPTCHA</span>
{captchaAge && (
<>
{` (valid until `}
<span>{" (valid until "}</span>
<DataBlock>{captchaAge}</DataBlock>
{`)`}
<span>{")"}</span>
</>
)}
</Requirement>
Expand Down
170 changes: 88 additions & 82 deletions src/requirements/Captcha/components/CompleteCaptcha.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,91 @@
import { Alert, AlertTitle } from "@/components/ui/Alert"
import { Button, ButtonProps } from "@/components/ui/Button"
import {
Box,
ButtonProps,
Center,
Code,
Icon,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Spinner,
Text,
useDisclosure,
} from "@chakra-ui/react"
Dialog,
DialogBody,
DialogCloseButton,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/Dialog"
import { useDisclosure } from "@/hooks/useDisclosure"
import { cn } from "@/lib/utils"
import HCaptcha from "@hcaptcha/react-hcaptcha"
import { Robot } from "@phosphor-icons/react"
import { CircleNotch, Robot, WarningCircle } from "@phosphor-icons/react/dist/ssr"
import { useMembershipUpdate } from "components/[guild]/JoinModal/hooks/useMembershipUpdate"
import { useRequirementContext } from "components/[guild]/Requirements/components/RequirementContext"
import useUser from "components/[guild]/hooks/useUser"
import Button from "components/common/Button"
import ErrorAlert from "components/common/ErrorAlert"
import { Modal } from "components/common/Modal"
import { DataBlock } from "components/common/DataBlock"
import { useRoleMembership } from "components/explorer/hooks/useMembership"
import { useFetcherWithSign } from "hooks/useFetcherWithSign"
import { Dispatch, ReactNode, SetStateAction } from "react"
import useSWRImmutable from "swr/immutable"
import useVerifyCaptcha from "../hooks/useVerifyCaptcha"

const CompleteCaptcha = (props: ButtonProps): JSX.Element => {
const CompleteCaptcha = ({ className, ...props }: ButtonProps): ReactNode => {
const { id: userId } = useUser()
const { id, roleId } = useRequirementContext()
const { onOpen, onClose, isOpen } = useDisclosure()
const { triggerMembershipUpdate } = useMembershipUpdate()

const { reqAccesses } = useRoleMembership(roleId)

const reqAccess = reqAccesses?.find((err) => err.requirementId === id)

const { isOpen, onOpen, setValue } = useDisclosure()

if (!userId || (!!reqAccess?.access && !reqAccess?.errorType)) return null

return (
<>
<Button
size="xs"
onClick={onOpen}
colorScheme="cyan"
leftIcon={<Icon as={Robot} />}
iconSpacing="1"
leftIcon={<Robot weight="bold" />}
// TODO: extract it to a constant, just like we did with PLATFORM_COLORS
className={cn(
"bg-cyan-500 text-white hover:bg-cyan-600 active:bg-cyan-700 active:dark:bg-cyan-300 hover:dark:bg-cyan-400",
className
)}
onClick={() => onOpen()}
{...props}
>
Complete CAPTCHA
</Button>

<CompleteCaptchaModal
onClose={onClose}
isOpen={isOpen}
<CompleteCaptchaDialog
open={isOpen}
onOpenChange={setValue}
onComplete={() => triggerMembershipUpdate({ roleIds: [roleId] })}
/>
</>
)
}

type Props = {
isOpen: boolean
onClose: () => void
type CompleteCaptchaDialogProps = {
open: boolean
onOpenChange: Dispatch<SetStateAction<boolean>>
onComplete?: () => void
shouldTriggerMembershipUpdate?: boolean
}

const CompleteCaptchaModal = ({ isOpen, onClose, onComplete }: Props) => {
const CompleteCaptchaDialog = ({
open,
onOpenChange,
onComplete,
}: CompleteCaptchaDialogProps) => {
const fetcherWithSign = useFetcherWithSign()
const {
data: getGateCallbackData,
isValidating,
error: getGateCallbackError,
} = useSWRImmutable(
isOpen
open
? [`/v2/util/gate-callbacks/session?requirementType=CAPTCHA`, { body: {} }]
: null,
fetcherWithSign
)

const { onSubmit, isLoading } = useVerifyCaptcha(() => {
onComplete?.()
onClose()
onOpenChange(false)
})

const onVerify = (token: string) =>
Expand All @@ -92,55 +95,58 @@ const CompleteCaptchaModal = ({ isOpen, onClose, onComplete }: Props) => {
})

return (
<Modal
{...{
isOpen,
onClose,
}}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Complete CAPTCHA</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Center flexDirection={"column"}>
{getGateCallbackError ? (
<ErrorAlert label="Couldn't generate CAPTCHA" />
) : (!getGateCallbackData?.callbackUrl && isValidating) || isLoading ? (
<>
<Spinner size="xl" mt="8" />
<Text mt="4" mb="8">
{`${isLoading ? "Verifying" : "Generating"} CAPTCHA`}
</Text>
</>
) : (
<>
<Box>
{typeof window !== "undefined" &&
window.location.hostname === "localhost" ? (
<Text textAlign="left">
HCaptcha doesn't work on localhost. Please use{" "}
<Code>127.0.0.1</Code> instead.
</Text>
) : (
<HCaptcha
sitekey="05bdce9d-3de2-4457-8318-85633ffd281c"
onVerify={onVerify}
/>
)}
</Box>
<Text mt="10" textAlign="center">
Please complete the CAPTCHA above! The modal will automatically
close on success
</Text>
</>
)}
</Center>
</ModalBody>
</ModalContent>
</Modal>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Complete CAPTCHA</DialogTitle>
</DialogHeader>

<DialogBody>
{getGateCallbackError ? (
<Alert variant="error" className="mb-6">
<WarningCircle weight="fill" className="size-6" />
<AlertTitle>Couldn't generate CAPTCHA</AlertTitle>
</Alert>
) : (!getGateCallbackData?.callbackUrl && isValidating) || isLoading ? (
<>
<CircleNotch weight="bold" className="mx-auto size-8 animate-spin" />
<p className="mt-4 mb-6 text-center">
{`${isLoading ? "Verifying" : "Generating"} CAPTCHA`}
</p>
</>
) : (
<>
<div className="mx-auto">
{typeof window !== "undefined" &&
window.location.hostname === "localhost" ? (
<p>
<span>{"HCaptcha doesn't work on localhost. Please use "}</span>
<DataBlock>127.0.0.1</DataBlock>
<span>{" instead."}</span>
</p>
) : (
<HCaptcha
sitekey="05bdce9d-3de2-4457-8318-85633ffd281c"
onVerify={onVerify}
/>
)}
</div>
{typeof window !== "undefined" &&
window.location.hostname !== "localhost" && (
<p className="mt-6 text-center">
Please complete the CAPTCHA above! The modal will automatically
close on success
</p>
)}
</>
)}
</DialogBody>

<DialogCloseButton />
</DialogContent>
</Dialog>
)
}

export default CompleteCaptcha
export { CompleteCaptchaModal }
export { CompleteCaptchaDialog }
25 changes: 17 additions & 8 deletions src/requirements/Captcha/hooks/useVerifyCaptcha.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
import useShowErrorToast from "hooks/useShowErrorToast"
import { useErrorToast } from "@/components/ui/hooks/useErrorToast"
import { useToast } from "@/components/ui/hooks/useToast"
import useSubmit from "hooks/useSubmit"
import useToast from "hooks/useToast"
import fetcher from "utils/fetcher"

const verifyCaptcha = ({ callback, token }: { callback: string; token: string }) =>
fetcher(callback, {
const verifyCaptcha = (
{ callback, token }: { callback: string; token: string } = {
callback: "",
token: "",
}
) => {
if (!callback) throw new Error("Invalid or missing callback")
if (!token) throw new Error("Invalid or missing token")

return fetcher(callback, {
body: {
token,
},
})
}

const useVerifyCaptcha = (onSuccess?: () => void) => {
const showErrorToast = useShowErrorToast()
const toast = useToast()
const errorToast = useErrorToast()
const { toast } = useToast()

return useSubmit(verifyCaptcha, {
onError: (error) => {
const errorMsg = "Couldn't verify CAPTCHA"
const correlationId = error.correlationId
showErrorToast(correlationId ? { error: errorMsg, correlationId } : errorMsg)
errorToast(correlationId ? { error: errorMsg, correlationId } : errorMsg)
},
onSuccess: () => {
onSuccess?.()
toast({
status: "success",
variant: "success",
title: "Successful verification",
})
},
Expand Down

0 comments on commit a82d243

Please sign in to comment.