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..151b44780656 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,80 +85,75 @@ 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; - - @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); } } - }); + } } ``` ### Connecting to the underlying store + ```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. + } } ``` ### Loading products for sale + ```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 @@ -170,8 +166,11 @@ underlying store: * [Verifying Google Play purchases](https://developer.android.com/google/play/billing/security#verify) + ```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 @@ -186,34 +185,36 @@ 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); -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) 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'; - -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 @@ -248,15 +249,21 @@ 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( +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 @@ -294,24 +301,21 @@ 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(); - await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.setDelegate(ExamplePaymentQueueDelegate()); } } -@override -Future disposeStore() { - if (Platform.isIOS) { - var iosPlatformAddition = _inAppPurchase - .getPlatformAddition(); - await iosPlatformAddition.setDelegate(null); +Future disposeStore() async { + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.setDelegate(null); } } ``` @@ -319,14 +323,14 @@ 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'; - class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { @override bool shouldContinueTransaction( - SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + SKPaymentTransactionWrapper transaction, + SKStorefrontWrapper storefront, + ) { return true; } @@ -339,11 +343,14 @@ 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 - .getPlatformAddition(); - await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); +Future showPriceConsent() async { + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.showPriceConsentIfNeeded(); + } } ``` @@ -355,36 +362,39 @@ 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'; -//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; + final int? index = productDetails.subscriptionIndex; + final List? offers = product.subscriptionOfferDetails; + if (index != null && offers != null && index < offers.length) { + print(offers[index].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); + } } ``` @@ -395,38 +405,36 @@ 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'; -//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(); + if (transactions.isNotEmpty) { + print(transactions.first.jsonRepresentation); + } +} ``` Please note that it is required to import `in_app_purchase_android` and/or `in_app_purchase_storekit`. @@ -437,10 +445,15 @@ 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(); -iosPlatformAddition.presentCodeRedemptionSheet(); +Future presentCodeRedemptionSheet() async { + 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/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..563e2010e084 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/lib/readme_examples.dart @@ -0,0 +1,275 @@ +// 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 '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_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 +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) { + final bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + await _deliverProduct(purchaseDetails); + } else { + _handleInvalidPurchase(purchaseDetails); + } + } + if (purchaseDetails.pendingCompletePurchase) { + 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(); + 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-setup +Future initStoreInfo() async { + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.setDelegate(ExamplePaymentQueueDelegate()); + } +} + +Future disposeStore() async { + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.setDelegate(null); + } +} +// #enddocregion price-consent-setup + +// #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 { + if (Platform.isIOS || Platform.isMacOS) { + final InAppPurchaseStoreKitPlatformAddition platformAddition = InAppPurchase.instance + .getPlatformAddition(); + await platformAddition.showPriceConsentIfNeeded(); + } +} +// #enddocregion price-consent-show + +// #docregion android-product-details +void handleAndroidProductDetails(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + 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 + +// #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(); + if (transactions.isNotEmpty) { + print(transactions.first.jsonRepresentation); + } +} +// #enddocregion sk2-transaction + +// #docregion code-redemption +Future presentCodeRedemptionSheet() async { + if (Platform.isIOS) { + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = InAppPurchase.instance + .getPlatformAddition(); + await iosPlatformAddition.presentCodeRedemptionSheet(); + } +} + +// #enddocregion code-redemption