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.2

Published

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

Downloads

338

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. |

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). Its build.rs compiles swift/storekit_bridge.swift to a static lib and links it; package.json lists StoreKit and Foundation so perry's link step adds -framework.
  • 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