Buy on StoreKit and Play Billing, then confirm with the backend (#78) (#122)
Deploy Beta / unit-tests (push) Successful in 13s
Unit Tests / test (push) Successful in 12s
Deploy Beta / deploy-beta (push) Successful in 3m9s

## Summary
- The Capacitor shell buys through StoreKit (iOS 15) and Play Billing instead of RevenueCat.
- After the store sheet succeeds, the app POSTs the signed transaction or Play purchase token to `/monetization/store/confirm/`.
- Restore reads current store entitlements and confirms each one. Web checkout stays on Stripe.
- The store account token from `GET /monetization/subscription/` is sent as `appAccountToken` / `obfuscatedAccountId`.

Depends on the `chat_backend` PR for issue #78 (`feat/78-native-store-purchases`).

## Test plan
- [x] `storeBilling`, `revenueCat`, `finance`, and `BillingSection` unit tests
- [ ] iOS sandbox purchase and restore against a backend with `APPLE_IAP_BUNDLE_ID` set
- [ ] Play license-test purchase and restore against a backend with `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` set
- [ ] Web billing still opens Stripe Checkout

Reviewed-on: #122
This commit was merged in pull request #122.
This commit is contained in:
westfarn committed 2026-10-19 04:38:43 -07:00
1 parent 4a5b05f421
commit 8b58192026
25 files changed
+702 -226

No files matched your search

+8 -25
View File
@@ -1,40 +1,23 @@
# Monetization (web Stripe + native RevenueCat)
# Monetization (web Stripe + native store billing)
## Channels
| Runtime | Checkout / manage | Ledger |
|---------|-------------------|--------|
| Web (`!isNativePlatform`) | Stripe Checkout + Customer Portal via `/monetization/...` | Stripe webhooks |
| Native Capacitor | RevenueCat IAP (`@revenuecat/purchases-capacitor`) | RevenueCat webhooks → same invoice/payment tables |
| Native Capacitor | StoreKit (iOS) and Play Billing (Android) | `POST /monetization/store/confirm/` verifies the store transaction and writes the same invoice/payment tables |
FE displays whatever `/monetization/invoices/` (and subscription) returns after refresh. Provider badge uses `provider` + `revenuecat_store`.
There is no App Store, Play, or RevenueCat webhook. After the store sheet succeeds, the app sends the signed StoreKit transaction or the Play purchase token. The backend checks it before granting access. Renewals and restores send the current store entitlement again. Access ends when the verified period end passes.
## App user id
FE displays whatever `/monetization/invoices/` (and subscription) returns after refresh. Provider badge uses `provider` (`app_store`, `play_store`, or legacy `revenuecat`) plus `revenuecat_store`.
RevenueCat `appUserID` = Django user pk as string.
## Store account token
1. Prefer `id` from `/user/get/` (serializer returns all model fields).
2. Else JWT `user_id` claim (`SIMPLE_JWT.USER_ID_CLAIM`).
`GET /monetization/subscription/` returns `store_account_token`. The native purchase passes that value as StoreKit `appAccountToken` and Play Billing `obfuscatedAccountId`. The backend rejects a token that belongs to a different user, and rejects a transaction already linked to another account.
Backend webhook resolver accepts numeric pk (or email fallback).
## Mobile env keys
In `.env.mobile`:
- `REACT_APP_REVENUECAT_APPLE_API_KEY`
- `REACT_APP_REVENUECAT_GOOGLE_API_KEY`
- `REACT_APP_REVENUECAT_OFFERING_ID` (optional)
Package identifiers in the RC dashboard should include plan slugs (e.g. `founders`) so `purchasePlan(planSlug)` can match packages/products. Optional backend `REVENUECAT_PRODUCT_PLAN_MAP` maps product id → plan slug.
## Auth hooks
- `Purchases.configure` once on native (first purchase / logIn).
- `logIn(appUserID)` after SignIn / SignUp / AuthCallback / AccountContext load.
- `logOut` on Header2 sign-out and Delete account.
Plan rows expose `store_product_id` (the store product identifier). Configure it on `SubscriptionPlan.revenuecat_product_id`, or set `REVENUECAT_PRODUCT_PLAN_MAP` so a product id maps to a plan slug.
## Related
- Issue #100 (FE) + companion backend RevenueCat webhook PR
- Backend issue #78
- [`ANDROID.md`](ANDROID.md) / [`IOS.md`](IOS.md) for store builds