跳转到内容

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;这两种情况下交易都保持未结束,下次同步时会重新提交。

  1. SDK 发起购买时,把 appAccountToken(iOS)或 obfuscatedAccountId(Android)设为 appUserId。
  2. 把签名交易或购买 token 提交到 POST /v1/receipts。
  3. 收到 2xx 响应后才结束 iOS 交易。在 Android 上由服务端确认或消耗,客户端从不做这一步。
  4. 如果请求失败,交易保持未结束,之后由 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。

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 开启测试广告。

广告奖励概览和 LevelPlay 页面给出了完整的 provider 示例。