diff --git a/src/components/common/Recaptcha/Recaptcha.tsx b/src/components/common/Recaptcha/Recaptcha.tsx index 3f53cd3..fc2e05c 100644 --- a/src/components/common/Recaptcha/Recaptcha.tsx +++ b/src/components/common/Recaptcha/Recaptcha.tsx @@ -32,13 +32,15 @@ interface RecaptchaProps { siteKey: string className?: string onChange?: (token: string) => void + onExpire?: () => void } export const Recaptcha = forwardRef( - function Recaptcha({ siteKey, className, onChange }, ref) { + function Recaptcha({ siteKey, className, onChange, onExpire }, ref) { const { containerRef, getToken, reset } = useRecaptcha({ siteKey, onChange, + onExpire, }) useImperativeHandle( diff --git a/src/hooks/useContactForm.ts b/src/hooks/useContactForm.ts index 6fd4810..0951d1d 100644 --- a/src/hooks/useContactForm.ts +++ b/src/hooks/useContactForm.ts @@ -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, @@ -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) @@ -390,9 +404,19 @@ export const useContactForm = (options: UseContactFormOptions) => { 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', @@ -425,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) }) }) @@ -468,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) @@ -512,6 +547,7 @@ export const useContactForm = (options: UseContactFormOptions) => { validateTypeOfEnquiry, validateDescription, clearRecaptchaError, + handleRecaptchaExpired, onSubmit, isFormValid, } diff --git a/src/index.css b/src/index.css index cb5f3d2..81c6845 100644 --- a/src/index.css +++ b/src/index.css @@ -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; diff --git a/src/pages/Contact/index.tsx b/src/pages/Contact/index.tsx index d318f00..88c40fa 100644 --- a/src/pages/Contact/index.tsx +++ b/src/pages/Contact/index.tsx @@ -44,6 +44,7 @@ const Contact = ({ isDarkMode }: ContactProps) => { handleDrop, handleFileInput, clearRecaptchaError, + handleRecaptchaExpired, onSubmit, } = useContactForm({ getRecaptchaToken: () => @@ -169,6 +170,7 @@ const Contact = ({ isDarkMode }: ContactProps) => { siteKey={RECAPTCHA_SITE_KEY} className="flex justify-center" onChange={clearRecaptchaError} + onExpire={handleRecaptchaExpired} /> {fieldErrors.recaptcha && (