Skip to content

React Native and Expo

@kasskeep/react-native is a thin client. It opens the store sheet, submits the result to Kasskeep and returns the server’s view of the customer. It never grants anything on its own. It wraps expo-iap, which uses StoreKit 2 on iOS and Play Billing on Android.

The package ships as a tarball that you keep in your project, so cloud builds need no registry credentials. Build it from the Kasskeep release with pnpm release:sdk, copy kasskeep-react-native-<version>.tgz into your app’s vendor/ directory and depend on it by path:

{
"dependencies": {
"@kasskeep/react-native": "file:vendor/kasskeep-react-native-0.5.0.tgz",
"expo-iap": "6.1.1",
"expo-constants": "~58.0.13"
}
}

Peer dependencies: expo-iap 6.1.1, expo-constants and react-native. Add the config plugin to app.json:

{ "expo": { "plugins": ["expo-iap"] } }

Purchases need a development build (npx expo prebuild, then npx expo run:ios or run:android, or EAS Build). In Expo Go only the test store works.

import { Kasskeep, ReplacementMode } from "@kasskeep/react-native";
Kasskeep.configure({ apiKey: "pk_...", baseUrl: "https://api.kasskeep.com", appUserId: user.id });
const sub = Kasskeep.addCustomerListener((customer) => setCustomer(customer));
const products = await Kasskeep.getProducts(["pro.monthly", "pro.yearly"], "subs");
const result = await Kasskeep.purchase({ productId: "pro.monthly", type: "subs" });
if (result.status === "completed") show(result.customer);
if (result.status === "pending") showWaitingForApproval();
await Kasskeep.purchase({ productId: "coins.100", type: "inapp", consumable: true });
await Kasskeep.purchase({
productId: "pro.yearly",
type: "subs",
android: { oldPurchaseToken: current.purchaseToken, oldProductId: "pro.monthly", replacementMode: ReplacementMode.CHARGE_PRORATED_PRICE },
});
await Kasskeep.restore();
await Kasskeep.getCustomer();
sub.remove();
Method Description
configure({ apiKey, baseUrl, appUserId, fetch?, autoSync?, testStore?, ads? }) Sets up the client. appUserId must be a UUID. With autoSync (default on) it resubmits unfinished transactions now and whenever the app returns to the foreground
setAppUserId(id) Switches user and clears the cached customer
getProducts(ids, "inapp" | "subs") Localized displayPrice, price, currency and introOfferEligible. Android results include androidOffers
purchase({ productId, type, consumable?, android?, quantity?, testOutcome? }) Resolves { status: "completed" | "cancelled" | "pending", customer? }
restore() Submits everything the store owns. Calls AppStore.sync() first on iOS
getCustomer() GET /v1/customers/:id
addCustomerListener(cb) / removeCustomerListener(cb) cb runs after every server response that returns a customer
syncPendingTransactions() Resubmits unfinished transactions, returns { submitted, failed }
checkout({ channel, productId, successUrl, cancelUrl, quantity? }) Web checkout, returns { url, sessionId }
ads.showRewarded({ adUnit, customData?, testOutcome? }) Rewarded ads, see below

ReplacementMode uses Google’s SubscriptionProductReplacementParams names and values: WITH_TIME_PRORATION=1, CHARGE_PRORATED_PRICE=2, WITHOUT_PRORATION=3, CHARGE_FULL_PRICE=4, DEFERRED=5, KEEP_EXISTING=6. A replacement needs both oldPurchaseToken and oldProductId. On iOS the system handles changes within a subscription group and android options are ignored.

Failures throw KasskeepError with a code: not_configured, invalid_app_user_id, native_unavailable, network, server, rejected, store, missing_token or invalid_request. retryable is true for network and server; in both cases the transaction stays unfinished and the next sync submits it again.

  1. The SDK starts the purchase with appAccountToken (iOS) or obfuscatedAccountId (Android) set to appUserId.
  2. It submits the signed transaction or purchase token to POST /v1/receipts.
  3. Only after a 2xx response does it finish the iOS transaction. On Android the server acknowledges or consumes; the client never does.
  4. If the request fails, the transaction stays unfinished and is submitted again by syncPendingTransactions() or the store’s update stream.

Ask to Buy and Android pending purchases resolve pending; once approved they arrive in the background and are submitted then.

const { url } = await Kasskeep.checkout({
channel: "stripe",
productId: "pro_monthly",
successUrl: "https://play.example.com/done",
cancelUrl: "https://play.example.com/shop",
});

The SDK only returns the hosted URL; open it yourself. Do not use it to sell digital goods inside an iOS or Android app. See Stripe.

Kasskeep.configure({ apiKey, baseUrl, appUserId, testStore: __DEV__ });
await Kasskeep.purchase({ productId: "demo.gems_100", type: "inapp", testOutcome: "pending" });

testStore: true or { prices?, outcome? } sends getProducts and purchase to the test store and never loads expo-iap, so it works in Expo Go. testOutcome simulates success, cancelled, pending or failed (which throws store). The app must have the test store enabled on the server.

The SDK does not bundle an ad SDK. You pass a rewardedProvider that wraps your AppLovin MAX or Unity LevelPlay integration, and Kasskeep tells it which user and parameters to attach to the server-side callback.

Kasskeep.configure({
apiKey, baseUrl, appUserId,
ads: {
rewardedProvider: maxRewarded,
adUnits: { revive: "a1b2c3d4e5f60718" },
testAds: __DEV__,
},
});
const r = await Kasskeep.ads.showRewarded({ adUnit: "revive", customData: "stage=3" });
  • RewardedProvider is { network, init?({ appUserId, appKey }), setUserId(appUserId), showRewarded({ adUnit, networkAdUnitId, appUserId, customData, serverParams }) } and resolves { status }.
  • adUnits maps your ad unit names to network ad unit ids. ads.appKey is required for network: "levelplay".
  • serverParams is { ad_unit, ad_unit_id, data? }. LevelPlay sends them back as custom parameters so the server can map the reward.
  • completed only means the ad played to the end on the device. Grant the reward from the ad.reward webhook, not from this result.
  • testAds: true or { outcome, network } calls the test endpoint instead of any ad SDK and returns rewardEventId. The app needs test ads enabled on the server.

The rewarded ads overview and the LevelPlay page show complete providers.