npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tabcommerceio/promo-sdk

v1.1.0

Published

Browser SDK for Tab Promo storefront facts and commerce helpers.

Readme

@tabcommerceio/promo-sdk

@tabcommerceio/promo-sdk is a browser-only, ESM-only SDK for rendering Tab Promo facts in a Shopify Online Store theme. It reads a shop's same-origin App Proxy and provides explicit Cart AJAX actions. Your theme owns the DOM, copy, accessibility, locale, and styling; Tab Promo owns promotion evaluation and Shopify checkout remains authoritative for discounts and final totals.

The package does not provide rendered Tab Promo HTML, UI components, an Admin API client, customer identity inputs, or a local promotion evaluator.

Stable since 1.0.0; releases follow SemVer. A repository version does not by itself mean that version has been published — check npm.

The committed manifest version is the release signal. A version change merged to main is validated and published by GitHub Actions through npm Trusted Publishing, then tagged as promo-sdk@<version>. Source changes without a version change are not published, and CI never bumps the version itself. Package source metadata points to the private tabcommerce/tab-promo repository. Prerelease publishes target the next dist-tag; stable versions use latest. The repository's publish:promo-sdk:dry command performs the same package gate without registry authentication or publication.

Service entitlement

Supported public promotion facts (/promos) require an effective active or trialing Pro or Growth subscription. Installing this npm package does not confer service access. Starter includes the official Theme Extension's rendered promotion experience and shared gift/top-up helpers. Those browser endpoints do not authenticate a particular JavaScript library; caller headers are not paid entitlement credentials. Free Preview does not provide production storefront access. Pending downgrades preserve the current effective plan's rights.

Install and initialize

pnpm add @tabcommerceio/promo-sdk@^1.0.0

Create one client for the current page or storefront shell. proxyBase must be the merchant-configured, same-origin relative /apps/... path. It is commonly /apps/tab-promo, but integrations must not assume that path is universal.

import { init, type PromoDiagnostic } from '@tabcommerceio/promo-sdk'

const promo = init({
  proxyBase: '/apps/tab-promo',
  timeoutMs: 2_500,
  onDiagnostic(diagnostic: PromoDiagnostic) {
    // Optional: send a non-shopper-facing diagnostic to your own monitoring.
    reportStorefrontDiagnostic(diagnostic)
  },
})

Do not pass an Admin token, API key, shop, customer identity, or an absolute URL. Shopify signs the App Proxy request and Tab Promo derives identity on the server.

Public root API

The package has one public import path:

import { init, PromoSdkError, inspectGiftClaims, type PromoClient } from '@tabcommerceio/promo-sdk'

Its runtime exports are init, PromoSdkError, and inspectGiftClaims. init(options) returns a PromoClient with these public methods:

| Method | Result | | --- | --- | | resolve(request) | Current PromosResponse, or null when a facts read cannot be used. | | mount(options) | Resolves facts for a renderer and returns a disposer. | | refresh() / notifyCartChanged() | Refresh active fact renderers; notify after a theme-owned successful cart mutation. | | getTopupProducts(input) | Live top-up products, or null when unavailable. | | addTopupItem(input) | A Shopify-confirmed cart after an explicit top-up action. | | claimGift(input) / removeGift(input) | A Shopify-confirmed cart after an explicit gift action. | | reconcileGiftClaims({ facts, ...options }) | A gift-claim report for one response's gift_facts snapshot; removal is opt-in with { remove: true }. | | applyDiscountCode(input) | Whether a valid Shopify checkout redirect was started. | | captureAttributionSource({ token }) | A Shopify-confirmed cart carrying a Share-link token for last-touch source reporting. The public method name is retained. | | destroy() | Removes SDK listeners, timers, and active lifecycle work. |

inspectGiftClaims(cart, gifts) is a pure report helper. It compares a Cart AJAX payload with current public gift facts and returns claim states without changing the cart. Each accepted PromosResponse also has an opaque, client-local gift_facts snapshot. Pass that exact snapshot to reconcileGiftClaims; it prevents one surface's refresh from changing the facts used to review another surface's claims.

All public types are exported from this same root, including Promo, PromosResponse, Gift, TopupPoolProduct, CartJsPayload, and PromoSdkErrorCode. Do not import package internals or rely on a subpath: the root API is the supported SemVer boundary.

Render promotion facts

Use resolve for a one-off fact read. A product surface requires a current product ID; pass the selected variant when it is known.

import type { PromosResponse } from '@tabcommerceio/promo-sdk'

const response: PromosResponse | null = await promo.resolve({
  surface: 'product',
  productId: currentProductId,
  variantId: selectedVariantId,
  country: currentCountryCode,
})

if (response) {
  renderPromotionFacts(response.promos)
} else {
  clearPromotionFacts()
}

Use mount for a section that should refresh while the shopper remains on the page. Dispose it when the section is unloaded, and call destroy() at final page-shell teardown.

const stop = await promo.mount({
  request: { surface: 'global' },
  render(response) {
    renderPromotionFacts(response.promos)
  },
  onEmpty() {
    clearPromotionFacts()
  },
})

document.addEventListener('shopify:section:unload', stop, { once: true })

Render only facts you understand. The supported primitives are progress, status, and ladder; keep a conservative no-op fallback as the wire contract is additive. Do not calculate eligibility, discounts, thresholds, or campaign combinations locally.

import type { Promo } from '@tabcommerceio/promo-sdk'

function renderPromotionFact(promo: Promo) {
  switch (promo.primitive) {
    case 'progress':
      renderProgress(promo.current, promo.target, promo.remaining)
      return
    case 'status':
      renderStatus(promo.eligible)
      return
    case 'ladder':
      renderLadder(promo.rungs, promo.current_rung, promo.next_rung)
      return
    default:
      // A future primitive is not a reason to make up promotional UI.
      return
  }
}

A promo may carry display, merchant-authored text that Shopify checkout already shows customers: title is the campaign name, present only when checkout shows it as the label of a rule without its own label, and message is the rule's checkout discount label (omitted when it repeats title). Either field, or the whole object, may be absent. The text is passed through verbatim in the merchant's authoring language: insert it as plain text (never as HTML), and keep localization and surrounding copy in your theme. It never contains internal campaign notes or unpublished edits.

function promoHeading(promo: Promo): string | undefined {
  // Prefer your own localized copy; fall back to the merchant's text.
  return localizedCopyFor(promo.campaign_id) ?? promo.display?.title ?? promo.display?.message
}

heading.textContent = promoHeading(promo) ?? ''

Money values are integer minor units in the reported presentment currency. Use the currency's own fraction digits rather than assuming two decimal places.

function formatMinor(value: number, currency: string, locale: string): string {
  const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency })
  const digits = formatter.resolvedOptions().maximumFractionDigits
  return formatter.format(value / 10 ** digits)
}

The minimal no-framework example is in examples/vanilla. It imports only public root symbols and renders the three supported primitive facts without adding a local eligibility or pricing calculation.

Explicit commerce actions

Commerce changes must result from a shopper action. For example, fetch a top-up pool only when its chooser opens, and add an available variant only after a shopper chooses it:

const products = await promo.getTopupProducts({ poolId })
if (products !== null) renderTopupChoices(products)

await promo.addTopupItem({ variantId: selectedVariantId, quantity: 1 })
promo.notifyCartChanged()

The App Proxy requires an uppercase ISO 4217 currency. When currency is omitted as above, the SDK first derives it (and a valid presentment rate when available) from the current Cart AJAX payload. Pass currency explicitly only when a custom cart provider cannot supply it; explicit values must be uppercase three-letter codes and presentmentCurrencyRate, when supplied, must be greater than zero.

For a pinned gift, pass its public Gift fact to claimGift({ gift }). For a shopper-choice gift, show only its public available display variants and pass the selected variantId. Resolve current facts, then inspect reconciliation results before asking the shopper to approve a removal pass:

const facts = await promo.resolve({ surface: 'global' })
if (facts) {
  const report = await promo.reconcileGiftClaims({ facts: facts.gift_facts })
  showGiftClaimReview(report.claims)

  if (shopperConfirmedCleanup) {
    await promo.reconcileGiftClaims({ facts: facts.gift_facts, remove: true })
  }
}

For X-of-Y gifts (selection: 'pool_choice'), render the explicit candidates and only their available display variants. The quota is shared across products and variants; distinct_products allows one unit per product, while allow_repeats allows repeated units up to the shared quota. X=1 and Y=1 use this same interface. Pass the shopper's selected vector in one request:

if (gift.selection === 'pool_choice' && gift.eligible) {
  await promo.claimGift({
    gift,
    selections: [{ variantId: selectedVariantId, quantity: selectedQuantity }],
  })
}

The helper reads a fresh cart, serializes gift writes within a client and confirms every requested quantity. It does not guarantee an atomic transaction across tabs. After success or failure, refresh promotion facts and the cart UI. gift_claim_partial reports confirmed and missing selections in error.giftClaim; keep the confirmed items and ask before requesting missing items. gift_claim_conflict reports a cart that needs review. An excess marked quantity stays paid at checkout until the shopper explicitly removes it.

For gift_claim_unknown, disable new submissions for that rule. Use promo.checkGiftClaim({ ruleId }) to observe the pending batch without writing, including after eligibility is lost. A fully observed batch resolves without another write. A subsequent claimGift attempt also checks pending state and never resends it while unresolved. Offer a separate explicit abandonment action using promo.abandonGiftClaim({ ruleId }); this only clears local pending state. A delayed Shopify response may still add items, so read the cart and current facts again before asking for a new selection. Do not automatically retry, replace unavailable gifts or remove paid items. Reconciliation preserves valid pool quantities and reduces only explicitly approved excess gift quantities.

Pool support first ships in 1.1.0; the published 1.0.0 does not contain it. This checkout does not establish npm publication — check npm for 1.1.0. The official Theme builds shared claim code directly from source; npm publication is not its prerequisite. Pool authoring is available to every normally authorized merchant without operator registration. Release compatibility requires pool-capable official Theme and both Function profiles. There are currently no external SDK users to migrate; future custom gift integrations must use pool-capable decoding and claim/cleanup helpers. See the rollout runbook.

Use the client methods for these actions. Cart-line metadata, transport details, headers, and browser storage are implementation details, not public integration inputs.

Failure model

Facts reads fail soft. resolve() and getTopupProducts() return null for a timeout, network error, malformed response, or unusable HTTP response. Clear or preserve non-promotional UI and never block cart or checkout because facts cannot be read. mount() calls onEmpty for an empty or failed current response.

The read deadline covers cart reads and resolve retries. A custom cartProvider(signal?) may use the optional signal to cancel its own work; the SDK still settles on timeout or destruction when a provider ignores it. Disposing a mount or starting its next refresh cancels the previous read. Already dispatched cart writes retain their own confirmation semantics and are never replayed by the read deadline.

Cart helpers fail closed. addTopupItem, gift helpers, and reconciliation removal resolve only after Shopify returns a confirming fresh cart; otherwise they throw PromoSdkError. Handle that error as an explicit cart-action failure, not as a successful promotion. applyDiscountCode() starts the locale-aware Shopify discount route and reports only whether browser navigation was initiated; checkout decides whether the code is accepted. Clipboard access is deliberately outside the SDK contract: it never copies a code or treats a clipboard write as discount success.

The SDK does not log to the shopper console by default. onDiagnostic is an optional integration hook for non-blocking read diagnostics.