Skip to content

Old Receipt Data Returned After Re-Subscription in Production #181530

Open

Description

@Nehal-1234

Steps to reproduce

Package(s):

in_app_purchase: ^3.2.3

in_app_purchase_storekit: ^0.3.21 (downgraded as workaround)

Description

When a user cancels a subscription and later purchases the same subscription product again, the app receives old receipt data instead of a fresh receipt. The receipt payload still contains the previous expiration date, even though the new purchase completes successfully.

This issue occurs only in production. The same flow works correctly in development/sandbox.

Steps to Reproduce

  • Install the app from App Store (production build)

  • Purchase a subscription

  • Cancel the subscription

  • After the subscription expires, purchase the same plan again

  • Read receipt data returned by in_app_purchase

Workarounds Tried

Downgraded in_app_purchase_storekit to ^0.3.21 (as suggested in this issue)

Result:

  • Works correctly in development/sandbox (cancel, renew, repurchase)

  • Does not work in production, issue still persists

Expected results

After a successful re-subscription, the app should receive updated receipt data

The receipt payload should contain the latest transaction and expiration date

Actual results

The app receives old receipt data

The expiration date corresponds to the previous subscription

Same behavior observed in receipt payload / webhook data

Code sample

Code sample
class InAppManager {
  // Properties
  final InAppPurchase _inAppPurchase = InAppPurchase.instance;
  Set<String> _consumableIds = <String>{};

  List<String> _notFoundIds = <String>[];
  List<ProductDetails> _products = <ProductDetails>[];

  bool isAvailable = false;
  bool purchasePending = false;
  bool loading = true;
  bool isRestoring = false;


  Function(IAPError error)? onError;
  Function(PurchaseDetails details)? onSuccess;
  Function(PurchaseDetails details)? onRestore;
  Function(String message)? onRestoreNoActive;



  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchaseDetails> activePurchases = <PurchaseDetails>[];


  // Auto-consume must be true on iOS.
  // To try without auto-consume on another platform, change `true` to `false` here.
  final bool autoConsume = Platform.isIOS || true;

  List<ProductDetails> getProducts() {
    return _products;
  }

  // Init
  InAppManager() {
    final Stream<List<PurchaseDetails>> purchaseUpdated =
        _inAppPurchase.purchaseStream;

    _subscription = purchaseUpdated.listen(
      (List<PurchaseDetails> purchaseDetailsList) {
        _listenToPurchaseUpdated(purchaseDetailsList);
      },
      onDone: () {
        _subscription.cancel();
      },
      onError: (Object error) {
        AlertMessage.instance.showMessage(msg: error.toString());
      },
    );
  }

  // Get the purchase and product details info.
  Future<void> initStoreInfo() async {
    final bool isAvailable = await _inAppPurchase.isAvailable();
    debugPrint('_inAppPurchase.isAvailable() : ${isAvailable}');
    //await AlertMessage.instance.showMessage(msg: 'inAppPurchase is available: ${isAvailable}');
    if (!isAvailable) {
      this.isAvailable = isAvailable;
      _products = <ProductDetails>[];
      _notFoundIds = <String>[];
      purchasePending = false;
      loading = false;
      return;
    }

    if (Platform.isIOS) {
      final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
          _inAppPurchase
              .getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
      await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
    }

    debugPrint('Ids : ${_consumableIds}');
    //await AlertMessage.instance.showMessage(msg: 'Ids : ${_consumableIds}', milliSec: 5000);

    final ProductDetailsResponse productDetailResponse =
        await _inAppPurchase.queryProductDetails(_consumableIds);
    debugPrint(
        'ProductDetailsResponse_notFoundIDs : ${productDetailResponse.notFoundIDs}');
    //await AlertMessage.instance.showMessage(msg: 'ProductDetailsResponse : ${ProductDetailsResponse}', milliSec: 5000);

    if (productDetailResponse.error != null) {
      this.isAvailable = isAvailable;
      _products = productDetailResponse.productDetails;
      _notFoundIds = productDetailResponse.notFoundIDs;
      purchasePending = false;
      loading = false;
      debugPrint('========= productDetailResponse.error =========');
      debugPrint('_notFoundIds : ${_notFoundIds}');
      debugPrint('${productDetailResponse.error?.code}');
      debugPrint('${productDetailResponse.error?.message}');
      debugPrint('${productDetailResponse.error?.details}');
      debugPrint('============================');
      return;
    }

    if (productDetailResponse.productDetails.isEmpty) {
      this.isAvailable = isAvailable;
      _products = productDetailResponse.productDetails;
      _notFoundIds = productDetailResponse.notFoundIDs;
      purchasePending = false;
      loading = false;
      debugPrint(
          '========= productDetailResponse.productDetails.isEmpty =========');
      debugPrint('_notFoundIds : ${_notFoundIds}');
      //await AlertMessage.instance.showMessage(msg: '_notFoundIds : ${_notFoundIds}', milliSec: 5000);
      debugPrint('============================');
      return;
    }

    this.isAvailable = isAvailable;
    _products = productDetailResponse.productDetails;

    _notFoundIds = productDetailResponse.notFoundIDs;
    purchasePending = false;

    debugPrint('========= products =========');
    _products.forEach((element) {
      debugPrint('${element.id}');
      debugPrint('${element.title}');
      debugPrint('${element.price}');
      debugPrint(' ');
    });
    debugPrint('============================');
  }

  // Buy product
  Future<void> buyProduct(String productId) async {
    final productDetails =
        _products.firstWhereOrNull((element) => element.id == productId);

    if (productDetails == null) {
      return;
    }

    late PurchaseParam purchaseParam;

    if (Platform.isAndroid) {
      purchaseParam = GooglePlayPurchaseParam(
        productDetails: productDetails,
      );
    } else {
      purchaseParam = PurchaseParam(
        productDetails: productDetails,
      );
    }

    await _inAppPurchase.buyConsumable(
      purchaseParam: purchaseParam,
      autoConsume: autoConsume,
    );
  }

  Future<void> buySubscription(String productId) async {
    debugPrint("productId :$productId");
    final productDetails =
        _products.firstWhereOrNull((element) => element.id == productId);

    debugPrint("productDetails :$productDetails");
    if (productDetails == null) {
      return;
    }

    late PurchaseParam purchaseParam;

    if (Platform.isAndroid) {
      purchaseParam = GooglePlayPurchaseParam(
        productDetails: productDetails,
      );
    } else {
      purchaseParam = PurchaseParam(
        productDetails: productDetails,
      );
    }

    debugPrint("💳 Calling buyNonConsumable...");
   
    try {
      await _inAppPurchase.buyNonConsumable(
        purchaseParam: purchaseParam,
      );
      debugPrint("✅ Purchase initiated");
    } catch (e) {
      debugPrint("❌ Purchase error: $e");
    }
  }

  Future<void> upOrDowngradeSubscription(
    String productId,
    SubscriptionDetails subscriptionDetails,
  ) async {
    final productDetails =
        _products.firstWhereOrNull((element) => element.id == productId);

    if (productDetails == null) {
      return;
    }

    late PurchaseParam purchaseParam;

    late GooglePlayPurchaseDetails oldPurchaseDetails;

    final platform = _inAppPurchase
        .getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
    final pastPurchases = await platform.queryPastPurchases();

    if (pastPurchases.pastPurchases.isEmpty) {
      return;
    }

    oldPurchaseDetails = pastPurchases.pastPurchases.firstWhere(
      (element) => element.purchaseID == subscriptionDetails.purchaseId,
    );

    if (Platform.isAndroid) {
      purchaseParam = GooglePlayPurchaseParam(
          productDetails: productDetails,
          changeSubscriptionParam: ChangeSubscriptionParam(
            oldPurchaseDetails: oldPurchaseDetails,
            replacementMode: ReplacementMode.withTimeProration,
            // prorationMode: ProrationMode.immediateWithTimeProration,
          ));
    }

    await _inAppPurchase.buyNonConsumable(
      purchaseParam: purchaseParam,
    );
  }


  // Sets products ids
  void setConsumableIds(Set<String> ids) {
    _consumableIds = ids;
  }

  // Listen to any purchase update.
  Future<void> _listenToPurchaseUpdated(
    List<PurchaseDetails> purchaseDetailsList,
  ) async {
    debugPrint(
        "📱 Purchase update received: ${purchaseDetailsList.length} items");

    for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
      debugPrint("Status: ${purchaseDetails.status}");
      debugPrint("Product ID: ${purchaseDetails.productID}");
      if (purchaseDetails.status == PurchaseStatus.pending) {
        showPendingUI();
      } else {
        if (purchaseDetails.status == PurchaseStatus.error) {
          debugPrint("❌ Error: ${purchaseDetails.error?.message}");
          handleError(purchaseDetails.error!);
        } else if (purchaseDetails.status == PurchaseStatus.purchased ||
            purchaseDetails.status == PurchaseStatus.restored) {
          if (Platform.isIOS) {
            // Small delay so StoreKit finishes processing
            await Future.delayed(const Duration(seconds: 2));

            final SK2Transaction? activeTransaction =
            await getLatestActiveTransaction();

            if (activeTransaction != null) {
              final expiry = _parseExpiry(activeTransaction.expirationDate);

              debugPrint("💳 Found ACTIVE transaction:");
              debugPrint("   Transaction ID: ${activeTransaction.id}");
              debugPrint("   Original ID: ${activeTransaction.originalId}");
              debugPrint("   Product ID: ${activeTransaction.productId}");
              debugPrint("   Purchase Date: ${activeTransaction.purchaseDate}");
              debugPrint("   Expires: $expiry");
            } else {
              debugPrint("⚠️ No active transaction found");
              debugPrint("   Falling back to PurchaseDetails for backend verification");
            }
          }

          if (isRestoring) {
            debugPrint("🔄 Restore Mode: Triggering onRestore callback");
            if (purchaseDetails.status == PurchaseStatus.restored || purchaseDetails.status == PurchaseStatus.purchased && onRestore != null) {
              onRestore!(purchaseDetails);
            }
          } else {
            final bool valid = await _verifyPurchase(purchaseDetails);
            if (valid) {
              debugPrint("valid : ${valid}");
              _deliverProduct(purchaseDetails);
            } else {
              _handleInvalidPurchase(purchaseDetails);
              return;
            }
          }
        }
        if (Platform.isAndroid) {
          if (!autoConsume) {
            final InAppPurchaseAndroidPlatformAddition androidAddition =
                _inAppPurchase.getPlatformAddition<
                    InAppPurchaseAndroidPlatformAddition>();
            await androidAddition.consumePurchase(purchaseDetails);
          }
        }
        if (purchaseDetails.pendingCompletePurchase) {
          await _inAppPurchase.completePurchase(purchaseDetails);
        }
      }
    }
  }

  // Show pending ui if any purchase pending.
  void showPendingUI() {
    purchasePending = true;
  }

  // Deliver product with receipt validation.
  Future<void> _deliverProduct(PurchaseDetails purchaseDetails) async {
    //await ConsumableStore.save(purchaseDetails.purchaseID!);
    purchasePending = false;
    if (onSuccess != null) {
      onSuccess!(purchaseDetails);
    }
  }

  // Handel error
  void handleError(IAPError error) {
    purchasePending = false;
    if (onError != null) {
      onError!(error);
    }
  }

  // Verify purchase with receipt validation.
  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) {
    // IMPORTANT!! Always verify a purchase before delivering the product.
    return Future<bool>.value(true);
  }

  // Handle invalid purchases.
  void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
    // handle invalid purchase here if  _verifyPurchase` failed.
  }

  Future<void> checkIosTransactions() async {
    if (!Platform.isIOS) return;

    try {
      final transactions = await SK2Transaction.transactions();
      final now = DateTime.now();

      debugPrint("📱 Total transactions found: ${transactions.length}");

      for (final tx in transactions) {
        final expiry = _parseExpiry(tx.expirationDate);

        final isActive = expiry != null && expiry.isAfter(now);

        if (isActive) {
          debugPrint("✅ ACTIVE Transaction:");
          debugPrint("   Transaction ID: ${tx.id}");
          debugPrint("   Original ID: ${tx.originalId}");
          debugPrint("   Product ID: ${tx.productId}");
          debugPrint("   Purchase Date: ${tx.purchaseDate}");
          debugPrint("   Expires: $expiry");
          break; // stop after first active subscription
        } else {
          debugPrint("⏰ Expired Transaction:");
          debugPrint("   Transaction ID: ${tx.id}");
          debugPrint("   Product ID: ${tx.productId}");
          debugPrint("   Expired At: $expiry");
        }
      }
    } catch (e) {
      debugPrint("❌ Error checking iOS transactions: $e");
    }
  }

  //  MAIN RESTORE METHOD
  Future<void> restorePurchases() async {
    debugPrint("🔄 Starting restore purchases...");

    if (!isAvailable) {
      debugPrint("❌ Store not available");
      if (onError != null) {
        onError!(IAPError(
          source: 'restore',
          code: 'store_not_available',
          message: 'Store is not available',
        ));
      }
      return;
    }

    isRestoring = true;
    activePurchases.clear();

    try {
      if (Platform.isAndroid) {
        await _restoreAndroidPurchases();
      } else if (Platform.isIOS) {
        await _restoreIOSPurchases();
      }
    } catch (e) {
      debugPrint("❌ Restore error: $e");
      isRestoring = false;
      if (onError != null) {
        onError!(IAPError(
          source: 'restore',
          code: 'restore_failed',
          message: e.toString(),
        ));
      }
    }
  }


  // 3. ANDROID RESTORE IMPLEMENTATION

  Future<void> _restoreAndroidPurchases() async {
    debugPrint("🤖 Restoring Android purchases...");

    final platform = _inAppPurchase
        .getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();

    final QueryPurchaseDetailsResponse pastPurchases =
    await platform.queryPastPurchases();

    if (pastPurchases.error != null) {
      debugPrint("❌ Error querying past purchases: ${pastPurchases.error}");
      isRestoring = false;
      if (onError != null) {
        onError!(pastPurchases.error!);
      }
      return;
    }

    debugPrint("📦 Found ${pastPurchases.pastPurchases.length} past purchases");

    bool foundActivePurchase = false;
    bool foundExpiredPurchase = false;

    for (final purchase in pastPurchases.pastPurchases) {
      debugPrint("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
      debugPrint("Checking purchase: ${purchase.productID}");
      debugPrint("Status: ${purchase.status}");
      debugPrint("Purchase ID: ${purchase.purchaseID}");

      // Check if it's a valid subscription from your product list
      if (_consumableIds.contains(purchase.productID)) {

        // ANDROID SPECIFIC: Check if subscription is active
        if (purchase is GooglePlayPurchaseDetails) {
          final isAutoRenewing = purchase.billingClientPurchase.isAutoRenewing;
          final isAcknowledged = purchase.billingClientPurchase.isAcknowledged;

          debugPrint("Auto-renewing: $isAutoRenewing");
          debugPrint("Acknowledged: $isAcknowledged");

          // Check purchase status
          if (purchase.status == PurchaseStatus.purchased) {
            // CRITICAL: For subscriptions, check if auto-renewing
            // If autoRenewing is false, subscription is cancelled/expired
            if (isAutoRenewing) {
              // Active subscription
              final bool valid = await _verifyPurchase(purchase);

              if (valid) {
                debugPrint("✅ ACTIVE subscription found: ${purchase.productID}");
                foundActivePurchase = true;
                activePurchases.add(purchase);

                // Trigger restore callback
                if (onRestore != null) {
                  onRestore!(purchase);
                }

                // Complete the purchase if needed
                if (purchase.pendingCompletePurchase) {
                  await _inAppPurchase.completePurchase(purchase);
                }
              }
            } else {
              // Subscription is cancelled or expired
              debugPrint("⚠️ EXPIRED/CANCELLED subscription: ${purchase.productID}");
              foundExpiredPurchase = true;
            }
          } else {
            debugPrint("⚠️ Purchase status is not 'purchased': ${purchase.status}");
          }
        }
      }
    }

    isRestoring = false;
    debugPrint("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

    if (!foundActivePurchase) {
      if (foundExpiredPurchase) {
        debugPrint("⚠️ Found expired/cancelled subscriptions but no active ones");
        if (onRestoreNoActive != null) {
          // onRestoreNoActive!("Your previous subscription has expired or been cancelled. Please purchase a new subscription.");
          onRestoreNoActive!("No previous purchases found to restore");
        } else if (onError != null) {
          onError!(IAPError(
            source: 'restore',
            code: 'subscription_expired',
            message: 'Your previous subscription has expired or been cancelled',
          ));
        }
      } else {
        debugPrint("⚠️ No purchases found at all");
        if (onError != null) {
          onError!(IAPError(
            source: 'restore',
            code: 'no_purchases',
            message: 'No previous purchases found to restore',
          ));
        }
      }
    } else {
      debugPrint("✅ Restore completed - found ${activePurchases.length} ACTIVE subscription(s)");
    }
  }

// ============================================
//  iOS RESTORE IMPLEMENTATION

  Future<void> _restoreIOSPurchases() async {
    debugPrint("🍎 Restoring iOS purchases...");

    // Call the native restore method
    await _inAppPurchase.restorePurchases();

    // Wait for the purchase stream to process restored purchases
    // The restored purchases will come through _listenToPurchaseUpdated
    await Future.delayed(Duration(seconds: 10));

    isRestoring = false;

    if (activePurchases.isEmpty) {
      debugPrint("⚠️ No active purchases found after restore");

      // For iOS, we need to check with your backend if subscription exists but expired
      // The restore might succeed but subscription could be expired
      if (onRestoreNoActive != null) {
        //  onRestoreNoActive!("No active subscription found. Your previous subscription may have expired.");
        onRestoreNoActive!("No previous purchases found to restore");
      } else if (onError != null) {
        onError!(IAPError(
          source: 'restore',
          code: 'no_active_subscription',
          message: 'No active subscription found to restore',
        ));
      }
    } else {
      debugPrint("✅ iOS Restore completed - found ${activePurchases.length} active purchase(s)");
    }
  }

  Future<ActiveSubscriptionInfo?> checkActiveSubscription() async {
    debugPrint("🔍 Checking for active subscription...");

    if (!isAvailable) {
      debugPrint("❌ Store not available");
      return null;
    }

    try {
      if (Platform.isAndroid) {
        return await _checkAndroidActiveSubscription();
      } else if (Platform.isIOS) {
        return await _checkIOSActiveSubscription();
      }
    } catch (e) {
      debugPrint("❌ Error checking subscription: $e");
      return null;
    }

    return null;
  }

  Future<ActiveSubscriptionInfo?> _checkAndroidActiveSubscription() async {
    debugPrint("🤖 Checking Android active subscription...");

    final platform = _inAppPurchase
        .getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();

    final QueryPurchaseDetailsResponse pastPurchases =
    await platform.queryPastPurchases();

    if (pastPurchases.error != null) {
      debugPrint("❌ Error querying past purchases: ${pastPurchases.error}");
      return null;
    }

    debugPrint("📦 Found ${pastPurchases.pastPurchases.length} past purchases");

    for (final purchase in pastPurchases.pastPurchases) {
      if (_consumableIds.contains(purchase.productID) &&
          purchase.status == PurchaseStatus.purchased) {

        if (purchase is GooglePlayPurchaseDetails) {
          final isAutoRenewing = purchase.billingClientPurchase.isAutoRenewing;
          final purchaseTime = purchase.billingClientPurchase.purchaseTime;

          debugPrint("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
          debugPrint("Product ID: ${purchase.productID}");
          debugPrint("Auto-renewing: $isAutoRenewing");
          debugPrint("Purchase Time: $purchaseTime");

          if (isAutoRenewing) {
            final bool valid = await _verifyPurchase(purchase);
            if (valid) {
              // Get product details for pricing info
              final productDetails = _products.firstWhereOrNull(
                    (p) => p.id == purchase.productID,
              );

              debugPrint("✅ ACTIVE subscription found: ${purchase.productID}");
              debugPrint("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

              return ActiveSubscriptionInfo(
                productId: purchase.productID,
                purchaseId: purchase.purchaseID ?? '',
                isActive: true,
                isAutoRenewing: true,
                purchaseToken: purchase.verificationData.serverVerificationData,
                purchaseDate: DateTime.fromMillisecondsSinceEpoch(purchaseTime),
                productPrice: productDetails?.price,
                productTitle: productDetails?.title,
                planType: _getPlanType(purchase.productID),
              );
            }
          } else {
            debugPrint("⚠️ Subscription exists but is cancelled/expired");
            debugPrint("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
          }
        }
      }
    }

    debugPrint("⚠️ No active subscription found");
    return null;
  }

  Future<ActiveSubscriptionInfo?> _checkIOSActiveSubscription() async {
    debugPrint("Checking iOS active subscription...");

    try {
      // Using StoreKit 2 for accurate subscription status
      final SK2Transaction? activeTransaction = await getLatestActiveTransaction();

      if (activeTransaction != null) {
        // Get product details
        final productDetails = _products.firstWhereOrNull(
              (p) => p.id == activeTransaction.productId,
        );

        final expiry = _parseExpiry(activeTransaction.expirationDate);

        debugPrint("✅ ACTIVE iOS subscription found");
        debugPrint("Product ID: ${activeTransaction.productId}");
        debugPrint("Transaction ID: ${activeTransaction.id}");
        debugPrint("Original ID: ${activeTransaction.originalId}");
        debugPrint("Expires: $expiry");

        return ActiveSubscriptionInfo(
          productId: activeTransaction.productId,
          purchaseId: activeTransaction.id.toString(),
          originalTransactionId: activeTransaction.originalId.toString(),
          isActive: true,
          isAutoRenewing: true, // If not expired, assume auto-renewing
          purchaseDate: DateTime.tryParse(activeTransaction.purchaseDate ?? '') ?? DateTime.now(),
          expiryDate: expiry,
          productPrice: productDetails?.price,
          productTitle: productDetails?.title,
          planType: _getPlanType(activeTransaction.productId),
        );
      }

      // Fallback: Check purchase stream
      if (activePurchases.isNotEmpty) {
        final purchase = activePurchases.first;
        final productDetails = _products.firstWhereOrNull(
              (p) => p.id == purchase.productID,
        );

        return ActiveSubscriptionInfo(
          productId: purchase.productID,
          purchaseId: purchase.purchaseID ?? '',
          isActive: true,
          isAutoRenewing: true,
          productPrice: productDetails?.price,
          productTitle: productDetails?.title,
          planType: _getPlanType(purchase.productID),
        );
      }

      debugPrint("⚠️ No active iOS subscription found");
      return null;
    } catch (e) {
      debugPrint("❌ Error checking iOS subscription: $e");
      return null;
    }
  }

  /// Helper to determine plan type from product ID
  String _getPlanType(String productId) {
    if (Platform.isAndroid) {
      if (productId == androidYear || androidPaywallSubscriptionUpgradeIds.contains(productId)) {
        return 'Annual';
      } else if (productId == androidMonth) {
        return 'Monthly';
      }
    } else {
      if (productId == iOSYear || iOSPaywallSubscriptionUpgradeIds.contains(productId)) {
        return 'Annual';
      } else if (productId == iOSMonth) {
        return 'Monthly';
      }
    }
    return 'Unknown';
  }


// Show dialog when active subscription exists


  ///-----------------



  DateTime? _parseExpiry(dynamic expiry) {
    if (expiry == null) return null;
    if (expiry is DateTime) return expiry;
    if (expiry is String) return DateTime.tryParse(expiry);
    return null;
  }

  Future<SK2Transaction?> getLatestActiveTransaction() async {
    if (!Platform.isIOS) return null;

    try {
      final transactions = await SK2Transaction.transactions();

      if (transactions.isEmpty) {
        debugPrint("⚠️ No transactions found");
        return null;
      }

      final now = DateTime.now();

      // Sort by purchase date DESC (latest first)
      transactions.sort((a, b) {
        final aDate = DateTime.tryParse(a.purchaseDate ?? '') ?? DateTime(0);
        final bDate = DateTime.tryParse(b.purchaseDate ?? '') ?? DateTime(0);
        return bDate.compareTo(aDate);
      });

      for (final tx in transactions) {
        final expiry = _parseExpiry(tx.expirationDate);

        debugPrint("""🔍 Checking transaction id: ${tx.id} originalId: ${tx.originalId} productId: ${tx.productId}purchaseDate: ${tx.purchaseDate}expirationDate: ${tx.expirationDate}""");

        if (expiry != null && expiry.isAfter(now)) {
          debugPrint("✅ Active subscription found");
          return tx;
        }
      }

      debugPrint("⚠️ No active subscription found");
      return null;
    } catch (e) {
      debugPrint("❌ Error checking transactions: $e");
      return null;
    }
  }

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
 Flutter (Channel stable, 3.35.4, on macOS 15.5 24F74 darwin-arm64, locale en-IN) [2.6s]
    • Flutter version 3.35.4 on channel stable at /Users/hyperlink/fvm/versions/3.35.4
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision d693b4b9db (4 months ago), 2025-09-16 14:27:41 +0000
    • Engine revision c298091351
    • Dart version 3.9.2
    • DevTools version 2.48.0
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations,
      enable-lldb-debugging

Activity

  1. khaled-0 commented on Jan 27, 2026

    @khaled-0

    What a coincident!
    We are hitting the same bug, Downgrading solved the issue.

    Looking at the change log, the only difference seems to be the StoreKit API V1 vs V2
    Our backend were still using StoreKit V1

    So calling InAppPurchaseStoreKitPlatform.enableStoreKit1() might solve the issue but We have not tried it yet.

  2. Nehal-1234 commented on Jan 27, 2026

    @Nehal-1234
    Author

    What a coincident! We are hitting the same bug, Downgrading solved the issue.

    we are still seeing the same issue. Did you find anything in the code ?

  3. khaled-0 commented on Jan 27, 2026

    @khaled-0

    What a coincident! We are hitting the same bug, Downgrading solved the issue.

    we are still seeing the same issue. Did you find anything in the code ?

    try in_app_purchase_storekit: 0.3.20 We got it to work for sandbox, earlier we were seeing malformed access-token error from our backend.

  4. tirth-patel-nc commented on Jan 28, 2026

    @tirth-patel-nc
    Member

    Please confirm whether #98910 #129881 #98484 matches your case?

  5. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jan 28, 2026
  6. khaled-0 commented on Jan 28, 2026

    @khaled-0

    Please confirm whether #98910 #129881 #98484 matches your case?

    nope. in my case the serverVerificationToken is incorrect in version > 0.3.21

  7. Nehal-1234 commented on Jan 28, 2026

    @Nehal-1234
    Author

    Please confirm whether #98910 #129881 #98484 matches your case?

    nope. in my case the serverVerificationToken is incorrect in version > 0.3.21

    dependency_overrides:
    in_app_purchase_storekit: 0.3.21
    This setup works correctly in the Sandbox environment, but production users are facing issues

  8. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jan 28, 2026
  9. added
    platform-iosiOS applications specifically
    packageflutter/packages repository. See also p: labels.
    team-iosOwned by iOS platform team
    and removed
    in triagePresently being triaged by the triage team
    on Jan 29, 2026
  10. added
    P2Important issues not at the top of the work list
    triaged-iosTriaged by iOS platform team
    on Jan 29, 2026
  11. Nehal-1234 commented on Feb 4, 2026

    @Nehal-1234
    Author

    Hello @LouiseHsu , @khaled-0 @tirth-patel-nc @ianloic Is there any other way to resolve this? Because in our app, users are getting stuck and are unable to proceed to the next steps

  12. dimad2602 commented on Feb 5, 2026

    @dimad2602

    Hello @Nehal-1234, @khaled-0, @tirth-patel-nc, @LouiseHsu, @ianloic,

    I am facing a very similar, if not identical, issue that I've previously reported here: #169744

    My observations align with yours regarding outdated receipt data after re-subscription:

    1. I consistently received outdated purchaseId and transaction data in the purchaseStream, even after clearing purchases from the Sandbox test account.
    2. The appAccountToken / appTransactionId would often remain the same or return an old value across distinct purchases, breaking backend logic.
    3. I tested this behavior thoroughly with both StoreKit1 (by enabling InAppPurchaseStoreKitPlatform.enableStoreKit1()) and StoreKit2 (default). While StoreKit1 sometimes showed the correct productId in the stream, the purchaseId itself was still outdated, indicating a deeper issue beyond just the StoreKit API version.
  13. ihr1337 commented on Feb 17, 2026

    @ihr1337

    The issue is still there. Old receipt is returning with status .restored, purchase completion is not needed.
    My app is not in production yet, so I met this in Sandbox only.

    Downgrading did help.
    Thanks for workaround and hope this will be fixed soon.

  14. snoopdoggy322 commented on Mar 24, 2026

    @snoopdoggy322

    I encountered the same issue, but I was getting "old" subscription renewal transactions returned even in a sandbox environment.
    Running the finish method on all transactions helped, and after that, they stopped returning to purchaseStream every time I tried to purchase a plan.

      #purchases
      in_app_purchase: ^3.2.3
      in_app_purchase_storekit: ^0.4.8+1
      Future<void> finishTransaction(PurchaseDetails transaction) async {
        if (Platform.isIOS && transaction is SK2PurchaseDetails && transaction.purchaseID != null) {
          final id = int.tryParse(transaction.purchaseID!);
          if (id != null) {
            try {
              SK2Transaction.finish(id);
            } catch (e) {
              log('failed to finish transaction -$id');
            }
          }
        }
      }
    

    I can't say what caused this behavior, but after validating the transactions, I also added the finish method and haven't encountered this issue since.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listcustomer: solarisp: in_app_purchasePlugin for in-app purchasepackageflutter/packages repository. See also p: labels.platform-iosiOS applications specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions