Support buying or updating stripe subscription

This commit is contained in:
Neeraj Gupta 2021-08-18 18:13:28 +05:30
parent 647ab51b3c
commit 13f19236fd
2 changed files with 247 additions and 7 deletions

View file

@ -0,0 +1,177 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:logging/logging.dart';
import 'package:photos/services/billing_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
import '../progress_dialog.dart';
const kMobilePaymentRedirect = "ente://payment/";
class PaymentWebPage extends StatefulWidget {
final String planId;
final String actionType;
const PaymentWebPage({Key key, this.planId, this.actionType})
: super(key: key);
@override
State<StatefulWidget> createState() => _PaymentWebPage();
}
class _PaymentWebPage extends State<PaymentWebPage> {
final _logger = Logger("PaymentWebPage");
UserService userService = UserService.instance;
BillingService billingService = BillingService.instance;
ProgressDialog _dialog;
InAppWebViewController webView;
double progress = 0;
String paymentWebToken;
String basePaymentUrl = "http://192.168.1.123:3001";
@override
void initState() {
userService.getPaymentToken().then((token) {
paymentWebToken = token;
setState(() {});
});
if (Platform.isAndroid && kDebugMode) {
AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
}
_dialog = createProgressDialog(context, "please wait...");
super.initState();
}
Uri _getPaymentUrl(String baseEndpoint, String productId, String paymentToken,
String actionType, String redirectUrl) {
final queryParameters = {
'productID': productId,
'paymentToken': paymentToken,
'action': actionType,
'redirectURL': redirectUrl,
};
var tryParse = Uri.tryParse(baseEndpoint);
if (kDebugMode && baseEndpoint.startsWith("http://")) {
return Uri.http(tryParse.authority, tryParse.path, queryParameters);
} else {
return Uri.https(tryParse.authority, tryParse.path, queryParameters);
}
}
@override
Widget build(BuildContext context) {
if (paymentWebToken == null) {
return Container();
}
Uri paymentUri = _getPaymentUrl(basePaymentUrl, widget.planId,
paymentWebToken, widget.actionType, kMobilePaymentRedirect);
_logger.info("paymentUrl : $paymentUri");
return WillPopScope(
onWillPop: () async => showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Are you sure you want to exit?'),
actions: <Widget>[
TextButton(
child: Text('yes'),
onPressed: () => Navigator.of(context).pop(true)),
TextButton(
child: Text('no'),
onPressed: () => Navigator.of(context).pop(false)),
])),
child: Scaffold(
appBar: AppBar(
title: const Text('ente payment'),
),
body: Column(
children: <Widget>[
(progress != 1.0)
? LinearProgressIndicator(value: progress)
: Container(),
Expanded(
child: InAppWebView(
initialUrlRequest: URLRequest(url: paymentUri),
onProgressChanged:
(InAppWebViewController controller, int progress) {
setState(() {
this.progress = progress / 100;
});
},
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
),
),
shouldOverrideUrlLoading:
(controller, navigationAction) async {
var loadingUri = navigationAction.request.url;
_logger.info("Loading url $loadingUri");
if (isPaymentActionComplete(loadingUri)) {
// handle the payment response
await handlePaymentResponse(loadingUri);
// and cancel the request
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
onConsoleMessage: (controller, consoleMessage) {
_logger.info(consoleMessage);
},
onLoadStart: (controller, navigationAction) async {
if (!_dialog.isShowing()) {
await _dialog.show();
}
},
onLoadStop: (controller, navigationAction) async {
if (_dialog.isShowing()) {
await _dialog.hide();
}
},
),
),
].where((Object o) => o != null).toList(),
),
));
}
@override
void dispose() {
super.dispose();
}
bool isPaymentActionComplete(Uri loadingUri) {
return loadingUri.toString().startsWith(kMobilePaymentRedirect);
}
Future<void> handlePaymentResponse(Uri uri) async {
var queryParams = uri.queryParameters;
_logger.info(queryParams);
// success or fail
var paymentStatus = queryParams['status'] ?? '';
var reason = queryParams['reason'] ?? '';
if ('fail' == paymentStatus) {
showToast("sorry, we couldn't process your payment due to $reason");
} else if (paymentStatus == 'success') {
// sessionID can be null in case of update.
var checkoutSessionID = queryParams['session_id'] ?? '';
await _dialog.show();
_logger.info("Receiving checkoutSession ID: $checkoutSessionID");
await billingService
.verifySubscription(widget.planId, checkoutSessionID,
paymentProvider: "stripe")
.then((value) {
showToast("thank you for subscribing to ente!");
});
if (_dialog.isShowing()) {
await _dialog.hide();
}
Navigator.of(context).pop(true);
}
}
}

View file

@ -11,9 +11,10 @@ import 'package:photos/events/subscription_purchased_event.dart';
import 'package:photos/models/billing_plan.dart';
import 'package:photos/models/subscription.dart';
import 'package:photos/services/billing_service.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/ui/billing_questions_widget.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/ui/payment/payment_web_page.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
import 'package:photos/ui/payment/subscription_plan_widget.dart';
import 'package:photos/ui/progress_dialog.dart';
@ -47,10 +48,13 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
List<BillingPlan> _plans;
bool _hasLoadedData = false;
bool _isActiveStripeSubscriber;
// based on this flag, we would show ente payment page with stripe plans
bool _isIndependentApk;
@override
void initState() {
_billingService.setIsOnSubscriptionPage(true);
_isIndependentApk = UpdateService.instance.isIndependentFlavor();
_billingService.fetchSubscription().then((subscription) async {
_currentSubscription = subscription;
_hasActiveSubscription = _currentSubscription.isValid();
@ -59,7 +63,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
_currentSubscription.paymentProvider == kStripe &&
_currentSubscription.isValid();
_plans = billingPlans.plans.where((plan) {
final productID = _isActiveStripeSubscriber
final productID = (_showStripePlans())
? plan.stripeID
: Platform.isAndroid
? plan.androidID
@ -67,16 +71,20 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
return productID != null && productID.isNotEmpty;
}).toList();
_freePlan = billingPlans.freePlan;
_usageFuture = _billingService.fetchUsage();
_hasLoadedData = true;
setState(() {});
setState(() {
});
});
_setupPurchaseUpdateStreamListener();
_dialog = createProgressDialog(context, "please wait...");
super.initState();
}
bool _showStripePlans() {
return _isActiveStripeSubscriber || _isIndependentApk;
}
void _setupPurchaseUpdateStreamListener() {
_purchaseUpdateSubscription = InAppPurchaseConnection
.instance.purchaseUpdatedStream
@ -201,7 +209,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
widgets.addAll([
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _isActiveStripeSubscriber
children: _showStripePlans()
? _getStripePlanWidgets()
: _getMobilePlanWidgets(),
),
@ -331,8 +339,63 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
if (isActive) {
return;
}
showErrorDialog(context, "sorry",
"please visit web.ente.io to manage your subscription");
await _dialog.show();
if (_usageFuture != null) {
final usage = await _usageFuture;
await _dialog.hide();
if (usage > plan.storage) {
showErrorDialog(
context, "sorry", "you cannot downgrade to this plan");
return;
}
}
if (_isActiveStripeSubscriber && !_isIndependentApk) {
showErrorDialog(context, "sorry",
"please visit web.ente.io to manage your subscription");
return;
}
if (_isActiveStripeSubscriber) {
// check if user really wants to change his plan plan
showDialog(context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'do you want really to change your subscription plan?'),
actions: <Widget>[
TextButton(
child: Text('yes'),
onPressed: () {
Navigator.of(context).pop('dialog');
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return PaymentWebPage(
planId: plan.stripeID,
actionType: "update");
},
),
); }
),
TextButton(
child: Text('no'),
onPressed: () => {
Navigator.of(context,
rootNavigator: true)
.pop('dialog')
}),
]);
});
} else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return PaymentWebPage(
planId: plan.stripeID, actionType: "buy");
},
),
);
}
},
child: SubscriptionPlanWidget(
storage: plan.storage,