StoreKit returns restored for SKUs marked Consumable (no purchase sheet); Flutter in_app_purchase + SK2

What platform are you targeting? And what version? iOS, testing in Sandbox on a physical device.

What version of Xcode are you using? [Xcode __]

What version of the OS are you testing on? iOS 18 on iPhone 15 pro.

What specific API are you using? StoreKit 2 via Flutter’s in_app_purchase plugin (Dart), which uses in_app_purchase_storekit under the hood.

What are the exact steps you took?

  1. In App Store Connect, I created several Consumable IAPs (status “Ready to Submit”). Example product IDs:

    • USD3.99TenMinuteCoffeePlan (Consumable)
    • USD24.99OneHourDinnerPlan (Consumable)
    • USD14.99InviteAFriendAsGenie (Consumable)
  2. Signed in as a Sandbox tester on device (Settings → App Store → Sandbox Account).

  3. App queries products with InAppPurchase.instance.queryProductDetails(ids) — products load successfully.

  4. Call buyConsumable(purchaseParam: PurchaseParam(productDetails: ...)).

  5. Listen to purchaseStream and log PurchaseDetails.

If something failed, what are the symptoms?

  • The purchase sheet often does not appear.
  • The purchase stream reports PurchaseStatus.restored, immediately, for SKUs that are marked Consumable.
  • Example log lines (from Dart):
Products loaded: 6
Product: id=USD3.99TenMinuteCoffeePlan, price=3.99
Product: id=USD24.99OneHourDinnerPlan, price=24.99
Product: id=USD14.99InviteAFriendAsGenie, price=14.99

Purchase update: productID=USD3.99TenMinuteCoffeePlan,
  status=PurchaseStatus.restored, pendingComplete=false, purchaseID=2000000991974131
Purchase update: productID=USD24.99OneHourDinnerPlan,
  status=PurchaseStatus.restored, pendingComplete=false, purchaseID=2000000992079251
Purchase update: productID=USD14.99InviteAFriendAsGenie,
  status=PurchaseStatus.restored, pendingComplete=false, purchaseID=2000000999910991

Purchase update: productID=USD29.99InviteAFriendAsGenie,
  status=PurchaseStatus.restored, pendingComplete=false, purchaseID=2000001003571920

If nothing failed, what results did you see? And what were you expecting?

  • Actual: restored events (no sheet) for items configured as Consumable.
  • Expected: For Consumables, a purchase sheet followed by purchased status. Consumables shouldn’t “restore”.

What else have you tried?

  • Verified every SKU shows Type = Consumable and Ready to Submit in App Store Connect; “Cleared for Sale” enabled; pricing/localization filled.
  • Created new product IDs (to avoid any prior non-consumable history).
  • Verified I’m not calling restorePurchases.
  • In the listener, I only grant benefits on PurchaseStatus.purchased (not on restored).
  • Observed that queryProductDetails succeeds; some IDs that aren’t fully configured return “not found,” as expected.

Minimal code (core bits):

final _iap = InAppPurchase.instance;

Future<void> init() async {
  final resp = await _iap.queryProductDetails({
    'USD3.99TenMinuteCoffeePlan',
    'USD24.99OneHourDinnerPlan',
    'USD14.99InviteAFriendAsGenie',
    'USD29.99InviteAFriendAsGenie',
  });
  _products = resp.productDetails;
  _sub = _iap.purchaseStream.listen(_onUpdates);
}

Future<void> buy(ProductDetails p) async {
  final param = PurchaseParam(productDetails: p);
  await _iap.buyConsumable(purchaseParam: param); // iOS SK2 path
}

void _onUpdates(List<PurchaseDetails> list) async {
  for (final pd in list) {
    print('status=${pd.status}, id=${pd.productID}, pending=${pd.pendingCompletePurchase}, purchaseID=${pd.purchaseID}');
    switch (pd.status) {
      case PurchaseStatus.purchased:
        // deliver & (if pendingCompletePurchase) completePurchase
        break;
      case PurchaseStatus.restored:
        // for consumables, I do not deliver here
        break;
      default:
        break;
    }
  }
}

Questions for the community/Apple:

  1. Under what conditions would StoreKit 2 return restored for a SKU that’s set to Consumable?

    • Is there any server-side caching of old product type or ownership tied to a product ID that could cause this in Sandbox?
  2. Is “Ready to Submit” sufficient for Sandbox testing of IAPs, or must the SKUs be attached to a submitted build before StoreKit treats them as consumable?

  3. If a product ID was ever created/purchased as Non-Consumable historically, does creating a new ASC entry with the same string ID as Consumable still cause restored for that tester?

  4. Besides creating brand-new product IDs and/or resetting the Sandbox tester’s purchase history, is there any other recommended way to clear this state?

Happy to provide a device sysdiagnose or a stripped test project if that helps. Thanks!

StoreKit returns restored for SKUs marked Consumable (no purchase sheet); Flutter in_app_purchase &#43; SK2
 
 
Q