| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; |
| import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; |
| |
| import 'consumable_store.dart'; |
| import 'example_payment_queue_delegate.dart'; |
| |
| void main() { |
| WidgetsFlutterBinding.ensureInitialized(); |
| |
| // When using the Android plugin directly it is mandatory to register |
| // the plugin as default instance as part of initializing the app. |
| InAppPurchaseStoreKitPlatform.registerPlatform(); |
| |
| runApp(_MyApp()); |
| } |
| |
| const String _kConsumableId = 'consumable'; |
| const String _kUpgradeId = 'upgrade'; |
| const String _kSilverSubscriptionId = 'subscription_silver'; |
| const String _kGoldSubscriptionId = 'subscription_gold'; |
| const List<String> _kProductIds = <String>[ |
| _kConsumableId, |
| _kUpgradeId, |
| _kSilverSubscriptionId, |
| _kGoldSubscriptionId, |
| ]; |
| |
| class _MyApp extends StatefulWidget { |
| @override |
| State<_MyApp> createState() => _MyAppState(); |
| } |
| |
| class _MyAppState extends State<_MyApp> { |
| final InAppPurchaseStoreKitPlatform _iapStoreKitPlatform = |
| InAppPurchasePlatform.instance as InAppPurchaseStoreKitPlatform; |
| final InAppPurchaseStoreKitPlatformAddition _iapStoreKitPlatformAddition = |
| InAppPurchasePlatformAddition.instance! |
| as InAppPurchaseStoreKitPlatformAddition; |
| late StreamSubscription<List<PurchaseDetails>> _subscription; |
| List<String> _notFoundIds = <String>[]; |
| List<ProductDetails> _products = <ProductDetails>[]; |
| List<PurchaseDetails> _purchases = <PurchaseDetails>[]; |
| List<String> _consumables = <String>[]; |
| bool _isAvailable = false; |
| bool _purchasePending = false; |
| bool _loading = true; |
| String? _queryProductError; |
| |
| @override |
| void initState() { |
| final Stream<List<PurchaseDetails>> purchaseUpdated = |
| _iapStoreKitPlatform.purchaseStream; |
| _subscription = |
| purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) { |
| _listenToPurchaseUpdated(purchaseDetailsList); |
| }, onDone: () { |
| _subscription.cancel(); |
| }, onError: (Object error) { |
| // handle error here. |
| }); |
| |
| // Register the example payment queue delegate |
| _iapStoreKitPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); |
| |
| initStoreInfo(); |
| super.initState(); |
| } |
| |
| Future<void> initStoreInfo() async { |
| final bool isAvailable = await _iapStoreKitPlatform.isAvailable(); |
| if (!isAvailable) { |
| setState(() { |
| _isAvailable = isAvailable; |
| _products = <ProductDetails>[]; |
| _purchases = <PurchaseDetails>[]; |
| _notFoundIds = <String>[]; |
| _consumables = <String>[]; |
| _purchasePending = false; |
| _loading = false; |
| }); |
| return; |
| } |
| |
| final ProductDetailsResponse productDetailResponse = |
| await _iapStoreKitPlatform.queryProductDetails(_kProductIds.toSet()); |
| if (productDetailResponse.error != null) { |
| setState(() { |
| _queryProductError = productDetailResponse.error!.message; |
| _isAvailable = isAvailable; |
| _products = productDetailResponse.productDetails; |
| _purchases = <PurchaseDetails>[]; |
| _notFoundIds = productDetailResponse.notFoundIDs; |
| _consumables = <String>[]; |
| _purchasePending = false; |
| _loading = false; |
| }); |
| return; |
| } |
| |
| if (productDetailResponse.productDetails.isEmpty) { |
| setState(() { |
| _queryProductError = null; |
| _isAvailable = isAvailable; |
| _products = productDetailResponse.productDetails; |
| _purchases = <PurchaseDetails>[]; |
| _notFoundIds = productDetailResponse.notFoundIDs; |
| _consumables = <String>[]; |
| _purchasePending = false; |
| _loading = false; |
| }); |
| return; |
| } |
| |
| final List<String> consumables = await ConsumableStore.load(); |
| setState(() { |
| _isAvailable = isAvailable; |
| _products = productDetailResponse.productDetails; |
| _notFoundIds = productDetailResponse.notFoundIDs; |
| _consumables = consumables; |
| _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(), |
| _buildConsumableBox(), |
| _buildRestoreButton(), |
| ], |
| ), |
| ); |
| } else { |
| stack.add(Center( |
| child: Text(_queryProductError!), |
| )); |
| } |
| if (_purchasePending) { |
| stack.add( |
| // TODO(goderbauer): Make this const when that's available on stable. |
| // ignore: prefer_const_constructors |
| Stack( |
| children: const <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) { |
| _iapStoreKitPlatform.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: previousPurchase != null |
| ? IconButton( |
| onPressed: () { |
| _iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); |
| }, |
| icon: const Icon(Icons.upgrade)) |
| : TextButton( |
| style: TextButton.styleFrom( |
| backgroundColor: Colors.green[800], |
| foregroundColor: Colors.white, |
| ), |
| onPressed: () { |
| final PurchaseParam purchaseParam = PurchaseParam( |
| productDetails: productDetails, |
| ); |
| if (productDetails.id == _kConsumableId) { |
| _iapStoreKitPlatform.buyConsumable( |
| purchaseParam: purchaseParam); |
| } else { |
| _iapStoreKitPlatform.buyNonConsumable( |
| purchaseParam: purchaseParam); |
| } |
| }, |
| child: Text(productDetails.price), |
| )); |
| }, |
| )); |
| |
| return Card( |
| child: Column( |
| children: <Widget>[productHeader, const Divider()] + productList)); |
| } |
| |
| Card _buildConsumableBox() { |
| if (_loading) { |
| return const Card( |
| child: ListTile( |
| leading: CircularProgressIndicator(), |
| title: Text('Fetching consumables...'))); |
| } |
| if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { |
| return const Card(); |
| } |
| const ListTile consumableHeader = |
| ListTile(title: Text('Purchased consumables')); |
| final List<Widget> tokens = _consumables.map((String id) { |
| return GridTile( |
| child: IconButton( |
| icon: const Icon( |
| Icons.stars, |
| size: 42.0, |
| color: Colors.orange, |
| ), |
| splashColor: Colors.yellowAccent, |
| onPressed: () => consume(id), |
| ), |
| ); |
| }).toList(); |
| return Card( |
| child: Column(children: <Widget>[ |
| consumableHeader, |
| const Divider(), |
| GridView.count( |
| crossAxisCount: 5, |
| shrinkWrap: true, |
| padding: const EdgeInsets.all(16.0), |
| children: tokens, |
| ) |
| ])); |
| } |
| |
| 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).colorScheme.primary, |
| foregroundColor: Colors.white, |
| ), |
| onPressed: () => _iapStoreKitPlatform.restorePurchases(), |
| child: const Text('Restore purchases'), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| Future<void> consume(String id) async { |
| await ConsumableStore.consume(id); |
| final List<String> consumables = await ConsumableStore.load(); |
| setState(() { |
| _consumables = consumables; |
| }); |
| } |
| |
| void showPendingUI() { |
| setState(() { |
| _purchasePending = true; |
| }); |
| } |
| |
| Future<void> deliverProduct(PurchaseDetails purchaseDetails) async { |
| // IMPORTANT!! Always verify purchase details before delivering the product. |
| if (purchaseDetails.productID == _kConsumableId) { |
| await ConsumableStore.save(purchaseDetails.purchaseID!); |
| final List<String> consumables = await ConsumableStore.load(); |
| setState(() { |
| _purchasePending = false; |
| _consumables = consumables; |
| }); |
| } else { |
| setState(() { |
| _purchases.add(purchaseDetails); |
| _purchasePending = false; |
| }); |
| } |
| } |
| |
| void handleError(IAPError error) { |
| setState(() { |
| _purchasePending = false; |
| }); |
| } |
| |
| Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) { |
| // IMPORTANT!! Always verify a purchase before delivering the product. |
| // For the purpose of an example, we directly return true. |
| return Future<bool>.value(true); |
| } |
| |
| void _handleInvalidPurchase(PurchaseDetails purchaseDetails) { |
| // handle invalid purchase here if _verifyPurchase` failed. |
| } |
| |
| void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) { |
| purchaseDetailsList.forEach(_handleReportedPurchaseState); |
| } |
| |
| Future<void> _handleReportedPurchaseState( |
| 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) { |
| final bool valid = await _verifyPurchase(purchaseDetails); |
| if (valid) { |
| await deliverProduct(purchaseDetails); |
| } else { |
| _handleInvalidPurchase(purchaseDetails); |
| return; |
| } |
| } |
| |
| if (purchaseDetails.pendingCompletePurchase) { |
| await _iapStoreKitPlatform.completePurchase(purchaseDetails); |
| } |
| } |
| } |
| } |