@resira/ui
v0.7.4
Published
React UI components for the Resira booking platform
Maintainers
Readme
@resira/ui v0.4.x
React booking UI for Resira. It includes a ready-to-embed widget, modal flow, provider, hooks, and lower-level components for custom booking experiences.
Installation
npm install @resira/ui@latest @resira/sdk@latestPeer dependencies:
- react >= 18
- react-dom >= 18
Quick start
import { ResiraProvider, ResiraBookingWidget } from "@resira/ui";
import "@resira/ui/styles.css";
export function App() {
return (
<ResiraProvider
apiKey="resira_live_abc123"
resourceId="prop-1"
domain="rental"
>
<ResiraBookingWidget />
</ResiraProvider>
);
}You can also use the higher-level modal component:
import { BookingModal } from "@resira/ui";
import "@resira/ui/styles.css";
<BookingModal apiKey="resira_live_abc123" domain="watersport" />;Supported domains
| Domain | Flow |
| --- | --- |
| rental | Calendar -> guest details -> terms -> payment |
| restaurant | Time slots -> guest details -> terms -> payment |
| watersport | Service -> time -> guest details -> terms -> payment |
| service | Service -> time -> guest details -> terms -> payment |
ResiraProvider
ResiraProvider initializes the SDK client, loads public property configuration, resolves theme and locale values, and provides booking state to child components.
<ResiraProvider
apiKey="resira_live_..."
resourceId="resource-id"
domain="rental"
config={{
theme: {
primaryColor: "#18181b",
borderRadius: "16px",
},
locale: {
bookNow: "Book now",
},
serviceLayout: "vertical",
visibleServiceCount: 4,
groupServicesByCategory: true,
stripePublishableKey: "pk_live_...",
showTerms: true,
showWaiver: false,
depositPercent: 50,
}}
>
<ResiraBookingWidget />
</ResiraProvider>Provider props
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| apiKey | string | Yes | Resira public API key |
| resourceId | string | No | Default resource or property ID. Omit for catalog mode |
| domain | "rental" \| "restaurant" \| "watersport" \| "service" | Yes | Booking flow type |
| config | ResiraProviderConfig | No | Theme, locale, layout, payment, and rendering overrides |
| onClose | () => void | No | Close callback for modal-driven flows |
Important config options
| Config field | Type | Description |
| --- | --- | --- |
| theme | ResiraTheme | Theme token overrides |
| locale | ResiraLocale | Text and i18n overrides |
| domainConfig | DomainConfig | Domain-specific rules such as min/max party size or duration |
| classNames | ResiraClassNames | CSS class overrides |
| serviceLayout | "vertical" \| "horizontal" | Product selector layout |
| visibleServiceCount | number | Number of visible services before scrolling |
| groupServicesByCategory | boolean | Group services by linked equipment type |
| renderServiceCard | (product, selected) => ReactNode | Custom service card rendering |
| baseUrl | string | Optional API origin override |
| baseUrls | Array<string \| WeightedBaseUrl> | Optional ordered fallback origin list |
| stripePublishableKey | string | Stripe publishable key override |
| termsText | string | Terms text override |
| waiverText | string | Waiver text override |
| showWaiver | boolean | Show waiver checkbox |
| showTerms | boolean | Show terms checkbox |
| showRemainingSpots | boolean | Show remaining capacity labels |
| depositPercent | number | Percent charged upfront |
Styling and customization
Import the stylesheet once:
import "@resira/ui/styles.css";The main customization layers are:
themetokens onResiraProvider- CSS custom properties under
.resira-root classNamesandrenderServiceCardfor deeper control
For more styling details, see CUSTOMIZATION.md.
Payment and remote config
The provider loads public configuration such as:
- Stripe publishable key
- deposit percentage
- terms requirements
- refund policy
You can override those values locally through config when needed.
Promoter mode (fast / low-bandwidth)
Use promoter mode when staff are creating bookings on-the-spot in unstable mobile networks.
<ResiraProvider
apiKey="resira_live_..."
domain="watersport"
config={{
promoterMode: {
enabled: true,
contactMode: "phone-required",
disableImages: true,
disablePromoValidation: true,
cacheTtlMs: 300000,
useStaleDataOnError: true,
autoAdvanceAvailability: true,
hidePromoInput: false,
},
}}
>
<ResiraBookingWidget />
</ResiraProvider>Promoter mode defaults are optimized for speed:
- Hides step indicator for a cleaner, faster flow
- Hides heavy service/resource images
- Caches resources/products/availability responses
- Falls back to stale cached data if network fails
- Uses phone-first guest validation by default
- Skips promo live-validation API call until payment/booking submit
Hooks and components
Main exports:
ResiraProvideruseResiraResiraBookingWidgetBookingModalBookingCalendarTimeSlotPickerResourcePickerProductSelectorGuestFormvalidateGuestFormWaiverConsentPaymentFormSummaryPreviewConfirmationViewuseAvailabilityuseReservationuseResourcesuseProductsusePaymentIntent(returnserrorDetail: ResiraApiError | ResiraNetworkError | Error | nullfor 409 / machine codes)useMultiServicePaymentIntent— single combined PaymentIntent for a cart with 2+ services (see below)DishShowcaseuseDishuseDishes
usePaymentIntent
error: Human-readable message.errorDetail: The thrown error — for API failures useerrorDetail instanceof ResiraApiErrorand readstatus,body, andmachineCode(body.code) for create-intent 409 conflicts (see@resira/sdkREADME).create(payload, { idempotencyKey? }): Pass an explicit idempotency key to match your slot hold / checkout; otherwise the hook derives a stable key from the payload so React Strict Mode / double effects reuse one logical request.- Development: Failed creates log
status,url, andbodyto the console.
Multi-service checkout (useMultiServicePaymentIntent)
When a guest books two or more services in one cart, you must call the backend once with all items so Stripe creates a single PaymentIntent. This produces:
- One combined total shown in Stripe Elements
- One authorization from the guest
- One confirmation email listing all services
Do NOT call usePaymentIntent().create() in a loop — that creates separate PaymentIntents, separate charges, and separate emails. That is the correct behavior only when the user intentionally makes two independent bookings.
Usage
import {
useMultiServicePaymentIntent,
useResira,
} from "@resira/ui";
function Checkout({ cart }) {
const { client } = useResira();
const {
create,
confirmAll,
paymentIntent,
creating,
confirming,
error,
} = useMultiServicePaymentIntent();
const startCheckout = async () => {
const result = await create({
items: cart.map((item) => ({
productId: item.productId,
holdId: item.holdId, // active slot hold from createSlotHold
startTime: item.startTime,
endTime: item.endTime,
durationMinutes: item.durationMinutes,
partySize: item.partySize,
resourceId: item.resourceId, // optional
upsellItems: item.upsells, // optional
promoCode: item.promoCode, // optional
})),
guestName: cart[0].guestName,
guestEmail: cart[0].guestEmail,
guestPhone: cart[0].guestPhone,
notes: cart[0].notes,
termsAccepted: true,
waiverAccepted: true,
});
if (!result) return;
if (result.totalAmountDue === 0) {
// Free booking — confirm immediately
await confirmAll(result.reservations);
} else {
// Mount result.clientSecret in Stripe Elements
// After Stripe confirms, call confirmAll(...)
}
};
}Key differences from usePaymentIntent
| | usePaymentIntent | useMultiServicePaymentIntent |
| --- | --- | --- |
| Services | 1 | 2+ |
| Stripe PI | 1 | 1 (combined total) |
| Emails | 1 | 1 (consolidated) |
| Return | reservationId | reservations[] (one per service) |
| Confirm | confirm(piId, reservationId) | confirmAll([{ reservationId, piId }, …]) |
Backend requirement
Requires the backend endpoint POST /v1/public/payments/create-multi-intent. If the backend does not yet expose it, the SDK call will 404 and you should fall back to sequential single-service booking.
DishShowcase
The DishShowcase component renders a trigger button that opens a nested modal for browsing dishes with 3D model previews and AR viewing. It uses <model-viewer> for the 3D experience.
Prerequisites
Load the <model-viewer> web component in your page:
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.0/model-viewer.min.js"></script>Single dish (by ID)
The dishId is shown on the dish page in the admin dashboard for easy copying.
import { DishShowcase } from "@resira/ui";
<DishShowcase dishId="dish-uuid">
View in 3D
</DishShowcase>Browse all dishes
<DishShowcase showAll>
Explore Our Menu
</DishShowcase>Filter by category
<DishShowcase showAll category="Main">
View Main Courses
</DishShowcase>Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| dishId | string | — | Single dish ID, opens directly to 3D viewer |
| showAll | boolean | false | Show all dishes in a browsable grid |
| category | string | — | Filter dishes by category |
| className | string | — | Custom CSS class on the trigger button |
| style | CSSProperties | — | Custom inline styles on the trigger button |
| children | ReactNode | required | Button label / content |
Inside a BookingModal
DishShowcase works as a nested modal — it renders above the booking modal with its own overlay and focus trap:
<BookingModal apiKey="resira_live_..." domain="restaurant">
<DishShowcase dishId="dish-uuid" style={{ marginBottom: 12 }}>
Preview this dish in 3D
</DishShowcase>
</BookingModal>Notes
- The widget supports catalog mode when
resourceIdis omitted. - Service and watersport flows group services by linked equipment category by default.
- Loading and error states now announce themselves to assistive technologies on the main widget surfaces.
- Client-side API origin stickiness/load-balancing has been removed; routing is deterministic unless you pass explicit fallback origins.
- The package is intended for browser usage and ships CSS separately via
@resira/ui/styles.css.
