@swift-food-services/catering-widget
v0.2.17
Published
Swift Food catering widget for partner sites
Downloads
2,678
Readme
@swift-food-services/catering-widget
The Swift Food catering flow, embeddable in any React app. Restaurant browsing, multi-session order building, contact details, and submission — one component, one CSS file, one publishable key.
npm install @swift-food-services/catering-widgetQuick start
import { CateringWidget } from "@swift-food-services/catering-widget";
export default function CateringPage() {
return (
<CateringWidget
publishableKey="pk_live_..."
googleMapsApiKey="AIzaSy..."
onOrderComplete={({ orderId }) => {
// Fires after the success-screen countdown (default 15s,
// configurable via `onOrderCompleteDelaySeconds`).
window.location.href = `/thanks?order=${orderId}`;
}}
/>
);
}Defaults: Swift-hosted backend, localStorage persistence, neutral theme.
Requirements
- React 18+ and React-DOM 18+ as peer dependencies
- A publishable key from Swift (see Getting a publishable key)
Props
| Prop | Type | Required | Description |
|---|---|---|---|
| publishableKey | string | yes | Your pk_live_... key from Swift. |
| partnerSlug | string | no | First-party only. Renders and attributes the session to a different partner than the one owning the key. Requires a key Swift has explicitly permitted to delegate; any other key is rejected with slug_delegation_not_permitted. If you are integrating the widget on your own site, identify yourself with publishableKey alone and omit this. |
| googleMapsApiKey | string | yes | Google Maps JavaScript API key with the Places library enabled. Used by the address-autocomplete field. |
| theme | Theme | no | Primary color, border radius, and font overrides. |
| initialData | InitialData | no | Pre-populate event window, address, and contact fields. Re-applied when its fingerprint changes between mounts; preserved on reload. See Pre-filling known event details. |
| allowedCateringTimes | AllowedCateringTimes | no | Partner-level catering availability window ({ start: "HH:MM", end: "HH:MM" }). Restricts the session delivery-time picker to slots within this range. |
| stickyTopOffset | number | no | Pixels to offset the widget's internal sticky date/session nav from the top of the viewport. Set this to the height of any sticky/fixed navbar in your host layout so the widget's nav doesn't slide underneath it. Defaults to 0. |
| onReady | () => void | no | Fires when the widget has initialized. |
| onOrderComplete | (result: OrderCompleteResult) => void | no | Fires after a successful order submission. See onOrderComplete behaviour for the timing. |
| onOrderCompleteDelaySeconds | number | no | Seconds to keep the success screen visible before firing onOrderComplete. Defaults to 15. Ignored if onOrderComplete is not provided. |
| onError | (error: WidgetError) => void | no | Fires on unrecoverable errors (bad key, network, submit failure). |
Examples
Next.js with router-based navigation
"use client";
import { CateringWidget } from "@swift-food-services/catering-widget";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<CateringWidget
publishableKey={process.env.NEXT_PUBLIC_SWIFT_PUBLISHABLE_KEY!}
googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}
onOrderComplete={({ orderId, accessToken }) => {
router.push(`/orders/${orderId}?token=${accessToken}`);
}}
/>
);
}Theming
<CateringWidget
publishableKey="pk_live_..."
theme={{
primary: "#0a7ea4",
radius: "12px",
font: "\"IBM Plex Mono\", monospace",
}}
/>Pre-filling known event details
<CateringWidget
publishableKey="pk_live_..."
initialData={{
eventName: "Alice's Birthday",
eventStartDate: "2026-07-12",
eventStartTime: "18:00",
eventEndDate: "2026-07-12",
eventEndTime: "22:00",
guestCount: 40,
deliveryAddress: {
line1: "1 Example Lane",
city: "London",
postcode: "E1 6AN",
lat: 51.5074,
lng: -0.1278,
},
contact: { name: "Alice", email: "[email protected]" },
}}
onOrderComplete={/* ... */}
/>Every field in initialData is optional. Fields remain editable after pre-fill.
How initialData interacts with persisted in-progress orders: the widget fingerprints the supplied initialData (event window, guest count, address, and contact fields) and stores the fingerprint alongside the order. On every mount it compares the incoming fingerprint to the stored one:
- Match (e.g. page reload) → the in-progress order is preserved;
initialDatais not re-applied. - Differ (e.g. customer navigated back to your booking page, edited details, then returned) → the stored order is cleared and
initialDatais applied fresh. - No fingerprint stored yet (first visit) →
initialDatais applied and the fingerprint is recorded.
For this to behave predictably, make sure the values you pass for the same booking are stable across renders — avoid recomputing dynamic values like new Date() inline in render.
The event window is a start–end pair (eventStartDate + eventStartTime to eventEndDate + eventEndTime). When set, the session editor restricts session dates to that range and filters the delivery-time picker so it stays within the window on the boundary days.
Address requires lat and lng. Delivery pricing depends on coordinates, so if you pass a deliveryAddress without both lat and lng, the widget ignores the entire address (no fields are pre-filled) and the guest is asked to pick one via the address-autocomplete field instead. The other initialData fields (event window, guest count, contact) still apply.
Restricting catering hours (allowedCateringTimes)
Pass a partner-level catering availability window to restrict the session delivery-time picker to a subset of the day. Times are 24-hour "HH:MM" strings.
<CateringWidget
publishableKey="pk_live_..."
allowedCateringTimes={{ start: "09:00", end: "19:00" }}
/>Each slot is a 30-minute window. A slot is shown only if it fits entirely within [start, end] — its start must be ≥ start and its end must be ≤ end. With end: "19:00", for example, the latest visible slot is 6:30 PM – 7:00 PM. If initialData also defines an event window, the two filters compose: the partner window is the outer bound, and the event start/end further narrow the picker on boundary days.
Offsetting below a host navbar
If your page has its own sticky/fixed navbar, pass its height as stickyTopOffset so the widget's internal date/session nav pins just below it:
<CateringWidget
publishableKey="pk_live_..."
stickyTopOffset={64}
/>Pass a live value if your navbar resizes (measure it with a ref + ResizeObserver and pass the current height). The prop is read on every render, so updates take effect immediately.
Types
import {
CateringWidget,
type CateringWidgetProps,
type Theme,
type InitialData,
type AllowedCateringTimes,
type OrderCompleteResult,
type OrderSummary,
type MealSession,
type MealSessionItem,
type WidgetError,
} from "@swift-food-services/catering-widget";See the generated dist/index.d.ts for the full shapes — your IDE picks them up automatically.
Callbacks
onOrderComplete(result)
Fires after a successful order submission. result contains:
orderId: string— Swift's order ID.accessToken: string— a token the customer can use to view the order on Swift's hosted view page.summary: OrderSummary— structured breakdown of the submitted order.
Timing. When onOrderComplete is wired, the widget shows its built-in success screen with a "Continuing in N seconds…" countdown for onOrderCompleteDelaySeconds seconds (default 15), then fires the callback. This gives the guest a moment to read the confirmation before the host navigates away. A "Continue now" button under the countdown lets the guest skip the wait — clicking it cancels the timer and fires onOrderComplete immediately.
If onOrderComplete is not wired, the success screen stays up indefinitely. The success screen shows the event, customer, and pricing summary, and any navigation is the host's responsibility (do it from onOrderComplete).
Heads up: the widget does not navigate on its own. If your onOrderComplete runs without triggering a navigation (e.g. you only fire analytics or close a modal), the guest will stay on the widget's order-confirmation page even after the callback fires. To send them elsewhere, do it from inside onOrderComplete — router.push(...), window.location.href = ..., hide the widget from your layout, etc.
<CateringWidget
publishableKey="pk_live_..."
googleMapsApiKey="AIzaSy..."
onOrderCompleteDelaySeconds={5}
onOrderComplete={({ orderId, accessToken }) => {
router.push(`/orders/${orderId}?token=${accessToken}`);
}}
/>onError(error)
Fires on errors the widget can't recover from. error.code is one of:
invalid_publishable_key— the key was rejected by Swift's backend.slug_delegation_not_permitted— apartnerSlugwas supplied with a key that is not permitted to delegate.unknown_partner_slug— the suppliedpartnerSlugdoes not match an active partner.session_failed— the widget session handshake failed.network_error— a backend request failed or returned a non-2xx.submit_failed— the order submission itself failed.unknown— anything else; seeerror.causefor details.
What the widget handles for you
- Restaurant browsing and menu exploration
- Multi-session meal building
- Contact details and delivery address
- Promo codes and pricing preview
- Google Maps address autocomplete (requires the
googleMapsApiKeyyou provide) - Order submission to Swift's API
- State persistence in
localStorage(namespaced)
What you're responsible for
- Getting a publishable key from Swift.
- Providing a Google Maps JavaScript API key (with the Places library enabled) via the
googleMapsApiKeyprop. Restrict the key to your own domain(s) in the Google Cloud Console. - Rendering
<CateringWidget>wherever you want the flow to live. - Navigating after
onOrderComplete. - Page chrome around the widget.
What you're not responsible for
- Payment. Swift sends a payment link by email after reviewing the order.
- Loading the Google Maps script — the widget injects the script using the key you provide.
- Making API calls to Swift.
- Session or auth management.
Getting a publishable key
Contact Swift at [email protected] with:
- Your company name.
- A technical contact for integration support.
Swift provisions a partner record and returns a publishable key (format: pk_live_...) out of band.
The key is public — it ships in the browser bundle.
Troubleshooting
Errors mentioning invalid_publishable_key or session_failed
Either the key is wrong or the key is inactive. Contact Swift if you need a new one.
TypeScript errors importing types
Ensure your tsconfig.json has "moduleResolution": "bundler" or "node16"/"nodenext".
Browser support
Modern evergreen browsers (Chrome, Firefox, Safari, Edge) on desktop and mobile.
Versioning
@swift-food-services/catering-widget follows semver. The public API is everything exported from the package entry — CateringWidget, its props, and the re-exported types.
Support
- Integration questions: [email protected]
- Bug reports: include your publishable key prefix (first 8 chars), the browser, and a reproducible example.
License
Proprietary. Distributed for use by authorized Swift Food partners only.
