@shoppexio/storefront 1.0.76 → 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
@@ -2417,6 +2417,18 @@ var publicInvoiceWireSchema = external_exports.object({
2417
2417
  * the reader: this schema is not it.
2418
2418
  */
2419
2419
  coupon_applied: external_exports.boolean().optional(),
2420
+ /**
2421
+ * How much of the order the applied coupon reaches, in lines — null when it
2422
+ * covers every line or no coupon is on the order. Counts only, so nothing
2423
+ * about which products the merchant scoped the code to leaves the server.
2424
+ * The discount line already shows the reduced amount; this is what lets the
2425
+ * checkout say "applied to 1 of 3 items" instead of leaving a smaller-than-
2426
+ * advertised discount to read as a broken code.
2427
+ */
2428
+ coupon_scope: external_exports.object({
2429
+ eligible_line_count: external_exports.number(),
2430
+ line_count: external_exports.number()
2431
+ }).nullable().optional(),
2420
2432
  crypto_mode: external_exports.string().nullable().optional(),
2421
2433
  crypto_transactions: external_exports.array(cryptoTransactionSchema).optional(),
2422
2434
  currency: external_exports.string().nullable(),
@@ -3878,6 +3890,9 @@ var SESSION_STORAGE_KEY = "shoppex:affiliate_session:v1";
3878
3890
  var DEFAULT_TTL_DAYS = 30;
3879
3891
  var FALLBACK_SESSION_KEY_LENGTH = 24;
3880
3892
  var SESSION_KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
3893
+ function normalizeMode(value) {
3894
+ return value === "FIRST_CLICK" ? "FIRST_CLICK" : "LAST_CLICK";
3895
+ }
3881
3896
  function nowMs() {
3882
3897
  return Date.now();
3883
3898
  }
@@ -3895,7 +3910,7 @@ function safeRead() {
3895
3910
  if (!raw) return null;
3896
3911
  const parsed = JSON.parse(raw);
3897
3912
  if (!parsed || typeof parsed.code !== "string" || typeof parsed.expiresAt !== "number") return null;
3898
- return parsed;
3913
+ return parsed.mode === void 0 ? parsed : { ...parsed, mode: normalizeMode(parsed.mode) };
3899
3914
  } catch {
3900
3915
  return null;
3901
3916
  }
@@ -3907,15 +3922,32 @@ function safeWrite(value) {
3907
3922
  } catch {
3908
3923
  }
3909
3924
  }
3910
- function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS) {
3925
+ function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS, mode) {
3911
3926
  const normalized = normalizeAffiliateCode(code);
3912
3927
  if (!normalized) {
3913
3928
  clearAffiliateCode();
3914
3929
  return null;
3915
3930
  }
3916
- safeWrite({ code: normalized, expiresAt: nowMs() + ttlMs(ttlDays) });
3931
+ safeWrite({
3932
+ code: normalized,
3933
+ expiresAt: nowMs() + ttlMs(ttlDays),
3934
+ windowDays: ttlDays,
3935
+ ...mode ? { mode } : {}
3936
+ });
3917
3937
  return normalized;
3918
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
+ }
3919
3951
  function clearAffiliateCode() {
3920
3952
  if (typeof window === "undefined") return;
3921
3953
  try {
@@ -4061,7 +4093,11 @@ async function captureAffiliateFromUrl(param = "ref") {
4061
4093
  }
4062
4094
  code = normalizeAffiliateCode(code);
4063
4095
  if (!code) return null;
4064
- 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
+ }
4065
4101
  if (isInitialized()) {
4066
4102
  try {
4067
4103
  const config = getConfig();
@@ -4070,8 +4106,14 @@ async function captureAffiliateFromUrl(param = "ref") {
4070
4106
  { shop_slug: config.storeSlug, code },
4071
4107
  { retries: 0 }
4072
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
+ }
4073
4115
  if (res.success && res.data?.accepted && res.data.affiliate_code) {
4074
- setAffiliateCode(res.data.affiliate_code);
4116
+ setAffiliateCode(res.data.affiliate_code, serverWindow, serverMode ?? void 0);
4075
4117
  return res.data.affiliate_code;
4076
4118
  }
4077
4119
  if (res.success && res.data && res.data.accepted === false) {
@@ -4080,10 +4122,10 @@ async function captureAffiliateFromUrl(param = "ref") {
4080
4122
  }
4081
4123
  return code;
4082
4124
  } catch {
4083
- return code;
4125
+ return mayHoldFirstClick ? held.code : code;
4084
4126
  }
4085
4127
  }
4086
- return code;
4128
+ return mayHoldFirstClick ? held.code : code;
4087
4129
  }
4088
4130
 
4089
4131
  // ../sdk/src/utils/cart-line-id.ts
@@ -6168,6 +6210,7 @@ var shoppex = {
6168
6210
  validateAffiliateCode,
6169
6211
  applyAffiliateCode,
6170
6212
  getAffiliateCode,
6213
+ getStoredAffiliate,
6171
6214
  setAffiliateCode,
6172
6215
  clearAffiliateCode,
6173
6216
  trackAffiliateEvent,