@shoppexio/storefront 1.0.77 → 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;
@@ -1576,7 +1603,26 @@ interface SearchOptions {
1576
1603
  declare function searchCatalogItems(query: string, options?: SearchOptions): Promise<SDKResponse<StorefrontCatalogSearchItem[]>>;
1577
1604
  declare function searchProducts(query: string, options?: SearchOptions): Promise<SDKResponse<Product[]>>;
1578
1605
 
1579
- declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number): string | null;
1606
+ type AffiliateAttributionMode = 'LAST_CLICK' | 'FIRST_CLICK';
1607
+ declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number, mode?: AffiliateAttributionMode): string | null;
1608
+ /**
1609
+ * The stored attribution, or null when nothing valid is held.
1610
+ *
1611
+ * Unlike `getAffiliateCode()` this also reports the mode the entry was written
1612
+ * under, which is what `captureAffiliateFromUrl` needs BEFORE it decides
1613
+ * whether it may overwrite.
1614
+ */
1615
+ declare function getStoredAffiliate(): {
1616
+ code: string;
1617
+ mode: AffiliateAttributionMode;
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;
1625
+ } | null;
1580
1626
  declare function clearAffiliateCode(): void;
1581
1627
  declare function getAffiliateCode(): string | null;
1582
1628
  declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_started', options?: {
@@ -1597,13 +1643,44 @@ declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_starte
1597
1643
  dedupeKey?: string;
1598
1644
  }): Promise<void>;
1599
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
+ */
1600
1663
  declare function applyAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1601
1664
  /**
1602
- * Capture an affiliate code from the current URL and store it for 30 days (last-click).
1665
+ * Capture an affiliate code from the current URL and store it (ADR-0067).
1603
1666
  *
1604
1667
  * Example:
1605
1668
  * - URL: https://mystore.com/product/abc?ref=deadbeef
1606
1669
  * - captureAffiliateFromUrl() stores "deadbeef" and returns it.
1670
+ *
1671
+ * Under LAST_CLICK (the default, and the only behaviour before ADR-0067) a new
1672
+ * `?ref=` replaces whatever was stored. Under FIRST_CLICK an existing,
1673
+ * unexpired attribution wins and the new code is NOT stored — the landing is
1674
+ * still reported to the server so the affiliate's click statistics stay
1675
+ * honest, but the credit stays with whoever was there first.
1676
+ *
1677
+ * The mode comes from the server, which means the FIRST landing in a browser
1678
+ * can never be blocked by it — correct, because there is nothing to protect
1679
+ * yet. From then on the mode travels with the stored entry.
1680
+ *
1681
+ * Enforcement is client-side. A buyer who edits localStorage can re-attribute
1682
+ * their own visit; this is the same trust level as the window itself and must
1683
+ * not be presented to merchants as fraud protection.
1607
1684
  */
1608
1685
  declare function captureAffiliateFromUrl(param?: string): Promise<string | null>;
1609
1686
 
@@ -1926,6 +2003,7 @@ declare const shoppex: {
1926
2003
  validateAffiliateCode: typeof validateAffiliateCode;
1927
2004
  applyAffiliateCode: typeof applyAffiliateCode;
1928
2005
  getAffiliateCode: typeof getAffiliateCode;
2006
+ getStoredAffiliate: typeof getStoredAffiliate;
1929
2007
  setAffiliateCode: typeof setAffiliateCode;
1930
2008
  clearAffiliateCode: typeof clearAffiliateCode;
1931
2009
  trackAffiliateEvent: typeof trackAffiliateEvent;
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;
@@ -1576,7 +1603,26 @@ interface SearchOptions {
1576
1603
  declare function searchCatalogItems(query: string, options?: SearchOptions): Promise<SDKResponse<StorefrontCatalogSearchItem[]>>;
1577
1604
  declare function searchProducts(query: string, options?: SearchOptions): Promise<SDKResponse<Product[]>>;
1578
1605
 
1579
- declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number): string | null;
1606
+ type AffiliateAttributionMode = 'LAST_CLICK' | 'FIRST_CLICK';
1607
+ declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number, mode?: AffiliateAttributionMode): string | null;
1608
+ /**
1609
+ * The stored attribution, or null when nothing valid is held.
1610
+ *
1611
+ * Unlike `getAffiliateCode()` this also reports the mode the entry was written
1612
+ * under, which is what `captureAffiliateFromUrl` needs BEFORE it decides
1613
+ * whether it may overwrite.
1614
+ */
1615
+ declare function getStoredAffiliate(): {
1616
+ code: string;
1617
+ mode: AffiliateAttributionMode;
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;
1625
+ } | null;
1580
1626
  declare function clearAffiliateCode(): void;
1581
1627
  declare function getAffiliateCode(): string | null;
1582
1628
  declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_started', options?: {
@@ -1597,13 +1643,44 @@ declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_starte
1597
1643
  dedupeKey?: string;
1598
1644
  }): Promise<void>;
1599
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
+ */
1600
1663
  declare function applyAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1601
1664
  /**
1602
- * Capture an affiliate code from the current URL and store it for 30 days (last-click).
1665
+ * Capture an affiliate code from the current URL and store it (ADR-0067).
1603
1666
  *
1604
1667
  * Example:
1605
1668
  * - URL: https://mystore.com/product/abc?ref=deadbeef
1606
1669
  * - captureAffiliateFromUrl() stores "deadbeef" and returns it.
1670
+ *
1671
+ * Under LAST_CLICK (the default, and the only behaviour before ADR-0067) a new
1672
+ * `?ref=` replaces whatever was stored. Under FIRST_CLICK an existing,
1673
+ * unexpired attribution wins and the new code is NOT stored — the landing is
1674
+ * still reported to the server so the affiliate's click statistics stay
1675
+ * honest, but the credit stays with whoever was there first.
1676
+ *
1677
+ * The mode comes from the server, which means the FIRST landing in a browser
1678
+ * can never be blocked by it — correct, because there is nothing to protect
1679
+ * yet. From then on the mode travels with the stored entry.
1680
+ *
1681
+ * Enforcement is client-side. A buyer who edits localStorage can re-attribute
1682
+ * their own visit; this is the same trust level as the window itself and must
1683
+ * not be presented to merchants as fraud protection.
1607
1684
  */
1608
1685
  declare function captureAffiliateFromUrl(param?: string): Promise<string | null>;
1609
1686
 
@@ -1926,6 +2003,7 @@ declare const shoppex: {
1926
2003
  validateAffiliateCode: typeof validateAffiliateCode;
1927
2004
  applyAffiliateCode: typeof applyAffiliateCode;
1928
2005
  getAffiliateCode: typeof getAffiliateCode;
2006
+ getStoredAffiliate: typeof getStoredAffiliate;
1929
2007
  setAffiliateCode: typeof setAffiliateCode;
1930
2008
  clearAffiliateCode: typeof clearAffiliateCode;
1931
2009
  trackAffiliateEvent: typeof trackAffiliateEvent;
package/dist/index.js CHANGED
@@ -3890,6 +3890,9 @@ var SESSION_STORAGE_KEY = "shoppex:affiliate_session:v1";
3890
3890
  var DEFAULT_TTL_DAYS = 30;
3891
3891
  var FALLBACK_SESSION_KEY_LENGTH = 24;
3892
3892
  var SESSION_KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
3893
+ function normalizeMode(value) {
3894
+ return value === "FIRST_CLICK" ? "FIRST_CLICK" : "LAST_CLICK";
3895
+ }
3893
3896
  function nowMs() {
3894
3897
  return Date.now();
3895
3898
  }
@@ -3907,7 +3910,7 @@ function safeRead() {
3907
3910
  if (!raw) return null;
3908
3911
  const parsed = JSON.parse(raw);
3909
3912
  if (!parsed || typeof parsed.code !== "string" || typeof parsed.expiresAt !== "number") return null;
3910
- return parsed;
3913
+ return parsed.mode === void 0 ? parsed : { ...parsed, mode: normalizeMode(parsed.mode) };
3911
3914
  } catch {
3912
3915
  return null;
3913
3916
  }
@@ -3919,15 +3922,35 @@ function safeWrite(value) {
3919
3922
  } catch {
3920
3923
  }
3921
3924
  }
3922
- function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS) {
3925
+ function restoreStoredAffiliate(value) {
3926
+ safeWrite(value);
3927
+ }
3928
+ function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS, mode) {
3923
3929
  const normalized = normalizeAffiliateCode(code);
3924
3930
  if (!normalized) {
3925
3931
  clearAffiliateCode();
3926
3932
  return null;
3927
3933
  }
3928
- safeWrite({ code: normalized, expiresAt: nowMs() + ttlMs(ttlDays) });
3934
+ safeWrite({
3935
+ code: normalized,
3936
+ expiresAt: nowMs() + ttlMs(ttlDays),
3937
+ windowDays: ttlDays,
3938
+ ...mode ? { mode } : {}
3939
+ });
3929
3940
  return normalized;
3930
3941
  }
3942
+ function getStoredAffiliate() {
3943
+ const stored = safeRead();
3944
+ if (!stored) return null;
3945
+ if (stored.expiresAt <= nowMs()) {
3946
+ clearAffiliateCode();
3947
+ return null;
3948
+ }
3949
+ const code = normalizeAffiliateCode(stored.code);
3950
+ if (!code) return null;
3951
+ const windowDays = typeof stored.windowDays === "number" && stored.windowDays > 0 ? stored.windowDays : DEFAULT_TTL_DAYS;
3952
+ return { code, mode: normalizeMode(stored.mode), windowDays, expiresAt: stored.expiresAt };
3953
+ }
3931
3954
  function clearAffiliateCode() {
3932
3955
  if (typeof window === "undefined") return;
3933
3956
  try {
@@ -4017,11 +4040,13 @@ async function validateAffiliateCode(code) {
4017
4040
  };
4018
4041
  }
4019
4042
  const config = getConfig();
4043
+ const held = getStoredAffiliate();
4020
4044
  const response = await post(
4021
4045
  "/v1/storefront/affiliates/resolve",
4022
4046
  {
4023
4047
  shop_slug: config.storeSlug,
4024
- code: normalizedCode
4048
+ code: normalizedCode,
4049
+ ...held && held.code !== normalizedCode ? { held_code: held.code } : {}
4025
4050
  },
4026
4051
  { retries: 0 }
4027
4052
  );
@@ -4049,15 +4074,36 @@ async function validateAffiliateCode(code) {
4049
4074
  ...response.data.program_enabled !== void 0 ? { program_enabled: response.data.program_enabled } : {},
4050
4075
  affiliate_code: normalizeAffiliateCode(response.data.affiliate_code),
4051
4076
  discount_active: Boolean(response.data.discount_active),
4052
- 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
4053
4084
  },
4054
4085
  ...response.message ? { message: response.message } : {}
4055
4086
  };
4056
4087
  }
4057
4088
  async function applyAffiliateCode(code) {
4089
+ const held = getStoredAffiliate();
4058
4090
  const result = await validateAffiliateCode(code);
4059
4091
  if (result.success && result.data?.affiliate_code) {
4060
- 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 } };
4061
4107
  }
4062
4108
  return result;
4063
4109
  }
@@ -4073,29 +4119,56 @@ async function captureAffiliateFromUrl(param = "ref") {
4073
4119
  }
4074
4120
  code = normalizeAffiliateCode(code);
4075
4121
  if (!code) return null;
4076
- setAffiliateCode(code);
4122
+ const held = getStoredAffiliate();
4123
+ const holdsOtherCode = held !== null && held.code !== code;
4124
+ const mayHoldFirstClick = holdsOtherCode && held.mode === "FIRST_CLICK";
4125
+ if (!mayHoldFirstClick) {
4126
+ setAffiliateCode(code, held?.windowDays ?? DEFAULT_TTL_DAYS, held?.mode);
4127
+ }
4077
4128
  if (isInitialized()) {
4078
4129
  try {
4079
4130
  const config = getConfig();
4080
4131
  const res = await post(
4081
4132
  "/v1/storefront/affiliates/attribution",
4082
- { 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
+ },
4083
4141
  { retries: 0 }
4084
4142
  );
4143
+ const serverMode = res.success && res.data?.attribution_mode ? normalizeMode(res.data.attribution_mode) : null;
4144
+ const serverWindow = res.success && typeof res.data?.attribution_window_days === "number" ? res.data.attribution_window_days : DEFAULT_TTL_DAYS;
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
+ }
4156
+ return held.code;
4157
+ }
4085
4158
  if (res.success && res.data?.accepted && res.data.affiliate_code) {
4086
- setAffiliateCode(res.data.affiliate_code);
4159
+ setAffiliateCode(res.data.affiliate_code, serverWindow, serverMode ?? void 0);
4087
4160
  return res.data.affiliate_code;
4088
4161
  }
4089
4162
  if (res.success && res.data && res.data.accepted === false) {
4090
4163
  clearAffiliateCode();
4091
4164
  return null;
4092
4165
  }
4093
- return code;
4166
+ return mayHoldFirstClick ? held.code : code;
4094
4167
  } catch {
4095
- return code;
4168
+ return mayHoldFirstClick ? held.code : code;
4096
4169
  }
4097
4170
  }
4098
- return code;
4171
+ return mayHoldFirstClick ? held.code : code;
4099
4172
  }
4100
4173
 
4101
4174
  // ../sdk/src/utils/cart-line-id.ts
@@ -6180,6 +6253,7 @@ var shoppex = {
6180
6253
  validateAffiliateCode,
6181
6254
  applyAffiliateCode,
6182
6255
  getAffiliateCode,
6256
+ getStoredAffiliate,
6183
6257
  setAffiliateCode,
6184
6258
  clearAffiliateCode,
6185
6259
  trackAffiliateEvent,