1
0
mirror of https://git.teknik.io/Teknikode/Teknik.git synced 2023-08-02 14:16:22 +02:00

Added bettter UX for managing subscription

This commit is contained in:
Uncled1023 2021-11-19 16:14:58 -08:00
parent ed9a94cba1
commit 243db31442
9 changed files with 130 additions and 57 deletions

View File

@ -8,10 +8,12 @@ namespace Teknik.Configuration
{ {
public class BillingConfig public class BillingConfig
{ {
public bool Enabled { get; set; }
public BillingType Type { get; set; } public BillingType Type { get; set; }
public string StripePublishApiKey { get; set; } public string StripePublishApiKey { get; set; }
public string StripeSecretApiKey { get; set; } public string StripeSecretApiKey { get; set; }
public string StripeCheckoutWebhookSecret { get; set; } public string StripeCheckoutWebhookSecret { get; set; }
public string StripeSubscriptionWebhookSecret { get; set; }
public string StripeCustomerWebhookSecret { get; set; } public string StripeCustomerWebhookSecret { get; set; }
public string UploadProductId { get; set; } public string UploadProductId { get; set; }
@ -19,10 +21,12 @@ namespace Teknik.Configuration
public BillingConfig() public BillingConfig()
{ {
Enabled = false;
Type = BillingType.Stripe; Type = BillingType.Stripe;
StripePublishApiKey = null; StripePublishApiKey = null;
StripeSecretApiKey = null; StripeSecretApiKey = null;
StripeCheckoutWebhookSecret = null; StripeCheckoutWebhookSecret = null;
StripeSubscriptionWebhookSecret = null;
StripeCustomerWebhookSecret = null; StripeCustomerWebhookSecret = null;
} }
} }

View File

@ -154,6 +154,17 @@
"action": "HandleSubscriptionChange" "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", "Name": "API.v1.Claims",
"HostTypes": [ "Full" ], "HostTypes": [ "Full" ],

View File

@ -183,21 +183,7 @@ namespace Teknik.Areas.Billing.Controllers
if (user == null) if (user == null)
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
if (user.BillingCustomer == null) var session = billingService.CreateCheckoutSession(user.BillingCustomer?.CustomerId,
{
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,
priceId, priceId,
Url.SubRouteUrl("billing", "Billing.CheckoutComplete", new { productId = price.ProductId }), Url.SubRouteUrl("billing", "Billing.CheckoutComplete", new { productId = price.ProductId }),
Url.SubRouteUrl("billing", "Billing.Subscriptions")); Url.SubRouteUrl("billing", "Billing.Subscriptions"));
@ -211,6 +197,15 @@ namespace Teknik.Areas.Billing.Controllers
var checkoutSession = billingService.GetCheckoutSession(session_id); var checkoutSession = billingService.GetCheckoutSession(session_id);
if (checkoutSession != null) 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); var subscription = billingService.GetSubscription(checkoutSession.SubscriptionId);
if (subscription != null) if (subscription != null)
{ {
@ -260,32 +255,6 @@ namespace Teknik.Areas.Billing.Controllers
return Redirect(Url.SubRouteUrl("billing", "Billing.ViewSubscriptions")); 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) public IActionResult SubscriptionSuccess(string priceId)
{ {
var vm = new SubscriptionSuccessViewModel(); var vm = new SubscriptionSuccessViewModel();

View File

@ -1493,5 +1493,31 @@ namespace Teknik.Areas.Users.Controllers
} }
return Json(new { error = "Invalid Type" }); 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" });
}
} }
} }

View File

@ -6,18 +6,24 @@
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml"; Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
} }
<div class="row"> <script>
<div class="col-sm-12"> var cancelSubscriptionURL = '@Url.SubRouteUrl("billing", "User.Action", new { action = "CancelSubscription" })';
<h2>Billing Information</h2> </script>
<hr />
</div>
</div>
<div class="row">
<div class="form-group col-sm-12">
<a href="@Model.PortalUrl">Click here</a> to view/modify your billing information and invoices.</>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.PortalUrl))
{
<div class="row">
<div class="col-sm-12">
<h2>Billing Information</h2>
<hr />
</div>
</div>
<div class="row">
<div class="form-group col-sm-12">
<a href="@Model.PortalUrl">Click here</a> to view/modify your billing information and invoices.
</div>
</div>
}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<h2>Active Subscriptions</h2> <h2>Active Subscriptions</h2>
@ -32,9 +38,10 @@
{ {
foreach (var subscription in Model.Subscriptions) foreach (var subscription in Model.Subscriptions)
{ {
<li class="list-group-item"> <li class="list-group-item" id="@subscription.SubscriptionId">
<h4 class="list-group-item-heading">@subscription.ProductName: <strong>@(StringHelper.GetBytesReadable(subscription.Storage))</strong> for <strong>@($"${subscription.Price:0.00} / {subscription.Interval}")</strong></h4> <h4 class="list-group-item-heading pull-left">@subscription.ProductName: <strong>@(StringHelper.GetBytesReadable(subscription.Storage))</strong> for <strong>@($"${subscription.Price:0.00} / {subscription.Interval}")</strong></h4>
<p><a href="@(Url.SubRouteUrl("billing", "Billing.CancelSubscription", new { subscriptionId = subscription.SubscriptionId, productId = subscription.ProductId }))">Cancel Subscription</a></p> <button role="button" class="btn btn-info cancel-subscription-button pull-right" data-subscription-id="@subscription.SubscriptionId" data-product-id="@subscription.ProductId">Cancel Subscription</button>
<div class="clearfix"></div>
</li> </li>
} }
} }
@ -46,3 +53,5 @@
</div> </div>
</div> </div>
</div> </div>
<bundle src="js/user.settings.billing.min.js" append-version="true"></bundle>

View File

@ -31,6 +31,7 @@
"Oidc": "readonly", "Oidc": "readonly",
// Common.js Functions // Common.js Functions
"confirmDialog": "readonly",
"deleteConfirm": "readonly", "deleteConfirm": "readonly",
"disableButton": "readonly", "disableButton": "readonly",
"enableButton": "readonly", "enableButton": "readonly",

View File

@ -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('<div class="alert alert-success alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>Subscription successfully canceled.</div>');
}
else {
$("#top_msg").css('display', 'inline', 'important');
$("#top_msg").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + parseErrorMessage(response) + '</div>');
}
},
error: function (response) {
$("#top_msg").css('display', 'inline', 'important');
$("#top_msg").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + parseErrorMessage(response.responseText) + '</div>');
}
});
} else {
enableButton('#cancel_subscription', 'Cancel Subscription');
}
});
});
});

View File

@ -117,15 +117,19 @@ $(function () {
}); });
function deleteConfirm(message, callback) { function deleteConfirm(message, callback) {
confirmDialog('Delete', 'Cancel', message, callback);
}
function confirmDialog(confirmLabel, cancelLabel, message, callback) {
bootbox.confirm({ bootbox.confirm({
message: "<h3>" + message + "</h3>", message: "<h3>" + message + "</h3>",
buttons: { buttons: {
confirm: { confirm: {
label: 'Delete', label: confirmLabel,
className: 'btn-danger' className: 'btn-danger'
}, },
cancel: { cancel: {
label: 'Cancel', label: cancelLabel,
className: 'btn-default' className: 'btn-default'
} }
}, },

View File

@ -260,6 +260,12 @@
"./wwwroot/js/app/User/AccountSettings.js" "./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", "outputFileName": "./wwwroot/js/user.settings.security.min.js",
"inputFiles": [ "inputFiles": [