ente/lib/ui/payment/payment_web_page.dart

270 lines
8.8 KiB
Dart
Raw Normal View History

import 'dart:io';
import 'package:collection/collection.dart' show IterableNullableExtension;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:logging/logging.dart';
2022-07-12 06:30:02 +00:00
import 'package:photos/ente_theme_data.dart';
2023-04-06 08:39:34 +00:00
import "package:photos/generated/l10n.dart";
2021-08-18 19:43:35 +00:00
import 'package:photos/models/subscription.dart';
import 'package:photos/services/billing_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/utils/dialog_util.dart';
class PaymentWebPage extends StatefulWidget {
final String? planId;
final String? actionType;
const PaymentWebPage({Key? key, this.planId, this.actionType})
2022-07-03 09:49:33 +00:00
: super(key: key);
@override
2021-08-23 10:15:45 +00:00
State<StatefulWidget> createState() => _PaymentWebPageState();
}
2021-08-23 10:15:45 +00:00
class _PaymentWebPageState extends State<PaymentWebPage> {
final _logger = Logger("PaymentWebPageState");
final UserService userService = UserService.instance;
final BillingService billingService = BillingService.instance;
final String basePaymentUrl = kWebPaymentBaseEndpoint;
late ProgressDialog _dialog;
InAppWebViewController? webView;
double progress = 0;
Uri? initPaymentUrl;
@override
void initState() {
userService.getPaymentToken().then((token) {
initPaymentUrl = _getPaymentUrl(token);
2021-08-23 10:24:56 +00:00
setState(() {});
});
if (Platform.isAndroid && kDebugMode) {
AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
}
super.initState();
}
@override
Widget build(BuildContext context) {
2023-04-06 08:39:34 +00:00
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
if (initPaymentUrl == null) {
return const EnteLoadingWidget();
}
return WillPopScope(
onWillPop: (() async => _buildPageExitWidget(context)),
2022-06-11 08:23:52 +00:00
child: Scaffold(
appBar: AppBar(
2023-04-06 08:39:34 +00:00
title: Text(S.of(context).subscription),
2022-06-11 08:23:52 +00:00
),
body: Column(
children: <Widget>[
2022-07-03 09:49:33 +00:00
(progress != 1.0)
? LinearProgressIndicator(value: progress)
: const SizedBox.shrink(),
2022-06-11 08:23:52 +00:00
Expanded(
child: InAppWebView(
initialUrlRequest: URLRequest(url: initPaymentUrl),
2022-07-03 09:49:33 +00:00
onProgressChanged:
(InAppWebViewController controller, int progress) {
2022-06-11 08:23:52 +00:00
setState(() {
this.progress = progress / 100;
});
},
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
),
),
2022-06-11 08:23:52 +00:00
shouldOverrideUrlLoading: (controller, navigationAction) async {
2022-08-29 14:43:31 +00:00
final loadingUri = navigationAction.request.url;
2022-06-11 08:23:52 +00:00
_logger.info("Loading url $loadingUri");
// handle the payment response
if (_isPaymentActionComplete(loadingUri)) {
await _handlePaymentResponse(loadingUri!);
2022-06-11 08:23:52 +00:00
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
onConsoleMessage: (controller, consoleMessage) {
_logger.info(consoleMessage);
},
onLoadStart: (controller, navigationAction) async {
if (!_dialog.isShowing()) {
await _dialog.show();
}
},
onLoadError: (controller, navigationAction, code, msg) async {
if (_dialog.isShowing()) {
await _dialog.hide();
}
},
2022-07-03 09:49:33 +00:00
onLoadHttpError:
(controller, navigationAction, code, msg) async {
2022-06-11 08:23:52 +00:00
_logger.info("onHttpError with $code and msg = $msg");
},
onLoadStop: (controller, navigationAction) async {
_logger.info("loadStart" + navigationAction.toString());
if (_dialog.isShowing()) {
await _dialog.hide();
}
},
),
2022-06-11 08:23:52 +00:00
),
].whereNotNull().toList(),
2022-06-11 08:23:52 +00:00
),
),
);
}
@override
void dispose() {
2021-08-18 19:43:35 +00:00
_dialog.hide();
super.dispose();
}
Uri _getPaymentUrl(String? paymentToken) {
2021-08-23 10:15:45 +00:00
final queryParameters = {
'productID': widget.planId,
'paymentToken': paymentToken,
'action': widget.actionType,
'redirectURL': kWebPaymentRedirectUrl,
};
2022-08-29 14:43:31 +00:00
final tryParse = Uri.tryParse(kWebPaymentBaseEndpoint);
2021-08-23 10:15:45 +00:00
if (kDebugMode && kWebPaymentBaseEndpoint.startsWith("http://")) {
return Uri.http(tryParse!.authority, tryParse.path, queryParameters);
2021-08-23 10:15:45 +00:00
} else {
return Uri.https(tryParse!.authority, tryParse.path, queryParameters);
2021-08-23 10:15:45 +00:00
}
}
2021-08-18 20:57:01 +00:00
// show dialog to handle accidental back press.
Future<bool> _buildPageExitWidget(BuildContext context) async {
final result = await showDialog(
2021-08-23 10:24:56 +00:00
context: context,
builder: (context) => AlertDialog(
2023-04-06 08:39:34 +00:00
title: Text(S.of(context).areYouSureYouWantToExit),
2021-08-23 10:24:56 +00:00
actions: <Widget>[
TextButton(
2023-04-06 08:39:34 +00:00
child: Text(
S.of(context).yes,
style: const TextStyle(
2022-06-11 08:23:52 +00:00
color: Colors.redAccent,
),
),
2021-08-23 10:24:56 +00:00
onPressed: () => Navigator.of(context).pop(true),
),
TextButton(
child: Text(
2023-04-06 08:39:34 +00:00
S.of(context).no,
2021-08-23 10:24:56 +00:00
style: TextStyle(
2022-07-12 06:30:02 +00:00
color: Theme.of(context).colorScheme.greenAlternative,
2021-08-23 10:24:56 +00:00
),
),
onPressed: () => Navigator.of(context).pop(false),
),
],
),
);
if (result != null) {
return result;
}
return false;
2021-08-18 20:57:01 +00:00
}
bool _isPaymentActionComplete(Uri? loadingUri) {
return loadingUri.toString().startsWith(kWebPaymentRedirectUrl);
}
2021-08-23 10:15:45 +00:00
Future<void> _handlePaymentResponse(Uri uri) async {
2022-08-29 14:43:31 +00:00
final queryParams = uri.queryParameters;
final paymentStatus = uri.queryParameters['status'] ?? '';
_logger.fine('handle payment response with status $paymentStatus');
if (paymentStatus == 'success') {
await _handlePaymentSuccess(queryParams);
2021-08-23 10:15:45 +00:00
} else if (paymentStatus == 'fail') {
2022-08-29 14:43:31 +00:00
final reason = queryParams['reason'] ?? '';
await _handlePaymentFailure(reason);
2021-08-18 19:43:35 +00:00
} else {
// should never reach here
_logger.severe("unexpected status", uri.toString());
showGenericErrorDialog(context: context);
2021-08-18 19:43:35 +00:00
}
}
Future<void> _handlePaymentFailure(String reason) async {
await showDialog(
2022-06-11 08:23:52 +00:00
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
2023-04-06 08:39:34 +00:00
title: Text(S.of(context).paymentFailed),
content: Text(S.of(context).paymentFailedWithReason(reason)),
2022-06-11 08:23:52 +00:00
actions: <Widget>[
TextButton(
2023-04-06 08:39:34 +00:00
child: Text(S.of(context).ok),
2022-06-11 08:23:52 +00:00
onPressed: () {
Navigator.of(context).pop('dialog');
},
),
],
),
);
Navigator.of(context).pop(true);
}
2021-08-18 19:43:35 +00:00
// return true if verifySubscription didn't throw any exceptions
2021-08-18 20:57:01 +00:00
Future<void> _handlePaymentSuccess(Map<String, String> queryParams) async {
2022-08-29 14:43:31 +00:00
final checkoutSessionID = queryParams['session_id'] ?? '';
2021-08-18 19:43:35 +00:00
await _dialog.show();
try {
2022-08-29 14:43:31 +00:00
final response = await billingService.verifySubscription(
2022-06-11 08:23:52 +00:00
widget.planId,
checkoutSessionID,
paymentProvider: stripe,
2022-06-11 08:23:52 +00:00
);
2021-08-18 20:57:01 +00:00
await _dialog.hide();
2022-12-30 15:42:03 +00:00
final content = widget.actionType == 'buy'
2023-04-06 08:39:34 +00:00
? S.of(context).yourPurchaseWasSuccessful
: S.of(context).yourSubscriptionWasUpdatedSuccessfully;
await _showExitPageDialog(
2023-04-18 09:05:21 +00:00
title: S.of(context).thankYou,
content: content,
);
2021-08-18 19:43:35 +00:00
} catch (error) {
_logger.severe(error);
await _dialog.hide();
2021-08-18 20:57:01 +00:00
await _showExitPageDialog(
2023-04-06 08:39:34 +00:00
title: S.of(context).failedToVerifyPaymentStatus,
content: S.of(context).pleaseWaitForSometimeBeforeRetrying,
2021-08-18 20:57:01 +00:00
);
}
2021-08-18 20:57:01 +00:00
}
// warn the user to wait for sometime before trying another payment
Future<dynamic> _showExitPageDialog({String? title, String? content}) {
2021-08-18 20:57:01 +00:00
return showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(title!),
content: Text(content!),
actions: <Widget>[
TextButton(
2022-06-11 08:23:52 +00:00
child: Text(
2023-04-06 08:39:34 +00:00
S.of(context).ok,
2022-07-12 06:30:02 +00:00
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
2022-06-11 08:23:52 +00:00
),
onPressed: () {
Navigator.of(context).pop('dialog');
},
),
],
),
).then((val) => Navigator.pop(context, true));
}
}