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:
parent
ed9a94cba1
commit
243db31442
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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" ],
|
||||||
|
@ -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();
|
||||||
|
@ -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" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,12 @@
|
|||||||
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
|
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var cancelSubscriptionURL = '@Url.SubRouteUrl("billing", "User.Action", new { action = "CancelSubscription" })';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Model.PortalUrl))
|
||||||
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<h2>Billing Information</h2>
|
<h2>Billing Information</h2>
|
||||||
@ -14,10 +20,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<a href="@Model.PortalUrl">Click here</a> to view/modify your billing information and invoices.</>
|
<a href="@Model.PortalUrl">Click here</a> to view/modify your billing information and invoices.
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
@ -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",
|
||||||
|
43
Teknik/Scripts/User/BillingSettings.js
Normal file
43
Teknik/Scripts/User/BillingSettings.js
Normal 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">×</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">×</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">×</button>' + parseErrorMessage(response.responseText) + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
enableButton('#cancel_subscription', 'Cancel Subscription');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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": [
|
||||||
|
Loading…
Reference in New Issue
Block a user