Achats InApp qui se multiplient

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")]),
    );
  }
}

Salut @mikl5484,

il y a énormément de code dans le bloc que tu as donné et ça rend la compréhension très difficile pour nous.
Peut être qu’en simplifiant les extraits de codes pour ne mettre que celui en rapport avec l’achat des produits tu auras plus de retours.

Happy coding!