Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.1.0+1

Add more consumable handling to the example app.

## 0.1.0

Beta relase.
Expand Down
38 changes: 38 additions & 0 deletions packages/in_app_purchase/example/lib/consumable_store.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';

// This is just a development prototype for locally storing consumables. Do not
// use this.
class ConsumableStore {
static const String _kPrefKey = 'consumables';
static Future<void> _writes = Future.value();

static Future<void> save(String id) {
_writes = _writes.then((void _) => _doSave(id));
return _writes;
}

static Future<void> consume(String id) {
_writes = _writes.then((void _) => _doConsume(id));
return _writes;
}

static Future<List<String>> load() async {
return (await SharedPreferences.getInstance()).getStringList(_kPrefKey) ??
[];
}

static Future<void> _doSave(String id) async {
List<String> cached = await load();
SharedPreferences prefs = await SharedPreferences.getInstance();
cached.add(id);
await prefs.setStringList(_kPrefKey, cached);
}

static Future<void> _doConsume(String id) async {
List<String> cached = await load();
SharedPreferences prefs = await SharedPreferences.getInstance();
cached.remove(id);
await prefs.setStringList(_kPrefKey, cached);
}
}
219 changes: 153 additions & 66 deletions packages/in_app_purchase/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ 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() {
runApp(MyApp());
}

// Switch this to true if you want to try out auto consume when buying a consumable.
const bool kAutoConsume = false;
const bool kAutoConsume = true;

const String _kConsumableId = 'consumable';
const List<String> _kProductIds = <String>[
'consumable',
_kConsumableId,
'upgrade',
'subscription'
];
Expand All @@ -26,9 +27,16 @@ class MyApp extends StatefulWidget {
}

class _MyAppState extends State<MyApp> {
final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance;
StreamSubscription<List<PurchaseDetails>> _subscription;

List<String> _notFoundIds = [];
List<ProductDetails> _products = [];
List<PurchaseDetails> _purchases = [];
List<String> _consumables = [];
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;

@override
void initState() {
Stream purchaseUpdated =
Expand All @@ -40,9 +48,60 @@ class _MyAppState extends State<MyApp> {
}, onError: (error) {
// handle error here.
});
initStoreInfo();
super.initState();
}

Future<void> initStoreInfo() async {
final bool isAvailable = await _connection.isAvailable();
if (!isAvailable) {
setState(() {
_isAvailable = isAvailable;
_products = [];
_purchases = [];
_notFoundIds = [];
_consumables = [];
_purchasePending = false;
_loading = false;
});
return;
}

ProductDetailsResponse productDetails =
await _connection.queryProductDetails(_kProductIds.toSet());
if (productDetails.productDetails.isEmpty) {
setState(() {
_isAvailable = isAvailable;
_products = productDetails.productDetails;
_purchases = [];
_notFoundIds = productDetails.notFoundIDs;
_consumables = [];
_purchasePending = false;
_loading = false;
});
return;
}

final QueryPurchaseDetailsResponse purchaseResponse =
await _connection.queryPastPurchases();
final List<PurchaseDetails> verifiedPurchases = [];
for (PurchaseDetails purchase in purchaseResponse.pastPurchases) {
if (await _verifyPurchase(purchase)) {
verifiedPurchases.add(purchase);
}
}
List<String> consumables = await ConsumableStore.load();
setState(() {
_isAvailable = isAvailable;
_products = productDetails.productDetails;
_purchases = verifiedPurchases;
_notFoundIds = productDetails.notFoundIDs;
_consumables = consumables;
_purchasePending = false;
_loading = false;
});
}

@override
void dispose() {
_subscription.cancel();
Expand All @@ -55,38 +114,9 @@ class _MyAppState extends State<MyApp> {
stack.add(
ListView(
children: [
FutureBuilder(
future: _buildConnectionCheckTile(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.error != null) {
return buildListCard(ListTile(
title: Text(
'Error connecting: ' + snapshot.error.toString())));
} else if (!snapshot.hasData) {
return Card(
child: ListTile(title: const Text('Trying to connect...')));
}
return snapshot.data;
},
),
FutureBuilder(
future: _buildProductList(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.error != null) {
return Center(
child: buildListCard(ListTile(
title:
Text('Error fetching products ${snapshot.error}'))),
);
} else if (!snapshot.hasData) {
return Card(
child: (ListTile(
leading: CircularProgressIndicator(),
title: Text('Fetching products...'))));
}
return snapshot.data;
},
),
_buildConnectionCheckTile(),
_buildProductList(),
_buildConsumableBox(),
],
),
);
Expand Down Expand Up @@ -118,17 +148,19 @@ class _MyAppState extends State<MyApp> {
);
}

Future<Card> _buildConnectionCheckTile() async {
final bool available = await InAppPurchaseConnection.instance.isAvailable();
Card _buildConnectionCheckTile() {
if (_loading) {
return Card(child: ListTile(title: const Text('Trying to connect...')));
}
final Widget storeHeader = ListTile(
leading: Icon(available ? Icons.check : Icons.block,
color: available ? Colors.green : ThemeData.light().errorColor),
leading: Icon(_isAvailable ? Icons.check : Icons.block,
color: _isAvailable ? Colors.green : ThemeData.light().errorColor),
title: Text(
'The store is ' + (available ? 'available' : 'unavailable') + '.'),
'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'),
);
final List<Widget> children = <Widget>[storeHeader];

if (!available) {
if (!_isAvailable) {
children.addAll([
Divider(),
ListTile(
Expand All @@ -142,21 +174,23 @@ class _MyAppState extends State<MyApp> {
return Card(child: Column(children: children));
}

Future<Card> _buildProductList() async {
InAppPurchaseConnection connection = InAppPurchaseConnection.instance;
final bool available = await connection.isAvailable();
if (!available) {
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',
style: Theme.of(context).textTheme.headline));
ProductDetailsResponse response =
await connection.queryProductDetails(_kProductIds.toSet());
List<ListTile> productList = <ListTile>[];
if (!response.notFoundIDs.isEmpty) {
if (!_notFoundIds.isEmpty) {
productList.add(ListTile(
title: Text('[${response.notFoundIDs.join(", ")}] not found',
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.')));
Expand All @@ -165,18 +199,14 @@ class _MyAppState extends State<MyApp> {
// 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 verity the purchase data.
Map<String, PurchaseDetails> purchases = Map.fromEntries(
((await connection.queryPastPurchases()).pastPurchases)
.map((PurchaseDetails purchase) {
Map<String, PurchaseDetails> purchases =
Map.fromEntries(_purchases.map((PurchaseDetails purchase) {
if (Platform.isIOS) {
InAppPurchaseConnection.instance.completePurchase(purchase);
}
if (Platform.isAndroid && purchase.productID == 'consumable') {
InAppPurchaseConnection.instance.consumePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
}));
productList.addAll(response.productDetails.map(
productList.addAll(_products.map(
(ProductDetails productDetails) {
PurchaseDetails previousPurchase = purchases[productDetails.id];
return ListTile(
Expand All @@ -197,12 +227,12 @@ class _MyAppState extends State<MyApp> {
productDetails: productDetails,
applicationUserName: null,
sandboxTesting: true);
if (productDetails.id == 'consumable') {
connection.buyConsumable(
if (productDetails.id == _kConsumableId) {
_connection.buyConsumable(
purchaseParam: purchaseParam,
autoConsume: kAutoConsume || Platform.isIOS);
} else {
connection.buyNonConsumable(
_connection.buyNonConsumable(
purchaseParam: purchaseParam);
}
},
Expand All @@ -215,19 +245,76 @@ class _MyAppState extends State<MyApp> {
Column(children: <Widget>[productHeader, Divider()] + productList));
}

void showPendingUI() {
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',
style: Theme.of(context).textTheme.headline));
final List<Widget> 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: <Widget>[
consumableHeader,
Divider(),
GridView.count(
crossAxisCount: 5,
children: tokens,
shrinkWrap: true,
padding: EdgeInsets.all(16.0),
)
]));
}

Future<void> consume(String id) async {
await ConsumableStore.consume(id);
final List<String> consumables = await ConsumableStore.load();
setState(() {
_purchasePending = true;
_consumables = consumables;
});
}

void deliverProduct(PurchaseDetails purchaseDetails) {
// IMPORTANT!! Always verify a purchase purchase details before deliver the product.
void showPendingUI() {
setState(() {
_purchasePending = false;
_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<String> consumables = await ConsumableStore.load();
setState(() {
_purchasePending = false;
_consumables = consumables;
});
} else {
setState(() {
_purchases.add(purchaseDetails);
_purchasePending = false;
});
}
}

void handleError(PurchaseError error) {
setState(() {
_purchasePending = false;
Expand Down Expand Up @@ -265,7 +352,7 @@ class _MyAppState extends State<MyApp> {
if (Platform.isIOS) {
InAppPurchaseConnection.instance.completePurchase(purchaseDetails);
} else if (Platform.isAndroid) {
if (!kAutoConsume && purchaseDetails.productID == 'consumable') {
if (!kAutoConsume && purchaseDetails.productID == _kConsumableId) {
InAppPurchaseConnection.instance.consumePurchase(purchaseDetails);
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/in_app_purchase/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
shared_preferences: ^0.5.2

dev_dependencies:
test: ^1.5.2
Expand Down
Loading