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

@tiledev/sdk-redo

v0.2.0

Published

Redo coverage (free returns / package protection) for Tile apps on @tiledev/sdk-shopify: asks Redo for the cart's coverage, keeps exactly one Redo line when the shopper chooses cover, and keeps it right as the cart changes. Headless: no UI.

Readme

@tiledev/sdk-redo

Redo coverage (free returns / package protection) for Tile apps on @tiledev/sdk-shopify. Redo sells its coverage as a product line the shopper adds to the Shopify cart. This package asks Redo what that line costs for the cart, puts exactly one of it in the cart when the shopper chooses cover, and keeps it right as the cart changes.

It is headless: no UI, no navigation, no shopper-facing words, no Live Layer. The app draws its own tile from useRedoCoverage.

npm install @tiledev/sdk-redo

@tiledev/sdk-shopify 0.15 or later and react are peers (the hook reads useCart and useCustomer). react-native is an optional peer and never imported.

What it does

the cart's other lines change → ask Redo (POST …/coverage-products)
  offer        → status "offered": the app shows its tile with offer.price
  not eligible → status "notEligible": a Redo line in the cart is taken out
  failed       → status "failed": the cart is left alone
shopper chooses cover    → setCovered(true):  one line of the offer's variant, attribute "true"
shopper chooses without  → setCovered(false): no Redo line,                   attribute "false"

The rules, each from Redo's own Hydrogen client (@redotech/redo-hydrogen 2.0.2) unless it says otherwise:

  1. Cover needs the shopper's consent. A cart starts uncovered, and the package never adds cover on its own. Only setCovered(true) does.
  2. Ask again only when the shopper's lines change. The quote key (quoteKeyFor) is each non-Redo line's id, variant, quantity, unit price and total, so adding or swapping the Redo line asks nothing new. An answer for a cart that has changed since is dropped.
  3. Exactly one Redo line. Covering takes out every other Redo line (an older tier, a second line, a quantity above 1) and adds one of the offer's variant, quantity 1. Uncovering takes out every Redo line.
  4. The attribute says the choice. cartAttribute from Redo's answer (default redo_opted_in_from_cart) is set to "true" or "false", keeping every other cart attribute.
  5. The cart follows Redo's answer. While covered, a new tier is swapped in (this package: Redo's client only fixes the tier on its next enable()). A cart that is no longer eligible, or that holds nothing but the Redo line, loses it (attribute "false").
  6. A failed answer touches nothing. A 400, a 500, no network, a timeout (10 s), or an answer we can't read: eligibility is unknown, so the cart is left as it is and onError is told.
  7. No store id, no Redo, unless the app gives a fixed offer. An empty storeId (or enabled: false) turns the whole package off: no request, no line is Redo's, no write. With fixedOffer (0.2.0), an app without a Team ID offers the same coverage for every cart instead (see below).

Setting it up in an app

1. The client, from the app's config

import { configureRedo, redoAwareGuard, redoCartExtension } from "@tiledev/sdk-redo";

export const redo = configureRedo({
  storeId: appConfig.redo?.storeId ?? "",             // the merchant's Redo Team ID; "" = off
  enabled: appConfig.redo?.enabled ?? true,
  productHandles: appConfig.redo?.productHandles ?? [], // the Redo product's handle(s): strongly recommended
  onError: (error, { at }) => reportError(error, { feature: "redo", at }),
});

Without a Team ID (0.2.0): Redo can't price each cart, but the app can offer one coverage for every cart, read from the store. fixedOffer is asked when a cart first needs an offer and kept; null (no such product yet) is notEligible and asked again next time; a throw is failed (onError at quote.fixedOffer). It is ignored while storeId is set. K-Marie's, the Redo product's first tier, as its earlier app charged:

export const redo = configureRedo({
  storeId: "",
  fixedOffer: async () => {
    const page = await shopify.collections.products("redo-product-apptile", { first: 5 });
    const product = page.nodes.find((each) => each.vendor === REDO_VENDOR);
    const tier = product?.variants.find((variant) => variant.availableForSale);
    if (!product || !tier) return null;
    return { variantId: tier.id, productId: product.id, title: product.title, price: tier.price, imageUrl: null, cartAttributeKey: "" };
  },
  productHandles: ["free-unlimited-return-valid-in-us"],
});

The trade-off is the price: one tier for every cart, where Redo's own quote charges more for a bigger order. Set the Team ID when the merchant has it, and Redo's quote takes over.

2. On ShopifyProvider

Build the extension list and the guard once (module constants, or one useMemo keyed on the config), since the provider reads them on every cart change and write:

const CART_EXTENSIONS = [cartHoldExtension({ client: cartHold }), redoCartExtension(redo)];
const CART_GUARD = redoAwareGuard(cartHoldGuard, redo); // cartHoldGuard may be undefined

<ShopifyProvider cartExtensions={CART_EXTENSIONS} cartGuard={CART_GUARD} onEvent={onShopifyEvent}>
  • redoCartExtension(redo): a Redo line is a service line on sdk-shopify's cart page (no quantity controls, no reservation clock).
  • redoAwareGuard(guard, redo): the app's guard with the Redo line kept out of it. Without it, Cart Hold would claim stock for the Redo line and stamp it like any product. A Redo add or increase is approved as it is; the guard isn't told when a Redo line lands or leaves; everything else reaches the guard unchanged. Only the hooks the guard has are on the result, so redoAwareGuard(undefined, redo) is {}, the same as no guard.

3. Toasts and analytics for the Redo line

sdk-shopify announces every line write as an event, the Redo line's too: cart:add with the "Added to cart" copy when cover goes on, cart:remove with "Removed from cart" when it comes off. The analytics SDK turns them into add_to_cart / remove_from_cart. To keep them quiet:

function onShopifyEvent(event: ShopifyEvent) {
  if (onlyRedoLinesChanged(event, redo)) return; // no toast (and decide whether analytics should hear it)
  showToast(event);
}

4. The cart screen's tile

const { status, offer, covered, busy, setCovered } = useRedoCoverage(redo);

| Field | Meaning | | --- | --- | | status | off · checking · offered · notEligible · failed. Show the tile for offered, and keep it (with offer) while checking. | | offer | { variantId, productId, title, price, imageUrl, cartAttributeKey }. While checking, the last offer (its price may be about to change). | | covered | The cart has a Redo line. Read from the cart, never kept separately. | | busy | A choice or a cart write is under way: disable the controls. | | setCovered(next) | The shopper's choice. true when it landed. Covering waits for Redo's answer when one is on its way; a choice made meanwhile wins. |

Redo's guidance is a choice at checkout, not only a toggle: the shopper picks "checkout with coverage" or "checkout without". The package supports both, since covered and setCovered are all either needs:

// A choice at checkout (Redo's own buttons go to checkout whatever the answer)
<Button disabled={busy} onPress={async () => { await setCovered(true); openCheckout(); }}>…</Button>
<Button disabled={busy} onPress={async () => { await setCovered(false); openCheckout(); }}>…</Button>

// A toggle
{offer && (status === "offered" || status === "checking") && (
  <CoverageTile price={offer.price} title={offer.title} on={covered} disabled={busy} onChange={setCovered} />
)}

The hook can be mounted on more than one screen: every hook on one client shares one question per cart, its answer, and one queue of cart writes.

What the merchant needs

  • A Redo Team ID (24 hex characters, e.g. 679d97672916716ed50b559f): the storeId. Without one, only a fixedOffer (one price for every cart).
  • The Redo product published to the app's sales channel. The Storefront API only sells what the app's channel can see; an unpublished Redo variant makes the add fail (onError at setCovered).
  • The Redo product's handle in the app's config (productHandles), so a Redo line is known from the first render: as a service line on the cart page, and in a cart that holds nothing else. Without it, a Redo line is known only once a quote has named its product, which is not a cart change, so sdk-shopify's cart page may show it as a product until the cart next changes, and a cart left with only a Redo line from an earlier session keeps it.

What it does not do

  • No UI, no copy, no Live Layer. The tile, its words and its placement are the app's.
  • It doesn't hide the Redo product from catalogue feeds (collections, search, recommendations). That needs an option in sdk-shopify; a follow-up. Meanwhile REDO_SEARCH_EXCLUSION (-vendor:'re:do' -product_type:package_protection) can be added to a Storefront search query.
  • It doesn't ask again when the shopper signs in. The customer id goes with the next question (as in Redo's client, whose key is the lines alone).
  • No web-only parts of Redo's client: its checkout-buttons HTML (GET …/checkout-buttons-ui), its info modal, and the redo.conciergeAssisted attribute it copies from the browser's localStorage.

Where it differs from Redo's Hydrogen client, and why

| Redo's client | This package | Why | | --- | --- | --- | | Sends every line, its own included, and cart.cost.totalAmount | Leaves Redo lines out; priceTotal is the other lines' totals added exactly | The same key always sends the same cart; sdk-shopify's totalAmount may or may not count a gift card, and counts a Redo line already in the cart | | Takes the Redo line out on the first answer after every page load | Keeps it | Cover chosen in an earlier session is still the shopper's choice; the toggle reads covered from the cart | | Fixes the tier only on its next enable() (its checkout button) | Swaps it as soon as Redo answers | A toggle has no later button press to fix it on | | A 200 that isn't JSON counts as not eligible (and takes the line out) | Failed: the cart is left alone | An answer we can't read says nothing about eligibility | | An offer with no price above 0 hides its buttons (eligible is false) | Failed (quote.response): hidden, and the cart left alone | The same for the shopper; reported, since it means Redo's answer changed shape |

Surface

Everything ships from the package root (no subpath).

| Export | Notes | | --- | --- | | configureRedo(options) | The client: enabled, quote(cart, customer?), isRedoLine(line), isRedoVariant(variantId), reportError(error, at). | | useRedoCoverage(redo) | { status, offer, covered, busy, setCovered }, inside ShopifyProvider. | | redoCartExtension(redo) | For cartExtensions: { name: "redo", lineFacts }. | | redoAwareGuard(guard, redo) | For cartGuard: the guard with Redo's line kept out. | | onlyRedoLinesChanged(event, redo) | For onEvent: every line the event changed is Redo's. | | quoteRequestFor(cart, redo, customer?) | Pure: the quote request body. | | quoteKeyFor(cart, redo) | Pure: changes only when the non-Redo lines change; "" for none. | | cartChangesFor(cart, redo, { covered, offer }) | Pure: { removeLineIds, addVariantId, attribute } for a choice. | | coverageStateFor(…) · automaticCartChangesFor(…) | Pure: what the hook shows, and the writes it makes on its own. | | REDO_VENDOR · REDO_SEARCH_EXCLUSION | "re:do" · "-vendor:'re:do' -product_type:package_protection". | | REDO_DEFAULT_CART_ATTRIBUTE_KEY · REDO_DEFAULT_API_BASE_URL · REDO_DEFAULT_TIMEOUT_MS | "redo_opted_in_from_cart" · "https://api.getredo.com/v2.2" · 10000. | | type RedoOptions, RedoClient, RedoOffer, RedoQuote, RedoCoverage, RedoCoverageStatus, RedoCartChanges, CartForRedo, … | |

interface RedoOptions {
  storeId: string;            // Redo Team ID; "" = off (unless fixedOffer)
  fixedOffer?: () => Promise<RedoOffer | null>; // 0.2.0: without a storeId, one offer for every cart
  enabled?: boolean;          // default true; false = off whatever the storeId
  apiBaseUrl?: string;        // default "https://api.getredo.com/v2.2"
  productHandles?: string[];  // the Redo product's handles, so its line is known before any quote
  fetch?: typeof fetch;       // for tests; called plainly, never as a method
  timeoutMs?: number;         // default 10000
  onError?: (error: unknown, context: { at: string }) => void;
}

The cart, extension and guard are typed by their shape, not by importing sdk-shopify's types, so the package builds without forcing an sdk-shopify version; test/types.check.ts checks the shapes against sdk-shopify's real Cart, CartExtension, CartLineGuard and ShopifyEvent.

Where onError is told (at)

| at | What failed | | --- | --- | | quote.fixedOffer | fixedOffer threw, or gave no variant, product or price above 0 (asked again next time). | | quote.status | Redo answered an error status (the error carries status and body). | | quote.network | Redo couldn't be reached (what fetch threw). | | quote.timeout | No answer within timeoutMs; the request is aborted. | | quote.response | A 200 we can't use: not JSON, no coverageProducts list, an offer with no variant, product or price above 0. | | quote.request | A cart that couldn't be turned into a request. | | setCovered | A cart write for the shopper's choice (Shopify refused it, the guard or the line limit refused the add, the attribute couldn't be written). | | keepCoverageRight | A cart write the hook made on its own (a tier swap, a removal). |

Wire protocol

POST {apiBaseUrl}/stores/{storeId}/coverage-products, Content-Type: application/json, no auth.

{
  "cart": {
    "lineItems": [{
      "id": "gid://shopify/CartLine/…",                  // Shopify gids, as Redo's client sends them
      "originalPrice": { "amount": "29.99", "currency": "USD" }, // one unit
      "priceTotal": { "amount": "59.98", "currency": "USD" },    // the line, after its own discounts
      "product": { "id": "gid://shopify/Product/1" },
      "variant": { "id": "gid://shopify/ProductVariant/1" },
      "quantity": 2
    }],
    "priceTotal": { "amount": "59.98", "currency": "USD" }       // the lines above, added
  },
  "customer": { "id": "", "country": "US" }                       // id "" when signed out
}

A 200 answers { "coverageProducts": [{ "cartInfoToEnable": { "productId", "variantId", "cartAttribute", "selectedVariant": { "price": { "amount", "currencyCode" }, "title", "image", "product" } } }] }. Redo names the product and variant by number; the package turns them into gids (gid://shopify/ProductVariant/<n>, gid://shopify/Product/<n>) and takes gids as they are. An empty list means the cart is not eligible.

Tests

npm test builds, then runs plain node scripts (no network, no renderer):

  • test/cart.test.mjs (20): the request, the key, the cart changes for every choice, the event helper.
  • test/client.test.mjs (26): off switches, the offer and its gids, the lines learned, not eligible, every failure (400, 500, 503, network, timeout, unreadable, unusable), an unbound fetch.
  • test/provider-parts.test.mjs (11): the cart extension and the guard composition.
  • test/coverage.test.mjs (12): the hook's decisions as plain functions.
  • test/use-redo-coverage.test.mjs (17): the hook end to end through a small stand-in for React and for sdk-shopify's useCart (asking, choosing, tier swaps, failures, out-of-order answers, quick choices, two screens).
  • tsc -p test: the shapes against sdk-shopify's real types.