Move PurchaseWidget to its own file

This commit is contained in:
Vishesh Handa
2020-07-09 12:25:30 +02:00
parent e5d4965844
commit 4109563293
2 changed files with 255 additions and 249 deletions

View File

@ -3,14 +3,9 @@ import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gitjournal/analytics.dart';
import 'package:gitjournal/.env.dart';
import 'package:gitjournal/iap.dart';
import 'package:gitjournal/settings.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:gitjournal/widgets/purchase_widget.dart';
class PurchaseScreen extends StatelessWidget {
final _scaffoldKey = GlobalKey<ScaffoldState>();
@ -118,122 +113,6 @@ class PurchaseScreen extends StatelessWidget {
}
}
class PurchaseButton extends StatelessWidget {
final Package package;
PurchaseButton(this.package);
@override
Widget build(BuildContext context) {
var price = package != null ? package.product.priceString : "Dev Mode";
return RaisedButton(
child: Text('Subscribe for $price / month'),
color: Theme.of(context).primaryColor,
padding: const EdgeInsets.fromLTRB(32.0, 16.0, 32.0, 16.0),
onPressed: package != null ? () => _handlePurchase(context) : null,
);
}
void _handlePurchase(BuildContext context) async {
try {
var purchaserInfo = await Purchases.purchasePackage(package);
var isPro = purchaserInfo.entitlements.all["pro"].isActive;
if (isPro) {
Settings.instance.proMode = true;
Settings.instance.proExpirationDate =
purchaserInfo.latestExpirationDate;
Settings.instance.save();
getAnalytics().logEvent(
name: "purchase_screen_thank_you",
);
Navigator.of(context).popAndPushNamed('/purchase_thank_you');
return;
}
} on PlatformException catch (e) {
var errorCode = PurchasesErrorHelper.getErrorCode(e);
var errorContent = "";
switch (errorCode) {
case PurchasesErrorCode.purchaseCancelledError:
errorContent = "User cancelled";
break;
case PurchasesErrorCode.purchaseNotAllowedError:
errorContent = "User not allowed to purchase";
break;
default:
errorContent = errorCode.toString();
break;
}
var dialog = AlertDialog(
title: const Text("Purchase Failed"),
content: Text(errorContent),
actions: <Widget>[
FlatButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
await showDialog(context: context, builder: (context) => dialog);
}
return null;
}
}
class LoadingWidget extends StatelessWidget {
const LoadingWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var children = <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Loading",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline4,
),
),
const SizedBox(height: 8.0),
const Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(
value: null,
),
),
];
var w = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
return WillPopScope(
onWillPop: _onWillPopLoading,
child: Container(
child: w,
color: theme.scaffoldBackgroundColor,
padding: const EdgeInsets.all(16.0),
constraints: const BoxConstraints.expand(),
),
);
}
Future<bool> _onWillPopLoading() async {
getAnalytics().logEvent(
name: "purchase_screen_close_loading",
);
return true;
}
}
class EmptyAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
@ -266,130 +145,3 @@ class _SingleChildScrollViewExpanded extends StatelessWidget {
);
}
}
class PurchaseWidget extends StatefulWidget {
@override
_PurchaseWidgetState createState() => _PurchaseWidgetState();
}
class _PurchaseWidgetState extends State<PurchaseWidget> {
List<Offering> _offerings;
Offering _selectedOffering;
var _scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
await InAppPurchases.confirmProPurchase();
if (Settings.instance.proMode) {
Navigator.of(context).pop();
}
await Purchases.setup(
environment['revenueCat'],
appUserId: Settings.instance.pseudoId,
);
Offerings offerings;
try {
offerings = await Purchases.getOfferings();
} catch (e) {
if (e is PlatformException) {
var snackBar = SnackBar(content: Text(e.message));
_scaffoldKey.currentState
..removeCurrentSnackBar()
..showSnackBar(snackBar);
return;
}
}
var offeringList = offerings.all.values.toList();
offeringList.retainWhere((Offering o) => o.identifier.contains("monthly"));
offeringList.sort((Offering a, Offering b) =>
a.monthly.product.price.compareTo(b.monthly.product.price));
print("Offerings: $offeringList");
// 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;
setState(() {
_offerings = offeringList;
_selectedOffering = _offerings.isNotEmpty ? _offerings.first : null;
if (_offerings.length > 1) {
_selectedOffering = _offerings[1];
} else {
var fakePackageJson = {
'identifier': 'monthly_fake',
'product': {
'identifier': 'fake_product',
'title': 'Fake Product',
'priceString': '0 Fake',
'price': 0.0,
},
};
var fakeOffer = Offering.fromJson(<String, dynamic>{
'identifier': 'monthly_fake_offering',
'monthly': fakePackageJson,
'availablePackages': [fakePackageJson],
});
_offerings = [fakeOffer];
_selectedOffering = _offerings[0];
}
});
}
@override
Widget build(BuildContext context) {
return _offerings == null ? const LoadingWidget() : buildBody(context);
}
Widget buildBody(BuildContext context) {
var slider = Slider(
min: _offerings.first.monthly.product.price,
max: _offerings.last.monthly.product.price + 0.50,
value: _selectedOffering.monthly.product.price,
onChanged: (double val) {
int i = -1;
for (i = 1; i < _offerings.length; i++) {
var prev = _offerings[i - 1].monthly.product;
var cur = _offerings[i].monthly.product;
if (prev.price < val && val <= cur.price) {
i--;
break;
}
}
if (val == _offerings.first.monthly.product.price) {
i = 0;
} else if (val >= _offerings.last.monthly.product.price) {
i = _offerings.length - 1;
}
if (i != -1) {
setState(() {
_selectedOffering = _offerings[i];
});
}
},
label: _selectedOffering.monthly.product.priceString,
divisions: _offerings.length,
);
return Column(
children: <Widget>[
slider,
const SizedBox(height: 16.0),
PurchaseButton(_selectedOffering?.monthly),
],
mainAxisAlignment: MainAxisAlignment.spaceAround,
);
}
}

View File

@ -0,0 +1,254 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gitjournal/analytics.dart';
import 'package:gitjournal/.env.dart';
import 'package:gitjournal/iap.dart';
import 'package:gitjournal/settings.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
class PurchaseButton extends StatelessWidget {
final Package package;
PurchaseButton(this.package);
@override
Widget build(BuildContext context) {
var price = package != null ? package.product.priceString : "Dev Mode";
return RaisedButton(
child: Text('Subscribe for $price / month'),
color: Theme.of(context).primaryColor,
padding: const EdgeInsets.fromLTRB(32.0, 16.0, 32.0, 16.0),
onPressed: package != null ? () => _handlePurchase(context) : null,
);
}
void _handlePurchase(BuildContext context) async {
try {
var purchaserInfo = await Purchases.purchasePackage(package);
var isPro = purchaserInfo.entitlements.all["pro"].isActive;
if (isPro) {
Settings.instance.proMode = true;
Settings.instance.proExpirationDate =
purchaserInfo.latestExpirationDate;
Settings.instance.save();
getAnalytics().logEvent(
name: "purchase_screen_thank_you",
);
Navigator.of(context).popAndPushNamed('/purchase_thank_you');
return;
}
} on PlatformException catch (e) {
var errorCode = PurchasesErrorHelper.getErrorCode(e);
var errorContent = "";
switch (errorCode) {
case PurchasesErrorCode.purchaseCancelledError:
errorContent = "User cancelled";
break;
case PurchasesErrorCode.purchaseNotAllowedError:
errorContent = "User not allowed to purchase";
break;
default:
errorContent = errorCode.toString();
break;
}
var dialog = AlertDialog(
title: const Text("Purchase Failed"),
content: Text(errorContent),
actions: <Widget>[
FlatButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
await showDialog(context: context, builder: (context) => dialog);
}
return null;
}
}
class LoadingWidget extends StatelessWidget {
const LoadingWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var children = <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Loading",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline4,
),
),
const SizedBox(height: 8.0),
const Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(
value: null,
),
),
];
var w = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
return WillPopScope(
onWillPop: _onWillPopLoading,
child: Container(
child: w,
color: theme.scaffoldBackgroundColor,
padding: const EdgeInsets.all(16.0),
constraints: const BoxConstraints.expand(),
),
);
}
Future<bool> _onWillPopLoading() async {
getAnalytics().logEvent(
name: "purchase_screen_close_loading",
);
return true;
}
}
class PurchaseWidget extends StatefulWidget {
@override
_PurchaseWidgetState createState() => _PurchaseWidgetState();
}
class _PurchaseWidgetState extends State<PurchaseWidget> {
List<Offering> _offerings;
Offering _selectedOffering;
var _scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
await InAppPurchases.confirmProPurchase();
if (Settings.instance.proMode) {
Navigator.of(context).pop();
}
await Purchases.setup(
environment['revenueCat'],
appUserId: Settings.instance.pseudoId,
);
Offerings offerings;
try {
offerings = await Purchases.getOfferings();
} catch (e) {
if (e is PlatformException) {
var snackBar = SnackBar(content: Text(e.message));
_scaffoldKey.currentState
..removeCurrentSnackBar()
..showSnackBar(snackBar);
return;
}
}
var offeringList = offerings.all.values.toList();
offeringList.retainWhere((Offering o) => o.identifier.contains("monthly"));
offeringList.sort((Offering a, Offering b) =>
a.monthly.product.price.compareTo(b.monthly.product.price));
print("Offerings: $offeringList");
// 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;
setState(() {
_offerings = offeringList;
_selectedOffering = _offerings.isNotEmpty ? _offerings.first : null;
if (_offerings.length > 1) {
_selectedOffering = _offerings[1];
} else {
var fakePackageJson = {
'identifier': 'monthly_fake',
'product': {
'identifier': 'fake_product',
'title': 'Fake Product',
'priceString': '0 Fake',
'price': 0.0,
},
};
var fakeOffer = Offering.fromJson(<String, dynamic>{
'identifier': 'monthly_fake_offering',
'monthly': fakePackageJson,
'availablePackages': [fakePackageJson],
});
_offerings = [fakeOffer];
_selectedOffering = _offerings[0];
}
});
}
@override
Widget build(BuildContext context) {
return _offerings == null ? const LoadingWidget() : buildBody(context);
}
Widget buildBody(BuildContext context) {
var slider = Slider(
min: _offerings.first.monthly.product.price,
max: _offerings.last.monthly.product.price + 0.50,
value: _selectedOffering.monthly.product.price,
onChanged: (double val) {
int i = -1;
for (i = 1; i < _offerings.length; i++) {
var prev = _offerings[i - 1].monthly.product;
var cur = _offerings[i].monthly.product;
if (prev.price < val && val <= cur.price) {
i--;
break;
}
}
if (val == _offerings.first.monthly.product.price) {
i = 0;
} else if (val >= _offerings.last.monthly.product.price) {
i = _offerings.length - 1;
}
if (i != -1) {
setState(() {
_selectedOffering = _offerings[i];
});
}
},
label: _selectedOffering.monthly.product.priceString,
divisions: _offerings.length,
);
return Column(
children: <Widget>[
slider,
const SizedBox(height: 16.0),
PurchaseButton(_selectedOffering?.monthly),
],
mainAxisAlignment: MainAxisAlignment.spaceAround,
);
}
}