diff --git a/content/docs/android/guides/web-checkout/linking-membership-to-iOS-app.mdx b/content/docs/android/guides/web-checkout/linking-membership-to-iOS-app.mdx
index c4676ef4..bd9012ed 100644
--- a/content/docs/android/guides/web-checkout/linking-membership-to-iOS-app.mdx
+++ b/content/docs/android/guides/web-checkout/linking-membership-to-iOS-app.mdx
@@ -3,4 +3,99 @@ title: "Redeeming In-App"
description: "Handle a deep link in your app and use the delegate methods."
---
-../../../../shared/web-checkout/linking-membership-to-iOS-app.mdx
\ No newline at end of file
+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.
+
+
+ If you're using Superwall to handle purchases, then you don't need to do anything here.
+
+
+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()
+
+ // 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.
\ No newline at end of file
diff --git a/content/docs/android/guides/web-checkout/post-checkout-redirecting.mdx b/content/docs/android/guides/web-checkout/post-checkout-redirecting.mdx
index f86ef4e7..64c6db20 100644
--- a/content/docs/android/guides/web-checkout/post-checkout-redirecting.mdx
+++ b/content/docs/android/guides/web-checkout/post-checkout-redirecting.mdx
@@ -3,4 +3,128 @@ title: "Post-Checkout Redirecting"
description: "Learn how to handle users redirecting back to your app after a web purchase."
---
-../../../../shared/web-checkout/post-checkout-redirecting.mdx
\ No newline at end of file
+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)
+
+
+ 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.
+
+
+- [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!")
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/content/docs/android/guides/web-checkout/using-revenuecat.mdx b/content/docs/android/guides/web-checkout/using-revenuecat.mdx
index 25e0fbb7..a7f6e961 100644
--- a/content/docs/android/guides/web-checkout/using-revenuecat.mdx
+++ b/content/docs/android/guides/web-checkout/using-revenuecat.mdx
@@ -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."
---
-../../../../shared/web-checkout/using-revenuecat.mdx
\ No newline at end of file
+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.
+
+
+ If you're using Superwall to handle purchases, then you don't need to do anything here.
+
+
+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.
+
+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
+ }
+ }
+ }
+}
+```
+
+
+ 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.
+
+
+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.
\ No newline at end of file
diff --git a/content/docs/expo/guides/web-checkout/linking-membership-to-iOS-app.mdx b/content/docs/expo/guides/web-checkout/linking-membership-to-iOS-app.mdx
index c4676ef4..206ea370 100644
--- a/content/docs/expo/guides/web-checkout/linking-membership-to-iOS-app.mdx
+++ b/content/docs/expo/guides/web-checkout/linking-membership-to-iOS-app.mdx
@@ -3,4 +3,82 @@ title: "Redeeming In-App"
description: "Handle a deep link in your app and use the delegate methods."
---
-../../../../shared/web-checkout/linking-membership-to-iOS-app.mdx
\ No newline at end of file
+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.
+
+
+ If you're using Superwall to handle purchases, then you don't need to do anything here.
+
+
+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 a custom PurchaseController (with either iOS StoreKit or Android Play Billing), 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:
+
+```typescript
+import Superwall, { SubscriptionStatus, Entitlement } from 'expo-superwall/compat';
+
+async function syncSubscriptionStatus(): Promise {
+ // Get the device entitlements from your purchase controller
+ // This will vary based on whether you're using RevenueCat, StoreKit, or Play Billing
+ const deviceEntitlements = await getDeviceEntitlements();
+
+ // Get the web entitlements from Superwall
+ const webEntitlements = await Superwall.shared.getWebEntitlements();
+
+ // Merge the two sets of entitlements
+ const allEntitlementIds = new Set([
+ ...deviceEntitlements,
+ ...webEntitlements.map(e => e.id)
+ ]);
+
+ // Update subscription status
+ if (allEntitlementIds.size > 0) {
+ const entitlements = Array.from(allEntitlementIds).map(id =>
+ new Entitlement(id)
+ );
+ Superwall.shared.setSubscriptionStatus(
+ SubscriptionStatus.Active(entitlements)
+ );
+ } else {
+ Superwall.shared.setSubscriptionStatus(SubscriptionStatus.Inactive());
+ }
+}
+
+// Helper function to get device entitlements
+// This is a simplified example - your implementation will depend on your purchase system
+async function getDeviceEntitlements(): Promise {
+ // For RevenueCat:
+ // const customerInfo = await Purchases.getCustomerInfo();
+ // return Object.keys(customerInfo.entitlements.active);
+
+ // For custom StoreKit/Play Billing:
+ // Query your store's API for current purchases
+ // Extract entitlement IDs from those purchases
+ // Return array of entitlement IDs
+
+ return []; // Replace with your actual implementation
+}
+```
+
+In addition to syncing the subscription status when purchasing and restoring, you'll need to sync it whenever `didRedeemLink(result)` is called:
+
+```typescript
+import { SuperwallDelegate, RedemptionResult } from 'expo-superwall/compat';
+
+export class SWDelegate extends SuperwallDelegate {
+ async didRedeemLink(result: RedemptionResult): Promise {
+ await syncSubscriptionStatus();
+ }
+}
+```
+
+### 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.
\ No newline at end of file
diff --git a/content/docs/expo/guides/web-checkout/post-checkout-redirecting.mdx b/content/docs/expo/guides/web-checkout/post-checkout-redirecting.mdx
index f86ef4e7..f90fdac8 100644
--- a/content/docs/expo/guides/web-checkout/post-checkout-redirecting.mdx
+++ b/content/docs/expo/guides/web-checkout/post-checkout-redirecting.mdx
@@ -3,4 +3,160 @@ title: "Post-Checkout Redirecting"
description: "Learn how to handle users redirecting back to your app after a web purchase."
---
-../../../../shared/web-checkout/post-checkout-redirecting.mdx
\ No newline at end of file
+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)
+
+
+ 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.
+
+
+- [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.
+
+```typescript
+import { SuperwallDelegate } from 'expo-superwall/compat';
+import { Toast } from 'react-native-toast-message'; // or your preferred toast library
+
+export class SWDelegate extends SuperwallDelegate {
+ willRedeemLink(): void {
+ // Show a loading indicator to the user
+ Toast.show({
+ type: 'info',
+ text1: '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` with `type: 'success'`: The redemption succeeded and contains information about the redeemed code.
+- `RedemptionResult` with `type: 'error'`: An error occurred while redeeming. You can check the error message via the error parameter.
+- `RedemptionResult` with `type: 'expiredCode'`: The code expired and contains information about whether a redemption email has been resent and an optional obfuscated email address.
+- `RedemptionResult` with `type: 'invalidCode'`: The code that was redeemed was invalid.
+- `RedemptionResult` with `type: '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.
+
+```typescript
+import { SuperwallDelegate, RedemptionResult } from 'expo-superwall/compat';
+import Superwall from 'expo-superwall/compat';
+import { Toast } from 'react-native-toast-message'; // or your preferred toast library
+
+export class SWDelegate extends SuperwallDelegate {
+ didRedeemLink(result: RedemptionResult): void {
+ switch (result.type) {
+ case 'expiredCode':
+ Toast.show({
+ type: 'error',
+ text1: 'Expired Link',
+ });
+ console.log('[!] code expired', result.code, result.expiredInfo);
+ break;
+
+ case 'error':
+ Toast.show({
+ type: 'error',
+ text1: result.error.message,
+ });
+ console.log('[!] error', result.code, result.error);
+ break;
+
+ case 'expiredSubscription':
+ Toast.show({
+ type: 'error',
+ text1: 'Expired Subscription',
+ });
+ console.log('[!] expired subscription', result.code, result.redemptionInfo);
+ break;
+
+ case 'invalidCode':
+ Toast.show({
+ type: 'error',
+ text1: 'Invalid Link',
+ });
+ console.log('[!] invalid code', result.code);
+ break;
+
+ case 'success':
+ const email = result.redemptionInfo?.purchaserInfo?.email;
+ if (email) {
+ Superwall.shared.setUserAttributes({ email });
+ Toast.show({
+ type: 'success',
+ text1: `Welcome, ${email}!`,
+ });
+ } else {
+ Toast.show({
+ type: 'success',
+ text1: 'Welcome!',
+ });
+ }
+ break;
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/content/docs/expo/guides/web-checkout/using-revenuecat.mdx b/content/docs/expo/guides/web-checkout/using-revenuecat.mdx
index 25e0fbb7..f5efbb3a 100644
--- a/content/docs/expo/guides/web-checkout/using-revenuecat.mdx
+++ b/content/docs/expo/guides/web-checkout/using-revenuecat.mdx
@@ -3,4 +3,110 @@ title: "Using RevenueCat"
description: "Handle a deep link in your app and use the delegate methods to link web checkouts with RevenueCat."
---
-../../../../shared/web-checkout/using-revenuecat.mdx
\ No newline at end of file
+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.
+
+
+ If you're using Superwall to handle purchases, then you don't need to do anything here.
+
+
+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.
+
+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:
+
+```typescript
+import { SuperwallDelegate, RedemptionResult } from 'expo-superwall/compat';
+import Purchases from 'react-native-purchases';
+
+export class SWDelegate extends SuperwallDelegate {
+ // The user tapped on a deep link to redeem a code
+ willRedeemLink(): void {
+ console.log('[!] willRedeemLink');
+ // Optionally show a loading indicator here
+ }
+
+ // Superwall received a redemption result and validated the purchase with Stripe.
+ async didRedeemLink(result: RedemptionResult): Promise {
+ console.log('[!] didRedeemLink', result);
+ // Send Stripe IDs to RevenueCat to link purchases to the customer
+
+ // Get a list of subscription ids tied to the customer.
+ const stripeSubscriptionIds =
+ result.type === 'success' ? result.stripeSubscriptionIds : null;
+
+ if (!stripeSubscriptionIds) {
+ return;
+ }
+
+ const revenueCatStripePublicAPIKey = 'strp.....'; // replace with your RevenueCat Stripe Public API Key
+ const appUserId = Purchases.getAppUserID();
+
+ // In the background, process all subscription IDs
+ await Promise.all(
+ stripeSubscriptionIds.map(async (stripeSubscriptionId) => {
+ try {
+ const response = await fetch('https://api.revenuecat.com/v1/receipts', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-Platform': 'stripe',
+ 'Authorization': `Bearer ${revenueCatStripePublicAPIKey}`,
+ },
+ body: JSON.stringify({
+ app_user_id: appUserId,
+ fetch_token: stripeSubscriptionId,
+ }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ console.log(`[!] Success: linked ${stripeSubscriptionId} to user ${appUserId}`, data);
+ } else {
+ console.error(`[!] Error: unable to link ${stripeSubscriptionId} to user ${appUserId}. Response:`, data);
+ }
+ } catch (error) {
+ console.error(`[!] Error: unable to link ${stripeSubscriptionId} to user ${appUserId}`, error);
+ }
+ })
+ );
+
+ /// After all network calls complete, invalidate the cache
+ try {
+ const customerInfo = await Purchases.getCustomerInfo({ fetchPolicy: 'FETCH_CURRENT' });
+
+ /// If you're using RevenueCat's `addCustomerInfoUpdateListener`, 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.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
+ /// const webEntitlements = Superwall.shared.entitlements.web
+
+ // Perform UI updates to let the user know their subscription was redeemed
+ // This runs automatically on the main thread in React Native
+ } catch (error) {
+ console.error('Error getting customer info', error);
+ }
+ }
+}
+```
+
+
+ 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.
+
+
+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.
\ No newline at end of file
diff --git a/content/docs/expo/meta.json b/content/docs/expo/meta.json
index a9aaf4b8..8e13fd74 100644
--- a/content/docs/expo/meta.json
+++ b/content/docs/expo/meta.json
@@ -16,6 +16,10 @@
"quickstart/setting-user-properties",
"quickstart/in-app-paywall-previews",
+ "---Common Use Cases---",
+ "guides/web-checkout",
+ "guides/3rd-party-analytics",
+
"---SDK Reference---",
"sdk-reference/index",
"sdk-reference/components",
@@ -30,7 +34,6 @@
"guides/experimental-flags",
"guides/advanced",
"guides/migrations",
- "guides/3rd-party-analytics",
"[Troubleshooting](https://support.superwall.com/articles/4780985851-troubleshooting-expo-sdk)",
"[Example App](https://github.com/superwall/expo-superwall/tree/main/example)"
]
diff --git a/content/docs/flutter/guides/web-checkout/linking-membership-to-iOS-app.mdx b/content/docs/flutter/guides/web-checkout/linking-membership-to-iOS-app.mdx
index c4676ef4..0b0630d9 100644
--- a/content/docs/flutter/guides/web-checkout/linking-membership-to-iOS-app.mdx
+++ b/content/docs/flutter/guides/web-checkout/linking-membership-to-iOS-app.mdx
@@ -3,4 +3,91 @@ title: "Redeeming In-App"
description: "Handle a deep link in your app and use the delegate methods."
---
-../../../../shared/web-checkout/linking-membership-to-iOS-app.mdx
\ No newline at end of file
+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.
+
+
+ If you're using Superwall to handle purchases, then you don't need to do anything here.
+
+
+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 a custom PurchaseController (with either iOS StoreKit or Android Play Billing), 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:
+
+```dart
+import 'package:superwall_flutter/superwall_flutter.dart';
+
+Future syncSubscriptionStatus() async {
+ // Get the device entitlements from your purchase controller
+ // This will vary based on whether you're using RevenueCat, StoreKit, or Play Billing
+ final deviceEntitlements = await getDeviceEntitlements();
+
+ // Get the web entitlements from Superwall
+ final webEntitlements = Superwall.shared.entitlements?.web ?? [];
+
+ // Merge the two sets of entitlements
+ final allEntitlementIds = {
+ ...deviceEntitlements,
+ ...webEntitlements.map((e) => e.id),
+ };
+
+ // Update subscription status
+ if (allEntitlementIds.isNotEmpty) {
+ final entitlements = allEntitlementIds
+ .map((id) => Entitlement(id: id))
+ .toSet();
+
+ await Superwall.shared.setSubscriptionStatus(
+ SubscriptionStatusActive(entitlements: entitlements),
+ );
+ } else {
+ await Superwall.shared.setSubscriptionStatus(
+ SubscriptionStatusInactive(),
+ );
+ }
+}
+
+// Helper function to get device entitlements
+// This is a simplified example - your implementation will depend on your purchase system
+Future> getDeviceEntitlements() async {
+ // For RevenueCat:
+ // final customerInfo = await Purchases.getCustomerInfo();
+ // return customerInfo.entitlements.active.keys.toList();
+
+ // For custom StoreKit/Play Billing:
+ // Query your store's API for current purchases
+ // Extract entitlement IDs from those purchases
+ // Return list of entitlement IDs
+
+ return []; // Replace with your actual implementation
+}
+```
+
+In addition to syncing the subscription status when purchasing and restoring, you'll need to sync it whenever `didRedeemLink(result)` is called:
+
+```dart
+import 'package:superwall_flutter/superwall_flutter.dart';
+
+class MySuperwallDelegate extends SuperwallDelegate {
+ @override
+ void didRedeemLink(RedemptionResult result) {
+ // Don't use async here directly, spawn a separate task
+ _handleRedemption(result);
+ }
+
+ Future _handleRedemption(RedemptionResult result) async {
+ await syncSubscriptionStatus();
+ }
+}
+```
+
+### 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.
\ No newline at end of file
diff --git a/content/docs/flutter/guides/web-checkout/post-checkout-redirecting.mdx b/content/docs/flutter/guides/web-checkout/post-checkout-redirecting.mdx
index f86ef4e7..baf1c6b0 100644
--- a/content/docs/flutter/guides/web-checkout/post-checkout-redirecting.mdx
+++ b/content/docs/flutter/guides/web-checkout/post-checkout-redirecting.mdx
@@ -3,4 +3,190 @@ title: "Post-Checkout Redirecting"
description: "Learn how to handle users redirecting back to your app after a web purchase."
---
-../../../../shared/web-checkout/post-checkout-redirecting.mdx
\ No newline at end of file
+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)
+
+
+ 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.
+
+
+- [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.
+
+```dart
+import 'package:superwall_flutter/superwall_flutter.dart';
+import 'package:flutter/material.dart';
+
+class MySuperwallDelegate extends SuperwallDelegate {
+ @override
+ void willRedeemLink() {
+ // Show a loading indicator to the user
+ print('Activating your purchase...');
+ // You might show a SnackBar or loading dialog here
+ }
+}
+```
+
+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. The result is a `RedemptionResult` which can be one of:
+
+- `RedemptionResult` with `type: RedemptionResultType.success`: The redemption succeeded and contains information about the redeemed code.
+- `RedemptionResult` with `type: RedemptionResultType.error`: An error occurred while redeeming. You can check the error message via the error parameter.
+- `RedemptionResult` with `type: RedemptionResultType.expiredCode`: The code expired and contains information about whether a redemption email has been resent and an optional obfuscated email address.
+- `RedemptionResult` with `type: RedemptionResultType.invalidCode`: The code that was redeemed was invalid.
+- `RedemptionResult` with `type: RedemptionResultType.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.
+
+```dart
+import 'package:superwall_flutter/superwall_flutter.dart';
+import 'package:flutter/material.dart';
+
+class MySuperwallDelegate extends SuperwallDelegate {
+ final BuildContext context; // Pass context if you need to show dialogs/snackbars
+
+ MySuperwallDelegate(this.context);
+
+ @override
+ void didRedeemLink(RedemptionResult result) {
+ switch (result.type) {
+ case RedemptionResultType.expiredCode:
+ _showMessage('Expired Link');
+ print('[!] code expired: ${result.code}, ${result.expiredInfo}');
+ break;
+
+ case RedemptionResultType.error:
+ _showMessage(result.error?.message ?? 'An error occurred');
+ print('[!] error: ${result.code}, ${result.error}');
+ break;
+
+ case RedemptionResultType.expiredSubscription:
+ _showMessage('Expired Subscription');
+ print('[!] expired subscription: ${result.code}, ${result.redemptionInfo}');
+ break;
+
+ case RedemptionResultType.invalidCode:
+ _showMessage('Invalid Link');
+ print('[!] invalid code: ${result.code}');
+ break;
+
+ case RedemptionResultType.success:
+ final email = result.redemptionInfo?.purchaserInfo?.email;
+ if (email != null) {
+ Superwall.shared.setUserAttributes({'email': email});
+ _showMessage('Welcome, $email!');
+ } else {
+ _showMessage('Welcome!');
+ }
+ break;
+ }
+ }
+
+ void _showMessage(String message) {
+ // Show a snackbar or toast message
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(message)),
+ );
+ }
+}
+```
+
+### Setting up the delegate
+
+Make sure to set your delegate when configuring Superwall:
+
+```dart
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ await Superwall.configure('pk_your_api_key');
+
+ // Set the delegate (you'll need access to BuildContext for UI operations)
+ runApp(MyApp());
+}
+
+class MyApp extends StatefulWidget {
+ @override
+ State createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ @override
+ void initState() {
+ super.initState();
+ // Set the delegate after the widget is initialized
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Superwall.shared.setDelegate(MySuperwallDelegate(context));
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: YourHomeScreen(),
+ );
+ }
+}
+```
\ No newline at end of file