@@ -6,16 +6,17 @@ import 'dart:async';
66import 'dart:io' ;
77import 'package:flutter/material.dart' ;
88import 'package:in_app_purchase/in_app_purchase.dart' ;
9+ import 'consumable_store.dart' ;
910
1011void main () {
1112 runApp (MyApp ());
1213}
1314
14- // Switch this to true if you want to try out auto consume when buying a consumable.
15- const bool kAutoConsume = false ;
15+ const bool kAutoConsume = true ;
1616
17+ const String _kConsumableId = 'consumable' ;
1718const List <String > _kProductIds = < String > [
18- 'consumable' ,
19+ _kConsumableId ,
1920 'upgrade' ,
2021 'subscription'
2122];
@@ -26,9 +27,16 @@ class MyApp extends StatefulWidget {
2627}
2728
2829class _MyAppState extends State <MyApp > {
30+ final InAppPurchaseConnection _connection = InAppPurchaseConnection .instance;
2931 StreamSubscription <List <PurchaseDetails >> _subscription;
30-
32+ List <String > _notFoundIds = [];
33+ List <ProductDetails > _products = [];
34+ List <PurchaseDetails > _purchases = [];
35+ List <String > _consumables = [];
36+ bool _isAvailable = false ;
3137 bool _purchasePending = false ;
38+ bool _loading = true ;
39+
3240 @override
3341 void initState () {
3442 Stream purchaseUpdated =
@@ -40,9 +48,60 @@ class _MyAppState extends State<MyApp> {
4048 }, onError: (error) {
4149 // handle error here.
4250 });
51+ initStoreInfo ();
4352 super .initState ();
4453 }
4554
55+ Future <void > initStoreInfo () async {
56+ final bool isAvailable = await _connection.isAvailable ();
57+ if (! isAvailable) {
58+ setState (() {
59+ _isAvailable = isAvailable;
60+ _products = [];
61+ _purchases = [];
62+ _notFoundIds = [];
63+ _consumables = [];
64+ _purchasePending = false ;
65+ _loading = false ;
66+ });
67+ return ;
68+ }
69+
70+ ProductDetailsResponse productDetails =
71+ await _connection.queryProductDetails (_kProductIds.toSet ());
72+ if (productDetails.productDetails.isEmpty) {
73+ setState (() {
74+ _isAvailable = isAvailable;
75+ _products = productDetails.productDetails;
76+ _purchases = [];
77+ _notFoundIds = productDetails.notFoundIDs;
78+ _consumables = [];
79+ _purchasePending = false ;
80+ _loading = false ;
81+ });
82+ return ;
83+ }
84+
85+ final QueryPurchaseDetailsResponse purchaseResponse =
86+ await _connection.queryPastPurchases ();
87+ final List <PurchaseDetails > verifiedPurchases = [];
88+ for (PurchaseDetails purchase in purchaseResponse.pastPurchases) {
89+ if (await _verifyPurchase (purchase)) {
90+ verifiedPurchases.add (purchase);
91+ }
92+ }
93+ List <String > consumables = await ConsumableStore .load ();
94+ setState (() {
95+ _isAvailable = isAvailable;
96+ _products = productDetails.productDetails;
97+ _purchases = verifiedPurchases;
98+ _notFoundIds = productDetails.notFoundIDs;
99+ _consumables = consumables;
100+ _purchasePending = false ;
101+ _loading = false ;
102+ });
103+ }
104+
46105 @override
47106 void dispose () {
48107 _subscription.cancel ();
@@ -55,38 +114,9 @@ class _MyAppState extends State<MyApp> {
55114 stack.add (
56115 ListView (
57116 children: [
58- FutureBuilder (
59- future: _buildConnectionCheckTile (),
60- builder: (BuildContext context, AsyncSnapshot snapshot) {
61- if (snapshot.error != null ) {
62- return buildListCard (ListTile (
63- title: Text (
64- 'Error connecting: ' + snapshot.error.toString ())));
65- } else if (! snapshot.hasData) {
66- return Card (
67- child: ListTile (title: const Text ('Trying to connect...' )));
68- }
69- return snapshot.data;
70- },
71- ),
72- FutureBuilder (
73- future: _buildProductList (),
74- builder: (BuildContext context, AsyncSnapshot snapshot) {
75- if (snapshot.error != null ) {
76- return Center (
77- child: buildListCard (ListTile (
78- title:
79- Text ('Error fetching products ${snapshot .error }' ))),
80- );
81- } else if (! snapshot.hasData) {
82- return Card (
83- child: (ListTile (
84- leading: CircularProgressIndicator (),
85- title: Text ('Fetching products...' ))));
86- }
87- return snapshot.data;
88- },
89- ),
117+ _buildConnectionCheckTile (),
118+ _buildProductList (),
119+ _buildConsumableBox (),
90120 ],
91121 ),
92122 );
@@ -118,17 +148,19 @@ class _MyAppState extends State<MyApp> {
118148 );
119149 }
120150
121- Future <Card > _buildConnectionCheckTile () async {
122- final bool available = await InAppPurchaseConnection .instance.isAvailable ();
151+ Card _buildConnectionCheckTile () {
152+ if (_loading) {
153+ return Card (child: ListTile (title: const Text ('Trying to connect...' )));
154+ }
123155 final Widget storeHeader = ListTile (
124- leading: Icon (available ? Icons .check : Icons .block,
125- color: available ? Colors .green : ThemeData .light ().errorColor),
156+ leading: Icon (_isAvailable ? Icons .check : Icons .block,
157+ color: _isAvailable ? Colors .green : ThemeData .light ().errorColor),
126158 title: Text (
127- 'The store is ' + (available ? 'available' : 'unavailable' ) + '.' ),
159+ 'The store is ' + (_isAvailable ? 'available' : 'unavailable' ) + '.' ),
128160 );
129161 final List <Widget > children = < Widget > [storeHeader];
130162
131- if (! available ) {
163+ if (! _isAvailable ) {
132164 children.addAll ([
133165 Divider (),
134166 ListTile (
@@ -142,21 +174,23 @@ class _MyAppState extends State<MyApp> {
142174 return Card (child: Column (children: children));
143175 }
144176
145- Future <Card > _buildProductList () async {
146- InAppPurchaseConnection connection = InAppPurchaseConnection .instance;
147- final bool available = await connection.isAvailable ();
148- if (! available) {
177+ Card _buildProductList () {
178+ if (_loading) {
179+ return Card (
180+ child: (ListTile (
181+ leading: CircularProgressIndicator (),
182+ title: Text ('Fetching products...' ))));
183+ }
184+ if (! _isAvailable) {
149185 return Card ();
150186 }
151187 final ListTile productHeader = ListTile (
152188 title: Text ('Products for Sale' ,
153189 style: Theme .of (context).textTheme.headline));
154- ProductDetailsResponse response =
155- await connection.queryProductDetails (_kProductIds.toSet ());
156190 List <ListTile > productList = < ListTile > [];
157- if (! response.notFoundIDs .isEmpty) {
191+ if (! _notFoundIds .isEmpty) {
158192 productList.add (ListTile (
159- title: Text ('[${response . notFoundIDs .join (", " )}] not found' ,
193+ title: Text ('[${_notFoundIds .join (", " )}] not found' ,
160194 style: TextStyle (color: ThemeData .light ().errorColor)),
161195 subtitle: Text (
162196 'This app needs special configuration to run. Please see example/README.md for instructions.' )));
@@ -165,18 +199,14 @@ class _MyAppState extends State<MyApp> {
165199 // This loading previous purchases code is just a demo. Please do not use this as it is.
166200 // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it.
167201 // We recommend that you use your own server to verity the purchase data.
168- Map <String , PurchaseDetails > purchases = Map .fromEntries (
169- ((await connection.queryPastPurchases ()).pastPurchases)
170- .map ((PurchaseDetails purchase) {
202+ Map <String , PurchaseDetails > purchases =
203+ Map .fromEntries (_purchases.map ((PurchaseDetails purchase) {
171204 if (Platform .isIOS) {
172205 InAppPurchaseConnection .instance.completePurchase (purchase);
173206 }
174- if (Platform .isAndroid && purchase.productID == 'consumable' ) {
175- InAppPurchaseConnection .instance.consumePurchase (purchase);
176- }
177207 return MapEntry <String , PurchaseDetails >(purchase.productID, purchase);
178208 }));
179- productList.addAll (response.productDetails .map (
209+ productList.addAll (_products .map (
180210 (ProductDetails productDetails) {
181211 PurchaseDetails previousPurchase = purchases[productDetails.id];
182212 return ListTile (
@@ -197,12 +227,12 @@ class _MyAppState extends State<MyApp> {
197227 productDetails: productDetails,
198228 applicationUserName: null ,
199229 sandboxTesting: true );
200- if (productDetails.id == 'consumable' ) {
201- connection .buyConsumable (
230+ if (productDetails.id == _kConsumableId ) {
231+ _connection .buyConsumable (
202232 purchaseParam: purchaseParam,
203233 autoConsume: kAutoConsume || Platform .isIOS);
204234 } else {
205- connection .buyNonConsumable (
235+ _connection .buyNonConsumable (
206236 purchaseParam: purchaseParam);
207237 }
208238 },
@@ -215,19 +245,76 @@ class _MyAppState extends State<MyApp> {
215245 Column (children: < Widget > [productHeader, Divider ()] + productList));
216246 }
217247
218- void showPendingUI () {
248+ Card _buildConsumableBox () {
249+ if (_loading) {
250+ return Card (
251+ child: (ListTile (
252+ leading: CircularProgressIndicator (),
253+ title: Text ('Fetching consumables...' ))));
254+ }
255+ if (! _isAvailable || _notFoundIds.contains (_kConsumableId)) {
256+ return Card ();
257+ }
258+ final ListTile consumableHeader = ListTile (
259+ title: Text ('Purchased consumables' ,
260+ style: Theme .of (context).textTheme.headline));
261+ final List <Widget > tokens = _consumables.map ((String id) {
262+ return GridTile (
263+ child: IconButton (
264+ icon: Icon (
265+ Icons .stars,
266+ size: 42.0 ,
267+ color: Colors .orange,
268+ ),
269+ splashColor: Colors .yellowAccent,
270+ onPressed: () => consume (id),
271+ ),
272+ );
273+ }).toList ();
274+ return Card (
275+ child: Column (children: < Widget > [
276+ consumableHeader,
277+ Divider (),
278+ GridView .count (
279+ crossAxisCount: 5 ,
280+ children: tokens,
281+ shrinkWrap: true ,
282+ padding: EdgeInsets .all (16.0 ),
283+ )
284+ ]));
285+ }
286+
287+ Future <void > consume (String id) async {
288+ await ConsumableStore .consume (id);
289+ final List <String > consumables = await ConsumableStore .load ();
219290 setState (() {
220- _purchasePending = true ;
291+ _consumables = consumables ;
221292 });
222293 }
223294
224- void deliverProduct (PurchaseDetails purchaseDetails) {
225- // IMPORTANT!! Always verify a purchase purchase details before deliver the product.
295+ void showPendingUI () {
226296 setState (() {
227- _purchasePending = false ;
297+ _purchasePending = true ;
228298 });
229299 }
230300
301+ void deliverProduct (PurchaseDetails purchaseDetails) async {
302+ // IMPORTANT!! Always verify a purchase purchase details before delivering the product.
303+ if (purchaseDetails.productID == _kConsumableId) {
304+ await ConsumableStore .save (purchaseDetails.purchaseID);
305+ List <String > consumables = await ConsumableStore .load ();
306+ setState (() {
307+ _purchasePending = false ;
308+ _consumables = consumables;
309+ });
310+ } else {
311+ setState (() {
312+ _purchases.add (purchaseDetails);
313+ _purchasePending = false ;
314+ });
315+ }
316+ }
317+
231318 void handleError (PurchaseError error) {
232319 setState (() {
233320 _purchasePending = false ;
@@ -265,7 +352,7 @@ class _MyAppState extends State<MyApp> {
265352 if (Platform .isIOS) {
266353 InAppPurchaseConnection .instance.completePurchase (purchaseDetails);
267354 } else if (Platform .isAndroid) {
268- if (! kAutoConsume && purchaseDetails.productID == 'consumable' ) {
355+ if (! kAutoConsume && purchaseDetails.productID == _kConsumableId ) {
269356 InAppPurchaseConnection .instance.consumePurchase (purchaseDetails);
270357 }
271358 }
0 commit comments