Skip to content

fix(kilo-pass): survive and report a store product that fails to query - #6618

Merged
iscekic merged 2 commits into
mainfrom
kwf/req-iap-ea28
Sep 23, 2026
Merged

iscekic merged 2 commits into
mainfrom
kwf/req-iap-ea28

Conversation

@iscekic

@iscekic iscekic commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Changelog for users

  • The Kilo Pass paywall keeps the tiers the store can resolve when another tier fails to query, instead of failing the whole paywall.
  • Restore purchases completes an owned purchase for a tier the store cannot list, instead of reporting nothing to restore.
  • A failed product query is reported as a handled outcome with the screen's own retry copy, not as an unhandled error.

Changelog for maintainers

  • New KiloPassProductQueryError records the platform, storefront, store code, response code, and failing identifiers.
  • Its stable fingerprint kilo-pass-product-query/<kind>/<platform>/<storefront>/<code> groups one store answer into one issue.
  • Only the store error's code and responseCode scalars are read; its message, any receipt, and any token are never recorded.
  • A combined query that rejects with two or more identifiers is retried one identifier at a time; the tiers that resolve are kept.
  • Identifiers the store never resolves warn under the same fingerprint; only an all-unresolved result throws the typed error.
  • Recovery, restore, and ownership act on the union of backend-advertised and store-resolved identifiers, completing an owned transaction for an unresolvable tier instead of releasing it.
  • reportAppError reads a validated self-description from a typed error; a malformed value keeps the generic app-error fingerprint.
  • Added Linux Android SDK roots (~/Android/sdk, ~/Android/Sdk, /usr/local/lib/android/sdk) so the emulator resource starts on Linux dev hosts, fixing a pre-existing resolution gap.

E2E proof

Device: open the Kilo Pass paywall on a build whose store cannot resolve one tier; Metro logs the decisive line `[kilo-pass] store product query <kind> platform=<ios|android>… — p1-restore.png

[p1] Device: open the Kilo Pass paywall on a build whose store cannot resolve one tier; Metro logs the decisive line `[kilo-pass] store product query platform=<ios|android>… — On android/emulator-5554 with the one-tier store failure injected at the expo-iap boundary by a temporary fixture (e2e-mobile-app/apply-fixture.py, reverted afterwards; git status clean), the scene passed (SCENE p1 OK, p1.longrun.log); Metro logged '[kilo-pass] store product query unresolved-identifiers platform=android storefront=play code=query-product productIds=[kilopass_tier49]' naming the platform and the failing identifier with no receipt or token (p1-metro.log); both resolvable tiers still rendered ('$199 in credits, $99.99/mo' and '$19 in credits, $24.99/mo', p1-tiers.longrun.log)…

[p1] Device: open the Kilo Pass paywall on a build whose store cannot resolve one tier; Metro logs the decisive line `[kilo-pass] store product query <kind> platform=<ios|android>… — e2e-mobile-app/p1-empty-no-purchases.png

[e1] Device: Kilo Pass paywall with a store that cannot resolve one tier (android) — On android/emulator-5554 the paywall logged [kilo-pass] store product query unresolved-identifiers platform=android storefront=play code=query-product productIds=[kilopass_tier49] (e1-metro.log), the two resolvable tiers still rendered ($19 in credits, $199 in credits — e1-paywall-resolved-tiers.txt), and Restore purchases completed the owned tier-49 purchase (scene SCENE e1 OK, e1-scene.log) by routing off (app)/kilo-pass to (app)/(tabs)/(3_profile) — the 'restored' branch, not 'empty' (e1-metro.log). The one-tier store failure was injected at the expo-iap boundary by a temporary…

[e1] Device: Kilo Pass paywall with a store that cannot resolve one tier (android) — prior/e1.png

[p1] Device: open the Kilo Pass paywall on a build whose store cannot resolve one tier; Metro logs the decisive line `[kilo-pass] store product query platform=<ios|android>…

Device: open the Kilo Pass paywall on a build whose store cannot resolve one tier; Metro logs the decisive line `[kilo-pass] store product query <kind> platform=<ios|android>… — p1-tiers.png

[e1] Device: Kilo Pass paywall with a store that cannot resolve one tier (android)

[e1] Device: Kilo Pass paywall with a store that cannot resolve one tier (android) — prior/e1-paywall-resolved-tiers.png

E2E proof — log excerpts

[p1] Device: open the Kilo Pass paywall on a build whose store cannot resolve on -> pass :: On android/emulator-5554 with the one-tier store failure injected at the expo-iap boundary by a temporary fixture (e2e-mobile-app/apply-fixture.py, reverted afterwards; git status clean), the scene passed (SCENE p1 OK, p1.longrun.log); Metro logged '[kilo-pass] store product query unresolved-identifiers platform=android storefront=play code=query-product productIds=[kilopass_tier49]' naming the platform and the failing identifier with no receipt or token (p1-metro.log); both resolvable tiers still rendered ('$199 in credits, $99.99/mo' and '$19 in credits, $24.99/mo', p1-tiers.longrun.log); Restore then dismissed the paywall to Profile ('(app)/kilo-pass' then '(app)/(tabs)/(3_profile)', p1-m
/home/igor_kilocode_ai/.local/share/kwf/sections/req-iap-ea28/e2e-mobile-app/p1-metro.log
# source: dev/logs/mobile.log (Metro console) during the p1 run on emulator-5554 (android), lines after 872
 WARN  [kilo-pass] store product query store-unavailable platform=android storefront=play code=query-product productIds=[kilopass_tier199, kilopass_tier49, kilopass_tier19]
 LOG  [screen-tracking] (app)/(tabs)/(0_home)
 LOG  [screen-tracking] (app)/(tabs)/(3_profile)
 WARN  [kilo-pass] store product query unresolved-identifiers platform=android storefront=play code=query-product productIds=[kilopass_tier49]
 WARN  [kilo-pass] store product query unresolved-identifiers platform=android storefront=play code=query-product productIds=[kilopass_tier49]
 LOG  [screen-tracking] (app)/kilo-pass
 LOG  [screen-tracking] (app)/(tabs)/(3_profile)
/home/igor_kilocode_ai/.local/share/kwf/sections/req-iap-ea28/e2e-mobile-app/p1.longrun.log
android.widget.Button Code Reviewer, Automatic PR reviews tappable [37,1016][1043,1160]
android.widget.TextView Code Reviewer tappable [171,1044][953,1090]
android.widget.TextView Automatic PR reviews tappable [171,1094][953,1131]
android.widget.Button Security Agent, Find and remediate vulnerabilities tappable [37,1187][1043,1330]
android.widget.TextView Security Agent tappable [171,1215][953,1261]
android.widget.TextView Find and remediate vulnerabilities tappable [171,1265][953,1302]
android.widget.TextView REVIEWS tappable [37,1385][1045,1429]
android.widget.Button PR Review, Review pull requests on mobile tappable [37,1457][1043,1600]
android.widget.TextView PR Review tappable [171,1485][953,1531]
android.widget.TextView Review pull requests on mobile tappable [171,1535][953,1572]
android.widget.TextView APP tappable [37,1654][1045,1698]
android.widget.Button Preferences, Appearance, notifications, thinking, and screen behavior tappable [37,1726][1043,1870]
android.widget.TextView Preferences tappable [171,1754][953,1800]
android.widget.TextView Appearance, notifications, thinking, and screen behavior tappable [171,1804][953,1841]
android.widget.Button Tutorial tappable [37,1897][1043,2031]
android.widget.TextView Tutorial tappable [171,1941][953,1987]
android.widget.TextView LINKED ACCOUNTS tappable [37,2086][1045,2130]
android.widget.TextView Email tappable [171,2186][1017,2195]
android.widget.Button Home, tab, 1 of 3 tappable [0,2195][360,2337]
android.widget.TextView HOME tappable [13,2281][347,2320]
android.widget.Button Agents, tab, 2 of 3 tappable [360,2195][720,2337]
android.widget.TextView AGENTS tappable [373,2281][707,2320]
android.widget.Button Profile, tab, 3 of 3 tappable [720,2195][1080,2337]
android.widget.TextView PROFILE tappable [733,2281][1067,2320]
/home/igor_kilocode_ai/.local/share/kwf/sections/req-iap-ea28/e2e-mobile-app/p1-tiers.longrun.log
SCENE p1-tiers OK
android.widget.LinearLayout com.kilocode.kiloapp:id/action_bar_root tappable [0,0][1080,2400]
android.widget.FrameLayout android:id/content tappable [0,0][1080,2400]
android.view.ViewGroup kilo-pass-native-iap tappable [0,0][1080,2400]
android.widget.Button Go back tappable [37,84][138,185]
android.view.View Kilo Pass tappable [138,102][944,167]
android.widget.TextView A monthly subscription that adds credits to your Kilo balance for running AI coding sessions in Kilo App. tappable [46,213][1036,296]
android.widget.Button $199 in credits, $99.99/mo tappable [46,324][1034,528]
android.widget.TextView $199 in credits tappable [95,373][766,429]
android.widget.TextView $99.99/mo tappable [802,373][986,429]
android.widget.TextView $199 paid credits added monthly for Kilo App usage. tappable [95,442][766,479]
android.widget.Button $19 in credits, $24.99/mo tappable [46,555][1034,759]
android.widget.TextView $19 in credits tappable [95,604][766,660]
android.widget.TextView $24.99/mo tappable [802,604][986,660]
android.widget.TextView $19 paid credits added monthly for Kilo App usage. tappable [95,673][766,710]
android.widget.Button Restore Purchases tappable [373,787][707,903]
android.widget.TextView Restore Purchases tappable [400,821][679,867]
android.widget.TextView Kilo Pass is an auto-renewable monthly subscription. Payment is charged to your Google Play account at confirmation of purchase. Subscriptions renew automatically each month at
android.widget.Button Terms of Use (EULA) tappable [840,1086][1018,1123]
android.widget.Button Privacy Policy tappable [422,1123][588,1160]
shot: /home/igor_kilocode_ai/.local/share/kwf/sections/req-iap-ea28/e2e-mobile-app/p1-tiers.png
/home/igor_kilocode_ai/.local/share/kwf/sections/req-iap-ea28/e2e-mobile-app/p1-empty-no-purchases.txt
<redacted>
<redacted>
                                      </android.widget.TextView>
<redacted>
                                    </android.view.ViewGroup>
                                  </android.widget.ScrollView>
                                </android.view.ViewGroup>
                              </android.view.ViewGroup>
                            </android.widget.ScrollView>
                          </android.view.ViewGroup>
                        </android.view.ViewGroup>
                      </android.view.ViewGroup>
<redacted>
                    </android.view.ViewGroup>
                  </android.view.ViewGroup>
                </android.view.ViewGroup>
              </android.widget.FrameLayout>
            </android.widget.FrameLayout>
          </android.widget.FrameLayout>
        </android.widget.LinearLayout>
      </android.widget.FrameLayout>
    </android.widget.LinearLayout>
  </android.widget.FrameLayout>
</hierarchy>
Owner request

Surface: mobile-app

Purchases fail in production. KILO-APP-HR, 68 events, 13 users, live:
[expo-iap]: PurchaseError: Failed to query product.

A user who cannot query a product cannot buy. The store connection is either
misconfigured or the product identifier is wrong.

What to build

  1. Establish which product identifier fails to resolve and on which platform.
  2. Fix the identifier or the store connection so the product queries.
  3. Report a failed product query as a typed, handled outcome with a stable
    fingerprint, not as an unhandled error.

Do not log a receipt, a purchase token, or a credential.

Proof

One must-run scenario that opens the paywall and queries the products. Quote the
decisive log lines from the scenario in the pull request body.

[e1] Device: Kilo Pass paywall with a store that cannot resolve one tier (android) — On android/emulator-5554 the paywall logged [kilo-pass] store product query unresolved-identifiers platform=android storefront=play code=query-product productIds=[kilopass_tier49] (e1-metro.log), the two resolvable tiers still rendered ($19 in credits, $199 in credits — e1-paywall-resolved-tiers.txt), and Restore purchases completed the owned tier-49 purchase (scene SCENE e1 OK, e1-scene.log) by routing off (app)/kilo-pass to (app)/(tabs)/(3_profile) — the 'restored' branch, not 'empty' (e1-metro.log). The one-tier store failure was injected at the expo-iap boundary by a temporary…

[e1] Device: Kilo Pass paywall with a store that cannot resolve one tier (android) — e1-live-store-unavailable.png

Follow-ups (not changed here)

  • not proved live: iOS: not run — the diff forks on no platform, so Android proves both

Comment thread apps/mobile/src/lib/kilo-pass/store-products-loader.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental commit now probes and reports identifiers a store omits from a successful combined query, resolving the previously flagged silent-drop path with no new defects in the changed production code.

Files Reviewed (2 files)
  • apps/mobile/src/lib/kilo-pass/store-products-loader.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/store-products-loader-omitted-identifiers.test.ts - 0 issues
Notes
  • The prior WARNING on the success-path omission is fixed: fetchStoreKiloPassProducts now diffs the store-resolved ids against the requested SKUs and routes the omitted ones through resolveProbedStoreKiloPassProducts, keeping tiers that resolve and reporting the rest under the stable unresolved-identifiers fingerprint.
  • An empty successful combined answer still returns no products, so the caller reports the authored no-matching-products copy; this is unchanged, intentional behavior documented inline and in the review thread.
  • The union of backend-advertised and store-resolved identifiers in getEnabledProductIds remains consistent across recovery, restore, and ownership; no receipt, token, or credential is logged or reported, and no listeners, subscriptions, timers, or unbounded caches were added.
Previous Review Summary (commit 2bb2c11)

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

Previous review (commit 2bb2c11)

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/store-products-loader.ts 85 A successful combined store query that omits an identifier is never probed, logged, or reported, so iOS silent omissions silently drop a tier and the promised typed fingerprint does not fire.
Files Reviewed (11 files)
  • apps/mobile/src/components/kilo-pass/kilo-pass-native-iap-owner.tsx - 0 issues
  • apps/mobile/src/lib/kilo-pass/product-query-failure.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/product-query-failure.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/store-products-loader.ts - 1 issue
  • apps/mobile/src/lib/kilo-pass/store-products-loader.test.ts - 0 issues
  • apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.test.tsx - 0 issues
  • apps/mobile/src/lib/telemetry/app-error-reporting.ts - 0 issues
  • apps/mobile/src/lib/telemetry/app-error-reporting.test.ts - 0 issues
  • apps/mobile/src/lib/telemetry/error-sink.ts - 0 issues
  • dev/local/mobile-android.ts - 0 issues
  • dev/local/mobile-android.test.ts - 0 issues
Notes
  • The union of backend-advertised and store-resolved identifiers in getEnabledProductIds is applied consistently to recovery, restore, and ownership, and the targeted tests cover the previously-lost tier-49 purchase.
  • Telemetry reads only schema-validated code/responseCode scalars; no receipt, purchase token, or credential is logged or reported.
  • No new listeners, subscriptions, timers, or unbounded caches were added, so this change introduces no memory leaks.
  • No .md documentation files were changed.

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch main

@iscekic
iscekic marked this pull request as draft September 23, 2026 05:09
@iscekic
iscekic marked this pull request as ready for review September 23, 2026 06:32
@iscekic

iscekic commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

This description names a scenario the proof did not capture:

  • not proved live: iOS: not run — the diff forks on no platform, so Android proves both

A repeated proof run rebuilds the same evidence, so no proof run is dispatched for a named gap. Merging with this gap open is your decision.

@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 23, 2026
@iscekic iscekic self-assigned this Sep 23, 2026
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