// Copyright 2019 The Chromium 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 'dart:io'; import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'consumable_store.dart'; void main() { // For play billing library 2.0 on Android, it is mandatory to call // [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) // as part of initializing the app. InAppPurchaseConnection.enablePendingPurchases(); runApp(_MyApp()); } const bool _kAutoConsume = true; const String _kConsumableId = 'consumable'; const List _kProductIds = [ _kConsumableId, 'upgrade', 'subscription' ]; class _MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<_MyApp> { final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance; StreamSubscription> _subscription; List _notFoundIds = []; List _products = []; List _purchases = []; List _consumables = []; bool _isAvailable = false; bool _purchasePending = false; bool _loading = true; String _queryProductError; @override void initState() { Stream purchaseUpdated = InAppPurchaseConnection.instance.purchaseUpdatedStream; _subscription = purchaseUpdated.listen((purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); }, onDone: () { _subscription.cancel(); }, onError: (error) { // handle error here. }); initStoreInfo(); super.initState(); } Future initStoreInfo() async { final bool isAvailable = await _connection.isAvailable(); if (!isAvailable) { setState(() { _isAvailable = isAvailable; _products = []; _purchases = []; _notFoundIds = []; _consumables = []; _purchasePending = false; _loading = false; }); return; } ProductDetailsResponse productDetailResponse = await _connection.queryProductDetails(_kProductIds.toSet()); if (productDetailResponse.error != null) { setState(() { _queryProductError = productDetailResponse.error.message; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; _purchases = []; _notFoundIds = productDetailResponse.notFoundIDs; _consumables = []; _purchasePending = false; _loading = false; }); return; } if (productDetailResponse.productDetails.isEmpty) { setState(() { _queryProductError = null; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; _purchases = []; _notFoundIds = productDetailResponse.notFoundIDs; _consumables = []; _purchasePending = false; _loading = false; }); return; } final QueryPurchaseDetailsResponse purchaseResponse = await _connection.queryPastPurchases(); if (purchaseResponse.error != null) { // handle query past purchase error.. } final List verifiedPurchases = []; for (PurchaseDetails purchase in purchaseResponse.pastPurchases) { if (await _verifyPurchase(purchase)) { verifiedPurchases.add(purchase); } } List consumables = await ConsumableStore.load(); setState(() { _isAvailable = isAvailable; _products = productDetailResponse.productDetails; _purchases = verifiedPurchases; _notFoundIds = productDetailResponse.notFoundIDs; _consumables = consumables; _purchasePending = false; _loading = false; }); } @override void dispose() { _subscription.cancel(); super.dispose(); } @override Widget build(BuildContext context) { List stack = []; if (_queryProductError == null) { stack.add( ListView( children: [ _buildConnectionCheckTile(), _buildProductList(), _buildConsumableBox(), ], ), ); } else { stack.add(Center( child: Text(_queryProductError), )); } if (_purchasePending) { stack.add( Stack( children: [ Opacity( opacity: 0.3, child: const ModalBarrier(dismissible: false, color: Colors.grey), ), Center( child: CircularProgressIndicator(), ), ], ), ); } return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('IAP Example'), ), body: Stack( children: stack, ), ), ); } Card _buildConnectionCheckTile() { if (_loading) { return Card(child: ListTile(title: const Text('Trying to connect...'))); } final Widget storeHeader = ListTile( leading: Icon(_isAvailable ? Icons.check : Icons.block, color: _isAvailable ? Colors.green : ThemeData.light().errorColor), title: Text( 'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'), ); final List children = [storeHeader]; if (!_isAvailable) { children.addAll([ Divider(), ListTile( title: Text('Not connected', style: TextStyle(color: ThemeData.light().errorColor)), subtitle: const Text( 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), ), ]); } return Card(child: Column(children: children)); } Card _buildProductList() { if (_loading) { return Card( child: (ListTile( leading: CircularProgressIndicator(), title: Text('Fetching products...')))); } if (!_isAvailable) { return Card(); } final ListTile productHeader = ListTile(title: Text('Products for Sale')); List productList = []; if (_notFoundIds.isNotEmpty) { productList.add(ListTile( title: Text('[${_notFoundIds.join(", ")}] not found', style: TextStyle(color: ThemeData.light().errorColor)), subtitle: 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. Map purchases = Map.fromEntries(_purchases.map((PurchaseDetails purchase) { if (purchase.pendingCompletePurchase) { InAppPurchaseConnection.instance.completePurchase(purchase); } return MapEntry(purchase.productID, purchase); })); productList.addAll(_products.map( (ProductDetails productDetails) { PurchaseDetails previousPurchase = purchases[productDetails.id]; return ListTile( title: Text( productDetails.title, ), subtitle: Text( productDetails.description, ), trailing: previousPurchase != null ? Icon(Icons.check) : TextButton( child: Text(productDetails.price), style: TextButton.styleFrom( backgroundColor: Colors.green[800], primary: Colors.white, ), onPressed: () { PurchaseParam purchaseParam = PurchaseParam( productDetails: productDetails, applicationUserName: null, sandboxTesting: true); if (productDetails.id == _kConsumableId) { _connection.buyConsumable( purchaseParam: purchaseParam, autoConsume: _kAutoConsume || Platform.isIOS); } else { _connection.buyNonConsumable( purchaseParam: purchaseParam); } }, )); }, )); return Card( child: Column(children: [productHeader, Divider()] + productList)); } Card _buildConsumableBox() { if (_loading) { return Card( child: (ListTile( leading: CircularProgressIndicator(), title: Text('Fetching consumables...')))); } if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { return Card(); } final ListTile consumableHeader = ListTile(title: Text('Purchased consumables')); final List tokens = _consumables.map((String id) { return GridTile( child: IconButton( icon: Icon( Icons.stars, size: 42.0, color: Colors.orange, ), splashColor: Colors.yellowAccent, onPressed: () => consume(id), ), ); }).toList(); return Card( child: Column(children: [ consumableHeader, Divider(), GridView.count( crossAxisCount: 5, children: tokens, shrinkWrap: true, padding: EdgeInsets.all(16.0), ) ])); } Future consume(String id) async { await ConsumableStore.consume(id); final List consumables = await ConsumableStore.load(); setState(() { _consumables = consumables; }); } void showPendingUI() { setState(() { _purchasePending = true; }); } void deliverProduct(PurchaseDetails purchaseDetails) async { // IMPORTANT!! Always verify a purchase purchase details before delivering the product. if (purchaseDetails.productID == _kConsumableId) { await ConsumableStore.save(purchaseDetails.purchaseID); List consumables = await ConsumableStore.load(); setState(() { _purchasePending = false; _consumables = consumables; }); } else { setState(() { _purchases.add(purchaseDetails); _purchasePending = false; }); } } void handleError(IAPError error) { setState(() { _purchasePending = false; }); } Future _verifyPurchase(PurchaseDetails purchaseDetails) { // IMPORTANT!! Always verify a purchase before delivering the product. // For the purpose of an example, we directly return true. return Future.value(true); } void _handleInvalidPurchase(PurchaseDetails purchaseDetails) { // handle invalid purchase here if _verifyPurchase` failed. } void _listenToPurchaseUpdated(List purchaseDetailsList) { purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { if (purchaseDetails.status == PurchaseStatus.pending) { showPendingUI(); } else { if (purchaseDetails.status == PurchaseStatus.error) { handleError(purchaseDetails.error); } else if (purchaseDetails.status == PurchaseStatus.purchased) { bool valid = await _verifyPurchase(purchaseDetails); if (valid) { deliverProduct(purchaseDetails); } else { _handleInvalidPurchase(purchaseDetails); return; } } if (Platform.isAndroid) { if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) { await InAppPurchaseConnection.instance .consumePurchase(purchaseDetails); } } if (purchaseDetails.pendingCompletePurchase) { await InAppPurchaseConnection.instance .completePurchase(purchaseDetails); } } }); } }