@@ -8,6 +8,8 @@ import { OtpType, TurnkeyError, TurnkeyErrorCodes } from "@turnkey/core";
88import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
99import { faEnvelope , faPhone } from "@fortawesome/free-solid-svg-icons" ;
1010import clsx from "clsx" ;
11+ import { Turnstile , type TurnstileInstance } from "@marsidev/react-turnstile" ;
12+ import { consumeCaptchaToken } from "../../utils/captcha" ;
1113
1214interface OtpVerificationProps {
1315 contact : string ;
@@ -17,7 +19,6 @@ interface OtpVerificationProps {
1719 alphanumeric ?: boolean | undefined ; // Whether the OTP is alphanumeric or numeric only. Defaults to true (alphanumeric).
1820 formattedContact ?: string ; // Optional formatted contact for display purposes
1921 sessionKey ?: string ; // Optional sessionKey for multisession
20- turnstileToken ?: string | null ; // Optional Turnstile token for CAPTCHA verification
2122 onContinue ?: ( optCode : string ) => Promise < void > ; // Optional callback for continue action
2223}
2324export function OtpVerification ( props : OtpVerificationProps ) {
@@ -28,10 +29,9 @@ export function OtpVerification(props: OtpVerificationProps) {
2829 alphanumeric = true ,
2930 formattedContact,
3031 sessionKey,
31- turnstileToken,
3232 onContinue = null , // Default to null if not provided
3333 } = props ;
34- const { initOtp, completeOtp } = useTurnkey ( ) ;
34+ const { initOtp, completeOtp, config , getTurnstileToken , setTurnstileToken } = useTurnkey ( ) ;
3535 const { closeModal, isMobile } = useModal ( ) ;
3636 const [ submitting , setSubmitting ] = useState < boolean > ( false ) ;
3737 const [ resending , setResending ] = useState < boolean > ( false ) ;
@@ -40,6 +40,12 @@ export function OtpVerification(props: OtpVerificationProps) {
4040 const [ error , setError ] = useState < string | null > ( null ) ;
4141 const [ shaking , setShaking ] = useState ( false ) ;
4242
43+ const turnstileRef = useRef < TurnstileInstance > ( null ) ;
44+ const [ showTurnstilePrompt , setShowTurnstilePrompt ] = useState ( false ) ;
45+
46+ const consumeToken = ( ) =>
47+ consumeCaptchaToken ( getTurnstileToken , setTurnstileToken , turnstileRef ) ;
48+
4349 const shakeInput = ( ) => {
4450 setShaking ( true ) ;
4551 setTimeout ( ( ) => setShaking ( false ) , 250 ) ;
@@ -57,7 +63,7 @@ export function OtpVerification(props: OtpVerificationProps) {
5763 contact,
5864 otpType,
5965 ...( sessionKey && { sessionKey } ) ,
60- ...( turnstileToken && { captchaToken : turnstileToken } ) , // Pass the Turnstile token if it exists
66+ ...( await consumeToken ( ) ) ,
6167 } ) ;
6268 closeModal ( ) ;
6369 }
@@ -80,8 +86,8 @@ export function OtpVerification(props: OtpVerificationProps) {
8086 const id = await initOtp ( {
8187 otpType,
8288 contact,
83- ...( turnstileToken && { captchaToken : turnstileToken } ) ,
84- } ) ; // Pass the Turnstile token if it exists
89+ ...( await consumeToken ( ) ) ,
90+ } ) ;
8591 setOtpId ( id ) ;
8692 setResent ( true ) ;
8793 } catch ( error ) {
@@ -94,7 +100,7 @@ export function OtpVerification(props: OtpVerificationProps) {
94100 return (
95101 < div
96102 className = { clsx (
97- "flex items-center justify-center py-3" ,
103+ "flex flex-col items-center justify-center py-3" ,
98104 isMobile ? "w-full" : "min-w-96" ,
99105 ) }
100106 >
@@ -154,6 +160,37 @@ export function OtpVerification(props: OtpVerificationProps) {
154160 < Spinner strokeWidth = { 1 } className = "size-1/2" />
155161 </ div >
156162 ) }
163+ { config ?. turnstileSiteKey && ! submitting && (
164+ < div className = "mt-3 flex flex-col text-left w-full" >
165+ { showTurnstilePrompt && (
166+ < p className = "text-icon-text-light/70 dark:text-icon-text-dark/70 text-sm mb-0.5" >
167+ Let us know you're human
168+ </ p >
169+ ) }
170+ < Turnstile
171+ ref = { turnstileRef }
172+ siteKey = { config . turnstileSiteKey }
173+ className = "!w-full !block [&>iframe]:!w-full"
174+ onSuccess = { ( token ) => {
175+ setTurnstileToken ( token ) ;
176+ } }
177+ onError = { ( ) => {
178+ setTurnstileToken ( null ) ;
179+ } }
180+ onExpire = { ( ) => {
181+ setTurnstileToken ( null ) ;
182+ } }
183+ onBeforeInteractive = { ( ) => {
184+ setShowTurnstilePrompt ( true ) ;
185+ } }
186+ options = { {
187+ theme : config . ui ?. darkMode ? "dark" : "light" ,
188+ appearance : "interaction-only" ,
189+ size : "flexible" ,
190+ } }
191+ />
192+ </ div >
193+ ) }
157194 </ div >
158195 ) ;
159196}
0 commit comments