From 243db3144248c3b88bf461d6ee4c3855daf7d21d Mon Sep 17 00:00:00 2001 From: Uncled1023 Date: Fri, 19 Nov 2021 16:14:58 -0800 Subject: [PATCH] Added bettter UX for managing subscription --- Configuration/BillingConfig.cs | 4 ++ Teknik/App_Data/endpointMappings.json | 11 ++++ .../Billing/Controllers/BillingController.cs | 51 ++++--------------- .../Areas/User/Controllers/UserController.cs | 26 ++++++++++ .../User/Settings/BillingSettings.cshtml | 37 +++++++++----- Teknik/Scripts/.eslintrc | 1 + Teknik/Scripts/User/BillingSettings.js | 43 ++++++++++++++++ Teknik/Scripts/common.js | 8 ++- Teknik/bundleconfig.json | 6 +++ 9 files changed, 130 insertions(+), 57 deletions(-) create mode 100644 Teknik/Scripts/User/BillingSettings.js diff --git a/Configuration/BillingConfig.cs b/Configuration/BillingConfig.cs index 67c3ce7..b2cc6ae 100644 --- a/Configuration/BillingConfig.cs +++ b/Configuration/BillingConfig.cs @@ -8,10 +8,12 @@ namespace Teknik.Configuration { public class BillingConfig { + public bool Enabled { get; set; } public BillingType Type { get; set; } public string StripePublishApiKey { get; set; } public string StripeSecretApiKey { get; set; } public string StripeCheckoutWebhookSecret { get; set; } + public string StripeSubscriptionWebhookSecret { get; set; } public string StripeCustomerWebhookSecret { get; set; } public string UploadProductId { get; set; } @@ -19,10 +21,12 @@ namespace Teknik.Configuration public BillingConfig() { + Enabled = false; Type = BillingType.Stripe; StripePublishApiKey = null; StripeSecretApiKey = null; StripeCheckoutWebhookSecret = null; + StripeSubscriptionWebhookSecret = null; StripeCustomerWebhookSecret = null; } } diff --git a/Teknik/App_Data/endpointMappings.json b/Teknik/App_Data/endpointMappings.json index db8a90e..60cc1e1 100644 --- a/Teknik/App_Data/endpointMappings.json +++ b/Teknik/App_Data/endpointMappings.json @@ -154,6 +154,17 @@ "action": "HandleSubscriptionChange" } }, + { + "Name": "API.v1.HandleCustomerDeletion", + "HostTypes": [ "Full" ], + "SubDomains": [ "api" ], + "Pattern": "v1/Billing/HandleCustomerDeletion", + "Area": "API", + "Defaults": { + "controller": "BillingAPIv1", + "action": "HandleCustomerDeletion" + } + }, { "Name": "API.v1.Claims", "HostTypes": [ "Full" ], diff --git a/Teknik/Areas/Billing/Controllers/BillingController.cs b/Teknik/Areas/Billing/Controllers/BillingController.cs index 81c0697..e9646e1 100644 --- a/Teknik/Areas/Billing/Controllers/BillingController.cs +++ b/Teknik/Areas/Billing/Controllers/BillingController.cs @@ -183,21 +183,7 @@ namespace Teknik.Areas.Billing.Controllers if (user == null) throw new UnauthorizedAccessException(); - if (user.BillingCustomer == null) - { - var custId = billingService.CreateCustomer(user.Username, null); - var customer = new Customer() - { - CustomerId = custId, - User = user - }; - _dbContext.Customers.Add(customer); - user.BillingCustomer = customer; - _dbContext.Entry(user).State = EntityState.Modified; - _dbContext.SaveChanges(); - } - - var session = billingService.CreateCheckoutSession(user.BillingCustomer.CustomerId, + var session = billingService.CreateCheckoutSession(user.BillingCustomer?.CustomerId, priceId, Url.SubRouteUrl("billing", "Billing.CheckoutComplete", new { productId = price.ProductId }), Url.SubRouteUrl("billing", "Billing.Subscriptions")); @@ -211,6 +197,15 @@ namespace Teknik.Areas.Billing.Controllers var checkoutSession = billingService.GetCheckoutSession(session_id); if (checkoutSession != null) { + User user = UserHelper.GetUser(_dbContext, User.Identity.Name); + if (user == null) + throw new UnauthorizedAccessException(); + + if (user.BillingCustomer == null) + { + BillingHelper.CreateCustomer(_dbContext, user, checkoutSession.CustomerId); + } + var subscription = billingService.GetSubscription(checkoutSession.SubscriptionId); if (subscription != null) { @@ -260,32 +255,6 @@ namespace Teknik.Areas.Billing.Controllers return Redirect(Url.SubRouteUrl("billing", "Billing.ViewSubscriptions")); } - public IActionResult CancelSubscription(string subscriptionId, string productId) - { - // Get Subscription Info - var billingService = BillingFactory.GetBillingService(_config.BillingConfig); - - var subscription = billingService.GetSubscription(subscriptionId); - if (subscription == null) - throw new ArgumentException("Invalid Subscription Id", "subscriptionId"); - - if (!subscription.Prices.Exists(p => p.ProductId == productId)) - throw new ArgumentException("Subscription does not relate to product", "productId"); - - var product = billingService.GetProduct(productId); - if (product == null) - throw new ArgumentException("Product does not exist", "productId"); - - var result = billingService.CancelSubscription(subscriptionId); - - var vm = new CancelSubscriptionViewModel() - { - ProductName = product.Name - }; - - return View(vm); - } - public IActionResult SubscriptionSuccess(string priceId) { var vm = new SubscriptionSuccessViewModel(); diff --git a/Teknik/Areas/User/Controllers/UserController.cs b/Teknik/Areas/User/Controllers/UserController.cs index 50ba236..8214a87 100644 --- a/Teknik/Areas/User/Controllers/UserController.cs +++ b/Teknik/Areas/User/Controllers/UserController.cs @@ -1493,5 +1493,31 @@ namespace Teknik.Areas.Users.Controllers } return Json(new { error = "Invalid Type" }); } + + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult CancelSubscription(string subscriptionId, string productId) + { + // Get Subscription Info + var billingService = BillingFactory.GetBillingService(_config.BillingConfig); + + var subscription = billingService.GetSubscription(subscriptionId); + if (subscription == null) + return Json(new { error = "Invalid Subscription Id" }); + + if (!subscription.Prices.Exists(p => p.ProductId == productId)) + return Json(new { error = "Subscription does not relate to product" }); + + var product = billingService.GetProduct(productId); + if (product == null) + return Json(new { error = "Product does not exist" }); + + var result = billingService.CancelSubscription(subscriptionId); + + if (result) + return Json(new { result = true }); + + return Json(new { error = "Unable to cancel subscription" }); + } } } diff --git a/Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml b/Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml index d940614..e3a1756 100644 --- a/Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml +++ b/Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml @@ -6,18 +6,24 @@ Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml"; } -
-
-

Billing Information

-
-
-
-
-
- Click here to view/modify your billing information and invoices. -
-
+ +@if (!string.IsNullOrEmpty(Model.PortalUrl)) +{ +
+
+

Billing Information

+
+
+
+
+
+ Click here to view/modify your billing information and invoices. +
+
+}

Active Subscriptions

@@ -32,9 +38,10 @@ { foreach (var subscription in Model.Subscriptions) { -
  • -

    @subscription.ProductName: @(StringHelper.GetBytesReadable(subscription.Storage)) for @($"${subscription.Price:0.00} / {subscription.Interval}")

    -

    Cancel Subscription

    +
  • +

    @subscription.ProductName: @(StringHelper.GetBytesReadable(subscription.Storage)) for @($"${subscription.Price:0.00} / {subscription.Interval}")

    + +
  • } } @@ -46,3 +53,5 @@
    + + diff --git a/Teknik/Scripts/.eslintrc b/Teknik/Scripts/.eslintrc index be4f680..32fd0d2 100644 --- a/Teknik/Scripts/.eslintrc +++ b/Teknik/Scripts/.eslintrc @@ -31,6 +31,7 @@ "Oidc": "readonly", // Common.js Functions + "confirmDialog": "readonly", "deleteConfirm": "readonly", "disableButton": "readonly", "enableButton": "readonly", diff --git a/Teknik/Scripts/User/BillingSettings.js b/Teknik/Scripts/User/BillingSettings.js new file mode 100644 index 0000000..881efe8 --- /dev/null +++ b/Teknik/Scripts/User/BillingSettings.js @@ -0,0 +1,43 @@ +/* globals cancelSubscriptionURL */ +$(document).ready(function () { + $('.cancel-subscription-button').click(function () { + disableButton('#cancel_subscription', 'Canceling Subscription...'); + + var subscriptionId = $(this).data('subscription-id'); + var productId = $(this).data('product-id'); + var element = $('#activeSubscriptionList [id="' + subscriptionId + '"'); + + confirmDialog('Confirm', 'Back', 'Are you sure you want to cancel your subscription?', function (result) { + if (result) { + $.ajax({ + type: "POST", + url: cancelSubscriptionURL, + data: AddAntiForgeryToken({ subscriptionId: subscriptionId, productId: productId }), + headers: { + 'X-Requested-With': 'XMLHttpRequest' + }, + xhrFields: { + withCredentials: true + }, + success: function (response) { + if (response.result) { + element.remove(); + $("#top_msg").css('display', 'inline', 'important'); + $("#top_msg").html('
    Subscription successfully canceled.
    '); + } + else { + $("#top_msg").css('display', 'inline', 'important'); + $("#top_msg").html('
    ' + parseErrorMessage(response) + '
    '); + } + }, + error: function (response) { + $("#top_msg").css('display', 'inline', 'important'); + $("#top_msg").html('
    ' + parseErrorMessage(response.responseText) + '
    '); + } + }); + } else { + enableButton('#cancel_subscription', 'Cancel Subscription'); + } + }); + }); +}); \ No newline at end of file diff --git a/Teknik/Scripts/common.js b/Teknik/Scripts/common.js index c507996..553be7f 100644 --- a/Teknik/Scripts/common.js +++ b/Teknik/Scripts/common.js @@ -117,15 +117,19 @@ $(function () { }); function deleteConfirm(message, callback) { + confirmDialog('Delete', 'Cancel', message, callback); +} + +function confirmDialog(confirmLabel, cancelLabel, message, callback) { bootbox.confirm({ message: "

    " + message + "

    ", buttons: { confirm: { - label: 'Delete', + label: confirmLabel, className: 'btn-danger' }, cancel: { - label: 'Cancel', + label: cancelLabel, className: 'btn-default' } }, diff --git a/Teknik/bundleconfig.json b/Teknik/bundleconfig.json index b3bd304..8c04a4f 100644 --- a/Teknik/bundleconfig.json +++ b/Teknik/bundleconfig.json @@ -260,6 +260,12 @@ "./wwwroot/js/app/User/AccountSettings.js" ] }, + { + "outputFileName": "./wwwroot/js/user.settings.billing.min.js", + "inputFiles": [ + "./wwwroot/js/app/User/BillingSettings.js" + ] + }, { "outputFileName": "./wwwroot/js/user.settings.security.min.js", "inputFiles": [