Bläddra i källkod

Support Stripe subscription renewal and cancellation

Neeraj Gupta 3 år sedan
förälder
incheckning
9175387f7d
3 ändrade filer med 206 tillägg och 54 borttagningar
  1. 38 3
      lib/models/subscription.dart
  2. 40 3
      lib/services/billing_service.dart
  3. 128 48
      lib/ui/payment/subscription_page.dart

+ 38 - 3
lib/models/subscription.dart

@@ -12,6 +12,7 @@ class Subscription {
   final int expiryTime;
   final int expiryTime;
   final String price;
   final String price;
   final String period;
   final String period;
+  final Attributes attributes;
 
 
   Subscription({
   Subscription({
     this.id,
     this.id,
@@ -22,6 +23,7 @@ class Subscription {
     this.expiryTime,
     this.expiryTime,
     this.price,
     this.price,
     this.period,
     this.period,
+    this.attributes,
   });
   });
 
 
   bool isValid() {
   bool isValid() {
@@ -56,7 +58,7 @@ class Subscription {
   }
   }
 
 
   Map<String, dynamic> toMap() {
   Map<String, dynamic> toMap() {
-    return {
+    var map = <String, dynamic>{
       'id': id,
       'id': id,
       'productID': productID,
       'productID': productID,
       'storage': storage,
       'storage': storage,
@@ -66,11 +68,14 @@ class Subscription {
       'price': price,
       'price': price,
       'period': period,
       'period': period,
     };
     };
+    if (attributes != null) {
+      map["attributes"] = attributes.toJson();
+    }
+    return map;
   }
   }
 
 
   factory Subscription.fromMap(Map<String, dynamic> map) {
   factory Subscription.fromMap(Map<String, dynamic> map) {
     if (map == null) return null;
     if (map == null) return null;
-
     return Subscription(
     return Subscription(
       id: map['id'],
       id: map['id'],
       productID: map['productID'],
       productID: map['productID'],
@@ -80,6 +85,9 @@ class Subscription {
       expiryTime: map['expiryTime'],
       expiryTime: map['expiryTime'],
       price: map['price'],
       price: map['price'],
       period: map['period'],
       period: map['period'],
+      attributes: map["attributes"] != null
+          ? Attributes.fromJson(map["attributes"])
+          : null,
     );
     );
   }
   }
 
 
@@ -90,7 +98,7 @@ class Subscription {
 
 
   @override
   @override
   String toString() {
   String toString() {
-    return 'Subscription(id: $id, productID: $productID, storage: $storage, originalTransactionID: $originalTransactionID, paymentProvider: $paymentProvider, expiryTime: $expiryTime, price: $price, period: $period)';
+    return 'Subscription{id: $id, productID: $productID, storage: $storage, originalTransactionID: $originalTransactionID, paymentProvider: $paymentProvider, expiryTime: $expiryTime, price: $price, period: $period, attributes: $attributes}';
   }
   }
 
 
   @override
   @override
@@ -120,3 +128,30 @@ class Subscription {
         period.hashCode;
         period.hashCode;
   }
   }
 }
 }
+
+class Attributes {
+  bool isCancelled;
+  String customerID;
+
+  Attributes({
+    this.isCancelled,
+    this.customerID});
+
+  Attributes.fromJson(dynamic json) {
+    isCancelled = json["isCancelled"];
+    customerID = json["customerID"];
+  }
+
+  Map<String, dynamic> toJson() {
+    var map = <String, dynamic>{};
+    map["isCancelled"] = isCancelled;
+    map["customerID"] = customerID;
+    return map;
+  }
+
+  @override
+  String toString() {
+    return 'Attributes{isCancelled: $isCancelled, customerID: $customerID}';
+  }
+
+}

+ 40 - 3
lib/services/billing_service.dart

@@ -83,7 +83,8 @@ class BillingService {
         ),
         ),
       );
       );
       return Subscription.fromMap(response.data["subscription"]);
       return Subscription.fromMap(response.data["subscription"]);
-    } catch (e) {
+    } catch (e, s) {
+      _logger.severe(e, s);
       rethrow;
       rethrow;
     }
     }
   }
   }
@@ -100,8 +101,44 @@ class BillingService {
       );
       );
       final subscription = Subscription.fromMap(response.data["subscription"]);
       final subscription = Subscription.fromMap(response.data["subscription"]);
       return subscription;
       return subscription;
-    } on DioError catch (e) {
-      _logger.severe(e);
+    } on DioError catch (e, s) {
+      _logger.severe(e, s);
+      rethrow;
+    }
+  }
+
+  Future<Subscription> cancelStripeSubscription() async {
+    try {
+      final response = await _dio.post(
+        _config.getHttpEndpoint() + "/billing/stripe/cancel-subscription",
+        options: Options(
+          headers: {
+            "X-Auth-Token": _config.getToken(),
+          },
+        ),
+      );
+      final subscription = Subscription.fromMap(response.data["subscription"]);
+      return subscription;
+    } on DioError catch (e, s) {
+      _logger.severe(e, s);
+      rethrow;
+    }
+  }
+
+  Future<Subscription> activateStripeSubscription() async {
+    try {
+      final response = await _dio.post(
+        _config.getHttpEndpoint() + "/billing/stripe/activate-subscription",
+        options: Options(
+          headers: {
+            "X-Auth-Token": _config.getToken(),
+          },
+        ),
+      );
+      final subscription = Subscription.fromMap(response.data["subscription"]);
+      return subscription;
+    } on DioError catch (e, s) {
+      _logger.severe(e, s);
       rethrow;
       rethrow;
     }
     }
   }
   }

+ 128 - 48
lib/ui/payment/subscription_page.dart

@@ -43,11 +43,15 @@ 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
   // based on this flag, we would show ente payment page with stripe plans
   bool _isIndependentApk;
   bool _isIndependentApk;
   bool _showYearlyPlan = false;
   bool _showYearlyPlan = false;
@@ -56,7 +60,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
   void initState() {
   void initState() {
     _billingService.setIsOnSubscriptionPage(true);
     _billingService.setIsOnSubscriptionPage(true);
     _isIndependentApk = UpdateService.instance.isIndependentFlavor();
     _isIndependentApk = UpdateService.instance.isIndependentFlavor();
-     _fetchSub();
+    _fetchSub();
     _setupPurchaseUpdateStreamListener();
     _setupPurchaseUpdateStreamListener();
     _dialog = createProgressDialog(context, "please wait...");
     _dialog = createProgressDialog(context, "please wait...");
     super.initState();
     super.initState();
@@ -68,13 +72,16 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
       showToast("Is yearly plan " + _currentSubscription.period);
       showToast("Is yearly plan " + _currentSubscription.period);
       _showYearlyPlan = _currentSubscription.isYearlyPlan();
       _showYearlyPlan = _currentSubscription.isYearlyPlan();
       _hasActiveSubscription = _currentSubscription.isValid();
       _hasActiveSubscription = _currentSubscription.isValid();
+      _isAutoReviewCancelled =
+          _currentSubscription.attributes?.isCancelled ?? false;
       _isActiveStripeSubscriber =
       _isActiveStripeSubscriber =
           _currentSubscription.paymentProvider == kStripe &&
           _currentSubscription.paymentProvider == kStripe &&
               _currentSubscription.isValid();
               _currentSubscription.isValid();
-      _filterPlansForUI();
       _usageFuture = _billingService.fetchUsage();
       _usageFuture = _billingService.fetchUsage();
-      _hasLoadedData = true;
-      setState(() {});
+      return _filterPlansForUI().then((value) {
+        _hasLoadedData = true;
+        setState(() {});
+      });
     });
     });
   }
   }
 
 
@@ -86,13 +93,14 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
       final productID = (_showStripePlans())
       final productID = (_showStripePlans())
           ? plan.stripeID
           ? plan.stripeID
           : Platform.isAndroid
           : Platform.isAndroid
-          ? plan.androidID
-          : plan.iosID;
+              ? plan.androidID
+              : plan.iosID;
       var isYearlyPlan = plan.period == 'year';
       var isYearlyPlan = plan.period == 'year';
       return productID != null &&
       return productID != null &&
           productID.isNotEmpty &&
           productID.isNotEmpty &&
           isYearlyPlan == _showYearlyPlan;
           isYearlyPlan == _showYearlyPlan;
     }).toList();
     }).toList();
+    setState(() {});
   }
   }
 
 
   FutureOr onWebPaymentGoBack(dynamic value) async {
   FutureOr onWebPaymentGoBack(dynamic value) async {
@@ -245,11 +253,15 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     ]);
     ]);
 
 
     if (_hasActiveSubscription) {
     if (_hasActiveSubscription) {
+      var endDate = getDateAndMonthAndYear(
+          DateTime.fromMicrosecondsSinceEpoch(_currentSubscription.expiryTime));
+      var message = "valid till " + endDate;
+      if (_isAutoReviewCancelled) {
+        message = "your subscription will be cancelled on $endDate";
+      }
       widgets.add(
       widgets.add(
         Text(
         Text(
-          "valid till " +
-              getDateAndMonthAndYear(DateTime.fromMicrosecondsSinceEpoch(
-                  _currentSubscription.expiryTime)),
+          message,
           style: TextStyle(
           style: TextStyle(
             color: Colors.white.withOpacity(0.6),
             color: Colors.white.withOpacity(0.6),
             fontSize: 14,
             fontSize: 14,
@@ -257,10 +269,17 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
         ),
         ),
       );
       );
     }
     }
+
     if (_showStripePlans()) {
     if (_showStripePlans()) {
       widgets.add(_showSubscriptionToggle());
       widgets.add(_showSubscriptionToggle());
     }
     }
 
 
+    if (_isIndependentApk &&
+        _hasActiveSubscription &&
+        _isActiveStripeSubscriber) {
+      widgets.add(_stripeSubscriptionToggleButton(_isAutoReviewCancelled));
+    }
+
     if (_hasActiveSubscription &&
     if (_hasActiveSubscription &&
         _currentSubscription.productID != kFreeProductID) {
         _currentSubscription.productID != kFreeProductID) {
       widgets.addAll([
       widgets.addAll([
@@ -269,6 +288,9 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
           child: GestureDetector(
           child: GestureDetector(
             onTap: () {
             onTap: () {
               if (_isActiveStripeSubscriber) {
               if (_isActiveStripeSubscriber) {
+                if(_isIndependentApk) {
+
+                }
                 return;
                 return;
               }
               }
               if (Platform.isAndroid) {
               if (Platform.isAndroid) {
@@ -350,6 +372,65 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     );
     );
   }
   }
 
 
+  Widget _stripeSubscriptionToggleButton(bool isCurrentlyCancelled) {
+    return TextButton(
+      child: Text(
+        isCurrentlyCancelled ? "renew subscription" : "cancel subscription",
+        style: TextStyle(
+          color: isCurrentlyCancelled ? Colors.greenAccent : Colors.redAccent,
+        ),
+      ),
+      onPressed: () {
+        showDialog(
+            context: context,
+            builder: (BuildContext context) {
+              return AlertDialog(
+                  title: Text(isCurrentlyCancelled
+                      ? 'confirm subscription renewal'
+                      : 'confirm subscription cancellation'),
+                  content: Text(isCurrentlyCancelled
+                      ? 'are you sure you want to renew>'
+                      : 'are you sure you want to cancel?'),
+                  actions: <Widget>[
+                    TextButton(
+                        child: Text(
+                          'yes',
+                          style: TextStyle(
+                            color: isCurrentlyCancelled
+                                ? Theme.of(context).buttonColor
+                                : Theme.of(context).errorColor,
+                          ),
+                        ),
+                        onPressed: () async {
+                          Navigator.of(context).pop('dialog');
+                          _dialog.show();
+                          if (isCurrentlyCancelled) {
+                            await _billingService.activateStripeSubscription();
+                          } else {
+                            await _billingService.cancelStripeSubscription();
+                          }
+                          await _fetchSub();
+                          _dialog.hide();
+                        }),
+                    TextButton(
+                        child: Text(
+                          'cancel',
+                          style: TextStyle(
+                            color: isCurrentlyCancelled
+                                ? Theme.of(context).errorColor
+                                : Theme.of(context).buttonColor,
+                          ),
+                        ),
+                        onPressed: () => {
+                              Navigator.of(context, rootNavigator: true)
+                                  .pop('dialog')
+                            }),
+                  ]);
+            });
+      },
+    );
+  }
+
   List<Widget> _getStripePlanWidgets() {
   List<Widget> _getStripePlanWidgets() {
     final List<Widget> planWidgets = [];
     final List<Widget> planWidgets = [];
     bool foundActivePlan = false;
     bool foundActivePlan = false;
@@ -370,6 +451,11 @@ 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();
               await _dialog.show();
               if (_usageFuture != null) {
               if (_usageFuture != null) {
                 final usage = await _usageFuture;
                 final usage = await _usageFuture;
@@ -380,47 +466,43 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
                   return;
                   return;
                 }
                 }
               }
               }
-              if (_isActiveStripeSubscriber && !_isIndependentApk) {
-                showErrorDialog(context, "sorry",
-                    "please visit web.ente.io to manage your subscription");
-                return;
-              }
-
               if (_isActiveStripeSubscriber) {
               if (_isActiveStripeSubscriber) {
                 // check if user really wants to change his plan plan
                 // check if user really wants to change his plan plan
-                  showDialog(context: context,
-                      builder: (BuildContext context)  {
-                    return AlertDialog(
-                            title: Text( 'confirm plan change'),
-                            content: Text("are you sure you want to change your plan?"),
-                            actions: <Widget>[
-                              TextButton(
-                                  child: Text('yes',
-                                    style: TextStyle(
+                showDialog(
+                    context: context,
+                    builder: (BuildContext context) {
+                      return AlertDialog(
+                          title: Text('confirm plan change'),
+                          content: Text(
+                              "are you sure you want to change your plan?"),
+                          actions: <Widget>[
+                            TextButton(
+                                child: Text(
+                                  'yes',
+                                  style: TextStyle(
                                     color: Theme.of(context).buttonColor,
                                     color: Theme.of(context).buttonColor,
                                   ),
                                   ),
                                 ),
                                 ),
                                 onPressed: () {
                                 onPressed: () {
-                                    Navigator.of(context).pop('dialog');
-                                    Navigator.of(context).push(
-                                      MaterialPageRoute(
-                                        builder: (BuildContext context) {
-                                          return PaymentWebPage(
-                                              planId: plan.stripeID,
-                                              actionType: "update");
-                                        },
-                                      ),
-                                    ).then((value) => onWebPaymentGoBack(value)); }
-                              ),
-                              TextButton(
-                                  child: Text('cancel'),
-                                  onPressed: () => {
-                                    Navigator.of(context,
-                                        rootNavigator: true)
-                                        .pop('dialog')
-                                  }),
-                            ]);
-                  });
+                                  Navigator.of(context).pop('dialog');
+                                  Navigator.of(context).push(
+                                    MaterialPageRoute(
+                                      builder: (BuildContext context) {
+                                        return PaymentWebPage(
+                                            planId: plan.stripeID,
+                                            actionType: "update");
+                                      },
+                                    ),
+                                  ).then((value) => onWebPaymentGoBack(value));
+                                }),
+                            TextButton(
+                                child: Text('cancel'),
+                                onPressed: () => {
+                                      Navigator.of(context, rootNavigator: true)
+                                          .pop('dialog')
+                                    }),
+                          ]);
+                    });
               } else {
               } else {
                 Navigator.of(context).push(
                 Navigator.of(context).push(
                   MaterialPageRoute(
                   MaterialPageRoute(
@@ -558,9 +640,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
       child: Row(
       child: Row(
         mainAxisAlignment: MainAxisAlignment.spaceAround,
         mainAxisAlignment: MainAxisAlignment.spaceAround,
         children: [
         children: [
-          _showYearlyPlan
-              ? Text("yearly plans")
-              : Text("monthly plans"),
+          _showYearlyPlan ? Text("yearly plans") : Text("monthly plans"),
           Switch(
           Switch(
             value: _showYearlyPlan,
             value: _showYearlyPlan,
             onChanged: (value) async {
             onChanged: (value) async {