Bonjour à tous,
J’ai un petit problème quelque peu embêtant sur mon app: les achats InApp s’ajoutent en fonction du nombre acheté: si j’achète un article, puis un autre article, lors de la livraison du second, j’en reçois 2 soit 3 en tout pour 2 acheté…
Ici, les 2 différents articles: un petit café, et un mug de café:
J’achète un petit café:
Je reçois mon café:
J’en achète un second et le reçois, jusque là tout va bien:
Enfin je prends un mug:
Et là, j’en reçois 3…
Je ne comprend pas…
Voici le code assemblé:
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
import 'consumable_store.dart';
const bool _kAutoConsume = true;
const String _kConsumableId1 = 'support_dev_1';
const String _kConsumableId2 = 'support_dev_2';
const String _kUpgradeId = 'upgrade';
const String _kSilverSubscriptionId = 'subscription_silver';
const String _kGoldSubscriptionId = 'subscription_gold';
const List<String> _kProductIds = <String>[
_kConsumableId1,
_kConsumableId2,
_kUpgradeId,
];
String lastConsumesId = "";
class Purchase extends StatefulWidget {
const Purchase({Key? key}) : super(key: key);
@override
_PurchaseState createState() => _PurchaseState();
}
class _PurchaseState extends State<Purchase> {
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<String> _notFoundIds = [];
List<ProductDetails> _products = [];
List<PurchaseDetails> _purchases = [];
List<String> _consumables = [];
List<String> _consumables2 = [];
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;
String? _queryProductError;
@override
void initState() {
final Stream<List<PurchaseDetails>> purchaseUpdated =
_inAppPurchase.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
return const AlertDialog(
title: Text("Votre commande est annulée"),
);
// handle error here.
});
initStoreInfo();
super.initState();
}
Future<void> initStoreInfo() async {
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
setState(() {
_isAvailable = isAvailable;
_products = [];
_purchases = [];
_notFoundIds = [];
_consumables = [];
_consumables2 = [];
_purchasePending = false;
_loading = false;
});
return;
}
if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
}
ProductDetailsResponse productDetailResponse =
await _inAppPurchase.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
setState(() {
_queryProductError = productDetailResponse.error!.message;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = [];
_notFoundIds = productDetailResponse.notFoundIDs;
_consumables = [];
_consumables2 = [];
_purchasePending = false;
_loading = false;
});
return;
}
if (productDetailResponse.productDetails.isEmpty) {
setState(() {
_queryProductError = null;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = [];
_notFoundIds = productDetailResponse.notFoundIDs;
_consumables = [];
_consumables2 = [];
_purchasePending = false;
_loading = false;
});
return;
}
List<String> consumables = await ConsumableStore.load();
setState(() {
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_notFoundIds = productDetailResponse.notFoundIDs;
_consumables = consumables;
_purchasePending = false;
_loading = false;
});
List<String> consumables2 = await ConsumableStore.load();
setState(() {
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_notFoundIds = productDetailResponse.notFoundIDs;
_consumables2 = consumables2;
_purchasePending = false;
_loading = false;
});
}
@override
void dispose() {
if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
iosPlatformAddition.setDelegate(null);
}
_subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
List<Widget> stack = [];
if (_queryProductError == null) {
stack.add(
ListView(
children: [
_buildConnectionCheckTile(),
_buildProductList(),
_buildConsumableBox(),
// _buildRestoreButton(),
],
),
);
} else {
stack.add(Center(
child: Text(_queryProductError!),
));
}
// if (_purchasePending) {
// stack.add(
// Stack(
// children: const [
// Opacity(
// opacity: 0.3,
// child: ModalBarrier(dismissible: false, color: Colors.grey),
// ),
// Center(
// child: CircularProgressIndicator(),
// ),
// ],
// ),
// );
// }
return Scaffold(
backgroundColor: Colors.blue[50],
appBar: AppBar(
backgroundColor: Colors.blue[900],
title: const Text("APCA-Conseil"),
),
body: Stack(children: stack),
);
}
Card _buildConnectionCheckTile() {
if (_loading) {
return const Card(
child: ListTile(title: Text('Ouverture de la cafétaria...')));
}
final Widget storeHeader = ListTile(
leading: Icon(_isAvailable ? Icons.coffee : Icons.coffee,
size: 50,
color: _isAvailable ? Colors.green : ThemeData.light().errorColor),
title: Text(
'La caféteria est ' + (_isAvailable ? 'ouverte' : 'fermée') + '.'),
);
final List<Widget> children = <Widget>[storeHeader];
if (!_isAvailable) {
children.addAll([
const Divider(),
ListTile(
title: Text("Vous n'êtes pas connecté...",
style: TextStyle(color: ThemeData.light().errorColor)),
),
]);
}
return Card(child: Column(children: children));
}
Card _buildProductList() {
if (_loading) {
return const Card(
child: (ListTile(
leading: CircularProgressIndicator(),
title: Text('Chargement de la carte des boissons...'))));
}
if (!_isAvailable) {
return const Card();
}
const ListTile productHeader = ListTile(
title: Text(
'Quelle taille de café ?',
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"L'auteur de cette application n'étant pas rénuméré, mais étant un grand consommateur de café, vous pouvez lui offrir un café."),
);
List<ListTile> productList = <ListTile>[];
// if (_notFoundIds.isNotEmpty) {
// productList.add(ListTile(
// title: Text('[${_notFoundIds.join(", ")}] not found',
// style: TextStyle(color: ThemeData.light().errorColor)),
// subtitle: const Text(
// 'This app needs special configuration to run. Please see example/README.md for instructions.')));
// }
// This loading previous purchases code is just a demo. Please do not use this as it is.
// In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it.
// We recommend that you use your own server to verify the purchase data.
Map<String, PurchaseDetails> purchases =
Map.fromEntries(_purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
_inAppPurchase.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
}));
productList.addAll(_products.map(
(ProductDetails productDetails) {
PurchaseDetails? previousPurchase = purchases[productDetails.id];
return ListTile(
title: Text(
productDetails.title,
),
subtitle: Text(
productDetails.description,
),
trailing: previousPurchase != null
? IconButton(
onPressed: () => confirmPriceChange(context),
icon: const Icon(Icons.upgrade))
: TextButton(
child: Text(productDetails.price),
style: TextButton.styleFrom(
backgroundColor: Colors.green[800],
primary: Colors.white,
),
onPressed: () {
late PurchaseParam purchaseParam;
if (Platform.isAndroid) {
// NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to
// verify the latest status of you your subscription by using server side receipt validation
// and update the UI accordingly. The subscription purchase status shown
// inside the app may not be accurate.
final oldSubscription =
_getOldSubscription(productDetails, purchases);
purchaseParam = GooglePlayPurchaseParam(
productDetails: productDetails,
applicationUserName: null,
changeSubscriptionParam: (oldSubscription != null)
? ChangeSubscriptionParam(
oldPurchaseDetails: oldSubscription,
prorationMode: ProrationMode
.immediateWithTimeProration,
)
: null);
} else {
purchaseParam = PurchaseParam(
productDetails: productDetails,
applicationUserName: null,
);
}
if (productDetails.id == _kConsumableId1) {
lastConsumesId = "_kConsumableId1";
_inAppPurchase.buyConsumable(
purchaseParam: purchaseParam,
autoConsume: _kAutoConsume || Platform.isIOS);
}
if (productDetails.id == _kConsumableId2) {
lastConsumesId = "_kConsumableId2";
_inAppPurchase.buyConsumable(
purchaseParam: purchaseParam,
autoConsume: _kAutoConsume || Platform.isIOS);
} else {
_inAppPurchase.buyNonConsumable(
purchaseParam: purchaseParam);
}
},
));
},
));
return Card(
child: Column(
children: <Widget>[productHeader, const Divider()] + productList));
}
Card _buildConsumableBox() {
if (_loading) {
return const Card(
child: (ListTile(
leading: CircularProgressIndicator(),
title: Text('Chargement de vos commandes passées...'))));
}
if (!_isAvailable || _notFoundIds.contains(_kConsumableId1)) {
return const Card();
}
if (!_isAvailable || _notFoundIds.contains(_kConsumableId2)) {
return const Card();
}
const ListTile consumableHeader = ListTile(
title: Text(
'Les cafés commandés :',
style: TextStyle(fontWeight: FontWeight.bold),
));
const ListTile consumableStyle1 = ListTile(
title: Text("Les p'tits cafés"),
);
const ListTile consumableStyle2 = ListTile(
title: Text("Les mugs de cafés"),
);
final List<Widget> token1 = _consumables
.map((String id1) {
return GridTile(
child: IconButton(
icon: const Icon(
Icons.coffee_sharp,
size: 42.0,
color: Colors.brown,
),
splashColor: Colors.green[50],
onPressed: () {
consume(id1);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const _ShowSupport1()));
//////////// APRES AVOIR APPUYER SUR LE CAFE ET AFFICHER LE MERCI, TOKEN DEVIENT UN CAFE VIDE ET APRES SI ON APPUI ON LE CONSUME(ID)
}),
);
})
.cast<Widget>()
.toList();
final List<Widget> token2 = _consumables2
.map((String id2) {
return GridTile(
child: IconButton(
icon: const Icon(
Icons.coffee_maker_sharp,
size: 42.0,
color: Colors.brown,
),
splashColor: Colors.green[50],
onPressed: () {
consume2(id2);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const _ShowSupport2()));
}),
);
})
.cast<Widget>()
.toList();
return Card(
child: Column(children: <Widget>[
consumableHeader,
const Divider(),
consumableStyle1,
GridView.count(
crossAxisCount: 5,
children: token1,
shrinkWrap: true,
padding: const EdgeInsets.all(16.0),
),
const Divider(),
consumableStyle2,
GridView.count(
crossAxisCount: 5,
children: token2,
shrinkWrap: true,
padding: const EdgeInsets.all(16.0),
),
]));
}
// GridTile lastConsumable1({id}) {
// GridTile lastConsumableTheme;
// lastConsumableTheme = GridTile(
// child: IconButton(
// icon: const Icon(
// Icons.coffee_sharp,
// size: 42.0,
// color: Colors.brown,
// ),
// splashColor: Colors.brown[50],
// onPressed: () {
// consume(id);
// Navigator.push(context,
// MaterialPageRoute(builder: (context) => const _ShowSupport1()));
// }),
// );
// return lastConsumableTheme;
// }
// GridTile lastConsumable2({id}) {
// GridTile lastConsumableTheme;
// lastConsumableTheme = GridTile(
// child: IconButton(
// icon: const Icon(
// Icons.coffee_maker_sharp,
// size: 42.0,
// color: Colors.brown,
// ),
// splashColor: Colors.brown[50],
// onPressed: () {
// consume(id);
// Navigator.push(context,
// MaterialPageRoute(builder: (context) => const _ShowSupport2()));
// }),
// );
// return lastConsumableTheme;
// }
// Widget _buildRestoreButton() {
// if (_loading) {
// return Container();
// }
// return Padding(
// padding: const EdgeInsets.all(4.0),
// child: Row(
// mainAxisSize: MainAxisSize.max,
// mainAxisAlignment: MainAxisAlignment.end,
// children: [
// TextButton(
// child: const Text('Restore purchases'),
// style: TextButton.styleFrom(
// backgroundColor: Theme.of(context).primaryColor,
// primary: Colors.white,
// ),
// onPressed: () => _inAppPurchase.restorePurchases(),
// ),
// ],
// ),
// );
// }
Future<void> consume(String id1) async {
await ConsumableStore.consume(id1);
final List<String> consumables = await ConsumableStore.load();
setState(() {
_consumables = consumables;
_purchasePending = false;
});
}
Future<void> consume2(String id2) async {
await ConsumableStore.consume(id2);
final List<String> consumables2 = await ConsumableStore.load();
setState(() {
_consumables2 = consumables2;
_purchasePending = false;
});
}
void showPendingUI() {
setState(() {
_purchasePending = true;
});
}
void deliverProduct(PurchaseDetails purchaseDetails) async {
// IMPORTANT!! Always verify purchase details before delivering the product.
if (purchaseDetails.productID == _kConsumableId1) {
await ConsumableStore.save(purchaseDetails.purchaseID!);
List<String> consumables = await ConsumableStore.load();
setState(() {
_purchasePending = false;
_consumables = consumables;
});
} else if (purchaseDetails.productID == _kConsumableId2) {
await ConsumableStore.save(purchaseDetails.purchaseID!);
List<String> consumables2 = await ConsumableStore.load();
setState(() {
_purchasePending = false;
_consumables2 = consumables2;
});
} else {
setState(() {
_purchases.add(purchaseDetails);
_purchasePending = false;
});
}
}
void handleError(IAPError error) {
setState(() {
_purchasePending = false;
});
}
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) {
// IMPORTANT!! Always verify a purchase before delivering the product.
// For the purpose of an example, we directly return true.
return Future<bool>.value(true);
}
void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
// handle invalid purchase here if _verifyPurchase` failed.
}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
// ignore: avoid_function_literals_in_foreach_calls
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
showPendingUI();
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
handleError(purchaseDetails.error!);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
bool valid = await _verifyPurchase(purchaseDetails);
if (valid) {
deliverProduct(purchaseDetails);
} else {
_handleInvalidPurchase(purchaseDetails);
return;
}
}
if (Platform.isAndroid) {
if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId1) {
final InAppPurchaseAndroidPlatformAddition androidAddition =
_inAppPurchase.getPlatformAddition<
InAppPurchaseAndroidPlatformAddition>();
await androidAddition.consumePurchase(purchaseDetails);
}
if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId2) {
final InAppPurchaseAndroidPlatformAddition androidAddition =
_inAppPurchase.getPlatformAddition<
InAppPurchaseAndroidPlatformAddition>();
await androidAddition.consumePurchase(purchaseDetails);
}
}
if (purchaseDetails.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchaseDetails);
}
}
});
}
Future<void> confirmPriceChange(BuildContext context) async {
if (Platform.isAndroid) {
final InAppPurchaseAndroidPlatformAddition androidAddition =
_inAppPurchase
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
var priceChangeConfirmationResult =
await androidAddition.launchPriceChangeConfirmationFlow(
sku: 'purchaseId',
);
if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Price change accepted'),
));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
priceChangeConfirmationResult.debugMessage ??
"Price change failed with code ${priceChangeConfirmationResult.responseCode}",
),
));
}
}
if (Platform.isIOS) {
var iapStoreKitPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iapStoreKitPlatformAddition.showPriceConsentIfNeeded();
}
}
GooglePlayPurchaseDetails? _getOldSubscription(
GooglePlayPurchaseDetails? oldSubscription;
if (productDetails.id == _kSilverSubscriptionId &&
purchases[_kGoldSubscriptionId] != null) {
oldSubscription =
purchases[_kGoldSubscriptionId] as GooglePlayPurchaseDetails;
} else if (productDetails.id == _kGoldSubscriptionId &&
purchases[_kSilverSubscriptionId] != null) {
oldSubscription =
purchases[_kSilverSubscriptionId] as GooglePlayPurchaseDetails;
}
return oldSubscription;
}
}
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
@override
bool shouldContinueTransaction(
SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
return true;
}
@override
bool shouldShowPriceConsent() {
return false;
}
}
class _ShowSupport1 extends StatefulWidget {
const _ShowSupport1();
@override
_ShowSupport1State createState() => _ShowSupport1State();
}
class _ShowSupport1State extends State<_ShowSupport1> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("APCA-Conseil"),
backgroundColor: Colors.blue[900],
),
body: ListView(children: [Image.asset("assets/IMG_0274.png")]),
);
}
}
class _ShowSupport2 extends StatefulWidget {
const _ShowSupport2();
@override
_ShowSupport2State createState() => _ShowSupport2State();
}
class _ShowSupport2State extends State<_ShowSupport2> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("APCA-Conseil"),
backgroundColor: Colors.blue[900],
),
body: ListView(children: [Image.asset("assets/IMG_0275.png")]),
);
}
}