Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(unlock-app): Migrate to next-captcha-v3 for reCAPTCHA handling #15405

Merged
merged 12 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions unlock-app/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ body {
#iframe-crossmint-payment-element {
width: 100%;
}

/* hide captcha badge */
.grecaptcha-badge {
visibility: hidden !important;
}
38 changes: 22 additions & 16 deletions unlock-app/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, { Suspense } from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReCaptchaProvider } from 'next-recaptcha-v3'

import { SessionProvider } from '~/hooks/useSession'
import { AirstackProvider } from '@airstack/airstack-react'
Expand All @@ -14,28 +15,33 @@ import { ConnectModalProvider } from '~/hooks/useConnectModal'
import Privy from '~/config/PrivyProvider'
import LoadingFallback from './Components/LoadingFallback'
import { queryClient } from '~/config/queryClient'
import { config } from '~/config/app'

export default function Providers({ children }: { children: React.ReactNode }) {
return (
<Privy>
<QueryClientProvider client={queryClient}>
<GlobalWrapper>
<SessionProvider>
<Suspense fallback={<LoadingFallback />}>
<ConnectModalProvider>
<AirstackProvider apiKey={'162b7c4dda5c44afdb0857b6b04454f99'}>
<ErrorBoundary
fallback={(props: any) => <ErrorFallback {...props} />}
<ReCaptchaProvider reCaptchaKey={config.recaptchaKey}>
<GlobalWrapper>
<SessionProvider>
<Suspense fallback={<LoadingFallback />}>
<ConnectModalProvider>
<AirstackProvider
apiKey={'162b7c4dda5c44afdb0857b6b04454f99'}
>
<ShouldOpenConnectModal />
{children}
</ErrorBoundary>
</AirstackProvider>
</ConnectModalProvider>
<Toaster />
</Suspense>
</SessionProvider>
</GlobalWrapper>
<ErrorBoundary
fallback={(props: any) => <ErrorFallback {...props} />}
>
<ShouldOpenConnectModal />
{children}
</ErrorBoundary>
</AirstackProvider>
</ConnectModalProvider>
<Toaster />
</Suspense>
</SessionProvider>
</GlobalWrapper>
</ReCaptchaProvider>
</QueryClientProvider>
</Privy>
)
Expand Down
3 changes: 1 addition & 2 deletions unlock-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"graphql": "16.9.0",
"lottie-react": "2.4.0",
"next": "14.2.21",
"next-recaptcha-v3": "1.5.2",
"node-forge": "1.3.1",
"p-limit": "6.2.0",
"postmate": "1.5.2",
Expand All @@ -52,7 +53,6 @@
"react-device-detect": "2.2.3",
"react-dropzone": "14.3.5",
"react-google-autocomplete": "2.7.3",
"react-google-recaptcha": "3.1.0",
"react-gtm-module": "2.0.11",
"react-hook-form": "7.53.2",
"react-hot-toast": "2.4.1",
Expand All @@ -79,7 +79,6 @@
"@types/postmate": "1.5.4",
"@types/qrcode.react": "1.0.5",
"@types/react": "18.3.18",
"@types/react-google-recaptcha": "2.1.9",
"@types/react-gtm-module": "2.0.4",
"@types/remove-markdown": "0.3.4",
"@unlock-protocol/tsconfig": "workspace:./packages/tsconfig",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useClaim } from '~/hooks/useClaim'
import { ethers } from 'ethers'

import ReCaptcha from 'react-google-recaptcha'

import { Button, Input, AddressInput } from '@unlock-protocol/ui'
import { Controller, useForm } from 'react-hook-form'
import { useEffect, useState } from 'react'
Expand All @@ -13,7 +11,7 @@ import { TransactionStatus } from '~/components/interface/checkout/main/checkout
import { onResolveName } from '~/utils/resolvers'
import { MetadataInputType } from '@unlock-protocol/core'
import { useRsvp } from '~/hooks/useRsvp'
import { useCaptcha } from '~/hooks/useCaptcha'
import { useReCaptcha } from 'next-recaptcha-v3'
import { useMutation } from '@tanstack/react-query'
import { useAuthenticate } from '~/hooks/useAuthenticate'

Expand Down Expand Up @@ -225,8 +223,8 @@ export const RegistrationForm = ({
captcha: string
}) => void
}) => {
const config = useConfig()
const { recaptchaRef, getCaptchaValue } = useCaptcha()
const { executeRecaptcha } = useReCaptcha()

const [loading, setLoading] = useState<boolean>(false)
const { account, email } = useAuthenticate()

Expand Down Expand Up @@ -260,7 +258,7 @@ export const RegistrationForm = ({
const onSubmit = async ({ recipient, ...data }: any) => {
setLoading(true)
try {
const captcha = await getCaptchaValue()
const captcha = await executeRecaptcha('submit')
await onRSVP({
recipient,
data,
Expand All @@ -283,13 +281,6 @@ export const RegistrationForm = ({
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col w-full gap-4"
>
<ReCaptcha
ref={recaptchaRef}
sitekey={config.recaptchaKey}
size="invisible"
badge="bottomleft"
/>

{/* TODO: delete me after May 1st 2024 once all new events use `metadataInputs` */}
{(!metadataInputs || metadataInputs.length === 0) && (
<>
Expand Down
101 changes: 83 additions & 18 deletions unlock-app/src/components/interface/checkout/main/Captcha.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { CheckoutService } from './checkoutMachine'
import { Button } from '@unlock-protocol/ui'
import React, { Fragment, useState } from 'react'
import { Button, Checkbox } from '@unlock-protocol/ui'
import { Fragment, useState } from 'react'
import { ToastHelper } from '~/components/helpers/toast.helper'
import ReCAPTCHA from 'react-google-recaptcha'
import { useConfig } from '~/utils/withConfig'
import { useSelector } from '@xstate/react'
import { PoweredByUnlock } from '../PoweredByUnlock'
import { Stepper } from '../Stepper'
import Disconnect from './Disconnect'
import { locksmith } from '~/config/locksmith'
import { useAuthenticate } from '~/hooks/useAuthenticate'
import { useReCaptcha } from 'next-recaptcha-v3'
import Link from 'next/link'
import { FaCheckCircle as CheckIcon } from 'react-icons/fa'

interface Props {
checkoutService: CheckoutService
Expand All @@ -20,21 +21,46 @@ export function Captcha({ checkoutService }: Props) {
checkoutService,
(state) => state.context
)
const config = useConfig()
const { account } = useAuthenticate()
const [recaptchaValue, setRecaptchaValue] = useState<string | null>(null)
const { executeRecaptcha } = useReCaptcha()
const [isChecked, setIsChecked] = useState(false)
const [isContinuing, setIsContinuing] = useState(false)
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const users = recipients.length > 0 ? recipients : [account!]

const handleCheckboxChange = async (checked: boolean) => {
if (recaptchaToken) {
return // Prevent unchecking after successful verification
}

setIsChecked(checked)
setError(null)

if (checked) {
try {
const token = await executeRecaptcha('captcha_checkout')
setRecaptchaToken(token)
} catch (error) {
setIsChecked(false)
setError('Failed to verify human status. Please try again.')
if (error instanceof Error) {
ToastHelper.error('Failed to verify human status. Please try again.')
}
}
}
}

const onContinue = async () => {
try {
setIsContinuing(true)
if (!recaptchaValue) {
if (!recaptchaToken) {
return
}

const response = await locksmith.getDataForRecipientsAndCaptcha(
users,
recaptchaValue!,
recaptchaToken,
lock!.address,
lock!.network
)
Expand All @@ -60,28 +86,67 @@ export function Captcha({ checkoutService }: Props) {
<Fragment>
<Stepper service={checkoutService} />
<main className="h-full px-6 py-2 overflow-auto">
<div className="flex justify-center">
<ReCAPTCHA
sitekey={config.recaptchaKey}
onChange={(token) => setRecaptchaValue(token)}
/>
<div className="flex flex-col items-center space-y-2 mt-5">
{recaptchaToken ? (
<div className="flex flex-col items-center gap-2">
<CheckIcon className="w-12 h-12 text-green-500" />
<span className="text-sm text-green-500">
Captcha verification passed
</span>
</div>
) : (
<div className="flex bg-gray-100 text-brand-ui-primary flex-col items-start pt-2 pb-1 px-2 rounded-md">
<Checkbox
fieldSize={'large'}
label="Solve captcha"
checked={isChecked}
disabled={!!recaptchaToken}
onChange={(e) => handleCheckboxChange(e.target.checked)}
/>
</div>
)}
{error && <p className="text-sm text-red-500">{error}</p>}
{!recaptchaToken && (
// Must-have text to display when hiding the captcha badge
<p className="text-center text-[10px]">
This site is protected by reCAPTCHA and the Google{' '}
<Link
target="_blank"
href="https://policies.google.com/privacy"
className="text-blue-500"
>
Privacy Policy
</Link>{' '}
and{' '}
<Link
target="_blank"
href="https://policies.google.com/terms"
className="text-blue-500"
>
Terms of Service
</Link>{' '}
apply.
</p>
)}
</div>
</main>
<footer className="grid items-center px-6 pt-6 border-t">
<Button
className="w-full"
disabled={!recaptchaValue || isContinuing}
disabled={!recaptchaToken || isContinuing}
loading={isContinuing}
onClick={(event) => {
event.preventDefault()
onContinue()
}}
>
{!recaptchaValue
{!isChecked
? 'Solve captcha to continue'
: isContinuing
? 'Continuing'
: 'Continue'}
: !recaptchaToken
? 'Verifying...'
: isContinuing
? 'Continuing'
: 'Continue'}
</Button>
<Disconnect service={checkoutService} />
<PoweredByUnlock />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import ReCaptcha from 'react-google-recaptcha'
import { CheckoutService } from './../checkoutMachine'
import { Button } from '@unlock-protocol/ui'
import { Fragment, useRef, useState } from 'react'
import { Fragment, useState } from 'react'
import { useSelector } from '@xstate/react'
import { PoweredByUnlock } from '../../PoweredByUnlock'
import { Pricing } from '../../Lock'
Expand All @@ -15,6 +14,7 @@ import { usePurchaseData } from '~/hooks/usePurchaseData'
import { useConfig } from '~/utils/withConfig'
import { PricingData } from './PricingData'
import Disconnect from '../Disconnect'
import { useReCaptcha } from 'next-recaptcha-v3'

interface Props {
checkoutService: CheckoutService
Expand All @@ -27,7 +27,7 @@ export function ConfirmClaim({ checkoutService, onConfirmed, onError }: Props) {
useSelector(checkoutService, (state) => state.context)
const config = useConfig()

const recaptchaRef = useRef<any>()
const { executeRecaptcha } = useReCaptcha()
const [isConfirming, setIsConfirming] = useState(false)

const { address: lockAddress, network: lockNetwork } = lock!
Expand Down Expand Up @@ -75,7 +75,7 @@ export function ConfirmClaim({ checkoutService, onConfirmed, onError }: Props) {

const onConfirmClaim = async () => {
setIsConfirming(true)
const captcha = await recaptchaRef.current?.executeAsync()
const captcha = await executeRecaptcha('claim')
const { hash } = await claim({
data: purchaseData?.[0],
captcha,
Expand All @@ -91,13 +91,6 @@ export function ConfirmClaim({ checkoutService, onConfirmed, onError }: Props) {

return (
<Fragment>
<ReCaptcha
ref={recaptchaRef}
sitekey={config.recaptchaKey}
size="invisible"
badge="bottomleft"
/>

<main className="h-full p-6 space-y-2 overflow-auto">
<div className="grid gap-y-2">
<h4 className="text-xl font-bold"> {lock!.name}</h4>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { CheckoutService } from './../checkoutMachine'
import { useConfig } from '~/utils/withConfig'
import { Button } from '@unlock-protocol/ui'
import { Fragment, useRef, useState } from 'react'
import { Fragment, useState } from 'react'
import { ToastHelper } from '~/components/helpers/toast.helper'
import { useSelector } from '@xstate/react'
import { PoweredByUnlock } from '../../PoweredByUnlock'
import { Pricing } from '../../Lock'
import { lockTickerSymbol } from '~/utils/checkoutLockUtils'
import { Lock } from '~/unlockTypes'
import ReCaptcha from 'react-google-recaptcha'
import { RiErrorWarningFill as ErrorIcon } from 'react-icons/ri'
import { useUpdateUsersMetadata } from '~/hooks/useUserMetadata'
import { usePricing } from '~/hooks/usePricing'
Expand Down Expand Up @@ -37,7 +36,7 @@ export function ConfirmCrossChainPurchase({
const { account } = useAuthenticate()
const { getWalletService } = useProvider()
const config = useConfig()
const recaptchaRef = useRef<any>()

const [isConfirming, setIsConfirming] = useState(false)

const { address: lockAddress, network: lockNetwork } = lock!
Expand Down Expand Up @@ -147,13 +146,6 @@ export function ConfirmCrossChainPurchase({

return (
<Fragment>
<ReCaptcha
ref={recaptchaRef}
sitekey={config.recaptchaKey}
size="invisible"
badge="bottomleft"
/>

<main className="h-full p-6 space-y-2 overflow-auto">
<div className="grid gap-y-2">
<h4 className="text-xl font-bold"> {lock!.name}</h4>
Expand Down
Loading
Loading