Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 46 additions & 0 deletions content/docs/android/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,52 @@ title: "Changelog"
description: "Release notes for the Superwall Android SDK"
---

## 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. ⚠️
- 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; this means that previously existing Multi-page paywalls, if republished, will now navigate back inside the paywall once republished.
- 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 Purchasely, make sure that provider's SDK supports Billing 9 and run through purchase, restore and subscription-status flows end to end.
- **If you use other subscription management libraries (RC, Purchasely, Adapty) and they do not support Play Billing 9:** you can pin the Billing Client version to 8 in the following way.

Add this to your app module's `build.gradle.kts`, outside the `android { }` block:

```kotlin
configurations.all {
resolutionStrategy.force("com.android.billingclient:billing:8.3.0")
}
```

Or in Groovy (`build.gradle`):

```groovy
configurations.all {
resolutionStrategy.force 'com.android.billingclient:billing:8.3.0'
}
```

You can confirm which version you end up with via `./gradlew :app:dependencies --configuration releaseRuntimeClasspath` — look for `com.android.billingclient:billing:9.1.0 -> 8.3.0`. Billing 8 still satisfies Google's August 31, 2026 requirement, so this is a safe interim state, and no Superwall functionality is lost: the SDK works on both 8.x and 9.x.

Note that a `-dontwarn com.android.billingclient.api.QueryPurchaseHistoryParams` ProGuard rule is **not** a fix. It silences the R8 error and lets the build through, but then throws at runtime.

## ⚠️ 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 content/docs/android/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ If you have feedback on any of our docs, please leave a rating and message at th

If you have any issues with the SDK, please [open an issue on GitHub](https://github.com/superwall/superwall-android/issues).

<SdkLatestVersion version="2.7.23" repoUrl="https://github.com/superwall/Superwall-Android" />
<SdkLatestVersion version="2.8.0" repoUrl="https://github.com/superwall/Superwall-Android" />
10 changes: 7 additions & 3 deletions content/docs/android/quickstart/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ can find the [latest release here](https://github.com/superwall/Superwall-Androi
<CodeGroup>

```gradle build.gradle
implementation "com.superwall.sdk:superwall-android:2.7.23"
implementation "com.superwall.sdk:superwall-android:2.8.0"
```

```kotlin build.gradle.kts
implementation("com.superwall.sdk:superwall-android:2.7.23")
implementation("com.superwall.sdk:superwall-android:2.8.0")
```

```toml libs.version.toml
[libraries]
superwall-android = { group = "com.superwall.sdk", name = "superwall-android", version = "2.7.23" }
superwall-android = { group = "com.superwall.sdk", name = "superwall-android", version = "2.8.0" }

// And in your build.gradle.kts
dependencies {
Expand All @@ -39,6 +39,10 @@ dependencies {

</CodeGroup>

<Warning>
Starting with `2.8.0`, the SDK requires **`minSdk` 23** (previously 21) because it upgrades the underlying Google Play Billing Library to 9.1.0. If your app's `minSdk` is below 23, raise it before upgrading. Because Billing is resolved to a single version across your app, this also upgrades Billing for any other library that depends on it (RevenueCat, Adapty, Purchasely, etc.) — confirm those providers support Billing 9 before shipping. See the [changelog](/android/changelog) for the full breaking-changes list and a workaround if you need to pin Billing to version 8.
</Warning>
Comment on lines +42 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the Unity Android dependency guidance

For fresh Unity Android builds, content/docs/unity/quickstart/install.mdx:54-55 still says the package pulls superwall-android:2.+ alongside Billing 8.0.0. That dynamic SDK selector now resolves 2.8.0, whose transitive Billing 9.1.0 dependency wins Gradle conflict resolution, so Unity users actually receive the same Billing upgrade described here while their installation page continues to promise Billing 8 and omits the compatibility warning. Update the Unity dependency list and add the relevant Billing 9 migration guidance there as part of this release sync.

Useful? React with 👍 / 👎.


Make sure to run **Sync Now** to force Android Studio to update.

<Frame>![](/images/installation/gradle-sync-now.png) </Frame>
Expand Down
40 changes: 22 additions & 18 deletions content/docs/android/quickstart/tracking-subscription-state.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class MainActivity : AppCompatActivity() {
The `SubscriptionStatus` sealed class has three possible states:

- `SubscriptionStatus.Unknown` - Status is not yet determined
- `SubscriptionStatus.Active(Set<String>)` - User has active entitlements (set of entitlement identifiers)
- `SubscriptionStatus.Active(Set<Entitlement>)` - User has active entitlements
- `SubscriptionStatus.Inactive` - User has no active entitlements

## Observing subscription status changes
Expand Down Expand Up @@ -149,10 +149,10 @@ If your app has multiple subscription tiers (e.g., Bronze, Silver, Gold), you ca
val status = Superwall.instance.subscriptionStatus.value
when (status) {
is SubscriptionStatus.Active -> {
if (status.entitlements.contains("gold")) {
if (status.entitlements.any { it.id == "gold" }) {
// Show gold-tier features
showGoldFeatures()
} else if (status.entitlements.contains("silver")) {
} else if (status.entitlements.any { it.id == "silver" }) {
// Show silver-tier features
showSilverFeatures()
}
Expand All @@ -170,23 +170,31 @@ class RevenueCatPurchaseController : PurchaseController {

override suspend fun purchase(
activity: Activity,
product: StoreProduct
product: StoreProduct,
basePlanId: String?,
offerId: String?
): PurchaseResult {
return try {
val result = Purchases.sharedInstance.purchase(activity, product.sku)
if (product.isCustomProduct) {
return PurchaseResult.Failed(
"Handle custom products with your own payment flow"
)
}

val productDetails = product.rawStoreProduct?.underlyingProductDetails
?: return PurchaseResult.Failed("Missing Google Play product details")
val result = Purchases.sharedInstance.purchase(activity, productDetails.productId)

// Update Superwall subscription status based on RevenueCat result
if (result.isSuccessful) {
val entitlements = result.customerInfo.entitlements.active.keys
Superwall.instance.setSubscriptionStatus(
SubscriptionStatus.Active(entitlements)
)
PurchaseResult.Purchased
Superwall.instance.setSubscriptionStatus(*entitlements.toTypedArray())
PurchaseResult.Purchased()
} else {
PurchaseResult.Failed(Exception("Purchase failed"))
PurchaseResult.Failed("Purchase failed")
}
} catch (e: Exception) {
PurchaseResult.Failed(e)
PurchaseResult.Failed(e.message ?: "Purchase failed")
}
}

Expand All @@ -196,14 +204,12 @@ class RevenueCatPurchaseController : PurchaseController {
val activeEntitlements = customerInfo.entitlements.active.keys

if (activeEntitlements.isNotEmpty()) {
Superwall.instance.setSubscriptionStatus(
SubscriptionStatus.Active(activeEntitlements)
)
Superwall.instance.setSubscriptionStatus(*activeEntitlements.toTypedArray())
} else {
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
}

RestorationResult.Restored
RestorationResult.Restored()
} catch (e: Exception) {
RestorationResult.Failed(e)
}
Expand All @@ -221,9 +227,7 @@ class SubscriptionManager {
val activeEntitlements = customerInfo.entitlements.active.keys

if (activeEntitlements.isNotEmpty()) {
Superwall.instance.setSubscriptionStatus(
SubscriptionStatus.Active(activeEntitlements)
)
Superwall.instance.setSubscriptionStatus(*activeEntitlements.toTypedArray())
} else {
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
}
Expand Down
6 changes: 4 additions & 2 deletions content/docs/android/sdk-reference/PaywallOptions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class PaywallOptions {
},
onBackPressed: {
type: "((PaywallInfo?) -> Boolean)?",
description: "Callback invoked when back button is pressed (requires `reroute_back_button` enabled in paywall settings). Return `true` to consume the back press, `false` to use SDK default behavior.",
description: "Callback invoked when back button is pressed (requires `reroute_back_button` enabled in paywall settings). Return `true` to consume the press. Return `false` to forward it to the paywall as `back_button_input`; the paywall navigates back one page when possible or closes through its standard manual-close path. Paywalls built on runtimes before `back_button_input` ignore the press, so republish them to use this behavior.",
default: "null",
},
}}
Expand Down Expand Up @@ -150,7 +150,9 @@ val paywallOptions = PaywallOptions().apply {
timeoutAfter = null
onBackPressed = { paywallInfo ->
// Custom back button handling
// Return true to consume the back press, false to use SDK default
// Return true to consume the back press. Returning false forwards it to
// the paywall, which navigates back one page or closes itself. Republish
// paywalls built before back_button_input support to use this behavior.
false
}
}
Expand Down
45 changes: 33 additions & 12 deletions content/docs/android/sdk-reference/PurchaseController.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,43 @@ Use this interface only if you want complete control over purchase handling, suc
interface PurchaseController {
suspend fun purchase(
activity: Activity,
product: StoreProduct
product: StoreProduct,
basePlanId: String?,
offerId: String?
): PurchaseResult

suspend fun restorePurchases(): RestorationResult
}
```

```java
// Java
public interface PurchaseController {
CompletableFuture<PurchaseResult> purchase(
Activity activity,
StoreProduct product
// Java uses the callback-based PurchaseControllerJava interface.
public interface PurchaseControllerJava {
void purchase(
ProductDetails productDetails,
String basePlanId,
String offerId,
kotlin.jvm.functions.Function1<? super PurchaseResult, kotlin.Unit> completion
);

void restorePurchases(
kotlin.jvm.functions.Function2<? super RestorationResult, ? super Throwable, kotlin.Unit> completion
);

CompletableFuture<RestorationResult> restorePurchases();
}
```

<Info>
Starting in `2.8.0`, Kotlin's `PurchaseController.purchase()` receives a `StoreProduct` (instead of the Billing Library's `ProductDetails`), which also supports [custom store products](#custom-store-products). For Google Play products, access the underlying `ProductDetails` through `product.rawStoreProduct?.underlyingProductDetails`. The default implementation of this method routes Google Play products to the older `purchase(activity, productDetails, basePlanId, offerId)` overload, so existing implementations of that overload keep working unchanged. That older overload is now deprecated; implement the `StoreProduct`-based method above going forward, and implement it directly if you need to support custom store products.
</Info>

`PurchaseControllerJava` remains a callback-based API that receives `ProductDetails`. It does not support custom store products.

## Parameters
<TypeTable
type={{
purchase: {
type: "activity: Activity, product: StoreProduct",
description: "Called when user initiates purchasing. Implement your purchase logic here. Activity is needed for Google Play Billing. Returns `PurchaseResult`.",
type: "activity: Activity, product: StoreProduct, basePlanId: String?, offerId: String?",
description: "Called when user initiates purchasing. Implement your purchase logic here. Activity is needed for Google Play Billing. `basePlanId` and `offerId` are optional identifiers for the base plan/offer being purchased. Returns `PurchaseResult`.",
required: true,
},
restorePurchases: {
Expand All @@ -56,11 +68,20 @@ public interface PurchaseController {


## Returns / State
- `purchase()` returns a `PurchaseResult` (`.Purchased`, `.Failed(Throwable)`, `.Cancelled`, or `.Pending`)
- `restorePurchases()` returns a `RestorationResult` (`.Restored` or `.Failed(Throwable?)`)
- `purchase()` returns a `PurchaseResult` (`.Purchased()`, `.Failed(errorMessage: String)`, `.Cancelled()`, or `.Pending()`)
- `restorePurchases()` returns a `RestorationResult` (`.Restored()` or `.Failed(Throwable?)`)

When using a PurchaseController, you must also manage [`subscriptionStatus`](/android/sdk-reference/subscriptionStatus) yourself.

## Custom store products

Products configured on a custom store in the Superwall dashboard (e.g. Stripe or your own payment backend) can be attached to paywalls. Their metadata (price, subscription period, trial) is fetched from the Superwall API instead of Google Play, and purchases for them are routed through your `PurchaseController`, bypassing Google Play Billing entirely.

- Kotlin `PurchaseController` implementations can check `product.isCustomProduct` in `purchase()` to detect a custom product and fulfill and persist it through their own payment flow. `PurchaseControllerJava` does not support custom store products.
- After your billing system grants the entitlement, call `Superwall.instance.setSubscriptionStatus(...)` to synchronize that entitlement state with Superwall. The SDK does not fulfill or persist custom purchases automatically.
- `StoreProduct.customTransactionId` is an SDK-generated identifier you can use as the original transaction identifier in your own analytics.
- Requires configuring the SDK with a `PurchaseController`.

## Usage

For implementation examples and detailed guidance, see [Using RevenueCat](/android/guides/using-revenuecat).
Loading
Loading