-
Notifications
You must be signed in to change notification settings - Fork 28.9k
Open
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work lista: productionIssues experienced in live production appsIssues experienced in live production appsp: 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-androidAndroid applications specificallyAndroid applications specificallyteam-androidOwned by Android platform teamOwned by Android platform teamtriaged-androidTriaged by Android platform teamTriaged by Android platform team
Description
What package does this bug report belong to?
in_app_purchase
What target platforms are you seeing this bug on?
Android
Have you already upgraded your packages?
Yes
Dependency versions
pubspec.lock
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
in_app_purchase:
dependency: "direct main"
description:
name: in_app_purchase
sha256: def70fbaa2a274f4d835677459f6f7afc5469de912438f86076e51cbd4cbd5b4
url: "https://pub.dev"
source: hosted
version: "3.1.13"
in_app_purchase_android:
dependency: transitive
description:
name: in_app_purchase_android
sha256: "28164faac635a6cc357c96f47813e675eec7622a8f41e829b501573dbbce2cce"
url: "https://pub.dev"
source: hosted
version: "0.3.0+17"
in_app_purchase_platform_interface:
dependency: transitive
description:
name: in_app_purchase_platform_interface
sha256: "412efce2b9238c5ace4f057acad43f793ed06880e366d26ae322e796cadb051a"
url: "https://pub.dev"
source: hosted
version: "1.3.7"
in_app_purchase_storekit:
dependency: transitive
description:
name: in_app_purchase_storekit
sha256: fd5a617c39198c96b3876a505058d08c02cf3103114564fbd669e06822fc30ee
url: "https://pub.dev"
source: hosted
version: "0.3.9"
Steps to reproduce
Unable to reproduce on local machine.
However our users are facing this issue.
- Run the code sample
- Purchase/upgrade a subscription product.
- Observe if the error INVALID_OFFER_TOKEN is thrown.
Expected results
The offer token must be valid.
Actual results
The check offerToken.equals(offerDetails.getOfferToken()
performed here
https://github.com/flutter/packages/blob/90baeee9484e2f760c22c1fa0e9ffb6d4c597d7e/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java#L277
causes the error.
Maybe the map cachedProducts
is not in sync with the productDetails of queryProductDetails
api.
Code sample
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/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
import 'consumable_store.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(_MyApp());
}
const String _kUpgradeId = 'upgrade';
const String _kSilverSubscriptionId = 'subscription_silver';
const String _kGoldSubscriptionId = 'subscription_gold';
const List<String> _kProductIds = <String>[
_kUpgradeId,
_kSilverSubscriptionId,
_kGoldSubscriptionId,
];
class _MyApp extends StatefulWidget {
@override
State<_MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<_MyApp> {
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<String> _notFoundIds = <String>[];
List<ProductDetails> _products = <ProductDetails>[];
List<PurchaseDetails> _purchases = <PurchaseDetails>[];
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;
String? _queryProductError;
@override
void initState() {
final Stream<List<PurchaseDetails>> purchaseUpdated =
_inAppPurchase.purchaseStream;
_subscription =
purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
_subscription.cancel();
}, onError: (Object error) {
// handle error here.
});
initStoreInfo();
super.initState();
}
Future<void> initStoreInfo() async {
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
setState(() {
_isAvailable = isAvailable;
_products = <ProductDetails>[];
_purchases = <PurchaseDetails>[];
_notFoundIds = <String>[];
_purchasePending = false;
_loading = false;
});
return;
}
final ProductDetailsResponse productDetailResponse =
await _inAppPurchase.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
setState(() {
_queryProductError = productDetailResponse.error!.message;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = <PurchaseDetails>[];
_notFoundIds = productDetailResponse.notFoundIDs;
_purchasePending = false;
_loading = false;
});
return;
}
if (productDetailResponse.productDetails.isEmpty) {
setState(() {
_queryProductError = null;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = <PurchaseDetails>[];
_notFoundIds = productDetailResponse.notFoundIDs;
_purchasePending = false;
_loading = false;
});
return;
}
setState(() {
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_notFoundIds = productDetailResponse.notFoundIDs;
_purchasePending = false;
_loading = false;
});
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final List<Widget> stack = <Widget>[];
if (_queryProductError == null) {
stack.add(
ListView(
children: <Widget>[
_buildConnectionCheckTile(),
_buildProductList(),
_buildRestoreButton(),
],
),
);
} else {
stack.add(Center(
child: Text(_queryProductError!),
));
}
if (_purchasePending) {
stack.add(
const Stack(
children: <Widget>[
Opacity(
opacity: 0.3,
child: 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 const Card(child: ListTile(title: Text('Trying to connect...')));
}
final Widget storeHeader = ListTile(
leading: Icon(_isAvailable ? Icons.check : Icons.block,
color: _isAvailable
? Colors.green
: ThemeData.light().colorScheme.error),
title:
Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'),
);
final List<Widget> children = <Widget>[storeHeader];
if (!_isAvailable) {
children.addAll(<Widget>[
const Divider(),
ListTile(
title: Text('Not connected',
style: TextStyle(color: ThemeData.light().colorScheme.error)),
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 const Card(
child: ListTile(
leading: CircularProgressIndicator(),
title: Text('Fetching products...')));
}
if (!_isAvailable) {
return const Card();
}
const ListTile productHeader = ListTile(title: Text('Products for Sale'));
final List<ListTile> productList = <ListTile>[];
if (_notFoundIds.isNotEmpty) {
productList.add(ListTile(
title: Text('[${_notFoundIds.join(", ")}] not found',
style: TextStyle(color: ThemeData.light().colorScheme.error)),
subtitle: const 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 verify the purchase data.
final Map<String, PurchaseDetails> purchases =
Map<String, PurchaseDetails>.fromEntries(
_purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
_inAppPurchase.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
}));
productList.addAll(_products.map(
(ProductDetails productDetails) {
final PurchaseDetails? previousPurchase = purchases[productDetails.id];
return ListTile(
title: Text(
productDetails.title,
),
subtitle: Text(
productDetails.description,
),
trailing: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.green[800],
foregroundColor: Colors.white,
),
onPressed: () {
late PurchaseParam purchaseParam;
if (Platform.isAndroid) {
// NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to
// verify the latest status of you your subscription by using server side receipt validation
// and update the UI accordingly. The subscription purchase status shown
// inside the app may not be accurate.
final GooglePlayPurchaseDetails? oldSubscription =
_getOldSubscription(productDetails, purchases);
purchaseParam = GooglePlayPurchaseParam(
productDetails: productDetails,
changeSubscriptionParam: (oldSubscription != null)
? ChangeSubscriptionParam(
oldPurchaseDetails: oldSubscription,
prorationMode:
ProrationMode.immediateWithTimeProration,
)
: null);
} else {
purchaseParam = PurchaseParam(
productDetails: productDetails,
);
}
_inAppPurchase.buyNonConsumable(
purchaseParam: purchaseParam);
},
child: Text(productDetails.price),
),
);
},
));
return Card(
child: Column(
children: <Widget>[productHeader, const Divider()] + productList));
}
Widget _buildRestoreButton() {
if (_loading) {
return Container();
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
style: TextButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
onPressed: () => _inAppPurchase.restorePurchases(),
child: const Text('Restore purchases'),
),
],
),
);
}
void showPendingUI() {
setState(() {
_purchasePending = true;
});
}
Future<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.
}
Future<void> _listenToPurchaseUpdated(
List<PurchaseDetails> purchaseDetailsList) async {
for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
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) {
final bool valid = await _verifyPurchase(purchaseDetails);
if (valid) {
unawaited(deliverProduct(purchaseDetails));
} else {
_handleInvalidPurchase(purchaseDetails);
return;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchaseDetails);
}
}
}
}
GooglePlayPurchaseDetails? _getOldSubscription(
ProductDetails productDetails, Map<String, PurchaseDetails> purchases) {
// This is just to demonstrate a subscription upgrade or downgrade.
// This method assumes that you have only 2 subscriptions under a group, 'subscription_silver' & 'subscription_gold'.
// The 'subscription_silver' subscription can be upgraded to 'subscription_gold' and
// the 'subscription_gold' subscription can be downgraded to 'subscription_silver'.
// Please remember to replace the logic of finding the old subscription Id as per your app.
// The old subscription is only required on Android since Apple handles this internally
// by using the subscription group feature in iTunesConnect.
GooglePlayPurchaseDetails? oldSubscription;
if (productDetails.id == _kSilverSubscriptionId &&
purchases[_kGoldSubscriptionId] != null) {
oldSubscription =
purchases[_kGoldSubscriptionId]! as GooglePlayPurchaseDetails;
} else if (productDetails.id == _kGoldSubscriptionId &&
purchases[_kSilverSubscriptionId] != null) {
oldSubscription =
purchases[_kSilverSubscriptionId]! as GooglePlayPurchaseDetails;
}
return oldSubscription;
}
}
Screenshots or Videos
Screenshots / Video demonstration
NOTE: The offerToken & productID have been blacked out in the screenshot.
Logs
No response
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.16.6, on macOS 12.7.3 21H1015 darwin-x64, locale en-US)
• Flutter version 3.16.6 on channel stable at /Users/appleapple/fvm/versions/3.16.6
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 46787ee49c (4 weeks ago), 2024-01-09 14:36:07 -0800
• Engine revision 3f3e560236
• Dart version 3.2.3
• DevTools version 2.28.4
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
• Android SDK at /Users/appleapple/Library/Android/sdk
• Platform android-34, build-tools 34.0.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 14C18
• CocoaPods version 1.15.2
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2023.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
[✓] VS Code (version 1.86.1)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.82.0
[✓] Connected device (2 available)
• macOS (desktop) • macos • darwin-x64 • macOS 12.7.3 21H1015 darwin-x64
• Chrome (web) • chrome • web-javascript • Google Chrome 121.0.6167.160
[✓] Network resources
• All expected network resources are available.
• No issues found!
absar
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work lista: productionIssues experienced in live production appsIssues experienced in live production appsp: 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-androidAndroid applications specificallyAndroid applications specificallyteam-androidOwned by Android platform teamOwned by Android platform teamtriaged-androidTriaged by Android platform teamTriaged by Android platform team