-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Open
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listfound in release: 2.10Found to occur in 2.10Found to occur in 2.10found in release: 2.11Found to occur in 2.11Found to occur in 2.11has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: in_app_purchasePlugin for in-app purchasePlugin for in-app purchasepackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyteam-iosOwned by iOS platform teamOwned by iOS platform teamtriaged-iosTriaged by iOS platform teamTriaged by iOS platform team
Description
Steps to Reproduce
- Setup a sandbox environment for in app purchase on iOS with a subscription
- Log in to the sandbox account on a iOS device
- Purchase the subscription
- InAppPurchase.instance.purchaseStream emits PurchaseDetails for the subscription
- Call InAppPurchase.instance.completePurchase if pendingCompletePurchase is true
- Restore purchases
- PurchaseDetails.pendingCompletePurchase is still true
Expected results: PurchaseDetails.pendingCompletePurchase = false after call to completePurchase
Actual results: PurchaseDetails.pendingCompletePurchase = true after call to completePurchase
AppStorePurchaseDetails.pendingCompletePurchase returns _pendingCompletePurchase.
I have also tested on Android, there pendingCompletePurchase changes to false as expected.
Code sample
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_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(_MyApp());
}
const String _kSubscriptionId = 'sku_monthly_level_1'; // TODO Set subscription id for your project here
const Set<String> _kProductIds = <String>{_kSubscriptionId};
class _MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<_MyApp> {
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<ProductDetails> _products = [];
List<PurchaseDetails> _purchases = [];
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;
String? _queryProductError;
@override
void initState() {
final Stream<List<PurchaseDetails>> purchaseUpdated = _inAppPurchase.purchaseStream;
_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 _inAppPurchase.isAvailable();
if (!isAvailable) {
setState(() {
_isAvailable = isAvailable;
_products = [];
_purchases = [];
_purchasePending = false;
_loading = false;
});
return;
}
ProductDetailsResponse productDetailResponse = await _inAppPurchase.queryProductDetails(_kProductIds);
if (productDetailResponse.error != null) {
setState(() {
_queryProductError = productDetailResponse.error!.message;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = [];
_purchasePending = false;
_loading = false;
});
return;
}
if (productDetailResponse.productDetails.isNotEmpty) {
setState(() {
_queryProductError = null;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = [];
_purchasePending = false;
_loading = false;
});
return;
}
}
@override
void dispose() {
if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
iosPlatformAddition.setDelegate(null);
}
_subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
List<Widget> stack = [];
if (_queryProductError == null) {
stack.add(
ListView(
children: [
_buildConnectionCheckTile(),
_buildProductList(),
_buildRestoreButton(),
],
),
);
} 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>[];
// 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 verify the purchase data.
Map<String, PurchaseDetails> purchases = Map.fromEntries(_purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
_inAppPurchase.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)
: TextButton(
child: Text(productDetails.price),
style: TextButton.styleFrom(
backgroundColor: Colors.green[800],
primary: Colors.white,
),
onPressed: () {
PurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails: productDetails,
applicationUserName: null,
);
_inAppPurchase.buyNonConsumable(purchaseParam: purchaseParam);
},
));
},
));
return Card(child: Column(children: <Widget>[productHeader, Divider()] + productList));
}
Widget _buildRestoreButton() {
if (_loading) {
return Container();
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
child: Text('Restore purchases'),
style: TextButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
primary: Colors.white,
),
onPressed: () => _inAppPurchase.restorePurchases(),
),
],
),
);
}
void showPendingUI() {
setState(() {
_purchasePending = true;
});
}
void deliverProduct(PurchaseDetails purchaseDetails) async {
// IMPORTANT!! Always verify 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 ||
purchaseDetails.status == PurchaseStatus.restored) {
bool valid = await _verifyPurchase(purchaseDetails);
if (valid) {
deliverProduct(purchaseDetails);
} else {
_handleInvalidPurchase(purchaseDetails);
return;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchaseDetails);
}
}
});
}
}
Logs
No errors or analysis warnings
miquelbeltran, tatraef, artsector, getpoucher, kaciula and 19 moreramsayamarinabinmittu
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listfound in release: 2.10Found to occur in 2.10Found to occur in 2.10found in release: 2.11Found to occur in 2.11Found to occur in 2.11has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: in_app_purchasePlugin for in-app purchasePlugin for in-app purchasepackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyteam-iosOwned by iOS platform teamOwned by iOS platform teamtriaged-iosTriaged by iOS platform teamTriaged by iOS platform team
Type
Projects
Status
Complex, High Priority
