@shoppexio/storefront 1.0.77 → 1.0.78

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
@@ -1576,7 +1576,20 @@ interface SearchOptions {
1576
1576
  declare function searchCatalogItems(query: string, options?: SearchOptions): Promise<SDKResponse<StorefrontCatalogSearchItem[]>>;
1577
1577
  declare function searchProducts(query: string, options?: SearchOptions): Promise<SDKResponse<Product[]>>;
1578
1578
 
1579
- declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number): string | null;
1579
+ type AffiliateAttributionMode = 'LAST_CLICK' | 'FIRST_CLICK';
1580
+ declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number, mode?: AffiliateAttributionMode): string | null;
1581
+ /**
1582
+ * The stored attribution, or null when nothing valid is held.
1583
+ *
1584
+ * Unlike `getAffiliateCode()` this also reports the mode the entry was written
1585
+ * under, which is what `captureAffiliateFromUrl` needs BEFORE it decides
1586
+ * whether it may overwrite.
1587
+ */
1588
+ declare function getStoredAffiliate(): {
1589
+ code: string;
1590
+ mode: AffiliateAttributionMode;
1591
+ windowDays: number;
1592
+ } | null;
1580
1593
  declare function clearAffiliateCode(): void;
1581
1594
  declare function getAffiliateCode(): string | null;
1582
1595
  declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_started', options?: {
@@ -1599,11 +1612,25 @@ declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_starte
1599
1612
  declare function validateAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1600
1613
  declare function applyAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1601
1614
  /**
1602
- * Capture an affiliate code from the current URL and store it for 30 days (last-click).
1615
+ * Capture an affiliate code from the current URL and store it (ADR-0067).
1603
1616
  *
1604
1617
  * Example:
1605
1618
  * - URL: https://mystore.com/product/abc?ref=deadbeef
1606
1619
  * - captureAffiliateFromUrl() stores "deadbeef" and returns it.
1620
+ *
1621
+ * Under LAST_CLICK (the default, and the only behaviour before ADR-0067) a new
1622
+ * `?ref=` replaces whatever was stored. Under FIRST_CLICK an existing,
1623
+ * unexpired attribution wins and the new code is NOT stored — the landing is
1624
+ * still reported to the server so the affiliate's click statistics stay
1625
+ * honest, but the credit stays with whoever was there first.
1626
+ *
1627
+ * The mode comes from the server, which means the FIRST landing in a browser
1628
+ * can never be blocked by it — correct, because there is nothing to protect
1629
+ * yet. From then on the mode travels with the stored entry.
1630
+ *
1631
+ * Enforcement is client-side. A buyer who edits localStorage can re-attribute
1632
+ * their own visit; this is the same trust level as the window itself and must
1633
+ * not be presented to merchants as fraud protection.
1607
1634
  */
1608
1635
  declare function captureAffiliateFromUrl(param?: string): Promise<string | null>;
1609
1636
 
@@ -1926,6 +1953,7 @@ declare const shoppex: {
1926
1953
  validateAffiliateCode: typeof validateAffiliateCode;
1927
1954
  applyAffiliateCode: typeof applyAffiliateCode;
1928
1955
  getAffiliateCode: typeof getAffiliateCode;
1956
+ getStoredAffiliate: typeof getStoredAffiliate;
1929
1957
  setAffiliateCode: typeof setAffiliateCode;
1930
1958
  clearAffiliateCode: typeof clearAffiliateCode;
1931
1959
  trackAffiliateEvent: typeof trackAffiliateEvent;
package/dist/index.d.ts CHANGED
@@ -1576,7 +1576,20 @@ interface SearchOptions {
1576
1576
  declare function searchCatalogItems(query: string, options?: SearchOptions): Promise<SDKResponse<StorefrontCatalogSearchItem[]>>;
1577
1577
  declare function searchProducts(query: string, options?: SearchOptions): Promise<SDKResponse<Product[]>>;
1578
1578
 
1579
- declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number): string | null;
1579
+ type AffiliateAttributionMode = 'LAST_CLICK' | 'FIRST_CLICK';
1580
+ declare function setAffiliateCode(code: string | null | undefined, ttlDays?: number, mode?: AffiliateAttributionMode): string | null;
1581
+ /**
1582
+ * The stored attribution, or null when nothing valid is held.
1583
+ *
1584
+ * Unlike `getAffiliateCode()` this also reports the mode the entry was written
1585
+ * under, which is what `captureAffiliateFromUrl` needs BEFORE it decides
1586
+ * whether it may overwrite.
1587
+ */
1588
+ declare function getStoredAffiliate(): {
1589
+ code: string;
1590
+ mode: AffiliateAttributionMode;
1591
+ windowDays: number;
1592
+ } | null;
1580
1593
  declare function clearAffiliateCode(): void;
1581
1594
  declare function getAffiliateCode(): string | null;
1582
1595
  declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_started', options?: {
@@ -1599,11 +1612,25 @@ declare function trackAffiliateEvent(eventType: 'add_to_cart' | 'checkout_starte
1599
1612
  declare function validateAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1600
1613
  declare function applyAffiliateCode(code: string): Promise<SDKResponse<AffiliateValidation>>;
1601
1614
  /**
1602
- * Capture an affiliate code from the current URL and store it for 30 days (last-click).
1615
+ * Capture an affiliate code from the current URL and store it (ADR-0067).
1603
1616
  *
1604
1617
  * Example:
1605
1618
  * - URL: https://mystore.com/product/abc?ref=deadbeef
1606
1619
  * - captureAffiliateFromUrl() stores "deadbeef" and returns it.
1620
+ *
1621
+ * Under LAST_CLICK (the default, and the only behaviour before ADR-0067) a new
1622
+ * `?ref=` replaces whatever was stored. Under FIRST_CLICK an existing,
1623
+ * unexpired attribution wins and the new code is NOT stored — the landing is
1624
+ * still reported to the server so the affiliate's click statistics stay
1625
+ * honest, but the credit stays with whoever was there first.
1626
+ *
1627
+ * The mode comes from the server, which means the FIRST landing in a browser
1628
+ * can never be blocked by it — correct, because there is nothing to protect
1629
+ * yet. From then on the mode travels with the stored entry.
1630
+ *
1631
+ * Enforcement is client-side. A buyer who edits localStorage can re-attribute
1632
+ * their own visit; this is the same trust level as the window itself and must
1633
+ * not be presented to merchants as fraud protection.
1607
1634
  */
1608
1635
  declare function captureAffiliateFromUrl(param?: string): Promise<string | null>;
1609
1636
 
@@ -1926,6 +1953,7 @@ declare const shoppex: {
1926
1953
  validateAffiliateCode: typeof validateAffiliateCode;
1927
1954
  applyAffiliateCode: typeof applyAffiliateCode;
1928
1955
  getAffiliateCode: typeof getAffiliateCode;
1956
+ getStoredAffiliate: typeof getStoredAffiliate;
1929
1957
  setAffiliateCode: typeof setAffiliateCode;
1930
1958
  clearAffiliateCode: typeof clearAffiliateCode;
1931
1959
  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,32 @@ function safeWrite(value) {
3919
3922
  } catch {
3920
3923
  }
3921
3924
  }
3922
- function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS) {
3925
+ function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS, mode) {
3923
3926
  const normalized = normalizeAffiliateCode(code);
3924
3927
  if (!normalized) {
3925
3928
  clearAffiliateCode();
3926
3929
  return null;
3927
3930
  }
3928
- safeWrite({ code: normalized, expiresAt: nowMs() + ttlMs(ttlDays) });
3931
+ safeWrite({
3932
+ code: normalized,
3933
+ expiresAt: nowMs() + ttlMs(ttlDays),
3934
+ windowDays: ttlDays,
3935
+ ...mode ? { mode } : {}
3936
+ });
3929
3937
  return normalized;
3930
3938
  }
3939
+ function getStoredAffiliate() {
3940
+ const stored = safeRead();
3941
+ if (!stored) return null;
3942
+ if (stored.expiresAt <= nowMs()) {
3943
+ clearAffiliateCode();
3944
+ return null;
3945
+ }
3946
+ const code = normalizeAffiliateCode(stored.code);
3947
+ if (!code) return null;
3948
+ const windowDays = typeof stored.windowDays === "number" && stored.windowDays > 0 ? stored.windowDays : DEFAULT_TTL_DAYS;
3949
+ return { code, mode: normalizeMode(stored.mode), windowDays };
3950
+ }
3931
3951
  function clearAffiliateCode() {
3932
3952
  if (typeof window === "undefined") return;
3933
3953
  try {
@@ -4073,7 +4093,11 @@ async function captureAffiliateFromUrl(param = "ref") {
4073
4093
  }
4074
4094
  code = normalizeAffiliateCode(code);
4075
4095
  if (!code) return null;
4076
- setAffiliateCode(code);
4096
+ const held = getStoredAffiliate();
4097
+ const mayHoldFirstClick = held !== null && held.mode === "FIRST_CLICK" && held.code !== code;
4098
+ if (!mayHoldFirstClick) {
4099
+ setAffiliateCode(code, held?.windowDays ?? DEFAULT_TTL_DAYS, held?.mode);
4100
+ }
4077
4101
  if (isInitialized()) {
4078
4102
  try {
4079
4103
  const config = getConfig();
@@ -4082,8 +4106,14 @@ async function captureAffiliateFromUrl(param = "ref") {
4082
4106
  { shop_slug: config.storeSlug, code },
4083
4107
  { retries: 0 }
4084
4108
  );
4109
+ const serverMode = res.success && res.data?.attribution_mode ? normalizeMode(res.data.attribution_mode) : null;
4110
+ 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) {
4113
+ return held.code;
4114
+ }
4085
4115
  if (res.success && res.data?.accepted && res.data.affiliate_code) {
4086
- setAffiliateCode(res.data.affiliate_code);
4116
+ setAffiliateCode(res.data.affiliate_code, serverWindow, serverMode ?? void 0);
4087
4117
  return res.data.affiliate_code;
4088
4118
  }
4089
4119
  if (res.success && res.data && res.data.accepted === false) {
@@ -4092,10 +4122,10 @@ async function captureAffiliateFromUrl(param = "ref") {
4092
4122
  }
4093
4123
  return code;
4094
4124
  } catch {
4095
- return code;
4125
+ return mayHoldFirstClick ? held.code : code;
4096
4126
  }
4097
4127
  }
4098
- return code;
4128
+ return mayHoldFirstClick ? held.code : code;
4099
4129
  }
4100
4130
 
4101
4131
  // ../sdk/src/utils/cart-line-id.ts
@@ -6180,6 +6210,7 @@ var shoppex = {
6180
6210
  validateAffiliateCode,
6181
6211
  applyAffiliateCode,
6182
6212
  getAffiliateCode,
6213
+ getStoredAffiliate,
6183
6214
  setAffiliateCode,
6184
6215
  clearAffiliateCode,
6185
6216
  trackAffiliateEvent,