|
@@ -11,11 +11,9 @@ import 'package:photos/events/subscription_purchased_event.dart';
|
|
import 'package:photos/models/billing_plan.dart';
|
|
import 'package:photos/models/billing_plan.dart';
|
|
import 'package:photos/models/subscription.dart';
|
|
import 'package:photos/models/subscription.dart';
|
|
import 'package:photos/services/billing_service.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/billing_questions_widget.dart';
|
|
-import 'package:photos/ui/common/dialogs.dart';
|
|
|
|
|
|
+import 'package:photos/ui/common_elements.dart';
|
|
import 'package:photos/ui/loading_widget.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/skip_subscription_widget.dart';
|
|
import 'package:photos/ui/payment/subscription_plan_widget.dart';
|
|
import 'package:photos/ui/payment/subscription_plan_widget.dart';
|
|
import 'package:photos/ui/progress_dialog.dart';
|
|
import 'package:photos/ui/progress_dialog.dart';
|
|
@@ -25,8 +23,6 @@ import 'package:photos/utils/dialog_util.dart';
|
|
import 'package:photos/utils/toast_util.dart';
|
|
import 'package:photos/utils/toast_util.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
|
|
-import '../web_page.dart';
|
|
|
|
-
|
|
|
|
class SubscriptionPage extends StatefulWidget {
|
|
class SubscriptionPage extends StatefulWidget {
|
|
final bool isOnboarding;
|
|
final bool isOnboarding;
|
|
|
|
|
|
@@ -46,82 +42,39 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
StreamSubscription _purchaseUpdateSubscription;
|
|
StreamSubscription _purchaseUpdateSubscription;
|
|
ProgressDialog _dialog;
|
|
ProgressDialog _dialog;
|
|
Future<int> _usageFuture;
|
|
Future<int> _usageFuture;
|
|
-
|
|
|
|
- // indicates if user's subscription plan is still active
|
|
|
|
bool _hasActiveSubscription;
|
|
bool _hasActiveSubscription;
|
|
- bool _isAutoReviewCancelled;
|
|
|
|
FreePlan _freePlan;
|
|
FreePlan _freePlan;
|
|
- List<BillingPlan> _plans = [];
|
|
|
|
|
|
+ List<BillingPlan> _plans;
|
|
bool _hasLoadedData = false;
|
|
bool _hasLoadedData = false;
|
|
bool _isActiveStripeSubscriber;
|
|
bool _isActiveStripeSubscriber;
|
|
|
|
|
|
- // based on this flag, we would show ente payment page with stripe plans
|
|
|
|
- bool _isIndependentApk;
|
|
|
|
- bool _showYearlyPlan = false;
|
|
|
|
-
|
|
|
|
@override
|
|
@override
|
|
void initState() {
|
|
void initState() {
|
|
_billingService.setIsOnSubscriptionPage(true);
|
|
_billingService.setIsOnSubscriptionPage(true);
|
|
- _isIndependentApk = UpdateService.instance.isIndependentFlavor();
|
|
|
|
- _fetchSub();
|
|
|
|
- _setupPurchaseUpdateStreamListener();
|
|
|
|
- _dialog = createProgressDialog(context, "please wait...");
|
|
|
|
- super.initState();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- Future<void> _fetchSub() async {
|
|
|
|
- return _billingService.fetchSubscription().then((subscription) async {
|
|
|
|
|
|
+ _billingService.fetchSubscription().then((subscription) async {
|
|
_currentSubscription = subscription;
|
|
_currentSubscription = subscription;
|
|
- _showYearlyPlan = _currentSubscription.isYearlyPlan();
|
|
|
|
_hasActiveSubscription = _currentSubscription.isValid();
|
|
_hasActiveSubscription = _currentSubscription.isValid();
|
|
- _isAutoReviewCancelled =
|
|
|
|
- _currentSubscription.attributes?.isCancelled ?? false;
|
|
|
|
|
|
+ final billingPlans = await _billingService.getBillingPlans();
|
|
_isActiveStripeSubscriber =
|
|
_isActiveStripeSubscriber =
|
|
_currentSubscription.paymentProvider == kStripe &&
|
|
_currentSubscription.paymentProvider == kStripe &&
|
|
_currentSubscription.isValid();
|
|
_currentSubscription.isValid();
|
|
|
|
+ _plans = billingPlans.plans.where((plan) {
|
|
|
|
+ final productID = _isActiveStripeSubscriber
|
|
|
|
+ ? plan.stripeID
|
|
|
|
+ : Platform.isAndroid
|
|
|
|
+ ? plan.androidID
|
|
|
|
+ : plan.iosID;
|
|
|
|
+ return productID != null && productID.isNotEmpty;
|
|
|
|
+ }).toList();
|
|
|
|
+ _freePlan = billingPlans.freePlan;
|
|
|
|
+
|
|
_usageFuture = _billingService.fetchUsage();
|
|
_usageFuture = _billingService.fetchUsage();
|
|
- return _filterPlansForUI().then((value) {
|
|
|
|
- _hasLoadedData = true;
|
|
|
|
- setState(() {});
|
|
|
|
- });
|
|
|
|
|
|
+ _hasLoadedData = true;
|
|
|
|
+ setState(() {});
|
|
});
|
|
});
|
|
- }
|
|
|
|
-
|
|
|
|
- // _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
|
|
|
|
- ? plan.androidID
|
|
|
|
- : plan.iosID;
|
|
|
|
- var isYearlyPlan = plan.period == 'year';
|
|
|
|
- return productID != null &&
|
|
|
|
- productID.isNotEmpty &&
|
|
|
|
- isYearlyPlan == _showYearlyPlan;
|
|
|
|
- }).toList();
|
|
|
|
- setState(() {});
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- FutureOr onWebPaymentGoBack(dynamic value) async {
|
|
|
|
- if (widget.isOnboarding) {
|
|
|
|
- Navigator.of(context).popUntil((route) => route.isFirst);
|
|
|
|
- } else {
|
|
|
|
- // refresh subscription
|
|
|
|
- await _dialog.show();
|
|
|
|
- try {
|
|
|
|
- await _fetchSub();
|
|
|
|
- } catch (e) {
|
|
|
|
- showToast("failed to refresh subscription");
|
|
|
|
- }
|
|
|
|
- await _dialog.hide();
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- bool _showStripePlans() {
|
|
|
|
- return _isActiveStripeSubscriber || _isIndependentApk;
|
|
|
|
|
|
+ _setupPurchaseUpdateStreamListener();
|
|
|
|
+ _dialog = createProgressDialog(context, "please wait...");
|
|
|
|
+ super.initState();
|
|
}
|
|
}
|
|
|
|
|
|
void _setupPurchaseUpdateStreamListener() {
|
|
void _setupPurchaseUpdateStreamListener() {
|
|
@@ -248,29 +201,19 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
widgets.addAll([
|
|
widgets.addAll([
|
|
Column(
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
- children: _showStripePlans()
|
|
|
|
|
|
+ children: _isActiveStripeSubscriber
|
|
? _getStripePlanWidgets()
|
|
? _getStripePlanWidgets()
|
|
: _getMobilePlanWidgets(),
|
|
: _getMobilePlanWidgets(),
|
|
),
|
|
),
|
|
Padding(padding: EdgeInsets.all(8)),
|
|
Padding(padding: EdgeInsets.all(8)),
|
|
]);
|
|
]);
|
|
|
|
|
|
- if (_showStripePlans()) {
|
|
|
|
- widgets.add(_showSubscriptionToggle());
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
if (_hasActiveSubscription) {
|
|
if (_hasActiveSubscription) {
|
|
- var endDate = getDateAndMonthAndYear(
|
|
|
|
- DateTime.fromMicrosecondsSinceEpoch(_currentSubscription.expiryTime));
|
|
|
|
- var message = "renews on $endDate";
|
|
|
|
- if (_currentSubscription.productID == kFreeProductID) {
|
|
|
|
- message = "free plan valid till $endDate";
|
|
|
|
- } else if (_isAutoReviewCancelled) {
|
|
|
|
- message = "your subscription will be cancelled on $endDate";
|
|
|
|
- }
|
|
|
|
widgets.add(
|
|
widgets.add(
|
|
Text(
|
|
Text(
|
|
- message,
|
|
|
|
|
|
+ "valid till " +
|
|
|
|
+ getDateAndMonthAndYear(DateTime.fromMicrosecondsSinceEpoch(
|
|
|
|
+ _currentSubscription.expiryTime)),
|
|
style: TextStyle(
|
|
style: TextStyle(
|
|
color: Colors.white.withOpacity(0.6),
|
|
color: Colors.white.withOpacity(0.6),
|
|
fontSize: 14,
|
|
fontSize: 14,
|
|
@@ -279,26 +222,15 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
- if (_isIndependentApk &&
|
|
|
|
- _hasActiveSubscription &&
|
|
|
|
- _isActiveStripeSubscriber) {
|
|
|
|
- widgets.add(_stripeSubscriptionToggleButton(_isAutoReviewCancelled));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
if (_hasActiveSubscription &&
|
|
if (_hasActiveSubscription &&
|
|
_currentSubscription.productID != kFreeProductID) {
|
|
_currentSubscription.productID != kFreeProductID) {
|
|
widgets.addAll([
|
|
widgets.addAll([
|
|
Align(
|
|
Align(
|
|
alignment: Alignment.center,
|
|
alignment: Alignment.center,
|
|
child: GestureDetector(
|
|
child: GestureDetector(
|
|
- onTap: () async {
|
|
|
|
|
|
+ onTap: () {
|
|
if (_isActiveStripeSubscriber) {
|
|
if (_isActiveStripeSubscriber) {
|
|
- if (_isIndependentApk) {
|
|
|
|
- await _launchStripePortal();
|
|
|
|
- return;
|
|
|
|
- } else {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
|
|
+ return;
|
|
}
|
|
}
|
|
if (Platform.isAndroid) {
|
|
if (Platform.isAndroid) {
|
|
launch(
|
|
launch(
|
|
@@ -315,7 +247,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
children: [
|
|
children: [
|
|
RichText(
|
|
RichText(
|
|
text: TextSpan(
|
|
text: TextSpan(
|
|
- text: _isActiveStripeSubscriber && !_isIndependentApk
|
|
|
|
|
|
+ text: _isActiveStripeSubscriber
|
|
? "visit web.ente.io to manage your subscription"
|
|
? "visit web.ente.io to manage your subscription"
|
|
: "payment details",
|
|
: "payment details",
|
|
style: TextStyle(
|
|
style: TextStyle(
|
|
@@ -379,65 +311,6 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
- 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();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- Widget _stripeSubscriptionToggleButton(bool isCurrentlyCancelled) {
|
|
|
|
- return TextButton(
|
|
|
|
- child: Text(
|
|
|
|
- isCurrentlyCancelled ? "renew subscription" : "cancel subscription",
|
|
|
|
- style: TextStyle(
|
|
|
|
- color: isCurrentlyCancelled ? Colors.greenAccent : Colors.redAccent,
|
|
|
|
- ),
|
|
|
|
- ),
|
|
|
|
- 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);
|
|
|
|
- }
|
|
|
|
- },
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- 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();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
List<Widget> _getStripePlanWidgets() {
|
|
List<Widget> _getStripePlanWidgets() {
|
|
final List<Widget> planWidgets = [];
|
|
final List<Widget> planWidgets = [];
|
|
bool foundActivePlan = false;
|
|
bool foundActivePlan = false;
|
|
@@ -458,45 +331,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
if (isActive) {
|
|
if (isActive) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
- if (_isActiveStripeSubscriber && !_isIndependentApk) {
|
|
|
|
- showErrorDialog(context, "sorry",
|
|
|
|
- "please visit web.ente.io to manage your subscription");
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- 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;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- String stripPurChaseAction = 'buy';
|
|
|
|
- if (_isActiveStripeSubscriber) {
|
|
|
|
- // 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';
|
|
|
|
- }
|
|
|
|
- Navigator.push(
|
|
|
|
- context, MaterialPageRoute(
|
|
|
|
- builder: (BuildContext context) {
|
|
|
|
- return PaymentWebPage(
|
|
|
|
- planId: plan.stripeID,
|
|
|
|
- actionType: stripPurChaseAction,
|
|
|
|
- );
|
|
|
|
- },
|
|
|
|
- ),
|
|
|
|
- ).then((value) => onWebPaymentGoBack(value));
|
|
|
|
|
|
+ showErrorDialog(context, "sorry",
|
|
|
|
+ "please visit web.ente.io to manage your subscription");
|
|
},
|
|
},
|
|
child: SubscriptionPlanWidget(
|
|
child: SubscriptionPlanWidget(
|
|
storage: plan.storage,
|
|
storage: plan.storage,
|
|
@@ -554,8 +390,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
final ProductDetailsResponse response =
|
|
final ProductDetailsResponse response =
|
|
- await InAppPurchaseConnection.instance
|
|
|
|
- .queryProductDetails({productID});
|
|
|
|
|
|
+ await InAppPurchaseConnection.instance
|
|
|
|
+ .queryProductDetails({productID});
|
|
if (response.notFoundIDs.isNotEmpty) {
|
|
if (response.notFoundIDs.isNotEmpty) {
|
|
_logger.severe("Could not find products: " +
|
|
_logger.severe("Could not find products: " +
|
|
response.notFoundIDs.toString());
|
|
response.notFoundIDs.toString());
|
|
@@ -569,8 +405,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
_currentSubscription.productID != plan.androidID;
|
|
_currentSubscription.productID != plan.androidID;
|
|
if (isCrossGradingOnAndroid) {
|
|
if (isCrossGradingOnAndroid) {
|
|
final existingProductDetailsResponse =
|
|
final existingProductDetailsResponse =
|
|
- await InAppPurchaseConnection.instance
|
|
|
|
- .queryProductDetails({_currentSubscription.productID});
|
|
|
|
|
|
+ await InAppPurchaseConnection.instance
|
|
|
|
+ .queryProductDetails({_currentSubscription.productID});
|
|
if (existingProductDetailsResponse.notFoundIDs.isNotEmpty) {
|
|
if (existingProductDetailsResponse.notFoundIDs.isNotEmpty) {
|
|
_logger.severe("Could not find existing products: " +
|
|
_logger.severe("Could not find existing products: " +
|
|
response.notFoundIDs.toString());
|
|
response.notFoundIDs.toString());
|
|
@@ -616,49 +452,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
return planWidgets;
|
|
return planWidgets;
|
|
}
|
|
}
|
|
|
|
|
|
- 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: [
|
|
|
|
- Text(
|
|
|
|
- "monthly plans",
|
|
|
|
- style: TextStyle(
|
|
|
|
- color: Theme.of(context)
|
|
|
|
- .buttonColor
|
|
|
|
- .withOpacity(_showYearlyPlan ? 0.2 : 1.0),
|
|
|
|
- ),
|
|
|
|
- ),
|
|
|
|
- Switch(
|
|
|
|
- value: _showYearlyPlan,
|
|
|
|
- onChanged: (value) async {
|
|
|
|
- _showYearlyPlan = value;
|
|
|
|
- await _filterPlansForUI();
|
|
|
|
- setState(() {});
|
|
|
|
- },
|
|
|
|
- ),
|
|
|
|
- Text(
|
|
|
|
- "yearly plans",
|
|
|
|
- style: TextStyle(
|
|
|
|
- color: Theme.of(context)
|
|
|
|
- .buttonColor
|
|
|
|
- .withOpacity(_showYearlyPlan ? 1.0 : 0.2),
|
|
|
|
- ),
|
|
|
|
- ),
|
|
|
|
- ],
|
|
|
|
- ),
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
void _addCurrentPlanWidget(List<Widget> planWidgets) {
|
|
void _addCurrentPlanWidget(List<Widget> planWidgets) {
|
|
- // don't add current plan if it's monthly plan but UI is showing yearly plans
|
|
|
|
- // and vice versa.
|
|
|
|
- if (_showYearlyPlan != _currentSubscription.isYearlyPlan() && _currentSubscription.productID != kFreeProductID) {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
int activePlanIndex = 0;
|
|
int activePlanIndex = 0;
|
|
for (; activePlanIndex < _plans.length; activePlanIndex++) {
|
|
for (; activePlanIndex < _plans.length; activePlanIndex++) {
|
|
if (_plans[activePlanIndex].storage > _currentSubscription.storage) {
|
|
if (_plans[activePlanIndex].storage > _currentSubscription.storage) {
|
|
@@ -681,3 +475,4 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|
);
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|