@frontdesk-africa/store-js 0.1.0 → 0.2.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 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
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[];
@@ -1042,6 +1072,34 @@ interface StoreCheckoutSessionView {
1042
1072
  createdAt: string;
1043
1073
  }
1044
1074
 
1075
+ /**
1076
+ * `POST /v1/store/customers/link` — attach the caller's own signed-in user to this workspace's
1077
+ * contacts (Supabase Phase 1c; design doc §7.6).
1078
+ *
1079
+ * TRUST MODEL: the caller holds a secret key, i.e. IS the merchant (or their installed backend).
1080
+ * They can already create and edit their own contacts in the portal, so accepting their assertion
1081
+ * that `email` was verified by THEIR auth system delegates nothing they do not have. The identity
1082
+ * being asserted must still be verified on the caller's side — that is stated at the API boundary,
1083
+ * not enforced here, because we cannot see their auth system.
1084
+ */
1085
+ interface StoreCustomerLinkInput {
1086
+ /** The email the CALLER's auth system verified. Never a guessed or user-typed-but-unverified one. */
1087
+ email: string;
1088
+ /** The caller's own user id (e.g. a Supabase Auth user id). Idempotency key: same ref, same link. */
1089
+ externalRef: string;
1090
+ /** Optional display name; wins over an empty contact name, never overwrites a set one. */
1091
+ name?: string;
1092
+ phone?: string;
1093
+ }
1094
+ interface StoreCustomerLinkView {
1095
+ /** The buyer's account ref — what `customer.linked` carries and checkout accepts. */
1096
+ customerRef: string;
1097
+ /** The workspace contact this landed on. */
1098
+ contactRef: string;
1099
+ /** True when this call created the link; false when the externalRef was already linked. */
1100
+ alreadyLinked: boolean;
1101
+ }
1102
+
1045
1103
  /** A resolved commission rate (after applying any per-workspace override over the platform default). */
1046
1104
  interface EventCommissionConfig {
1047
1105
  /** Percentage cut on the gross (0..100). */
@@ -2398,6 +2456,12 @@ declare function createStoreClient(opts: StoreClientOptions): {
2398
2456
  * stock until the session lapses. Cancelling an already-paid checkout returns 409.
2399
2457
  */
2400
2458
  cancelCheckout: (ref: string) => Promise<StoreCheckoutSessionView>;
2459
+ /**
2460
+ * Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
2461
+ * your auth system verified. Idempotent per externalRef, and the response is identical whether
2462
+ * the contact existed before — it cannot be used to probe who is already a customer.
2463
+ */
2464
+ linkCustomer: (input: StoreCustomerLinkInput) => Promise<StoreCustomerLinkView>;
2401
2465
  };
2402
2466
  type StoreClient = ReturnType<typeof createStoreClient>;
2403
2467
  /**
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[];
@@ -1042,6 +1072,34 @@ interface StoreCheckoutSessionView {
1042
1072
  createdAt: string;
1043
1073
  }
1044
1074
 
1075
+ /**
1076
+ * `POST /v1/store/customers/link` — attach the caller's own signed-in user to this workspace's
1077
+ * contacts (Supabase Phase 1c; design doc §7.6).
1078
+ *
1079
+ * TRUST MODEL: the caller holds a secret key, i.e. IS the merchant (or their installed backend).
1080
+ * They can already create and edit their own contacts in the portal, so accepting their assertion
1081
+ * that `email` was verified by THEIR auth system delegates nothing they do not have. The identity
1082
+ * being asserted must still be verified on the caller's side — that is stated at the API boundary,
1083
+ * not enforced here, because we cannot see their auth system.
1084
+ */
1085
+ interface StoreCustomerLinkInput {
1086
+ /** The email the CALLER's auth system verified. Never a guessed or user-typed-but-unverified one. */
1087
+ email: string;
1088
+ /** The caller's own user id (e.g. a Supabase Auth user id). Idempotency key: same ref, same link. */
1089
+ externalRef: string;
1090
+ /** Optional display name; wins over an empty contact name, never overwrites a set one. */
1091
+ name?: string;
1092
+ phone?: string;
1093
+ }
1094
+ interface StoreCustomerLinkView {
1095
+ /** The buyer's account ref — what `customer.linked` carries and checkout accepts. */
1096
+ customerRef: string;
1097
+ /** The workspace contact this landed on. */
1098
+ contactRef: string;
1099
+ /** True when this call created the link; false when the externalRef was already linked. */
1100
+ alreadyLinked: boolean;
1101
+ }
1102
+
1045
1103
  /** A resolved commission rate (after applying any per-workspace override over the platform default). */
1046
1104
  interface EventCommissionConfig {
1047
1105
  /** Percentage cut on the gross (0..100). */
@@ -2398,6 +2456,12 @@ declare function createStoreClient(opts: StoreClientOptions): {
2398
2456
  * stock until the session lapses. Cancelling an already-paid checkout returns 409.
2399
2457
  */
2400
2458
  cancelCheckout: (ref: string) => Promise<StoreCheckoutSessionView>;
2459
+ /**
2460
+ * Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
2461
+ * your auth system verified. Idempotent per externalRef, and the response is identical whether
2462
+ * the contact existed before — it cannot be used to probe who is already a customer.
2463
+ */
2464
+ linkCustomer: (input: StoreCustomerLinkInput) => Promise<StoreCustomerLinkView>;
2401
2465
  };
2402
2466
  type StoreClient = ReturnType<typeof createStoreClient>;
2403
2467
  /**
package/dist/index.js CHANGED
@@ -151,7 +151,16 @@ function createStoreClient(opts) {
151
151
  */
152
152
  cancelCheckout: /* @__PURE__ */ __name((ref) => request(`/store/checkouts/${enc(ref)}/cancel`, {
153
153
  method: "POST"
154
- }), "cancelCheckout")
154
+ }), "cancelCheckout"),
155
+ /**
156
+ * Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
157
+ * your auth system verified. Idempotent per externalRef, and the response is identical whether
158
+ * the contact existed before — it cannot be used to probe who is already a customer.
159
+ */
160
+ linkCustomer: /* @__PURE__ */ __name((input) => request("/store/customers/link", {
161
+ method: "POST",
162
+ body: JSON.stringify(input)
163
+ }), "linkCustomer")
155
164
  };
156
165
  }
157
166
  __name(createStoreClient, "createStoreClient");
package/dist/index.mjs CHANGED
@@ -117,7 +117,16 @@ function createStoreClient(opts) {
117
117
  */
118
118
  cancelCheckout: /* @__PURE__ */ __name((ref) => request(`/store/checkouts/${enc(ref)}/cancel`, {
119
119
  method: "POST"
120
- }), "cancelCheckout")
120
+ }), "cancelCheckout"),
121
+ /**
122
+ * Attach YOUR OWN signed-in user to this workspace's contacts. SERVER ONLY. Only send an email
123
+ * your auth system verified. Idempotent per externalRef, and the response is identical whether
124
+ * the contact existed before — it cannot be used to probe who is already a customer.
125
+ */
126
+ linkCustomer: /* @__PURE__ */ __name((input) => request("/store/customers/link", {
127
+ method: "POST",
128
+ body: JSON.stringify(input)
129
+ }), "linkCustomer")
121
130
  };
122
131
  }
123
132
  __name(createStoreClient, "createStoreClient");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontdesk-africa/store-js",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",