Skip to content
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
13 changes: 11 additions & 2 deletions src/components/common/Recaptcha/Recaptcha.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ declare global {
* reCAPTCHA v2 global helper (loaded from https://www.google.com/recaptcha/api.js)
*/
ready: (cb: () => void) => void
render: (container: HTMLElement, params: { sitekey: string }) => number
render: (
container: HTMLElement,
params: {
sitekey: string
callback?: (token: string) => void
'expired-callback'?: () => void
}
) => number
getResponse: (widgetId?: number) => string
reset: (widgetId?: number) => void
}
Expand All @@ -25,13 +32,15 @@ interface RecaptchaProps {
siteKey: string
className?: string
onChange?: (token: string) => void
onExpire?: () => void
}

export const Recaptcha = forwardRef<RecaptchaHandle, RecaptchaProps>(
function Recaptcha({ siteKey, className, onChange }, ref) {
function Recaptcha({ siteKey, className, onChange, onExpire }, ref) {
const { containerRef, getToken, reset } = useRecaptcha({
siteKey,
onChange,
onExpire,
})

useImperativeHandle(
Expand Down
52 changes: 40 additions & 12 deletions src/hooks/useContactForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
uploadToPresignedUrl,
createServiceRequestWithKeys,
} from '@/utils/upload'
import { captureAppException } from '@/lib/sentry'
import { FetchClientError } from '@/utils/fetchClient'
import {
MAX_TOTAL_UPLOAD_BYTES,
MAX_FILES,
Expand Down Expand Up @@ -116,6 +118,18 @@ export const useContactForm = (options: UseContactFormOptions) => {
setRecaptchaCompleted(true)
}, [])

const handleRecaptchaExpired = useCallback(() => {
setRecaptchaCompleted(false)
setFieldErrors(prev => ({
...prev,
recaptcha: 'reCAPTCHA verification expired. Please verify again.',
}))
captureAppException(new Error('reCAPTCHA verification expired'), {
source: 'app',
tags: { 'recaptcha.event': 'expired' },
})
}, [])

const addFiles = useCallback(
(newFiles: File[]) => {
const nonEmpty = newFiles.filter(file => file.size > 0)
Expand Down Expand Up @@ -381,26 +395,28 @@ export const useContactForm = (options: UseContactFormOptions) => {
size: a.file.size,
}))
const presigned = await getPresignedUrls(files)
const presignedByFilename = presigned.reduce<
Record<string, typeof presigned>
>((acc, current) => {
if (!acc[current.filename]) acc[current.filename] = []
acc[current.filename].push(current)
return acc
}, {})

const uploadResults = await Promise.allSettled(
pendingItems.map(item => {
const candidateList = presignedByFilename[item.file.name] || []
const p = candidateList.shift()
pendingItems.map((item, index) => {
const p = presigned[index]
if (!p) {
setAttachmentStatus(item.id, {
status: 'error',
error: 'No upload URL was returned for this file.',
})
return Promise.reject(
new Error('No upload URL was returned for this file.')
const error = new Error(
'No upload URL was returned for this file.'
)
captureAppException(error, {
source: 'support-api',
tags: { 'upload.stage': 'presign_mismatch' },
extra: {
pendingCount: pendingItems.length,
presignedCount: presigned.length,
missingIndex: index,
},
})
return Promise.reject(error)
}
setAttachmentStatus(item.id, {
status: 'uploading',
Expand Down Expand Up @@ -433,6 +449,10 @@ export const useContactForm = (options: UseContactFormOptions) => {
error:
err instanceof Error ? err.message : 'Upload failed',
})
captureAppException(err, {
source: 'support-api',
tags: { 'upload.stage': 'presigned_put' },
})
return Promise.reject(err)
})
})
Expand Down Expand Up @@ -476,6 +496,13 @@ export const useContactForm = (options: UseContactFormOptions) => {
const msg =
rawMessage && rawMessage !== 'Failed to fetch' ? rawMessage : fallback
trackSupportFormFailed(typeOfEnquiry || 'Unknown', msg)
// FetchClientError failures are already captured with request context in fetchClient.ts
if (!(err instanceof FetchClientError)) {
captureAppException(err, {
source: 'support-api',
tags: { 'contact.stage': 'submit' },
})
}
setSubmitError(msg)
} finally {
setIsSubmitting(false)
Expand Down Expand Up @@ -520,6 +547,7 @@ export const useContactForm = (options: UseContactFormOptions) => {
validateTypeOfEnquiry,
validateDescription,
clearRecaptchaError,
handleRecaptchaExpired,
onSubmit,
isFormValid,
}
Expand Down
15 changes: 13 additions & 2 deletions src/hooks/useRecaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,30 @@ const SCRIPT_ID = 'recaptcha-v2-script'
type UseRecaptchaOptions = {
siteKey: string | null | undefined
onChange?: (token: string) => void
onExpire?: () => void
}

export const useRecaptcha = ({ siteKey, onChange }: UseRecaptchaOptions) => {
export const useRecaptcha = ({
siteKey,
onChange,
onExpire,
}: UseRecaptchaOptions) => {
const containerRef = useRef<HTMLDivElement | null>(null)
const widgetIdRef = useRef<number | null>(null)
const siteKeyRef = useRef(siteKey)
const onChangeRef = useRef(onChange)
const onExpireRef = useRef(onExpire)

siteKeyRef.current = siteKey
onChangeRef.current = onChange
onExpireRef.current = onExpire

useEffect(() => {
if (!siteKeyRef.current) return

const ensureRendered = () => {
if (!containerRef.current || widgetIdRef.current !== null) return
if (!siteKeyRef.current) return
const grecaptcha = globalThis.window?.grecaptcha
if (!grecaptcha || typeof grecaptcha.render !== 'function') return

Expand All @@ -29,7 +37,10 @@ export const useRecaptcha = ({ siteKey, onChange }: UseRecaptchaOptions) => {
callback: (token: string) => {
onChangeRef.current?.(token)
},
} as unknown as { sitekey: string })
'expired-callback': () => {
onExpireRef.current?.()
},
})
}

const loadScript = () => {
Expand Down
5 changes: 5 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4331,6 +4331,11 @@ p.small {
height: 24px;
}

.connect-magiclink-button-icon {
width: 24px;
height: 24px;
}

.connect-metamask-button-text {
display: flex;
flex-direction: row;
Expand Down
2 changes: 2 additions & 0 deletions src/pages/Contact/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const Contact = ({ isDarkMode }: ContactProps) => {
handleDrop,
handleFileInput,
clearRecaptchaError,
handleRecaptchaExpired,
onSubmit,
} = useContactForm({
getRecaptchaToken: () =>
Expand Down Expand Up @@ -169,6 +170,7 @@ const Contact = ({ isDarkMode }: ContactProps) => {
siteKey={RECAPTCHA_SITE_KEY}
className="flex justify-center"
onChange={clearRecaptchaError}
onExpire={handleRecaptchaExpired}
/>
{fieldErrors.recaptcha && (
<FieldError
Expand Down
Loading