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 中是不是存在针对此项目的错误?