Flutter IAP - 您已经拥有此项目的错误

Posted

技术标签:

【中文标题】Flutter IAP - 您已经拥有此项目的错误【英文标题】:Flutter IAP - Error you already own this item 【发布时间】:2021-09-16 02:36:53 【问题描述】:

我正在尝试在 Consumables 上实现 Flutter InApp 购买,但当我再次尝试购买时,我不断收到以下消息 - 错误您已经拥有此项目。 我希望用户一次又一次地购买。

这发生在 android 上。

我正在使用in_app_purchase: ^0.3.4+5 我使用了官方文档中的代码作为插件 -

    // Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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/store_kit_wrappers.dart';
import 'consumable_store.dart';

void main() 
  // For play billing library 2.0 on Android, it is mandatory to call
  // [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases)
  // as part of initializing the app.
  InAppPurchaseConnection.enablePendingPurchases();
  runApp(MyApp());


// Original code link: https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/example/lib/main.dart

const bool kAutoConsume = true;

const String _kConsumableId = '';
const String _kSubscriptionId = '';
const List<String> _kProductIds = <String>[
  _kConsumableId,
  'noadforfifteendays',
  _kSubscriptionId
];

// TODO: Please Add your android product ID here
const List<String> _kAndroidProductIds = <String>[
  ''
];
//Example
//const List<String> _kAndroidProductIds = <String>[
//  'ADD_YOUR_ANDROID_PRODUCT_ID_1',
//  'ADD_YOUR_ANDROID_PRODUCT_ID_2',
//  'ADD_YOUR_ANDROID_PRODUCT_ID_3'
//];

// TODO: Please Add your ios product ID here
const List<String> _kiOSProductIds = <String>[
  ''
];
//Example
//const List<String> _kiOSProductIds = <String>[
//  'ADD_YOUR_IOS_PRODUCT_ID_1',
//  'ADD_YOUR_IOS_PRODUCT_ID_2',
//  'ADD_YOUR_IOS_PRODUCT_ID_3'
//];

class MyApp extends StatefulWidget 
  @override
  _MyAppState createState() => _MyAppState();


class _MyAppState extends State<MyApp> 
  final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance;
  StreamSubscription<List<PurchaseDetails>> _subscription;
  List<String> _notFoundIds = [];
  List<ProductDetails> _products = [];
  List<PurchaseDetails> _purchases = [];
  List<String> _consumables = [];
  bool _isAvailable = false;
  bool _purchasePending = false;
  bool _loading = true;
  String _queryProductError;

  @override
  void initState() 

    DateTime currentDate = DateTime.now();
    DateTime noADDate;

    var fiftyDaysFromNow = currentDate.add(new Duration(days: 50));
    print('$fiftyDaysFromNow.month - $fiftyDaysFromNow.day - $fiftyDaysFromNow.year $fiftyDaysFromNow.hour:$fiftyDaysFromNow.minute');

    Stream purchaseUpdated =
        InAppPurchaseConnection.instance.purchaseUpdatedStream;
    _subscription = purchaseUpdated.listen((purchaseDetailsList) 
      _listenToPurchaseUpdated(purchaseDetailsList);
    , onDone: () 
      _subscription.cancel();
    , onError: (error) 
      // handle error here.
    );
    initStoreInfo();
    super.initState();
  

  Future<void> initStoreInfo() async 
    final bool isAvailable = await _connection.isAvailable();

    if (!isAvailable) 
      setState(() 
        _isAvailable = isAvailable;
        _products = [];
        _purchases = [];
        _notFoundIds = [];
        _consumables = [];
        _purchasePending = false;
        _loading = false;
      );
      return;
    

    ProductDetailsResponse productDetailResponse =
    await _connection.queryProductDetails(Platform.isIOS ? _kiOSProductIds.toSet() : _kAndroidProductIds.toSet());//_kProductIds.toSet());
    if (productDetailResponse.error != null) 
      setState(() 
        _queryProductError = productDetailResponse.error.message;
        _isAvailable = isAvailable;
        _products = productDetailResponse.productDetails;
        _purchases = [];
        _notFoundIds = productDetailResponse.notFoundIDs;
        _consumables = [];
        _purchasePending = false;
        _loading = false;
      );
      return;
    

    if (productDetailResponse.productDetails.isEmpty) 
      setState(() 
        _queryProductError = null;
        _isAvailable = isAvailable;
        _products = productDetailResponse.productDetails;
        _purchases = [];
        _notFoundIds = productDetailResponse.notFoundIDs;
        _consumables = [];
        _purchasePending = false;
        _loading = false;
      );
      return;
    

    final QueryPurchaseDetailsResponse purchaseResponse =
    await _connection.queryPastPurchases();
    if (purchaseResponse.error != null) 
      // handle query past purchase error..
    
    final List<PurchaseDetails> verifiedPurchases = [];
    for (PurchaseDetails purchase in purchaseResponse.pastPurchases) 
      if (await _verifyPurchase(purchase)) 
        verifiedPurchases.add(purchase);
      
    
    List<String> consumables = await ConsumableStore.load();
    setState(() 
      _isAvailable = isAvailable;
      _products = productDetailResponse.productDetails;
      _purchases = verifiedPurchases;
      _notFoundIds = productDetailResponse.notFoundIDs;
      _consumables = consumables;
      _purchasePending = false;
      _loading = false;
    );
  

  @override
  void dispose() 
    _subscription.cancel();
    super.dispose();
  

  @override
  Widget build(BuildContext context) 
    List<Widget> stack = [];
    if (_queryProductError == null) 
      stack.add(
        ListView(
          children: [
            _buildConnectionCheckTile(),
            _buildProductList(),
            _buildConsumableBox(),
          ],
        ),
      );
     else 
      stack.add(Center(
        child: Text(_queryProductError),
      ));
    
    if (_purchasePending) 
      stack.add(
        Stack(
          children: [
            Opacity(
              opacity: 0.3,
              child: const ModalBarrier(dismissible: false, color: Colors.grey),
            ),
            Center(
              child: CircularProgressIndicator(),
            ),
          ],
        ),
      );
    

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('IAP Example'),
        ),
        body: Stack(
          children: stack,
        ),
      ),
    );
  

  Card _buildConnectionCheckTile() 
    if (_loading) 
      return Card(child: ListTile(title: const Text('Trying to connect...')));
    
    final Widget storeHeader = ListTile(
      leading: Icon(_isAvailable ? Icons.check : Icons.block,
          color: _isAvailable ? Colors.green : ThemeData.light().errorColor),
      title: Text(
          'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'),
    );
    final List<Widget> children = <Widget>[storeHeader];

    if (!_isAvailable) 
      children.addAll([
        Divider(),
        ListTile(
          title: Text('Not connected',
              style: TextStyle(color: ThemeData.light().errorColor)),
          subtitle: const Text(
              'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'),
        ),
      ]);
    
    return Card(child: Column(children: children));
  

  Card _buildProductList() 
    if (_loading) 
      return Card(
          child: (ListTile(
              leading: CircularProgressIndicator(),
              title: Text('Fetching products...'))));
    
    if (!_isAvailable) 
      return Card();
    
    final ListTile productHeader = ListTile(title: Text('Products for Sale'));
    List<ListTile> productList = <ListTile>[];
    if (_notFoundIds.isNotEmpty) 
      productList.add(ListTile(
          title: Text('[$_notFoundIds.join(", ")] not found',
              style: TextStyle(color: ThemeData.light().errorColor)),
          subtitle: 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 verity the purchase data.
    Map<String, PurchaseDetails> purchases =
    Map.fromEntries(_purchases.map((PurchaseDetails purchase) 
      if (purchase.pendingCompletePurchase) 
        InAppPurchaseConnection.instance.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
                ? Icon(Icons.check)
                : FlatButton(
              child: Text(productDetails.price),
              color: Colors.green[800],
              textColor: Colors.white,
              onPressed: () 
                PurchaseParam purchaseParam = PurchaseParam(
                    productDetails: productDetails,
                    applicationUserName: null,
                    sandboxTesting: false);
                if (productDetails.id == _kConsumableId) 
                  _connection.buyConsumable(
                      purchaseParam: purchaseParam,
                      autoConsume: kAutoConsume || Platform.isIOS);
                 else 
                  _connection.buyNonConsumable(
                      purchaseParam: purchaseParam);
                
              ,
            ));
      ,
    ));

    return Card(
        child:
        Column(children: <Widget>[productHeader, Divider()] + productList));
  

  Card _buildConsumableBox() 
    if (_loading) 
      return Card(
          child: (ListTile(
              leading: CircularProgressIndicator(),
              title: Text('Fetching consumables...'))));
    
    if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) 
      return Card();
    
    final ListTile consumableHeader =
    ListTile(title: Text('Purchased consumables'));
    final List<Widget> tokens = _consumables.map((String id) 
      return GridTile(
        child: IconButton(
          icon: Icon(
            Icons.stars,
            size: 42.0,
            color: Colors.orange,
          ),
          splashColor: Colors.yellowAccent,
          onPressed: () => consume(id),
        ),
      );
    ).toList();
    return Card(
        child: Column(children: <Widget>[
          consumableHeader,
          Divider(),
          GridView.count(
            crossAxisCount: 5,
            children: tokens,
            shrinkWrap: true,
            padding: EdgeInsets.all(16.0),
          )
        ]));
  

  Future<void> consume(String id) async 
    print('consume id is $id');
    await ConsumableStore.consume(id);
    final List<String> consumables = await ConsumableStore.load();
    setState(() 
      _consumables = consumables;
    );
  

  void showPendingUI() 
    setState(() 
      _purchasePending = true;
    );
  

  void deliverProduct(PurchaseDetails purchaseDetails) async 
    print('deliverProduct'); // Last
    // IMPORTANT!! Always verify a purchase purchase details before delivering the product.
    if (purchaseDetails.productID == _kConsumableId) 
      await ConsumableStore.save(purchaseDetails.purchaseID);
      List<String> consumables = await ConsumableStore.load();
      setState(() 
        _purchasePending = false;
        _consumables = consumables;
      );
     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.
    print('_verifyPurchase');
    return Future<bool>.value(true);
  

  void _handleInvalidPurchase(PurchaseDetails purchaseDetails) 
    // handle invalid purchase here if  _verifyPurchase` failed.
    print('_handleInvalidPurchase');
  

  void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) 
    print('_listenToPurchaseUpdated');
    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 (Platform.isAndroid) 
          if (!kAutoConsume && purchaseDetails.productID == _kConsumableId) 
            await InAppPurchaseConnection.instance
                .consumePurchase(purchaseDetails);
          
        
        if (purchaseDetails.pendingCompletePurchase) 
          await InAppPurchaseConnection.instance
              .completePurchase(purchaseDetails);
        
      
    );
  

我该如何解决这个问题?

【问题讨论】:

【参考方案1】:

确保您的产品是消耗品,并且也是使用 buyConsumable 调用购买的。

【讨论】:

以上是关于Flutter IAP - 您已经拥有此项目的错误的主要内容,如果未能解决你的问题,请参考以下文章

SSIS Union All - SSIS 中是不是存在针对此项目的错误?

IAP:普通产品+多合一

Maven:此项目的打包未将文件分配给构建工件

IAP:普通产品+一体化

Google Play 应用内购买问题 - 错误您已经拥有此项目

带有 Firebase 身份验证和收入 cat 的 IAP 的 Flutter 应用程序未连接