Skip to content

feat(mobile): offer Google Play Kilo Pass purchase - #5618

Open
iscekic wants to merge 9 commits into
android-iap-e895-s5from
android-iap-e895-s6
Open

feat(mobile): offer Google Play Kilo Pass purchase#5618
iscekic wants to merge 9 commits into
android-iap-e895-s5from
android-iap-e895-s6

Conversation

@iscekic

@iscekic iscekic commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • An Android user can now buy a Kilo Pass subscription inside the app. Google Play charges the account and the subscription starts right away.
  • An Android user who already has a Google Play Kilo Pass can tap Manage to open Google Play and change or cancel the plan.
  • On Android, if the Google account already owns Kilo Pass on a different Kilo account, the app says so and blocks a second purchase.
  • On Android, the legal note under the prices now explains Google Play billing, and the missing-product and connection messages name Google Play.
  • After a user changes a subscription in Google Play and returns, the Kilo Pass card refreshes to the new state.
  • The Google Play purchase, manage, and legal text now appears in the user's chosen language.

The native IAP owner now mounts on both iOS and Android and stays the Kilo Pass route's single useIAP call 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 through KiloPassNativeIapContextValue. Completion now routes Play purchases to completePlayPurchase and App Store purchases to completeAppStorePurchase.

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 the completePlayPurchase mutation; computes enabledGoogleProductIds and exposes ownedGoogleProductId and ownedGooglePurchaseToken; 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. StoreKiloPassPurchaseRequest gains a Google variant with a SKU list and subscription offer tokens, AppStoreKiloPassPurchaseActionsDeps gains completePlayPurchase and the enabled Google product IDs, and completion calls completePlayPurchase with 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, the completePlayPurchase dependency, the Play error strings, and the Play request path with the offer-token guard; routes completion by purchase.store; extends recovery and restore to the Google ID list.

Store product loading now resolves Play SKUs. StoreKiloPassProduct gains an optional offerToken for the Play base plan, and joinAppStoreKiloPassProducts joins backend products to store products on googleProductId for Play. useStoreKiloPassProducts enables on Android and reports a Play connection-timeout message.

Files
  • apps/mobile/src/lib/kilo-pass/store-products.ts — adds offerToken to StoreKiloPassProduct and 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, and googlePurchaseToken, and the screen renders Play copy for the already-owned, empty-product, and connection states. The root view carries the kilo-pass-native-iap test ID.

Files
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.tsx — reads ownedGoogleProductId and ownedGooglePurchaseToken, 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 the kilo-pass-native-iap test ID.

A Google Play Kilo Pass now manages through Play. The profile card maps a google_play payment 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 by obfuscatedAccountIdAndroid, 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 the google_play Manage action to the lazy Play helper with a tier-to-product-ID lookup, refetches on app foreground, and passes Platform.OS to 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 — adds obfuscatedAccountIdAndroid to the ownership preflight for Play, makes the google_play provider 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.tskiloPassLegalDisclosure takes the platform and returns the Play disclosure on Android.
  • packages/app-shared/src/commerce/purchase-presentation.ts — removes KILO_PASS_UNAVAILABLE_DESCRIPTION and KILO_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.

Case What it proves Platform Result
A1 The native Kilo Pass purchase screen mounts on Android, and the back control returns to the profile. android passed
A2 The Android screen shows the Google Play empty-products tile with Try again, and no App Store copy. android passed
A3 The Android subscribe screen shows Restore Purchases and no web Manage control. android passed
I1 The iOS Kilo Pass sheet mounts on a clean first open with a dismiss control. ios passed after the fix in round 2
I2 The iOS sheet shows the Terms of Use (EULA) and Privacy Policy links and no Google Play copy. ios passed after the fix in round 2
I3 The iOS sheet shows all three priced Subscribe tiles. ios passed after the fix in round 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.
kilo-pass-ios-open.png

Reviewer Notes

  • after merge: The human configures Play Console subscription products, the Publisher service account, and RTDN Pub/Sub after this code lands, including GOOGLE_PLAY_RTDN_PUSH_AUDIENCE and GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL.

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.

  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
  6. android-iap-e895-s6feat(mobile): offer Google Play Kilo Pass purchase #5618 ← this PR (tip)

Comment thread apps/mobile/src/lib/kilo-pass/subscription-card-state.ts
@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 (86 files)
  • apps/mobile/src/i18n/locales/*.json
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)
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.tsx
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.test.ts
  • apps/mobile/src/lib/kilo-pass/subscription-card-state.ts
  • apps/mobile/src/lib/kilo-pass/subscription-card-content-state.test.ts

Previous review (commit ea4d4d5)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/kilo-pass/subscription-card-state.ts 253 Google Play pass on iOS shows Manage and opens App Store management
Files Reviewed (107 files)
  • apps/mobile/src/components/kilo-pass/kilo-pass-native-iap-owner.tsx - 0 issues
  • apps/mobile/src/components/kilo-pass/kilo-pass-play-manage.ts - 0 issues
  • apps/mobile/src/components/kilo-pass/kilo-pass-play-manage.test.ts - 0 issues
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx - 0 issues
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.tsx - 0 issues
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/legal-links.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/legal-links.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/store-products.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/store-products.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/store-products-loader.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/store-products-loader.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/subscription-card-state.ts - 1 issue
  • apps/mobile/src/lib/kilo-pass/subscription-card-state.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/subscription-card-content-state.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/subscription-card-ownership-preflight.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-products.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.test.tsx - 0 issues
  • packages/app-shared/src/commerce/purchase-presentation.ts - 0 issues
  • apps/mobile/src/i18n/locales/*.json - 0 issues

Fix these issues in Kilo Cloud


Reviewed by grok-4.6 · Input: 131.1K · Output: 8K · Cached: 485.8K

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

@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 27, 2026

useEffect(() => {
if (Platform.OS !== 'ios') {
if (!isIapPlatform) {

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: 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.

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