@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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.79
4
+
5
+ - Attribution is now decided by the policy of the code that EARNED the hold, never by the one that just arrived. `captureAffiliateFromUrl()` sends the held code so the server can report its mode, and a first-click hold survives an affiliate on a last-click tier landing afterwards.
6
+ - `applyAffiliateCode()` applies the same rules as a `?ref=` landing. It stores the code with the shop's (or tier's) real mode and window instead of a fixed 30-day last-click entry, and it no longer overrides a standing first-click hold — a themed "apply code" field was previously a way around the rule.
7
+ - `applyAffiliateCode()` reports what is ATTRIBUTED: `data.affiliate_code` is the winning code and the new `data.applied` says whether that is the one you passed. Passing `data.affiliate_code` to `checkout()` therefore stays correct in both cases.
8
+ - `validateAffiliateCode()` passes the resolved `attribution_mode`, `attribution_window_days` and `held_attribution_mode` through instead of dropping them.
9
+ - `getStoredAffiliate()` also returns `expiresAt`, so a hold can be restored without extending the window it earned.
10
+ - A transport failure or an HTTP error such as a rate limit no longer returns the arriving code while storage still holds another: both paths answer with the code that keeps the attribution.
11
+
3
12
  ## 1.0.78
4
13
 
5
14
  - `captureAffiliateFromUrl()` honours the shop's attribution mode (ADR-0067). Under `FIRST_CLICK` a held, unexpired attribution wins and a newer `?ref=` no longer replaces it; the landing is still reported so click statistics stay complete. `LAST_CLICK` is unchanged and remains the default.
package/dist/index.cjs CHANGED
@@ -19317,6 +19317,9 @@ function safeWrite(value) {
19317
19317
  } catch {
19318
19318
  }
19319
19319
  }
19320
+ function restoreStoredAffiliate(value) {
19321
+ safeWrite(value);
19322
+ }
19320
19323
  function setAffiliateCode(code, ttlDays = DEFAULT_TTL_DAYS, mode) {
19321
19324
  const normalized = normalizeAffiliateCode(code);
19322
19325
  if (!normalized) {
@@ -19341,7 +19344,7 @@ function getStoredAffiliate() {
19341
19344
  const code = normalizeAffiliateCode(stored.code);
19342
19345
  if (!code) return null;
19343
19346
  const windowDays = typeof stored.windowDays === "number" && stored.windowDays > 0 ? stored.windowDays : DEFAULT_TTL_DAYS;
19344
- return { code, mode: normalizeMode(stored.mode), windowDays };
19347
+ return { code, mode: normalizeMode(stored.mode), windowDays, expiresAt: stored.expiresAt };
19345
19348
  }
19346
19349
  function clearAffiliateCode() {
19347
19350
  if (typeof window === "undefined") return;
@@ -19432,11 +19435,13 @@ async function validateAffiliateCode(code) {
19432
19435
  };
19433
19436
  }
19434
19437
  const config2 = getConfig();
19438
+ const held = getStoredAffiliate();
19435
19439
  const response = await post(
19436
19440
  "/v1/storefront/affiliates/resolve",
19437
19441
  {
19438
19442
  shop_slug: config2.storeSlug,
19439
- code: normalizedCode
19443
+ code: normalizedCode,
19444
+ ...held && held.code !== normalizedCode ? { held_code: held.code } : {}
19440
19445
  },
19441
19446
  { retries: 0 }
19442
19447
  );
@@ -19464,15 +19469,36 @@ async function validateAffiliateCode(code) {
19464
19469
  ...response.data.program_enabled !== void 0 ? { program_enabled: response.data.program_enabled } : {},
19465
19470
  affiliate_code: normalizeAffiliateCode(response.data.affiliate_code),
19466
19471
  discount_active: Boolean(response.data.discount_active),
19467
- discount_percent: Number(response.data.discount_percent ?? 0)
19472
+ discount_percent: Number(response.data.discount_percent ?? 0),
19473
+ // Passed through rather than dropped: a caller that stores the code has
19474
+ // no other way to learn the policy, and defaulting to 30-day last-click
19475
+ // would quietly discard a first-click tier and any custom window.
19476
+ attribution_mode: normalizeMode(response.data.attribution_mode),
19477
+ 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,
19478
+ held_attribution_mode: response.data.held_attribution_mode === "FIRST_CLICK" || response.data.held_attribution_mode === "LAST_CLICK" ? response.data.held_attribution_mode : null
19468
19479
  },
19469
19480
  ...response.message ? { message: response.message } : {}
19470
19481
  };
19471
19482
  }
19472
19483
  async function applyAffiliateCode(code) {
19484
+ const held = getStoredAffiliate();
19473
19485
  const result = await validateAffiliateCode(code);
19474
19486
  if (result.success && result.data?.affiliate_code) {
19475
- setAffiliateCode(result.data.affiliate_code);
19487
+ const applied = result.data.affiliate_code;
19488
+ const holdsOtherCode = held !== null && held.code !== applied;
19489
+ const heldMode = holdsOtherCode ? result.data.held_attribution_mode ?? held.mode : null;
19490
+ if (heldMode === "FIRST_CLICK" && held) {
19491
+ return {
19492
+ ...result,
19493
+ data: { ...result.data, affiliate_code: held.code, applied: false }
19494
+ };
19495
+ }
19496
+ setAffiliateCode(
19497
+ applied,
19498
+ result.data.attribution_window_days ?? DEFAULT_TTL_DAYS,
19499
+ result.data.attribution_mode
19500
+ );
19501
+ return { ...result, data: { ...result.data, applied: true } };
19476
19502
  }
19477
19503
  return result;
19478
19504
  }
@@ -19489,7 +19515,8 @@ async function captureAffiliateFromUrl(param = "ref") {
19489
19515
  code = normalizeAffiliateCode(code);
19490
19516
  if (!code) return null;
19491
19517
  const held = getStoredAffiliate();
19492
- const mayHoldFirstClick = held !== null && held.mode === "FIRST_CLICK" && held.code !== code;
19518
+ const holdsOtherCode = held !== null && held.code !== code;
19519
+ const mayHoldFirstClick = holdsOtherCode && held.mode === "FIRST_CLICK";
19493
19520
  if (!mayHoldFirstClick) {
19494
19521
  setAffiliateCode(code, held?.windowDays ?? DEFAULT_TTL_DAYS, held?.mode);
19495
19522
  }
@@ -19498,13 +19525,29 @@ async function captureAffiliateFromUrl(param = "ref") {
19498
19525
  const config2 = getConfig();
19499
19526
  const res = await post(
19500
19527
  "/v1/storefront/affiliates/attribution",
19501
- { shop_slug: config2.storeSlug, code },
19528
+ {
19529
+ shop_slug: config2.storeSlug,
19530
+ code,
19531
+ // Sent so the server can report the policy of the attribution we
19532
+ // already hold. Its mode — not the arriving code's — decides whether
19533
+ // that hold may be released.
19534
+ ...held ? { held_code: held.code } : {}
19535
+ },
19502
19536
  { retries: 0 }
19503
19537
  );
19504
19538
  const serverMode = res.success && res.data?.attribution_mode ? normalizeMode(res.data.attribution_mode) : null;
19505
19539
  const serverWindow = res.success && typeof res.data?.attribution_window_days === "number" ? res.data.attribution_window_days : DEFAULT_TTL_DAYS;
19506
- const holdsFirstClick = mayHoldFirstClick && serverMode !== "LAST_CLICK";
19507
- if (holdsFirstClick) {
19540
+ const heldMode = res.success && res.data?.held_attribution_mode ? normalizeMode(res.data.held_attribution_mode) : null;
19541
+ const holdsFirstClick = holdsOtherCode && (heldMode ?? held?.mode) === "FIRST_CLICK";
19542
+ if (holdsFirstClick && held) {
19543
+ if (!mayHoldFirstClick) {
19544
+ restoreStoredAffiliate({
19545
+ code: held.code,
19546
+ expiresAt: held.expiresAt,
19547
+ windowDays: held.windowDays,
19548
+ mode: heldMode ?? "FIRST_CLICK"
19549
+ });
19550
+ }
19508
19551
  return held.code;
19509
19552
  }
19510
19553
  if (res.success && res.data?.accepted && res.data.affiliate_code) {
@@ -19515,7 +19558,7 @@ async function captureAffiliateFromUrl(param = "ref") {
19515
19558
  clearAffiliateCode();
19516
19559
  return null;
19517
19560
  }
19518
- return code;
19561
+ return mayHoldFirstClick ? held.code : code;
19519
19562
  } catch {
19520
19563
  return mayHoldFirstClick ? held.code : code;
19521
19564
  }