Skip to content

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listp: 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
    No fields configured for issues without a type.

    Projects

    Status
    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions