@shoppexio/storefront 1.0.60 → 1.0.61

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.61
4
+
5
+ - Add `mountCheckoutChallenge()` so custom storefront domains can complete the hosted checkout verification broker and retry invoice creation.
6
+
3
7
  ## 1.0.60
4
8
 
5
9
  - Reject post-purchase `redirectUrl` values that still contain unresolved template placeholders.
package/dist/index.cjs CHANGED
@@ -71,6 +71,7 @@ __export(src_exports, {
71
71
  loyalty: () => loyalty,
72
72
  me: () => me,
73
73
  mergeSettings: () => mergeSettings,
74
+ mountCheckoutChallenge: () => mountCheckoutChallenge,
74
75
  normalizeSearchQuery: () => normalizeSearchQuery,
75
76
  normalizeStorefrontCustomFields: () => normalizeStorefrontCustomFields,
76
77
  order: () => order,
@@ -15638,7 +15639,7 @@ var PaymentGatewayStateSchema = object({
15638
15639
  });
15639
15640
 
15640
15641
  // ../contracts/src/style-center.ts
15641
- var CHECKOUT_PLATFORM_BRAND_COLOR = "#2563eb";
15642
+ var CHECKOUT_PLATFORM_BRAND_COLOR = "#7c3aed";
15642
15643
  var CHECKOUT_PLATFORM_FONT_FAMILY = "Geist";
15643
15644
  var checkoutStyleSurfaceValues = ["checkout", "payment_link", "embed"];
15644
15645
  var checkoutStyleDensityValues = ["comfortable", "compact"];
@@ -15665,17 +15666,28 @@ var checkoutStyleTokenDefinitions = [
15665
15666
  // background. Setting an explicit hex would clobber the ambient gradient and
15666
15667
  // the right-panel `--surface-2` accent for every default-theme checkout.
15667
15668
  { key: "color.background", cssVar: "--spx-checkout-bg", group: "color", type: "color", default: "" },
15668
- { key: "color.surface", cssVar: "--spx-checkout-surface", group: "color", type: "color", default: "#18181b" },
15669
- { key: "color.surfaceRaised", cssVar: "--spx-checkout-surface-raised", group: "color", type: "color", default: "#27272a" },
15670
- { key: "color.text", cssVar: "--spx-checkout-text", group: "color", type: "color", default: "#fafafa", protected: true },
15671
- { key: "color.textMuted", cssVar: "--spx-checkout-text-muted", group: "color", type: "color", default: "#a1a1aa" },
15672
- // Default '#3f3f46' (zinc-700) feeds many runtime-style fallbacks for
15673
- // payment-method cards, product cards, pills, and inputs. Setting it to ''
15674
- // would turn those slots' `var(--spx-checkout-border, currentColor)` into
15675
- // bright text-color borders. The grey-divider regression on the aside is
15676
- // fixed in the layout file by using the Tailwind `border-border` class
15677
- // instead of binding to this token, which keeps both concerns separated.
15678
- { key: "color.border", cssVar: "--spx-checkout-border", group: "color", type: "color", default: "#222222" },
15669
+ // The four neutrals below are the DARK appearance of the hosted checkout's
15670
+ // tonal ladder (`apps/checkout/app/globals.css`); the light values sit in
15671
+ // CHECKOUT_PLATFORM_BASELINE_LIGHT_DEFAULTS. They are materialised into the
15672
+ // live checkout root, so they are what a shop with no Style Center theme
15673
+ // actually renders which is why each one has to name the SAME rung the
15674
+ // component paints, not a palette of its own.
15675
+ // surface → `--surface-0`, the working panel the buyer pays in
15676
+ // surfaceRaised `--card`, a panel raised above that panel
15677
+ // (the provider widget frame and its loading skeleton)
15678
+ // text → `--foreground`
15679
+ // textMuted → `--muted-foreground`
15680
+ // border → `--border`
15681
+ { key: "color.surface", cssVar: "--spx-checkout-surface", group: "color", type: "color", default: "#1b1b1b" },
15682
+ { key: "color.surfaceRaised", cssVar: "--spx-checkout-surface-raised", group: "color", type: "color", default: "#222222" },
15683
+ { key: "color.text", cssVar: "--spx-checkout-text", group: "color", type: "color", default: "#f5f5f5", protected: true },
15684
+ { key: "color.textMuted", cssVar: "--spx-checkout-text-muted", group: "color", type: "color", default: "#a1a1a1" },
15685
+ // Never '': the border token feeds runtime-style fallbacks for inputs,
15686
+ // provider widgets and buttons, and an unset value turns those slots'
15687
+ // `var(--spx-checkout-border, …)` chains loose. The hosted checkout draws
15688
+ // almost no hairlines any more — the slots that still do read this, the rest
15689
+ // resolve their border to `transparent` in the system CSS.
15690
+ { key: "color.border", cssVar: "--spx-checkout-border", group: "color", type: "color", default: "#2c2c2c" },
15679
15691
  { key: "color.focus", cssVar: "--spx-checkout-focus", group: "color", type: "color", default: CHECKOUT_PLATFORM_BRAND_COLOR, protected: true },
15680
15692
  { key: "color.success", cssVar: "--spx-checkout-success", group: "color", type: "color", default: "#22c55e", protected: true },
15681
15693
  { key: "color.warning", cssVar: "--spx-checkout-warning", group: "color", type: "color", default: "#f59e0b", protected: true },
@@ -15687,9 +15699,16 @@ var checkoutStyleTokenDefinitions = [
15687
15699
  // actually loaded.
15688
15700
  { key: "typography.googleFontFamily", cssVar: "--spx-checkout-google-font", group: "typography", type: "font", default: "" },
15689
15701
  { key: "typography.baseSize", cssVar: "--spx-checkout-font-size", group: "typography", type: "number", default: 14, min: 12, max: 18, step: 1, unit: "px" },
15690
- { key: "shape.buttonRadius", cssVar: "--spx-checkout-button-radius", group: "shape", type: "number", default: 8, min: 0, max: 24, step: 1, unit: "px" },
15691
- { key: "shape.inputRadius", cssVar: "--spx-checkout-input-radius", group: "shape", type: "number", default: 8, min: 0, max: 24, step: 1, unit: "px" },
15692
- { key: "shape.cardRadius", cssVar: "--spx-checkout-card-radius", group: "shape", type: "number", default: 12, min: 0, max: 28, step: 1, unit: "px" },
15702
+ // 12px, not 8, for the same reason as the input below: the hosted pay CTA is
15703
+ // `rounded-xl` and this token is materialised over every slotted button. At 8
15704
+ // the store-credit and manual actions rendered one step tighter than the
15705
+ // field directly above them and than the CTA they stand in for.
15706
+ { key: "shape.buttonRadius", cssVar: "--spx-checkout-button-radius", group: "shape", type: "number", default: 10, min: 0, max: 24, step: 1, unit: "px" },
15707
+ // 12px, not 8: the hosted `Input` primitive is `rounded-lg`, and this token is
15708
+ // materialised over it. At 8 the system CSS rounded every field one step
15709
+ // tighter than the component that drew it.
15710
+ { key: "shape.inputRadius", cssVar: "--spx-checkout-input-radius", group: "shape", type: "number", default: 10, min: 0, max: 24, step: 1, unit: "px" },
15711
+ { key: "shape.cardRadius", cssVar: "--spx-checkout-card-radius", group: "shape", type: "number", default: 10, min: 0, max: 28, step: 1, unit: "px" },
15693
15712
  {
15694
15713
  key: "spacing.density",
15695
15714
  cssVar: "--spx-checkout-density",
@@ -15701,8 +15720,8 @@ var checkoutStyleTokenDefinitions = [
15701
15720
  { key: "spacing.controlHeight", cssVar: "--spx-checkout-control-height", group: "spacing", type: "number", default: 48, min: 36, max: 56, step: 2, unit: "px" },
15702
15721
  { key: "component.primaryButton.background", cssVar: "--spx-checkout-button-bg", group: "component", type: "color", default: "", protected: true },
15703
15722
  { key: "component.primaryButton.text", cssVar: "--spx-checkout-button-text", group: "component", type: "color", default: "#ffffff", protected: true },
15704
- { key: "component.input.background", cssVar: "--spx-checkout-input-bg", group: "component", type: "color", default: "#111111" },
15705
- { key: "component.input.border", cssVar: "--spx-checkout-input-border", group: "component", type: "color", default: "#222222" },
15723
+ { key: "component.input.background", cssVar: "--spx-checkout-input-bg", group: "component", type: "color", default: "#292929" },
15724
+ { key: "component.input.border", cssVar: "--spx-checkout-input-border", group: "component", type: "color", default: "#2c2c2c" },
15706
15725
  { key: "component.input.focusRing", cssVar: "--spx-checkout-input-focus-ring", group: "component", type: "color", default: "" },
15707
15726
  // Default '' is filtered out by the preview-iframe normaliser, so the
15708
15727
  // CSS fallback chain on [data-spx-slot="summary.panel"] resolves to
@@ -15727,14 +15746,14 @@ var checkoutStyleTokenDefinitions = [
15727
15746
  { key: "component.brandAvatar.border", cssVar: "--spx-checkout-brand-avatar-border", group: "component", type: "color", default: "rgba(255,255,255,0.1)" },
15728
15747
  { key: "component.pill.background", cssVar: "--spx-checkout-pill-bg", group: "component", type: "color", default: "rgba(255,255,255,0.04)" },
15729
15748
  { key: "component.pill.border", cssVar: "--spx-checkout-pill-border", group: "component", type: "color", default: "rgba(255,255,255,0.08)" },
15730
- { key: "component.pill.text", cssVar: "--spx-checkout-pill-text", group: "component", type: "color", default: "#a1a1aa" },
15749
+ { key: "component.pill.text", cssVar: "--spx-checkout-pill-text", group: "component", type: "color", default: "#a1a1a1" },
15731
15750
  { key: "component.errorBanner.background", cssVar: "--spx-checkout-error-bg", group: "component", type: "color", default: "rgba(239,68,68,0.1)" },
15732
15751
  { key: "component.errorBanner.border", cssVar: "--spx-checkout-error-border", group: "component", type: "color", default: "rgba(239,68,68,0.2)" },
15733
15752
  { key: "component.errorBanner.text", cssVar: "--spx-checkout-error-text", group: "component", type: "color", default: "#ef4444" },
15734
15753
  { key: "component.warningBanner.background", cssVar: "--spx-checkout-warning-bg", group: "component", type: "color", default: "" },
15735
15754
  { key: "component.warningBanner.border", cssVar: "--spx-checkout-warning-border", group: "component", type: "color", default: "" },
15736
15755
  { key: "component.warningBanner.text", cssVar: "--spx-checkout-warning-text", group: "component", type: "color", default: "" },
15737
- { key: "component.radio.idleRing", cssVar: "--spx-checkout-radio-idle-ring", group: "component", type: "color", default: "rgba(161,161,170,0.5)" },
15756
+ { key: "component.radio.idleRing", cssVar: "--spx-checkout-radio-idle-ring", group: "component", type: "color", default: "rgba(161,161,161,0.5)" },
15738
15757
  { key: "component.radio.checkedFill", cssVar: "--spx-checkout-radio-checked-fill", group: "component", type: "color", default: "" },
15739
15758
  { key: "component.radio.checkedIcon", cssVar: "--spx-checkout-radio-checked-icon", group: "component", type: "color", default: "#ffffff" },
15740
15759
  { key: "component.divider.color", cssVar: "--spx-checkout-divider", group: "component", type: "color", default: "rgba(255,255,255,0.06)" }
@@ -16536,6 +16555,32 @@ var externalPaymentAdapterConformanceResponseSchema = externalPaymentAdapterBase
16536
16555
  });
16537
16556
 
16538
16557
  // ../contracts/src/redirect-link-template.ts
16558
+ var REMAINING_PLACEHOLDER_PATTERNS = [
16559
+ /\{\{[\s\S]*?\}\}/g,
16560
+ /(?<!\{)\{[^{}]+\}(?!\})/g
16561
+ ];
16562
+ function findDanglingBraceFragments(template) {
16563
+ let stripped = template;
16564
+ for (const pattern of REMAINING_PLACEHOLDER_PATTERNS) {
16565
+ stripped = stripped.replace(pattern, "");
16566
+ }
16567
+ return /[{}]/.test(stripped) ? [...new Set(stripped.match(/[{}]+/g) ?? [])] : [];
16568
+ }
16569
+ function findRemainingProductRedirectPlaceholders(template) {
16570
+ if (!template.trim()) {
16571
+ return [];
16572
+ }
16573
+ const remaining = /* @__PURE__ */ new Set();
16574
+ for (const pattern of REMAINING_PLACEHOLDER_PATTERNS) {
16575
+ for (const match of template.matchAll(pattern)) {
16576
+ remaining.add(match[0]);
16577
+ }
16578
+ }
16579
+ for (const fragment of findDanglingBraceFragments(template)) {
16580
+ remaining.add(fragment);
16581
+ }
16582
+ return [...remaining];
16583
+ }
16539
16584
  function isSafeHttpsRedirectTemplateUrl(url2) {
16540
16585
  const trimmed = url2.trim();
16541
16586
  if (!trimmed) {
@@ -16597,6 +16642,11 @@ function requirePriceDisplayUnlessPayWhatYouWant(product, ctx) {
16597
16642
  }
16598
16643
  var publicInvoiceWireProductSchema = external_exports.object({
16599
16644
  addons: external_exports.array(publicInvoiceWireProductAddonSchema).optional(),
16645
+ /** Per-line cart-edit capability from the enricher; the checkout renders no
16646
+ * control without an explicit true. */
16647
+ quantity_editable: external_exports.boolean().optional(),
16648
+ addons_editable: external_exports.boolean().optional(),
16649
+ removable: external_exports.boolean().optional(),
16600
16650
  available_addons: external_exports.array(external_exports.unknown()).optional(),
16601
16651
  cloudflare_image_id: external_exports.string().nullable(),
16602
16652
  created_at: external_exports.string().optional(),
@@ -19916,6 +19966,9 @@ function resolvePostPurchaseRedirectUrl(redirectUrl) {
19916
19966
  if (!isSafeHttpsRedirectTemplateUrl(normalized)) {
19917
19967
  return { value: null, error: "redirectUrl must be an absolute HTTPS URL." };
19918
19968
  }
19969
+ if (findRemainingProductRedirectPlaceholders(normalized).length > 0) {
19970
+ return { value: null, error: "redirectUrl must not contain template placeholders." };
19971
+ }
19919
19972
  return { value: normalized, error: null };
19920
19973
  }
19921
19974
  var pendingCheckoutCreates = /* @__PURE__ */ new Map();
@@ -20328,6 +20381,67 @@ function buildCheckoutUrlSync() {
20328
20381
  throw new Error("buildCheckoutUrlSync is deprecated. Use buildCheckoutUrl (async) instead.");
20329
20382
  }
20330
20383
 
20384
+ // ../sdk/src/modules/checkout-challenge.ts
20385
+ var TURNSTILE_FRAME_MESSAGE_SOURCE = "shoppex-turnstile";
20386
+ var TURNSTILE_FRAME_MESSAGE_VERSION = 1;
20387
+ function readFrameMessage(value, nonce) {
20388
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
20389
+ const record2 = value;
20390
+ if (record2.source !== TURNSTILE_FRAME_MESSAGE_SOURCE || record2.version !== TURNSTILE_FRAME_MESSAGE_VERSION || record2.nonce !== nonce || !["success", "expired", "timeout", "error"].includes(String(record2.type))) return null;
20391
+ if (record2.type === "success" && (typeof record2.token !== "string" || !record2.token.trim())) {
20392
+ return null;
20393
+ }
20394
+ return {
20395
+ type: record2.type,
20396
+ ...typeof record2.token === "string" ? { token: record2.token.trim() } : {}
20397
+ };
20398
+ }
20399
+ function mountCheckoutChallenge(container, challenge, callbacks) {
20400
+ if (challenge.provider !== "turnstile" || !challenge.siteKey.trim()) {
20401
+ throw new Error("Checkout challenge is invalid.");
20402
+ }
20403
+ const win = container.ownerDocument.defaultView;
20404
+ if (!win) throw new Error("Checkout challenge requires a browser document.");
20405
+ const checkoutBaseUrl = getConfig().checkoutBaseUrl;
20406
+ const frameUrl = new URL("/turnstile", checkoutBaseUrl);
20407
+ if (frameUrl.protocol !== "https:" && frameUrl.protocol !== "http:") {
20408
+ throw new Error("Checkout base URL must use http or https.");
20409
+ }
20410
+ const nonce = win.crypto.randomUUID();
20411
+ frameUrl.searchParams.set("site_key", challenge.siteKey.trim());
20412
+ frameUrl.searchParams.set("nonce", nonce);
20413
+ const frame = container.ownerDocument.createElement("iframe");
20414
+ frame.src = frameUrl.toString();
20415
+ frame.title = "Checkout verification";
20416
+ frame.referrerPolicy = "no-referrer";
20417
+ frame.style.border = "0";
20418
+ frame.style.width = "100%";
20419
+ frame.style.height = "72px";
20420
+ let disposed = false;
20421
+ const onMessage = (event) => {
20422
+ if (disposed || event.origin !== frameUrl.origin || event.source !== frame.contentWindow) return;
20423
+ const message = readFrameMessage(event.data, nonce);
20424
+ if (!message) return;
20425
+ if (message.type === "success") callbacks.onSuccess(message.token);
20426
+ if (message.type === "expired" || message.type === "timeout") callbacks.onExpired?.();
20427
+ if (message.type === "error") callbacks.onUnavailable?.();
20428
+ };
20429
+ const onFrameError = () => callbacks.onUnavailable?.();
20430
+ win.addEventListener("message", onMessage);
20431
+ frame.addEventListener("error", onFrameError, { once: true });
20432
+ container.appendChild(frame);
20433
+ return {
20434
+ element: frame,
20435
+ dispose() {
20436
+ if (disposed) return;
20437
+ disposed = true;
20438
+ win.removeEventListener("message", onMessage);
20439
+ frame.removeEventListener("error", onFrameError);
20440
+ frame.remove();
20441
+ }
20442
+ };
20443
+ }
20444
+
20331
20445
  // ../sdk/src/modules/coupons.ts
20332
20446
  async function resolveShopId() {
20333
20447
  const cachedShopId2 = getShopId();
@@ -21337,6 +21451,7 @@ var shoppex = {
21337
21451
  checkout,
21338
21452
  buildCheckoutUrl,
21339
21453
  buildCheckoutUrlSync,
21454
+ mountCheckoutChallenge,
21340
21455
  // Affiliates
21341
21456
  captureAffiliateFromUrl,
21342
21457
  validateAffiliateCode,
@@ -21485,6 +21600,7 @@ if (typeof window !== "undefined") {
21485
21600
  loyalty,
21486
21601
  me,
21487
21602
  mergeSettings,
21603
+ mountCheckoutChallenge,
21488
21604
  normalizeSearchQuery,
21489
21605
  normalizeStorefrontCustomFields,
21490
21606
  order,