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.
Install
Section titled “Install”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.
Errors
Section titled “Errors”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.
How a purchase flows
Section titled “How a purchase flows”- The SDK starts the purchase with
appAccountToken(iOS) orobfuscatedAccountId(Android) set toappUserId. - It submits the signed transaction or purchase token to
POST /v1/receipts. - Only after a 2xx response does it finish the iOS transaction. On Android the server acknowledges or consumes; the client never does.
- 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.
Web checkout
Section titled “Web checkout”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.
Test store mode
Section titled “Test store mode”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.
Rewarded ads
Section titled “Rewarded ads”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" });RewardedProvideris{ network, init?({ appUserId, appKey }), setUserId(appUserId), showRewarded({ adUnit, networkAdUnitId, appUserId, customData, serverParams }) }and resolves{ status }.adUnitsmaps your ad unit names to network ad unit ids.ads.appKeyis required fornetwork: "levelplay".serverParamsis{ ad_unit, ad_unit_id, data? }. LevelPlay sends them back as custom parameters so the server can map the reward.completedonly means the ad played to the end on the device. Grant the reward from thead.rewardwebhook, not from this result.testAds: trueor{ outcome, network }calls the test endpoint instead of any ad SDK and returnsrewardEventId. The app needs test ads enabled on the server.
The rewarded ads overview and the LevelPlay page show complete providers.