From b55a60ae30b93b299ceb7c2df5d63a2a73488d5e Mon Sep 17 00:00:00 2001 From: Lever Date: Fri, 31 Jul 2026 16:19:45 +0000 Subject: [PATCH 1/3] feat(android): update for 2.8.0 Bumps documented Android SDK version from 2.7.23 to 2.8.0. - content/docs/android/index.mdx: SdkLatestVersion 2.7.23 -> 2.8.0 - content/docs/android/sdk-reference/index.mdx: SdkLatestVersion 2.7.23 -> 2.8.0 - content/docs/android/quickstart/install.mdx: Gradle/KTS/TOML install snippets bumped to 2.8.0; added a Warning about the minSdk 21 -> 23 requirement introduced by the Play Billing Library 9.1.0 upgrade. - content/docs/android/changelog.mdx: inserted the upstream 2.8.0 entry verbatim above 2.7.23, preserving all historical entries (Enhancements, Breaking Changes, minSdk notice, Deprecations). - content/docs/android/sdk-reference/PurchaseController.mdx: updated the purchase() signature to include the basePlanId/offerId params it has taken since before this release (was previously stale/incomplete in the docs), documented the new default StoreProduct-based overload introduced in 2.8.0, and added a "Custom store products" section covering product.isCustomProduct / customTransactionId per the upstream changelog. Co-Authored-By: Claude Sonnet 5 --- content/docs/android/changelog.mdx | 46 +++++++++++++++++++ content/docs/android/index.mdx | 2 +- content/docs/android/quickstart/install.mdx | 10 ++-- .../sdk-reference/PurchaseController.mdx | 25 ++++++++-- content/docs/android/sdk-reference/index.mdx | 2 +- 5 files changed, 76 insertions(+), 9 deletions(-) diff --git a/content/docs/android/changelog.mdx b/content/docs/android/changelog.mdx index 54fd5b63..d67db2a3 100644 --- a/content/docs/android/changelog.mdx +++ b/content/docs/android/changelog.mdx @@ -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 diff --git a/content/docs/android/index.mdx b/content/docs/android/index.mdx index 740392bb..597aa726 100644 --- a/content/docs/android/index.mdx +++ b/content/docs/android/index.mdx @@ -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). - + diff --git a/content/docs/android/quickstart/install.mdx b/content/docs/android/quickstart/install.mdx index dd344b0d..b5d32ac6 100644 --- a/content/docs/android/quickstart/install.mdx +++ b/content/docs/android/quickstart/install.mdx @@ -20,16 +20,16 @@ can find the [latest release here](https://github.com/superwall/Superwall-Androi ```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 { @@ -39,6 +39,10 @@ dependencies { + +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. + + Make sure to run **Sync Now** to force Android Studio to update. ![](/images/installation/gradle-sync-now.png) diff --git a/content/docs/android/sdk-reference/PurchaseController.mdx b/content/docs/android/sdk-reference/PurchaseController.mdx index f75898cd..63c92b52 100644 --- a/content/docs/android/sdk-reference/PurchaseController.mdx +++ b/content/docs/android/sdk-reference/PurchaseController.mdx @@ -19,7 +19,9 @@ 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 @@ -31,19 +33,25 @@ interface PurchaseController { public interface PurchaseController { CompletableFuture purchase( Activity activity, - StoreProduct product + StoreProduct product, + String basePlanId, + String offerId ); CompletableFuture restorePurchases(); } ``` + +Starting in `2.8.0`, `purchase()` receives a `StoreProduct` (instead of the Billing Library's `ProductDetails`), which also supports [custom store products](#custom-store-products). For Google Play products, the underlying `ProductDetails` is available via `product.rawStoreProduct`. 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. + + ## Parameters + From 0880d06a669dbd31147cc2c759d33ff7badf7881 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Mon, 3 Aug 2026 11:35:03 -0700 Subject: [PATCH 2/3] fix(android): correct 2.8.0 documentation --- content/docs/android/changelog.mdx | 2 +- .../tracking-subscription-state.mdx | 40 +++++++------- .../android/sdk-reference/PaywallOptions.mdx | 5 +- .../sdk-reference/PurchaseController.mdx | 28 +++++----- .../advanced/setSubscriptionStatus.mdx | 52 +++++++++---------- .../sdk-reference/subscriptionStatus.mdx | 20 +++---- content/docs/using-revenuecat.mdx | 14 ++++- content/shared/advanced-configuration.mdx | 19 +++++-- .../viewing-purchased-products.mdx | 10 +++- content/shared/using-revenuecat.mdx | 14 ++++- 10 files changed, 120 insertions(+), 84 deletions(-) diff --git a/content/docs/android/changelog.mdx b/content/docs/android/changelog.mdx index d67db2a3..773cbf40 100644 --- a/content/docs/android/changelog.mdx +++ b/content/docs/android/changelog.mdx @@ -10,7 +10,7 @@ description: "Release notes for the Superwall Android SDK" - 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. +- 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. diff --git a/content/docs/android/quickstart/tracking-subscription-state.mdx b/content/docs/android/quickstart/tracking-subscription-state.mdx index 1002c7ae..4a8d1447 100644 --- a/content/docs/android/quickstart/tracking-subscription-state.mdx +++ b/content/docs/android/quickstart/tracking-subscription-state.mdx @@ -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)` - User has active entitlements (set of entitlement identifiers) +- `SubscriptionStatus.Active(Set)` - User has active entitlements - `SubscriptionStatus.Inactive` - User has no active entitlements ## Observing subscription status changes @@ -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() } @@ -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") } } @@ -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) } @@ -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) } diff --git a/content/docs/android/sdk-reference/PaywallOptions.mdx b/content/docs/android/sdk-reference/PaywallOptions.mdx index 6c72010f..be445d05 100644 --- a/content/docs/android/sdk-reference/PaywallOptions.mdx +++ b/content/docs/android/sdk-reference/PaywallOptions.mdx @@ -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.", default: "null", }, }} @@ -150,7 +150,8 @@ 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. false } } diff --git a/content/docs/android/sdk-reference/PurchaseController.mdx b/content/docs/android/sdk-reference/PurchaseController.mdx index 63c92b52..d9c7bd24 100644 --- a/content/docs/android/sdk-reference/PurchaseController.mdx +++ b/content/docs/android/sdk-reference/PurchaseController.mdx @@ -29,23 +29,27 @@ interface PurchaseController { ``` ```java -// Java -public interface PurchaseController { - CompletableFuture purchase( - Activity activity, - StoreProduct product, +// Java uses the callback-based PurchaseControllerJava interface. +public interface PurchaseControllerJava { + void purchase( + ProductDetails productDetails, String basePlanId, - String offerId + String offerId, + kotlin.jvm.functions.Function1 completion + ); + + void restorePurchases( + kotlin.jvm.functions.Function2 completion ); - - CompletableFuture restorePurchases(); } ``` -Starting in `2.8.0`, `purchase()` receives a `StoreProduct` (instead of the Billing Library's `ProductDetails`), which also supports [custom store products](#custom-store-products). For Google Play products, the underlying `ProductDetails` is available via `product.rawStoreProduct`. 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. +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, the underlying `ProductDetails` is available via `product.rawStoreProduct`. 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. +`PurchaseControllerJava` remains a callback-based API that receives `ProductDetails`. It does not support custom store products. + ## Parameters )`, or `SubscriptionStatus.Inactive`. You can also pass entitlement identifier strings directly to the `setSubscriptionStatus(vararg entitlements: String)` overload.", required: true, }, }} @@ -44,9 +44,7 @@ This function returns `Unit`. The new status will be reflected in the [`subscrip Set active subscription with entitlements: ```kotlin // User purchased premium subscription -Superwall.instance.setSubscriptionStatus( - SubscriptionStatus.Active(setOf("premium", "pro_features")) -) +Superwall.instance.setSubscriptionStatus("premium", "pro_features") ``` Set inactive subscription: @@ -67,23 +65,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") } } @@ -93,14 +99,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) } @@ -113,14 +117,11 @@ Listen for external subscription changes: class SubscriptionManager { fun onSubscriptionStatusChanged(isActive: Boolean, entitlements: Set) { - val status = if (isActive) { - SubscriptionStatus.Active(entitlements) + if (isActive) { + Superwall.instance.setSubscriptionStatus(*entitlements.toTypedArray()) } else { - SubscriptionStatus.Inactive + Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive) } - - // Update Superwall whenever subscription status changes externally - Superwall.instance.setSubscriptionStatus(status) } } ``` @@ -128,13 +129,10 @@ class SubscriptionManager { Java usage: ```java // Set active subscription -Set entitlements = Set.of("premium", "pro_features"); -Superwall.getInstance().setSubscriptionStatus( - new SubscriptionStatus.Active(entitlements) -); +Superwall.getInstance().setSubscriptionStatus("premium", "pro_features"); // Set inactive subscription Superwall.getInstance().setSubscriptionStatus( SubscriptionStatus.Inactive.INSTANCE ); -``` \ No newline at end of file +``` diff --git a/content/docs/android/sdk-reference/subscriptionStatus.mdx b/content/docs/android/sdk-reference/subscriptionStatus.mdx index fce25c59..c8cb47d5 100644 --- a/content/docs/android/sdk-reference/subscriptionStatus.mdx +++ b/content/docs/android/sdk-reference/subscriptionStatus.mdx @@ -31,7 +31,7 @@ public void setSubscriptionStatus(SubscriptionStatus status) ## Parameters This property accepts a `SubscriptionStatus` sealed class value: - `SubscriptionStatus.Unknown` - Status is not yet determined -- `SubscriptionStatus.Active(Set)` - User has active entitlements (set of entitlement identifiers) +- `SubscriptionStatus.Active(Set)` - User has active entitlements - `SubscriptionStatus.Inactive` - User has no active entitlements ## Returns / State @@ -41,9 +41,7 @@ Returns a `StateFlow` that emits the current subscription st Set subscription status (when using PurchaseController): ```kotlin -Superwall.instance.setSubscriptionStatus( - SubscriptionStatus.Active(setOf("premium", "pro_features")) -) +Superwall.instance.setSubscriptionStatus("premium", "pro_features") Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive) ``` @@ -112,14 +110,8 @@ Java usage: SubscriptionStatus status = Superwall.getInstance() .getSubscriptionStatus().getValue(); -// Observe changes -Superwall.getInstance().getSubscriptionStatus() - .observe(this, status -> { - updateUI(status); - }); - // Set status (when using PurchaseController) -Superwall.getInstance().setSubscriptionStatus( - new SubscriptionStatus.Active(Set.of("premium")) -); -``` \ No newline at end of file +Superwall.getInstance().setSubscriptionStatus("premium"); +``` + +To observe status changes from Java, set a `SuperwallDelegateJava` and implement its `subscriptionStatusDidChange(from, to)` callback. diff --git a/content/docs/using-revenuecat.mdx b/content/docs/using-revenuecat.mdx index b8efca82..40b17909 100644 --- a/content/docs/using-revenuecat.mdx +++ b/content/docs/using-revenuecat.mdx @@ -102,7 +102,6 @@ package com.superwall.superapp import android.app.Activity import android.content.Context -import com.android.billingclient.api.ProductDetails import com.revenuecat.purchases.CustomerInfo import com.revenuecat.purchases.LogLevel import com.revenuecat.purchases.ProductType @@ -127,6 +126,7 @@ 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.abstractions.product.StoreProduct as SuperwallStoreProduct import kotlinx.coroutines.CompletableDeferred suspend fun Purchases.awaitProducts(productIds: List): List { @@ -262,10 +262,20 @@ class RevenueCatPurchaseController( */ override suspend fun purchase( activity: Activity, - productDetails: ProductDetails, + product: SuperwallStoreProduct, basePlanId: String?, offerId: String?, ): PurchaseResult { + // Custom store products are not available through RevenueCat. Route these + // through your own payment flow and update Superwall's subscription status. + if (product.isCustomProduct) { + return PurchaseResult.Failed("Custom products require an external payment flow") + } + + val productDetails = + product.rawStoreProduct?.underlyingProductDetails + ?: return PurchaseResult.Failed("Missing Google Play product details") + // Find products matching productId from RevenueCat val products = Purchases.sharedInstance.awaitProducts(listOf(productDetails.productId)) // Choose the product which matches the given base plan. diff --git a/content/shared/advanced-configuration.mdx b/content/shared/advanced-configuration.mdx index 79a6b513..d7389186 100644 --- a/content/shared/advanced-configuration.mdx +++ b/content/shared/advanced-configuration.mdx @@ -189,14 +189,23 @@ class MyPurchaseController(val context: Context): PurchaseController { // 1 override suspend fun purchase( activity: Activity, - productDetails: ProductDetails, + product: StoreProduct, basePlanId: String?, offerId: String? ): PurchaseResult { + if (product.isCustomProduct) { + // TODO: Purchase custom products with your own billing system, then + // update Superwall.instance.setSubscriptionStatus(...). + return PurchaseResult.Failed("Custom product handling is not implemented") + } + + val productDetails = product.rawStoreProduct?.underlyingProductDetails + ?: return PurchaseResult.Failed("Missing Google Play product details") + // TODO // ---- - // Purchase via GoogleBilling, RevenueCat, Qonversion or however + // Purchase productDetails via GoogleBilling, RevenueCat, Qonversion or however // you like and return a valid PurchaseResult return PurchaseResult.Purchased() @@ -207,9 +216,9 @@ class MyPurchaseController(val context: Context): PurchaseController { // TODO // ---- - // Restore purchases and return true if successful. + // Restore purchases and return RestorationResult.Restored() if successful. - return RestorationResult.Success() + return RestorationResult.Restored() } } ``` @@ -470,7 +479,7 @@ myService.subscriptionStatusDidChange { if (it.hasActiveSubscription) { Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Active(entitlements)) } else { - Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive(entitlements)) + Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive) } } ``` diff --git a/content/shared/showing-paywalls/viewing-purchased-products.mdx b/content/shared/showing-paywalls/viewing-purchased-products.mdx index e663fd51..67f1f55d 100644 --- a/content/shared/showing-paywalls/viewing-purchased-products.mdx +++ b/content/shared/showing-paywalls/viewing-purchased-products.mdx @@ -496,10 +496,18 @@ NS_ASSUME_NONNULL_END class MyPurchaseController(val context: Context): PurchaseController { override suspend fun purchase( activity: Activity, - productDetails: ProductDetails, + product: StoreProduct, basePlanId: String?, offerId: String? ): PurchaseResult { + if (product.isCustomProduct) { + // Purchase custom products with your own billing system, then update + // Superwall.instance.setSubscriptionStatus(...). + return PurchaseResult.Failed("Custom product handling is not implemented") + } + + val productDetails = product.rawStoreProduct?.underlyingProductDetails + ?: return PurchaseResult.Failed("Missing Google Play product details") println("Kicking off purchase of $basePlanId") return PurchaseResult.Purchased() } diff --git a/content/shared/using-revenuecat.mdx b/content/shared/using-revenuecat.mdx index 3c5b2cb2..9e6595c0 100644 --- a/content/shared/using-revenuecat.mdx +++ b/content/shared/using-revenuecat.mdx @@ -105,7 +105,6 @@ package com.superwall.superapp import android.app.Activity import android.content.Context -import com.android.billingclient.api.ProductDetails import com.revenuecat.purchases.CustomerInfo import com.revenuecat.purchases.LogLevel import com.revenuecat.purchases.ProductType @@ -130,6 +129,7 @@ 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.abstractions.product.StoreProduct as SuperwallStoreProduct import kotlinx.coroutines.CompletableDeferred suspend fun Purchases.awaitProducts(productIds: List): List { @@ -265,10 +265,20 @@ class RevenueCatPurchaseController( */ override suspend fun purchase( activity: Activity, - productDetails: ProductDetails, + product: SuperwallStoreProduct, basePlanId: String?, offerId: String?, ): PurchaseResult { + // Custom store products are not available through RevenueCat. Route these + // through your own payment flow and update Superwall's subscription status. + if (product.isCustomProduct) { + return PurchaseResult.Failed("Custom products require an external payment flow") + } + + val productDetails = + product.rawStoreProduct?.underlyingProductDetails + ?: return PurchaseResult.Failed("Missing Google Play product details") + // Find products matching productId from RevenueCat val products = Purchases.sharedInstance.awaitProducts(listOf(productDetails.productId)) // Choose the product which matches the given base plan. From 50bc76d7f256852a73dcab4f07f199f0f7165755 Mon Sep 17 00:00:00 2001 From: Duncan Crawbuck Date: Mon, 3 Aug 2026 12:38:04 -0700 Subject: [PATCH 3/3] fix(docs): address android sdk review notes --- content/docs/android/sdk-reference/PaywallOptions.mdx | 5 +++-- .../docs/android/sdk-reference/PurchaseController.mdx | 6 +++--- .../sdk-reference/advanced/setSubscriptionStatus.mdx | 2 ++ content/docs/unity/quickstart/install.mdx | 9 ++++++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/content/docs/android/sdk-reference/PaywallOptions.mdx b/content/docs/android/sdk-reference/PaywallOptions.mdx index be445d05..309015fe 100644 --- a/content/docs/android/sdk-reference/PaywallOptions.mdx +++ b/content/docs/android/sdk-reference/PaywallOptions.mdx @@ -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 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.", + 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", }, }} @@ -151,7 +151,8 @@ val paywallOptions = PaywallOptions().apply { onBackPressed = { paywallInfo -> // Custom back button handling // Return true to consume the back press. Returning false forwards it to - // the paywall, which navigates back one page or closes itself. + // the paywall, which navigates back one page or closes itself. Republish + // paywalls built before back_button_input support to use this behavior. false } } diff --git a/content/docs/android/sdk-reference/PurchaseController.mdx b/content/docs/android/sdk-reference/PurchaseController.mdx index d9c7bd24..eebb0774 100644 --- a/content/docs/android/sdk-reference/PurchaseController.mdx +++ b/content/docs/android/sdk-reference/PurchaseController.mdx @@ -45,7 +45,7 @@ public interface PurchaseControllerJava { ``` -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, the underlying `ProductDetails` is available via `product.rawStoreProduct`. 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. +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. `PurchaseControllerJava` remains a callback-based API that receives `ProductDetails`. It does not support custom store products. @@ -77,8 +77,8 @@ When using a PurchaseController, you must also manage [`subscriptionStatus`](/an 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 it via their own payment flow. `PurchaseControllerJava` does not support custom store products. -- On a successful purchase, grant the entitlement yourself by calling `Superwall.instance.setSubscriptionStatus(...)` — the SDK does not do this automatically for custom products. +- 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`. diff --git a/content/docs/android/sdk-reference/advanced/setSubscriptionStatus.mdx b/content/docs/android/sdk-reference/advanced/setSubscriptionStatus.mdx index 4ea2e528..19ff0207 100644 --- a/content/docs/android/sdk-reference/advanced/setSubscriptionStatus.mdx +++ b/content/docs/android/sdk-reference/advanced/setSubscriptionStatus.mdx @@ -17,11 +17,13 @@ Manually updates the subscription status when using a custom [`PurchaseControlle ## Signature ```kotlin fun Superwall.setSubscriptionStatus(status: SubscriptionStatus) +fun Superwall.setSubscriptionStatus(vararg entitlements: String) ``` ```java // Java public void setSubscriptionStatus(SubscriptionStatus status) +public void setSubscriptionStatus(String... entitlements) ``` ## Parameters diff --git a/content/docs/unity/quickstart/install.mdx b/content/docs/unity/quickstart/install.mdx index 8476a37a..abb4a0a4 100644 --- a/content/docs/unity/quickstart/install.mdx +++ b/content/docs/unity/quickstart/install.mdx @@ -52,7 +52,7 @@ No manual Android SDK setup is required for the default Unity Gradle export. The Gradle `.androidlib` module that declares the Superwall paywall activities and pulls: - `com.superwall.sdk:superwall-android:2.+` -- Google Play Billing `8.0.0` +- Google Play Billing `9.1.0` (through the currently resolved Superwall Android SDK) - Material Components `1.12.0` - Kotlin coroutines for Android `1.9.0` @@ -63,4 +63,11 @@ The included Android manifest adds the internet, network state, notification, an not remove the generated Superwall `.androidlib` dependency. + + The dynamic `2.+` Android dependency currently resolves to Superwall Android `2.8.0`, which requires + `minSdk` 23 and upgrades Google Play Billing to 9.1.0. If your Unity project uses another billing + library, confirm that it supports Billing 9 before shipping. See the [Android changelog](/android/changelog) + for migration guidance and a temporary Billing 8 workaround. + + After installing, continue to [Configure the SDK](/unity/quickstart/configure).