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
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,99 @@ title: "Redeeming In-App"
description: "Handle a deep link in your app and use the delegate methods."
---

<include>../../../../shared/web-checkout/linking-membership-to-iOS-app.mdx</include>
After purchasing from a web paywall, the user will be redirected to your app by a deep link to redeem their purchase on device.
Please follow our [Post-Checkout Redirecting](/web-checkout-post-checkout-redirecting) guide to handle this user experience.

<Note>
If you're using Superwall to handle purchases, then you don't need to do anything here.
</Note>

If you're using your own `PurchaseController`, you will need to update the subscription status with the redeemed web entitlements. If you're using RevenueCat, you should follow our [Using RevenueCat](/web-checkout-using-revenuecat) guide.

### Using a PurchaseController

If you're using Google Play Billing in your PurchaseController, you'll need to merge the web entitlements with the device entitlements before setting the subscription status.
Here's an example of how you might do this:

```kotlin
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.QueryPurchasesParams
import com.superwall.sdk.Superwall
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

suspend fun syncSubscriptionStatus(billingClient: BillingClient) {
withContext(Dispatchers.IO) {
val productIds = mutableSetOf<String>()

// Get the device entitlements from Google Play Billing
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()

val purchasesResult = billingClient.queryPurchasesAsync(params)

// Collect purchased product IDs
purchasesResult.purchasesList.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
purchase.products.forEach { productId ->
productIds.add(productId)
}
}
}

// Get products from Superwall and extract their entitlements
val storeProducts = Superwall.instance.getProducts(productIds)
val deviceEntitlements = storeProducts.flatMap { it.entitlements }.toSet()

// Get the web entitlements from Superwall
val webEntitlements = Superwall.instance.entitlements.web

// Merge the two sets of entitlements
val allEntitlements = deviceEntitlements + webEntitlements

// Update subscription status on the main thread
withContext(Dispatchers.Main) {
if (allEntitlements.isNotEmpty()) {
Superwall.instance.setSubscriptionStatus(
SubscriptionStatus.Active(allEntitlements)
)
} else {
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
}
}
}
}
```

In addition to syncing the subscription status when purchasing and restoring, you'll need to sync it whenever `didRedeemLink(result:)` is called:

```kotlin
import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.models.redemption.RedemptionResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class SWDelegate(private val billingClient: BillingClient) : SuperwallDelegate {
private val coroutineScope = CoroutineScope(Dispatchers.Main)

override fun didRedeemLink(result: RedemptionResult) {
coroutineScope.launch {
syncSubscriptionStatus(billingClient)
}
}
}
```

### Refreshing of web entitlements

If you aren't using a Purchase Controller, the SDK will refresh the web entitlements every 24 hours.

### Redeeming while a paywall is open

If a redeem event occurs when a paywall is open, the SDK will track that as a restore event and the paywall will close.
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,128 @@ title: "Post-Checkout Redirecting"
description: "Learn how to handle users redirecting back to your app after a web purchase."
---

<include>../../../../shared/web-checkout/post-checkout-redirecting.mdx</include>
After a user completes a web purchase, Superwall needs to redirect them back to your app. You can configure this behavior in two ways:

## Post-Purchase Behavior Modes

You can configure how users are redirected after checkout in your [Application Settings](/web-checkout-configuring-stripe-keys-and-settings#post-purchase-behavior):

### Redeem Mode (Default)

Superwall manages the entire redemption experience:
- Users are automatically deep linked to your app with a redemption code
- Fallback to App Store/Play Store if the app isn't installed
- Redemption emails are sent automatically
- The SDK handles redemption via delegate methods (detailed below)

This is the recommended mode for most apps.

### Redirect Mode

Redirect users to your own custom URL with purchase information:
- **When to use**: You want to show a custom success page, perform additional actions before redemption, or have your own deep linking infrastructure
- **What you receive**: Purchase data is passed as query parameters to your URL

**Query Parameters Included**:
- `app_user_id` - The user's identifier from your app
- `email` - User's email address
- `stripe_subscription_id` - The Stripe subscription ID
- Any custom placement parameters you set

**Example**:
```
https://yourapp.com/success?
app_user_id=user_123&
email=user@example.com&
stripe_subscription_id=sub_1234567890&
campaign_id=summer_sale
```

You'll need to implement your own logic to handle the redirect and deep link users into your app.

---

## Setting Up Deep Links

Whether you're showing a checkout page in a browser or using the In-App Browser, the Superwall SDK relies on deep links to redirect back to your app.

#### Prerequisites
1. [Configuring Stripe Keys and Settings](/web-checkout-configuring-stripe-keys-and-settings)
2. [Deep Links](/in-app-paywall-previews)

<Warning>
If you're not using Superwall to handle purchases, then you'll need to follow extra steps to redeem the web purchase in your app.
</Warning>

- [Using RevenueCat](/web-checkout-using-revenuecat)
- [Using a PurchaseController](/web-checkout-linking-membership-to-iOS-app#using-a-purchasecontroller)

---

## Handling Redemption (Redeem Mode)

When using Redeem mode (the default), handle the user experience when they're redirected back to your app using `SuperwallDelegate` methods:

### willRedeemLink

When your app opens via the deep link, we will call the delegate method `willRedeemLink()` before making a network call to redeem the code.
At this point, you might wish to display a loading indicator in your app so the user knows that the purchase is being redeemed.

```kotlin
class SWDelegate : SuperwallDelegate {
override fun willRedeemLink() {
// Show a loading indicator to the user
showToast("Activating your purchase...")
}
}
```

You can manually dismiss the paywall at this point if needed, but note that the paywall will be dismissed automatically when the `didRedeemLink` method is called.

### didRedeemLink

After receiving a response from the network, we will call `didRedeemLink(result:)` with the result of redeeming the code. This result can be one of the following:

- `RedemptionResult.Success`: The redemption succeeded and contains information about the redeemed code.
- `RedemptionResult.Error`: An error occurred while redeeming. You can check the error message via the error parameter.
- `RedemptionResult.ExpiredCode`: The code expired and contains information about whether a redemption email has been resent and an optional obfuscated email address.
- `RedemptionResult.InvalidCode`: The code that was redeemed was invalid.
- `RedemptionResult.ExpiredSubscription`: The subscription that the code redeemed has expired.

On network failure, the SDK will retry up to 6 times before returning an `Error` `RedemptionResult` in `didRedeemLink(result:)`.

Here, you should remove any loading UI you added in `willRedeemLink` and show a message to the user based on the result. If a paywall is presented, it will be dismissed automatically.

```kotlin
class SWDelegate : SuperwallDelegate {
override fun didRedeemLink(result: RedemptionResult) {
when (result) {
is RedemptionResult.ExpiredCode -> {
showToast("Expired Link")
Log.d("Superwall", "Code expired: ${result.code}, ${result.expiredInfo}")
}
is RedemptionResult.Error -> {
showToast(result.error.message)
Log.d("Superwall", "Error: ${result.code}, ${result.error}")
}
is RedemptionResult.ExpiredSubscription -> {
showToast("Expired Subscription")
Log.d("Superwall", "Expired subscription: ${result.code}, ${result.redemptionInfo}")
}
is RedemptionResult.InvalidCode -> {
showToast("Invalid Link")
Log.d("Superwall", "Invalid code: ${result.code}")
}
is RedemptionResult.Success -> {
val email = result.redemptionInfo.purchaserInfo.email
if (email != null) {
Superwall.instance.setUserAttributes(mapOf("email" to email))
showToast("Welcome, $email!")
} else {
showToast("Welcome!")
}
}
}
}
}
```
128 changes: 127 additions & 1 deletion content/docs/android/guides/web-checkout/using-revenuecat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,130 @@ title: "Using RevenueCat"
description: "Handle a deep link in your app and use the delegate methods to link web checkouts with RevenueCat."
---

<include>../../../../shared/web-checkout/using-revenuecat.mdx</include>
After purchasing from a web paywall, the user will be redirected to your app by a deep link to redeem their purchase on device. Please follow our [Post-Checkout Redirecting](/web-checkout-post-checkout-redirecting) guide to handle this user experience.

<Note>
If you're using Superwall to handle purchases, then you don't need to do anything here.
</Note>

<Warning>You only need to use a `PurchaseController` if you want end-to-end control of the purchasing pipeline. The recommended way to use RevenueCat with Superwall is by putting it in observer mode.</Warning>

If you're using your own `PurchaseController`, you should follow our [Redeeming In-App](/web-checkout-linking-membership-to-iOS-app) guide.

### Using a PurchaseController with RevenueCat

If you're using RevenueCat, you'll need to follow [steps 1 to 4 in their guide](https://www.revenuecat.com/docs/web/integrations/stripe) to set up Stripe with RevenueCat. Then, you'll need to
associate the RevenueCat customer with the Stripe subscription IDs returned from redeeming the code. You can do this by extracting the ids from the `RedemptionResult` and sending them to RevenueCat's API
by using the `didRedeemLink(result:)` delegate method:

```kotlin
import com.revenuecat.purchases.Purchases
import com.superwall.sdk.Superwall
import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.models.redemption.RedemptionResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

class SWDelegate : SuperwallDelegate {
private val client = OkHttpClient()
private val coroutineScope = CoroutineScope(Dispatchers.IO)

// The user tapped on a deep link to redeem a code
override fun willRedeemLink() {
Log.d("Superwall", "[!] willRedeemLink")
// Optionally show a loading indicator here
}

// Superwall received a redemption result and validated the purchase with Stripe.
override fun didRedeemLink(result: RedemptionResult) {
Log.d("Superwall", "[!] didRedeemLink: $result")
// Send Stripe IDs to RevenueCat to link purchases to the customer

// Get a list of subscription ids tied to the customer.
val stripeSubscriptionIds = when (result) {
is RedemptionResult.Success -> result.stripeSubscriptionIds
else -> null
} ?: return

val revenueCatStripePublicAPIKey = "strp....." // replace with your RevenueCat Stripe Public API Key
val appUserId = Purchases.sharedInstance.appUserID

// In the background using coroutines...
coroutineScope.launch {
// For each subscription id, link it to the user in RevenueCat
stripeSubscriptionIds.forEach { stripeSubscriptionId ->
try {
val json = JSONObject().apply {
put("app_user_id", appUserId)
put("fetch_token", stripeSubscriptionId)
}

val requestBody = json.toString()
.toRequestBody("application/json".toMediaType())

val request = Request.Builder()
.url("https://api.revenuecat.com/v1/receipts")
.post(requestBody)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("X-Platform", "stripe")
.addHeader("Authorization", "Bearer $revenueCatStripePublicAPIKey")
.build()

val response = client.newCall(request).execute()
val responseBody = response.body?.string()

if (response.isSuccessful) {
Log.d("Superwall", "[!] Success: linked $stripeSubscriptionId to user $appUserId: $responseBody")
} else {
Log.e("Superwall", "[!] Error: unable to link $stripeSubscriptionId to user $appUserId. Response: $responseBody")
}
} catch (e: Exception) {
Log.e("Superwall", "[!] Error: unable to link $stripeSubscriptionId to user $appUserId", e)
}
}

/// After all network calls complete, invalidate the cache
Purchases.sharedInstance.getCustomerInfo(
onSuccess = { customerInfo ->
/// If you're using RevenueCat's `UpdatedCustomerInfoListener`, or keeping Superwall Entitlements in sync
/// via RevenueCat's listener methods, you don't need to do anything here. Those methods will be
/// called automatically when this call fetches the most up to date customer info, ignoring any local caches.

/// Otherwise, if you're manually calling `Purchases.sharedInstance.getCustomerInfo` to keep Superwall's entitlements
/// in sync, you should use the newly updated customer info here to do so.

/// You could always access web entitlements here as well
/// val webEntitlements = Superwall.instance.entitlements.web
},
onError = { error ->
Log.e("Superwall", "Error getting customer info", error)
}
)

// After all network calls complete, update UI on the main thread
withContext(Dispatchers.Main) {
// Perform UI updates on the main thread, like letting the user know their subscription was redeemed
}
}
}
}
```

<Warning>
If you call `logIn` from RevenueCat's SDK, then you need to call the logic you've implemented
inside `didRedeemLink(result:)` again. For example, that means if `logIn` was invoked from
RevenueCat, you'd either abstract out this logic above into a function to call again, or simply
call this function directly.
</Warning>

The web entitlements will be returned along with other existing entitlements in the `CustomerInfo` object accessible via RevenueCat's SDK.

If you're logging in and out of RevenueCat, make sure to resend the Stripe subscription IDs to RevenueCat's endpoint after logging in.
Loading