@@ -25,6 +25,7 @@ import Layout from '../MembershipLayout';
2525import { IPublicClientApplication , AccountInfo } from '@azure/msal-browser' ;
2626import { getUserAccessToken , initMsalClient } from '@/utils/msal' ;
2727import { syncIdentity } from '@/utils/api' ;
28+ import { Turnstile } from '@marsidev/react-turnstile' ;
2829
2930const decimalHelper = ( num : number ) => {
3031 if ( Number . isInteger ( num ) ) {
@@ -47,7 +48,11 @@ enum InputStatus {
4748}
4849
4950const baseUrl = process . env . NEXT_PUBLIC_MERCH_API_BASE_URL ;
50- const coreBaseUrl = process . env . NEXT_PUBLIC_EVENTS_API_BASE_URL ;
51+ const coreBaseUrl = process . env . NEXT_PUBLIC_CORE_API_BASE_URL ;
52+ const turnstileSiteKey = process . env . NEXT_PUBLIC_TURNSTILE_SITE_KEY ;
53+ if ( ! turnstileSiteKey ) {
54+ throw new Error ( "Turnstile site key missing." )
55+ }
5156
5257const WrappedMerchItem = ( ) => {
5358 return (
@@ -61,6 +66,7 @@ const MerchItem = () => {
6166 const itemid = useSearchParams ( ) . get ( 'id' ) || '' ;
6267 const [ merchList , setMerchList ] = useState < Record < string , any > > ( { } ) ;
6368 const [ pca , setPca ] = useState < IPublicClientApplication | null > ( null ) ;
69+ const [ token , setToken ] = React . useState < string > ( )
6470
6571 // Form State
6672 const [ email , setEmail ] = useState ( '' ) ;
@@ -79,6 +85,14 @@ const MerchItem = () => {
7985
8086 const modalErrorMessage = useDisclosure ( ) ;
8187 const [ errorMessage , setErrorMessage ] = useState < ErrorCode | null > ( null ) ;
88+ const clearTurnstileToken = ( ) => setToken ( undefined ) ;
89+ const turnstileWidget = ( id : string ) => < Turnstile
90+ id = { id }
91+ siteKey = { turnstileSiteKey }
92+ onSuccess = { setToken }
93+ onExpire = { clearTurnstileToken }
94+ onError = { clearTurnstileToken }
95+ /> ;
8296
8397 useEffect ( ( ) => {
8498 ( async ( ) => {
@@ -243,27 +257,48 @@ const MerchItem = () => {
243257 if ( ! pca ) {
244258 setErrorMessage ( { code : 403 , message : 'Authentication service is not initialized.' } ) ;
245259 modalErrorMessage . onOpen ( ) ;
260+ setIsLoading ( false ) ;
261+ return ;
262+ }
263+
264+ if ( ! token ) {
265+ setErrorMessage ( { code : 400 , message : 'Please complete the security verification.' } ) ;
266+ modalErrorMessage . onOpen ( ) ;
267+ setIsLoading ( false ) ;
246268 return ;
247269 }
248270
249271 const accessToken = await getUserAccessToken ( pca ) ;
250272 if ( ! accessToken ) {
251273 setErrorMessage ( { code : 403 , message : 'Failed to retrieve authentication token.' } ) ;
252274 modalErrorMessage . onOpen ( ) ;
275+ setIsLoading ( false ) ;
253276 return ;
254277 }
255278
256279 await syncIdentity ( accessToken ) ;
257280
258281 const url = `${ baseUrl } /api/v1/checkout/session?itemid=${ itemid } &size=${ size } &quantity=${ quantity } ` ;
259- axios . get ( url , { headers : { 'x-uiuc-token' : accessToken } } )
282+ axios . get ( url , {
283+ headers : {
284+ 'x-uiuc-token' : accessToken ,
285+ 'x-turnstile-token' : token ,
286+ }
287+ } )
260288 . then ( response => window . location . replace ( response . data ) )
261289 . catch ( handleApiError ) ;
262290 } ;
263291
264292 const purchaseHandler = async ( ) => {
265293 setIsLoading ( true ) ;
266294
295+ if ( ! token ) {
296+ setErrorMessage ( { code : 400 , message : 'Please complete the security verification.' } ) ;
297+ modalErrorMessage . onOpen ( ) ;
298+ setIsLoading ( false ) ;
299+ return ;
300+ }
301+
267302 if ( selectedTab === 'illinois' ) {
268303 if ( ! pca || ! user ) {
269304 setErrorMessage ( { code : 403 , message : 'You must be logged in to purchase with Illinois Checkout.' } ) ;
@@ -275,7 +310,11 @@ const MerchItem = () => {
275310 } else { // Guest flow
276311 // Non-Illinois email, use guest checkout
277312 const url = `${ baseUrl } /api/v1/checkout/session?itemid=${ itemid } &size=${ size } &quantity=${ quantity } &email=${ email } ` ;
278- axios . get ( url ) // No auth token needed
313+ axios . get ( url , {
314+ headers : {
315+ 'x-turnstile-token' : token ,
316+ }
317+ } )
279318 . then ( response => window . location . replace ( response . data ) )
280319 . catch ( handleApiError ) ;
281320 }
@@ -431,6 +470,7 @@ const MerchItem = () => {
431470 color = { inputQuantityStatus === InputStatus . INVALID ? 'danger' : 'default' }
432471 errorMessage = { inputQuantityStatus === InputStatus . INVALID && 'Invalid Quantity' }
433472 />
473+ { turnstileWidget ( 'wid1' ) }
434474 < Button
435475 color = "primary" size = "lg"
436476 isDisabled = { ! isFormValidated || isLoading || totalCapacity ( ) === 0 }
@@ -479,6 +519,7 @@ const MerchItem = () => {
479519 color = { inputQuantityStatus === InputStatus . INVALID ? 'danger' : 'default' }
480520 errorMessage = { inputQuantityStatus === InputStatus . INVALID && 'Invalid Quantity' }
481521 />
522+ { turnstileWidget ( 'wid2' ) }
482523 < Button
483524 color = "primary" size = "lg"
484525 isDisabled = { ! isFormValidated || isLoading || totalCapacity ( ) === 0 }
0 commit comments