Why does Flutter In-App Purchase not return base plans in restore purchase?
02:03 13 Dec 2025

Why basePlanId Is Missing on Restored Purchases

✅ Expected Platform Behavior (Not a Bug)

On Android / Google Play, when restoring a subscription using Flutter In-App Purchase, the base plan details are NOT returned.
The PurchaseDetails object only contains:

  • productID

  • purchaseID

  • transactionDate

  • status

⚠️ All base plans under the same product share the same productID, so it is impossible to determine the original plan from the restore payload alone.

Future _handleAndroidPurchases(List purchaseDetailsList) async {
customLoggerFunction("=== _handleAndroidPurchases() ===");
customLoggerFunction("Received ${purchaseDetailsList.length} purchase updates");

for (final purchase in purchaseDetailsList) {
  customLoggerFunction("Processing purchase: ${purchase.productID} | status: ${purchase.status}");

  // Complete pending purchases immediately
  if (purchase.status == PurchaseStatus.pending) {
    customLoggerFunction("Pending purchase: ${purchase.productID}");
    try {
      await _iap.completePurchase(purchase);
      customLoggerFunction("✅ Pending purchase completed");
    } catch (e) {
      customLoggerFunction("❌ Failed to complete pending purchase: $e");
    }
    continue;
  }

  // Ignore error or canceled
  if (purchase.status == PurchaseStatus.error || purchase.status == PurchaseStatus.canceled) {
    customLoggerFunction("Ignored: ${purchase.status}");
    continue;
  }

  // Only process purchased (new) or restored
  if (purchase.status != PurchaseStatus.purchased && purchase.status != PurchaseStatus.restored) {
    continue;
  }

  // Duplicate detection using transaction ID
  final transactionId = purchase.purchaseID ?? purchase.transactionDate ?? "";
  if (transactionId.isNotEmpty && _processedTransactions.contains(transactionId)) {
    customLoggerFunction("⭐️ Skipping already processed transaction: $transactionId");
    continue;
  }

  // Prevent concurrent activation
  if (_isActivatingPurchase) {
    customLoggerFunction("⚠️ Already activating a purchase, skipping this one to avoid race");
    continue;
  }

  // Mark as processed early (before activation)
  if (transactionId.isNotEmpty) {
    _processedTransactions.add(transactionId);
    customLoggerFunction("📝 Marked transaction as processed: $transactionId");
  }


 else if (purchase.status == PurchaseStatus.restored) {
        // ─────── RESTORE ───────
        // For restores: rely on saved plan type from previous successful purchase
        customLoggerFunction("🔄 RESTORE flow");

        PremiumPlanVariables? restoredPlan;

        // Strategy 1: Load saved plan type (this is what worked in your old code)
        try {
          final savedPlanType = await SecureStorageService.readSecureData(AppLocalKeys.sPlanType);
          customLoggerFunction("💾 Saved plan type from storage: $savedPlanType");

          if (savedPlanType != null) {
            restoredPlan = switch (savedPlanType) {
              "weekly" => PremiumPlanVariables.weekly,
              "monthly" => PremiumPlanVariables.monthly,
              "yearly" => PremiumPlanVariables.yearly,
              _ => PremiumPlanVariables.monthly,
            };
            customLoggerFunction("✅ Loaded restored plan from storage: $restoredPlan");
          }
        } catch (e) {
          customLoggerFunction("⚠️ Failed to read saved plan type: $e");
        }

        // Strategy 2: If no saved plan, query offers and use a safe fallback (prefer longer plans)
        if (restoredPlan == null) {
          customLoggerFunction("No saved plan → querying product details for fallback");

          try {
            final response = await _iap.queryProductDetails({purchase.productID}.toSet());
            if (response.productDetails.isNotEmpty) {
              customLoggerFunction("✅ Product details query succeeded");
              customLoggerFunction("Product Detailss ${response.productDetails.length}");
              for(final product in response.productDetails) {
                customLoggerFunction("Product Id ${product.id}");
                customLoggerFunction("Product Title ${product.title}");
                customLoggerFunction("Product Price ${product.price}");
              }
              final product = response.productDetails.first;
              // final product = response.productDetails.first;
              if (product is GooglePlayProductDetails) {
                final offers = product.productDetails.subscriptionOfferDetails;
                if (offers != null && offers.isNotEmpty) {
                  // Prefer yearly > monthly > weekly to avoid showing "Weekly" wrongly
                  final order = ['yearly', 'monthly', 'weekly'];
                  for (final plan in order) {
                    final match = offers.firstWhereOrNull(
                          (o) => o.basePlanId.toLowerCase() == plan || o.basePlanId.toLowerCase().contains(plan) == true,
                    );
                    if (match != null) {
                      restoredPlan = switch (plan) {
                        'yearly' => PremiumPlanVariables.yearly,
                        'monthly' => PremiumPlanVariables.monthly,
                        'weekly' => PremiumPlanVariables.weekly,
                        _ => PremiumPlanVariables.monthly,
                      };
                      customLoggerFunction("Fallback preferred plan from offers: $restoredPlan");
                      break;
                    }
                  }

                  // Ultimate fallback
                  if (restoredPlan == null) {
                    final firstBasePlanId = offers.first.basePlanId ?? 'monthly';
                    customLoggerFunction("🎲 Ultimate fallback using first offer: $firstBasePlanId");
                    restoredPlan = switch (firstBasePlanId.toLowerCase()) {
                      _ when firstBasePlanId.toLowerCase().contains('weekly') => PremiumPlanVariables.weekly,
                      _ when firstBasePlanId.toLowerCase().contains('monthly') => PremiumPlanVariables.monthly,
                      _ when firstBasePlanId.toLowerCase().contains('yearly') => PremiumPlanVariables.yearly,
                      _ => PremiumPlanVariables.monthly,
                    };
                  }
                }
              }
            }
          } catch (e, s) {
            
          }
        }

        // Final fallback for restore
        restoredPlan ??= PremiumPlanVariables.monthly;
        

        selectedAndroidPlan = restoredPlan;

        // Activate restore (no acknowledge needed)
        await _verifyAndActivatePlan(purchase.productID, purchase);

        
      }
android flutter in-app-purchase restore in-app-subscription