Consumable Products

Set up consumable products for Superwall paywalls in Flutter apps.

Use consumable products when a purchase should grant a quantity that can be used up, such as credits, coins, boosts, or tokens.

This guide covers the Superwall-only flow where purchases are started from paywalls and you are not using a PurchaseController.

The platform sections below explain the iOS and Android store requirements that still apply to Flutter apps.

iOS

This guide assumes purchases are made from Superwall paywalls and that you are not using a PurchaseController.

Consumable products are one-time purchases that users can buy repeatedly, such as credits, tokens, boosts, or packs. Non-consumable products are also one-time purchases, but they grant permanent access, such as a lifetime unlock.

Superwall uses entitlements to decide whether a user has ongoing access. Because consumables are meant to be used up, they should usually not grant entitlements. Your app should listen for the purchase, grant the consumable benefit in your own system, and treat Superwall's purchase history as a record of what happened.

Dashboard Setup

  1. Create the consumable in App Store Connect.
  2. Add the product in Superwall from Products.
  3. Use the App Store product identifier.
  4. Set Period to None (Lifetime / Consumable).
  5. Leave Entitlements empty.
  6. Add the product to any paywall that should sell it.

Do not attach an entitlement to a consumable unless the purchase should also unlock ongoing access. If a consumable has no entitlement, buying it does not make the user's subscription status active.

Include Consumables In Purchase History

Apple excludes consumable purchases from App Store purchase history unless you opt in. Add SKIncludeConsumableInAppPurchaseHistory to your app's Info.plist as a Boolean set to YES.

<key>SKIncludeConsumableInAppPurchaseHistory</key>
<true/>

When this key is present and set to YES, Superwall uses StoreKit 2 on iOS 18 and later. On earlier iOS versions, the SDK falls back to StoreKit 1 for purchase history support.

Grant The Consumable Benefit

Superwall does not maintain balances for consumables. Grant credits, tokens, or other benefits from your app or backend after the transactionComplete event. Make this operation idempotent so retries do not double-credit the user.

import SuperwallKit

final class SWDelegate: SuperwallDelegate {
  func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) {
    guard case let .transactionComplete(transaction, product, _, _) = eventInfo.event else {
      return
    }

    guard product.productIdentifier == "com.example.credits_100" else {
      return
    }

    Task {
      await ConsumablesService.shared.grantCredits(
        count: 100,
        productId: product.productIdentifier,
        transactionId: transaction?.storeTransactionId
      )
    }
  }
}

Superwall.shared.delegate = SWDelegate()
#import <SuperwallKit/SuperwallKit-Swift.h>

@interface SWDelegate : NSObject <SWKSuperwallDelegate>
@end

@implementation SWDelegate

- (void)handleSuperwallEventWithInfo:(SWKSuperwallEventInfo *)eventInfo {
  if (eventInfo.event != SWKSuperwallEventTransactionComplete) {
    return;
  }

  NSString *productId = eventInfo.params[@"primary_product_id"];
  if (![productId isEqualToString:@"com.example.credits_100"]) {
    return;
  }

  NSString *transactionId = eventInfo.params[@"store_transaction_id"];
  [[ConsumablesService shared] grantCredits:100
                                  productId:productId
                              transactionId:transactionId];
}

@end

[Superwall sharedInstance].delegate = [SWDelegate new];

Read Purchase History

Consumable and non-consumable purchases appear in customerInfo.nonSubscriptions. Use isConsumable to distinguish consumables from lifetime purchases.

let customerInfo = Superwall.shared.customerInfo

let consumables = customerInfo.nonSubscriptions.filter { $0.isConsumable }
for purchase in consumables {
  print("Consumable purchased: \(purchase.productId)")
}
SWKCustomerInfo *customerInfo = [Superwall sharedInstance].customerInfo;

for (SWKNonSubscriptionTransaction *purchase in customerInfo.nonSubscriptions) {
  if (purchase.isConsumable) {
    NSLog(@"Consumable purchased: %@", purchase.productId);
  }
}

Android

This guide assumes purchases are made from Superwall paywalls and that you are not using a PurchaseController.

Consumable products are one-time purchases that users can buy repeatedly, such as credits, tokens, boosts, or packs. Non-consumable products are also one-time purchases, but they grant permanent access, such as a lifetime unlock.

Superwall uses entitlements to decide whether a user has ongoing access. Because consumables are meant to be used up, they should usually not grant entitlements. Your app should listen for the purchase, grant the consumable benefit in your own system, and then consume the Google Play purchase token so the item can be purchased again.

Dashboard Setup

  1. Create the product as an in-app product in Google Play Console.
  2. Add the product in Superwall from Products.
  3. Use the Google Play product ID.
  4. Set Period to None (Lifetime / Consumable).
  5. Leave Entitlements empty.
  6. Add the product to any paywall that should sell it.

Do not attach an entitlement to a consumable unless the purchase should also unlock ongoing access. If a consumable has no entitlement, buying it does not make the user's subscription status active.

Consume The Purchase Token

Google Play in-app products must be consumed after you grant the benefit. If you do not consume the purchase token, the user may not be able to buy that same consumable again.

Use the purchaseToken from the TransactionComplete event, grant the benefit, then call Superwall.instance.consume(purchaseToken).

Superwall.instance.consume(purchaseToken) is available in Android SDK 2.6.2 and later.

import androidx.lifecycle.lifecycleScope
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.analytics.superwall.SuperwallEventInfo
import com.superwall.sdk.delegate.SuperwallDelegate
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity(), SuperwallDelegate {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Superwall.instance.delegate = this
  }

  override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
    val event = eventInfo.event
    if (event !is SuperwallEvent.TransactionComplete) {
      return
    }

    if (event.product.productIdentifier != "coins_100") {
      return
    }

    val purchaseToken = event.transaction?.purchaseToken ?: return

    lifecycleScope.launch {
      ConsumablesService.grantCoins(
        count = 100,
        productId = event.product.productIdentifier,
        purchaseToken = purchaseToken,
      )

      Superwall.instance.consume(purchaseToken)
        .onFailure { error ->
          // Retry consumption after confirming the benefit was granted.
          println("Failed to consume purchase: ${error.message}")
        }
    }
  }
}
import com.superwall.sdk.Superwall;
import com.superwall.sdk.analytics.superwall.SuperwallEvent;
import com.superwall.sdk.analytics.superwall.SuperwallEventInfo;
import com.superwall.sdk.delegate.SuperwallDelegateJava;
import com.superwall.sdk.store.abstractions.transactions.StoreTransactionType;
import kotlin.Unit;

public class MainActivity extends AppCompatActivity implements SuperwallDelegateJava {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Superwall.getInstance().setJavaDelegate(this);
  }

  @Override
  public void handleSuperwallEvent(SuperwallEventInfo eventInfo) {
    if (!(eventInfo.getEvent() instanceof SuperwallEvent.TransactionComplete)) {
      return;
    }

    SuperwallEvent.TransactionComplete event =
      (SuperwallEvent.TransactionComplete) eventInfo.getEvent();

    if (!event.getProduct().getProductIdentifier().equals("coins_100")) {
      return;
    }

    StoreTransactionType transaction = event.getTransaction();
    if (transaction == null) {
      return;
    }

    String purchaseToken = transaction.getPurchaseToken();

    ConsumablesService.grantCoins(100, event.getProduct().getProductIdentifier(), purchaseToken);

    Superwall.getInstance().consume(purchaseToken, result -> {
      // Check the Result in your app and retry if consumption fails.
      return Unit.INSTANCE;
    });
  }
}

Grant the benefit before consuming the token. If your grant call fails, leave the purchase unconsumed and retry after your app confirms the user received the benefit.

Read Purchase History

Consumable and non-consumable purchases appear in customerInfo.nonSubscriptions. Use isConsumable to distinguish consumables from lifetime purchases.

val customerInfo = Superwall.instance.getCustomerInfo()

val consumables = customerInfo.nonSubscriptions.filter { it.isConsumable }
for (purchase in consumables) {
  println("Consumable purchased: ${purchase.productId}")
}
CustomerInfo customerInfo = Superwall.getInstance().getCustomerInfo();

for (NonSubscriptionTransaction purchase : customerInfo.getNonSubscriptions()) {
  if (purchase.isConsumable()) {
    System.out.println("Consumable purchased: " + purchase.getProductId());
  }
}

Flutter Event Handling

In Flutter, listen for purchase events with SuperwallDelegate or a PaywallPresentationHandler. Android consumables still need the Google Play purchase token consumed after you grant the benefit.

Use Superwall.shared.consume(purchaseToken) for the Android consumption call. The current Flutter SDK exposes consume() directly, but its public StoreTransaction wrapper does not expose the Android purchaseToken from Superwall's transactionComplete event. If your Flutter app sells Android consumables, expose that token from your Android purchase handling path before calling consume().

How is this guide?

On this page