React Native 与 Expo
@kasskeep/react-native 是一个轻量客户端。它拉起商店购买面板,把结果提交给 Kasskeep,再返回服务端视角下的客户状态。它自己从不发放任何东西。它封装了 expo-iap,在 iOS 上用 StoreKit 2,在 Android 上用 Play Billing。
这个包以 tarball 形式提供,放在你自己的项目里,所以云端构建不需要 registry 凭据。在 Kasskeep 发布版本里用 pnpm release:sdk 构建,把 kasskeep-react-native-<version>.tgz 复制到 App 的 vendor/ 目录,按路径依赖它:
{ "dependencies": { "@kasskeep/react-native": "file:vendor/kasskeep-react-native-0.5.0.tgz", "expo-iap": "6.1.1", "expo-constants": "~58.0.13" }}对等依赖:expo-iap 6.1.1、expo-constants 和 react-native。在 app.json 里加上配置插件:
{ "expo": { "plugins": ["expo-iap"] } }购买需要 development build(先 npx expo prebuild,再 npx expo run:ios 或 run:android,或者用 EAS Build)。在 Expo Go 里只有测试商店能用。
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();| 方法 | 说明 |
|---|---|
configure({ apiKey, baseUrl, appUserId, fetch?, autoSync?, testStore?, ads? }) |
初始化客户端。appUserId 必须是 UUID。开启 autoSync(默认开启)时,会立即重新提交未结束的交易,之后每次 App 回到前台也会提交 |
setAppUserId(id) |
切换用户,并清空缓存的客户状态 |
getProducts(ids, "inapp" | "subs") |
返回本地化的 displayPrice、price、currency 和 introOfferEligible。Android 结果还包含 androidOffers |
purchase({ productId, type, consumable?, android?, quantity?, testOutcome? }) |
返回 { status: "completed" | "cancelled" | "pending", customer? } |
restore() |
提交商店里该用户拥有的全部购买。在 iOS 上会先调用 AppStore.sync() |
getCustomer() |
GET /v1/customers/:id |
addCustomerListener(cb) / removeCustomerListener(cb) |
每次服务端响应返回客户状态后都会调用 cb |
syncPendingTransactions() |
重新提交未结束的交易,返回 { submitted, failed } |
checkout({ channel, productId, successUrl, cancelUrl, quantity? }) |
网页结账,返回 { url, sessionId } |
ads.showRewarded({ adUnit, customData?, testOutcome? }) |
激励广告,见下文 |
ReplacementMode 沿用 Google SubscriptionProductReplacementParams 的名称和取值:WITH_TIME_PRORATION=1、CHARGE_PRORATED_PRICE=2、WITHOUT_PRORATION=3、CHARGE_FULL_PRICE=4、DEFERRED=5、KEEP_EXISTING=6。替换订阅需要同时提供 oldPurchaseToken 和 oldProductId。在 iOS 上,同一订阅组内的变更由系统处理,android 选项会被忽略。
失败时抛出带 code 的 KasskeepError:not_configured、invalid_app_user_id、native_unavailable、network、server、rejected、store、missing_token 或 invalid_request。network 和 server 的 retryable 为 true;这两种情况下交易都保持未结束,下次同步时会重新提交。
- SDK 发起购买时,把
appAccountToken(iOS)或obfuscatedAccountId(Android)设为appUserId。 - 把签名交易或购买 token 提交到
POST /v1/receipts。 - 收到 2xx 响应后才结束 iOS 交易。在 Android 上由服务端确认或消耗,客户端从不做这一步。
- 如果请求失败,交易保持未结束,之后由
syncPendingTransactions()或商店的更新流重新提交。
Ask to Buy 和 Android 的待处理购买会返回 pending;批准后它们会在后台到达,届时再提交。
const { url } = await Kasskeep.checkout({ channel: "stripe", productId: "pro_monthly", successUrl: "https://play.example.com/done", cancelUrl: "https://play.example.com/shop",});SDK 只返回托管页面的 URL,需要你自己打开。不要用它在 iOS 或 Android App 内售卖数字商品。见 Stripe。
测试商店模式
Section titled “测试商店模式”Kasskeep.configure({ apiKey, baseUrl, appUserId, testStore: __DEV__ });await Kasskeep.purchase({ productId: "demo.gems_100", type: "inapp", testOutcome: "pending" });testStore: true 或 { prices?, outcome? } 会把 getProducts 和 purchase 发到测试商店,并且完全不加载 expo-iap,所以在 Expo Go 里也能用。testOutcome 可模拟 success、cancelled、pending 或 failed(会抛出 store)。服务端必须为该 App 开启测试商店。
SDK 不内置任何广告 SDK。你传入一个 rewardedProvider,由它封装你的 AppLovin MAX 或 Unity LevelPlay 集成;Kasskeep 告诉它要给服务端回调附上哪个用户和哪些参数。
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的形状是{ network, init?({ appUserId, appKey }), setUserId(appUserId), showRewarded({ adUnit, networkAdUnitId, appUserId, customData, serverParams }) },返回{ status }。adUnits把你的统一广告位名映射到广告网络的广告位 id。network: "levelplay"时必须提供ads.appKey。serverParams是{ ad_unit, ad_unit_id, data? }。LevelPlay 会把它们作为自定义参数回传,服务端据此对应奖励。completed只表示广告在设备上播完了。请根据ad.reward回调发放奖励,而不是根据这个结果。testAds: true或{ outcome, network }会调用测试接口而不是任何广告 SDK,并返回rewardEventId。服务端需要为该 App 开启测试广告。