2021-01-08 17:13:10 +00:00
|
|
|
import 'dart:async';
|
2021-01-06 16:48:48 +00:00
|
|
|
import 'dart:io';
|
|
|
|
|
2021-01-08 09:41:59 +00:00
|
|
|
import 'package:flutter/cupertino.dart';
|
2021-01-06 16:09:42 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/widgets.dart';
|
2021-01-31 20:41:41 +00:00
|
|
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
2021-01-08 17:13:10 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2021-01-10 20:49:22 +00:00
|
|
|
import 'package:photos/core/event_bus.dart';
|
2021-02-02 16:35:38 +00:00
|
|
|
import 'package:photos/events/subscription_purchased_event.dart';
|
2021-01-06 16:09:42 +00:00
|
|
|
import 'package:photos/models/billing_plan.dart';
|
2021-01-30 18:26:32 +00:00
|
|
|
import 'package:photos/models/subscription.dart';
|
2021-01-06 16:09:42 +00:00
|
|
|
import 'package:photos/services/billing_service.dart';
|
2021-08-18 12:43:28 +00:00
|
|
|
import 'package:photos/services/update_service.dart';
|
2021-07-26 15:12:24 +00:00
|
|
|
import 'package:photos/ui/billing_questions_widget.dart';
|
2021-08-20 11:22:01 +00:00
|
|
|
import 'package:photos/ui/common/dialogs.dart';
|
2021-01-06 16:09:42 +00:00
|
|
|
import 'package:photos/ui/loading_widget.dart';
|
2021-08-18 12:43:28 +00:00
|
|
|
import 'package:photos/ui/payment/payment_web_page.dart';
|
2021-08-17 09:40:05 +00:00
|
|
|
import 'package:photos/ui/payment/skip_subscription_widget.dart';
|
|
|
|
import 'package:photos/ui/payment/subscription_plan_widget.dart';
|
2021-07-07 21:10:56 +00:00
|
|
|
import 'package:photos/ui/progress_dialog.dart';
|
2021-02-01 11:14:22 +00:00
|
|
|
import 'package:photos/utils/data_util.dart';
|
2021-07-07 21:10:56 +00:00
|
|
|
import 'package:photos/utils/date_time_util.dart';
|
2021-01-06 16:48:48 +00:00
|
|
|
import 'package:photos/utils/dialog_util.dart';
|
2021-01-31 20:41:41 +00:00
|
|
|
import 'package:photos/utils/toast_util.dart';
|
2021-07-07 21:10:56 +00:00
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2021-01-06 16:09:42 +00:00
|
|
|
|
2021-08-19 17:22:14 +00:00
|
|
|
import '../web_page.dart';
|
|
|
|
|
2021-01-08 17:13:10 +00:00
|
|
|
class SubscriptionPage extends StatefulWidget {
|
2021-02-25 15:14:31 +00:00
|
|
|
final bool isOnboarding;
|
|
|
|
|
|
|
|
const SubscriptionPage({
|
|
|
|
this.isOnboarding = false,
|
|
|
|
Key key,
|
|
|
|
}) : super(key: key);
|
2021-01-06 16:09:42 +00:00
|
|
|
|
2021-01-08 17:13:10 +00:00
|
|
|
@override
|
|
|
|
_SubscriptionPageState createState() => _SubscriptionPageState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SubscriptionPageState extends State<SubscriptionPage> {
|
2021-01-18 17:05:01 +00:00
|
|
|
final _logger = Logger("SubscriptionPage");
|
2021-01-30 18:26:32 +00:00
|
|
|
final _billingService = BillingService.instance;
|
|
|
|
Subscription _currentSubscription;
|
2021-01-08 17:13:10 +00:00
|
|
|
StreamSubscription _purchaseUpdateSubscription;
|
2021-01-30 08:14:41 +00:00
|
|
|
ProgressDialog _dialog;
|
2021-02-01 11:14:22 +00:00
|
|
|
Future<int> _usageFuture;
|
2021-08-19 11:23:48 +00:00
|
|
|
|
|
|
|
// indicates if user's subscription plan is still active
|
2021-02-01 11:14:22 +00:00
|
|
|
bool _hasActiveSubscription;
|
2021-08-19 11:23:48 +00:00
|
|
|
bool _isAutoReviewCancelled;
|
2021-07-31 06:08:37 +00:00
|
|
|
FreePlan _freePlan;
|
2021-08-19 11:23:48 +00:00
|
|
|
List<BillingPlan> _plans = [];
|
2021-05-19 16:45:21 +00:00
|
|
|
bool _hasLoadedData = false;
|
2021-07-31 06:08:37 +00:00
|
|
|
bool _isActiveStripeSubscriber;
|
2021-08-19 11:23:48 +00:00
|
|
|
|
2021-08-18 12:43:28 +00:00
|
|
|
// based on this flag, we would show ente payment page with stripe plans
|
|
|
|
bool _isIndependentApk;
|
2021-08-19 07:53:37 +00:00
|
|
|
bool _showYearlyPlan = false;
|
2021-01-08 17:13:10 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
2021-01-30 18:26:32 +00:00
|
|
|
_billingService.setIsOnSubscriptionPage(true);
|
2021-08-18 12:43:28 +00:00
|
|
|
_isIndependentApk = UpdateService.instance.isIndependentFlavor();
|
2021-08-19 11:23:48 +00:00
|
|
|
_fetchSub();
|
2021-08-19 06:42:33 +00:00
|
|
|
_setupPurchaseUpdateStreamListener();
|
|
|
|
_dialog = createProgressDialog(context, "please wait...");
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _fetchSub() async {
|
|
|
|
return _billingService.fetchSubscription().then((subscription) async {
|
2021-05-19 16:23:01 +00:00
|
|
|
_currentSubscription = subscription;
|
2021-08-19 07:53:37 +00:00
|
|
|
_showYearlyPlan = _currentSubscription.isYearlyPlan();
|
2021-05-19 16:23:01 +00:00
|
|
|
_hasActiveSubscription = _currentSubscription.isValid();
|
2021-08-19 11:23:48 +00:00
|
|
|
_isAutoReviewCancelled =
|
|
|
|
_currentSubscription.attributes?.isCancelled ?? false;
|
2021-07-31 06:08:37 +00:00
|
|
|
_isActiveStripeSubscriber =
|
|
|
|
_currentSubscription.paymentProvider == kStripe &&
|
|
|
|
_currentSubscription.isValid();
|
2021-02-01 11:14:22 +00:00
|
|
|
_usageFuture = _billingService.fetchUsage();
|
2021-08-19 11:23:48 +00:00
|
|
|
return _filterPlansForUI().then((value) {
|
|
|
|
_hasLoadedData = true;
|
|
|
|
setState(() {});
|
|
|
|
});
|
2021-05-19 16:23:01 +00:00
|
|
|
});
|
2021-08-19 06:42:33 +00:00
|
|
|
}
|
|
|
|
|
2021-08-19 07:53:37 +00:00
|
|
|
// _filterPlansForUI is used for initializing initState & plan toggle states
|
|
|
|
Future<void> _filterPlansForUI() async {
|
|
|
|
final billingPlans = await _billingService.getBillingPlans();
|
|
|
|
_freePlan = billingPlans.freePlan;
|
|
|
|
_plans = billingPlans.plans.where((plan) {
|
|
|
|
final productID = (_showStripePlans())
|
|
|
|
? plan.stripeID
|
|
|
|
: Platform.isAndroid
|
2021-08-19 11:23:48 +00:00
|
|
|
? plan.androidID
|
|
|
|
: plan.iosID;
|
2021-08-19 07:53:37 +00:00
|
|
|
var isYearlyPlan = plan.period == 'year';
|
|
|
|
return productID != null &&
|
|
|
|
productID.isNotEmpty &&
|
|
|
|
isYearlyPlan == _showYearlyPlan;
|
|
|
|
}).toList();
|
2021-08-19 11:23:48 +00:00
|
|
|
setState(() {});
|
2021-08-19 07:53:37 +00:00
|
|
|
}
|
|
|
|
|
2021-08-19 08:33:29 +00:00
|
|
|
FutureOr onWebPaymentGoBack(dynamic value) async {
|
2021-08-19 06:42:33 +00:00
|
|
|
if (widget.isOnboarding) {
|
|
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
|
|
} else {
|
2021-08-19 08:33:29 +00:00
|
|
|
// refresh subscription
|
2021-08-19 17:22:14 +00:00
|
|
|
await _dialog.show();
|
2021-08-19 08:33:29 +00:00
|
|
|
try {
|
|
|
|
await _fetchSub();
|
|
|
|
} catch (e) {
|
|
|
|
showToast("failed to refresh subscription");
|
|
|
|
}
|
2021-08-19 17:22:14 +00:00
|
|
|
await _dialog.hide();
|
2021-08-19 06:42:33 +00:00
|
|
|
}
|
2021-05-19 16:23:01 +00:00
|
|
|
}
|
2021-01-30 08:41:05 +00:00
|
|
|
|
2021-08-18 12:43:28 +00:00
|
|
|
bool _showStripePlans() {
|
|
|
|
return _isActiveStripeSubscriber || _isIndependentApk;
|
|
|
|
}
|
|
|
|
|
2021-05-19 16:23:01 +00:00
|
|
|
void _setupPurchaseUpdateStreamListener() {
|
2021-01-31 20:41:41 +00:00
|
|
|
_purchaseUpdateSubscription = InAppPurchaseConnection
|
|
|
|
.instance.purchaseUpdatedStream
|
|
|
|
.listen((purchases) async {
|
2021-02-01 19:14:04 +00:00
|
|
|
if (!_dialog.isShowing()) {
|
|
|
|
await _dialog.show();
|
|
|
|
}
|
2021-01-31 20:41:41 +00:00
|
|
|
for (final purchase in purchases) {
|
2021-02-01 19:14:04 +00:00
|
|
|
_logger.info("Purchase status " + purchase.status.toString());
|
2021-01-31 20:41:41 +00:00
|
|
|
if (purchase.status == PurchaseStatus.purchased) {
|
|
|
|
try {
|
|
|
|
final newSubscription = await _billingService.verifySubscription(
|
2021-02-01 19:14:04 +00:00
|
|
|
purchase.productID,
|
|
|
|
purchase.verificationData.serverVerificationData,
|
|
|
|
);
|
2021-01-31 20:41:41 +00:00
|
|
|
await InAppPurchaseConnection.instance.completePurchase(purchase);
|
2021-05-09 18:44:05 +00:00
|
|
|
String text = "thank you for subscribing!";
|
2021-03-02 19:36:43 +00:00
|
|
|
if (!widget.isOnboarding) {
|
|
|
|
final isUpgrade = _hasActiveSubscription &&
|
|
|
|
newSubscription.storage > _currentSubscription.storage;
|
|
|
|
final isDowngrade = _hasActiveSubscription &&
|
|
|
|
newSubscription.storage < _currentSubscription.storage;
|
|
|
|
if (isUpgrade) {
|
|
|
|
text = "your plan was successfully upgraded";
|
|
|
|
} else if (isDowngrade) {
|
|
|
|
text = "your plan was successfully downgraded";
|
|
|
|
}
|
2021-01-31 20:41:41 +00:00
|
|
|
}
|
|
|
|
showToast(text);
|
2021-05-19 16:23:01 +00:00
|
|
|
_currentSubscription = newSubscription;
|
|
|
|
_hasActiveSubscription = _currentSubscription.isValid();
|
|
|
|
setState(() {});
|
|
|
|
await _dialog.hide();
|
|
|
|
Bus.instance.fire(SubscriptionPurchasedEvent());
|
|
|
|
if (widget.isOnboarding) {
|
2021-02-01 19:14:04 +00:00
|
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
|
|
}
|
2021-01-31 20:41:41 +00:00
|
|
|
} catch (e) {
|
|
|
|
_logger.warning("Could not complete payment ", e);
|
|
|
|
await _dialog.hide();
|
|
|
|
showErrorDialog(
|
|
|
|
context,
|
|
|
|
"payment failed",
|
|
|
|
"please talk to " +
|
|
|
|
(Platform.isAndroid ? "PlayStore" : "AppStore") +
|
|
|
|
" support if you were charged");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
|
|
|
await InAppPurchaseConnection.instance.completePurchase(purchase);
|
2021-02-02 18:45:09 +00:00
|
|
|
await _dialog.hide();
|
2021-03-15 14:15:30 +00:00
|
|
|
} else if (purchase.status == PurchaseStatus.error) {
|
|
|
|
await _dialog.hide();
|
2021-01-30 18:26:32 +00:00
|
|
|
}
|
2021-01-08 17:13:10 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_purchaseUpdateSubscription.cancel();
|
2021-01-30 18:26:32 +00:00
|
|
|
_billingService.setIsOnSubscriptionPage(false);
|
2021-01-08 17:13:10 +00:00
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2021-01-06 16:09:42 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final appBar = AppBar(
|
2021-01-30 07:22:21 +00:00
|
|
|
title: Text("subscription"),
|
2021-01-06 16:09:42 +00:00
|
|
|
);
|
|
|
|
return Scaffold(
|
|
|
|
appBar: appBar,
|
2021-05-19 21:18:35 +00:00
|
|
|
body: _getBody(),
|
2021-01-06 16:09:42 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-19 21:18:35 +00:00
|
|
|
Widget _getBody() {
|
2021-05-19 16:23:01 +00:00
|
|
|
if (_hasLoadedData) {
|
2021-05-19 21:18:35 +00:00
|
|
|
return _buildPlans();
|
2021-05-19 16:23:01 +00:00
|
|
|
}
|
|
|
|
return loadWidget;
|
2021-01-06 16:09:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-19 21:18:35 +00:00
|
|
|
Widget _buildPlans() {
|
2021-07-26 15:09:26 +00:00
|
|
|
final widgets = <Widget>[];
|
2021-03-10 03:29:39 +00:00
|
|
|
if (widget.isOnboarding) {
|
2021-01-31 21:00:12 +00:00
|
|
|
widgets.add(Padding(
|
2021-04-01 23:33:43 +00:00
|
|
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
|
2021-01-31 21:00:12 +00:00
|
|
|
child: Text(
|
2021-05-12 16:42:15 +00:00
|
|
|
"ente preserves your memories, so they're always available to you, even if you lose your device",
|
2021-01-31 21:00:12 +00:00
|
|
|
style: TextStyle(
|
|
|
|
color: Colors.white54,
|
|
|
|
height: 1.2,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
));
|
|
|
|
} else {
|
2021-07-26 15:09:26 +00:00
|
|
|
widgets.add(
|
|
|
|
SizedBox(
|
|
|
|
height: 50,
|
|
|
|
child: FutureBuilder(
|
|
|
|
future: _usageFuture,
|
|
|
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
|
|
|
if (snapshot.hasData) {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
child: Text("current usage is " + formatBytes(snapshot.data)),
|
|
|
|
);
|
|
|
|
} else if (snapshot.hasError) {
|
|
|
|
return Container();
|
|
|
|
} else {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
child: loadWidget,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
2021-01-31 21:02:43 +00:00
|
|
|
),
|
2021-07-26 15:09:26 +00:00
|
|
|
);
|
2021-01-31 21:00:12 +00:00
|
|
|
}
|
2021-05-19 17:22:00 +00:00
|
|
|
widgets.addAll([
|
|
|
|
Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2021-08-18 12:43:28 +00:00
|
|
|
children: _showStripePlans()
|
2021-05-19 17:22:00 +00:00
|
|
|
? _getStripePlanWidgets()
|
|
|
|
: _getMobilePlanWidgets(),
|
|
|
|
),
|
|
|
|
Padding(padding: EdgeInsets.all(8)),
|
|
|
|
]);
|
2021-01-31 21:08:26 +00:00
|
|
|
|
2021-08-19 17:22:14 +00:00
|
|
|
if (_showStripePlans()) {
|
|
|
|
widgets.add(_showSubscriptionToggle());
|
|
|
|
}
|
|
|
|
|
2021-05-19 21:18:35 +00:00
|
|
|
if (_hasActiveSubscription) {
|
2021-08-19 11:23:48 +00:00
|
|
|
var endDate = getDateAndMonthAndYear(
|
|
|
|
DateTime.fromMicrosecondsSinceEpoch(_currentSubscription.expiryTime));
|
2021-08-19 17:22:14 +00:00
|
|
|
var message = "renews on $endDate";
|
|
|
|
if (_currentSubscription.productID == kFreeProductID) {
|
|
|
|
message = "free plan valid till $endDate";
|
|
|
|
} else if (_isAutoReviewCancelled) {
|
2021-08-19 11:23:48 +00:00
|
|
|
message = "your subscription will be cancelled on $endDate";
|
|
|
|
}
|
2021-05-19 21:18:35 +00:00
|
|
|
widgets.add(
|
|
|
|
Text(
|
2021-08-19 11:23:48 +00:00
|
|
|
message,
|
2021-05-19 21:18:35 +00:00
|
|
|
style: TextStyle(
|
|
|
|
color: Colors.white.withOpacity(0.6),
|
|
|
|
fontSize: 14,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2021-08-19 11:23:48 +00:00
|
|
|
|
|
|
|
if (_isIndependentApk &&
|
|
|
|
_hasActiveSubscription &&
|
|
|
|
_isActiveStripeSubscriber) {
|
|
|
|
widgets.add(_stripeSubscriptionToggleButton(_isAutoReviewCancelled));
|
|
|
|
}
|
|
|
|
|
2021-02-25 15:14:31 +00:00
|
|
|
if (_hasActiveSubscription &&
|
|
|
|
_currentSubscription.productID != kFreeProductID) {
|
2021-01-31 21:08:26 +00:00
|
|
|
widgets.addAll([
|
2021-02-01 08:48:13 +00:00
|
|
|
Align(
|
|
|
|
alignment: Alignment.center,
|
|
|
|
child: GestureDetector(
|
2021-08-19 17:22:14 +00:00
|
|
|
onTap: () async {
|
2021-07-31 06:08:37 +00:00
|
|
|
if (_isActiveStripeSubscriber) {
|
2021-08-19 17:22:14 +00:00
|
|
|
if (_isIndependentApk) {
|
|
|
|
await _launchStripePortal();
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
return;
|
2021-08-19 11:23:48 +00:00
|
|
|
}
|
2021-05-19 17:07:52 +00:00
|
|
|
}
|
2021-02-03 14:24:19 +00:00
|
|
|
if (Platform.isAndroid) {
|
|
|
|
launch(
|
|
|
|
"https://play.google.com/store/account/subscriptions?sku=" +
|
|
|
|
_currentSubscription.productID +
|
|
|
|
"&package=io.ente.photos");
|
|
|
|
} else {
|
|
|
|
launch("https://apps.apple.com/account/billing");
|
|
|
|
}
|
2021-02-01 08:48:13 +00:00
|
|
|
},
|
|
|
|
child: Container(
|
2021-05-19 17:07:52 +00:00
|
|
|
padding: EdgeInsets.fromLTRB(40, 80, 40, 80),
|
2021-02-16 14:01:29 +00:00
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
RichText(
|
|
|
|
text: TextSpan(
|
2021-08-19 17:22:14 +00:00
|
|
|
text: _isActiveStripeSubscriber && !_isIndependentApk
|
2021-05-19 17:07:52 +00:00
|
|
|
? "visit web.ente.io to manage your subscription"
|
|
|
|
: "payment details",
|
2021-02-16 14:01:29 +00:00
|
|
|
style: TextStyle(
|
2021-07-31 06:08:37 +00:00
|
|
|
color: _isActiveStripeSubscriber
|
2021-05-19 17:07:52 +00:00
|
|
|
? Colors.white
|
|
|
|
: Colors.blue,
|
2021-02-16 14:01:29 +00:00
|
|
|
fontFamily: 'Ubuntu',
|
|
|
|
fontSize: 15,
|
|
|
|
),
|
|
|
|
),
|
2021-05-19 17:07:52 +00:00
|
|
|
textAlign: TextAlign.center,
|
2021-02-16 14:01:29 +00:00
|
|
|
),
|
|
|
|
],
|
2021-02-01 08:48:13 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2021-01-31 21:08:26 +00:00
|
|
|
]);
|
2021-05-19 21:18:35 +00:00
|
|
|
}
|
|
|
|
if (widget.isOnboarding &&
|
|
|
|
_currentSubscription.productID == kFreeProductID) {
|
2021-08-17 09:40:05 +00:00
|
|
|
widgets.addAll([SkipSubscriptionWidget(freePlan: _freePlan)]);
|
2021-05-19 21:18:35 +00:00
|
|
|
}
|
|
|
|
if (_currentSubscription.productID == kFreeProductID) {
|
2021-02-05 11:10:05 +00:00
|
|
|
widgets.addAll([
|
2021-02-01 08:48:13 +00:00
|
|
|
Align(
|
|
|
|
alignment: Alignment.center,
|
|
|
|
child: GestureDetector(
|
2021-02-25 15:14:31 +00:00
|
|
|
behavior: HitTestBehavior.translucent,
|
2021-02-01 08:48:13 +00:00
|
|
|
onTap: () {
|
2021-02-03 14:24:19 +00:00
|
|
|
showModalBottomSheet<void>(
|
2021-05-19 21:18:35 +00:00
|
|
|
backgroundColor: Color.fromRGBO(10, 15, 15, 1.0),
|
2021-02-03 14:24:19 +00:00
|
|
|
barrierColor: Colors.black87,
|
|
|
|
context: context,
|
|
|
|
builder: (context) {
|
|
|
|
return BillingQuestionsWidget();
|
|
|
|
},
|
|
|
|
);
|
2021-02-01 08:48:13 +00:00
|
|
|
},
|
|
|
|
child: Container(
|
2021-05-19 21:25:24 +00:00
|
|
|
padding: EdgeInsets.all(40),
|
2021-02-01 08:48:13 +00:00
|
|
|
child: RichText(
|
|
|
|
text: TextSpan(
|
2021-02-03 14:24:19 +00:00
|
|
|
text: "questions?",
|
2021-02-01 08:48:13 +00:00
|
|
|
style: TextStyle(
|
|
|
|
color: Colors.blue,
|
|
|
|
fontFamily: 'Ubuntu',
|
|
|
|
),
|
2021-01-31 21:00:12 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2021-02-01 08:48:13 +00:00
|
|
|
]);
|
|
|
|
}
|
2021-01-06 16:09:42 +00:00
|
|
|
return SingleChildScrollView(
|
2021-07-26 15:09:26 +00:00
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
|
|
children: widgets,
|
2021-01-06 16:09:42 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2021-02-25 15:14:31 +00:00
|
|
|
|
2021-08-19 17:22:14 +00:00
|
|
|
Future<void> _launchStripePortal() async {
|
|
|
|
await _dialog.show();
|
|
|
|
try {
|
|
|
|
String url = await _billingService.getStripeCustomerPortalUrl();
|
|
|
|
Navigator.of(context).push(
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (BuildContext context) {
|
|
|
|
return WebPage("payment details", url);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
).then((value) => onWebPaymentGoBack);
|
|
|
|
} catch (e) {
|
|
|
|
await _dialog.hide();
|
|
|
|
showGenericErrorDialog(context);
|
|
|
|
}
|
|
|
|
await _dialog.hide();
|
|
|
|
}
|
|
|
|
|
2021-08-19 11:23:48 +00:00
|
|
|
Widget _stripeSubscriptionToggleButton(bool isCurrentlyCancelled) {
|
|
|
|
return TextButton(
|
|
|
|
child: Text(
|
|
|
|
isCurrentlyCancelled ? "renew subscription" : "cancel subscription",
|
|
|
|
style: TextStyle(
|
|
|
|
color: isCurrentlyCancelled ? Colors.greenAccent : Colors.redAccent,
|
|
|
|
),
|
|
|
|
),
|
2021-08-20 11:22:01 +00:00
|
|
|
onPressed: () async {
|
|
|
|
var result = await showChoiceDialog(
|
|
|
|
context,
|
|
|
|
isCurrentlyCancelled
|
|
|
|
? 'subscription renewal'
|
|
|
|
: 'subscription cancellation',
|
|
|
|
isCurrentlyCancelled
|
|
|
|
? 'are you sure you want to renew?'
|
|
|
|
: 'are you sure you want to cancel?',
|
|
|
|
firstAction: 'yes',
|
|
|
|
secondAction: 'no');
|
|
|
|
if (result == DialogUserChoice.firstChoice) {
|
|
|
|
toggleStripeSubscription(isCurrentlyCancelled);
|
|
|
|
}
|
2021-08-19 11:23:48 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-08-19 17:22:14 +00:00
|
|
|
Future<void> toggleStripeSubscription(bool isCurrentlyCancelled) async {
|
|
|
|
await _dialog.show();
|
|
|
|
try {
|
|
|
|
if (isCurrentlyCancelled) {
|
|
|
|
await _billingService.activateStripeSubscription();
|
|
|
|
} else {
|
|
|
|
await _billingService.cancelStripeSubscription();
|
|
|
|
}
|
|
|
|
await _fetchSub();
|
|
|
|
} catch (e) {
|
|
|
|
showToast(isCurrentlyCancelled ? 'failed to renew' : 'failed to cancel');
|
|
|
|
}
|
|
|
|
await _dialog.hide();
|
|
|
|
}
|
|
|
|
|
2021-05-19 17:22:00 +00:00
|
|
|
List<Widget> _getStripePlanWidgets() {
|
|
|
|
final List<Widget> planWidgets = [];
|
2021-07-26 16:08:53 +00:00
|
|
|
bool foundActivePlan = false;
|
2021-07-31 06:08:37 +00:00
|
|
|
for (final plan in _plans) {
|
2021-05-19 17:22:00 +00:00
|
|
|
final productID = plan.stripeID;
|
2021-07-26 16:08:53 +00:00
|
|
|
if (productID == null || productID.isEmpty) {
|
2021-05-19 17:22:00 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
final isActive =
|
|
|
|
_hasActiveSubscription && _currentSubscription.productID == productID;
|
2021-07-26 16:08:53 +00:00
|
|
|
if (isActive) {
|
|
|
|
foundActivePlan = true;
|
|
|
|
}
|
2021-05-19 17:22:00 +00:00
|
|
|
planWidgets.add(
|
|
|
|
Material(
|
|
|
|
child: InkWell(
|
|
|
|
onTap: () async {
|
|
|
|
if (isActive) {
|
|
|
|
return;
|
|
|
|
}
|
2021-08-19 11:23:48 +00:00
|
|
|
if (_isActiveStripeSubscriber && !_isIndependentApk) {
|
|
|
|
showErrorDialog(context, "sorry",
|
|
|
|
"please visit web.ente.io to manage your subscription");
|
|
|
|
return;
|
|
|
|
}
|
2021-08-18 12:43:28 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2021-08-20 11:22:01 +00:00
|
|
|
String stripPurChaseAction = 'buy';
|
2021-08-18 12:43:28 +00:00
|
|
|
if (_isActiveStripeSubscriber) {
|
2021-08-20 11:22:01 +00:00
|
|
|
// confirm if user wants to change plan or not
|
|
|
|
var result = await showChoiceDialog(
|
|
|
|
context,
|
|
|
|
"confirm plan change",
|
|
|
|
"are you sure you want to change your plan?",
|
|
|
|
firstAction: "yes",
|
|
|
|
secondAction: 'no');
|
|
|
|
if (result == DialogUserChoice.secondChoice) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
stripPurChaseAction = 'update';
|
2021-08-18 12:43:28 +00:00
|
|
|
}
|
2021-08-20 11:22:01 +00:00
|
|
|
Navigator.push(
|
|
|
|
context, MaterialPageRoute(
|
|
|
|
builder: (BuildContext context) {
|
|
|
|
return PaymentWebPage(
|
|
|
|
planId: plan.stripeID,
|
|
|
|
actionType: stripPurChaseAction,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
).then((value) => onWebPaymentGoBack(value));
|
2021-05-19 17:22:00 +00:00
|
|
|
},
|
|
|
|
child: SubscriptionPlanWidget(
|
|
|
|
storage: plan.storage,
|
|
|
|
price: plan.price,
|
|
|
|
period: plan.period,
|
|
|
|
isActive: isActive,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2021-07-26 16:08:53 +00:00
|
|
|
if (!foundActivePlan) {
|
|
|
|
_addCurrentPlanWidget(planWidgets);
|
|
|
|
}
|
2021-05-19 17:22:00 +00:00
|
|
|
return planWidgets;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Widget> _getMobilePlanWidgets() {
|
2021-07-26 16:08:53 +00:00
|
|
|
bool foundActivePlan = false;
|
2021-05-19 16:23:01 +00:00
|
|
|
final List<Widget> planWidgets = [];
|
2021-05-19 21:18:35 +00:00
|
|
|
if (_hasActiveSubscription &&
|
2021-03-10 03:29:39 +00:00
|
|
|
_currentSubscription.productID == kFreeProductID) {
|
2021-07-26 16:08:53 +00:00
|
|
|
foundActivePlan = true;
|
2021-03-10 03:29:39 +00:00
|
|
|
planWidgets.add(
|
|
|
|
SubscriptionPlanWidget(
|
2021-07-31 06:08:37 +00:00
|
|
|
storage: _freePlan.storage,
|
2021-03-10 03:29:39 +00:00
|
|
|
price: "free",
|
|
|
|
period: "",
|
|
|
|
isActive: true,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2021-07-31 06:08:37 +00:00
|
|
|
for (final plan in _plans) {
|
2021-03-10 03:29:39 +00:00
|
|
|
final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
|
|
|
|
final isActive =
|
|
|
|
_hasActiveSubscription && _currentSubscription.productID == productID;
|
2021-07-26 16:08:53 +00:00
|
|
|
if (isActive) {
|
|
|
|
foundActivePlan = true;
|
|
|
|
}
|
2021-03-10 03:29:39 +00:00
|
|
|
planWidgets.add(
|
|
|
|
Material(
|
|
|
|
child: InkWell(
|
|
|
|
onTap: () async {
|
|
|
|
if (isActive) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await _dialog.show();
|
|
|
|
if (_usageFuture != null) {
|
|
|
|
final usage = await _usageFuture;
|
|
|
|
if (usage > plan.storage) {
|
|
|
|
await _dialog.hide();
|
|
|
|
showErrorDialog(
|
|
|
|
context, "sorry", "you cannot downgrade to this plan");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
final ProductDetailsResponse response =
|
|
|
|
await InAppPurchaseConnection.instance
|
2021-07-26 15:09:26 +00:00
|
|
|
.queryProductDetails({productID});
|
2021-03-10 03:29:39 +00:00
|
|
|
if (response.notFoundIDs.isNotEmpty) {
|
|
|
|
_logger.severe("Could not find products: " +
|
|
|
|
response.notFoundIDs.toString());
|
|
|
|
await _dialog.hide();
|
|
|
|
showGenericErrorDialog(context);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final isCrossGradingOnAndroid = Platform.isAndroid &&
|
|
|
|
_hasActiveSubscription &&
|
|
|
|
_currentSubscription.productID != kFreeProductID &&
|
|
|
|
_currentSubscription.productID != plan.androidID;
|
|
|
|
if (isCrossGradingOnAndroid) {
|
|
|
|
final existingProductDetailsResponse =
|
2021-07-26 15:09:26 +00:00
|
|
|
await InAppPurchaseConnection.instance
|
|
|
|
.queryProductDetails({_currentSubscription.productID});
|
2021-03-10 03:29:39 +00:00
|
|
|
if (existingProductDetailsResponse.notFoundIDs.isNotEmpty) {
|
|
|
|
_logger.severe("Could not find existing products: " +
|
|
|
|
response.notFoundIDs.toString());
|
|
|
|
await _dialog.hide();
|
|
|
|
showGenericErrorDialog(context);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final subscriptionChangeParam = ChangeSubscriptionParam(
|
|
|
|
oldPurchaseDetails: PurchaseDetails(
|
|
|
|
purchaseID: null,
|
|
|
|
productID: _currentSubscription.productID,
|
|
|
|
verificationData: null,
|
|
|
|
transactionDate: null,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
await InAppPurchaseConnection.instance.buyNonConsumable(
|
|
|
|
purchaseParam: PurchaseParam(
|
|
|
|
productDetails: response.productDetails[0],
|
|
|
|
changeSubscriptionParam: subscriptionChangeParam,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
await InAppPurchaseConnection.instance.buyNonConsumable(
|
|
|
|
purchaseParam: PurchaseParam(
|
|
|
|
productDetails: response.productDetails[0],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
child: SubscriptionPlanWidget(
|
|
|
|
storage: plan.storage,
|
|
|
|
price: plan.price,
|
|
|
|
period: plan.period,
|
|
|
|
isActive: isActive,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2021-07-26 16:08:53 +00:00
|
|
|
if (!foundActivePlan) {
|
|
|
|
_addCurrentPlanWidget(planWidgets);
|
|
|
|
}
|
2021-03-10 03:29:39 +00:00
|
|
|
return planWidgets;
|
|
|
|
}
|
|
|
|
|
2021-08-19 07:53:37 +00:00
|
|
|
Widget _showSubscriptionToggle() {
|
|
|
|
return Container(
|
|
|
|
padding: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
|
|
|
|
margin: EdgeInsets.only(bottom: 12),
|
|
|
|
// color: Color.fromRGBO(10, 40, 40, 0.3),
|
|
|
|
child: Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
|
|
children: [
|
2021-08-19 17:20:47 +00:00
|
|
|
Text(
|
|
|
|
"monthly plans",
|
|
|
|
style: TextStyle(
|
|
|
|
color: Theme.of(context)
|
|
|
|
.buttonColor
|
|
|
|
.withOpacity(_showYearlyPlan ? 0.2 : 1.0),
|
|
|
|
),
|
|
|
|
),
|
2021-08-19 07:53:37 +00:00
|
|
|
Switch(
|
|
|
|
value: _showYearlyPlan,
|
|
|
|
onChanged: (value) async {
|
|
|
|
_showYearlyPlan = value;
|
|
|
|
await _filterPlansForUI();
|
|
|
|
setState(() {});
|
|
|
|
},
|
|
|
|
),
|
2021-08-19 17:20:47 +00:00
|
|
|
Text(
|
|
|
|
"yearly plans",
|
|
|
|
style: TextStyle(
|
|
|
|
color: Theme.of(context)
|
|
|
|
.buttonColor
|
|
|
|
.withOpacity(_showYearlyPlan ? 1.0 : 0.2),
|
|
|
|
),
|
|
|
|
),
|
2021-08-19 07:53:37 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-07-26 16:08:53 +00:00
|
|
|
void _addCurrentPlanWidget(List<Widget> planWidgets) {
|
2021-08-19 07:53:37 +00:00
|
|
|
// don't add current plan if it's monthly plan but UI is showing yearly plans
|
|
|
|
// and vice versa.
|
2021-08-19 17:22:14 +00:00
|
|
|
if (_showYearlyPlan != _currentSubscription.isYearlyPlan() && _currentSubscription.productID != kFreeProductID) {
|
2021-08-19 07:53:37 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-07-26 16:08:53 +00:00
|
|
|
int activePlanIndex = 0;
|
2021-07-31 06:08:37 +00:00
|
|
|
for (; activePlanIndex < _plans.length; activePlanIndex++) {
|
|
|
|
if (_plans[activePlanIndex].storage > _currentSubscription.storage) {
|
2021-07-26 16:08:53 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
planWidgets.insert(
|
|
|
|
activePlanIndex,
|
|
|
|
Material(
|
|
|
|
child: InkWell(
|
|
|
|
onTap: () {},
|
|
|
|
child: SubscriptionPlanWidget(
|
|
|
|
storage: _currentSubscription.storage,
|
|
|
|
price: _currentSubscription.price,
|
|
|
|
period: _currentSubscription.period,
|
|
|
|
isActive: true,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2021-01-06 16:09:42 +00:00
|
|
|
}
|