Keep the SDK working on devices without Play Billing - #472
Conversation
GoogleBillingWrapper now remembers when billing is unavailable and fails requests straight away instead of reconnecting for each one. A billing client that can't be created marks billing unavailable rather than leaving queued requests hanging, and availability is probed again whenever the app returns to the foreground. BillingNotAvailable is no longer cached per product for the life of the process. StoreManager applies a single rule when billing is unavailable: a paywall presents with whatever resolved without Play (test, custom and substitute products) and only fails when nothing resolved and test mode is off. AutomaticPurchaseController no longer throws from its constructor when the billing client can't be created, and purchase() fails instead of waiting forever when the connection can't be established. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues — a few rough edges inline.
Reviewed changes
Reviewed the whole PR: the new billing-availability state in GoogleBillingWrapper, the null-safe AutomaticPurchaseController, the partial-presentation rule in StoreManager.getProducts, and the new tests. I checked that each new test would fail against the old behaviour, and that a mixed paywall's unresolved Play product fails at purchase time with PurchaseResult.Failed("Product not found") instead of hanging.
BillingAvailabilitystate: once setup reportsBILLING_UNAVAILABLE/FEATURE_NOT_SUPPORTED, orcreateBillingClientthrows, requests fail straight away throughexecuteRequestOnUIThread. Billing is probed again the next time the app comes to the foreground.- Per-product failure cache removed:
BillingNotAvailableis no longer written to the staticproductsCache, so billing that becomes available later isn't hidden by an old failure. AutomaticPurchaseControllerhardening: creating the client can no longer failconfigure(), andpurchase()retries a failed connection once with a 10s limit, then returnsFailed.StoreManagerrule:fetchOrAwaitProductsreturnsbillingUnavailableinstead of throwing it.getProductsnow fails only when nothing resolved and test mode is off.
ℹ️ Nitpicks
- The instrumented
GoogleBillingWrapperTeststill describes the removed cache (test_billing_not_available_is_cached,test_multiple_products_cached_on_billing_not_available, "Should be BillingNotAvailable from cache", and the "Products cache" region header). These tests still pass, but only through the new fast-fail path inexecuteRequestOnUIThread, so the names now describe behaviour that no longer exists.
Claude Opus | 𝕏
The test mode modal and the purchase and restore drawers put a rounded background on their content, but BottomSheetDialog wraps it in a sheet container with its own opaque background, whose square corners showed behind the rounded ones. Make that container transparent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a "Configure with test mode" option to the purchase controller test screen and a Maestro flow that configures in test mode, presents a Play paywall from the test catalog and completes a simulated purchase. The flow needs the Play Store disabled on the device, so it isn't part of the default flows in config.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No blockers. One minor suggestion inline about the sheet corner fix.
Reviewed changes
This review covers what changed since the last Pullfrog review (b75ed4d): the fix for the test mode sheet corners and the new test_app flow for a device without Play Billing. The billing, StoreManager, and AutomaticPurchaseController code is unchanged, so the three earlier threads are still open.
- Transparent sheet container: added
BottomSheetDialog.setSheetContent, which clears the background ofdesign_bottom_sheet.TestModeModal,TestModePurchaseDrawerandTestModeRestoreDrawernow use it. - test_app "Configure with test mode": added an option to
configureWithoutPCthat setsTestModeBehavior.ALWAYS, plus a button that calls it. - Maestro
no_billing_test_mode.yaml: added an opt-in flow (tagno_billing) that covers test mode, a Play-only paywall and a simulated purchase on a device with the Play Store disabled.
Claude Opus | 𝕏
- Log a warning listing the products a paywall presents without when billing is unavailable, so a missing price can be traced from logs. - Make BillingAvailability and GoogleBillingWrapper.availability internal. - Clarify that only setup-time unavailability is remembered. - Clear the test mode sheet container's background after its first layout, since BottomSheetBehavior swaps in its own background then on Material-themed hosts. - Rename the instrumented wrapper tests that still described the removed per-product failure cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On devices with a broken Play Store (e.g. microG-based emulators), billing setup keeps returning ERROR instead of BILLING_UNAVAILABLE. That was retried forever, so product requests queued behind the connection and a paywall load never finished. After three transient setup failures in a row, billing is now marked unavailable so waiting requests fail. Reconnect attempts carry on and a later successful setup makes billing available again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No blockers. One suggestion inline about the partial result being cached.
Reviewed changes
This review covers what changed since the last Pullfrog review (c6a6241): fixes for the earlier review comments, and the new rule that marks billing unavailable after repeated transient setup failures. All four earlier threads are fixed and resolved.
- Transient setup failure limit: added
MAX_TRANSIENT_SETUP_FAILURES = 3. After three transient setup results in a row (ERROR,SERVICE_UNAVAILABLE, ...), billing is marked unavailable and queued requests fail. Reconnects with backoff carry on, and the counter resets onOKand on the foreground re-probe. The two new tests would fail against the old behaviour. - Narrowed public API:
BillingAvailabilityandGoogleBillingWrapper.availabilityare nowinternal. - Partial presentation log:
StoreManager.getProductsnow logs aLogLevel.warnwith the product ids that didn't resolve. - Sheet background timing:
setSheetContentnow clears the container background indoOnLayout. That runs afterBottomSheetBehavior.onLayoutChildswaps in itsMaterialShapeDrawable, so it also works on Material-themed hosts. - Comment and test wording: the
awaitGetProducts.onErrorcomment and the instrumentedGoogleBillingWrapperTestnames now describe the fast-fail behaviour instead of the removed cache.
Claude Opus | 𝕏
A paywall that presented without some Play products because billing was unavailable was cached with failAt unset, so PaywallRequestManager never reloaded its products. Once billing recovered (after the foreground re-probe, or a reconnect following transient setup failures), the paywall still showed without Play prices for the rest of the session. Set failAt for a partial load so the next request retries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found. The open thread about the partial result being cached is fixed.
Reviewed changes
This review covers what changed since the last Pullfrog review (4bac825). Apart from coverage badge updates, that's one commit, and it fixes the thread about a partial load being cached for the whole session.
- Partial loads now retry: when billing is unavailable and some products didn't resolve,
StoreManager.getProductsnow setspaywall.productsLoadingInfo.failAt. On the next request,PaywallRequestManagerseesfailAton the cached paywall and runsaddProductsagain, so Play prices appear once billing is back. - Retry stays cheap without billing: on a device that really has no billing, each retry finds the custom products already cached, so
fetchAndCacheCustomProductsmakes no/productscall, andexecuteRequestOnUIThreadfails the Play lookup straight away.PaywallProductsLoadstill reportsComplete, withproductsLoadFailTimeset. - Tests: the mixed-paywall test now asserts that
failAtis set, which would fail without this fix. A new test checks that a secondgetProductscall returns the Play product once billing works, which provesStoreManagerdoesn't cache theBillingNotAvailableerror.
Claude Opus | 𝕏

Problem
On a device without Google Play Billing (no Play Store, no signed-in account), the SDK broke even for flows that don't need Play: test mode and custom store products.
AutomaticPurchaseControllerrethrew from its constructor when the billing client couldn't be created, which failsconfigure().AutomaticPurchaseController.purchase()waited forever for a connection that had already failed.GoogleBillingWrapper, queued product requests were never completed.Changes
GoogleBillingWrappertracks aBillingAvailabilitystate (Unknown,Available,Unavailable).BillingNotAvailableandqueryAllPurchasesreturns empty, with no reconnect or retries.ERROR,SERVICE_UNAVAILABLE, ...) also mark billing unavailable. On devices with a broken Play Store, setup returnedERRORforever, so product requests queued behind the connection and the paywall load never finished. Reconnects carry on, and a later successful setup makes billing available again.BillingNotAvailableis no longer cached in the static products cache.StoreManagerapplies one rule ingetProducts: when billing is unavailable, the paywall presents with whatever resolved without Play, and only fails when nothing resolved and test mode is off. The two test-mode-only catches infetchOrAwaitProductsare gone.BillingNotAvailable(unchanged)AutomaticPurchaseControllerno longer throws from its constructor, andpurchase()gives a failed connection one more attempt and then returnsPurchaseResult.Failed.Test mode sheets: the modal and the purchase and restore drawers showed square corners behind their rounded backgrounds, because
BottomSheetDialog's sheet container has its own opaque background. It's now transparent.Behaviour changes to note
AutomaticPurchaseController.purchase()now checks the connection before building the billing flow params.Testing
GoogleBillingWrapperAvailabilityTestandAutomaticPurchaseControllerTest, plus two newStoreManagerTestcases../gradlew :superwall:testDebugUnitTestpasses (1334 tests). Instrumented test sources compile but were not run.pm disable-user com.android.vending). Billing reportsBILLING_UNAVAILABLEand nothing crashes. In test mode, the modal shows, a Play-only paywall presents with test-catalog prices, and a simulated purchase completes (transaction_complete, status becomes active).ERROR. Before the transient-failure change, a normal paywall hung with no presentation result. Now it fails with the billing error, and the test mode flow passes.app(Theme.MaterialComponents).test_app/maestro/testmode/no_billing_test_mode.yamlcovers that path and passes on the lim.run phone. It needs the Play Store disabled first, so it isn't inconfig.yaml's default flows.🤖 Generated with Claude Code