Beauty Affairs HK

Medispa booking

How the React booking calendar reads its config, asks Sesami for availability, and turns a treatment booking into a cart line.

The booking calendar is the only React application in the theme. It shows treatment availability, lets a shopper pick a clinic, day and time, and adds the booking to the cart.

Medispa booking flow: product metafields and the medispa_locations metaobject feed a Liquid snippet that writes window.theme.medispaCalendarConfig. The React CalendarRoot reads that config, fetches availability from the Sesami SDK, looks up the deposit selling plan through a section render request, and adds the treatment to the cart. Klaviyo form submissions are forwarded to a Zapier webhook.

Ownership

ValueOwned byChanged where
Clinic name, address, phone, hoursShopify metaobjectContent > Metaobjects > Medispa locations
Treatment price, deposit, pay-in-clinic amountProduct metafieldsProducts > [product] > Metafields
Fallback amounts and wordingTheme settingsCustomize > Medispa Deposit
Slot availabilitySesamiSesami's own dashboard. Not in Shopify
Deposit selling planDownpay appThe app. The theme reads it and never writes it
Booking formKlaviyoKlaviyo, with the form ID set per product or per store

This repository holds no availability data. The theme asks Sesami which slots are free and shows the answer.

How the config reaches React

snippets/medispa-calendar-config.liquid builds a JSON object and assigns it to window.theme.medispaCalendarConfig. For each product it works out the deposit amount, the pay-in-clinic amount, whether each payment option is on, and the Klaviyo form IDs. When a metafield is absent, it uses the theme setting.

snippets/medispa-calendar-placeholder.liquid renders an empty div and loads the bundle. src/entrypoints/medispa-calendar-react.tsx finds that div and mounts CalendarRoot into it.

The mount waits for Sesami. deferJS polls until window.SesamiSDK exists, because the calendar cannot ask for availability without it. If the Sesami script is blocked or fails, the placeholder stays empty and the console shows no error.

The calendar keeps its state in Jotai atoms in calendar-atoms.ts. Several atoms read window.theme.medispaCalendarConfig at module scope, so the config must exist before the module is imported, not only before the component renders.

Two payment paths

A treatment can offer one or both of these.

Pay a deposit now. The shopper pays a deposit at checkout and the balance in clinic. This path needs a selling plan from the Downpay app. The calendar fetches it with a section render request to products/<handle>?sections=product-calendar-config, which returns the JSON that sections/product-calendar-config.liquid outputs. The custom.enable_checkout metafield turns this path off for one product. The snippet treats an absent metafield as enabled, so older products keep the deposit option.

Pay in clinic. The shopper pays nothing online. The custom.enable_pay_in_store metafield controls this path for one product, and a custom.pay_in_store metafield on the landing page overrides it for every product on that page. The snippet detects the override by the metafield's type, not its value, so an explicit false still overrides.

The cart does not store the remaining balance. A mini-cart rule recalculates it on every cart change, for any line whose vendor contains medispa. See Mini-cart.

Amounts stay in dollars until formatting

The config snippet receives every price in dollars, whether it comes from a render parameter, the landing page metafield or a theme setting. Shopify's money filters expect cents, so the snippet converts once, into a separate variable, at the point of formatting. The source comments state this rule, because converting twice is the most likely way to break it.

The Klaviyo and Zapier path

CalendarRoot listens for the klaviyoForms event on window. When a form is submitted, it builds a payload with a fixed shape, including the selected product, and posts it to the URL in the Medispa Booking Form Submit URL theme setting.

Klaviyo still receives the submission, and the webhook gets a copy. Clearing the webhook setting stops the copy and the form keeps working. The payload type is KlaviyoZapierPayload in src/@types/calendar.ts. The source builds the payload key by key, so Zapier receives the same fields in the same order every time.

Hong Kong differs from Australia

The Hong Kong and Australian stores share this code. Australia has a custom.medispa_landing_page metafield that supplies the offer price. Hong Kong does not, so Hong Kong templates pass the landing page's offer price as a render parameter. The Australian lookup stays first in the fallback chain so the two stores' code can still be merged. Leave it in place when you edit the snippet.

Failure modes

Empty space where the calendar should be. Sesami did not load. Look for the Sesami script in the network tab, and check for an ad blocker.

The calendar throws on load. window.theme.medispaCalendarConfig is undefined because the template does not render snippets/medispa-calendar-config.liquid.

The deposit option is missing. Either custom.enable_checkout is false on the product, or the product has no Downpay selling plan and the section render request returns none.

Wrong clinic against a treatment. The calendar matches clinics to products by looking for the clinic handle inside the product handle. Renaming a product handle can break that match without any error, and the calendar then shows the default name.

Prices a hundred times too large or too small. Code converted dollars to cents twice, or not at all.

Source map

ConcernFile
Config constructionsnippets/medispa-calendar-config.liquid
Inline variant of the configsnippets/medispa-calendar-config-inline.liquid
Mount pointsnippets/medispa-calendar-placeholder.liquid
React entrysrc/entrypoints/medispa-calendar-react.tsx
Main component and data fetchingsrc/components/medispa-calendar/CalendarRoot.tsx
Payment options and confirmationsrc/components/medispa-calendar/CalendarConfirm.tsx
Shared statesrc/components/medispa-calendar/calendar-atoms.ts
Types for config, slots and the Sesami SDKsrc/@types/calendar.ts
Selling plan JSONsections/product-calendar-config.liquid
Deferred mount helpersrc/components/utils/defer.ts

On this page