Skip to content

feat(kilo-pass): add completePlayPurchase mutation - #5608

Open
iscekic wants to merge 6 commits into
android-iap-e895-s4from
android-iap-e895-s5
Open

feat(kilo-pass): add completePlayPurchase mutation#5608
iscekic wants to merge 6 commits into
android-iap-e895-s4from
android-iap-e895-s5

Conversation

@iscekic

@iscekic iscekic commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • An Android user can now finish a Google Play Kilo Pass purchase and activate the subscription in the app, the same flow that iOS users use for the App Store.
  • The purchase check on the Google Play store now recognizes an existing Google Play subscription as belonging to that store, so a Play-owned Kilo Pass no longer blocks the purchase as a subscription from another provider.
  • A Google Play purchase that belongs to a different Kilo account is refused, with a prompt to sign in to the Google account that bought it.

completePlayPurchase is a new mutation that finishes a Google Play Kilo Pass purchase. It verifies the Play purchase token, rejects an account-token mismatch, and completes the store purchase through the shared path. It records the completion on the google_play tracking channel, and the new assertGooglePlayAccountTokenMatchesUser and mapPlayCompletionError helpers give Play the account guard and error mapping the App Store already has.

The preflightPurchase mutation now handles the Play storefront. It resolves the product by googleProductId and returns unknown_product when that ID is missing or unknown, and it applies the single-account ownership guard through the untrusted googlePurchaseToken with an owned_by_another_account result. On Play, the already_subscribed check excludes a live GooglePlay subscription but still refuses a live Stripe or App Store subscription.

The presentation and preflight input schemas add the optional supportsNativePlayKiloPass flag; preflight also adds googleProductId and googlePurchaseToken. Clients that omit the new fields keep today's presentation and checks. completeAppStorePurchase now rejects anything other than the exact iOS, App Store, Kilo Pass combination because the shared native-IAP gate now also admits Play.

Files
  • apps/web/src/routers/kilo-pass-router.ts — imports the Google Play product lookup and verifier; adds supportsNativePlayKiloPass, googleProductId, and googlePurchaseToken to the input schemas and threads supportsNativePlayKiloPass through the presentation and preflight calls; adds the Play account-token assertion and the Play completion error mapping; resolves Play products by googleProductId, guards Play purchase tokens by owner, and excludes GooglePlay from the Play already-subscribed check; narrows the App Store completion guard to the exact iOS/App Store/Kilo Pass combination; adds the completePlayPurchase mutation that verifies the Play token, matches the account token, completes the store purchase, and tracks on google_play.

Tests: 1 test file updated — apps/web/src/routers/kilo-pass-router.test.ts, 389 insertions.
Generated: none.


Verification

No E2E report is attached for this level, and no manual test paths were run. Runtime verification runs once on the tip PR (level 6) after all levels land, and this level is reviewed on its own diff.

Visual Changes

Visual Changes: N/A

Reviewer Notes

Human steps: none.

Notes: E2E: bot-e2e — runtime verification runs once on the tip PR (level 6) after all levels land. This level is reviewed on its own diff.

Stacked PRs — merge bottom to top. Each level shows only its own diff.

Runtime verification (E2E, user advocacy, simplify) runs on the tip PR over every level.
Every level keeps its own checks, its own bot review, and its own threads; each one is answered on its own PR.
Each level is its own deliverable: it builds and passes its own checks alone.
A finding on a level is repaired on that level, then carried upward with stack.sh forward.

  1. android-iap-e895feat(commerce): add Play Kilo Pass presentation flag #5581
  2. android-iap-e895-s2feat(kilo-pass): add Google Play publisher SDK #5582
  3. android-iap-e895-s3feat(kilo-pass): add Google Play purchase verifier #5586
  4. android-iap-e895-s4feat(kilo-pass): handle Play real-time notifications #5598
  5. android-iap-e895-s5feat(kilo-pass): add completePlayPurchase mutation #5608 ← this PR
  6. android-iap-e895-s6feat(mobile): offer Google Play Kilo Pass purchase #5618 (tip)

@kilo-code-bot

kilo-code-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • apps/web/src/routers/kilo-pass-router.ts
Previous Review Summary (commit ad753a5)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit ad753a5)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • apps/web/src/routers/kilo-pass-router.ts
  • apps/web/src/routers/kilo-pass-router.test.ts

Reviewed by grok-4.6 · Input: 161.8K · Output: 10.5K · Cached: 356.9K

Review guidance: REVIEW.md from base branch android-iap-e895-s4

@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 27, 2026
});
}
try {
const purchase = await verifyGooglePlayKiloPassPurchase(input.purchaseToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bot: completePlayPurchase accepts provider tokens without validating Google Play's subscription lifecycle state, so a revoked/non-entitled purchase with a future expiry can be activated.

Suggested fix: Make verifyGooglePlayKiloPassPurchase (or an immediately adjacent completion guard) allowlist only Google Play states that grant entitlement before calling completeStoreKiloPassPurchase. Do not rely on a future expiryTimeMs alone: subscriptionState is decoded but currently unused, while the shared completion service writes status: 'active' on both insert and conflict update. Add verifier-level tests covering rejected revoked/on-hold/paused/pending states plus the valid active and canceling-but-unexpired cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants