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<String, PurchaseDetails> purchases =
Map.fromEntries(_purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
InAppPurchaseConnection.instance.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(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)
: FlatButton(
child: Text(productDetails.price),
color: Colors.green[800],
textColor: 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: <Widget>[productHeader, Divider()] + productList));
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.
}
import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:globens_flutter_client/utils/utils.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'consumable store.dart';
const bool _kAutoConsume = true;
const String _kConsumableId = 'consumable'; const List _kProductIds = ['globens_2020', 'globens_subscription'];
class ProductList extends StatefulWidget { @override _ProductListState createState() => _ProductListState(); }
class _ProductListState extends State {
final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance;
StreamSubscription<List> _subscription;
List _notFoundIds = [];
List _products = [];
List _purchases = [];
List _consumables = [];
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;
List stack = [];
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;
}
}
@override void dispose() { _subscription.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { 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 Scaffold( appBar: AppBar( title: Text("Products"), leading: backButton(_onBackButtonPressed, context), ), body: SafeArea( child: 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];
}
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.')));
}
}
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);
}
}
});
}
void _onBackButtonPressed(BuildContext context) { Navigator.of(context).pop(); } }