Почему Flutter In-App Purchase не возвращает базовые планы при восстановлении покупки?Android

Форум для тех, кто программирует под Android
Ответить
Anonymous
 Почему Flutter In-App Purchase не возвращает базовые планы при восстановлении покупки?

Сообщение Anonymous »

Почему basePlanId отсутствует при восстановленных покупках
✅ Ожидаемое поведение платформы (не ошибка)
В Android / Google Play при восстановлении подписки с помощью Покупки в приложении Flutter детали базового плана НЕ возвращаются.

Объект PurchaseDetails содержит только:
  • productID
  • purchaseID
  • transactionDate
  • status
⚠️ Все базовые планы в рамках одного и того же продукта имеют один и тот же идентификатор продукта, поэтому невозможно определить исходный план только по полезной нагрузке восстановления.
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);

}


Подробнее здесь: https://stackoverflow.com/questions/798 ... e-purchase
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Android»