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}
+
)}
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 } : {}),