From a5ec6cf20b6b3e395a01d192a06fc9a714a0fdf6 Mon Sep 17 00:00:00 2001 From: Gerardo Quintanar Morales Date: Mon, 31 Aug 2026 16:36:49 -0600 Subject: [PATCH 1/3] Add README excerpts and changelog entry --- .../in_app_purchase/CHANGELOG.md | 1 + .../in_app_purchase/in_app_purchase/README.md | 18 ++ .../in_app_purchase/ci_config.yaml | 2 +- .../example/lib/readme_examples.dart | 240 ++++++++++++++++++ 4 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 3e35cab282b1..81d367765b92 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,6 +1,7 @@ ## 3.3.0 * Updates `in_app_purchase_android` dependency to `^0.5.0`. +* Updates README examples and doc excerpts to match the current package API and extraction workflow. ## 3.2.4 diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 8f3b1c9cdcaa..f88b2a5e1bdd 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -1,3 +1,4 @@ + A storefront-independent API for purchases in Flutter apps. @@ -84,6 +85,7 @@ You should always start listening to purchase update as early as possible to be to catch all purchase updates, including the ones from the previous app session. To listen to the update: + ```dart class _MyAppState extends State { StreamSubscription> _subscription; @@ -111,6 +113,7 @@ class _MyAppState extends State { Here is an example of how to handle purchase updates: + ```dart void _listenToPurchaseUpdated(List purchaseDetailsList) { purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { @@ -139,6 +142,7 @@ void _listenToPurchaseUpdated(List purchaseDetailsList) { ### Connecting to the underlying store + ```dart final bool available = await InAppPurchase.instance.isAvailable(); if (!available) { @@ -148,6 +152,7 @@ if (!available) { ### Loading products for sale + ```dart // Set literals require Dart 2.2. Alternatively, use // `Set _kIds = ['product1', 'product2'].toSet()`. @@ -170,6 +175,7 @@ underlying store: * [Verifying Google Play purchases](https://developer.android.com/google/play/billing/security#verify) + ```dart await InAppPurchase.instance.restorePurchases(); ``` @@ -186,6 +192,7 @@ Both underlying stores handle consumable and non-consumable products differently you're using `InAppPurchase`, you need to make a distinction here and call the right purchase method for each type. + ```dart final ProductDetails productDetails = ... // Saved earlier from queryProductDetails(). final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); @@ -201,6 +208,7 @@ if (_isConsumable(productDetails)) { StoreKit 2 Specific Purchases (iOS/macOS) When StoreKit 2 is enabled, you can use Sk2PurchaseParam to include StoreKit 2 specific parameters such as win-back offer identifiers or promotional offers with signatures. + ```dart import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; @@ -248,6 +256,7 @@ users from accidentally purchasing multiple subscriptions. Refer to the [Creating a Subscription Group](https://developer.apple.com/app-store/subscriptions/#groups) section of [Apple's subscription guide](https://developer.apple.com/app-store/subscriptions/). + ```dart final PurchaseDetails oldPurchaseDetails = ...; PurchaseParam purchaseParam = GooglePlayPurchaseParam( @@ -294,6 +303,7 @@ popup at a different time, for example after clicking a button. To know when the App Store wants to show a popup and prevent this from happening a queue delegate can be registered. The `InAppPurchaseStoreKitPlatformAddition` contains a `setDelegate(SKPaymentQueueDelegateWrapper? delegate)` function that can be used to set a delegate or remove one by setting it to `null`. + ```dart //import for InAppPurchaseStoreKitPlatformAddition import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; @@ -319,6 +329,7 @@ The delegate that is set should implement `SKPaymentQueueDelegateWrapper` and ha `shouldShowPriceConsent`. When setting `shouldShowPriceConsent` to false the default popup will not be shown and the app needs to show this later. + ```dart // import for SKPaymentQueueDelegateWrapper import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; @@ -339,6 +350,7 @@ class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { The dialog can be shown by calling `showPriceConsentIfNeeded` on the `InAppPurchaseStoreKitPlatformAddition`. This future will complete immediately when the dialog is shown. A confirmed transaction will be delivered on the `purchaseStream`. + ```dart if (Platform.isIOS) { var iapStoreKitPlatformAddition = _inAppPurchase @@ -355,6 +367,7 @@ containing properties only available on all endorsed platforms. However, in some when the platform is Android and `AppStoreProductDetails` on iOS. Accessing the skuDetails (on Android) or the skProduct (on iOS) provides all the information that is available in the original platform objects. This is an example on how to get the `introductoryPricePeriod` on Android: + ```dart //import for GooglePlayProductDetails import 'package:in_app_purchase_android/in_app_purchase_android.dart'; @@ -368,6 +381,7 @@ if (productDetails is GooglePlayProductDetails) { ``` And this is the way to get the subscriptionGroupIdentifier of a subscription on iOS: + ```dart //import for AppStoreProductDetails import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; @@ -395,6 +409,7 @@ when the platform is Android and `AppStorePurchaseDetails` on iOS. Accessing the skPaymentTransaction provides all the information that is available in the original platform objects. This is an example on how to get the `originalJson` on Android: + ```dart //import for GooglePlayPurchaseDetails import 'package:in_app_purchase_android/in_app_purchase_android.dart'; @@ -408,6 +423,7 @@ if (purchaseDetails is GooglePlayPurchaseDetails) { ``` How to get the `transactionState` of a purchase in iOS, using the original StoreKit API: + ```dart //import for AppStorePurchaseDetails import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; @@ -421,6 +437,7 @@ if (purchaseDetails is AppStorePurchaseDetails) { ``` How to get the `jsonRepresentation` of a transaction in iOS, using StoreKit 2: + ```dart //import for SK2TransactionWrapper import 'package:in_app_purchase_storekit/store_kit_2_wrappers.dart'; @@ -437,6 +454,7 @@ The following code brings up a sheet that enables the user to redeem offer codes that you've set up in App Store Connect. For more information on redeeming offer codes, see [Implementing Offer Codes in Your App](https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/implementing_offer_codes_in_your_app). + ```dart InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.getPlatformAddition(); diff --git a/packages/in_app_purchase/in_app_purchase/ci_config.yaml b/packages/in_app_purchase/in_app_purchase/ci_config.yaml index b352e13e0dfa..e1fa4a00cb18 100644 --- a/packages/in_app_purchase/in_app_purchase/ci_config.yaml +++ b/packages/in_app_purchase/in_app_purchase/ci_config.yaml @@ -1,2 +1,2 @@ # TODO(stuartmorgan): Remove this; see https://github.com/flutter/flutter/issues/102679 -exempt_from_excerpts: true +exempt_from_excerpts: false diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart b/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart new file mode 100644 index 000000000000..b584f092b2d2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart @@ -0,0 +1,240 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +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_2_wrappers.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; + +/// Example app used for README excerpts. +class ExampleApp extends StatefulWidget { + const ExampleApp({super.key}); + + @override + State createState() => _ExampleAppState(); +} + +class _ExampleAppState extends State { + late final StreamSubscription> _subscription; + + @override + void initState() { + super.initState(); + final Stream> purchaseUpdated = + InAppPurchase.instance.purchaseStream; + + // #docregion purchase-updates + _subscription = purchaseUpdated.listen( + (purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, + onDone: () { + _subscription.cancel(); + }, + onError: (error) { + // handle error here. + }, + ); + // #enddocregion purchase-updates + } + + @override + Widget build(BuildContext context) => const SizedBox(); + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} + +// #docregion purchase-updates-handler +void _listenToPurchaseUpdated(List purchaseDetailsList) { + for (final 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 = true; // await _verifyPurchase(purchaseDetails); + if (valid) { + // _deliverProduct(purchaseDetails); + } else { + // _handleInvalidPurchase(purchaseDetails); + } + } + if (purchaseDetails.pendingCompletePurchase) { + // await InAppPurchase.instance.completePurchase(purchaseDetails); + } + } + } +} +// #enddocregion purchase-updates-handler + +// #docregion store-availability +Future checkStoreAvailability() async { + final bool available = await InAppPurchase.instance.isAvailable(); + if (!available) { + // The store cannot be reached or accessed. Update the UI accordingly. + } +} +// #enddocregion store-availability + +// #docregion product-query +Future loadProducts() async { + const Set productIds = {'product1', 'product2'}; + final ProductDetailsResponse response = + await InAppPurchase.instance.queryProductDetails(productIds); + if (response.notFoundIDs.isNotEmpty) { + // Handle the error. + } + final List products = response.productDetails; +} +// #enddocregion product-query + +// #docregion restore-purchases +Future restorePurchases() async { + await InAppPurchase.instance.restorePurchases(); +} +// #enddocregion restore-purchases + +// #docregion purchase-flow +void makePurchase(ProductDetails productDetails) { + final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); + if (_isConsumable(productDetails)) { + InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam); + } else { + InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); + } + // From here the purchase flow will be handled by the underlying store. + // Updates will be delivered to the `InAppPurchase.instance.purchaseStream`. +} + +bool _isConsumable(ProductDetails productDetails) => productDetails.id == 'consumable'; +// #enddocregion purchase-flow + +// #docregion sk2-purchase +Future makeStoreKit2Purchase(ProductDetails productDetails) async { + // import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; + final Sk2PurchaseParam purchaseParamSk2 = Sk2PurchaseParam( + productDetails: productDetails, + winBackOfferId: 'your_win_back_offer_id', + ); + + await InAppPurchase.instance.buyNonConsumable( + purchaseParam: purchaseParamSk2, + ); +} +// #enddocregion sk2-purchase + +// #docregion upgrade-subscription +void upgradeSubscription( + ProductDetails productDetails, + GooglePlayPurchaseDetails oldPurchaseDetails, +) { + final PurchaseParam purchaseParam = GooglePlayPurchaseParam( + productDetails: productDetails, + changeSubscriptionParam: ChangeSubscriptionParam( + oldPurchaseDetails: oldPurchaseDetails, + replacementMode: ReplacementMode.withTimeProration, + ), + ); + InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); +} +// #enddocregion upgrade-subscription + +// #docregion price-consent-delegate +class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, + SKStorefrontWrapper storefront, + ) { + return true; + } + + @override + bool shouldShowPriceConsent() { + return false; + } +} +// #enddocregion price-consent-delegate + +// #docregion price-consent-show +Future showPriceConsent() async { + final InAppPurchaseStoreKitPlatformAddition iapStoreKitPlatformAddition = + InAppPurchase.instance + .getPlatformAddition(); + await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); +} +// #enddocregion price-consent-show + +// #docregion android-product-details +void handleAndroidProductDetails(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + print(product.subscriptionOfferDetails![productDetails.subscriptionIndex!].pricingPhases.first); + } +} +// #enddocregion android-product-details + +// #docregion ios-product-details +void handleIosProductDetails(ProductDetails productDetails) { + if (productDetails is AppStoreProductDetails) { + final SKProductWrapper skProduct = productDetails.skProduct; + print(skProduct.subscriptionGroupIdentifier); + } +} +// #enddocregion ios-product-details + +// #docregion ios-product-details-storekit2 +void handleIosProductDetailsSk2(ProductDetails productDetails) { + if (productDetails is AppStoreProduct2Details) { + final SK2Product product = productDetails.sk2Product; + print(product.subscription?.subscriptionGroupID); + } +} +// #enddocregion ios-product-details-storekit2 + +// #docregion android-purchase-details +void handleAndroidPurchaseDetails(PurchaseDetails purchaseDetails) { + if (purchaseDetails is GooglePlayPurchaseDetails) { + final PurchaseWrapper billingClientPurchase = + purchaseDetails.billingClientPurchase; + print(billingClientPurchase.originalJson); + } +} +// #enddocregion android-purchase-details + +// #docregion ios-purchase-details +void handleIosPurchaseDetails(PurchaseDetails purchaseDetails) { + if (purchaseDetails is AppStorePurchaseDetails) { + final SKPaymentTransactionWrapper skProduct = + purchaseDetails.skPaymentTransaction; + print(skProduct.transactionState); + } +} +// #enddocregion ios-purchase-details + +// #docregion sk2-transaction +Future readSk2Transactions() async { + final List transactions = await SK2Transaction.transactions(); + print(transactions[0].jsonRepresentation); +} +// #enddocregion sk2-transaction + +// #docregion code-redemption +Future presentCodeRedemptionSheet() async { + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = + InAppPurchase.instance.getPlatformAddition(); + await iosPlatformAddition.presentCodeRedemptionSheet(); +} +// #enddocregion code-redemption From 406539974980d07849965ec02cad4b98b61de1f3 Mon Sep 17 00:00:00 2001 From: Gerardo Quintanar Morales Date: Wed, 2 Sep 2026 09:26:47 -0600 Subject: [PATCH 2/3] Fix in_app_purchase README excerpts --- .../in_app_purchase/in_app_purchase/README.md | 242 +++++++++--------- .../example/lib/readme_examples.dart | 71 +++-- 2 files changed, 162 insertions(+), 151 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index f88b2a5e1bdd..4f581c9401f5 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -87,56 +87,44 @@ To listen to the update: ```dart -class _MyAppState extends State { - StreamSubscription> _subscription; - - @override - void initState() { - final Stream purchaseUpdated = - InAppPurchase.instance.purchaseStream; - _subscription = purchaseUpdated.listen((purchaseDetailsList) { - _listenToPurchaseUpdated(purchaseDetailsList); - }, onDone: () { - _subscription.cancel(); - }, onError: (error) { - // handle error here. - }); - super.initState(); - } - - @override - void dispose() { +_subscription = purchaseUpdated.listen( + (purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, + onDone: () { _subscription.cancel(); - super.dispose(); - } + }, + onError: (error) { + // handle error here. + }, +); ``` Here is an example of how to handle purchase updates: ```dart -void _listenToPurchaseUpdated(List purchaseDetailsList) { - purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { +Future _listenToPurchaseUpdated(List purchaseDetailsList) async { + for (final 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) { - bool valid = await _verifyPurchase(purchaseDetails); + purchaseDetails.status == PurchaseStatus.restored) { + final bool valid = await _verifyPurchase(purchaseDetails); if (valid) { - _deliverProduct(purchaseDetails); + await _deliverProduct(purchaseDetails); } else { _handleInvalidPurchase(purchaseDetails); } } if (purchaseDetails.pendingCompletePurchase) { - await InAppPurchase.instance - .completePurchase(purchaseDetails); + await InAppPurchase.instance.completePurchase(purchaseDetails); } } - }); + } } ``` @@ -144,9 +132,11 @@ void _listenToPurchaseUpdated(List purchaseDetailsList) { ```dart -final bool available = await InAppPurchase.instance.isAvailable(); -if (!available) { - // The store cannot be reached or accessed. Update the UI accordingly. +Future checkStoreAvailability() async { + final bool available = await InAppPurchase.instance.isAvailable(); + if (!available) { + // The store cannot be reached or accessed. Update the UI accordingly. + } } ``` @@ -154,15 +144,16 @@ if (!available) { ```dart -// Set literals require Dart 2.2. Alternatively, use -// `Set _kIds = ['product1', 'product2'].toSet()`. -const Set _kIds = {'product1', 'product2'}; -final ProductDetailsResponse response = - await InAppPurchase.instance.queryProductDetails(_kIds); -if (response.notFoundIDs.isNotEmpty) { - // Handle the error. +Future loadProducts() async { + const Set productIds = {'product1', 'product2'}; + final ProductDetailsResponse response = await InAppPurchase.instance.queryProductDetails( + productIds, + ); + if (response.notFoundIDs.isNotEmpty) { + // Handle the error. + } + final List products = response.productDetails; } -List products = response.productDetails; ``` ### Restoring previous purchases @@ -177,7 +168,9 @@ underlying store: ```dart -await InAppPurchase.instance.restorePurchases(); +Future restorePurchases() async { + await InAppPurchase.instance.restorePurchases(); +} ``` Note that the App Store does not have any APIs for querying consumable @@ -194,15 +187,18 @@ call the right purchase method for each type. ```dart -final ProductDetails productDetails = ... // Saved earlier from queryProductDetails(). -final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); -if (_isConsumable(productDetails)) { - InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam); -} else { - InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); +void makePurchase(ProductDetails productDetails) { + final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); + if (_isConsumable(productDetails)) { + InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam); + } else { + InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); + } + // From here the purchase flow will be handled by the underlying store. + // Updates will be delivered to the `InAppPurchase.instance.purchaseStream`. } -// From here the purchase flow will be handled by the underlying store. -// Updates will be delivered to the `InAppPurchase.instance.purchaseStream`. + +bool _isConsumable(ProductDetails productDetails) => productDetails.id == 'consumable'; ``` StoreKit 2 Specific Purchases (iOS/macOS) @@ -210,18 +206,15 @@ When StoreKit 2 is enabled, you can use Sk2PurchaseParam to include StoreKit 2 s ```dart -import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; - -final productDetails = ...; // Obtained from queryProductDetails - -final purchaseParamSk2 = Sk2PurchaseParam( - productDetails: productDetails, - winBackOfferId: 'your_win_back_offer_id', -); +Future makeStoreKit2Purchase(ProductDetails productDetails) async { + // import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; + final Sk2PurchaseParam purchaseParamSk2 = Sk2PurchaseParam( + productDetails: productDetails, + winBackOfferId: 'your_win_back_offer_id', + ); -await InAppPurchase.instance.buyNonConsumable( - purchaseParam: purchaseParamSk2, -); + await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParamSk2); +} ``` ### Completing a purchase @@ -258,14 +251,19 @@ users from accidentally purchasing multiple subscriptions. Refer to the ```dart -final PurchaseDetails oldPurchaseDetails = ...; -PurchaseParam purchaseParam = GooglePlayPurchaseParam( +void upgradeSubscription( + ProductDetails productDetails, + GooglePlayPurchaseDetails oldPurchaseDetails, +) { + final PurchaseParam purchaseParam = GooglePlayPurchaseParam( productDetails: productDetails, changeSubscriptionParam: ChangeSubscriptionParam( - oldPurchaseDetails: oldPurchaseDetails, - replacementMode: ReplacementMode.withTimeProration)); -InAppPurchase.instance - .buyNonConsumable(purchaseParam: purchaseParam); + oldPurchaseDetails: oldPurchaseDetails, + replacementMode: ReplacementMode.withTimeProration, + ), + ); + InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); +} ``` ### Confirming subscription price changes @@ -303,24 +301,20 @@ popup at a different time, for example after clicking a button. To know when the App Store wants to show a popup and prevent this from happening a queue delegate can be registered. The `InAppPurchaseStoreKitPlatformAddition` contains a `setDelegate(SKPaymentQueueDelegateWrapper? delegate)` function that can be used to set a delegate or remove one by setting it to `null`. - + ```dart -//import for InAppPurchaseStoreKitPlatformAddition -import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; - Future initStoreInfo() async { if (Platform.isIOS) { - var iosPlatformAddition = _inAppPurchase - .getPlatformAddition(); + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); } } -@override -Future disposeStore() { +Future disposeStore() async { if (Platform.isIOS) { - var iosPlatformAddition = _inAppPurchase - .getPlatformAddition(); + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); await iosPlatformAddition.setDelegate(null); } } @@ -331,13 +325,12 @@ needs to show this later. ```dart -// import for SKPaymentQueueDelegateWrapper -import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; - class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { @override bool shouldContinueTransaction( - SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + SKPaymentTransactionWrapper transaction, + SKStorefrontWrapper storefront, + ) { return true; } @@ -352,8 +345,8 @@ The dialog can be shown by calling `showPriceConsentIfNeeded` on the `InAppPurch will complete immediately when the dialog is shown. A confirmed transaction will be delivered on the `purchaseStream`. ```dart -if (Platform.isIOS) { - var iapStoreKitPlatformAddition = _inAppPurchase +Future showPriceConsent() async { + final InAppPurchaseStoreKitPlatformAddition iapStoreKitPlatformAddition = InAppPurchase.instance .getPlatformAddition(); await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); } @@ -369,36 +362,33 @@ when the platform is Android and `AppStoreProductDetails` on iOS. Accessing the This is an example on how to get the `introductoryPricePeriod` on Android: ```dart -//import for GooglePlayProductDetails -import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -//import for SkuDetailsWrapper -import 'package:in_app_purchase_android/billing_client_wrappers.dart'; - -if (productDetails is GooglePlayProductDetails) { - SkuDetailsWrapper skuDetails = (productDetails as GooglePlayProductDetails).skuDetails; - print(skuDetails.introductoryPricePeriod); +void handleAndroidProductDetails(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + print(product.subscriptionOfferDetails![productDetails.subscriptionIndex!].pricingPhases.first); + } } ``` And this is the way to get the subscriptionGroupIdentifier of a subscription on iOS: ```dart -//import for AppStoreProductDetails -import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; -//import for SKProductWrapper -import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; - -if (productDetails is AppStoreProductDetails) { - SKProductWrapper skProduct = (productDetails as AppStoreProductDetails).skProduct; - print(skProduct.subscriptionGroupIdentifier); +void handleIosProductDetails(ProductDetails productDetails) { + if (productDetails is AppStoreProductDetails) { + final SKProductWrapper skProduct = productDetails.skProduct; + print(skProduct.subscriptionGroupIdentifier); + } } +``` -// With StoreKit 2 -import 'package:in_app_purchase_storekit/store_kit_2_wrappers.dart'; - -if (productDetails is AppStoreProduct2Details) { - SK2Product product = (productDetails as AppStoreProduct2Details).sk2Product; - print(product.subscription?.subscriptionGroupID); +With StoreKit 2: + +```dart +void handleIosProductDetailsSk2(ProductDetails productDetails) { + if (productDetails is AppStoreProduct2Details) { + final SK2Product product = productDetails.sk2Product; + print(product.subscription?.subscriptionGroupID); + } } ``` @@ -411,39 +401,32 @@ skPaymentTransaction provides all the information that is available in the origi This is an example on how to get the `originalJson` on Android: ```dart -//import for GooglePlayPurchaseDetails -import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -//import for PurchaseWrapper -import 'package:in_app_purchase_android/billing_client_wrappers.dart'; - -if (purchaseDetails is GooglePlayPurchaseDetails) { - PurchaseWrapper billingClientPurchase = (purchaseDetails as GooglePlayPurchaseDetails).billingClientPurchase; - print(billingClientPurchase.originalJson); +void handleAndroidPurchaseDetails(PurchaseDetails purchaseDetails) { + if (purchaseDetails is GooglePlayPurchaseDetails) { + final PurchaseWrapper billingClientPurchase = purchaseDetails.billingClientPurchase; + print(billingClientPurchase.originalJson); + } } ``` How to get the `transactionState` of a purchase in iOS, using the original StoreKit API: ```dart -//import for AppStorePurchaseDetails -import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; -//import for SKProductWrapper -import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; - -if (purchaseDetails is AppStorePurchaseDetails) { - SKPaymentTransactionWrapper skProduct = (purchaseDetails as AppStorePurchaseDetails).skPaymentTransaction; - print(skProduct.transactionState); +void handleIosPurchaseDetails(PurchaseDetails purchaseDetails) { + if (purchaseDetails is AppStorePurchaseDetails) { + final SKPaymentTransactionWrapper skProduct = purchaseDetails.skPaymentTransaction; + print(skProduct.transactionState); + } } ``` How to get the `jsonRepresentation` of a transaction in iOS, using StoreKit 2: ```dart -//import for SK2TransactionWrapper -import 'package:in_app_purchase_storekit/store_kit_2_wrappers.dart'; - -List transactions = await SK2Transaction.transactions(); -print(transactions[0].jsonRepresentation); +Future readSk2Transactions() async { + final List transactions = await SK2Transaction.transactions(); + print(transactions[0].jsonRepresentation); +} ``` Please note that it is required to import `in_app_purchase_android` and/or `in_app_purchase_storekit`. @@ -456,9 +439,12 @@ redeeming offer codes, see [Implementing Offer Codes in Your App](https://develo ```dart -InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = - InAppPurchase.getPlatformAddition(); -iosPlatformAddition.presentCodeRedemptionSheet(); +Future presentCodeRedemptionSheet() async { + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); + await iosPlatformAddition.presentCodeRedemptionSheet(); +} + ``` > **note:** The `InAppPurchaseStoreKitPlatformAddition` is defined in the `in_app_purchase_storekit.dart` diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart b/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart index b584f092b2d2..29153e98b629 100644 --- a/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart +++ b/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart @@ -3,6 +3,7 @@ // 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'; @@ -26,8 +27,7 @@ class _ExampleAppState extends State { @override void initState() { super.initState(); - final Stream> purchaseUpdated = - InAppPurchase.instance.purchaseStream; + final Stream> purchaseUpdated = InAppPurchase.instance.purchaseStream; // #docregion purchase-updates _subscription = purchaseUpdated.listen( @@ -55,30 +55,40 @@ class _ExampleAppState extends State { } // #docregion purchase-updates-handler -void _listenToPurchaseUpdated(List purchaseDetailsList) { +Future _listenToPurchaseUpdated(List purchaseDetailsList) async { for (final purchaseDetails in purchaseDetailsList) { if (purchaseDetails.status == PurchaseStatus.pending) { - // _showPendingUI(); + _showPendingUI(); } else { if (purchaseDetails.status == PurchaseStatus.error) { - // _handleError(purchaseDetails.error!); + _handleError(purchaseDetails.error!); } else if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) { - final bool valid = true; // await _verifyPurchase(purchaseDetails); + final bool valid = await _verifyPurchase(purchaseDetails); if (valid) { - // _deliverProduct(purchaseDetails); + await _deliverProduct(purchaseDetails); } else { - // _handleInvalidPurchase(purchaseDetails); + _handleInvalidPurchase(purchaseDetails); } } if (purchaseDetails.pendingCompletePurchase) { - // await InAppPurchase.instance.completePurchase(purchaseDetails); + await InAppPurchase.instance.completePurchase(purchaseDetails); } } } } // #enddocregion purchase-updates-handler +void _showPendingUI() {} + +void _handleError(IAPError error) {} + +Future _verifyPurchase(PurchaseDetails purchaseDetails) async => true; + +Future _deliverProduct(PurchaseDetails purchaseDetails) async {} + +void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {} + // #docregion store-availability Future checkStoreAvailability() async { final bool available = await InAppPurchase.instance.isAvailable(); @@ -91,8 +101,9 @@ Future checkStoreAvailability() async { // #docregion product-query Future loadProducts() async { const Set productIds = {'product1', 'product2'}; - final ProductDetailsResponse response = - await InAppPurchase.instance.queryProductDetails(productIds); + final ProductDetailsResponse response = await InAppPurchase.instance.queryProductDetails( + productIds, + ); if (response.notFoundIDs.isNotEmpty) { // Handle the error. } @@ -129,9 +140,7 @@ Future makeStoreKit2Purchase(ProductDetails productDetails) async { winBackOfferId: 'your_win_back_offer_id', ); - await InAppPurchase.instance.buyNonConsumable( - purchaseParam: purchaseParamSk2, - ); + await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParamSk2); } // #enddocregion sk2-purchase @@ -151,6 +160,24 @@ void upgradeSubscription( } // #enddocregion upgrade-subscription +// #docregion price-consent-setup +Future initStoreInfo() async { + if (Platform.isIOS) { + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + } +} + +Future disposeStore() async { + if (Platform.isIOS) { + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(null); + } +} +// #enddocregion price-consent-setup + // #docregion price-consent-delegate class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { @override @@ -170,9 +197,8 @@ class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { // #docregion price-consent-show Future showPriceConsent() async { - final InAppPurchaseStoreKitPlatformAddition iapStoreKitPlatformAddition = - InAppPurchase.instance - .getPlatformAddition(); + final InAppPurchaseStoreKitPlatformAddition iapStoreKitPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); } // #enddocregion price-consent-show @@ -207,8 +233,7 @@ void handleIosProductDetailsSk2(ProductDetails productDetails) { // #docregion android-purchase-details void handleAndroidPurchaseDetails(PurchaseDetails purchaseDetails) { if (purchaseDetails is GooglePlayPurchaseDetails) { - final PurchaseWrapper billingClientPurchase = - purchaseDetails.billingClientPurchase; + final PurchaseWrapper billingClientPurchase = purchaseDetails.billingClientPurchase; print(billingClientPurchase.originalJson); } } @@ -217,8 +242,7 @@ void handleAndroidPurchaseDetails(PurchaseDetails purchaseDetails) { // #docregion ios-purchase-details void handleIosPurchaseDetails(PurchaseDetails purchaseDetails) { if (purchaseDetails is AppStorePurchaseDetails) { - final SKPaymentTransactionWrapper skProduct = - purchaseDetails.skPaymentTransaction; + final SKPaymentTransactionWrapper skProduct = purchaseDetails.skPaymentTransaction; print(skProduct.transactionState); } } @@ -233,8 +257,9 @@ Future readSk2Transactions() async { // #docregion code-redemption Future presentCodeRedemptionSheet() async { - final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = - InAppPurchase.instance.getPlatformAddition(); + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); await iosPlatformAddition.presentCodeRedemptionSheet(); } + // #enddocregion code-redemption From 56c9ad38876f9c9b2ab6e7768e544720fef62ac5 Mon Sep 17 00:00:00 2001 From: Gerardo Quintanar Morales Date: Mon, 7 Sep 2026 09:55:33 -0600 Subject: [PATCH 3/3] Address gemini-code-assist review comments on readme_examples.dart Guard StoreKit platform-addition calls with the correct iOS/macOS platform checks, and avoid unsafe null assertions/index access when reading Android subscription offers and StoreKit 2 transactions. --- .../in_app_purchase/in_app_purchase/README.md | 39 ++++++++++++------- .../example/lib/readme_examples.dart | 38 +++++++++++------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 4f581c9401f5..151b44780656 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -304,18 +304,18 @@ can be used to set a delegate or remove one by setting it to `null`. ```dart Future initStoreInfo() async { - if (Platform.isIOS) { - final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance .getPlatformAddition(); - await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + await platformAddition.setDelegate(ExamplePaymentQueueDelegate()); } } Future disposeStore() async { - if (Platform.isIOS) { - final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance .getPlatformAddition(); - await iosPlatformAddition.setDelegate(null); + await platformAddition.setDelegate(null); } } ``` @@ -346,9 +346,11 @@ will complete immediately when the dialog is shown. A confirmed transaction will ```dart Future showPriceConsent() async { - final InAppPurchaseStoreKitPlatformAddition iapStoreKitPlatformAddition = InAppPurchase.instance - .getPlatformAddition(); - await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.showPriceConsentIfNeeded(); + } } ``` @@ -365,7 +367,11 @@ This is an example on how to get the `introductoryPricePeriod` on Android: void handleAndroidProductDetails(ProductDetails productDetails) { if (productDetails is GooglePlayProductDetails) { final ProductDetailsWrapper product = productDetails.productDetails; - print(product.subscriptionOfferDetails![productDetails.subscriptionIndex!].pricingPhases.first); + final int? index = productDetails.subscriptionIndex; + final List? offers = product.subscriptionOfferDetails; + if (index != null && offers != null && index < offers.length) { + print(offers[index].pricingPhases.first); + } } } ``` @@ -425,7 +431,9 @@ How to get the `jsonRepresentation` of a transaction in iOS, using StoreKit 2: ```dart Future readSk2Transactions() async { final List transactions = await SK2Transaction.transactions(); - print(transactions[0].jsonRepresentation); + if (transactions.isNotEmpty) { + print(transactions.first.jsonRepresentation); + } } ``` @@ -440,11 +448,12 @@ redeeming offer codes, see [Implementing Offer Codes in Your App](https://develo ```dart Future presentCodeRedemptionSheet() async { - final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance - .getPlatformAddition(); - await iosPlatformAddition.presentCodeRedemptionSheet(); + if (Platform.isIOS) { + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); + await iosPlatformAddition.presentCodeRedemptionSheet(); + } } - ``` > **note:** The `InAppPurchaseStoreKitPlatformAddition` is defined in the `in_app_purchase_storekit.dart` diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart b/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart index 29153e98b629..563e2010e084 100644 --- a/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart +++ b/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart @@ -162,18 +162,18 @@ void upgradeSubscription( // #docregion price-consent-setup Future initStoreInfo() async { - if (Platform.isIOS) { - final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance .getPlatformAddition(); - await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + await platformAddition.setDelegate(ExamplePaymentQueueDelegate()); } } Future disposeStore() async { - if (Platform.isIOS) { - final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance .getPlatformAddition(); - await iosPlatformAddition.setDelegate(null); + await platformAddition.setDelegate(null); } } // #enddocregion price-consent-setup @@ -197,9 +197,11 @@ class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { // #docregion price-consent-show Future showPriceConsent() async { - final InAppPurchaseStoreKitPlatformAddition iapStoreKitPlatformAddition = InAppPurchase.instance - .getPlatformAddition(); - await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.showPriceConsentIfNeeded(); + } } // #enddocregion price-consent-show @@ -207,7 +209,11 @@ Future showPriceConsent() async { void handleAndroidProductDetails(ProductDetails productDetails) { if (productDetails is GooglePlayProductDetails) { final ProductDetailsWrapper product = productDetails.productDetails; - print(product.subscriptionOfferDetails![productDetails.subscriptionIndex!].pricingPhases.first); + final int? index = productDetails.subscriptionIndex; + final List? offers = product.subscriptionOfferDetails; + if (index != null && offers != null && index < offers.length) { + print(offers[index].pricingPhases.first); + } } } // #enddocregion android-product-details @@ -251,15 +257,19 @@ void handleIosPurchaseDetails(PurchaseDetails purchaseDetails) { // #docregion sk2-transaction Future readSk2Transactions() async { final List transactions = await SK2Transaction.transactions(); - print(transactions[0].jsonRepresentation); + if (transactions.isNotEmpty) { + print(transactions.first.jsonRepresentation); + } } // #enddocregion sk2-transaction // #docregion code-redemption Future presentCodeRedemptionSheet() async { - final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance - .getPlatformAddition(); - await iosPlatformAddition.presentCodeRedemptionSheet(); + if (Platform.isIOS) { + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); + await iosPlatformAddition.presentCodeRedemptionSheet(); + } } // #enddocregion code-redemption