mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-29 18:38:36 +08:00
Move PurchaseWidget to its own file
This commit is contained in:
@ -3,14 +3,9 @@ import 'dart:io';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:gitjournal/analytics.dart';
|
import 'package:gitjournal/analytics.dart';
|
||||||
import 'package:gitjournal/.env.dart';
|
import 'package:gitjournal/widgets/purchase_widget.dart';
|
||||||
import 'package:gitjournal/iap.dart';
|
|
||||||
import 'package:gitjournal/settings.dart';
|
|
||||||
|
|
||||||
import 'package:purchases_flutter/purchases_flutter.dart';
|
|
||||||
|
|
||||||
class PurchaseScreen extends StatelessWidget {
|
class PurchaseScreen extends StatelessWidget {
|
||||||
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
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 {
|
class EmptyAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
254
lib/widgets/purchase_widget.dart
Normal file
254
lib/widgets/purchase_widget.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user