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

@kebabty/react-native

v0.8.3

Published

Kebabty subscription platform SDK for React Native — entitlements, paywalls and store purchase validation

Readme

@kebabty/react-native

Kebabty subscription SDK for React Native. StoreKit 2 on iOS, Play Billing on Android, with identity, entitlements, dashboard paywalls and server-side validation on both.

Install

npm install @kebabty/react-native
cd ios && pod install

Requires React Native 0.76+ and React 18+. iOS deployment target 15.1 (StoreKit 2); Android minSdk 24, compileSdk/targetSdk 36, JDK 17. The Android module autolinks through Gradle — no install step of its own — and adds com.android.installreferrer:installreferrer:2.2 and com.android.billingclient:billing:9.1.0 to your app. Override the billing version with ext.playBillingVersion if you need to, but not below 8.1.

Setup

import { Kebabty, KebabtyProvider } from '@kebabty/react-native'

await Kebabty.configure({ apiKey: 'pk_kbty_…' })

That is the whole setup. The origin, the app version and persistent storage are the SDK's own — it reads the version off your app bundle and persists through its native module (UserDefaults on iOS, SharedPreferences on Android). The only other thing worth passing is something only your app knows:

await Kebabty.configure({
  apiKey: 'pk_kbty_…',
  // pass the id here when the user is already signed in at launch
  appUserId: user ? String(user.id) : undefined,
})

Wrap the app in <KebabtyProvider> to let fire() present paywalls.

configure() restores the identity of the previous run, identifies the install, and picks up any StoreKit transaction left unfinished.

Identity

await Kebabty.identify(String(user.id)) // after login
await Kebabty.logout() // after logout — continues with a fresh anonymous id

An anonymous id stops resolving a profile once an appUserId claims it, so the SDK persists both ids and re-sends the appUserId on every launch. Two things follow:

  • logout() is not optional. The identity survives reinstall-free app restarts on its own; the only thing that clears it is logout(). An app that signs a user out without calling it keeps reporting purchases against the previous customer.
  • configure({ appUserId }) still wins over the remembered id, so passing it is harmless and is the right thing to do when your own store already knows who is signed in at launch.

Kebabty.getAppUserId() returns the identity currently in force.

Signed identity (optional)

An app that sets an identity secret in the dashboard must prove every appUserId it claims. The secret never ships in the binary — your backend computes hex(hmac_sha256(identity_secret, appUserId)) and the SDK sends it as X-Kebabty-Signature:

await Kebabty.configure({
  apiKey: 'pk_kbty_…',
  identitySignature: (appUserId) => api.get(`/kebabty-signature/${appUserId}`),
})

The callback is asked once per identity, cached, and re-asked if the digest is ever rejected (a rotated secret). A literal digest works too, via configure({ appUserId, identitySignature }), identify(id, { signature }) or Kebabty.setIdentitySignature(digest).

A literal digest only ever covers the appUserId passed alongside it, never one restored from a previous launch — so an app that signs identity and uses a literal digest has to pass appUserId to configure() every time. If it does not, the launch cannot sign the remembered id, the server refuses it, and the SDK warns and falls back to anonymous. The callback form has no such rule.

Apps with no identity secret pass nothing and behave exactly as before.

Purchases

const profile = await Kebabty.purchase(storeProductId)
const restored = await Kebabty.restorePurchases()
profile.entitlements.premium?.isActive

purchase() resolves only after the backend has recorded the purchase, and only then is it settled with the store — finished on StoreKit, acknowledged on Play. Nothing else settles it: a purchase the server never accepted stays open so a later launch can retry it. On Play that ordering has a deadline attached, since Google refunds anything unacknowledged after three days.

Play does not replace one subscription with another — it sells both. When the user is switching plans, name the one they are leaving:

await Kebabty.purchase('premium_yearly', {
  replaceStoreProductId: 'premium_monthly',
  replacementMode: 'charge_prorated_price',
})

Both fields are ignored on iOS, where the subscription group settles it.

A 409 (KebabtyPurchaseConflictError) means the purchase is registered to another Kebabty profile — permanent for the account asking, not for the device. The SDK stops replaying that transaction instead of destroying it, and tries it once more the moment identify() names a different user, which is exactly what the refusal tells the customer to do. The error carries .profile: the caller's own record as the server returned it with the refusal.

Restoring

restorePurchases() re-validates everything the store account currently grants — StoreKit's entitlements, or what the signed-in Google account still owns — and returns the profile the server ends up holding — a 200 from validate is not by itself a restore. It throws when the store had something and the account still holds nothing:

| throws | meaning | | ------------------------------------- | ------------------------------------------------------------- | | KebabtyPurchaseConflictError | held by another account — your transfer policy keeps it there | | KebabtyRestoreError family_shared | shared through Family Sharing, so it never leaves the buyer (iOS) | | KebabtyRestoreError not_granted | accepted, but no entitlement maps to the product |

An Apple ID with nothing to restore is not an error: the profile comes back unchanged.

Errors

Everything the SDK throws is a KebabtyError with a code:

| code | meaning | | -------------------- | -------------------------------------------------- | | network / server | transient; retryable is true | | unauthorized | the SDK key was rejected | | identity_signature | signed identity required, missing or wrong | | profile_not_found | no profile for these ids (the SDK recovers itself) | | purchase_conflict | the transaction belongs to another account | | purchase_invalid | Apple would not vouch for the transaction | | restore_incomplete | restored everything the store had, granted nothing |

message is the developer-facing text; displayMessage is safe to show a user and is what the built-in paywall UI renders.

try {
  await Kebabty.purchase(id)
} catch (error) {
  if (error instanceof KebabtyPurchaseConflictError) showRestoreHelp()
  else if (error instanceof KebabtyError) toast(error.displayMessage)
}

Analytics

fire() presents the paywall mapped to an action key and reports impression, CTA, purchase, restore, close and dismiss-survey answers itself. Events are batched, persisted across launches and deduplicated server-side.

Observer mode

Already have in-app-purchase code and keeping it?

await Kebabty.configure({ apiKey: 'pk_kbty_…', purchasesCompletedBy: 'app' })

Kebabty stops buying and stops completing: purchase() refuses, a StoreKit transaction is validated and never finished, a Play purchase is validated and never acknowledged on the device. It still watches the store and still keeps entitlements, audiences, experiments and analytics working. Your code buys, delivers, completes with the store, and then calls syncPurchases().

The built-in paywalls still work — hand them your purchase code:

await Kebabty.fire('LOCKED_POST', { onPurchase: (id) => yourIap.buy(id) })

On Android there is a second switch: set the app's acknowledgement policy to the host app in the dashboard too, or the backend keeps acknowledging Play purchases out from under your code. Set both, or neither — the SDK warns once and the Lab shows a failure while they disagree. iOS needs only the SDK switch. See the Observer mode page in the docs.

Test mode

await Kebabty.configure({ apiKey: 'pk_kbty_…', mode: 'test' })

A LAB badge rides in the corner — drag it to either edge, tap to open — and everything the SDK knows is behind it: the profile and its entitlements, the catalog joined with what StoreKit will actually sell, every request with both bodies, every analytics event and whether it landed, every paywall payload with its audience and experiment arm, and the transactions StoreKit is still holding. It opens on a diagnostics list that names the usual integration failures — a pod that is not in the build, product ids the store does not know, a missing provider, a rejected key — with the fix on the row.

A debug build shows it without being asked. A release build that did not name mode: 'test' renders nothing and records nothing. Nothing else about the SDK changes.

See CHANGELOG.md for what changed in 0.3.0.

kebabty-sdk