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

@elata-biosciences/app-payments

v0.3.0

Published

In-app purchases for sandboxed apps in the Elata appstore

Readme

@elata-biosciences/app-payments

In-app purchases for sandboxed apps running in the Elata appstore.

The appstore renders apps inside a sandboxed iframe with no wallet or backend access. This package lets the app request a purchase from the parent frame and read what the user owns, all over postMessage. The parent owns the payment UI (PayEmbed, wallet, on-chain settlement) and the session. Your app declares intent and reads entitlements; it never touches wallets, USDC, or transaction signing.

Install

pnpm add @elata-biosciences/app-payments

The API surface

import {
  requestPurchase,
  hasItem,
  getOwnedItems,
  getCatalog,
  AppPaymentsError,
} from "@elata-biosciences/app-payments";

| Function | Signature | Returns | Use it to | |----------|-----------|---------|-----------| | requestPurchase | (input: RequestPurchaseInput) | Promise<PurchaseResult> | Start a purchase; opens the host checkout | | hasItem | (contentId: number, opts?) | Promise<boolean> | Check whether the user owns one item | | getOwnedItems | (opts?) | Promise<number[]> | List every owned contentId (sorted) | | getCatalog | (opts?) | Promise<CatalogItem[]> | List the app's items (price/title/desc) |

You are responsible for exactly two things: read entitlements to decide what to show, and react to the purchase result (grant the benefit on success; handle cancel/error gracefully). The host does everything else.

The correct flow

Don't call requestPurchase in isolation. Gate the UI on ownership first, then purchase, then apply and persist the benefit:

import {
  getCatalog,
  getOwnedItems,
  requestPurchase,
  AppPaymentsError,
} from "@elata-biosciences/app-payments";

// 1. Read the catalog and what the user already owns.
const [items, owned] = await Promise.all([getCatalog(), getOwnedItems()]);
const ownedSet = new Set(owned);

// 2. Render: owned items show "Use", unowned show "Buy".
function render() {
  for (const item of items) {
    const isOwned = ownedSet.has(item.contentId);
    // isOwned ? showUse(item) : showBuy(item)
  }
}

// 3. Buy. Branch on result.status.
async function buy(item) {
  try {
    const result = await requestPurchase({
      contentId: item.contentId,
      priceUsdc: formatUsdc(item.priceUsdc), // display hint only (see below)
      title: item.title,
      description: item.description ?? undefined,
    });
    switch (result.status) {
      case "success":
        ownedSet.add(item.contentId);
        applyBenefit(item.contentId); // 4. grant the effect now…
        render();
        break;
      case "cancelled":
        break; // user backed out before paying — no-op
      case "error":
        showError(result.error);
        break;
    }
  } catch (err) {
    // Thrown only for SDK-level problems (no parent, bad input, timeout).
    showError(err instanceof AppPaymentsError ? `${err.code}: ${err.message}` : String(err));
  }
}

// 5. On startup, re-derive ownership and re-apply — the host is the source of
// truth, so the benefit survives reloads without any local persistence.
for (const id of owned) applyBenefit(id);

requestPurchase records that the user owns an item. Your app decides what owning it does — the applyBenefit step is the part that's actually a feature.

Purchasing

RequestPurchaseInput:

{ contentId: number; priceUsdc?: string; title?: string; description?: string; timeoutMs?: number }

priceUsdc / title / description are display hints only — the host refetches the authoritative listing and overrides them. requestPurchase defaults to a 5-minute timeout.

PurchaseResult is a discriminated union — branch on status:

{ status: "success",   requestId: string, txHash: string }
{ status: "cancelled", requestId: string }
{ status: "error",     requestId: string, error: string }

The promise never rejects on a normal cancelled / error outcome — those are part of the resolved value. It only rejects (throwing AppPaymentsError) on local failures: see the error table below.

The empty-txHash gotcha

When the user already owns the item, the host short-circuits and returns { status: "success", txHash: "" }. If you key off if (result.txHash) you'll mishandle the already-owned case. Branch on result.status, never on txHash truthiness.

Reading entitlements

if (await hasItem(7)) unlockPermanentSkin();
const owned = await getOwnedItems(); // e.g. [1, 4, 7]

Both take an optional { timeoutMs?, window? } (default timeout 10 s) and follow the same error model as below.

Host support: hasItem / getOwnedItems require the appstore's offchain entitlement handlers (appstore PR #472). Until that ships, calls against the live host time out. Degrade gracefully: use a short timeoutMs and, on timeout, fall back to showing items as available rather than dead-ending the UI.

let ownedSet;
try {
  ownedSet = new Set(await getOwnedItems({ timeoutMs: 4000 }));
} catch (err) {
  if (err instanceof AppPaymentsError && err.code === "timeout") {
    ownedSet = new Set();        // ownership unknown — show everything as buyable
  } else throw err;
}

The catalog and price units

getCatalog() returns the app's active listings so you don't hard-code IDs or prices:

interface CatalogItem {
  contentId: number;
  title: string;
  description?: string | null;
  imageUrl?: string | null;
  priceUsdc: string; // USDC in BASE UNITS (6 decimals): "50000" === $0.05
}

Mind the two price conventions — they share a field name but differ:

  • CatalogItem.priceUsdc is base units (6 decimals). Format it for display:
    const formatUsdc = (base: string) => `$${(Number(base) / 1e6).toFixed(2)}`;
    formatUsdc("50000"); // "$0.05"
  • RequestPurchaseInput.priceUsdc is a free-form display hint the SDK does not interpret and the host overrides. Pass whatever you'd show the user (e.g. the formatted string above).

Error model

requestPurchase rejects, and the entitlement queries reject, only with AppPaymentsError (has a .code):

| Code | Meaning | |------|---------| | invalid_input | contentId was not a non-negative integer (or a bad option type) | | no_window | not running in a browser environment | | no_parent | not running inside an iframe with an appstore parent | | no_crypto | crypto.randomUUID is unavailable | | timeout | no response within timeoutMs | | not_authenticated | host reports no signed-in user (entitlement queries) | | fetch_failed | host failed to look up entitlements |

How it works

  1. Generates a requestId via crypto.randomUUID().
  2. Posts { type: "elata:iap:request", requestId, contentId, ... } to window.parent.
  3. Listens for the matching { type: "elata:iap:result", requestId, ... } reply.
  4. Resolves the promise; cleans up the listener and timer.

The entitlement queries work the same way with their own message types (elata:iap:hasItem, elata:iap:listOwned, elata:iap:getCatalog). The parent validates the message source against the app's registered origin before responding, refetches authoritative data, and replies.

Security model

  • The app cannot fabricate ownership. Entitlements are written server-side only after the on-chain transaction confirms; the app just reads them.
  • The app cannot read card details, wallet keys, or session tokens. The parent owns those surfaces.
  • The price the app sends is a hint; the host always refetches the authoritative price.
  • Replay safety is per-request, not single-ownership enforcement. The host keys on (userId, requestId), so re-sending the same request is idempotent and won't double-charge. A fresh requestId for an item the user already owns records another entitlement row — the checkout UI is what discourages repeat buys, the server does not forbid them. Treat the host (via getOwnedItems / hasItem) as the source of truth for what a user owns.

Try it

  • Runnable demo: examples/iap-demo — one self-contained index.html that runs standalone via a built-in mock host, exercising the full flow.
  • Guide: Using IAP in a browser app.
  • Platform integration guide: IAP_SDK_INTEGRATION.md in the appstore repo (host-side details, product model, known gaps).

Versioning

This package is 0.x and the protocol is v1. Breaking protocol changes will bump both the package major version and the protocol version field.