diff --git a/lib/purchase_manager.dart b/lib/purchase_manager.dart index d3fb9217..a7fee1a8 100644 --- a/lib/purchase_manager.dart +++ b/lib/purchase_manager.dart @@ -7,21 +7,18 @@ import 'package:gitjournal/app_settings.dart'; import 'package:gitjournal/error_reporting.dart'; import 'package:gitjournal/iap.dart'; import 'package:gitjournal/utils/logger.dart'; - -enum PurchaseError { - StoreCannotBeReached, -} +import 'package:gitjournal/widgets/purchase_slider.dart'; // ignore_for_file: cancel_subscriptions -typedef PurchaseCallback = void Function(PurchaseError, SubscriptionStatus); +typedef PurchaseCallback = void Function(String, SubscriptionStatus); class PurchaseManager { InAppPurchaseConnection con; StreamSubscription> _subscription; List _callbacks = []; - static PurchaseError error; + static String error; static PurchaseManager _instance; static Future init() async { @@ -36,7 +33,7 @@ class PurchaseManager { final bool available = await _instance.con.isAvailable(); if (!available) { - error = PurchaseError.StoreCannotBeReached; + error = "Store cannot be reached"; _instance = null; return null; } @@ -100,10 +97,17 @@ class PurchaseManager { void _handleIAPError(IAPError err) { var msg = "${err.code} - ${err.message} - ${err.details}"; Log.e(msg); + + _handleError(msg); } void _handleError(String err) { Log.e(err); + + Log.i("Calling Purchase Error Callbacks: ${_callbacks.length}"); + for (var callback in _callbacks) { + callback(err, null); + } } void _deliverProduct(SubscriptionStatus status) { @@ -112,14 +116,22 @@ class PurchaseManager { appSettings.proExpirationDate = status.expiryDate.toIso8601String(); appSettings.save(); + Log.i("Calling Purchase Completed Callbacks: ${_callbacks.length}"); for (var callback in _callbacks) { - callback(null, status); + callback("", status); } } + /// Returns the ProductDetails sorted by price Future queryProductDetails(Set skus) async { // Cache this response? - final response = await _instance.con.queryProductDetails(skus); + var response = await _instance.con.queryProductDetails(skus); + response.productDetails.sort((a, b) { + var pa = PaymentInfo.fromProductDetail(a); + var pb = PaymentInfo.fromProductDetail(b); + return pa.value.compareTo(pb.value); + }); + return response; } diff --git a/lib/screens/purchase_screen.dart b/lib/screens/purchase_screen.dart index 3ccbe7fb..e9a69de4 100644 --- a/lib/screens/purchase_screen.dart +++ b/lib/screens/purchase_screen.dart @@ -8,7 +8,6 @@ import 'package:gitjournal/analytics.dart'; import 'package:gitjournal/purchase_manager.dart'; import 'package:gitjournal/screens/feature_timeline_screen.dart'; import 'package:gitjournal/utils/logger.dart'; -import 'package:gitjournal/widgets/purchase_slider.dart'; import 'package:gitjournal/widgets/purchase_widget.dart'; import 'package:gitjournal/widgets/scroll_view_without_animation.dart'; @@ -54,18 +53,10 @@ class _PurchaseScreenState extends State { } if (!mounted) return; - - var products = response.productDetails; - products.sort((a, b) { - var pa = PaymentInfo.fromProductDetail(a); - var pb = PaymentInfo.fromProductDetail(b); - return pa.value.compareTo(pb.value); - }); - - if (products.isEmpty) return; + if (response.productDetails.isEmpty) return; setState(() { - minYearlyPurchase = products.first.price; + minYearlyPurchase = response.productDetails.first.price; }); } diff --git a/lib/widgets/purchase_widget.dart b/lib/widgets/purchase_widget.dart index 3ae506ad..f1249ed0 100644 --- a/lib/widgets/purchase_widget.dart +++ b/lib/widgets/purchase_widget.dart @@ -3,13 +3,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:function_types/function_types.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; -import 'package:provider/provider.dart'; import 'package:gitjournal/analytics.dart'; import 'package:gitjournal/app_settings.dart'; import 'package:gitjournal/error_reporting.dart'; import 'package:gitjournal/iap.dart'; +import 'package:gitjournal/purchase_manager.dart'; import 'package:gitjournal/utils/logger.dart'; import 'package:gitjournal/widgets/purchase_slider.dart'; @@ -17,8 +18,16 @@ class PurchaseButton extends StatelessWidget { final ProductDetails product; final String timePeriod; final bool subscription; + final Func1 purchaseStarted; + final PurchaseCallback purchaseCompleted; - PurchaseButton(this.product, this.timePeriod, {@required this.subscription}); + PurchaseButton( + this.product, + this.timePeriod, { + @required this.subscription, + @required this.purchaseStarted, + @required this.purchaseCompleted, + }); @override Widget build(BuildContext context) { @@ -43,14 +52,22 @@ class PurchaseButton extends StatelessWidget { } Future _initPurchase(BuildContext context) async { - var purchaseParam = PurchaseParam(productDetails: product); - var sentSuccess = await InAppPurchaseConnection.instance - .buyNonConsumable(purchaseParam: purchaseParam); + var pm = await PurchaseManager.init(); + if (pm == null) { + purchaseCompleted(PurchaseManager.error, null); + return; + } + var sentSuccess = await pm.buyNonConsumable(product, purchaseCompleted); + purchaseStarted(sentSuccess); + + /* if (!sentSuccess) { var dialog = PurchaseFailedDialog(tr("widgets.PurchaseButton.failSend")); await showDialog(context: context, builder: (context) => dialog); + return; } + */ } void _reportExceptions(BuildContext context) async { @@ -89,10 +106,9 @@ class PurchaseWidget extends StatefulWidget { class _PurchaseWidgetState extends State { List _products; ProductDetails _selectedProduct; - StreamSubscription> _subscription; String error = ""; - bool pendingPurchase = false; + bool _pendingPurchase = false; @override void initState() { @@ -101,33 +117,21 @@ class _PurchaseWidgetState extends State { } Future initPlatformState() async { - InAppPurchaseConnection.enablePendingPurchases(); - final iapCon = InAppPurchaseConnection.instance; - - final bool available = await iapCon.isAvailable(); - if (!available) { + var pm = await PurchaseManager.init(); + if (pm == null) { setState(() { - error = "Store cannot be reached"; + error = PurchaseManager.error; }); - return; } - final response = await iapCon.queryProductDetails(widget.skus); + final response = await pm.queryProductDetails(widget.skus); if (response.error != null) { Log.e("IAP queryProductDetails: ${response.error}"); } - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. if (!mounted) return; var products = response.productDetails; - products.sort((a, b) { - var pa = PaymentInfo.fromProductDetail(a); - var pb = PaymentInfo.fromProductDetail(b); - return pa.value.compareTo(pb.value); - }); Log.i("Products: ${products.length}"); for (var p in products) { Log.i("Product ${p.id} -> ${p.price}"); @@ -148,94 +152,6 @@ class _PurchaseWidgetState extends State { // FIXME: Add a fake product for development } }); - - // Start listening for changes - final purchaseUpdates = iapCon.purchaseUpdatedStream; - _subscription = purchaseUpdates.listen(_listenToPurchaseUpdated); - } - - void _listenToPurchaseUpdated( - List purchaseDetailsList) async { - for (var pd in purchaseDetailsList) { - await _handlePurchaseUpdate(pd); - } - } - - Future _handlePurchaseUpdate(PurchaseDetails purchaseDetails) async { - Log.i( - "PurchaseDetailsUpdated: {productID: ${purchaseDetails.productID}, purchaseID: ${purchaseDetails.purchaseID}, status: ${purchaseDetails.status}"); - - if (purchaseDetails.status == PurchaseStatus.pending) { - //showPendingUI(); - Log.i("Pending - ${purchaseDetails.productID}"); - if (mounted) { - setState(() { - pendingPurchase = true; - }); - } - return; - } - - setState(() { - pendingPurchase = false; - }); - if (purchaseDetails.status == PurchaseStatus.error) { - _handleIAPError(purchaseDetails.error); - return; - } else if (purchaseDetails.status == PurchaseStatus.purchased) { - Log.i("Verifying purchase sub"); - try { - var subStatus = await verifyPurchase(purchaseDetails); - if (subStatus.isPro) { - _deliverProduct(subStatus); - } else { - _handleError(tr('widgets.PurchaseWidget.failed')); - return; - } - } catch (err) { - _handleError(err.toString()); - } - } - if (purchaseDetails.pendingCompletePurchase) { - Log.i("Pending Complete Purchase - ${purchaseDetails.productID}"); - - try { - await InAppPurchaseConnection.instance - .completePurchase(purchaseDetails); - } catch (e, stackTrace) { - logException(e, stackTrace); - } - } - } - - void _handleIAPError(IAPError err) { - var msg = "${err.code} - ${err.message} - ${err.details}"; - _handleError(msg); - } - - void _handleError(String err) { - if (err.toLowerCase().contains("usercanceled")) { - Log.e(err); - return; - } - var dialog = PurchaseFailedDialog(err); - showDialog(context: context, builder: (context) => dialog); - } - - void _deliverProduct(SubscriptionStatus status) { - var appSettings = Provider.of(context, listen: false); - appSettings.proMode = status.isPro; - appSettings.proExpirationDate = status.expiryDate.toIso8601String(); - appSettings.save(); - - logEvent(Event.PurchaseScreenThankYou); - Navigator.of(context).popAndPushNamed('/purchase_thank_you'); - } - - @override - void dispose() { - _subscription?.cancel(); - super.dispose(); } @override @@ -243,7 +159,7 @@ class _PurchaseWidgetState extends State { if (error.isNotEmpty) { return Text("Failed to load: $error"); } - if (pendingPurchase) { + if (_pendingPurchase) { return const CircularProgressIndicator(); } return _products == null @@ -301,6 +217,12 @@ class _PurchaseWidgetState extends State { _selectedProduct, widget.timePeriod, subscription: widget.isSubscription, + purchaseStarted: (bool started) { + setState(() { + _pendingPurchase = started; + }); + }, + purchaseCompleted: _purchaseCompleted, ), ], mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -326,6 +248,27 @@ class _PurchaseWidgetState extends State { return null; } + + void _purchaseCompleted(String err, SubscriptionStatus subStatus) { + if (!mounted) return; + + if (err.isEmpty) { + Log.i("Purchase Completed: $subStatus"); + logEvent(Event.PurchaseScreenThankYou); + Navigator.of(context).popAndPushNamed('/purchase_thank_you'); + return; + } + + if (err.toLowerCase().contains("usercanceled")) { + setState(() { + _pendingPurchase = false; + }); + Log.e(err); + return; + } + var dialog = PurchaseFailedDialog(err); + showDialog(context: context, builder: (context) => dialog); + } } class _PurchaseSliderButton extends StatelessWidget {