Flutter IAP - Error you already own this item
380
If you want to test only with non-consumable product, then you can refund the test purchase and use it again later.
Comments
-
tate_xy over 1 year
I'm trying to implement Flutter InApp purchases on Consumables but I keep getting the following - error you already own this item when I try to buy again. I want the user to buy over and over again.
This is happening on android.
I'm using
in_app_purchase: ^0.3.4+5
I used the code in official docs for the plugin -// 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); } } }); } }
How do I solve this?