Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit c19d60a

Browse files
author
Michael Klimushyn
authored
[in_app_purchase] Add consumable demo (#1577)
Also changes how the example app handles state to avoid bugs with consumables not getting updated based on purchasing. Previously the app was relying on FutureBuilders, but these didn't handle state updates over time and across Widgets correctly.
1 parent 1a4ed62 commit c19d60a

File tree

5 files changed

+198
-67
lines changed

5 files changed

+198
-67
lines changed

packages/in_app_purchase/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.1.0+1
2+
3+
Add more consumable handling to the example app.
4+
15
## 0.1.0
26

37
Beta relase.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'dart:async';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
4+
// This is just a development prototype for locally storing consumables. Do not
5+
// use this.
6+
class ConsumableStore {
7+
static const String _kPrefKey = 'consumables';
8+
static Future<void> _writes = Future.value();
9+
10+
static Future<void> save(String id) {
11+
_writes = _writes.then((void _) => _doSave(id));
12+
return _writes;
13+
}
14+
15+
static Future<void> consume(String id) {
16+
_writes = _writes.then((void _) => _doConsume(id));
17+
return _writes;
18+
}
19+
20+
static Future<List<String>> load() async {
21+
return (await SharedPreferences.getInstance()).getStringList(_kPrefKey) ??
22+
[];
23+
}
24+
25+
static Future<void> _doSave(String id) async {
26+
List<String> cached = await load();
27+
SharedPreferences prefs = await SharedPreferences.getInstance();
28+
cached.add(id);
29+
await prefs.setStringList(_kPrefKey, cached);
30+
}
31+
32+
static Future<void> _doConsume(String id) async {
33+
List<String> cached = await load();
34+
SharedPreferences prefs = await SharedPreferences.getInstance();
35+
cached.remove(id);
36+
await prefs.setStringList(_kPrefKey, cached);
37+
}
38+
}

packages/in_app_purchase/example/lib/main.dart

Lines changed: 153 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ import 'dart:async';
66
import 'dart:io';
77
import 'package:flutter/material.dart';
88
import 'package:in_app_purchase/in_app_purchase.dart';
9+
import 'consumable_store.dart';
910

1011
void 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';
1718
const List<String> _kProductIds = <String>[
18-
'consumable',
19+
_kConsumableId,
1920
'upgrade',
2021
'subscription'
2122
];
@@ -26,9 +27,16 @@ class MyApp extends StatefulWidget {
2627
}
2728

2829
class _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
}

packages/in_app_purchase/example/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ dependencies:
66
flutter:
77
sdk: flutter
88
cupertino_icons: ^0.1.2
9+
shared_preferences: ^0.5.2
910

1011
dev_dependencies:
1112
test: ^1.5.2

0 commit comments

Comments
 (0)