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

@perryts/storekit

v0.1.3

Published

StoreKit 2 in-app purchase bindings (iOS / macOS) for the Perry TypeScript-to-native compiler. For Android, see @perryts/play-billing.

Downloads

234

Readme

@perryts/storekit

StoreKit 2 in-app purchase bindings (iOS / macOS) for Perry. For Android, use @perryts/play-billing.

Platforms

| Target | Implementation | | --------------- | ----------------------------------------------------------------------- | | iOS 16+ | Native — Swift bridge over StoreKit 2 (Product.products(for:), etc.). | | macOS 13+ | Native — same Swift bridge. | | Android | Stub. Use @perryts/play-billing for Google Play Billing. | | Linux / Windows | Stub — every call resolves with a "not available" JSON payload. |

Build requirements

No Swift toolchain, Xcode, or macOS host is required to build this package — for any target, including iOS:

  • Linux / Windows / Android targets — pure Rust. crate-stub (and crate-ios, which compiles the same stub fallback for non-Apple targets) builds with nothing but a Rust toolchain.
  • iOS / macOS targets on a Maccrate-ios/build.rs detects swiftc via xcrun and compiles swift/storekit_bridge.swift from source, as before.
  • iOS / macOS targets on a non-Mac host — the npm package ships prebuilt Swift bridge archives under crate-ios/prebuilt/<rust-triple>/ (built on macOS CI at publish time from the same Swift source). When build.rs finds no swiftc, it links the prebuilt archive for the target triple instead. The Rust side is pure Rust (perry-ffidashmap, once_cell), so cargo build --target aarch64-apple-ios needs only rustup target add aarch64-apple-ios — no Apple SDK, no C compiler. Perry's Linux builders then link the resulting archive into the app with ld64.lld and an Apple sysroot (PERRY_IOS_SYSROOT).

Set PERRY_STOREKIT_BRIDGE=source or =prebuilt to force either path. When working from a git checkout (which does not contain the prebuilt archives) on a non-Mac host, run scripts/build-bridges.sh once on a Mac and copy crate-ios/prebuilt/ over — or consume the npm package, which includes them.

Installation

npm install @perryts/storekit

The package targets perry-ffi ABI v0.5 (perry.nativeLibrary.abiVersion: "0.5" in package.json). Perry validates compatibility at build time.

Quick start

import {
  js_storekit_start_listener,
  js_storekit_load_products,
  js_storekit_purchase,
  js_storekit_has_subscription,
  js_storekit_get_jws,
  js_storekit_restore,
} from "@perryts/storekit";

// Boot the Transaction.updates listener once at launch — handles
// Ask-to-Buy approvals, family-shared entitlements, auto-renew, etc.
js_storekit_start_listener();

// Load products you have configured in App Store Connect.
const productsJson = await js_storekit_load_products(
  "com.example.pro_monthly,com.example.pro_annual",
);
const products = JSON.parse(productsJson);

// Drive the purchase sheet. The product must have been loaded first —
// StoreKit 2 needs the in-memory `Product` value to call `purchase()`.
const purchaseJson = await js_storekit_purchase("com.example.pro_monthly");
const purchase = JSON.parse(purchaseJson);
if (purchase.success) {
  // purchase.jws → server-side receipt validation
  // purchase.transactionId, purchase.purchaseDate → audit log
}

// Check entitlements at any time.
const subJson = await js_storekit_has_subscription();
const { hasSubscription } = JSON.parse(subJson);

Typed wrapper (recommended)

The native FFI returns JSON strings so the cross-language contract stays simple. In your app, wrap the calls with the types this package re-exports:

import {
  js_storekit_load_products,
  js_storekit_purchase,
  js_storekit_has_subscription,
  js_storekit_get_jws,
  js_storekit_restore,
  type Product,
  type PurchaseResult,
  type HasSubscriptionResult,
  type JwsResult,
  type RestoreResult,
} from "@perryts/storekit";

export async function loadProducts(ids: string[]): Promise<Product[]> {
  const json = await js_storekit_load_products(ids.join(","));
  const parsed = JSON.parse(json);
  if (parsed && typeof parsed === "object" && "error" in parsed) {
    throw new Error(parsed.error as string);
  }
  return parsed as Product[];
}

export async function purchase(productId: string): Promise<PurchaseResult> {
  const json = await js_storekit_purchase(productId);
  return JSON.parse(json) as PurchaseResult;
}

export async function hasSubscription(): Promise<boolean> {
  const json = await js_storekit_has_subscription();
  return (JSON.parse(json) as HasSubscriptionResult).hasSubscription;
}

export async function getJWS(): Promise<string | null> {
  const json = await js_storekit_get_jws();
  return (JSON.parse(json) as JwsResult).jws;
}

export async function restorePurchases(): Promise<RestoreResult> {
  const json = await js_storekit_restore();
  return JSON.parse(json) as RestoreResult;
}

The Product and PurchaseResult shapes match the sketch in issue #537, with one practical addition: PurchaseResult.jws carries the App Store-issued JWS, which is what Apple's App Store Server API expects for server-side validation. (The legacy base64 receipt is no longer the recommended StoreKit 2 path.)

API reference

js_storekit_start_listener(): void

Start Transaction.updates in a detached Task. Verified transactions are automatically finish()-ed. Call exactly once at app launch — calling again cancels the previous listener.

js_storekit_load_products(commaSeparatedIds: string): Promise<string>

Resolves with a JSON array of Product objects. On failure: {"error": "..."}.

Loaded products are cached in a Swift actor so js_storekit_purchase can look them up by ID.

js_storekit_purchase(productId: string): Promise<string>

Resolves with a JSON PurchaseResult. Possible shapes:

{ "success": true,  "jws": "eyJ…", "productId": "…", "transactionId": "…", "purchaseDate": "2026-05-07T10:23:11.123Z", "cancelled": false }
{ "success": false, "cancelled": true }
{ "success": false, "pending": true }
{ "success": false, "error": "…" }

js_storekit_has_subscription(): Promise<string>

Resolves with {"hasSubscription": boolean}. True iff at least one of Transaction.currentEntitlements is verified and has no revocationDate.

js_storekit_get_jws(): Promise<string>

Resolves with {"jws": "…"} (most recent verified entitlement) or {"jws": null} (no active entitlement). Hand the JWS to your server, validate against Apple, then trust it.

js_storekit_restore(): Promise<string>

Calls AppStore.sync(). Resolves with {"success": true} or {"error": "…", "success": false}. Apple recommends only invoking this from a user-tapped "Restore Purchases" button.

How it's wired

TypeScript                Rust (perry-ffi 0.5)         Swift (StoreKit 2)
-------------------       ----------------------       ----------------------
js_storekit_purchase  →   #[no_mangle] extern "C"  →   @_cdecl bridge fn
                          fn js_storekit_purchase      runs Task { … }
                          returns *mut Promise         calls back with JSON
                          ←─── promise.resolve_string(json) ←──────────
  • crate-ios/ — Apple-platform crate. Depends on perry-ffi = "0.5" (tracked at git+https://github.com/PerryTS/perry). When targeting iOS/macOS, its build.rs compiles swift/storekit_bridge.swift to a static lib and links it — or, on hosts without swiftc (Linux builders), links the prebuilt bridge archive shipped under crate-ios/prebuilt/<rust-triple>/; package.json lists StoreKit and Foundation so perry's link step adds -framework. For any other target, build.rs is a no-op and the crate compiles the same stub fallback as crate-stub, so it builds on machines without Swift or Xcode.
  • crate-stub/ — non-Apple crate. Same exported js_storekit_* symbol set, but every call resolves immediately with a "not available on this platform" payload so calling code can fall back to a Stripe/web flow without #ifdef-style platform checks.
  • package.json :: perry.nativeLibrary — declares abiVersion: "0.5", the FFI symbol list, and per-target crate / lib / frameworks.

Server-side validation

This binding does not validate JWS receipts itself — that's plain HTTPS against Apple's App Store Server API. A typical flow:

  1. Client: js_storekit_purchase("…") → JWS.
  2. Client → your server: POST /verify-storekit { jws }.
  3. Server: validate signature, check transactionId, mark entitlement.
  4. Server → client: confirmation.

Periodically poll js_storekit_has_subscription() (or react to Transaction.updates) to keep the local cache fresh.

License

MIT