Browse Source

Added bettter UX for managing subscription

feature/billing
Teknikode 8 months ago
parent
commit
243db31442
  1. 4
      Configuration/BillingConfig.cs
  2. 11
      Teknik/App_Data/endpointMappings.json
  3. 51
      Teknik/Areas/Billing/Controllers/BillingController.cs
  4. 26
      Teknik/Areas/User/Controllers/UserController.cs
  5. 35
      Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml
  6. 1
      Teknik/Scripts/.eslintrc
  7. 43
      Teknik/Scripts/User/BillingSettings.js
  8. 8
      Teknik/Scripts/common.js
  9. 6
      Teknik/bundleconfig.json

4
Configuration/BillingConfig.cs

@ -8,10 +8,12 @@ namespace Teknik.Configuration @@ -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 @@ -19,10 +21,12 @@ namespace Teknik.Configuration
public BillingConfig()
{
Enabled = false;
Type = BillingType.Stripe;
StripePublishApiKey = null;
StripeSecretApiKey = null;
StripeCheckoutWebhookSecret = null;
StripeSubscriptionWebhookSecret = null;
StripeCustomerWebhookSecret = null;
}
}

11
Teknik/App_Data/endpointMappings.json

@ -154,6 +154,17 @@ @@ -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" ],

51
Teknik/Areas/Billing/Controllers/BillingController.cs

@ -183,21 +183,7 @@ namespace Teknik.Areas.Billing.Controllers @@ -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 @@ -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 @@ -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();

26
Teknik/Areas/User/Controllers/UserController.cs

@ -1493,5 +1493,31 @@ namespace Teknik.Areas.Users.Controllers @@ -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" });
}
}
}

35
Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml

@ -6,18 +6,24 @@ @@ -6,18 +6,24 @@
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
}
<div class="row">
<div class="col-sm-12">
<h2>Billing Information</h2>
<hr />
<script>
var cancelSubscriptionURL = '@Url.SubRouteUrl("billing", "User.Action", new { action = "CancelSubscription" })';
</script>
@if (!string.IsNullOrEmpty(Model.PortalUrl))
{
<div class="row">
<div class="col-sm-12">
<h2>Billing Information</h2>
<hr />
</div>
</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 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>
}
<div class="row">
<div class="col-sm-12">
<h2>Active Subscriptions</h2>
@ -32,9 +38,10 @@ @@ -32,9 +38,10 @@
{
foreach (var subscription in Model.Subscriptions)
{
<li class="list-group-item">
<h4 class="list-group-item-heading">@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>
<li class="list-group-item" id="@subscription.SubscriptionId">
<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>
<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>
}
}
@ -46,3 +53,5 @@ @@ -46,3 +53,5 @@
</div>
</div>
</div>
<bundle src="js/user.settings.billing.min.js" append-version="true"></bundle>

1
Teknik/Scripts/.eslintrc

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

43
Teknik/Scripts/User/BillingSettings.js

@ -0,0 +1,43 @@ @@ -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');
}
});
});
});

8
Teknik/Scripts/common.js

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

6
Teknik/bundleconfig.json

@ -260,6 +260,12 @@ @@ -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": [

Loading…
Cancel
Save