66667


# 概要 及びエラー

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は取得できています。


CC BY-SA 4.0
2020-05-06T09:06:34.627
66346
2020-05-26T07:56:27.867
2020-05-06T15:06:33.797
40006
40006
question
アプリ内課金 flutter
flutter, in_app_purchase iOSアプリ内課金 There is a pending transaction for the same product identifier


自己解決しました。

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


CC BY-SA 4.0
2020-05-16T08:12:31.197
66667
2020-05-16T08:12:31.197
40006
66346
answer


私も同じような症状が起きて悩まされていました。おそらくiOSのサブスクリプション押した時のダイアログでキャンセルした際に、InAppPurchaseConnection.instance.completePurchaseで完了していないのが原因かと思います（私はそうでした）

completePurchaseの仕様にはstatusがerrorの時も完了しろと書いていました。

以下、InAppPurchaseConnection内のコメントから抜粋

[code]

      /// the purchase needs to be completed if the [PurchaseDetails.status] is [PurchaseStatus.error].
    
[/code]

完了忘れてしまった場合、queryPastPurchasesではPurchaseDetailsがとれないので、別の方法でトランザクションの完了が必要そうです。

解決方法は次のissue内に書かれていました。  
<https://github.com/flutter/flutter/issues/32759#issuecomment-620947340>

[code]

    import 'package:in_app_purchase/store_kit_wrappers.dart';
    
    ...
    
    var paymentWrapper = SKPaymentQueueWrapper();
    var transactions = await paymentWrapper.transactions();
    transactions.forEach((transaction) async {
       await paymentWrapper.finishTransaction(transaction);
    });
    
[/code]

これを実施するとキャンセルしたトランザクションも完了され、そして再度InAppPurchaseConnection.instance.buyNonConsumableすると、正常にサブスクリプション購入を進めることができました。

ご参考まで。


CC BY-SA 4.0
2020-05-26T07:46:54.093
67002
2020-05-26T07:56:27.867
2020-05-26T07:56:27.867
40336
40336
66346
answer