Repository navigation
Old Receipt Data Returned After Re-Subscription in Production #181530
Description
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
Labels
Type
Projects
- StatusShow more project fieldsIn progress
Activity
khaled-0 commentedon Jan 27, 2026
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.Nehal-1234 commentedon Jan 27, 2026
we are still seeing the same issue. Did you find anything in the code ?
khaled-0 commentedon Jan 27, 2026
try
in_app_purchase_storekit: 0.3.20We got it to work for sandbox, earlier we were seeingmalformed access-tokenerror from our backend.tirth-patel-nc commentedon Jan 28, 2026
Please confirm whether #98910 #129881 #98484 matches your case?
khaled-0 commentedon Jan 28, 2026
nope. in my case the serverVerificationToken is incorrect in version > 0.3.21
Nehal-1234 commentedon Jan 28, 2026
dependency_overrides:
in_app_purchase_storekit: 0.3.21
This setup works correctly in the Sandbox environment, but production users are facing issues
Nehal-1234 commentedon Feb 4, 2026
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
dimad2602 commentedon Feb 5, 2026
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:
purchaseIdandtransaction datain thepurchaseStream, even after clearing purchases from the Sandbox test account.appAccountToken/appTransactionIdwould often remain the same or return an old value across distinct purchases, breaking backend logic.StoreKit1(by enablingInAppPurchaseStoreKitPlatform.enableStoreKit1()) andStoreKit2(default). WhileStoreKit1sometimes showed the correctproductIdin the stream, thepurchaseIditself was still outdated, indicating a deeper issue beyond just the StoreKit API version.ihr1337 commentedon Feb 17, 2026
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.
snoopdoggy322 commentedon Mar 24, 2026
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.
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.
[in_app_purchase_storekit] Expose expiration date directly for subscr…