feat(mobile): offer Google Play Kilo Pass purchase - #5618
Conversation
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (86 files)
Previous Review Summaries (2 snapshots, latest commit f3cc022)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit f3cc022)Status: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Previous review (commit ea4d4d5)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (107 files)
Reviewed by grok-4.6 · Input: 131.1K · Output: 8K · Cached: 485.8K Review guidance: REVIEW.md from base branch |
|
|
||
| useEffect(() => { | ||
| if (Platform.OS !== 'ios') { | ||
| if (!isIapPlatform) { |
There was a problem hiding this comment.
bot: A failed Play ownership lookup still enables purchasing
Suggested fix: Track ownership lookup failure separately from successful completion. For Android (and ideally both stores), only set ownershipChecked after refreshAvailablePurchases() resolves; on rejection, keep purchase tiles disabled and expose a retry path that reruns the lookup. Add a test where getAvailablePurchases rejects and assert neither preflight nor requestPurchase can run. This matters when a device's Play account owns a Kilo Pass linked to another Kilo account: without the fetched owned SKU/token, the client cannot block the purchase and the server preflight receives no googlePurchaseToken to apply its ownership lookup.
Summary
The native IAP owner now mounts on both iOS and Android and stays the Kilo Pass route's single
useIAPcall site. It resolves the Play base-plan offer token, derives the enabled Google product IDs, and exposes the owned Google product ID and purchase token throughKiloPassNativeIapContextValue. Completion now routes Play purchases tocompletePlayPurchaseand App Store purchases tocompleteAppStorePurchase.Files
apps/mobile/src/components/kilo-pass/kilo-pass-native-iap-owner.tsx— resolves the subscription offer token and stores it on each store product; wires thecompletePlayPurchasemutation; computesenabledGoogleProductIdsand exposesownedGoogleProductIdandownedGooglePurchaseToken; injects the storefront, Play completion, and Google ID loaders into the purchase actions; guards the request, pending, and recovery effects for both stores.The store purchase actions now drive the Play storefront.
StoreKiloPassPurchaseRequestgains a Google variant with a SKU list and subscription offer tokens,AppStoreKiloPassPurchaseActionsDepsgainscompletePlayPurchaseand the enabled Google product IDs, and completion callscompletePlayPurchasewith the purchase token. The error mapper adds the Play account-token and not-linked strings, and restore and recovery filter against enabled Google product IDs.Files
apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts— adds the Google request branch, thecompletePlayPurchasedependency, the Play error strings, and the Play request path with the offer-token guard; routes completion bypurchase.store; extends recovery and restore to the Google ID list.Store product loading now resolves Play SKUs.
StoreKiloPassProductgains an optionalofferTokenfor the Play base plan, andjoinAppStoreKiloPassProductsjoins backend products to store products ongoogleProductIdfor Play.useStoreKiloPassProductsenables on Android and reports a Play connection-timeout message.Files
apps/mobile/src/lib/kilo-pass/store-products.ts— addsofferTokentoStoreKiloPassProductand picks the join key by storefront.apps/mobile/src/lib/kilo-pass/store-products-loader.ts— selects the SKU key and the no-match message by storefront and forwards the storefront to the join.apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-products.ts— enables the query on Android, passes the storefront to the loader, and switches the connection-timeout copy by platform.The subscription screen now mounts native IAP on Android and sends the Play preflight payload. The preflight call carries the Android platform, the Play storefront,
googleProductId, andgooglePurchaseToken, and the screen renders Play copy for the already-owned, empty-product, and connection states. The root view carries thekilo-pass-native-iaptest ID.Files
apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.tsx— readsownedGoogleProductIdandownedGooglePurchaseToken, builds the Play preflight payload, routes tier-change taps on Android to Play management, selects Play copy for the already-owned and empty/error states, and adds thekilo-pass-native-iaptest ID.A Google Play Kilo Pass now manages through Play. The profile card maps a
google_playpayment provider to a Manage action that opens Play subscription management, and a new lazy helper deep-links to Play with the package name and a tier-to-product-ID map. The ownership preflight detects Play purchases byobfuscatedAccountIdAndroid, and the card refetches its presentation and state when the app returns to the foreground.Files
apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx— maps thegoogle_playManage action to the lazy Play helper with a tier-to-product-ID lookup, refetches on app foreground, and passesPlatform.OSto the accessibility helper.apps/mobile/src/components/kilo-pass/kilo-pass-play-manage.ts— new helper; deep-links to Play subscriptions with the package name and reports a Play-specific failure toast.apps/mobile/src/lib/kilo-pass/subscription-card-state.ts— addsobfuscatedAccountIdAndroidto the ownership preflight for Play, makes thegoogle_playprovider manageable, keeps an App Store pass on a Play device inert, and adds the Play management accessibility hint.The legal disclosure now selects Play copy on Android, and the two shared English constants for the unavailable and web-management descriptions are removed. Each app renders those descriptions from its own catalog, so the shared package no longer carries that English source of truth.
Files
apps/mobile/src/lib/kilo-pass/legal-links.ts—kiloPassLegalDisclosuretakes the platform and returns the Play disclosure on Android.packages/app-shared/src/commerce/purchase-presentation.ts— removesKILO_PASS_UNAVAILABLE_DESCRIPTIONandKILO_PASS_WEB_MANAGEMENT_DESCRIPTION.The locale catalogs now carry the Play copy. The English catalog gains the 13 Play product, connection, purchase, management, and legal strings and drops the two dead unavailable and web-management keys, and every non-English catalog gains the same 13 strings translated.
Files
apps/mobile/src/i18n/locales/en.json— adds the 13 Play product, connection, purchase, management, and legal strings; removes the two dead keys.apps/mobile/src/i18n/locales/af.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/am.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ar.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/az.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/be.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/bg.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/bn.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/bs.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ca.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ckb.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/cs.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/cy.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/da.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/de.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/el.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/es.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/et.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/eu.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/fa.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/fi.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/fil.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/fr.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ga.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/gl.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/gu.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ha.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/he.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/hi.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/hr.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ht.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/hu.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/hy.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/id.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ig.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/is.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/it.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ja.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ka.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/kk.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/km.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/kn.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ko.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/lo.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/lt.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/lv.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/mg.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/mi.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/mk.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ml.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/mn.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/mr.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ms.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/mt.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/my.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/nb.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ne.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/nl.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/om.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/or.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/pa.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/pl.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ps.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/pt-BR.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/pt.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ro.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ru.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/si.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/sk.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/sl.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/so.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/sq.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/sr.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/sv.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/sw.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ta.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/te.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/th.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/tr.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/uk.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/ur.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/uz.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/vi.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/yo.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/zh-Hans.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/zh-Hant.json— adds the 13 translated Play strings; removes the two dead keys.apps/mobile/src/i18n/locales/zu.json— adds the 13 translated Play strings; removes the two dead keys.Tests: 9 test files changed — 1 added, 8 updated.
Generated: none.
Verification
The Android emulator and the iOS simulator ran six cases across rounds 1 and 2.
Round 1 left I2 and I3 unrun after the I1 failure; the corrected single-open scenarios in round 2 ran and passed all three iOS cases.
The iOS kilo-pass sheet lost the Close control on a second deep-link open within one app session; round 1 reproduced it twice. It still reproduces on the deep-link test path only; it does not reproduce on the real in-app navigation path, where round 2 passed.
Visual Changes
Android Kilo Pass native in-app purchase screen. No picture shows this screen. All three round-1 candidates are black frames; the largest pixel value in each is 0. The round report names a visible Kilo Pass title, a Google Play empty-products tile, and Restore Purchases, but the pictures contain none of it. The Android screen therefore has no picture in this report.
iOS Kilo Pass native in-app purchase sheet. The user now sees a Kilo Pass sheet with three priced Subscribe tiles, a Restore Purchases action, and Apple subscription legal text with Terms of Use (EULA) and Privacy Policy links. The change reads in the sheet body: the three tiles sit under the Kilo Pass header, the dismiss chevron sits at the header left, and the legal links sit at the sheet bottom.

Reviewer Notes
Notes: E2E: bot-e2e — runtime verification runs on this tip PR. A bot cannot complete a Play Billing purchase; a real Play sandbox purchase is human-assisted evidence after store config.
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.
android-iap-e895— feat(commerce): add Play Kilo Pass presentation flag #5581android-iap-e895-s2— feat(kilo-pass): add Google Play publisher SDK #5582android-iap-e895-s3— feat(kilo-pass): add Google Play purchase verifier #5586android-iap-e895-s4— feat(kilo-pass): handle Play real-time notifications #5598android-iap-e895-s5— feat(kilo-pass): add completePlayPurchase mutation #5608android-iap-e895-s6— feat(mobile): offer Google Play Kilo Pass purchase #5618 ← this PR (tip)