feat(mobile): offer Google Play Kilo Pass purchase - #5618
Open
iscekic wants to merge 14 commits into
Open
Conversation
This was referenced Aug 27, 2026
Contributor
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 |
pandemicsyn
approved these changes
Aug 27, 2026
Show the app's own localized message when the store SDK fails to load
products. The SDK message ("Failed to query product") reached the user
and overrode the localized copy.
Make the products retry a button. The whole error card was the press
target, and it carried a second, different retry label.
Drop the KiloClaw mention from the subscribe sheet description in all 87
catalogs. The trailing product list goes with it.
The expo-iap 5.x Purchase type no longer carries `platform`; the Play
purchase test fixtures set it. Remove the dead field.
Android applied the parent line height to each nested link Text, so the legal disclosure grew to many times its height. Drop the leading class.
The effect marked ownership as checked in a finally block, so a rejected lookup enabled the tiles with an empty owned-purchase list. A pass owned by another Kilo account was then invisible to both the client check and the server preflight, which receives no purchase token. Keep the tiles disabled, show the store connection error and offer a retry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)