Repository navigation
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>
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>
- 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>
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>
Keep the SDK working on devices without Play Billing
Migrates PaywallViewCache and TestMode onto the StateActor primitives: PaywallCacheState/PaywallCacheContext own the view map and active key, TestModeState gains pure reducers and async actions with TestModeContext and TestModeLogic, and ConfigState moves up to the config package. Rebased onto develop (Sep 2026) with these adaptations: - Keep develop's loadingColor on PaywallViewCache/LoadingView. - Port develop's productsLoaded/awaitTestProducts so StoreManager can wait for the test product catalog; the deferred is carried through session copies. - SetActive preserves an existing session and only clears entitlement selections when the activation reason changes, matching develop. - TestMode.activate is suspend and awaited via `immediate`; ConfigState launches it in its scope so the modal never blocks config. - Cache view factories no longer hop to Dispatchers.Main: acquire* block the caller (usually main) with runBlocking, so that hop deadlocked. - Keep develop's ensureActive guard in PaywallMessageHandler. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ports the entitlements slice of the March actor draft (ir/refactor/actors) onto current develop: - EntitlementsState holds status, product entitlements, device, backing and web entitlements as an immutable snapshot with pure reducers; `all`, `active` and `inactive` are derived from it. createInitialEntitlementsState rebuilds the snapshot from storage before the actor starts, including web entitlements from the latest redemption response. - Entitlements becomes a facade over a StateActor implementing EntitlementsContext. Status changes and product entitlement updates are persisted immediately instead of through a collected flow. - Web entitlements are cached in state rather than re-read from storage on every access, so WebPaywallRedeemer now publishes them through a new Factory.setWebEntitlements hook at each point it writes the redemption response. - Adds EntitlementsRefactorSafetyTest (46 cases) and reworks EntitlementsTest for the actor construction. Differences from the draft: the constructor keeps `Entitlements(storage)` working via defaults, and EntitlementsContext no longer carries HasExternalPurchaseControllerFactory since no action used it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Acquire loading/shimmer views through the cache in SuperwallPaywallActivity so getPaywall() + startWithView() no longer crashes when they have not been created yet. - Route external ViewStorage writes (activity launch/restore, DebugView) through new synchronous PaywallViewCache.storeView/removeView so cache state and ViewStorage cannot diverge; RemoveAllExceptActive now removes exactly the keys it evicted. - Run the cache actor on the container ioScope instead of a leaked scope. - Only publish polled web entitlements when they are persisted. - Tests: cover external writers and lazy loading/shimmer, assert the redeemer publishes what it persists, tighten weak assertions, drop obsolete delays, and extract a makeEntitlements helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SuperwallPaywallActivity and DebugView now reach paywall views through an internal PaywallViewRegistry obtained from DependencyContainer.makeViewRegistry(), instead of reaching into paywallManager.cache or ViewStorage directly. Writes go through the cache so it and ViewStorage stay in sync; reads use ViewStorage, which survives Activity recreation. PaywallManager's cache is private again. Adds an on-device test proving the loading and shimmer views can be built on a thread without a Looper, as the cache actor does, and still draw and animate on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createInitialEntitlementsState replayed the saved status before the stored product entitlements. AddProductEntitlements replaces allTracked, so status entitlements not tied to a product dropped out of `all` while still in `active`, until the next status update. The old startup code never replaced that set. Restore product entitlements first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AddProductEntitlements overwrote allTracked, so a status-only entitlement dropped out of `all` once product entitlements loaded while still showing in `active`. Also rename the off-main cache acquire test to match what it asserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… override - Take the delegate's previous status from the status flow. Entitlements now persists before the listener runs, so reading storage gave from == to. - AddProductEntitlements no longer writes allTracked; `all` already unions product entitlements, and this avoids stale entries on a remapped product. - Add SubscriptionStatusDelegateOverrideTest for setting the status from inside subscriptionStatusDidChange (add, remove, replace, grant). - Stub the suspend PaywallViewCache.save with coEvery in PaywallManagerExperimentIsolationTest so unit tests compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Actor refactor: PaywallViewCache, TestMode and Entitlements
An empty Play Billing read set the status to inactive no matter why it was empty. The queries can fail outright - the billing client is not ready at launch, the query times out, the retries run out - and the code published inactive first and only then scheduled its retry, so a paying subscriber lost access for at least a second. An active purchase that config no longer maps to an entitlement did the same. Nothing that is not an answer may now demote a subscriber whose entitlement has not expired. A read that succeeds and reports no purchases is still an answer and deactivates straight away, so refunds and cancellations behave as before. The status carries config-shaped entitlements, which have no expiry date, so the dates come from the device CustomerInfo that ReceiptManager builds from Play receipts and persists across launches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Match refuting product ids across namespaces: backfill productIds from the device records and compare on the subscription id, so config-shaped entitlements and full config ids line up with raw Play ids. An entitlement with no known product ids is never refuted. - Keep web entitlements out of activeDeviceEntitlements so WebPaywallRedeemer.clear() and the restore message stay Play-only. - Don't carry entitlements flagged inactive into the published Active. - Document the null-expiry limitation and the device-clock trade-off. - Log the swallowed storage error. - Add tests built from config-shaped entitlements and real id shapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep subscribers active through a Play read that answered nothing
Purchases were only acknowledged from the billing flow callback, with no retry. Pending purchases that settled later, purchases interrupted by process death, Play Store promo codes and failed acknowledgements were never acknowledged and got refunded by Play after 3 days. The subscription status sync now acknowledges any purchased, unacknowledged purchase it reads, and acknowledgements wait for a ready client and retry. In-flight tokens are tracked so the callback and the sync don't both acknowledge the same purchase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Acknowledge unacknowledged purchases on every status sync
Port of the iOS SDK's Customer Center (superwall/Superwall-iOS#509): a self-service screen where users can view and restore purchases, cancel or change a Google Play subscription, request a refund, manage a web subscription and contact support. - Superwall.presentCustomerCenter()/dismissCustomerCenter(), configured via SuperwallOptions.customerCenter or per presentation - CustomerCenterDelegate and customerCenter_* events - Store-specific actions mapped to Google Play: subscription page for cancel/change plan, order history for refunds - Web catalogue fill-in for Stripe/Paddle products, cached for 5 minutes - Strings localized in the same 45 languages as iOS - restore can now suppress the SDK's own failure prompts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PurFQ7fxGjSF6y6ojfrd81
Like iOS, where the Customer Center inherits the app's tint, it now takes the colorPrimary of the theme it was presented from (else the application's) instead of Material 3's default purple. A configured accent still wins, and system blue fills in when the app's theme only carries framework or Material/AppCompat defaults. Spinners and dialog buttons are tinted too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PurFQ7fxGjSF6y6ojfrd81
From superwall/Superwall-iOS#533, the fixes that apply on Android: - A sheet leaves with the detail screen that asked for it. When a purchase's detail screen goes, say because a refresh removed the purchase, its survey is dropped along with the pending action, so answering it can't act on a purchase the customer no longer sees. A sheet whose action finishes after its screen has gone isn't shown over the root. Root sheets stay while a detail screen covers them. - With no product id or package, Google Play's subscription link opens the list of subscriptions instead of a deep link to no subscription. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Translate the App Store store label in every locale, and fail the strings test when a translation is missing an English key - Don't list an entitlement twice when its only link to a listed purchase is its latest product - Keep the rows busy while a survey gates an action, and until that action finishes, so a survey-gated restore can't run twice - Run the view model on the SDK's MainScope, whose exception handler keeps an unexpected error from crashing the host app - End a presentation whose activity never starts, such as one blocked from the background, so later presentations aren't refused as already presented - Fold the presentsFailureAlert KDoc into the existing block and drop the empty branch around the restore failure prompts - Use relative padding where the start and end differ, document why USER_CANCELLED stays, and sort the new imports Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A restore that throws, say on a Play billing error, now ends on the dismissible nothing-found alert instead of leaving the restoring overlay up for good - Coming back from a web management page reloads web entitlements from Superwall, so a cancelled or changed Stripe/Paddle subscription shows without waiting up to a day for the next poll. The redeemer's poll body is now refreshWebEntitlements(), which both use - Test the Customer Center manager: a presentation whose activity never starts ends after the attach timeout, one that attached is left alone Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Read the locale through ConfigurationCompat. Configuration.getLocales() only exists from API 24, so presenting the Customer Center crashed on Android 6, which the SDK still supports - Run the live adapters' network and billing work on the container's IOScope, as the rest of the SDK does: product lookup (through StoreManager rather than Superwall.getProducts), the catalogue fetch, the purchase refresh and restore. The view model runs on the main thread, so the catalogue request threw NetworkOnMainThreadException and web products never got their details - refreshWebEntitlements uses the redeemer's IOScope too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running the live adapters on IOScope's full context swapped the caller's job for IOScope's SupervisorJob, so the work stopped being a child of its caller. The catalogue fetch's 5 second timeout couldn't cancel it, and offline its retry waits kept web purchase cards on placeholders for about a minute. Closing the Customer Center didn't reach it either. The adapters now take IOScope's dispatcher and error handling without its job. refreshWebEntitlements is back to a plain extraction of the poll body, with the Customer Center moving it off the main thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestMode tracks each event from its own coroutine on the IO pool, so the Open and Close events could land after activate returned, out of order, or into the list from two threads at once. The test now receives each event from a channel: Open while the modal is showing, Close after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b8dtzb Add Customer Center feature for subscription management
…view can't load register() could stall for the life of the process when a Google Play product query at launch was dropped: a request that found the billing client disconnected between being queued and running was discarded without a callback, so its products stayed Loading and every paywall request, plus every later register() behind it in the serial queue, waited forever. Such requests are now re-queued until the client reconnects, a query Play never answers times out after 15s, a disconnect with queued requests reconnects, and no second startConnection is issued while one is in flight. A paywall whose webview can't load, or whose paywalls.timeoutAfter passes, is now dismissed as declined with the webViewFailedToLoad close reason, matching iOS. The register handler gets onDismiss followed by the feature block for a non-gated placement or onError for a gated one, instead of a paywall left on screen (and, with timeoutAfter, an onPresent followed by a bare onError). A paywall that fails to load off-screen reloads the next time it is presented. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e setup answers awaitGetProducts now has a 15s timeout, so advancing virtual time to idle while a request is queued fails it with Timeout before the test delivers the simulated setup result. Run the queued request with runCurrent instead, the same change as TestModeBillingUnavailableIntegrationTest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-error Fail product loads instead of hanging, and dismiss paywalls whose webview can't load
This branch was successfully deployed
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.
2.8.5
Enhancements
Superwall.instance.presentCustomerCenter(), dismiss it withSuperwall.instance.dismissCustomerCenter(), and configure it withSuperwallOptions.customerCenter.CustomerCenterDelegateand thecustomerCenter_open,customerCenter_close,customerCenter_action,customerCenter_surveyResponseandcustomerCenter_refundRequestevents.Fixes
AutomaticPurchaseControllernow acknowledges any unacknowledged purchase whenever it syncs the subscription status, including on launch, and retries acknowledgements that fail.register()never calling its handler, and every later placement stalling behind it, when a Google Play product query at launch was dropped. A request that found the billing client disconnected between being queued and running was discarded without a callback, leaving its products loading forever. Such requests are now re-queued until the client reconnects, a product query that Google Play never answers times out after 15 seconds, a disconnect with queued requests reconnects, and a secondstartConnectionis no longer issued while one is in flight.paywalls.timeoutAfterpassed, is now dismissed as declined with thewebViewFailedToLoadclose reason, matching iOS. The register handler getsonDismissand then the feature block for a non-gated placement, oronErrorfor a gated one. Previously the paywall stayed on screen, and withtimeoutAfterset the handler receivedonPresentfollowed by a bareonErrorwhile the paywall was still showing. A paywall that fails to load off-screen, for example during preload, reloads the next time it is presented.Checklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.ktlintin the main directory and fixed any issues.