@frontdesk-africa/store-js 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -6
- package/dist/index.d.mts +169 -3
- package/dist/index.d.ts +169 -3
- package/dist/index.js +37 -2
- package/dist/index.mjs +37 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,27 @@ const item = await store.product('blue-mug')
|
|
|
49
49
|
|
|
50
50
|
You never choose the workspace: the key does. Any workspace ref you send is ignored.
|
|
51
51
|
|
|
52
|
+
### Products sold in several dimensions
|
|
53
|
+
|
|
54
|
+
A product whose merchant sells it by Size *and* Colour carries an `options` array: one entry per
|
|
55
|
+
group, each with its own `display` (`dropdown`, `text`, `color`, `image`) and its list of values.
|
|
56
|
+
The buyer picks one value per group, and that set resolves to a single variant — the one whose
|
|
57
|
+
`optionValueRefs` holds exactly those value refs. Checkout is unchanged: you still send that
|
|
58
|
+
variant's `ref`.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const item = await store.product('shirt-dress')
|
|
62
|
+
|
|
63
|
+
// options is absent on an ordinary product — render item.variants as a flat list then.
|
|
64
|
+
const picked = { [item.options![0].ref]: 'value-ref-small', [item.options![1].ref]: 'value-ref-teal' }
|
|
65
|
+
const chosen = item.variants.find(
|
|
66
|
+
(v) => v.optionValueRefs?.length === 2 && v.optionValueRefs.every((r) => Object.values(picked).includes(r)),
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Dim a value when every variant still holding it is `soldOut` (or has no price in the buyer's
|
|
71
|
+
currency), and swap your gallery to a value's `media` when it is selected.
|
|
72
|
+
|
|
52
73
|
## Checkout (server)
|
|
53
74
|
|
|
54
75
|
You never handle card details and you never price a cart. The buyer pays on a FrontDesk-hosted page
|
|
@@ -67,6 +88,10 @@ const checkout = await store.createCheckout(
|
|
|
67
88
|
redirect(checkout.hostedUrl)
|
|
68
89
|
```
|
|
69
90
|
|
|
91
|
+
Every `variantRef` must be a live one from the catalogue. An unknown or unpublished ref is refused
|
|
92
|
+
with `400 VALIDATION_ERROR` listing the bad refs in `error.details.invalid`, so a typo fails on your
|
|
93
|
+
server instead of in front of your buyer.
|
|
94
|
+
|
|
70
95
|
Then, when the buyer comes back:
|
|
71
96
|
|
|
72
97
|
```ts
|
|
@@ -131,15 +156,39 @@ the check will fail in a way that looks like a key mismatch.
|
|
|
131
156
|
Payloads are thin — refs only. Re-fetch through the API for detail, so a replayed delivery can never
|
|
132
157
|
present stale figures as current.
|
|
133
158
|
|
|
159
|
+
## Orders + keeping a local copy in sync
|
|
160
|
+
|
|
161
|
+
`orders()` (server only, secret key) lists your own orders as a **delta read**: pass the largest
|
|
162
|
+
`updatedAt` you have seen back as `updatedSince` and upsert by `ref` — the `>=` comparison repeats
|
|
163
|
+
the boundary row on purpose so a same-second tie can never drop one. `products({ updatedSince })`
|
|
164
|
+
works the same way for the catalogue.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
let cursor = loadCursor() // null on first run = full backfill
|
|
168
|
+
for (;;) {
|
|
169
|
+
const page = await store.orders(cursor ? { updatedSince: cursor, limit: 200 } : { limit: 200 })
|
|
170
|
+
for (const order of page) upsertByRef(order)
|
|
171
|
+
if (page.length < 200) break
|
|
172
|
+
cursor = page[page.length - 1].updatedAt
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Deletions never show up as absence in a delta page: they arrive as the `product.deleted` and
|
|
177
|
+
`order.deleted` webhooks — remove those refs from your copy when they land. FrontDesk stays the
|
|
178
|
+
source of truth; on any conflict, its row wins.
|
|
179
|
+
|
|
180
|
+
Confirming a single purchase is still `getCheckout(ref)`, not a scan of `orders()`.
|
|
181
|
+
|
|
134
182
|
## Test mode
|
|
135
183
|
|
|
136
|
-
A `fd_sk_test_…` key opens a test checkout
|
|
137
|
-
**
|
|
138
|
-
|
|
184
|
+
A `fd_sk_test_…` key opens a test checkout that completes with a **real charge on the provider's
|
|
185
|
+
sandbox** — pay it with a Paystack test card (the popup offers a Success option). There is exactly
|
|
186
|
+
one way to finish a checkout in either mode.
|
|
139
187
|
|
|
140
|
-
|
|
141
|
-
`checkout.completed` with `
|
|
142
|
-
|
|
188
|
+
The order it creates is REAL and marked `test` everywhere: no money moves, nothing is held from
|
|
189
|
+
stock, and `checkout.completed` fires with the real `orderRef`. Test orders show under a Test badge
|
|
190
|
+
in the portal, only a test key can read them (`orders()` never mixes envs), and they are
|
|
191
|
+
hard-deleted after 90 days — the `order.deleted` webhook tells your mirror when that happens.
|
|
143
192
|
|
|
144
193
|
---
|
|
145
194
|
|
package/dist/index.d.mts
CHANGED
|
@@ -576,7 +576,13 @@ type PublicProductInfo = Omit<ProductInfoView, 'hsCode'>;
|
|
|
576
576
|
/** How a package product's options are chosen: one tier (radio) vs a menu (pick many + quantity). */
|
|
577
577
|
type SelectionMode = 'choose_one' | 'choose_many';
|
|
578
578
|
/** How the storefront draws a product's option picker. A variant missing an image/hex falls back to text. */
|
|
579
|
-
type VariantDisplay = 'text' | 'image' | 'color';
|
|
579
|
+
type VariantDisplay = 'text' | 'image' | 'color' | 'dropdown';
|
|
580
|
+
/**
|
|
581
|
+
* Multi-group variations: how ONE variation group's picker is drawn. Per-group (Size = dropdown,
|
|
582
|
+
* Color = swatches), unlike the legacy product-level VariantDisplay, which keeps governing products
|
|
583
|
+
* that have no groups. A value missing its hex/image falls back to a text chip.
|
|
584
|
+
*/
|
|
585
|
+
type OptionGroupDisplay = 'dropdown' | 'text' | 'color' | 'image';
|
|
580
586
|
/** Product/option payment modes: the shipped package modes + Pay Later (Save-For-It deferred). */
|
|
581
587
|
type ProductPricingMode = PackagePricingMode | 'pay_later';
|
|
582
588
|
interface ProductAddonOption {
|
|
@@ -645,6 +651,23 @@ interface StoreAvailabilitySummary {
|
|
|
645
651
|
leadTimeMinutes: number;
|
|
646
652
|
}
|
|
647
653
|
type PreorderDepositType = 'percent' | 'fixed';
|
|
654
|
+
interface PublicProductOptionValue {
|
|
655
|
+
ref: string;
|
|
656
|
+
label: string;
|
|
657
|
+
/** '#RRGGBB' for the colour swatch under display 'color'; absent/null falls back to a text chip. */
|
|
658
|
+
swatchHex?: string | null;
|
|
659
|
+
/** Selecting this value swaps the product gallery to these images. Absent = keep the gallery. */
|
|
660
|
+
media?: string[];
|
|
661
|
+
}
|
|
662
|
+
/** One variation axis of a multi-group product (e.g. Size, Color). The buyer picks one value per
|
|
663
|
+
* group; the picked set resolves to exactly one variant (see PublicProductVariant.optionValueRefs). */
|
|
664
|
+
interface PublicProductOptionGroup {
|
|
665
|
+
ref: string;
|
|
666
|
+
name: string;
|
|
667
|
+
/** How to draw THIS group's picker: a select, text chips, colour swatches, or image tiles. */
|
|
668
|
+
display: OptionGroupDisplay;
|
|
669
|
+
values: PublicProductOptionValue[];
|
|
670
|
+
}
|
|
648
671
|
interface PublicProductVariant {
|
|
649
672
|
ref: string;
|
|
650
673
|
name: string;
|
|
@@ -668,6 +691,9 @@ interface PublicProductVariant {
|
|
|
668
691
|
availableQty?: number | null;
|
|
669
692
|
/** '#RRGGBB' for the colour swatch; null under 'color' mode falls back to a text chip. */
|
|
670
693
|
swatchHex?: string | null;
|
|
694
|
+
/** Multi-group products: the option values this combination is made of (one value ref per group,
|
|
695
|
+
* in group sort order). Absent on ungrouped products — render the flat `variants` picker then. */
|
|
696
|
+
optionValueRefs?: string[];
|
|
671
697
|
description?: string | null;
|
|
672
698
|
badge?: string | null;
|
|
673
699
|
features?: PackageFeature[];
|
|
@@ -702,6 +728,10 @@ interface PublicProduct {
|
|
|
702
728
|
selectionMode?: SelectionMode;
|
|
703
729
|
/** How the option picker is drawn: text chips (default), image swatches, or colour swatches. */
|
|
704
730
|
variantDisplay?: VariantDisplay;
|
|
731
|
+
/** Multi-group variations: one picker per group, each drawn per its `display`. When present, the
|
|
732
|
+
* buyer picks one value per group and the picks resolve to a variant via `optionValueRefs`; when
|
|
733
|
+
* absent, render the flat `variants` list exactly as before (existing storefronts keep working). */
|
|
734
|
+
options?: PublicProductOptionGroup[];
|
|
705
735
|
schedulingEnabled?: boolean;
|
|
706
736
|
/** Add-on groups the buyer can pick from (option-scoped groups carry variantRef). */
|
|
707
737
|
addonGroups?: ProductAddonGroup[];
|
|
@@ -822,6 +852,9 @@ interface PublicProductSummary {
|
|
|
822
852
|
ratingAvg?: number;
|
|
823
853
|
/** Published review count behind `ratingAvg`. Absent under the floor, for the same reason. */
|
|
824
854
|
ratingCount?: number;
|
|
855
|
+
/** Last change to the product or its variants (ISO). On GET /v1/store/products this is the
|
|
856
|
+
* `updatedSince` delta cursor: pass the largest value you have seen back to get what changed. */
|
|
857
|
+
updatedAt?: string;
|
|
825
858
|
}
|
|
826
859
|
type CollectionCoverKind = 'image' | 'video';
|
|
827
860
|
/** Buyer-facing collection in the storefront payload / public endpoints. */
|
|
@@ -860,6 +893,14 @@ interface DeliveryZone {
|
|
|
860
893
|
* payloads and on single-currency stores. */
|
|
861
894
|
prices?: ZoneFee[];
|
|
862
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Store order lifecycle — the union of the standard product-cart axis (pending → paid → fulfilled) and
|
|
898
|
+
* the package-BOOKING axis (requested → awaiting_payment → confirmed → in_progress → completed). One
|
|
899
|
+
* table (store_orders) carries both since Unified Products; the DB enum matches this set.
|
|
900
|
+
*/
|
|
901
|
+
type StoreOrderStatus = 'pending' | 'awaiting_payment' | 'requested' | 'accepted' | 'declined' | 'confirmed' | 'in_progress' | 'paid' | 'completed' | 'fulfilled' | 'cancelled' | 'expired' | 'refunded' | 'archived'
|
|
902
|
+
/** Held while a Save For It plan funds toward this product (no stock reserved); flips to `paid` on completion. */
|
|
903
|
+
| 'saving';
|
|
863
904
|
interface StoreCheckoutItem {
|
|
864
905
|
variantRef: string;
|
|
865
906
|
quantity: number;
|
|
@@ -879,6 +920,14 @@ interface StoreShippingAddress {
|
|
|
879
920
|
lat?: number | null;
|
|
880
921
|
lng?: number | null;
|
|
881
922
|
}
|
|
923
|
+
/** `awaiting_stock` = paid, but the item was sold out when the Save For It plan completed. The merchant
|
|
924
|
+
* owes a unit they do not have; the order must stay OUT of the ship-now queue until they restock, at
|
|
925
|
+
* which point it clears back to `unfulfilled` on its own. */
|
|
926
|
+
/** `preordered` = the buyer deliberately bought something the merchant does not have yet (PO-1). Kept
|
|
927
|
+
* distinct from `awaiting_stock` because the merchant's queue has to tell "a saver's plan completed and
|
|
928
|
+
* I owe them" apart from "I pre-sold this on purpose", and because a part-paid pre-order must NOT rejoin
|
|
929
|
+
* the ship-now queue on restock. */
|
|
930
|
+
type StoreOrderFulfillmentStatus = 'unfulfilled' | 'fulfilled' | 'shipped' | 'delivered' | 'awaiting_stock' | 'preordered';
|
|
882
931
|
|
|
883
932
|
/**
|
|
884
933
|
* Packages — sellable offerings + priced tiers (options). Spec §13.1, §10.18 (PV-012/054).
|
|
@@ -966,7 +1015,12 @@ interface StoreTicketItem {
|
|
|
966
1015
|
*/
|
|
967
1016
|
interface StoreEventCheckoutInput {
|
|
968
1017
|
tickets: StoreTicketItem[];
|
|
969
|
-
/**
|
|
1018
|
+
/**
|
|
1019
|
+
* 🔴 NOT SUPPORTED YET — a non-empty array is refused with 400. It was declared with D18 and never
|
|
1020
|
+
* wired: the hosted ticket page has no product concept, so anything sent here reached the buyer as
|
|
1021
|
+
* neither a line nor a charge. Kept on the type because the contract is frozen (D13). Sell
|
|
1022
|
+
* merchandise with a separate POST /v1/store/checkouts until the hosted page can carry it.
|
|
1023
|
+
*/
|
|
970
1024
|
products?: StoreCheckoutItem[] | null;
|
|
971
1025
|
contact: StoreCheckoutContact;
|
|
972
1026
|
/** Buyer DOB, when the event uses order-level age checks. YYYY-MM-DD. */
|
|
@@ -1042,6 +1096,92 @@ interface StoreCheckoutSessionView {
|
|
|
1042
1096
|
createdAt: string;
|
|
1043
1097
|
}
|
|
1044
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* GET /v1/store/orders — the merchant's own store orders, over their SECRET key.
|
|
1101
|
+
*
|
|
1102
|
+
* Built for delta sync (the Supabase mirror pulls through `updatedSince`), which is why the shape
|
|
1103
|
+
* is a snapshot a mirror can upsert whole: refs, money in minor units, line snapshots that survive
|
|
1104
|
+
* product edits. Live and test orders never mix — the key's env picks the set, exactly like
|
|
1105
|
+
* checkouts. What is NOT here is deliberate: no provider references, no ledger refs, no wallet or
|
|
1106
|
+
* payout figures — the standing constraint (no money surface behind an installed key) applies to
|
|
1107
|
+
* this read exactly as it does to mirrors.
|
|
1108
|
+
*/
|
|
1109
|
+
interface StoreOrderRecordLine {
|
|
1110
|
+
/** Snapshot refs: null when the product/variant was deleted after purchase. */
|
|
1111
|
+
productRef: string | null;
|
|
1112
|
+
productName: string;
|
|
1113
|
+
variantRef: string | null;
|
|
1114
|
+
variantName: string;
|
|
1115
|
+
imageUrl: string | null;
|
|
1116
|
+
quantity: number;
|
|
1117
|
+
unitPriceMinor: number;
|
|
1118
|
+
subtotalMinor: number;
|
|
1119
|
+
}
|
|
1120
|
+
interface StoreOrderRecord {
|
|
1121
|
+
ref: string;
|
|
1122
|
+
/** The short human reference shown to the buyer (receipts, support). */
|
|
1123
|
+
shortReference: string;
|
|
1124
|
+
/** The full lifecycle union covers BOTH axes: product carts (pending → paid → fulfilled) and
|
|
1125
|
+
* package bookings (requested → awaiting_payment → confirmed → in_progress → completed).
|
|
1126
|
+
* `saving` (an in-progress savings plan) never appears here — it is not an order yet. */
|
|
1127
|
+
status: Exclude<StoreOrderStatus, 'saving'>;
|
|
1128
|
+
fulfillmentStatus: StoreOrderFulfillmentStatus;
|
|
1129
|
+
/** Paid with a test key: no real money moved, hard-deleted after 90 days (`order.deleted`). */
|
|
1130
|
+
test: boolean;
|
|
1131
|
+
currency: string;
|
|
1132
|
+
subtotalMinor: number;
|
|
1133
|
+
deliveryFeeMinor: number;
|
|
1134
|
+
totalMinor: number;
|
|
1135
|
+
buyer: {
|
|
1136
|
+
name: string;
|
|
1137
|
+
email: string | null;
|
|
1138
|
+
phone: string | null;
|
|
1139
|
+
};
|
|
1140
|
+
needsDelivery: boolean;
|
|
1141
|
+
shipping: {
|
|
1142
|
+
address1: string | null;
|
|
1143
|
+
address2: string | null;
|
|
1144
|
+
city: string | null;
|
|
1145
|
+
state: string | null;
|
|
1146
|
+
country: string | null;
|
|
1147
|
+
zoneRef: string | null;
|
|
1148
|
+
zoneName: string | null;
|
|
1149
|
+
} | null;
|
|
1150
|
+
lines: StoreOrderRecordLine[];
|
|
1151
|
+
paidAt: string | null;
|
|
1152
|
+
createdAt: string;
|
|
1153
|
+
/** The delta cursor: pass the largest value you have seen back as `updatedSince`. */
|
|
1154
|
+
updatedAt: string;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* `POST /v1/store/customers/link` — attach the caller's own signed-in user to this workspace's
|
|
1159
|
+
* contacts (Supabase Phase 1c; design doc §7.6).
|
|
1160
|
+
*
|
|
1161
|
+
* TRUST MODEL: the caller holds a secret key, i.e. IS the merchant (or their installed backend).
|
|
1162
|
+
* They can already create and edit their own contacts in the portal, so accepting their assertion
|
|
1163
|
+
* that `email` was verified by THEIR auth system delegates nothing they do not have. The identity
|
|
1164
|
+
* being asserted must still be verified on the caller's side — that is stated at the API boundary,
|
|
1165
|
+
* not enforced here, because we cannot see their auth system.
|
|
1166
|
+
*/
|
|
1167
|
+
interface StoreCustomerLinkInput {
|
|
1168
|
+
/** The email the CALLER's auth system verified. Never a guessed or user-typed-but-unverified one. */
|
|
1169
|
+
email: string;
|
|
1170
|
+
/** The caller's own user id (e.g. a Supabase Auth user id). Idempotency key: same ref, same link. */
|
|
1171
|
+
externalRef: string;
|
|
1172
|
+
/** Optional display name; wins over an empty contact name, never overwrites a set one. */
|
|
1173
|
+
name?: string;
|
|
1174
|
+
phone?: string;
|
|
1175
|
+
}
|
|
1176
|
+
interface StoreCustomerLinkView {
|
|
1177
|
+
/** The buyer's account ref — what `customer.linked` carries and checkout accepts. */
|
|
1178
|
+
customerRef: string;
|
|
1179
|
+
/** The workspace contact this landed on. */
|
|
1180
|
+
contactRef: string;
|
|
1181
|
+
/** True when this call created the link; false when the externalRef was already linked. */
|
|
1182
|
+
alreadyLinked: boolean;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1045
1185
|
/** A resolved commission rate (after applying any per-workspace override over the platform default). */
|
|
1046
1186
|
interface EventCommissionConfig {
|
|
1047
1187
|
/** Percentage cut on the gross (0..100). */
|
|
@@ -2341,7 +2481,16 @@ declare function createStoreClient(opts: StoreClientOptions): {
|
|
|
2341
2481
|
idempotencyKey?: string;
|
|
2342
2482
|
}) => Promise<T>;
|
|
2343
2483
|
storefront: () => Promise<Storefront>;
|
|
2344
|
-
|
|
2484
|
+
/**
|
|
2485
|
+
* Every public product. With `updatedSince` (ISO 8601) it becomes a delta read for keeping a
|
|
2486
|
+
* local copy fresh: ordered oldest change first, `limit` pages it (max 200), and each summary's
|
|
2487
|
+
* `updatedAt` is the next cursor. The comparison is >= so the boundary row repeats — upsert by
|
|
2488
|
+
* ref. Deleted products arrive as the `product.deleted` webhook, not as an absence here.
|
|
2489
|
+
*/
|
|
2490
|
+
products: (params?: {
|
|
2491
|
+
updatedSince?: string;
|
|
2492
|
+
limit?: number;
|
|
2493
|
+
}) => Promise<PublicProductSummary[]>;
|
|
2345
2494
|
product: (slug: string) => Promise<PublicProduct>;
|
|
2346
2495
|
productSlots: (slug: string, opts: {
|
|
2347
2496
|
variantRef: string;
|
|
@@ -2398,6 +2547,23 @@ declare function createStoreClient(opts: StoreClientOptions): {
|
|
|
2398
2547
|
* stock until the session lapses. Cancelling an already-paid checkout returns 409.
|
|
2399
2548
|
*/
|
|
2400
2549
|
cancelCheckout: (ref: string) => Promise<StoreCheckoutSessionView>;
|
|
2550
|
+
/**
|
|
2551
|
+
* Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
|
|
2552
|
+
* your auth system verified. Idempotent per externalRef, and the response is identical whether
|
|
2553
|
+
* the contact existed before — it cannot be used to probe who is already a customer.
|
|
2554
|
+
*/
|
|
2555
|
+
linkCustomer: (input: StoreCustomerLinkInput) => Promise<StoreCustomerLinkView>;
|
|
2556
|
+
/**
|
|
2557
|
+
* Your own store orders, as a delta read. SERVER ONLY — needs a secret key, and the key's env
|
|
2558
|
+
* picks the set (a live key never sees test orders). Page forward on `updatedSince` using each
|
|
2559
|
+
* order's `updatedAt` as the next cursor and upsert by ref (the >= comparison repeats the
|
|
2560
|
+
* boundary row). A page shorter than `limit` means you are caught up. This is a sync and
|
|
2561
|
+
* back-office read: to confirm a single purchase, keep using `getCheckout`.
|
|
2562
|
+
*/
|
|
2563
|
+
orders: (params?: {
|
|
2564
|
+
updatedSince?: string;
|
|
2565
|
+
limit?: number;
|
|
2566
|
+
}) => Promise<StoreOrderRecord[]>;
|
|
2401
2567
|
};
|
|
2402
2568
|
type StoreClient = ReturnType<typeof createStoreClient>;
|
|
2403
2569
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -576,7 +576,13 @@ type PublicProductInfo = Omit<ProductInfoView, 'hsCode'>;
|
|
|
576
576
|
/** How a package product's options are chosen: one tier (radio) vs a menu (pick many + quantity). */
|
|
577
577
|
type SelectionMode = 'choose_one' | 'choose_many';
|
|
578
578
|
/** How the storefront draws a product's option picker. A variant missing an image/hex falls back to text. */
|
|
579
|
-
type VariantDisplay = 'text' | 'image' | 'color';
|
|
579
|
+
type VariantDisplay = 'text' | 'image' | 'color' | 'dropdown';
|
|
580
|
+
/**
|
|
581
|
+
* Multi-group variations: how ONE variation group's picker is drawn. Per-group (Size = dropdown,
|
|
582
|
+
* Color = swatches), unlike the legacy product-level VariantDisplay, which keeps governing products
|
|
583
|
+
* that have no groups. A value missing its hex/image falls back to a text chip.
|
|
584
|
+
*/
|
|
585
|
+
type OptionGroupDisplay = 'dropdown' | 'text' | 'color' | 'image';
|
|
580
586
|
/** Product/option payment modes: the shipped package modes + Pay Later (Save-For-It deferred). */
|
|
581
587
|
type ProductPricingMode = PackagePricingMode | 'pay_later';
|
|
582
588
|
interface ProductAddonOption {
|
|
@@ -645,6 +651,23 @@ interface StoreAvailabilitySummary {
|
|
|
645
651
|
leadTimeMinutes: number;
|
|
646
652
|
}
|
|
647
653
|
type PreorderDepositType = 'percent' | 'fixed';
|
|
654
|
+
interface PublicProductOptionValue {
|
|
655
|
+
ref: string;
|
|
656
|
+
label: string;
|
|
657
|
+
/** '#RRGGBB' for the colour swatch under display 'color'; absent/null falls back to a text chip. */
|
|
658
|
+
swatchHex?: string | null;
|
|
659
|
+
/** Selecting this value swaps the product gallery to these images. Absent = keep the gallery. */
|
|
660
|
+
media?: string[];
|
|
661
|
+
}
|
|
662
|
+
/** One variation axis of a multi-group product (e.g. Size, Color). The buyer picks one value per
|
|
663
|
+
* group; the picked set resolves to exactly one variant (see PublicProductVariant.optionValueRefs). */
|
|
664
|
+
interface PublicProductOptionGroup {
|
|
665
|
+
ref: string;
|
|
666
|
+
name: string;
|
|
667
|
+
/** How to draw THIS group's picker: a select, text chips, colour swatches, or image tiles. */
|
|
668
|
+
display: OptionGroupDisplay;
|
|
669
|
+
values: PublicProductOptionValue[];
|
|
670
|
+
}
|
|
648
671
|
interface PublicProductVariant {
|
|
649
672
|
ref: string;
|
|
650
673
|
name: string;
|
|
@@ -668,6 +691,9 @@ interface PublicProductVariant {
|
|
|
668
691
|
availableQty?: number | null;
|
|
669
692
|
/** '#RRGGBB' for the colour swatch; null under 'color' mode falls back to a text chip. */
|
|
670
693
|
swatchHex?: string | null;
|
|
694
|
+
/** Multi-group products: the option values this combination is made of (one value ref per group,
|
|
695
|
+
* in group sort order). Absent on ungrouped products — render the flat `variants` picker then. */
|
|
696
|
+
optionValueRefs?: string[];
|
|
671
697
|
description?: string | null;
|
|
672
698
|
badge?: string | null;
|
|
673
699
|
features?: PackageFeature[];
|
|
@@ -702,6 +728,10 @@ interface PublicProduct {
|
|
|
702
728
|
selectionMode?: SelectionMode;
|
|
703
729
|
/** How the option picker is drawn: text chips (default), image swatches, or colour swatches. */
|
|
704
730
|
variantDisplay?: VariantDisplay;
|
|
731
|
+
/** Multi-group variations: one picker per group, each drawn per its `display`. When present, the
|
|
732
|
+
* buyer picks one value per group and the picks resolve to a variant via `optionValueRefs`; when
|
|
733
|
+
* absent, render the flat `variants` list exactly as before (existing storefronts keep working). */
|
|
734
|
+
options?: PublicProductOptionGroup[];
|
|
705
735
|
schedulingEnabled?: boolean;
|
|
706
736
|
/** Add-on groups the buyer can pick from (option-scoped groups carry variantRef). */
|
|
707
737
|
addonGroups?: ProductAddonGroup[];
|
|
@@ -822,6 +852,9 @@ interface PublicProductSummary {
|
|
|
822
852
|
ratingAvg?: number;
|
|
823
853
|
/** Published review count behind `ratingAvg`. Absent under the floor, for the same reason. */
|
|
824
854
|
ratingCount?: number;
|
|
855
|
+
/** Last change to the product or its variants (ISO). On GET /v1/store/products this is the
|
|
856
|
+
* `updatedSince` delta cursor: pass the largest value you have seen back to get what changed. */
|
|
857
|
+
updatedAt?: string;
|
|
825
858
|
}
|
|
826
859
|
type CollectionCoverKind = 'image' | 'video';
|
|
827
860
|
/** Buyer-facing collection in the storefront payload / public endpoints. */
|
|
@@ -860,6 +893,14 @@ interface DeliveryZone {
|
|
|
860
893
|
* payloads and on single-currency stores. */
|
|
861
894
|
prices?: ZoneFee[];
|
|
862
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Store order lifecycle — the union of the standard product-cart axis (pending → paid → fulfilled) and
|
|
898
|
+
* the package-BOOKING axis (requested → awaiting_payment → confirmed → in_progress → completed). One
|
|
899
|
+
* table (store_orders) carries both since Unified Products; the DB enum matches this set.
|
|
900
|
+
*/
|
|
901
|
+
type StoreOrderStatus = 'pending' | 'awaiting_payment' | 'requested' | 'accepted' | 'declined' | 'confirmed' | 'in_progress' | 'paid' | 'completed' | 'fulfilled' | 'cancelled' | 'expired' | 'refunded' | 'archived'
|
|
902
|
+
/** Held while a Save For It plan funds toward this product (no stock reserved); flips to `paid` on completion. */
|
|
903
|
+
| 'saving';
|
|
863
904
|
interface StoreCheckoutItem {
|
|
864
905
|
variantRef: string;
|
|
865
906
|
quantity: number;
|
|
@@ -879,6 +920,14 @@ interface StoreShippingAddress {
|
|
|
879
920
|
lat?: number | null;
|
|
880
921
|
lng?: number | null;
|
|
881
922
|
}
|
|
923
|
+
/** `awaiting_stock` = paid, but the item was sold out when the Save For It plan completed. The merchant
|
|
924
|
+
* owes a unit they do not have; the order must stay OUT of the ship-now queue until they restock, at
|
|
925
|
+
* which point it clears back to `unfulfilled` on its own. */
|
|
926
|
+
/** `preordered` = the buyer deliberately bought something the merchant does not have yet (PO-1). Kept
|
|
927
|
+
* distinct from `awaiting_stock` because the merchant's queue has to tell "a saver's plan completed and
|
|
928
|
+
* I owe them" apart from "I pre-sold this on purpose", and because a part-paid pre-order must NOT rejoin
|
|
929
|
+
* the ship-now queue on restock. */
|
|
930
|
+
type StoreOrderFulfillmentStatus = 'unfulfilled' | 'fulfilled' | 'shipped' | 'delivered' | 'awaiting_stock' | 'preordered';
|
|
882
931
|
|
|
883
932
|
/**
|
|
884
933
|
* Packages — sellable offerings + priced tiers (options). Spec §13.1, §10.18 (PV-012/054).
|
|
@@ -966,7 +1015,12 @@ interface StoreTicketItem {
|
|
|
966
1015
|
*/
|
|
967
1016
|
interface StoreEventCheckoutInput {
|
|
968
1017
|
tickets: StoreTicketItem[];
|
|
969
|
-
/**
|
|
1018
|
+
/**
|
|
1019
|
+
* 🔴 NOT SUPPORTED YET — a non-empty array is refused with 400. It was declared with D18 and never
|
|
1020
|
+
* wired: the hosted ticket page has no product concept, so anything sent here reached the buyer as
|
|
1021
|
+
* neither a line nor a charge. Kept on the type because the contract is frozen (D13). Sell
|
|
1022
|
+
* merchandise with a separate POST /v1/store/checkouts until the hosted page can carry it.
|
|
1023
|
+
*/
|
|
970
1024
|
products?: StoreCheckoutItem[] | null;
|
|
971
1025
|
contact: StoreCheckoutContact;
|
|
972
1026
|
/** Buyer DOB, when the event uses order-level age checks. YYYY-MM-DD. */
|
|
@@ -1042,6 +1096,92 @@ interface StoreCheckoutSessionView {
|
|
|
1042
1096
|
createdAt: string;
|
|
1043
1097
|
}
|
|
1044
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* GET /v1/store/orders — the merchant's own store orders, over their SECRET key.
|
|
1101
|
+
*
|
|
1102
|
+
* Built for delta sync (the Supabase mirror pulls through `updatedSince`), which is why the shape
|
|
1103
|
+
* is a snapshot a mirror can upsert whole: refs, money in minor units, line snapshots that survive
|
|
1104
|
+
* product edits. Live and test orders never mix — the key's env picks the set, exactly like
|
|
1105
|
+
* checkouts. What is NOT here is deliberate: no provider references, no ledger refs, no wallet or
|
|
1106
|
+
* payout figures — the standing constraint (no money surface behind an installed key) applies to
|
|
1107
|
+
* this read exactly as it does to mirrors.
|
|
1108
|
+
*/
|
|
1109
|
+
interface StoreOrderRecordLine {
|
|
1110
|
+
/** Snapshot refs: null when the product/variant was deleted after purchase. */
|
|
1111
|
+
productRef: string | null;
|
|
1112
|
+
productName: string;
|
|
1113
|
+
variantRef: string | null;
|
|
1114
|
+
variantName: string;
|
|
1115
|
+
imageUrl: string | null;
|
|
1116
|
+
quantity: number;
|
|
1117
|
+
unitPriceMinor: number;
|
|
1118
|
+
subtotalMinor: number;
|
|
1119
|
+
}
|
|
1120
|
+
interface StoreOrderRecord {
|
|
1121
|
+
ref: string;
|
|
1122
|
+
/** The short human reference shown to the buyer (receipts, support). */
|
|
1123
|
+
shortReference: string;
|
|
1124
|
+
/** The full lifecycle union covers BOTH axes: product carts (pending → paid → fulfilled) and
|
|
1125
|
+
* package bookings (requested → awaiting_payment → confirmed → in_progress → completed).
|
|
1126
|
+
* `saving` (an in-progress savings plan) never appears here — it is not an order yet. */
|
|
1127
|
+
status: Exclude<StoreOrderStatus, 'saving'>;
|
|
1128
|
+
fulfillmentStatus: StoreOrderFulfillmentStatus;
|
|
1129
|
+
/** Paid with a test key: no real money moved, hard-deleted after 90 days (`order.deleted`). */
|
|
1130
|
+
test: boolean;
|
|
1131
|
+
currency: string;
|
|
1132
|
+
subtotalMinor: number;
|
|
1133
|
+
deliveryFeeMinor: number;
|
|
1134
|
+
totalMinor: number;
|
|
1135
|
+
buyer: {
|
|
1136
|
+
name: string;
|
|
1137
|
+
email: string | null;
|
|
1138
|
+
phone: string | null;
|
|
1139
|
+
};
|
|
1140
|
+
needsDelivery: boolean;
|
|
1141
|
+
shipping: {
|
|
1142
|
+
address1: string | null;
|
|
1143
|
+
address2: string | null;
|
|
1144
|
+
city: string | null;
|
|
1145
|
+
state: string | null;
|
|
1146
|
+
country: string | null;
|
|
1147
|
+
zoneRef: string | null;
|
|
1148
|
+
zoneName: string | null;
|
|
1149
|
+
} | null;
|
|
1150
|
+
lines: StoreOrderRecordLine[];
|
|
1151
|
+
paidAt: string | null;
|
|
1152
|
+
createdAt: string;
|
|
1153
|
+
/** The delta cursor: pass the largest value you have seen back as `updatedSince`. */
|
|
1154
|
+
updatedAt: string;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* `POST /v1/store/customers/link` — attach the caller's own signed-in user to this workspace's
|
|
1159
|
+
* contacts (Supabase Phase 1c; design doc §7.6).
|
|
1160
|
+
*
|
|
1161
|
+
* TRUST MODEL: the caller holds a secret key, i.e. IS the merchant (or their installed backend).
|
|
1162
|
+
* They can already create and edit their own contacts in the portal, so accepting their assertion
|
|
1163
|
+
* that `email` was verified by THEIR auth system delegates nothing they do not have. The identity
|
|
1164
|
+
* being asserted must still be verified on the caller's side — that is stated at the API boundary,
|
|
1165
|
+
* not enforced here, because we cannot see their auth system.
|
|
1166
|
+
*/
|
|
1167
|
+
interface StoreCustomerLinkInput {
|
|
1168
|
+
/** The email the CALLER's auth system verified. Never a guessed or user-typed-but-unverified one. */
|
|
1169
|
+
email: string;
|
|
1170
|
+
/** The caller's own user id (e.g. a Supabase Auth user id). Idempotency key: same ref, same link. */
|
|
1171
|
+
externalRef: string;
|
|
1172
|
+
/** Optional display name; wins over an empty contact name, never overwrites a set one. */
|
|
1173
|
+
name?: string;
|
|
1174
|
+
phone?: string;
|
|
1175
|
+
}
|
|
1176
|
+
interface StoreCustomerLinkView {
|
|
1177
|
+
/** The buyer's account ref — what `customer.linked` carries and checkout accepts. */
|
|
1178
|
+
customerRef: string;
|
|
1179
|
+
/** The workspace contact this landed on. */
|
|
1180
|
+
contactRef: string;
|
|
1181
|
+
/** True when this call created the link; false when the externalRef was already linked. */
|
|
1182
|
+
alreadyLinked: boolean;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1045
1185
|
/** A resolved commission rate (after applying any per-workspace override over the platform default). */
|
|
1046
1186
|
interface EventCommissionConfig {
|
|
1047
1187
|
/** Percentage cut on the gross (0..100). */
|
|
@@ -2341,7 +2481,16 @@ declare function createStoreClient(opts: StoreClientOptions): {
|
|
|
2341
2481
|
idempotencyKey?: string;
|
|
2342
2482
|
}) => Promise<T>;
|
|
2343
2483
|
storefront: () => Promise<Storefront>;
|
|
2344
|
-
|
|
2484
|
+
/**
|
|
2485
|
+
* Every public product. With `updatedSince` (ISO 8601) it becomes a delta read for keeping a
|
|
2486
|
+
* local copy fresh: ordered oldest change first, `limit` pages it (max 200), and each summary's
|
|
2487
|
+
* `updatedAt` is the next cursor. The comparison is >= so the boundary row repeats — upsert by
|
|
2488
|
+
* ref. Deleted products arrive as the `product.deleted` webhook, not as an absence here.
|
|
2489
|
+
*/
|
|
2490
|
+
products: (params?: {
|
|
2491
|
+
updatedSince?: string;
|
|
2492
|
+
limit?: number;
|
|
2493
|
+
}) => Promise<PublicProductSummary[]>;
|
|
2345
2494
|
product: (slug: string) => Promise<PublicProduct>;
|
|
2346
2495
|
productSlots: (slug: string, opts: {
|
|
2347
2496
|
variantRef: string;
|
|
@@ -2398,6 +2547,23 @@ declare function createStoreClient(opts: StoreClientOptions): {
|
|
|
2398
2547
|
* stock until the session lapses. Cancelling an already-paid checkout returns 409.
|
|
2399
2548
|
*/
|
|
2400
2549
|
cancelCheckout: (ref: string) => Promise<StoreCheckoutSessionView>;
|
|
2550
|
+
/**
|
|
2551
|
+
* Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
|
|
2552
|
+
* your auth system verified. Idempotent per externalRef, and the response is identical whether
|
|
2553
|
+
* the contact existed before — it cannot be used to probe who is already a customer.
|
|
2554
|
+
*/
|
|
2555
|
+
linkCustomer: (input: StoreCustomerLinkInput) => Promise<StoreCustomerLinkView>;
|
|
2556
|
+
/**
|
|
2557
|
+
* Your own store orders, as a delta read. SERVER ONLY — needs a secret key, and the key's env
|
|
2558
|
+
* picks the set (a live key never sees test orders). Page forward on `updatedSince` using each
|
|
2559
|
+
* order's `updatedAt` as the next cursor and upsert by ref (the >= comparison repeats the
|
|
2560
|
+
* boundary row). A page shorter than `limit` means you are caught up. This is a sync and
|
|
2561
|
+
* back-office read: to confirm a single purchase, keep using `getCheckout`.
|
|
2562
|
+
*/
|
|
2563
|
+
orders: (params?: {
|
|
2564
|
+
updatedSince?: string;
|
|
2565
|
+
limit?: number;
|
|
2566
|
+
}) => Promise<StoreOrderRecord[]>;
|
|
2401
2567
|
};
|
|
2402
2568
|
type StoreClient = ReturnType<typeof createStoreClient>;
|
|
2403
2569
|
/**
|
package/dist/index.js
CHANGED
|
@@ -83,7 +83,19 @@ function createStoreClient(opts) {
|
|
|
83
83
|
request,
|
|
84
84
|
// ------------------------------------------------------------- read (fd_pk_)
|
|
85
85
|
storefront: /* @__PURE__ */ __name(() => request("/store/storefront"), "storefront"),
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Every public product. With `updatedSince` (ISO 8601) it becomes a delta read for keeping a
|
|
88
|
+
* local copy fresh: ordered oldest change first, `limit` pages it (max 200), and each summary's
|
|
89
|
+
* `updatedAt` is the next cursor. The comparison is >= so the boundary row repeats — upsert by
|
|
90
|
+
* ref. Deleted products arrive as the `product.deleted` webhook, not as an absence here.
|
|
91
|
+
*/
|
|
92
|
+
products: /* @__PURE__ */ __name((params) => {
|
|
93
|
+
const q = new URLSearchParams();
|
|
94
|
+
if (params?.updatedSince) q.set("updatedSince", params.updatedSince);
|
|
95
|
+
if (params?.limit) q.set("limit", String(params.limit));
|
|
96
|
+
const qs = q.toString();
|
|
97
|
+
return request(`/store/products${qs ? `?${qs}` : ""}`);
|
|
98
|
+
}, "products"),
|
|
87
99
|
product: /* @__PURE__ */ __name((slug) => request(`/store/products/${enc(slug)}`), "product"),
|
|
88
100
|
productSlots: /* @__PURE__ */ __name((slug, opts2) => request(`/store/products/${enc(slug)}/slots?variantRef=${enc(opts2.variantRef)}&date=${enc(opts2.date)}`), "productSlots"),
|
|
89
101
|
collections: /* @__PURE__ */ __name(() => request("/store/collections"), "collections"),
|
|
@@ -151,7 +163,30 @@ function createStoreClient(opts) {
|
|
|
151
163
|
*/
|
|
152
164
|
cancelCheckout: /* @__PURE__ */ __name((ref) => request(`/store/checkouts/${enc(ref)}/cancel`, {
|
|
153
165
|
method: "POST"
|
|
154
|
-
}), "cancelCheckout")
|
|
166
|
+
}), "cancelCheckout"),
|
|
167
|
+
/**
|
|
168
|
+
* Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
|
|
169
|
+
* your auth system verified. Idempotent per externalRef, and the response is identical whether
|
|
170
|
+
* the contact existed before — it cannot be used to probe who is already a customer.
|
|
171
|
+
*/
|
|
172
|
+
linkCustomer: /* @__PURE__ */ __name((input) => request("/store/customers/link", {
|
|
173
|
+
method: "POST",
|
|
174
|
+
body: JSON.stringify(input)
|
|
175
|
+
}), "linkCustomer"),
|
|
176
|
+
/**
|
|
177
|
+
* Your own store orders, as a delta read. SERVER ONLY — needs a secret key, and the key's env
|
|
178
|
+
* picks the set (a live key never sees test orders). Page forward on `updatedSince` using each
|
|
179
|
+
* order's `updatedAt` as the next cursor and upsert by ref (the >= comparison repeats the
|
|
180
|
+
* boundary row). A page shorter than `limit` means you are caught up. This is a sync and
|
|
181
|
+
* back-office read: to confirm a single purchase, keep using `getCheckout`.
|
|
182
|
+
*/
|
|
183
|
+
orders: /* @__PURE__ */ __name((params) => {
|
|
184
|
+
const q = new URLSearchParams();
|
|
185
|
+
if (params?.updatedSince) q.set("updatedSince", params.updatedSince);
|
|
186
|
+
if (params?.limit) q.set("limit", String(params.limit));
|
|
187
|
+
const qs = q.toString();
|
|
188
|
+
return request(`/store/orders${qs ? `?${qs}` : ""}`);
|
|
189
|
+
}, "orders")
|
|
155
190
|
};
|
|
156
191
|
}
|
|
157
192
|
__name(createStoreClient, "createStoreClient");
|
package/dist/index.mjs
CHANGED
|
@@ -49,7 +49,19 @@ function createStoreClient(opts) {
|
|
|
49
49
|
request,
|
|
50
50
|
// ------------------------------------------------------------- read (fd_pk_)
|
|
51
51
|
storefront: /* @__PURE__ */ __name(() => request("/store/storefront"), "storefront"),
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Every public product. With `updatedSince` (ISO 8601) it becomes a delta read for keeping a
|
|
54
|
+
* local copy fresh: ordered oldest change first, `limit` pages it (max 200), and each summary's
|
|
55
|
+
* `updatedAt` is the next cursor. The comparison is >= so the boundary row repeats — upsert by
|
|
56
|
+
* ref. Deleted products arrive as the `product.deleted` webhook, not as an absence here.
|
|
57
|
+
*/
|
|
58
|
+
products: /* @__PURE__ */ __name((params) => {
|
|
59
|
+
const q = new URLSearchParams();
|
|
60
|
+
if (params?.updatedSince) q.set("updatedSince", params.updatedSince);
|
|
61
|
+
if (params?.limit) q.set("limit", String(params.limit));
|
|
62
|
+
const qs = q.toString();
|
|
63
|
+
return request(`/store/products${qs ? `?${qs}` : ""}`);
|
|
64
|
+
}, "products"),
|
|
53
65
|
product: /* @__PURE__ */ __name((slug) => request(`/store/products/${enc(slug)}`), "product"),
|
|
54
66
|
productSlots: /* @__PURE__ */ __name((slug, opts2) => request(`/store/products/${enc(slug)}/slots?variantRef=${enc(opts2.variantRef)}&date=${enc(opts2.date)}`), "productSlots"),
|
|
55
67
|
collections: /* @__PURE__ */ __name(() => request("/store/collections"), "collections"),
|
|
@@ -117,7 +129,30 @@ function createStoreClient(opts) {
|
|
|
117
129
|
*/
|
|
118
130
|
cancelCheckout: /* @__PURE__ */ __name((ref) => request(`/store/checkouts/${enc(ref)}/cancel`, {
|
|
119
131
|
method: "POST"
|
|
120
|
-
}), "cancelCheckout")
|
|
132
|
+
}), "cancelCheckout"),
|
|
133
|
+
/**
|
|
134
|
+
* Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
|
|
135
|
+
* your auth system verified. Idempotent per externalRef, and the response is identical whether
|
|
136
|
+
* the contact existed before — it cannot be used to probe who is already a customer.
|
|
137
|
+
*/
|
|
138
|
+
linkCustomer: /* @__PURE__ */ __name((input) => request("/store/customers/link", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
body: JSON.stringify(input)
|
|
141
|
+
}), "linkCustomer"),
|
|
142
|
+
/**
|
|
143
|
+
* Your own store orders, as a delta read. SERVER ONLY — needs a secret key, and the key's env
|
|
144
|
+
* picks the set (a live key never sees test orders). Page forward on `updatedSince` using each
|
|
145
|
+
* order's `updatedAt` as the next cursor and upsert by ref (the >= comparison repeats the
|
|
146
|
+
* boundary row). A page shorter than `limit` means you are caught up. This is a sync and
|
|
147
|
+
* back-office read: to confirm a single purchase, keep using `getCheckout`.
|
|
148
|
+
*/
|
|
149
|
+
orders: /* @__PURE__ */ __name((params) => {
|
|
150
|
+
const q = new URLSearchParams();
|
|
151
|
+
if (params?.updatedSince) q.set("updatedSince", params.updatedSince);
|
|
152
|
+
if (params?.limit) q.set("limit", String(params.limit));
|
|
153
|
+
const qs = q.toString();
|
|
154
|
+
return request(`/store/orders${qs ? `?${qs}` : ""}`);
|
|
155
|
+
}, "orders")
|
|
121
156
|
};
|
|
122
157
|
}
|
|
123
158
|
__name(createStoreClient, "createStoreClient");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontdesk-africa/store-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Typed client for the FrontDesk Storefront API: catalogue, events, forms, hosted checkout and signed webhooks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://api.frontdesk.africa/v1/store/docs",
|