Browse Source

Added bettter UX for managing subscription

tags/5.1.0
Teknikode 2 weeks ago
parent
commit
243db31442

+ 4
- 0
Configuration/BillingConfig.cs View File

@@ -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;
}
}

+ 11
- 0
Teknik/App_Data/endpointMappings.json View File

@@ -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" ],

+ 10
- 41
Teknik/Areas/Billing/Controllers/BillingController.cs View File

@@ -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();

+ 26
- 0
Teknik/Areas/User/Controllers/UserController.cs View File

@@ -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" });
}
}
}

+ 22
- 13
Teknik/Areas/User/Views/User/Settings/BillingSettings.cshtml View File

@@ -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 @@
{
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 @@
</div>
</div>
</div>

<bundle src="js/user.settings.billing.min.js" append-version="true"></bundle>

+ 1
- 0
Teknik/Scripts/.eslintrc View File

@@ -31,6 +31,7 @@
"Oidc": "readonly",

// Common.js Functions
"confirmDialog": "readonly",
"deleteConfirm": "readonly",
"disableButton": "readonly",
"enableButton": "readonly",

+ 43
- 0
Teknik/Scripts/User/BillingSettings.js 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');
}
});
});
});

+ 6
- 2
Teknik/Scripts/common.js View File

@@ -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
- 0
Teknik/bundleconfig.json View File

@@ -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