Skip to content

Keep the SDK working on devices without Play Billing - #472

Merged
ianrumac merged 10 commits into
developfrom
ir/feat/storeless
Oct 1, 2026
Merged

ianrumac merged 10 commits into
developfrom
ir/feat/storeless

Conversation

@ianrumac

@ianrumac ianrumac commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

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.

  • A paywall with one Play product failed entirely, even when its custom products had loaded.
  • AutomaticPurchaseController rethrew from its constructor when the billing client couldn't be created, which fails configure().
  • AutomaticPurchaseController.purchase() waited forever for a connection that had already failed.
  • If creating the billing client threw inside GoogleBillingWrapper, queued product requests were never completed.
  • Nothing remembered that billing was unavailable, so every request reconnected, while the failure was cached per product for the life of the process.

Changes

GoogleBillingWrapper tracks a BillingAvailability state (Unknown, Available, Unavailable).

  • Once unavailable, requests fail straight away with BillingNotAvailable and queryAllPurchases returns empty, with no reconnect or retries.
  • A billing client that can't be created marks billing unavailable and fails the queued requests.
  • Availability is probed again when the app returns to the foreground, so signing in to Play later recovers.
  • Three transient setup failures in a row (ERROR, SERVICE_UNAVAILABLE, ...) also mark billing unavailable. On devices with a broken Play Store, setup returned ERROR forever, 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.
  • BillingNotAvailable is no longer cached in the static products cache.
  • When a paywall presents without some products because billing is unavailable, a warning lists them.

StoreManager applies one rule in getProducts: 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 in fetchOrAwaitProducts are gone.

Device without billing Test mode on Test mode off
Play-only paywall Presents Fails with BillingNotAvailable (unchanged)
Mixed paywall Presents Presents with the custom products (new)
Custom-only paywall Presents Presents

AutomaticPurchaseController no longer throws from its constructor, and purchase() gives a failed connection one more attempt and then returns PurchaseResult.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

  • Mixed paywalls on a no-billing device now present with their Play products unresolved, so those show without a price.
  • AutomaticPurchaseController.purchase() now checks the connection before building the billing flow params.

Testing

  • New GoogleBillingWrapperAvailabilityTest and AutomaticPurchaseControllerTest, plus two new StoreManagerTest cases.
  • ./gradlew :superwall:testDebugUnitTest passes (1334 tests). Instrumented test sources compile but were not run.
  • On-device: a lim.run Android 15 phone and a local Pixel 8 emulator, both with the Play Store disabled (pm disable-user com.android.vending). Billing reports BILLING_UNAVAILABLE and 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).
  • Also on a lim.run phone with the Play Store enabled (microG-based, no account): setup fails with 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.
  • The sheet corner fix was checked on both test_app (framework theme) and the sample app (Theme.MaterialComponents).
  • New Maestro flow test_app/maestro/testmode/no_billing_test_mode.yaml covers that path and passes on the lim.run phone. It needs the Play Store disabled first, so it isn't in config.yaml's default flows.
  • No CHANGELOG entry yet, since 2.8.4 is already tagged and the next version isn't set.

🤖 Generated with Claude Code

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>

@pullfrog pullfrog Bot left a comment

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.

ℹ️ 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.

  • BillingAvailability state: once setup reports BILLING_UNAVAILABLE/FEATURE_NOT_SUPPORTED, or createBillingClient throws, requests fail straight away through executeRequestOnUIThread. Billing is probed again the next time the app comes to the foreground.
  • Per-product failure cache removed: BillingNotAvailable is no longer written to the static productsCache, so billing that becomes available later isn't hidden by an old failure.
  • AutomaticPurchaseController hardening: creating the client can no longer fail configure(), and purchase() retries a failed connection once with a 10s limit, then returns Failed.
  • StoreManager rule: fetchOrAwaitProducts returns billingUnavailable instead of throwing it. getProducts now fails only when nothing resolved and test mode is off.

ℹ️ Nitpicks

  • The instrumented GoogleBillingWrapperTest still 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 in executeRequestOnUIThread, so the names now describe behaviour that no longer exists.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Claude Opus | 𝕏

Comment thread superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt
Comment thread superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt Outdated
Comment thread superwall/src/main/java/com/superwall/sdk/billing/BillingAvailability.kt Outdated
github-actions Bot and others added 3 commits September 30, 2026 15:04
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>

@pullfrog pullfrog Bot left a comment

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.

ℹ️ 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 of design_bottom_sheet. TestModeModal, TestModePurchaseDrawer and TestModeRestoreDrawer now use it.
  • test_app "Configure with test mode": added an option to configureWithoutPC that sets TestModeBehavior.ALWAYS, plus a button that calls it.
  • Maestro no_billing_test_mode.yaml: added an opt-in flow (tag no_billing) that covers test mode, a Play-only paywall and a simulated purchase on a device with the Play Store disabled.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Claude Opus | 𝕏

ianrumac and others added 2 commits October 1, 2026 14:45
- 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>

@pullfrog pullfrog Bot left a comment

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.

ℹ️ 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 on OK and on the foreground re-probe. The two new tests would fail against the old behaviour.
  • Narrowed public API: BillingAvailability and GoogleBillingWrapper.availability are now internal.
  • Partial presentation log: StoreManager.getProducts now logs a LogLevel.warn with the product ids that didn't resolve.
  • Sheet background timing: setSheetContent now clears the container background in doOnLayout. That runs after BottomSheetBehavior.onLayoutChild swaps in its MaterialShapeDrawable, so it also works on Material-themed hosts.
  • Comment and test wording: the awaitGetProducts.onError comment and the instrumented GoogleBillingWrapperTest names now describe the fast-fail behaviour instead of the removed cache.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Claude Opus | 𝕏

Comment thread superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt
github-actions Bot and others added 3 commits October 1, 2026 12:54
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>

@pullfrog pullfrog Bot left a comment

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.

✅ 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.getProducts now sets paywall.productsLoadingInfo.failAt. On the next request, PaywallRequestManager sees failAt on the cached paywall and runs addProducts again, 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 fetchAndCacheCustomProducts makes no /products call, and executeRequestOnUIThread fails the Play lookup straight away. PaywallProductsLoad still reports Complete, with productsLoadFailTime set.
  • Tests: the mixed-paywall test now asserts that failAt is set, which would fail without this fix. A new test checks that a second getProducts call returns the Play product once billing works, which proves StoreManager doesn't cache the BillingNotAvailable error.

Pullfrog  | View workflow run | Using Claude Opus | 𝕏

@ianrumac
ianrumac merged commit 76477c6 into develop Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant