# Changelog

## 1.0.90

Types only; no runtime change. (1.0.86 was skipped; 1.0.87 to 1.0.89 are other releases, all included.)

- `Shop.payment_methods`: the public payment methods the backend derives from
  the shop's live gateways, `{ id, label, kind }` in display order (cards,
  wallets, crypto). `Shop.stock_alerts_enabled`: whether sold-out products may
  offer "Notify me".
- `Product.short_description`, `Product.label_singular` /
  `Product.label_plural` (the merchant's unit words) and
  `Product.service_status` (`operational`, `updating`, `detected` or
  `offline`, with an optional note, for storefront `/status` pages; null when
  the merchant set none).
- `PriceVariant.description`: the merchant's one-line note on an option.

## 1.0.89

- `getShopReviewsPage(cursor, { product })` fails with `code: "reviews_product_filter_unsupported"` (exported as `PRODUCT_FILTER_UNSUPPORTED`) when the API answers without echoing the filter it applied, instead of returning the whole shop's reviews as if they were the product's. `ShopReviewsPage.product` carries the echoed id.

## 1.0.88

- Cart pings carry `cart_seq` and `cart_seq_scope`, a sequence number taken when the cart state is reported and shared by all tabs of the shop (the current time in ms, or one above the highest sequence any tab handed out). The counter record holds the scope (created on first use) and is updated under a Web Lock (`navigator.locks`), so two tabs never hand out the same sequence or create two scopes; where Web Locks are unavailable it is updated without the lock. The server keeps the cart with the highest sequence of the scope and ignores an older one that arrives later, so a slow ping from one tab can no longer overwrite a newer cart another tab already reported.
- The confirmed cart state records its sequence and is only replaced by a delivered state with a higher one, the same rule the server applies. A cart state counts as already reported when it is the state the server will hold: a newer state another tab is sending, else the confirmed state. So reverting to the confirmed cart while another tab sends a newer one still goes out.
- Each tab still sends its cart states one at a time and drops a waiting state once a newer one is asked for; this now only saves pings, the order comes from the sequence.
- Page views and heartbeats without a cart are unchanged and carry no sequence.

## 1.0.87

- `getPage()`, `getPages()` and `getShopReviewsPage()` keep the transport `status` (and `code`) on a failure, like the other modules, so a storefront can tell a page that does not exist (404) from an API it could not reach.
- `getShopReviewsPage(cursor, { product })` narrows a page to one product by its public id (the catalog `id`). The page's `summary` then counts that product's reviews only, so a product's review list can page on demand with a correct breakdown.

## 1.0.85

- Cart reporting keeps two shared values: the cart state the server confirmed (written only after a 2xx response) and a short-lived in-flight claim that lets another tab skip the `storage` echo while a send runs. Only the newest claim settles: a send whose claim was replaced confirms and clears nothing, and a failed send (network error, non-OK response, or no answer within 10 seconds) writes nothing, so an unsent cart can never look reported. A tab sends its cart states one at a time and in order, dropping a waiting state once a newer one is asked for, so an older cart cannot reach the server after a newer one. Whether a change is already reported is decided when its turn comes, so reverting to the confirmed cart while another state is being sent still goes out.
- On code storefronts, `trackPageView(cartValue, itemCount)` passes a partial cart through unchanged instead of filling the missing field with 0.
- Storefront client-error reports cut `endpoint`/`message` to 500 and `page_url`/`request_url` to 2000 characters, the bounds the ping endpoint enforces, so a long page URL no longer gets the report rejected.

## 1.0.84

- `touchStorefrontPresence()` sends `event_type: "presence"`. The server refreshes the live session but no longer counts the ping as a shop view, so an idle tab with `sx-online-users` (or any theme that touches presence on a timer) stops adding a view every few minutes.
- New `trackCartActivity(cartValue, itemCount)`: reports a cart change for the live visitor map as a presence ping, without counting a page view. A cart state that any tab of the shop already reported is not sent again, so the cross-tab `storage` echo of one change is not a second ping.
- On code storefronts (platform page tracker active), `trackPageView(cartValue, itemCount)` sends the cart as cart activity instead of a second counted view.
- `trackPageView()` is otherwise unchanged: one call counts one view.

## 1.0.82

- Page views and presence send the page path (query and fragment dropped, long paths capped), so the dashboard can show the top live page.
- On code storefronts the platform injects a page tracker. While it runs, `trackPageView()` without cart data and `touchStorefrontPresence()` stand down so a view is not counted twice. `trackPageView(cartValue, itemCount)` still reports the cart activity the tracker cannot see.

## 1.0.81

- `ProductCategory` carries `sort_order`, the merchant's category position from the dashboard. Product-level category references arrive sorted by it, so a theme can order its filter row and grouped sections the way the merchant arranged them instead of alphabetically.

## 1.0.80

- Keep an existing affiliate code while the server resolves its attribution policy. Checkout can no longer read a replacement before a first-click decision arrives.
- Keep the existing code and its expiry when policy resolution fails or rejects the arriving code.

## 1.0.79

- 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.
- `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.
- `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.
- `validateAffiliateCode()` passes the resolved `attribution_mode`, `attribution_window_days` and `held_attribution_mode` through instead of dropping them.
- `getStoredAffiliate()` also returns `expiresAt`, so a hold can be restored without extending the window it earned.
- 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.

## 1.0.78

- `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.
- The attribution window is no longer fixed at 30 days: `/attribution` returns `attribution_mode` and `attribution_window_days`, and a newly stored referral uses the shop's (or its tier's) window.
- New export `getStoredAffiliate()`, returning `{ code, mode }` for the held attribution, or `null`.
- `setAffiliateCode(code, ttlDays, mode?)` accepts the mode to persist. Existing two-argument calls are unaffected.
- The `shoppex:affiliate_code:v1` storage key is unchanged and entries written by earlier versions keep working: an entry without a mode reads as `LAST_CLICK`, the behaviour it was written under. No attribution is lost on upgrade.
- Note: first-click is enforced in the browser, like the window itself. It is an attribution rule, not fraud protection.

## 1.0.77

- Headless customer replacement requests now require `lineItemId` in the SDK types, matching the live API. Pass the invoice position UUID; omitted or null IDs are not valid. The read-only `replacementEligibility` query still allows omitting its line ID.

## 1.0.75

- `getStore`, `getStorefront`, `resolveStoreByDomain`, `getProducts`, `getStorefrontProductsPage`, `getProduct` and `getCategories` now forward the full failure contract (`code`, `errorParams`, `status`, `responseReceived`, `responseDefinitive`) instead of rebuilding `{ success: false, message }`, so `code === 'errors.storefront.currency_unavailable'` is branchable through every catalog read. A slug `getProduct()` whose store lookup failed returns that failure instead of a misleading not-found.

## 1.0.74

- Catalog reads now resolve the buyer currency with the same rule as cart quotes and checkout: a `?currency=` on the page URL wins, then `init({ currency })`. Before, a merchant-hosted page with `?currency=` priced the catalog in the init currency and the cart in the URL currency.
- `shoppex.invalidateCache(prefixOrKey)` matches the bare key again across every locale/currency namespace (1.0.73 keys are `<locale>|<currency>|<key>`, which made prefix invalidation a no-op).
- A rejected `init()` (invalid currency) leaves the previous configuration, including the cached shop id, untouched.

## 1.0.73

- `init({ currency })` is now enforced on every priced read (ADR-0066). `getStorefront`, `getStore`, `resolveStoreByDomain`, `getProducts`, `getStorefrontProductsPage` and `getProduct` send it as `?currency=`; before, only cart quotes and checkout used it, so a catalog could show one currency and the checkout price in another.
- A currency the shop has not enabled fails those reads with `code: 'errors.storefront.currency_unavailable'` (`errorParams.requested`, `errorParams.available`) instead of silently serving the shop default. New export `CURRENCY_UNAVAILABLE_ERROR_CODE`.
- `init` normalizes the currency to an upper-case ISO 4217 code and throws `ValidationError` for anything else.
- Response cache keys carry locale and buyer currency; a re-init with another audience no longer reads the previous audience's payloads.
- `Shop` gains `default_currency` and `available_currencies`; `Shop.currency` is documented as the currency the response is priced in.

## 1.0.72

- REMOVED: the checkout Turnstile verification lane. `mountCheckoutChallenge`, `CheckoutChallengeCallbacks`, `CheckoutChallengeFrame`, `ApiChallenge`, `SDKResponse.challenge`, `retryWithProof`, and the `autoResolveChallenge`/`turnstileToken` checkout options are gone; invoice creation is a single request again and rate-limit refusals are plain 429s. Bot protection moved to the platform edge. The `X-Idempotency-Key` machinery is unchanged.

## 1.0.70

- One replay engine: a turnstile-challenged refusal now carries `retryWithProof` — the frozen hand-off (cart bytes, codes, currency, quote proof, and the ORIGINAL idempotency key) replays with only the fresh proof attached, and the server's create-request uniqueness is the double-invoice guarantee. Client coalescing is UX-only. New `quoteToken` option; shared Turnstile frame protocol.

## 1.0.69

- Replace the timed replay-share window with real waiter tracking (a settled replay lives exactly as long as an initial duplicate is still in flight), pin the quote proof and currency through the storefront widget replay via the new `quoteToken` option and currency tri-state, and spend the embed replay budget down exactly under widget flicker.

## 1.0.68

- Harden the automatic challenge flow end to end: the interactive overlay is dismissible and always settles (timeout message, overall ceiling), failed replays are never cached, the success-share window shrinks to real latency skew, the replay pin carries currency verbatim, the coalescing key carries autoRedirect/locale, and `autoResolveChallenge: false` lets integrations keep their own challenge UX.

## 1.0.67

- Harden replay coalescing: keys carry the entry point and every effective create-intent dimension, and settled replays stay shareable for a grace window so a slow initial refusal joins the finished replay instead of minting a second invoice.

## 1.0.66

- Coalesce concurrent automatic challenge replays: overlapping identical checkout calls share one broker and one replay, so a double-fired handler can no longer mint a second invoice for one buyer action.

## 1.0.65

- Replay a challenge-refused checkout with the exact refused hand-off (cart bytes, codes, currency, email, quote proof) instead of re-reading shared storage; a cart another tab changed refuses instead of billing.

## 1.0.64

- Resolve checkout verification challenges automatically: `checkout()` and `buildCheckoutUrl()` mount the hosted broker themselves on a challenge refusal and replay the create once with the delivered proof — no merchant wiring, no second click. `mountCheckoutChallenge()` gains an `onVisibilityChange` callback.

## 1.0.63

- Keep managed checkout verification collapsed until Cloudflare requires buyer interaction.

## 1.0.62

- Fail open with an unavailable callback when the hosted checkout verification broker never completes its ready handshake.

## 1.0.61

- Add `mountCheckoutChallenge()` so custom storefront domains can complete the hosted checkout verification broker and retry invoice creation.

## 1.0.60

- Reject post-purchase `redirectUrl` values that still contain unresolved template placeholders.

## 1.0.59

- Add `redirectUrl` to `checkout()` and `buildCheckoutUrl()` for channel-specific post-purchase returns.
- Keep `currency` and the post-purchase redirect in the same idempotent invoice-create intent.
- Publish npm from the public Storefront wrapper and keep the internal CDN implementation private.
- Correct the cart custom-field and `line_id` examples in the npm and Mintlify documentation.

## 1.0.58

- Add buyer-selected `currency` to `checkout()` and `buildCheckoutUrl()` so quote and invoice creation use the same currency.

## 1.0.57

- Preserve checkout-origin Turnstile challenge details on `buildCheckoutUrl()` errors so headless callers can render the proof and retry.

## 1.0.56

- Retain checkout idempotency keys until invoice-create responses are both parseable and definitive, preventing duplicate orders after truncated response bodies.
- Expose server-requested Turnstile challenges so official storefront checkout surfaces can collect and resubmit human-verification proofs.

## 1.0.55

- Retain independent checkout idempotency keys for overlapping buyer intents, so an ambiguous response cannot duplicate an earlier invoice when another checkout starts concurrently.

## 1.0.54

- Reuse checkout idempotency keys after ambiguous transport failures, while rotating them after definitive responses or changed buyer intent.
- Forward optional `storefront_checkout` Turnstile proofs without changing the idempotent create command.

## 1.0.51

- Send a fresh idempotency key for every storefront checkout action so network retries replay safely without reviving an older abandoned cart.

## 1.0.50

- Expose authoritative paid and due amounts in customer order details.
- Expose volume discounts as a separate buyer-facing price adjustment.

## 1.0.49

- Expose the complete customer-order pricing breakdown, including buyer-paid processing fees.

## 1.0.48

- Preserve Headless customer review scores by exposing the stored `1`, `3`, and `5` sentiment levels.
- Count the 256-character review limit by visible Unicode characters, including joined emoji.

## 1.0.47

- Add authenticated `orderReview()` and `submitOrderReview()` methods to the Headless Customer SDK.
- Scope review reads and writes to the signed-in customer's own shop and order.

## 1.0.46

- Headless customer dashboards now expose Shoppex file deliverables as authenticated `downloadIndex` values instead of hosted-portal-only URLs.

## 1.0.45

- Expand `@shoppexio/storefront/customer` from order history to buyer-safe customer portal parity: account settings, support, licenses, wallet checkout handoffs, subscriptions, replacements, loyalty, warranties, favorites, affiliate links, and reseller flows.
- Validate every new customer response against the shared Shoppex wire contracts.
- Publish the matching customer portal wire contracts with the SDK release.
- Keep direct money movement, provider redirects, password/session controls, reseller balance spending, and durable API-key creation behind Shoppex-controlled portal handoffs.

## 1.0.44

- Actually ship the `@shoppexio/storefront/customer` subpath in the published tarball: 1.0.42/1.0.43 documented the entry, but the release pipeline packs `packages/sdk`, whose exports map did not carry it (`ERR_PACKAGE_PATH_NOT_EXPORTED`).

## 1.0.43

- Require an explicit file index for customer downloads so the SDK never guesses which delivered file to return.
- Expose safe `downloadIndex` descriptors for headless order details.
- Keep the local customer session when remote logout fails, so revocation can be retried.

## 1.0.42

- Add `@shoppexio/storefront/customer` for custom OTP login, customer details, order history, order details, and downloads.
- Add opt-in `rememberMe` sessions for up to 30 days.
- Return safe `downloadIndex` descriptors for delivered files and retain the local session when logout revocation can be retried.

## 0.3.1

- Add `program_enabled` to affiliate validation responses.
- Clarify disabled affiliate program handling for headless storefronts.

## 0.3.0

- Publish the active Storefront SDK runtime under `@shoppexio/storefront`.
- Include cart coupon validation types for `{ productId, variantId }`.

## 0.1.1

- Package metadata and release polish.

## 0.1.0

- first public storefront package release
- typed package entrypoint for the existing Shoppex storefront runtime
