diff --git a/content/docs/android/changelog.mdx b/content/docs/android/changelog.mdx
index 54fd5b63..773cbf40 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.

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..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 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",
},
}}
@@ -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
}
}
diff --git a/content/docs/android/sdk-reference/PurchaseController.mdx b/content/docs/android/sdk-reference/PurchaseController.mdx
index f75898cd..eebb0774 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
@@ -27,23 +29,33 @@ 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,
+ kotlin.jvm.functions.Function1 super PurchaseResult, kotlin.Unit> completion
+ );
+
+ void restorePurchases(
+ kotlin.jvm.functions.Function2 super RestorationResult, ? super Throwable, kotlin.Unit> completion
);
-
- CompletableFuture restorePurchases();
}
```
+
+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.
+
## Parameters
)`, or `SubscriptionStatus.Inactive`. You can also pass entitlement identifier strings directly to the `setSubscriptionStatus(vararg entitlements: String)` overload.",
required: true,
},
}}
@@ -44,9 +46,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 +67,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 +101,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 +119,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 +131,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/index.mdx b/content/docs/android/sdk-reference/index.mdx
index d3bc0f21..ffcb6708 100644
--- a/content/docs/android/sdk-reference/index.mdx
+++ b/content/docs/android/sdk-reference/index.mdx
@@ -15,4 +15,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/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/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).
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.