Skip to content
Open

2.8.0 #447

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
58c6733
Add custom store products, new purchase controller method
ianrumac May 11, 2026
9816ab6
Add CustomPurchaseController, fix referenecs to external controllers
ianrumac Jul 24, 2026
8c69af0
Add storefront test
ianrumac Jul 24, 2026
62e0de5
Update coverage badge [skip ci]
github-actions[bot] Jul 24, 2026
cae6f3a
Update Google Play Billing Library to 9.1.0
ianrumac Jul 27, 2026
87a13e4
Deprecate old purchase controller method
ianrumac Jul 27, 2026
201081a
Merge branch 'develop' into ir/feat/update-billing
ianrumac Jul 27, 2026
edcf9cd
Merge branch 'develop' into ir/feat/custom-store-products
ianrumac Jul 27, 2026
bdcdfa6
Expand 2.8.0 changelog entry
ianrumac Jul 27, 2026
2bb7304
Raise minSdk to 23 for Billing 9 and add changelog warning
ianrumac Jul 27, 2026
1e92927
Update coverage badge [skip ci]
github-actions[bot] Jul 27, 2026
5f85974
Bump changelog
ianrumac Jul 27, 2026
a5d3173
Update tests
ianrumac Jul 27, 2026
cb5c21d
Merge pull request #407 from superwall/ir/feat/custom-store-products
ianrumac Jul 28, 2026
89d5181
Merge branch 'develop' into ir/feat/update-billing
ianrumac Jul 28, 2026
f408ef0
Merge pull request #444 from superwall/ir/feat/update-billing
ianrumac Jul 28, 2026
d342606
feat: forward re-routed back presses into the paywall as back_button_…
claude Jul 27, 2026
cec47f3
feat: forward back presses to the paywall by default, gated by capabi…
claude Jul 27, 2026
d7a538c
revert to opt-in: only forward back presses when reroute_back_button …
claude Jul 27, 2026
d8a2c31
forward back presses unconditionally; drop capability probe
claude Jul 28, 2026
2e4192f
drop pressed field from BackButtonInputEvent
claude Jul 28, 2026
8cc6d5f
move back-button changelog entry into 2.8.0 after rebase
claude Jul 28, 2026
dcc8e35
route back press through PaywallMessageHandler
claude Jul 28, 2026
37b081b
Update coverage badge [skip ci]
github-actions[bot] Jul 28, 2026
977380d
Merge pull request #442 from superwall/claude/reroute-back-button-input
ianrumac Jul 29, 2026
7c0d31d
Keep test store product naming
ianrumac Jul 29, 2026
2cfa2db
Update options back from dev
ianrumac Jul 29, 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.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

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

## 2.8.0

## Enhancements
- Updates Google Play Billing Library from 8.0.0 to 9.1.0. See the [Play Billing Library 9 migration guide](https://developer.android.com/google/play/billing/migrate-gpblv9) for the full list of changes.
- Updates the RevenueCat SDK used by the sample apps from 9.2.0 to 10.14.1 for Billing 9 compatibility.
- Adds support for custom store products. Products configured on a custom store in the Superwall dashboard (e.g. Stripe or your own payment backend) can now be attached to paywalls: their metadata (price, subscription period, trial) is fetched from the Superwall API instead of Google Play and templated into the paywall like any other product. Purchases are routed through your `PurchaseController`, bypassing Google Play Billing entirely — check `product.isCustomProduct` to fulfill them via your own payment flow, and grant their entitlements with `Superwall.instance.setSubscriptionStatus(...)` on success. Requires configuring the SDK with a `PurchaseController`.
- Adds a unified `PurchaseController.purchase(activity, product: StoreProduct, basePlanId, offerId)` method that handles both Google Play and custom store products. For Play products, the underlying `ProductDetails` are available via `product.rawStoreProduct`.
- Custom purchases produce full transaction analytics (`transaction_start`/`transaction_complete`, `subscriptionStart`/`freeTrialStart`) with an SDK-generated transaction identifier exposed as `StoreProduct.customTransactionId`, and free-trial eligibility for custom products is derived from the customer's entitlement history.
- Adds `ApiStoreProduct`, a product backed by Superwall API data, used for custom store products.

## Breaking Changes
- System back presses are now forwarded into the paywall as a `back_button_input` message instead of dismissing it directly: multi-page flows navigate back one page, and paywalls with nowhere to go back (root page, single page) close themselves through the standard manual-close path (`Declined`/`ManualClose`) — so single-page paywalls dismiss the same as before, from the app's perspective. When `reroute_back_button` is enabled in Paywall settings, the `PaywallOptions.onBackPressed` app callback keeps first refusal before the press is forwarded. Paywalls built on runtimes that predate `back_button_input` will ignore the press; make sure paywalls are re-published on a current runtime.
- Removes the deprecated `SuperwallBillingFlowParams.Builder.setSkuDetails(SkuDetails)`. Billing Library 9 removes `SkuDetails` entirely, so this method can no longer exist. Use `setProductDetailsParamsList(...)` with `ProductDetails` instead.
- Removes the unused internal `com.superwall.sdk.billing.SWProduct`, which wrapped the now-removed `SkuDetails`.
- Internal purchase-history queries now resolve current purchases via `QueryPurchasesParams` — Billing Library 9 removes the purchase-history APIs (`queryPurchaseHistoryAsync`, `QueryPurchaseHistoryParams`).
- **Impact:** if your app still calls the removed Billing Library APIs (`SkuDetails`, `SkuDetailsParams`, `querySkuDetailsAsync`, `queryPurchaseHistoryAsync`, `BillingClient.SkuType`, or the no-arg `enablePendingPurchases()`), it will no longer compile once it picks up Billing 9 through this SDK. Migrate those call sites to the `ProductDetails` APIs before upgrading; the [migration guide](https://developer.android.com/google/play/billing/migrate-gpblv9) has a mapping of every removed API to its replacement.
- **Please test your billing and purchasing flows before shipping this upgrade.** Because the Billing Library is resolved to a single version across your app, upgrading Superwall also upgrades Billing for everything else that depends on it. If you use Google Play Billing directly, or another subscription provider such as RevenueCat, Adapty or Qonversion, make sure that provider's SDK supports Billing 9 and run through purchase, restore and subscription-status flows end to end.

## ⚠️ Minimum SDK version raised to 23

Google Play Billing Library 9 requires Android 6.0 (API 23), so the SDK's `minSdk` is now **23** (previously 21). If your app's `minSdk` is below 23, you'll need to raise it to pick up this release — devices on Android 5.x will no longer receive app updates that include this SDK version.

## Deprecations

- Deprecates `PurchaseController.purchase(activity, productDetails, basePlanId, offerId)` in favor of the `StoreProduct`-based method above. Existing implementations keep working unchanged — the new method's default implementation routes Google Play purchases to the deprecated one — but purchasing custom store products requires implementing the new method.

## 2.7.23

## Fixes
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {

defaultConfig {
applicationId = "com.superwall.superapp"
minSdk = 22
minSdk = 23
targetSdk = 34
versionCode = 2
versionName = "1.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.superwall.superapp.purchase

import android.app.Activity
import android.content.Context
import android.util.Log
import com.android.billingclient.api.ProductDetails
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.LogLevel
Expand All @@ -27,11 +28,14 @@ import com.superwall.sdk.delegate.RestorationResult
import com.superwall.sdk.delegate.subscription_controller.PurchaseController
import com.superwall.sdk.models.entitlements.Entitlement
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import com.superwall.sdk.store.Entitlements
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.String
import kotlin.collections.mutableListOf

// Extension function to convert callback to suspend function
suspend fun Purchases.awaitProducts(productIds: List<String>): List<StoreProduct> {
Expand Down Expand Up @@ -115,8 +119,10 @@ class RevenueCatPurchaseController(
UpdatedCustomerInfoListener {
private var superwallCustomerInfoJob: Job? = null
private val scope = CoroutineScope(Dispatchers.Main)
val purchased: MutableList<String>

init {
purchased = mutableListOf<String>()
Purchases.logLevel = LogLevel.DEBUG
Purchases.configure(
PurchasesConfiguration
Expand Down Expand Up @@ -175,7 +181,7 @@ class RevenueCatPurchaseController(
emptySet()
}

val allEntitlements = rcEntitlements + webEntitlements
val allEntitlements = rcEntitlements + webEntitlements + purchased.map { Entitlement(it) }

if (allEntitlements.isNotEmpty()) {
setSubscriptionStatus(SubscriptionStatus.Active(allEntitlements))
Expand All @@ -189,10 +195,27 @@ class RevenueCatPurchaseController(
*/
override suspend fun purchase(
activity: Activity,
productDetails: ProductDetails,
product: com.superwall.sdk.store.abstractions.product.StoreProduct,
basePlanId: String?,
offerId: String?,
): PurchaseResult {
// Custom store products aren't on Google Play — fulfill them through your own
// payment flow and grant the entitlements yourself.
if (product.isCustomProduct) {
// Example only, save the entitlements to storage here to persist
purchased.add("custom_entitlement")

// Sync your subscription status
Purchases.sharedInstance.getCustomerInfoWith {
updateSubscriptionStatus(it)
}
return PurchaseResult.Purchased()
}

val productDetails =
product.rawStoreProduct?.underlyingProductDetails
?: return PurchaseResult.Failed("Missing product details for ${product.fullIdentifier}")

// Find products matching productId from RevenueCat
val products = Purchases.sharedInstance.awaitProducts(listOf(productDetails.productId))
// Choose the product which matches the given base plan.
Expand Down
2 changes: 1 addition & 1 deletion example/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ android {

defaultConfig {
applicationId = "com.superwall.superapp"
minSdk = 22
minSdk = 23
targetSdk = 34
versionCode = 1
versionName = "1.0.0"
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[versions]
billing_version = "8.0.0"
billing_version = "9.1.0"
browser_version = "1.8.0"
gradle_plugin_version = "8.6.1"
jna_version = "5.14.0@aar"
kotlinxCoroutinesGuavaVersion = "1.9.0"
leakcanaryAndroidVersion = "2.14"
lifecycleProcessVersion = "2.8.5"
orchestratorVersion = "1.5.0"
revenue_cat_version = "9.2.0"
revenue_cat_version = "10.14.1"
compose_version = "2024.12.01"
kotlinx_serialization_json_version = "1.7.2"
activity_compose_version = "1.9.3"
Expand Down
2 changes: 1 addition & 1 deletion superwall-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ android {
compileSdk = 35

defaultConfig {
minSdk = 22
minSdk = 23

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
2 changes: 1 addition & 1 deletion superwall/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ android {
namespace = "com.superwall.sdk"

defaultConfig {
minSdkVersion(21)
minSdkVersion(23)
targetSdkVersion(33)

aarMetadata {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class TrackingLogicTest {

override suspend fun makeStoreTransaction(transaction: Purchase): StoreTransaction = mockk()

override suspend fun makeStoreTransaction(
customTransactionId: String,
productIdentifier: String,
purchaseDate: java.util.Date,
): StoreTransaction = mockk()

override suspend fun activeProductIds(): List<String> = emptyList()

override suspend fun makeIdentityManager(): IdentityManager = mockk()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -400,7 +400,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -525,7 +525,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -581,7 +581,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -631,7 +631,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -676,7 +676,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -722,7 +722,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
any(),
any(),
)
Expand Down Expand Up @@ -911,7 +911,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -957,7 +957,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
any(),
any(),
)
Expand Down Expand Up @@ -1002,7 +1002,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
any(),
any(),
)
Expand Down Expand Up @@ -1050,7 +1050,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down Expand Up @@ -1098,7 +1098,7 @@ class TransactionManagerTest {
coEvery {
purchaseController.purchase(
any(),
any(),
any<StoreProduct>(),
"basePlan",
any(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ package com.superwall.sdk.billing

import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchaseHistoryParams
import com.android.billingclient.api.QueryPurchasesParams

internal fun @receiver:BillingClient.ProductType String.buildQueryPurchaseHistoryParams(): QueryPurchaseHistoryParams? =
internal fun @receiver:BillingClient.ProductType String.buildQueryPurchaseHistoryParams(): QueryPurchasesParams? =
when (this) {
BillingClient.ProductType.INAPP,
BillingClient.ProductType.SUBS,
-> QueryPurchaseHistoryParams.newBuilder().setProductType(this).build()
-> QueryPurchasesParams.newBuilder().setProductType(this).build()
else -> null
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.superwall.sdk.billing.observer

import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.SkuDetails

class SuperwallBillingFlowParams private constructor(
internal val params: BillingFlowParams,
Expand Down Expand Up @@ -39,12 +38,6 @@ class SuperwallBillingFlowParams private constructor(
builder.setProductDetailsParamsList(productDetailsParamsList.map { it.toOriginal() })
}

@Deprecated("Use setProductDetailsParamsList instead")
fun setSkuDetails(skuDetails: SkuDetails): Builder =
apply {
builder.setSkuDetails(skuDetails)
}

fun setSubscriptionUpdateParams(params: SubscriptionUpdateParams): Builder =
apply {
builder.setSubscriptionUpdateParams(params.toOriginal())
Expand Down
Loading
Loading