Promote dev → uat: GRO-2211/2218/2207 + GRO-2234 portal Book New (cumulative) (#56)
CI / Lint & Typecheck (push) Successful in 28s
CI / Test (push) Successful in 28s
CI / Build & Push Docker Image (push) Successful in 41s
CI / Test (pull_request) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 27s
CI / Build & Push Docker Image (pull_request) Successful in 47s
CI / Lint & Typecheck (push) Successful in 28s
CI / Test (push) Successful in 28s
CI / Build & Push Docker Image (push) Successful in 41s
CI / Test (pull_request) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 27s
CI / Build & Push Docker Image (pull_request) Successful in 47s
This commit was merged in pull request #56.
This commit is contained in:
@@ -8,6 +8,28 @@ import { ANALYTICS_EVENTS, fireAnalyticsEvent } from '../../lib/analytics';
|
||||
// responds with `{error: "..."}` on 4xx, and we must not treat that as slots.
|
||||
const AVAILABILITY_ERROR_MESSAGE = 'Failed to load time slots';
|
||||
|
||||
/**
|
||||
* Re-mint an SSO-bridge portal session from the active Better Auth session.
|
||||
* Defense-in-depth for GRO-2234: if a portal call returns 401 mid-flow (the
|
||||
* impersonation session lapsed during a slow wizard), the customer's Better
|
||||
* Auth cookie is still valid, so we can transparently obtain a fresh portal
|
||||
* session id and retry once. Returns the new session id, or null if no Better
|
||||
* Auth session is available (e.g. staff/dev impersonation paths).
|
||||
*/
|
||||
async function remintPortalSession(): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch('/api/portal/session-from-auth', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json().catch(() => ({}))) as { sessionId?: string };
|
||||
return data.sessionId ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAvailability(
|
||||
params: { serviceId: string; date: string },
|
||||
sessionId: string | null,
|
||||
@@ -993,26 +1015,40 @@ export function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/portal/waitlist', {
|
||||
const payload = JSON.stringify({
|
||||
petId: selectedPet.id,
|
||||
serviceId: selectedServices[0]?.id,
|
||||
serviceIds: selectedServices.map((s) => s.id),
|
||||
addOnIds: selectedAddOns.map((s) => s.id),
|
||||
groomerId: selectedGroomer === 'first-available' ? null : selectedGroomer,
|
||||
preferredDate: selectedDate,
|
||||
preferredTime: slotToTime(selectedTime),
|
||||
notes: notes || undefined,
|
||||
recurring: recurring || undefined,
|
||||
});
|
||||
const submitWaitlist = (id: string) =>
|
||||
fetch('/api/portal/waitlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Impersonation-Session-Id': sessionId ?? '',
|
||||
'X-Impersonation-Session-Id': id,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
petId: selectedPet.id,
|
||||
serviceId: selectedServices[0]?.id,
|
||||
serviceIds: selectedServices.map((s) => s.id),
|
||||
addOnIds: selectedAddOns.map((s) => s.id),
|
||||
groomerId: selectedGroomer === 'first-available' ? null : selectedGroomer,
|
||||
preferredDate: selectedDate,
|
||||
preferredTime: slotToTime(selectedTime),
|
||||
notes: notes || undefined,
|
||||
recurring: recurring || undefined,
|
||||
}),
|
||||
body: payload,
|
||||
});
|
||||
|
||||
try {
|
||||
let response = await submitWaitlist(sessionId);
|
||||
|
||||
// GRO-2234: a deliberately-paced wizard can outlive the portal session.
|
||||
// The customer's Better Auth session is still valid, so transparently
|
||||
// re-mint a fresh portal session and retry once before surfacing an error.
|
||||
if (response.status === 401) {
|
||||
const freshSessionId = await remintPortalSession();
|
||||
if (freshSessionId) {
|
||||
response = await submitWaitlist(freshSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
setConfirmed(true);
|
||||
fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_STEP_SUBMIT, { step: "submit", flow: "portal" });
|
||||
|
||||
Reference in New Issue
Block a user