Grant mobile plans from verified App Store and Play purchases (#78) (#79)
Deploy Beta / unit-tests (push) Successful in 10s
Unit Tests / test (push) Successful in 9s
Deploy Beta / docker (push) Successful in 23s
Deploy Beta / deploy-beta (push) Successful in 1m19s

## Summary
- Mobile access is granted by `POST /api/finance/store/confirm/` after the app finishes a StoreKit or Play Billing purchase.
- App Store signed transactions are checked against Apple Root CA G3. Play purchase tokens are checked with the Play Developer API.
- The RevenueCat webhook no longer grants a plan. Stripe Checkout and the Stripe webhook stay for the website.
- A purchase account token must match the signed-in user, and a transaction already linked to another account is rejected. Access ends when the verified period end passes unless the app confirms again.

Client: ai_ml_operations/chat_web_app#122

## Config
- `APPLE_IAP_BUNDLE_ID` (default `ai.hesychia.chat`)
- `GOOGLE_PLAY_PACKAGE_NAME`
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` (JSON or a file path)
- Plan `revenuecat_product_id` or `REVENUECAT_PRODUCT_PLAN_MAP` maps the store product to a plan

## Test plan
- [x] `python manage.py test monetization.tests.test_store_purchases monetization.tests.test_revenuecat monetization.tests.test_plans_quotas`
- [ ] Confirm an App Store sandbox transaction and a Play license-test purchase against a configured backend
- [ ] Confirm a token already linked to another user is rejected
- [ ] Confirm `GET /subscription/` returns `store_account_token` and flips a lapsed store sub to canceled

Reviewed-on: #79
This commit was merged in pull request #79.
This commit is contained in:
westfarn committed 2026-10-19 04:37:49 -07:00
1 parent bb5c43a4e7
commit 4fc2b87bb4
14 files changed
+1141 -119

No files matched your search

+5 -2
View File
@@ -247,6 +247,8 @@ def needs_checkout(user) -> bool:
UserSubscription.Source.ADMIN,
UserSubscription.Source.STRIPE,
UserSubscription.Source.REVENUECAT,
UserSubscription.Source.APP_STORE,
UserSubscription.Source.PLAY_STORE,
):
return False
return True
@@ -457,8 +459,9 @@ def assign_plan_from_revenuecat(
cancel_at_period_end: bool | None = None,
current_period_end=None,
keep_existing_plan_if_unknown: bool = False,
source: str | None = None,
) -> UserSubscription:
"""Assign a catalog plan from a RevenueCat store purchase event."""
"""Assign a catalog plan from a verified store purchase."""
seed_subscription_plans(update_existing=False)
existing = (
UserSubscription.objects.filter(user=user).select_related("plan").first()
@@ -493,7 +496,7 @@ def assign_plan_from_revenuecat(
sub = assign_plan(
user,
plan=plan,
source=UserSubscription.Source.REVENUECAT,
source=source or UserSubscription.Source.REVENUECAT,
status=status,
revenuecat_original_transaction_id=revenuecat_original_transaction_id or "",
log_auth_event=False,