Skip to content
Merged

2.8.5 #476

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b75ed4d
Keep the SDK working on devices without Play Billing
ianrumac Sep 30, 2026
a1d31a2
Update coverage badge [skip ci]
github-actions[bot] Sep 30, 2026
f92e497
Fix square corners showing behind test mode sheets
ianrumac Oct 1, 2026
c6a6241
Add a test_app flow for test mode without Play Billing
ianrumac Oct 1, 2026
2873e09
Address review comments on billing availability
ianrumac Oct 1, 2026
4bac825
Treat repeated transient billing setup failures as unavailable
ianrumac Oct 1, 2026
35c2baf
Update coverage badge [skip ci]
github-actions[bot] Oct 1, 2026
55f43e1
Update coverage badge [skip ci]
github-actions[bot] Oct 1, 2026
aaf4f81
Reload a paywall's products after a partial billing-unavailable load
ianrumac Oct 1, 2026
d3b029c
Update coverage badge [skip ci]
github-actions[bot] Oct 1, 2026
76477c6
Merge pull request #472 from superwall/ir/feat/storeless
ianrumac Oct 1, 2026
a61b386
Add actors for PV cache and TestMode
ianrumac May 2, 2026
ff0339d
Move Entitlements onto the StateActor primitives
ianrumac Sep 15, 2026
fa17c96
Address review on actor refactor
ianrumac Sep 25, 2026
6b5b225
Route activity view access through PaywallViewRegistry
ianrumac Sep 25, 2026
7c8446f
Keep status entitlements in all after a cold start
ianrumac Sep 25, 2026
c10cfac
Minor fixes
ianrumac Sep 30, 2026
ff76d54
Merge product entitlements into allTracked instead of replacing
ianrumac Sep 30, 2026
2d272fb
Fix delegate status 'from', keep allTracked status-only, pin delegate…
ianrumac Sep 30, 2026
4933348
Assert the last persisted status in delegate override tests
ianrumac Oct 2, 2026
b8eea44
Update coverage badge [skip ci]
github-actions[bot] Oct 2, 2026
6b0aaf8
Merge pull request #466 from superwall/ir/refactor/actor-cache-test-mode
ianrumac Oct 2, 2026
87f0208
Keep subscribers active through a Play read that answered nothing
yusuftor Sep 22, 2026
6345ce2
Address review on the empty-read guard
ianrumac Oct 5, 2026
57be470
Add changelog entry for the mapping-failure fix
ianrumac Oct 5, 2026
1bfc5bc
Merge pull request #470 from superwall/ytor/empty-read-no-downgrade
ianrumac Oct 5, 2026
9b0e61b
Acknowledge unacknowledged purchases on every status sync
ianrumac Oct 7, 2026
7830ce8
Update coverage badge [skip ci]
github-actions[bot] Oct 7, 2026
c781a1a
Merge pull request #474 from superwall/ir/fix/acknowledge-on-sync
ianrumac Oct 7, 2026
1e078d5
Add the Customer Center
claude Sep 23, 2026
4aee94a
Tint the Customer Center with the host app's colour
claude Sep 24, 2026
9b3240c
Port the iOS Customer Center sheet fixes
ianrumac Oct 5, 2026
ac86a8a
Address review on the Customer Center
ianrumac Oct 5, 2026
2063760
Recover from a failed restore and refresh after web management
ianrumac Oct 5, 2026
a84b501
Keep the Customer Center off the main thread and working on API 23
ianrumac Oct 5, 2026
465f182
Keep the Customer Center's IO work cancellable
ianrumac Oct 7, 2026
7946903
Wait for the test-mode modal events instead of racing them
ianrumac Oct 7, 2026
805c3e0
List the Customer Center under 2.8.5
ianrumac Oct 7, 2026
f25f6c2
Add an Open Customer Center button to the event timeline screen
ianrumac Oct 7, 2026
951f017
Merge pull request #471 from superwall/claude/ios-sdk-implementation-…
ianrumac Oct 7, 2026
4c95c74
Fail product loads instead of hanging, and dismiss paywalls whose web…
ianrumac Oct 8, 2026
2a4e4eb
Update coverage badge [skip ci]
github-actions[bot] Oct 8, 2026
e1a1c8b
Drive the billing wrapper instrumentation tests with runCurrent befor…
ianrumac Oct 8, 2026
86881e9
Merge pull request #475 from superwall/ir/fix/product-failure-forward…
ianrumac Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub.

## 2.8.5

## Enhancements
- Adds the Customer Center, a self-service screen where users can view and restore their purchases, cancel or change a Google Play subscription, request a refund, manage a web subscription and contact support. Present it with `Superwall.instance.presentCustomerCenter()`, dismiss it with `Superwall.instance.dismissCustomerCenter()`, and configure it with `SuperwallOptions.customerCenter`.
- Adds `CustomerCenterDelegate` and the `customerCenter_open`, `customerCenter_close`, `customerCenter_action`, `customerCenter_surveyResponse` and `customerCenter_refundRequest` events.

## Fixes
- Fix subscribers with an unexpired subscription being reported as inactive when Google Play fails to answer a purchase query, for example when the billing client isn't ready at launch. A query that succeeds and reports no purchases still deactivates straight away.
- Fix subscribers being reported as inactive when Google Play returns an active purchase whose product no longer maps to an entitlement in config. A lost mapping is no longer treated as the subscription ending.
- Fix purchases being refunded by Google Play for not being acknowledged when they did not complete through the billing flow callback, for example a pending purchase that settled while the app was closed, the app being killed mid-purchase, or a Play Store promo code. The `AutomaticPurchaseController` now acknowledges any unacknowledged purchase whenever it syncs the subscription status, including on launch, and retries acknowledgements that fail.
- Fix `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 second `startConnection` is no longer issued while one is in flight.
- A paywall whose webview can't load, because every URL and retry has been used up or `paywalls.timeoutAfter` passed, is now dismissed as declined with the `webViewFailedToLoad` close reason, matching iOS. The register handler gets `onDismiss` and then the feature block for a non-gated placement, or `onError` for a gated one. Previously the paywall stayed on screen, and with `timeoutAfter` set the handler received `onPresent` followed by a bare `onError` while the paywall was still showing. A paywall that fails to load off-screen, for example during preload, reloads the next time it is presented.

## 2.8.4

## Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
Expand All @@ -50,6 +51,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.sp
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.superapp.ui.theme.MyApplicationTheme
import org.json.JSONArray
Expand Down Expand Up @@ -122,6 +124,17 @@ private fun TimelineListScreen(
modifier = Modifier.padding(16.dp),
)

Button(
onClick = { Superwall.instance.presentCustomerCenter() },
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp),
) {
Text("Open Customer Center")
}

if (timelines.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.After
Expand Down Expand Up @@ -229,8 +230,9 @@ class GoogleBillingWrapperTest {
runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
}

// Advance so the async block runs and adds its request to the queue
advanceUntilIdle()
// runCurrent, not advanceUntilIdle: idling would run the product query timeout
// before the setup result below is delivered.
runCurrent()

capturedStateListener?.onBillingSetupFinished(
billingResult(BillingClient.BillingResponseCode.BILLING_UNAVAILABLE),
Expand All @@ -257,8 +259,9 @@ class GoogleBillingWrapperTest {
runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
}

// Advance so the async block runs and adds its request to the queue
advanceUntilIdle()
// runCurrent, not advanceUntilIdle: idling would run the product query timeout
// before the setup result below is delivered.
runCurrent()

capturedStateListener?.onBillingSetupFinished(
billingResult(BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED),
Expand Down Expand Up @@ -387,11 +390,11 @@ class GoogleBillingWrapperTest {
}

// ========================================================================
// Region: Products cache — transient errors are not cached
// Region: Billing unavailable — later requests fail fast
// ========================================================================

@Test
fun test_billing_not_available_is_cached() =
fun test_billing_not_available_fails_later_requests_fast() =
runTest {
Given("a wrapper where billing is unavailable") {
val wrapper = createWrapper(clientReady = false)
Expand All @@ -402,8 +405,9 @@ class GoogleBillingWrapperTest {
runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
}

// Advance so the async block runs and adds its request to the queue
advanceUntilIdle()
// runCurrent, not advanceUntilIdle: idling would run the product query timeout
// before the setup result below is delivered.
runCurrent()

capturedStateListener?.onBillingSetupFinished(
billingResult(BillingClient.BillingResponseCode.BILLING_UNAVAILABLE),
Expand All @@ -416,11 +420,11 @@ class GoogleBillingWrapperTest {
outcome1.exceptionOrNull() is BillingError.BillingNotAvailable,
)

Then("a second call should fail immediately from cache without hitting billing") {
Then("a second call should fail immediately without reconnecting") {
val outcome2 = runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
assertTrue("Second call should also fail", outcome2.isFailure)
assertTrue(
"Should be BillingNotAvailable from cache",
"Should be BillingNotAvailable",
outcome2.exceptionOrNull() is BillingError.BillingNotAvailable,
)
}
Expand All @@ -429,7 +433,7 @@ class GoogleBillingWrapperTest {
}

@Test
fun test_multiple_products_cached_on_billing_not_available() =
fun test_billing_not_available_fails_later_requests_for_any_product() =
runTest {
Given("multiple products that fail due to billing unavailable") {
val wrapper = createWrapper(clientReady = false)
Expand All @@ -442,20 +446,21 @@ class GoogleBillingWrapperTest {
runCatching { wrapper.awaitGetProducts(ids) }
}

// Advance so the async block runs and adds its request to the queue
advanceUntilIdle()
// runCurrent, not advanceUntilIdle: idling would run the product query timeout
// before the setup result below is delivered.
runCurrent()

capturedStateListener?.onBillingSetupFinished(
billingResult(BillingClient.BillingResponseCode.BILLING_UNAVAILABLE),
)

assertTrue(result1.await().isFailure)

Then("retrying any single product should fail from cache immediately") {
Then("retrying any single product should fail immediately") {
val outcome = runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
assertTrue(outcome.isFailure)
assertTrue(
"Should be a cached BillingNotAvailable error",
"Should be BillingNotAvailable",
outcome.exceptionOrNull() is BillingError.BillingNotAvailable,
)
}
Expand All @@ -475,8 +480,9 @@ class GoogleBillingWrapperTest {
runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
}

// Advance so the async block runs and adds its request to the queue
advanceUntilIdle()
// runCurrent, not advanceUntilIdle: idling would run the product query timeout
// before the setup result below is delivered.
runCurrent()

// SERVICE_UNAVAILABLE retries connection but does NOT drain requests
capturedStateListener?.onBillingSetupFinished(
Expand All @@ -495,11 +501,11 @@ class GoogleBillingWrapperTest {
outcome1.exceptionOrNull() is BillingError.BillingNotAvailable,
)

Then("product is cached as BillingNotAvailable, second call fails from cache") {
Then("billing is known to be unavailable, so a second call fails immediately") {
val outcome2 = runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
assertTrue("Second call should also fail", outcome2.isFailure)
assertTrue(
"Should be BillingNotAvailable from cache",
"Should be BillingNotAvailable",
outcome2.exceptionOrNull() is BillingError.BillingNotAvailable,
)
}
Expand Down Expand Up @@ -777,7 +783,7 @@ class GoogleBillingWrapperTest {
runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
}

advanceUntilIdle()
runCurrent()

capturedStateListener?.onBillingSetupFinished(
billingResult(
Expand Down Expand Up @@ -811,7 +817,7 @@ class GoogleBillingWrapperTest {
runCatching { wrapper.awaitGetProducts(setOf("p1:base:sw-auto")) }
}

advanceUntilIdle()
runCurrent()

When("SERVICE_UNAVAILABLE occurs (requests stay in queue)") {
capturedStateListener?.onBillingSetupFinished(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.superwall.sdk.config

import And
import Given
import Then
import When
Expand All @@ -10,7 +9,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.Tier
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.options.SuperwallOptions
import com.superwall.sdk.dependencies.DependencyContainer
import com.superwall.sdk.misc.Either
Expand Down
Loading
Loading