feat(contact-us): Add hCaptcha verification to contact form

- Install @hcaptcha/react-hcaptcha dependency for CAPTCHA protection
- Add hCaptcha component integration with site key configuration
- Implement captcha token state management with verification callbacks
- Add token validation before form submission with user feedback
- Reset captcha after successful form submission
- Include captcha in form submission payload as hcaptcha_token
- Update Next.js to version 15.5.7 for compatibility
- Enhance form security by requiring CAPTCHA verification before contact submission
This commit is contained in:
AmirReza Jamali
2025-12-08 16:56:14 +03:30
parent 1204b2d3db
commit 2182386f9c
3 changed files with 102 additions and 43 deletions
+30 -2
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import api from "@/service/api"; import api from "@/service/api";
import { useState } from "react"; import HCaptcha from "@hcaptcha/react-hcaptcha";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import InputGroup from "../_components/InputGroup"; import InputGroup from "../_components/InputGroup";
@@ -22,18 +23,37 @@ const ContactUsForm = ({
columnsPerRow = 1, columnsPerRow = 1,
}: IProps) => { }: IProps) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [hcaptchaToken, setHcaptchaToken] = useState<string | null>(null);
const captchaRef = useRef<HCaptcha>(null);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset, reset,
} = useForm<TContactUsForm>(); } = useForm<TContactUsForm>();
const onCaptchaVerify = (token: string) => {
setHcaptchaToken(token);
};
const onCaptchaExpire = () => {
setHcaptchaToken(null);
};
const onSubmit = async (data: TContactUsForm) => { const onSubmit = async (data: TContactUsForm) => {
if (!hcaptchaToken) {
toast.error("Please complete the captcha verification");
return;
}
setIsLoading(() => true); setIsLoading(() => true);
try { try {
const response = (await api.post("contacts", { ...data })).data; const response = (
await api.post("contacts", { ...data, hcaptcha_token: hcaptchaToken })
).data;
toast.success(response.message); toast.success(response.message);
reset(); reset();
setHcaptchaToken(null);
captchaRef.current?.resetCaptcha();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
toast.error("Something went wrong"); toast.error("Something went wrong");
@@ -143,6 +163,14 @@ const ContactUsForm = ({
}, },
}} }}
/> />
<div className={`${colSpanClass}`}>
<HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY || ""}
onVerify={onCaptchaVerify}
onExpire={onCaptchaExpire}
ref={captchaRef}
/>
</div>
<div className={`${colSpanClass} flex justify-end`}> <div className={`${colSpanClass} flex justify-end`}>
<button className="bg-(--primary) cursor-pointer w-fit text-white rounded-full py-4 px-6 flex justify-center items-center text-center"> <button className="bg-(--primary) cursor-pointer w-fit text-white rounded-full py-4 px-6 flex justify-center items-center text-center">
{isLoading ? <Loading /> : buttonText} {isLoading ? <Loading /> : buttonText}
+70 -40
View File
@@ -8,13 +8,14 @@
"name": "tm-landing", "name": "tm-landing",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@hcaptcha/react-hcaptcha": "^1.16.0",
"@tanstack/react-query": "^5.90.7", "@tanstack/react-query": "^5.90.7",
"@types/dompurify": "^3.2.0", "@types/dompurify": "^3.2.0",
"apexcharts": "^5.3.5", "apexcharts": "^5.3.5",
"axios": "^1.12.2", "axios": "^1.12.2",
"dompurify": "^3.3.0", "dompurify": "^3.3.0",
"firebase": "^12.6.0", "firebase": "^12.6.0",
"next": "15.5.5", "next": "^15.5.7",
"react": "19.1.0", "react": "19.1.0",
"react-apexcharts": "^1.8.0", "react-apexcharts": "^1.8.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@@ -43,6 +44,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.5.0.tgz", "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.5.0.tgz",
@@ -692,6 +702,26 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@hcaptcha/loader": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/@hcaptcha/loader/-/loader-2.3.0.tgz",
"integrity": "sha512-i4lnNxKBe+COf3R1nFZEWaZoHIoJjvDgWqvcNrdZq8ehoSNMN6KVZ56dcQ02qKie2h3+BkbkwlJA9DOIuLlK/g==",
"license": "MIT"
},
"node_modules/@hcaptcha/react-hcaptcha": {
"version": "1.16.0",
"resolved": "https://registry.npmmirror.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.16.0.tgz",
"integrity": "sha512-ALRsAwQDCtGeR9QXrLjf7IQsWlaIg1dQWpPYdBR03KHkUrLgRjmqLKkOXaq8L+0qcicXRasHRuvXp2Vinun9aw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.17.9",
"@hcaptcha/loader": "^2.3.0"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/@img/colour": { "node_modules/@img/colour": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz",
@@ -1184,15 +1214,15 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/env/-/env-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/env/-/env-15.5.7.tgz",
"integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==", "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
"integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==", "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1206,9 +1236,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
"integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==", "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1222,9 +1252,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
"integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==", "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1238,9 +1268,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
"integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==", "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1254,9 +1284,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
"integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==", "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1270,9 +1300,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
"integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==", "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1286,9 +1316,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
"integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==", "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1302,9 +1332,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
"integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==", "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2659,12 +2689,12 @@
} }
}, },
"node_modules/next": { "node_modules/next": {
"version": "15.5.5", "version": "15.5.7",
"resolved": "https://registry.npmmirror.com/next/-/next-15.5.5.tgz", "resolved": "https://registry.npmmirror.com/next/-/next-15.5.7.tgz",
"integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==", "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "15.5.5", "@next/env": "15.5.7",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31", "postcss": "8.4.31",
@@ -2677,14 +2707,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0" "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.5", "@next/swc-darwin-arm64": "15.5.7",
"@next/swc-darwin-x64": "15.5.5", "@next/swc-darwin-x64": "15.5.7",
"@next/swc-linux-arm64-gnu": "15.5.5", "@next/swc-linux-arm64-gnu": "15.5.7",
"@next/swc-linux-arm64-musl": "15.5.5", "@next/swc-linux-arm64-musl": "15.5.7",
"@next/swc-linux-x64-gnu": "15.5.5", "@next/swc-linux-x64-gnu": "15.5.7",
"@next/swc-linux-x64-musl": "15.5.5", "@next/swc-linux-x64-musl": "15.5.7",
"@next/swc-win32-arm64-msvc": "15.5.5", "@next/swc-win32-arm64-msvc": "15.5.7",
"@next/swc-win32-x64-msvc": "15.5.5", "@next/swc-win32-x64-msvc": "15.5.7",
"sharp": "^0.34.3" "sharp": "^0.34.3"
}, },
"peerDependencies": { "peerDependencies": {
+2 -1
View File
@@ -8,13 +8,14 @@
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"@hcaptcha/react-hcaptcha": "^1.16.0",
"@tanstack/react-query": "^5.90.7", "@tanstack/react-query": "^5.90.7",
"@types/dompurify": "^3.2.0", "@types/dompurify": "^3.2.0",
"apexcharts": "^5.3.5", "apexcharts": "^5.3.5",
"axios": "^1.12.2", "axios": "^1.12.2",
"dompurify": "^3.3.0", "dompurify": "^3.3.0",
"firebase": "^12.6.0", "firebase": "^12.6.0",
"next": "15.5.5", "next": "^15.5.7",
"react": "19.1.0", "react": "19.1.0",
"react-apexcharts": "^1.8.0", "react-apexcharts": "^1.8.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",