@shoppexio/storefront 1.0.78 → 1.0.79

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/dist/index.d.cts CHANGED
@@ -591,6 +591,33 @@ interface AffiliateValidation {
591
591
  discount_active: boolean;
592
592
  discount_percent: number;
593
593
  message?: string;
594
+ /**
595
+ * The attribution policy that governs this code (ADR-0067).
596
+ *
597
+ * Absent when the code did not resolve, or when an older API answered. A
598
+ * caller storing the code itself must apply these rather than the 30-day
599
+ * last-click default, or a first-click tier and any non-30-day window are
600
+ * silently lost.
601
+ */
602
+ attribution_mode?: 'LAST_CLICK' | 'FIRST_CLICK';
603
+ attribution_window_days?: number;
604
+ /**
605
+ * The policy of the attribution the browser already holds, when one was sent.
606
+ * Null when that code no longer resolves — the stored mode is then the only
607
+ * information left.
608
+ */
609
+ held_attribution_mode?: 'LAST_CLICK' | 'FIRST_CLICK' | null;
610
+ /**
611
+ * `applyAffiliateCode` only: whether the code you passed is the one now
612
+ * attributed.
613
+ *
614
+ * False when an unexpired first-click attribution kept the sale — the code
615
+ * you passed is still valid, it just did not win. `affiliate_code` then
616
+ * carries the code that DID, because the documented integration feeds that
617
+ * field straight into `checkout({ affiliateCode })` and an explicit checkout
618
+ * code overrides storage.
619
+ */
620
+ applied?: boolean;
594
621
  }
595
622
  interface ProductGroup {
596
623
  id?: string;
@@ -1589,6 +1616,12 @@ declare function getStoredAffiliate(): {
1589
1616
  code: string;
1590
1617
  mode: AffiliateAttributionMode;
1591
1618
  windowDays: number;
1619
+ /**
1620
+ * When this entry expires, as written. Reported so a hold that has to be put
1621
+ * back after an early overwrite keeps its original expiry — the window runs
1622
+ * from the click that earned it, not from the landing that failed to take it.
1623
+ */
1624
+ expiresAt: number;
1592
1625
  } | null;
1593
1626
  declare function clearAffiliateCode(): void;
1594
1627
  declare function getAffiliateCode(): string | null;
@@ -1610,6 +1643,23 @@ declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_starte
1610
1643
  dedupeKey?: string;
1611
1644
  }): Promise<void>;
1612
1645
  declare function validateAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1646
+ /**
1647
+ * Validate a manually entered code and store it (ADR-0067).
1648
+ *
1649
+ * Attribution is governed by the same rules as a `?ref=` landing, deliberately:
1650
+ * a themed "apply code" box must not be a way around first-click, or the rule
1651
+ * is bypassed by any storefront that offers one. So a still-protected hold
1652
+ * keeps the sale, and the stored entry always carries the mode and window the
1653
+ * server just reported — the old unconditional `setAffiliateCode(code)` wrote
1654
+ * a 30-day last-click entry whatever the shop or tier had configured.
1655
+ *
1656
+ * The result reports what is now ATTRIBUTED, not merely what was validated:
1657
+ * `affiliate_code` carries the winning code and `applied` says whether that is
1658
+ * the one you passed. The documented integration hands `affiliate_code`
1659
+ * straight to `checkout({ affiliateCode })`, and an explicit checkout code
1660
+ * overrides storage — so returning the losing code here would credit it and
1661
+ * walk right around the hold this function just honoured.
1662
+ */
1613
1663
  declare function applyAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1614
1664
  /**
1615
1665
  * Capture an affiliate code from the current URL and store it (ADR-0067).
package/dist/index.d.ts CHANGED
@@ -591,6 +591,33 @@ interface AffiliateValidation {
591
591
  discount_active: boolean;
592
592
  discount_percent: number;
593
593
  message?: string;
594
+ /**
595
+ * The attribution policy that governs this code (ADR-0067).
596
+ *
597
+ * Absent when the code did not resolve, or when an older API answered. A
598
+ * caller storing the code itself must apply these rather than the 30-day
599
+ * last-click default, or a first-click tier and any non-30-day window are
600
+ * silently lost.
601
+ */
602
+ attribution_mode?: 'LAST_CLICK' | 'FIRST_CLICK';
603
+ attribution_window_days?: number;
604
+ /**
605
+ * The policy of the attribution the browser already holds, when one was sent.
606
+ * Null when that code no longer resolves — the stored mode is then the only
607
+ * information left.
608
+ */
609
+ held_attribution_mode?: 'LAST_CLICK' | 'FIRST_CLICK' | null;
610
+ /**
611
+ * `applyAffiliateCode` only: whether the code you passed is the one now
612
+ * attributed.
613
+ *
614
+ * False when an unexpired first-click attribution kept the sale — the code
615
+ * you passed is still valid, it just did not win. `affiliate_code` then
616
+ * carries the code that DID, because the documented integration feeds that
617
+ * field straight into `checkout({ affiliateCode })` and an explicit checkout
618
+ * code overrides storage.
619
+ */
620
+ applied?: boolean;
594
621
  }
595
622
  interface ProductGroup {
596
623
  id?: string;
@@ -1589,6 +1616,12 @@ declare function getStoredAffiliate(): {
1589
1616
  code: string;
1590
1617
  mode: AffiliateAttributionMode;
1591
1618
  windowDays: number;
1619
+ /**
1620
+ * When this entry expires, as written. Reported so a hold that has to be put
1621
+ * back after an early overwrite keeps its original expiry — the window runs
1622
+ * from the click that earned it, not from the landing that failed to take it.
1623
+ */
1624
+ expiresAt: number;
1592
1625
  } | null;
1593
1626
  declare function clearAffiliateCode(): void;
1594
1627
  declare function getAffiliateCode(): string | null;
@@ -1610,6 +1643,23 @@ declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_starte
1610
1643
  dedupeKey?: string;
1611
1644
  }): Promise<void>;
1612
1645
  declare function validateAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1646
+ /**
1647
+ * Validate a manually entered code and store it (ADR-0067).
1648
+ *
1649
+ * Attribution is governed by the same rules as a `?ref=` landing, deliberately:
1650
+ * a themed "apply code" box must not be a way around first-click, or the rule
1651
+ * is bypassed by any storefront that offers one. So a still-protected hold
1652
+ * keeps the sale, and the stored entry always carries the mode and window the
1653
+ * server just reported — the old unconditional `setAffiliateCode(code)` wrote
1654
+ * a 30-day last-click entry whatever the shop or tier had configured.
1655
+ *
1656
+ * The result reports what is now ATTRIBUTED, not merely what was validated:
1657
+ * `affiliate_code` carries the winning code and `applied` says whether that is
1658
+ * the one you passed. The documented integration hands `affiliate_code`
1659
+ * straight to `checkout({ affiliateCode })`, and an explicit checkout code
1660
+ * overrides storage — so returning the losing code here would credit it and
1661
+ * walk right around the hold this function just honoured.
1662
+ */
1613
1663
  declare function applyAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1614
1664
  /**
1615
1665
  * Capture an affiliate code from the current URL and store it (ADR-0067).
package/dist/index.js CHANGED
@@ -3922,6 +3922,9 @@ function safeWrite(value) {
3922
3922
  } catch {
3923
3923
  }
3924
3924
  }
3925
+ function restoreStoredAffiliate(value) {
3926
+ safeWrite(value);
3927
+ }
3925
3928
  function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS, mode) {
3926
3929
  const normalized = normalizeAffiliateCode(code);
3927
3930
  if (!normalized) {
@@ -3946,7 +3949,7 @@ function getStoredAffiliate() {
3946
3949
  const code = normalizeAffiliateCode(stored.code);
3947
3950
  if (!code) return null;
3948
3951
  const windowDays = typeof stored.windowDays === "number" && stored.windowDays > 0 ? stored.windowDays : DEFAULT_TTL_DAYS;
3949
- return { code, mode: normalizeMode(stored.mode), windowDays };
3952
+ return { code, mode: normalizeMode(stored.mode), windowDays, expiresAt: stored.expiresAt };
3950
3953
  }
3951
3954
  function clearAffiliateCode() {
3952
3955
  if (typeof window === "undefined") return;
@@ -4037,11 +4040,13 @@ async function validateAffiliateCode(code) {
4037
4040
  };
4038
4041
  }
4039
4042
  const config = getConfig();
4043
+ const held = getStoredAffiliate();
4040
4044
  const response = await post(
4041
4045
  "/v1/storefront/affiliates/resolve",
4042
4046
  {
4043
4047
  shop_slug: config.storeSlug,
4044
- code: normalizedCode
4048
+ code: normalizedCode,
4049
+ ...held && held.code !== normalizedCode ? { held_code: held.code } : {}
4045
4050
  },
4046
4051
  { retries: 0 }
4047
4052
  );
@@ -4069,15 +4074,36 @@ async function validateAffiliateCode(code) {
4069
4074
  ...response.data.program_enabled !== void 0 ? { program_enabled: response.data.program_enabled } : {},
4070
4075
  affiliate_code: normalizeAffiliateCode(response.data.affiliate_code),
4071
4076
  discount_active: Boolean(response.data.discount_active),
4072
- discount_percent: Number(response.data.discount_percent ?? 0)
4077
+ discount_percent: Number(response.data.discount_percent ?? 0),
4078
+ // Passed through rather than dropped: a caller that stores the code has
4079
+ // no other way to learn the policy, and defaulting to 30-day last-click
4080
+ // would quietly discard a first-click tier and any custom window.
4081
+ attribution_mode: normalizeMode(response.data.attribution_mode),
4082
+ attribution_window_days: typeof response.data.attribution_window_days === "number" && response.data.attribution_window_days >= 1 ? Math.trunc(response.data.attribution_window_days) : DEFAULT_TTL_DAYS,
4083
+ held_attribution_mode: response.data.held_attribution_mode === "FIRST_CLICK" || response.data.held_attribution_mode === "LAST_CLICK" ? response.data.held_attribution_mode : null
4073
4084
  },
4074
4085
  ...response.message ? { message: response.message } : {}
4075
4086
  };
4076
4087
  }
4077
4088
  async function applyAffiliateCode(code) {
4089
+ const held = getStoredAffiliate();
4078
4090
  const result = await validateAffiliateCode(code);
4079
4091
  if (result.success && result.data?.affiliate_code) {
4080
- setAffiliateCode(result.data.affiliate_code);
4092
+ const applied = result.data.affiliate_code;
4093
+ const holdsOtherCode = held !== null && held.code !== applied;
4094
+ const heldMode = holdsOtherCode ? result.data.held_attribution_mode ?? held.mode : null;
4095
+ if (heldMode === "FIRST_CLICK" && held) {
4096
+ return {
4097
+ ...result,
4098
+ data: { ...result.data, affiliate_code: held.code, applied: false }
4099
+ };
4100
+ }
4101
+ setAffiliateCode(
4102
+ applied,
4103
+ result.data.attribution_window_days ?? DEFAULT_TTL_DAYS,
4104
+ result.data.attribution_mode
4105
+ );
4106
+ return { ...result, data: { ...result.data, applied: true } };
4081
4107
  }
4082
4108
  return result;
4083
4109
  }
@@ -4094,7 +4120,8 @@ async function captureAffiliateFromUrl(param = "ref") {
4094
4120
  code = normalizeAffiliateCode(code);
4095
4121
  if (!code) return null;
4096
4122
  const held = getStoredAffiliate();
4097
- const mayHoldFirstClick = held !== null && held.mode === "FIRST_CLICK" && held.code !== code;
4123
+ const holdsOtherCode = held !== null && held.code !== code;
4124
+ const mayHoldFirstClick = holdsOtherCode && held.mode === "FIRST_CLICK";
4098
4125
  if (!mayHoldFirstClick) {
4099
4126
  setAffiliateCode(code, held?.windowDays ?? DEFAULT_TTL_DAYS, held?.mode);
4100
4127
  }
@@ -4103,13 +4130,29 @@ async function captureAffiliateFromUrl(param = "ref") {
4103
4130
  const config = getConfig();
4104
4131
  const res = await post(
4105
4132
  "/v1/storefront/affiliates/attribution",
4106
- { shop_slug: config.storeSlug, code },
4133
+ {
4134
+ shop_slug: config.storeSlug,
4135
+ code,
4136
+ // Sent so the server can report the policy of the attribution we
4137
+ // already hold. Its mode — not the arriving code's — decides whether
4138
+ // that hold may be released.
4139
+ ...held ? { held_code: held.code } : {}
4140
+ },
4107
4141
  { retries: 0 }
4108
4142
  );
4109
4143
  const serverMode = res.success && res.data?.attribution_mode ? normalizeMode(res.data.attribution_mode) : null;
4110
4144
  const serverWindow = res.success && typeof res.data?.attribution_window_days === "number" ? res.data.attribution_window_days : DEFAULT_TTL_DAYS;
4111
- const holdsFirstClick = mayHoldFirstClick && serverMode !== "LAST_CLICK";
4112
- if (holdsFirstClick) {
4145
+ const heldMode = res.success && res.data?.held_attribution_mode ? normalizeMode(res.data.held_attribution_mode) : null;
4146
+ const holdsFirstClick = holdsOtherCode && (heldMode ?? held?.mode) === "FIRST_CLICK";
4147
+ if (holdsFirstClick && held) {
4148
+ if (!mayHoldFirstClick) {
4149
+ restoreStoredAffiliate({
4150
+ code: held.code,
4151
+ expiresAt: held.expiresAt,
4152
+ windowDays: held.windowDays,
4153
+ mode: heldMode ?? "FIRST_CLICK"
4154
+ });
4155
+ }
4113
4156
  return held.code;
4114
4157
  }
4115
4158
  if (res.success && res.data?.accepted && res.data.affiliate_code) {
@@ -4120,7 +4163,7 @@ async function captureAffiliateFromUrl(param = "ref") {
4120
4163
  clearAffiliateCode();
4121
4164
  return null;
4122
4165
  }
4123
- return code;
4166
+ return mayHoldFirstClick ? held.code : code;
4124
4167
  } catch {
4125
4168
  return mayHoldFirstClick ? held.code : code;
4126
4169
  }