Skip to content

Commit ba635b7

Browse files
author
Herve Tribouilloy
committed
Moved cloudflare turnstile away from add to cart and signup in favour of a single turnstil when the widget is loaded
1 parent 1ea2c6d commit ba635b7

14 files changed

Lines changed: 68 additions & 110 deletions

File tree

vite_project/src/BookingSystemWidget.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export function BookingSystemWidget({ host }: Props) {
1717
}
1818

1919
return (
20-
<SystemStateProvider config={booking}>
21-
<UserStateProvider config={user}>
22-
<BookingSystemWrapper venueId={booking.venueId} />
23-
</UserStateProvider>
24-
</SystemStateProvider>
20+
<SystemStateProvider config={booking}>
21+
<UserStateProvider config={user}>
22+
<BookingSystemWrapper venueId={booking.venueId} />
23+
</UserStateProvider>
24+
</SystemStateProvider>
2525
);
2626
}

vite_project/src/components/BookingSystem.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import {EventDashboard} from "./event/EventDashboard.tsx";
77
import {BookingContextSummary} from "./event/Dashboard/BookingContextSummary.tsx";
88
import {useMediaQuery} from "../hooks/ui/useMediaQuery.tsx";
99
import {HostFilter} from "./event/Filter/HostFilter.tsx";
10+
import {Spinner} from "./global/Spinner.tsx";
1011

11-
export function BookingSystem() {
12+
interface Props {
13+
canStartBooking: boolean
14+
}
15+
16+
export function BookingSystem({canStartBooking}: Props) {
1217
const { visitIntent} = useVisitIntentState();
1318
const isMobile = useMediaQuery('(max-width: 768px)');
1419

20+
if (!canStartBooking) return <Spinner />
21+
1522
return (
1623
<div className="booking-system">
1724
{(visitIntent.weekIntent !== '' && visitIntent.eventTypeId !== '') && (<>

vite_project/src/components/BookingSystemWrapper.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@ import type {ConfigInfoState} from "../state/Config/type.ts";
88
import {VisitIntentStateProvider} from "../state/Intent/VisitIntentStateProvider.tsx";
99
import {BookingSystem} from "./BookingSystem.tsx";
1010
import {ConfigStateProvider} from "../state/Config/ConfigStateProvider.tsx";
11+
import {Turnstile} from "../security/Turnstile.tsx";
12+
import {useSystemState} from "../state/System/useSystemState.ts";
13+
import {useHumanVerification} from "../hooks/domain/useHumanVerification.tsx";
1114

1215
interface Props {
1316
venueId: string
1417
}
1518

1619
export function BookingSystemWrapper({venueId}: Props) {
1720
const { venue, venueError: venueError } = useVenue(venueId);
21+
const { cloudflareKey, isTurnstileEnabled } = useSystemState()
22+
const turnstileEnabled = isTurnstileEnabled();
23+
const { onToken, isHumanVerified } = useHumanVerification();
24+
const canStartBooking = isHumanVerified;
1825

1926
const {
2027
eventHosts,
@@ -38,6 +45,12 @@ export function BookingSystemWrapper({venueId}: Props) {
3845

3946
activity('config-load', 'Config Data',{venue, eventHosts, groups});
4047

48+
if (!turnstileEnabled) {
49+
activity('form-ready', 'Turnstile Disabled',{
50+
cloudflareKey
51+
}, 'warn');
52+
}
53+
4154
const config: ConfigInfoState = {
4255
venue,
4356
eventHosts,
@@ -47,7 +60,15 @@ export function BookingSystemWrapper({venueId}: Props) {
4760
return (
4861
<ConfigStateProvider config={config}>
4962
<VisitIntentStateProvider eventTypeGroups={config.eventTypeGroups} eventHosts={config.eventHosts}>
50-
<BookingSystem />
63+
<BookingSystem canStartBooking={canStartBooking} />
64+
{/* 2. The security gate */}
65+
{turnstileEnabled && (
66+
<Turnstile
67+
siteKey={cloudflareKey}
68+
containerId="booking-turnstile"
69+
onToken={onToken}
70+
/>
71+
)}
5172
</VisitIntentStateProvider>
5273
</ConfigStateProvider>
5374
);

vite_project/src/components/event/Dashboard/DayEvent/AddToCart.tsx

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import {useEffect, useState} from "react";
1+
import {useEffect} from "react";
22
import {getEventCartQty} from "../../../../domain/cart/cart.ts";
33
import {ErrorState} from "../../../global/ErrorState.tsx";
44
import {useUserState} from "../../../../state/User/useUserState.ts";
55
import {useEventState} from "../../../../state/Event/useEventState.ts";
66
import {useVisitIntentState} from "../../../../state/Intent/useVisitIntentState.ts";
77
import {useAddToCart} from "../../../../hooks/domain/useAddToCart.tsx";
88
import {useDashboardState} from "../../../../state/Dashboard/useDashboardState.ts";
9-
import {Turnstile} from "../../../../security/Turnstile.tsx";
10-
import {useSystemState} from "../../../../state/System/useSystemState.ts";
11-
import {activity} from "../../../../../activity";
129
import {UserState} from "../../../user-authentication/UserState.tsx";
13-
import {useHumanVerification} from "../../../../hooks/domain/useHumanVerification.tsx";
1410

1511
interface AddToCartProps {
1612
onRequireAuth: () => void
@@ -22,10 +18,6 @@ export function AddToCart({onRequireAuth}: AddToCartProps) {
2218
const { visitIntent } = useVisitIntentState();
2319
const { addToCart, loadingAddToCart, errorAddToCart } = useAddToCart();
2420
const { increaseVersionNumber, setLastBookedEventId } = useDashboardState();
25-
const [awaitingSecurity, setAwaitingSecurity] = useState(false);
26-
const { cloudflareKey, isTurnstileEnabled } = useSystemState();
27-
const turnstileEnabled = isTurnstileEnabled();
28-
const { token, onToken, isHumanVerified, requireVerification } = useHumanVerification();
2921

3022
const refreshDashboard = () => {
3123
increaseVersionNumber()
@@ -38,44 +30,29 @@ export function AddToCart({onRequireAuth}: AddToCartProps) {
3830
return;
3931
}
4032

41-
if (!isHumanVerified) {
42-
requireVerification()
43-
return;
44-
}
45-
4633
try {
4734
await addToCart({
4835
eventId: eventState.activeEventId,
4936
eventTypeId: visitIntent.eventTypeId,
5037
shampoo: eventState.shampoo ? 1 : 0,
51-
userId: user?.id || '',
52-
turnstileToken: token as string,
38+
userId: user?.id || ''
5339
});
5440

5541
refreshDashboard();
56-
setAwaitingSecurity(false);
5742
} catch (err) {
5843
console.error(err);
59-
setAwaitingSecurity(false);
6044
}
6145
};
6246

6347
const onAddClick = async () => {
6448
if (!user) {
65-
setAwaitingSecurity(true);
6649
onRequireAuth();
6750
return;
6851
}
6952

7053
await handleAdd();
7154
};
7255

73-
useEffect(() => {
74-
if (awaitingSecurity && token && user) {
75-
handleAdd();
76-
}
77-
}, [awaitingSecurity, token, user]);
78-
7956
const activeEventId = eventState.activeEventId;
8057

8158
const eventAlreadyInCart =
@@ -86,16 +63,10 @@ export function AddToCart({onRequireAuth}: AddToCartProps) {
8663
const canAttemptAdd =
8764
!!activeEventId &&
8865
!eventAlreadyInCart &&
89-
!loadingAddToCart &&
90-
(!turnstileEnabled || Boolean(token));
66+
!loadingAddToCart;
9167

9268
if (errorAddToCart) return <ErrorState />
9369

94-
activity('form-ready', 'Can submit',{
95-
turnstileEnabled,
96-
token
97-
});
98-
9970
return (
10071
<>
10172
{eventAlreadyInCart && (
@@ -109,13 +80,6 @@ export function AddToCart({onRequireAuth}: AddToCartProps) {
10980
>
11081
Book{loadingAddToCart && 'ing'} appointment+-
11182
</button>
112-
{isTurnstileEnabled() && (
113-
<Turnstile
114-
siteKey={cloudflareKey}
115-
containerId="booking-turnstile"
116-
onToken={onToken}
117-
/>
118-
)}
11983
<UserState />
12084
</>
12185
);

vite_project/src/components/user-authentication/SignUp.tsx

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import {useEffect, useState} from "react";
1+
import {useState} from "react";
22
import {useRegisterUser} from "../../hooks/domain/useRegisterUser.tsx";
33
import {Spinner} from "../global/Spinner.tsx";
4-
import {useSystemState} from "../../state/System/useSystemState.ts";
5-
import {activity} from "../../../activity";
6-
import {Turnstile} from "../../security/Turnstile.tsx";
7-
import {useHumanVerification} from "../../hooks/domain/useHumanVerification.tsx";
84

95
interface SignUpProps {
106
onSuccess?: () => void;
@@ -19,24 +15,10 @@ export const SignUp: React.FC<SignUpProps> = ({ onSuccess, onCancel }) => {
1915
confirmPassword: '',
2016
});
2117
const { register, loadingRegister } = useRegisterUser();
22-
const { cloudflareKey, isTurnstileEnabled } = useSystemState()
23-
const [awaitingSecurity, setAwaitingSecurity] = useState(false);
24-
2518
const [error, setError] = useState<string | null>(null);
2619
const [loading, setLoading] = useState(false);
2720

28-
const { token, onToken, isHumanVerified, requireVerification } = useHumanVerification();
29-
30-
const turnstileEnabled = isTurnstileEnabled();
31-
const canSubmit =
32-
status !== "loading" &&
33-
(!turnstileEnabled || Boolean(token));
34-
35-
useEffect(() => {
36-
if (awaitingSecurity && token) {
37-
submitSignUp()
38-
}
39-
}, [awaitingSecurity, token]);
21+
const canSubmit = status !== "loading";
4022

4123
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
4224
setValues(v => ({ ...v, [e.target.name]: e.target.value }));
@@ -49,12 +31,6 @@ export const SignUp: React.FC<SignUpProps> = ({ onSuccess, onCancel }) => {
4931

5032
async function submitSignUp() {
5133
setError(null);
52-
setAwaitingSecurity(true);
53-
54-
if (!isHumanVerified) {
55-
requireVerification()
56-
return;
57-
}
5834

5935
if (values.password !== values.confirmPassword) {
6036
setError('Passwords do not match');
@@ -81,20 +57,12 @@ export const SignUp: React.FC<SignUpProps> = ({ onSuccess, onCancel }) => {
8157
setError('Signup failed');
8258
} finally {
8359
setLoading(false);
84-
setAwaitingSecurity(false);
8560
}
8661
}
8762

88-
if (!turnstileEnabled) {
89-
activity('form-ready', 'Turnstile Disabled',{
90-
cloudflareKey
91-
}, 'warn');
92-
}
93-
9463
if (loadingRegister) return <Spinner />
9564

9665
return (
97-
<>
9866
<form className="widget-form" onSubmit={handleSubmit}>
9967
<h2>Sign Up</h2>
10068

@@ -162,14 +130,5 @@ export const SignUp: React.FC<SignUpProps> = ({ onSuccess, onCancel }) => {
162130
)}
163131
</fieldset>
164132
</form>
165-
{/* 2. The security gate */}
166-
{turnstileEnabled && (
167-
<Turnstile
168-
siteKey={cloudflareKey}
169-
containerId="booking-turnstile"
170-
onToken={onToken}
171-
/>
172-
)}
173-
</>
174133
);
175134
};

vite_project/src/domain/user/authentication.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,18 @@ export async function loginWithGoogle(config: UserConfig) {
5454

5555
activity('login', 'Google Login');
5656
window.location.href = `${config.auth}/auth/google?returnTo=${returnTo}`;
57+
}
58+
59+
export async function verifyUser(config: UserConfig, token: string) {
60+
const res = await fetch(`${config.auth}/security/verify-human`, {
61+
method: 'POST',
62+
headers: { 'Content-Type': 'application/json' },
63+
body: JSON.stringify({ token }),
64+
});
65+
66+
activity('verifyUser', 'Verify User', res);
67+
68+
if (!res.ok) {
69+
throw new Error('Logout failed');
70+
}
5771
}

vite_project/src/hooks/domain/useHumanVerification.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@ export function useHumanVerification() {
2828

2929
useEffect(() => {
3030
const onSuccess = (e: CustomEvent) => {
31-
setToken(e.detail.token);
32-
setVerifiedAt(Date.now());
31+
onToken(e.detail.token);
3332
};
3433

3534
const onExpired = () => {
36-
setToken(null);
37-
setVerifiedAt(null);
35+
onToken(null);
3836
};
3937

4038
window.addEventListener("booking:security-success", onSuccess as EventListener);

vite_project/src/hooks/domain/useTurnstileVerification.tsx

Whitespace-only changes.

vite_project/src/hooks/infra/useKeystoneAddToCart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type {KeystoneCartEventParams} from "../../types/infra/keystone";
33
import {useSystemState} from "../../state/System/useSystemState.ts";
44

55
const MUTATION = `
6-
mutation AddToCart($eventId: ID!, $eventTypeId: ID!, $shampoo: Int, $userId: ID!, $turnstileToken: String!) {
7-
addToCart(eventId: $eventId, eventTypeId: $eventTypeId, shampoo: $shampoo, userId: $userId, turnstileToken: $turnstileToken)
6+
mutation AddToCart($eventId: ID!, $eventTypeId: ID!, $shampoo: Int, $userId: ID!) {
7+
addToCart(eventId: $eventId, eventTypeId: $eventTypeId, shampoo: $shampoo, userId: $userId)
88
}
99
`;
1010

vite_project/src/security/Turnstile.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useEffect, useRef } from "react";
2-
import {ensureTurnstileLoaded} from "./turnstileService.ts";
32
import {activity} from "../../activity";
4-
3+
import {ensureTurnstileLoaded} from "./turnstileService.ts";
4+
import {verifyUser} from "../domain/user/authentication.ts";
5+
import {useUserState} from "../state/User/useUserState.ts";
56

67
type TurnstileProps = {
78
siteKey: string;
@@ -11,6 +12,7 @@ type TurnstileProps = {
1112

1213
export function Turnstile({ siteKey, onToken, containerId }: TurnstileProps) {
1314
const widgetId = useRef<string | null>(null);
15+
const { config } = useUserState()
1416

1517
useEffect(() => {
1618
let cancelled = false;
@@ -30,7 +32,10 @@ export function Turnstile({ siteKey, onToken, containerId }: TurnstileProps) {
3032

3133
widgetId.current = window.turnstile.render(container, {
3234
sitekey: siteKey,
33-
callback: onToken,
35+
callback: (token: string) => {
36+
verifyUser(config, token)
37+
onToken(token)
38+
},
3439
"expired-callback": () => onToken(null),
3540
});
3641
});

0 commit comments

Comments
 (0)