flutter, in_app_purchase iOSアプリ内課金 There is a pending transaction for the same product identifier


# 概要 及びエラー

Flutter
でアプリ内課金のあるアプリ開発をしています。[in_app_purchase](https://pub.dev/packages/in_app_purchase)
を利用して、実装しようとしているのですが、iOSの方で下記のようなエラーが発生して課金トランザクションが開始されません。

下記のエラーは `_connection.buyNonConsumable(purchaseParam: purchaseParam);`
実行時に発生します。

※
FlutterではなくXcodeのエラーを表示しています。(Flutterに設定しているAppleIDとデプロイ先のAppleIDが異なり、アプリ内課金などの権限必要系がローカルで実行できないため苦肉の策として)

[code]

    2020-05-06 17:36:11.621840+0900 Runner[31281:6882106] [VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception: PlatformException(storekit_duplicate_product_object, There is a pending transaction for the same product identifier. Please either wait for it to be finished or finish it manuelly using `completePurchase` to avoid edge cases., {applicationUsername: null, requestData: null, quantity: 1, productIdentifier: net.deshiapp.record_book.delete_admob_and_use_stats, simulatesAskToBuyInSandbox: null})
    #0      StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:569:7)
    #1      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:321:33)
    <asynchronous suspension>
    #2      SKPaymentQueueWrapper.addPayment (package:in_app_purchase/src/store_kit_wrappers/sk_payment_queue_wrapper.dart:88:19)
    #3      AppStoreConnection.buyNonConsumable (package:in_app_purchase/src/in_app_purchase/app_store_connection.dart:48:34)
    #4      _MarketViewState.build.<anonymous closure> (package:record_book/presentations/market_view.dart:280:31)
    #5      _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:706:14)
    #6      _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:789:36)
    #7      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182:24)
    #8      TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:486:11)
    #9      BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:264:5)
    #10     BaseTapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:199:7)
    #11     PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:467:9)
    #12     PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:76:12)
    #13     PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:117:9)
    #14     _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:379:8)
    #15     PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:115:18)
    #16     PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:101:7)
    #17     GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:218:19)
    #18     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:198:22)
    #19     GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:156:7)
    #20     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:102:7)
    #21     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:86:7)
    #22     _rootRunUnary (dart:async/zone.dart:1138:13)
    #23     _CustomZone.runUnary (dart:async/zone.dart:1031:19)
    #24     _CustomZone.runUnaryGuarded (dart:async/zone.dart:933:7)
    #25     _invoke1 (dart:ui/hooks.dart:273:10)
    #26     _dispatchPointerDataPacket (dart:ui/hooks.dart:182:5)
    
    
[/code]

## 環境

in_app_purchase: 0.3.3

flutter doctor

[code]

    /Users/chibatoshinori/tools/flutter/bin/flutter doctor --verbose
    [✓] Flutter (Channel stable, v1.12.13+hotfix.9, on Mac OS X 10.15.4 19E287, locale ja)
        • Flutter version 1.12.13+hotfix.9 at /Users/chibatoshinori/tools/flutter
        • Framework revision f139b11009 (5 weeks ago), 2020-03-30 13:57:30 -0700
        • Engine revision af51afceb8
        • Dart version 2.7.2
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
        • Android SDK at /Users/chibatoshinori/Library/Android/sdk
        • Android NDK location not configured (optional; useful for native profiling support)
        • Platform android-29, build-tools 28.0.3
        • Java binary at: /Users/chibatoshinori/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6392135/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 11.4.1)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Xcode 11.4.1, Build version 11E503a
        • CocoaPods version 1.8.4
    
    [✓] Android Studio (version 3.6)
        • Android Studio at /Users/chibatoshinori/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6392135/Android Studio.app/Contents
        • Flutter plugin version 45.1.1
        • Dart plugin version 192.7761
        • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
    
    [!] IntelliJ IDEA Ultimate Edition (version 2020.1.1)
        • IntelliJ at /Users/chibatoshinori/Applications/JetBrains Toolbox/IntelliJ IDEA Ultimate.app
        ✗ Flutter plugin not installed; this adds Flutter specific functionality.
        ✗ Dart plugin not installed; this adds Dart specific functionality.
        • For information about installing plugins, see
          https://flutter.dev/intellij-setup/#installing-the-plugins
    
    [✓] VS Code (version 1.44.2)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.10.1
    
[/code]

## ソースコード

[in_app_purchaseのexample](https://pub.dev/packages/in_app_purchase#-example-
tab-)を基に実装しています。

[code]

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:in_app_purchase/in_app_purchase.dart';
    import 'package:record_book/core/constants.dart';
    import 'package:record_book/widgets/progress.dart';
    
    const String _kUpgradeId = Constants.productID;
    const List<String> _kProductIds = <String>[_kUpgradeId];
    
    class MarketView extends StatefulWidget {
      @override
      _MarketViewState createState() => _MarketViewState();
    }
    
    class _MarketViewState extends State<MarketView> {
      final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance;
      StreamSubscription<List<PurchaseDetails>> _subscription;
      List<String> _notFoundIds = [];
      List<ProductDetails> _products = [];
      List<PurchaseDetails> _purchases = [];
      bool _isAvailable = false;
      bool _purchasePending = false;
      bool _loading = true;
      String _queryProductError;
    
      @override
      void initState() {
        Stream purchaseUpdated =
            InAppPurchaseConnection.instance.purchaseUpdatedStream;
        _subscription = purchaseUpdated.listen((dynamic purchaseDetailsList) {
          print('purchaseUpdated listen: $purchaseDetailsList');
          _listenToPurchaseUpdated(purchaseDetailsList as List<PurchaseDetails>);
        }, onDone: () {
          print('purchaseUpdated listen on Done');
          _subscription.cancel();
        }, onError: (dynamic error) {
          // handle error here.
          print('in purchaseUpdated onError: $error');
        }) as StreamSubscription<List<PurchaseDetails>>;
        initStoreInfo();
        super.initState();
      }
    
      Future<void> initStoreInfo() async {
        print('init Store info');
        final isAvailable = await _connection.isAvailable();
        print('isAvailabel: $isAvailable');
        if (!isAvailable) {
          setState(() {
            _isAvailable = isAvailable;
            _products = [];
            _purchases = [];
            _notFoundIds = [];
            _purchasePending = false;
            _loading = false;
          });
          return;
        }
    
        final productDetailResponse =
            await _connection.queryProductDetails(_kProductIds.toSet());
        print('productDetailsResponse: $productDetailResponse');
        if (productDetailResponse.error != null) {
          print('productDetailResponse.error: ${productDetailResponse.error}');
          setState(() {
            _queryProductError = productDetailResponse.error.message;
            _isAvailable = isAvailable;
            _products = productDetailResponse.productDetails;
            _purchases = [];
            _notFoundIds = productDetailResponse.notFoundIDs;
            _purchasePending = false;
            _loading = false;
          });
          return;
        }
    
        if (productDetailResponse.productDetails.isEmpty) {
          print('productDetailResponse id Empty');
          setState(() {
            _queryProductError = null;
            _isAvailable = isAvailable;
            _products = productDetailResponse.productDetails;
            _purchases = [];
            _notFoundIds = productDetailResponse.notFoundIDs;
            _purchasePending = false;
            _loading = false;
          });
          return;
        }
    
        final purchaseResponse = await _connection.queryPastPurchases();
    
        if (purchaseResponse.error != null) {
          // handle query past purchase error..
          print('purchaseReponse.error: ${purchaseResponse.error}');
          print('message: ${purchaseResponse.error.message}');
          print('details: ${purchaseResponse.error.details}');
          print('code: ${purchaseResponse.error.code}');
        }
    
        final List<PurchaseDetails> verifiedPurchases = [];
        for (PurchaseDetails purchase in purchaseResponse.pastPurchases) {
          print('verify purchase: ${purchase.productID}');
          if (await _verifyPurchase(purchase)) {
            print(
                '[purchase] id: ${purchase.productID}, status: ${purchase.status}, pendingCompletePurchase: ${purchase.pendingCompletePurchase}');
            verifiedPurchases.add(purchase);
          }
        }
    
        setState(() {
          _isAvailable = isAvailable;
          _products = productDetailResponse.productDetails;
          _purchases = verifiedPurchases;
          _notFoundIds = productDetailResponse.notFoundIDs;
          _purchasePending = false;
          _loading = false;
        });
      }
    
      @override
      void dispose() {
        _subscription.cancel();
        super.dispose();
      }
    
      void showPendingUI() {
        setState(() {
          _purchasePending = true;
        });
      }
    
      void deliverProduct(PurchaseDetails purchaseDetails) async {
        // IMPORTANT!! Always verify a purchase purchase details before delivering the product.
        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) {
        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) {
              bool valid = await _verifyPurchase(purchaseDetails);
              if (valid) {
                deliverProduct(purchaseDetails);
              } else {
                _handleInvalidPurchase(purchaseDetails);
                return;
              }
            }
    
            if (purchaseDetails.pendingCompletePurchase) {
              print('completPurchase: ${purchaseDetails.status}');
              await InAppPurchaseConnection.instance
                  .completePurchase(purchaseDetails);
            }
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        final product = _products.isNotEmpty
            ? _products.firstWhere((p) => p.id == Constants.productID)
            : null;
        final price = product != null ? product.price : '---';
    
        return Scaffold(
          appBar: AppBar(
            title: const Text('開発者を応援する'),
          ),
          body: Padding(
            padding: const EdgeInsets.all(8),
            child: Column(
              children: <Widget>[
                _loading ? linearProgress() : const SizedBox(width: 0, height: 0),
                _purchasePending
                    ? const Text('purchase pending')
                    : const SizedBox(width: 0, height: 0),
                const Text(
                  'コーヒーを1杯奢っていただくと、開発者の励みになるだけでなく、'
                  '広告の削除とちょっとした機能追加が受けられます。'
                  'しかし、それはあなたの自由です。',
                ),
                Text(
                  '※サブスクリプションではありません。',
                  style: Theme.of(context).textTheme.caption,
                ),
                Text(
                  '購入による追加機能',
                  style: Theme.of(context).textTheme.title,
                ),
                ListTile(
                  leading: Icon(
                    Icons.check_circle,
                    color: Colors.lightBlue,
                  ),
                  title: const Text(
                    '広告の削除',
                  ),
                ),
                ListTile(
                  leading: Icon(
                    Icons.check_circle,
                    color: Colors.lightBlue,
                  ),
                  title: const Text(
                    '詳細な統計情報ページの開放',
                  ),
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: <Widget>[
                    Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text(
                          '購入状態',
                          style: Theme.of(context).textTheme.caption,
                        ),
                        Text(
                          '購入状態',
                          style: TextStyle(
                            fontSize: 22,
                            fontWeight: FontWeight.bold,
                            color: Colors.lightBlue,
                          ),
                        ),
                      ],
                    ),
                    OutlineButton(
                      color: Colors.lightBlue,
                      child: const Text(
                        '復元',
                      ),
                      onPressed: () {},
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                const Divider(),
                ListTile(
                  title: Text(
                    '応援する',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  trailing: RaisedButton(
                    child: Text(
                      price,
                      style: TextStyle(
                        color: Colors.white,
                      ),
                    ),
                    color: Colors.blue,
                    onPressed: () {
                      final purchaseParam = PurchaseParam(productDetails: product);
                      _connection.buyNonConsumable(purchaseParam: purchaseParam);
                    },
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    
[/code]

## 考えていること

[code]

    if (purchaseDetails.pendingCompletePurchase) {
      print('completPurchase: ${purchaseDetails.status}');
      await InAppPurchaseConnection.instance
              .completePurchase(purchaseDetails);
    }
    
[/code]

の `completePurchase(purchaseDetails)`が実行できればいいのでは？と考えたのですが、

[code]

    final purchaseResponse = await _connection.queryPastPurchases();
    
[/code]

のプロパティ`purchaseResponse.pastPurchases` は空のListで、purchaseDetailsが取得できていません。

## その他

  * Androidは実行できています。
  * productDetailsは取得できています。




自己解決しました。

根本的な解決ではないですが、iOSのproduct idを新しく作り直したら、解消されました。




自己解決しました。

根本的な解決ではないですが、iOSのproduct idを新しく作り直したら、解消されました。

