diff --git a/estore/src/components/CartDrawer.tsx b/estore/src/components/CartDrawer.tsx index 0c0668f..69126c2 100644 --- a/estore/src/components/CartDrawer.tsx +++ b/estore/src/components/CartDrawer.tsx @@ -150,12 +150,29 @@ export default function CartDrawer() { const [infoErrors, setInfoErrors] = useState<{ firstName?: string; lastName?: string; email?: string; phone?: string; balloon?: string }>({}) const [balloonAgreement, setBalloonAgreement] = useState(false) + const [deliveryStepError, setDeliveryStepError] = useState('') const isValidEmail = (v: string) => /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/.test(v.trim()) const isValidPhone = (v: string) => v.replace(/\D/g, '').length === 10 + const goToDeliveryWithError = (msg: string) => { + setDeliveryStepError(msg) + setStep('delivery') + } + const validateAndContinue = () => { + // If the delivery slot was cleared after the user moved past the delivery step + // (e.g. a high-rate item was removed), send them back to re-select. + if (effectiveFulfillment === 'delivery' && !deliverySlot) { + goToDeliveryWithError('Please select a delivery date and time to continue.') + return + } + if (effectiveFulfillment === 'pickup' && !pickupSlot) { + setStep('delivery') + return + } + const errors: typeof infoErrors = {} if (!custFirst.trim()) errors.firstName = 'First name is required' if (!custLast.trim()) errors.lastName = 'Last name is required' @@ -172,6 +189,10 @@ export default function CartDrawer() { } } + const handleCheckoutError = (msg: string) => { + goToDeliveryWithError(msg) + } + const fullAddress = [street, city, state, zip].filter(Boolean).join(', ') const canQuote = street.trim() && city.trim() const pickupSlots = useMemo(() => getPickupSlots(pickupDate, hoursConfig), [pickupDate, hoursConfig]) @@ -592,7 +613,7 @@ export default function CartDrawer() { address={fullAddress} tier={quote.tier} value={deliverySlot} - onChange={setDeliverySlot} + onChange={(v) => { setDeliverySlot(v); if (v) setDeliveryStepError('') }} />
@@ -612,6 +633,11 @@ export default function CartDrawer() { const deliveryFooter = ( <> + {deliveryStepError && ( +

+ {deliveryStepError} +

+ )} {effectiveFulfillment === 'delivery' && (

Delivery fee is based on driving distance from our shop. @@ -941,7 +967,7 @@ export default function CartDrawer() { {bodyContent[step]} {/* PaymentForm stays mounted to prevent Square DOM errors on step changes */}

- +
)} diff --git a/estore/src/components/PaymentForm.tsx b/estore/src/components/PaymentForm.tsx index 4eee3a0..2be9049 100644 --- a/estore/src/components/PaymentForm.tsx +++ b/estore/src/components/PaymentForm.tsx @@ -55,6 +55,7 @@ export interface CheckoutPayload { interface Props { payload: CheckoutPayload onSuccess: (orderId: string, shortRef: string) => void + onError?: (message: string, status: number) => void active: boolean // true only when the payment step is visible } @@ -63,7 +64,36 @@ const SDK_URL = ? 'https://web.squarecdn.com/v1/square.js' : 'https://sandbox.web.squarecdn.com/v1/square.js' -export default function PaymentForm({ payload, onSuccess, active }: Props) { +function buildMailtoLink(payload: CheckoutPayload): string { + const { fmt: fmtCents } = { fmt: (c: number) => `$${(c / 100).toFixed(2)}` } + const items = payload.lineItems + .map((li) => ` • ${li.quantity}× ${li.name} — ${fmtCents(li.priceCents * li.quantity)}${li.note ? ` (${li.note})` : ''}`) + .join('\n') + const fulfillment = payload.deliverySlotISO + ? `Delivery: ${new Date(payload.deliverySlotISO).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })} to ${payload.deliveryAddress ?? ''}` + : payload.pickupSlotISO + ? `Pickup: ${new Date(payload.pickupSlotISO).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })}` + : 'Fulfillment: not set' + const body = [ + `Hi Beach Party Balloons,`, + ``, + `I tried to place an order online but ran into a payment error. Could you please send me an invoice?`, + ``, + `Name: ${payload.customerFirstName} ${payload.customerLastName}`, + `Email: ${payload.customerEmail}`, + `Phone: ${payload.customerPhone}`, + ``, + `Order:`, + items, + ``, + fulfillment, + `Total: ${fmtCents(payload.grandTotal)}`, + ].join('\n') + + return `mailto:info@beachpartyballoons.com?subject=${encodeURIComponent('Online Order — Invoice Request')}&body=${encodeURIComponent(body)}` +} + +export default function PaymentForm({ payload, onSuccess, onError, active }: Props) { const appId = process.env.NEXT_PUBLIC_SQUARE_APP_ID ?? '' const locationId = process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID ?? '' @@ -183,8 +213,13 @@ export default function PaymentForm({ payload, onSuccess, active }: Props) { const data = await res.json() if (!res.ok || !data.success) { - setError(data.error ?? 'Checkout failed — please try again or contact us.') + const msg = data.error ?? 'Checkout failed — please try again or contact us.' if (res.status !== 409) console.error('[checkout] response:', data) + if (res.status === 400 && onError) { + onError(msg, res.status) + return + } + setError(msg) return } @@ -236,11 +271,20 @@ export default function PaymentForm({ payload, onSuccess, active }: Props) { )} {error && ( -
- {error} +
+

+ {error} +

+

+ If this keeps happening, you can{' '} + + email us your order details + + {' '}and we'll send you an invoice. +

)} diff --git a/estore/src/lib/square.ts b/estore/src/lib/square.ts index 2d89153..83347c7 100644 --- a/estore/src/lib/square.ts +++ b/estore/src/lib/square.ts @@ -280,7 +280,7 @@ export async function createSquareOrder(params: { phoneNumber: params.fulfillment.recipientPhone, address: { addressLine1: params.fulfillment.addressLine1 }, }, - scheduleType: params.fulfillment.deliverAt ? 'SCHEDULED' : 'ASAP', + scheduleType: 'SCHEDULED', ...(params.fulfillment.deliverAt ? { deliverAt: params.fulfillment.deliverAt } : {}), ...(params.fulfillment.deliveryWindowDuration ? { deliveryWindowDuration: params.fulfillment.deliveryWindowDuration } : {}), ...(params.fulfillment.note ? { note: params.fulfillment.note } : {}),