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

expo-iap-utils

v1.1.0

Published

Expo module with lightweight read-only StoreKit 2 / Google Play Billing utilities — storefront country, transaction-based identity recovery, and other helpers that bypass the IAP/subscription engine.

Readme

expo-iap-utils

npm version license

A small Expo module with read-only StoreKit 2 / Google Play Billing utilities that intentionally bypass the heavyweight IAP/subscription engines. Each util encapsulates its own short-lived native lifecycle, so JS callers never have to think about initConnection, endConnection, or transaction listeners.

Designed to coexist with subscription SDKs (Adapty, RevenueCat, Qonversion, react-native-iap) without fighting them for the global StoreKit/BillingClient state.

Available utilities

| Function | Returns | | --- | --- | | getStorefrontCountry() | Active App Store storefront / Play billing country as lowercase ISO 3166-1 alpha-2 ("us", "de", …) | | getCustomerUserIdFromTransactions() | The app-embedded identifier (appAccountToken on iOS, obfuscatedAccountId on Android) carried by past purchases on the active store account, or null if none | | syncStoreTransactions() | Forces StoreKit to refresh transactions from Apple (StoreKit 2 AppStore.sync()); no-op on Android. Use from user-initiated "Restore Purchases" only |

More utilities will be added here as the library grows.

Why

If you already have a subscription SDK, you usually don't want a second IAP framework in the binary just to ask one or two read-only questions about the store account. This library covers those edge cases:

  • Real storefront country for pricing/LTV reporting, distinct from device locale.
  • Identity recovery from past transactions for apps with no traditional auth — read the user identifier the app itself embedded at purchase time, surviving reinstalls and device changes on the same store account.

Installation

npx expo install expo-iap-utils

Then rebuild your native project:

npx expo prebuild --clean   # if you use CNG
npx pod-install             # iOS

Requirements

| Platform | Minimum | | --- | --- | | iOS | 15.1 (StoreKit 2) | | Android | API 21 + Google Play Services | | Web | Modern browser with Intl.Locale |

API

getStorefrontCountry(): Promise<string | null>

Lowercase ISO 3166-1 alpha-2 of the active storefront, or null if it cannot be determined.

import { getStorefrontCountry } from 'expo-iap-utils';

const country = await getStorefrontCountry();
// → "us" | "de" | "jp" | … | null

The first successful call is cached for the JS runtime lifetime (storefront cannot change without an app restart). Failed lookups are not cached, so transient cold-start misses retry on the next call.

How it works

iOS — StoreKit 2 await Storefront.current?.countryCode. Async getter that suspends until StoreKit resolves the active App Store account; no SKPaymentTransactionObserver or other setup required. Returns alpha-3 (e.g. "USA"); JS normalizes to alpha-2 via a 249-entry mapping.

Android — short-lived BillingClientgetBillingConfigAsyncendConnection. Independent from any other BillingClient your app may have open (e.g. an IAP SDK's). Returns null for every failure mode (no Play Services, setup failure, billing error) so JS can fall back.

WebIntl.Locale(navigator.language).region, normalized through the same pipeline.

Contract

  • Output is guaranteed to be either:
    • A two-character, lowercase, ASCII string matching ^[a-z]{2}$, or
    • null.
  • No undefined, no uppercase, no whitespace.
  • Unknown ISO codes (e.g. if Apple adds a new region not in the lookup table) resolve to null rather than leaking a raw alpha-3.

getCustomerUserIdFromTransactions(): Promise<string | null>

Reads the per-purchase identifier the app embedded at purchase time — appAccountToken on iOS (StoreKit 2) or obfuscatedAccountId on Android (Play Billing) — by walking on-device transaction history. Returns the first usable token, or null if none is stored.

import { getCustomerUserIdFromTransactions } from 'expo-iap-utils';

const recoveredId = await getCustomerUserIdFromTransactions();
// → "550E8400-E29B-41D4-A716-446655440000" | null

Intended for apps without traditional authentication that use the purchase token as a stable per-user identifier across reinstalls and devices on the same store account.

When it returns null

  • The user has never purchased anything on this store account.
  • Their purchase predates the app passing appAccountToken / obfuscatedAccountId.
  • The platform has no readable transaction history (web, no Play Services, etc.).

How it works

iOS — iterates StoreKit.Transaction.currentEntitlements first (active subs / non-consumables), then falls back to Transaction.all (full history, for users whose subscription has lapsed). Skips .familyShared ownership: a Family Sharing inheritor's token belongs to the family organizer, not the current user. Skips unverified results. Returns the UUID uppercased, or null.

Android — short-lived BillingClientqueryPurchasesAsync(SUBS)queryPurchasesAsync(INAPP)endConnection. Returns the first non-empty obfuscatedAccountId from Purchase.getAccountIdentifiers(). Independent from any other BillingClient (e.g. an IAP SDK's).

Web — always null.

Caveats

  • The function reads what the app itself stored at purchase time. You must pass the identifier when initiating purchases (StoreKit 2: Product.PurchaseOption.appAccountToken(uuid); Play Billing: BillingFlowParams.Builder().setObfuscatedAccountId(...)). Most subscription SDKs (Adapty, RevenueCat, etc.) accept this value at activation.
  • iOS appAccountToken is a UUID. If your in-app user IDs are not UUIDs, generate a deterministic UUIDv5 from your user ID before passing it to the store.
  • Not memoized — caller decides if/when to re-query.

syncStoreTransactions(): Promise<boolean>

Forces StoreKit to fetch the latest transactions and subscription status from Apple's servers, wrapping StoreKit 2's AppStore.sync(). On Android this is a no-op (Google Play Billing queryPurchasesAsync is always fresh) and resolves true so callers don't need to branch on Platform.OS.

import { syncStoreTransactions, getCustomerUserIdFromTransactions } from 'expo-iap-utils';

// Inside a "Restore Purchases" button handler
async function onRestorePressed() {
  await syncStoreTransactions();
  const recovered = await getCustomerUserIdFromTransactions();
  // … hand off to your auth/identity flow
}

⚠️ Apple guideline: the iOS sync may pop a system "Sign In to App Store" sheet if the device isn't signed in. Apple's review team penalizes apps that show that sheet outside an explicit user-initiated action. Call this only from a "Restore Purchases" button handler (or equivalent), never from app start, background refresh, or implicit recovery.

Why pair it with getCustomerUserIdFromTransactions()

Transaction.all and Transaction.currentEntitlements read from StoreKit's local cache. On a freshly reinstalled app (or after switching Apple ID) that cache may be empty until StoreKit syncs. syncStoreTransactions() is the documented way to force that sync before reading.

Return value

  • true — sync succeeded (iOS) or no-op (Android).
  • false — sync threw on iOS (most often the user dismissed the sign-in sheet, or there was no network), or the platform has no native module (web).

Comparison to other approaches

| Source | What it returns | Notes | | --- | --- | --- | | expo-localization regionCode | Device region (alpha-2) | Diverges from the user's store account | | react-native-iap getStorefront() / getAvailablePurchases() | Storefront / past purchases | Requires initConnection lifecycle and registers global IAP listeners — risk of double-handling alongside another subscription SDK | | AdaptyPaywallProduct.regionCode | Storefront (alpha-2) | Requires loading a paywall first | | expo-iap-utils | Storefront, transaction-based identity | No setup, no listeners, coexists with subscription SDKs |

License

MIT © Dmitry Matatov