Anonymous
Почему предыдущие покупки запускают прослушиватель покупок в приложении во время инициализации во Flutter (iOS)?
Сообщение
Anonymous » 24 янв 2025, 13:01
Flutter iOS: прослушиватель покупок в приложении, отражающий старые покупки при инициализации
Привет всем!
Я интегрирую покупки в приложении в свое приложение Flutter с помощью пакета in_app_purchase . Я успешно настроил все как для Google Play Store, так и для Apple App Store в тестовой среде.
Текущая настройка
Покупки работают нормально на обеих платформах. Я могу совершить покупку по подписке, и это правильно отображается в Google Play и в песочнице iOS.
Я создал контроллер Getx, в котором инициализирую прослушиватель покупок и информацию о магазине. Ниже приведен соответствующий код:
Код: Выделить всё
Listener Initialization in onInit:
late InAppPurchasePlatform _inAppPurchasePlatform;
@override
Future onInit() async {
_inAppPurchasePlatform = InAppPurchasePlatform.instance;
final Stream purchaseUpdated =
_inAppPurchasePlatform.purchaseStream;
_subscription0 =
purchaseUpdated.listen((List purchaseDetailsList) {
_handlePurchaseUpdates(purchaseDetailsList);
}, onDone: () {
_subscription0.cancel();
print("purchase listener onDone :");
}, onError: (Object error) {
print("purchase listener erro :" + error.toString());
});
initStoreInfo();
super.onInit();
}
Реализация initStoreInfo:
Код: Выделить всё
Future initStoreInfo() async {
print("initStoreInfo called:");
try {
final bool isAvailable = await _inAppPurchasePlatform.isAvailable();
if (!isAvailable) {
error.value = 'Store is unavailable. Please try again later.';
return;
}
print("Store is available.");
final List fetchedPlans = await fetchIAPPlanModel();
plans.addAll(fetchedPlans);
_kProductIds.addAll(fetchedPlans
.map((plan) => plan.productId ?? '')
.where((id) => id.isNotEmpty));
final ProductDetailsResponse productDetailResponse =
await _inAppPurchasePlatform
.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
error.value = productDetailResponse.error!.message!;
return;
}
if (productDetailResponse.productDetails.isEmpty) {
error.value = 'No products available at the moment.';
return;
}
// print(
// "Product Details Fetched: ${productDetailResponse.productDetails.map((e) =>
e.rawPrice).toList()}");
_linkPlansWithProductDetails(productDetailResponse.productDetails);
if (GetPlatform.isAndroid) {
// Set user choice billing
final InAppPurchaseAndroidPlatformAddition addition =
InAppPurchasePlatformAddition.instance!
as InAppPurchaseAndroidPlatformAddition;
unawaited(
addition.setBillingChoice(BillingChoiceMode.userChoiceBilling));
} else if (Platform.isIOS) {
//write code for ios
}
} catch (e) {
print("Error initializing store: $e");
error.value = "Error initializing store: $e";
isLoading.value = false;
return;
}
}
< /code>
Функция _handlepurchaseupdates < /p>
void _handlePurchaseUpdates(List
purchaseDetailsList) async {
print("purchaseDetailsList : " + purchaseDetailsList.length.toString());
final list = _plansController.subscriptionsList
.map((e) => e.transactionIds)
.toList();
list.map((e) => print("listitems : " + e.toString()));
print("list : " + list.toString());
print("list : " + list.length.toString());
for (final PurchaseDetails details in purchaseDetailsList) {
purchases[details.productID] = details;
var purchaseID = details.purchaseID;
print(" purchaseID:" + purchaseID.toString());
print("PurchaseDetails.status :" + details.status.toString());
if (details.pendingCompletePurchase) {
try {
await _inAppPurchasePlatform.completePurchase(details);
} catch (e) {
print("Error completing purchase: $e");
}
}
if (details.status == PurchaseStatus.canceled) {
purchasePending.value = false;
isLoading.value = false;
isButtonClicked.value = false;
return;
}
// Print error details if the purchase failed
if (details.status == PurchaseStatus.error) {
error.value = details.error?.message ?? "Unknown error occurred.";
purchasePending.value = false;
isLoading.value = false;
isButtonClicked.value = false;
Get.snackbar(
'Purchase Error',
'',
backgroundColor: Colors.red,
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3),
);
// Check if the error is due to "itemAlreadyOwned"
if (details.error?.message == "BillingResponse.itemAlreadyOwned") {
// Show dialog to the user
_showAlreadyOwnedDialog();
}
print("PurchaseStatus.error " + details.error!.message.toString());
return;
}
isLoading.value = true;
if (details.status == PurchaseStatus.pending) {
purchasePending.value = true;
return;
} else if (details.status == PurchaseStatus.error) {
error.value = details.error?.message ?? "Unknown error occurred.";
purchasePending.value = false;
await _sendPurchaseToBackend(details, false);
} else if (details.status == PurchaseStatus.restored) {
error.value = details.error?.message ?? "Subscription restored";
isButtonClicked.value = false;
return;
} else if (details.status == PurchaseStatus.purchased) {
// final bool isValid = await _verifyPurchase(details);
activePlanId.value = details.productID;
purchasePending.value = true;
// Notify backend about the successful transaction
await _sendPurchaseToBackend(details, true);
_processedPurchases.add(details.productID);
}
if (details.pendingCompletePurchase) {
await _inAppPurchasePlatform.completePurchase(details);
await fetchUserActivePlan();
}
}
< /code>
} < /p>
Проблема
на iOS, как только приложение запускается, и контроллер Getx инициализируется, покупка в приложении Слушатель (BoickAseStream) также инициализируется. Тем не менее, слушатель сразу же предоставляет купленный detailslist, содержащий предыдущие покупки, даже несмотря на то, что новая плата не произведена. < /P>
> bookasestatus. Приобретенен
Это поведение проблематично, потому что я не могу различать старые покупки (из предыдущих сессий) и новых покупок. < /p>
То, что я пробовал
Я запускаю всплывающее окно счета через кнопку после запуска, которая работает нормально. Но эта проблема возникает до того, как будет совершена какая -либо новая покупка. < /P>
Мой вопрос
Почему слушатель Boickestream возвращает предыдущие покупки на iOS во время инициализации? < /P>
< P> Как я могу различить старые покупки и новые? P> Любая помощь или альтернативные идеи реализации будут высоко оценены! < /p>
Результат для < /p>
print("old originalT :" +
details.skPaymentTransaction.originalTransaction!.toString());
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
}, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
}
см. полный ответ
Подробнее здесь:
https://stackoverflow.com/questions/793 ... initializa
1737712897
Anonymous
Flutter iOS: прослушиватель покупок в приложении, отражающий старые покупки при инициализации Привет всем! Я интегрирую покупки в приложении в свое приложение Flutter с помощью пакета in_app_purchase . Я успешно настроил все как для Google Play Store, так и для Apple App Store в тестовой среде. Текущая настройка Покупки работают нормально на обеих платформах. Я могу совершить покупку по подписке, и это правильно отображается в Google Play и в песочнице iOS. Я создал контроллер Getx, в котором инициализирую прослушиватель покупок и информацию о магазине. Ниже приведен соответствующий код: [code] Listener Initialization in onInit: late InAppPurchasePlatform _inAppPurchasePlatform; @override Future onInit() async { _inAppPurchasePlatform = InAppPurchasePlatform.instance; final Stream purchaseUpdated = _inAppPurchasePlatform.purchaseStream; _subscription0 = purchaseUpdated.listen((List purchaseDetailsList) { _handlePurchaseUpdates(purchaseDetailsList); }, onDone: () { _subscription0.cancel(); print("purchase listener onDone :"); }, onError: (Object error) { print("purchase listener erro :" + error.toString()); }); initStoreInfo(); super.onInit(); } [/code] Реализация initStoreInfo: [code] Future initStoreInfo() async { print("initStoreInfo called:"); try { final bool isAvailable = await _inAppPurchasePlatform.isAvailable(); if (!isAvailable) { error.value = 'Store is unavailable. Please try again later.'; return; } print("Store is available."); final List fetchedPlans = await fetchIAPPlanModel(); plans.addAll(fetchedPlans); _kProductIds.addAll(fetchedPlans .map((plan) => plan.productId ?? '') .where((id) => id.isNotEmpty)); final ProductDetailsResponse productDetailResponse = await _inAppPurchasePlatform .queryProductDetails(_kProductIds.toSet()); if (productDetailResponse.error != null) { error.value = productDetailResponse.error!.message!; return; } if (productDetailResponse.productDetails.isEmpty) { error.value = 'No products available at the moment.'; return; } // print( // "Product Details Fetched: ${productDetailResponse.productDetails.map((e) => e.rawPrice).toList()}"); _linkPlansWithProductDetails(productDetailResponse.productDetails); if (GetPlatform.isAndroid) { // Set user choice billing final InAppPurchaseAndroidPlatformAddition addition = InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; unawaited( addition.setBillingChoice(BillingChoiceMode.userChoiceBilling)); } else if (Platform.isIOS) { //write code for ios } } catch (e) { print("Error initializing store: $e"); error.value = "Error initializing store: $e"; isLoading.value = false; return; } } < /code> Функция _handlepurchaseupdates < /p> void _handlePurchaseUpdates(List purchaseDetailsList) async { print("purchaseDetailsList : " + purchaseDetailsList.length.toString()); final list = _plansController.subscriptionsList .map((e) => e.transactionIds) .toList(); list.map((e) => print("listitems : " + e.toString())); print("list : " + list.toString()); print("list : " + list.length.toString()); for (final PurchaseDetails details in purchaseDetailsList) { purchases[details.productID] = details; var purchaseID = details.purchaseID; print(" purchaseID:" + purchaseID.toString()); print("PurchaseDetails.status :" + details.status.toString()); if (details.pendingCompletePurchase) { try { await _inAppPurchasePlatform.completePurchase(details); } catch (e) { print("Error completing purchase: $e"); } } if (details.status == PurchaseStatus.canceled) { purchasePending.value = false; isLoading.value = false; isButtonClicked.value = false; return; } // Print error details if the purchase failed if (details.status == PurchaseStatus.error) { error.value = details.error?.message ?? "Unknown error occurred."; purchasePending.value = false; isLoading.value = false; isButtonClicked.value = false; Get.snackbar( 'Purchase Error', '', backgroundColor: Colors.red, snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 3), ); // Check if the error is due to "itemAlreadyOwned" if (details.error?.message == "BillingResponse.itemAlreadyOwned") { // Show dialog to the user _showAlreadyOwnedDialog(); } print("PurchaseStatus.error " + details.error!.message.toString()); return; } isLoading.value = true; if (details.status == PurchaseStatus.pending) { purchasePending.value = true; return; } else if (details.status == PurchaseStatus.error) { error.value = details.error?.message ?? "Unknown error occurred."; purchasePending.value = false; await _sendPurchaseToBackend(details, false); } else if (details.status == PurchaseStatus.restored) { error.value = details.error?.message ?? "Subscription restored"; isButtonClicked.value = false; return; } else if (details.status == PurchaseStatus.purchased) { // final bool isValid = await _verifyPurchase(details); activePlanId.value = details.productID; purchasePending.value = true; // Notify backend about the successful transaction await _sendPurchaseToBackend(details, true); _processedPurchases.add(details.productID); } if (details.pendingCompletePurchase) { await _inAppPurchasePlatform.completePurchase(details); await fetchUserActivePlan(); } } < /code> } < /p> Проблема на iOS, как только приложение запускается, и контроллер Getx инициализируется, покупка в приложении Слушатель (BoickAseStream) также инициализируется. Тем не менее, слушатель сразу же предоставляет купленный detailslist, содержащий предыдущие покупки, даже несмотря на то, что новая плата не произведена. < /P> > bookasestatus. Приобретенен Это поведение проблематично, потому что я не могу различать старые покупки (из предыдущих сессий) и новых покупок. < /p> То, что я пробовал Я запускаю всплывающее окно счета через кнопку после запуска, которая работает нормально. Но эта проблема возникает до того, как будет совершена какая -либо новая покупка. < /P> Мой вопрос Почему слушатель Boickestream возвращает предыдущие покупки на iOS во время инициализации? < /P> < P> Как я могу различить старые покупки и новые? P> Любая помощь или альтернативные идеи реализации будут высоко оценены! < /p> Результат для < /p> print("old originalT :" + details.skPaymentTransaction.originalTransaction!.toString()); flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null } [/code] см. полный ответ Подробнее здесь: [url]https://stackoverflow.com/questions/79383409/why-do-previous-purchases-trigger-the-in-app-purchase-listener-during-initializa[/url]