diff --git a/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs b/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs index b903341b..b1c3ee0e 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs @@ -687,6 +687,9 @@ public async Task CancelAddon(int addonTypeId) [Authorize(Policy = ResgridResources.Department_Update)] public async Task GetStripeSession(int id, int count, string discountCode = null, CancellationToken cancellationToken = default) { + if (count < 1 || count > 200) + return BadRequest("Invalid entity pack count."); + var plan = await _subscriptionsService.GetPlanByIdAsync(id); var stripeCustomerId = await _departmentSettingsService.GetStripeCustomerIdForDepartmentAsync(DepartmentId); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); @@ -725,6 +728,9 @@ public async Task GetStripeUpdate() [Authorize(Policy = ResgridResources.Department_Update)] public async Task GetPaddleCheckout(int id, int count, string discountCode = null, CancellationToken cancellationToken = default) { + if (count < 1 || count > 200) + return BadRequest("Invalid entity pack count."); + var plan = await _subscriptionsService.GetPlanByIdAsync(id); var paddleCustomerId = await _departmentSettingsService.GetPaddleCustomerIdForDepartmentAsync(DepartmentId); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); diff --git a/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml b/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml index 4ce641c7..e0743208 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml @@ -107,17 +107,6 @@ - diff --git a/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml index 4d1fdc4b..2fa1b859 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml @@ -731,7 +731,7 @@ { tier: 0, marginalUserSlots: 5, costPerUser: 200.0 }, { tier: 1, marginalUserSlots: 100, costPerUser: 20.0 }, { tier: 2, marginalUserSlots: 1000, costPerUser: 15.0 }, - { tier: 3, marginalUserSlots: 1000, costPerUser: 10.0 }, + { tier: 3, marginalUserSlots: 5000, costPerUser: 10.0 }, { tier: 4, marginalUserSlots: 999999999, costPerUser: 5.0 }, ]; diff --git a/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml index fda41ebb..a4c6ae52 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml @@ -6,74 +6,69 @@ @section Styles { - - } @@ -100,67 +95,56 @@ Free tier signups are not available on this Resgrid instance. Please select a plan below to complete your registration. -

Move the slider below to select the number of Entities (Users + Units) you require. Your first 10 entities are included at no charge — each additional pack of 10 entities is billed at the rate shown. Select Buy Yearly or Buy Monthly to proceed to the Stripe checkout page.

+

Select the number of Entities (Users + Units) you require using the slider or text box below. Your first 10 entities are included at no charge — each additional pack of 10 entities is billed at the rate shown. Select Buy Yearly or Buy Monthly to proceed to checkout.

-
-
-

Entities

- Users or Units sold in packs of 10 -
-
-
- - - -
-
-
+
+

+ Users or Units sold in packs of 10 +
+ +
+
+ + Entities
+
-
-
-
- Entities -
+
+
+
+ Monthly ($): +
+
+ 0.00
-
- - Monthly billing amount -
- -

- .00 -
+
+
+
+ Yearly ($): +
+
+ 0.00
-
- - Yearly (annual) billing amount -
- -

- .00 -
+
+
+
+
+ @if (Model.IsPaddleDepartment) { + + } else { + + }
-
-
-
- @if (Model.IsPaddleDepartment) { - Buy Yearly - } else { - Buy Yearly - } -
-
- @if (Model.IsPaddleDepartment) { - Buy Monthly - } else { - Buy Monthly - } -
+
+ @if (Model.IsPaddleDepartment) { + + } else { + + }
- +
@if (!string.IsNullOrWhiteSpace(Model.DiscountCode)) @@ -196,53 +180,41 @@ @if (!Model.IsPaddleDepartment) { var stripe = Stripe('@Model.StripeKey'); } - var slider = 0; - var val = 20; var discountCode = '@Html.Raw(Model.DiscountCode ?? "")'; function stripeCheckout(id) { - const amount = slider == 1 ? val : $("#amount").val(); + var amount = parseInt(document.getElementById('amount-input').value) || 0; - if (amount && amount > 10) { - const packs = (amount / 10) - 1; + if (amount > 10) { + var packs = (amount / 10) - 1; $.ajax({ url: resgrid.absoluteBaseUrl + '/User/Subscription/GetStripeSession?id=' + id + '&count=' + packs + (discountCode ? '&discountCode=' + encodeURIComponent(discountCode) : ''), contentType: 'application/json', type: 'GET' }).done(function (data) { - if (data) { - if (data.SessionId) { - stripe.redirectToCheckout({ - sessionId: data.SessionId - }).then(function (result) { - swal({ - title: "Purchase Error", - text: "Error redirecting to Stripe for checkout. Stripe error: " + result.error.message, - icon: "error", - buttons: true, - dangerMode: false - }); - }); - } + if (data && data.SessionId) { + stripe.redirectToCheckout({ sessionId: data.SessionId }).then(function (result) { + if (result.error) { + swal({ title: "Purchase Error", text: "Error redirecting to Stripe: " + result.error.message, icon: "error", buttons: true, dangerMode: false }); + } + }); + } else { + swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false }); } + }).fail(function () { + swal({ title: "Connection Error", text: "Unable to reach the server. Please check your connection and try again.", icon: "error", buttons: true, dangerMode: false }); }); } else { - swal({ - title: "Cannot Purchase", - text: "Please select more than 10 entities to purchase a plan.", - icon: "warning", - buttons: true, - dangerMode: false - }); + swal({ title: "Cannot Purchase", text: "Please select more than 10 entities to purchase a plan.", icon: "warning", buttons: true, dangerMode: false }); } } function paddleCheckout(id) { - const amount = slider == 1 ? val : $("#amount").val(); + var amount = parseInt(document.getElementById('amount-input').value) || 0; - if (amount && amount > 10) { - const packs = (amount / 10) - 1; + if (amount > 10) { + var packs = (amount / 10) - 1; $.ajax({ url: resgrid.absoluteBaseUrl + '/User/Subscription/GetPaddleCheckout?id=' + id + '&count=' + packs + (discountCode ? '&discountCode=' + encodeURIComponent(discountCode) : ''), @@ -251,24 +223,18 @@ }).done(function (data) { if (data) { if (data.HasActiveSub) { - swal({ - title: "Active Subscription", - text: "You already have an active subscription. Please manage your existing subscription instead.", - icon: "warning", - buttons: true, - dangerMode: false - }); + swal({ title: "Active Subscription", text: "You already have an active subscription. Please manage your existing subscription instead.", icon: "warning", buttons: true, dangerMode: false }); + return; + } + + if (!data.PriceId) { + swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false }); return; } var checkoutSettings = { - settings: { - successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id - }, - items: [{ - priceId: data.PriceId, - quantity: packs - }] + settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id }, + items: [{ priceId: data.PriceId, quantity: packs }] }; if (data.CustomerId) { @@ -276,91 +242,40 @@ } Paddle.Checkout.open(checkoutSettings); + } else { + swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false }); } + }).fail(function () { + swal({ title: "Connection Error", text: "Unable to reach the server. Please check your connection and try again.", icon: "error", buttons: true, dangerMode: false }); }); } else { - swal({ - title: "Cannot Purchase", - text: "Please select more than 10 entities to purchase a plan.", - icon: "warning", - buttons: true, - dangerMode: false - }); + swal({ title: "Cannot Purchase", text: "Please select more than 10 entities to purchase a plan.", icon: "warning", buttons: true, dangerMode: false }); } } - $(document).ready(function () { - $("#slider").slider({ - animate: true, - value: 20, - min: 20, - max: 2000, - step: 10, - create: function () { - let handle = $("#handle-text"); - handle.text($(this).slider("value")); - }, - slide: function (event, ui) { - slider = 1; - val = ui.value; - update(1, ui.value); - } - }); - - $("#amount").val(20); - $("#amount-input").val(20); - update(); - }); - - function update(sliderVal, uiVal) { - let handle = $("#handle-text"); - var $amount = sliderVal == 1 ? uiVal : $("#amount").val(); - - handle.text($amount); - $("#amount").val($amount); - $("#amount-input").val($amount); - - if ($amount > 10) { - const totalCostMonthly = calculateCostFromUsers($amount, true); - const totalCostYearly = calculateCostFromUsers($amount, false); - - $("#monthly-label").text(totalCostMonthly); - $("#yearly-label").text(totalCostYearly); - - $("#buyYearlyButton").show(); - $("#buyMonthlyButton").show(); - } else { - $("#monthly-label").text(0); - $("#yearly-label").text(0); - - $("#buyYearlyButton").hide(); - $("#buyMonthlyButton").hide(); - } - } - - const calculateCostFromUsers = (totalNumUsers, isMonthly) => { - const pricingTiersMonthly = [ - { tier: 0, marginalUserSlots: 5, costPerUser: 20.0 }, - { tier: 1, marginalUserSlots: 100, costPerUser: 2.0 }, - { tier: 2, marginalUserSlots: 1000, costPerUser: 1.5 }, - { tier: 3, marginalUserSlots: 5000, costPerUser: 1.0 }, - { tier: 4, marginalUserSlots: 999999999, costPerUser: 0.5 }, + var calculateCostFromUsers = function (totalNumUsers, isMonthly) { + var pricingTiersMonthly = [ + { marginalUserSlots: 5, costPerUser: 20.0 }, + { marginalUserSlots: 100, costPerUser: 2.0 }, + { marginalUserSlots: 1000, costPerUser: 1.5 }, + { marginalUserSlots: 5000, costPerUser: 1.0 }, + { marginalUserSlots: 999999999, costPerUser: 0.5 } ]; - const pricingTiersYearly = [ - { tier: 0, marginalUserSlots: 5, costPerUser: 200.0 }, - { tier: 1, marginalUserSlots: 100, costPerUser: 20.0 }, - { tier: 2, marginalUserSlots: 1000, costPerUser: 15.0 }, - { tier: 3, marginalUserSlots: 1000, costPerUser: 10.0 }, - { tier: 4, marginalUserSlots: 999999999, costPerUser: 5.0 }, + var pricingTiersYearly = [ + { marginalUserSlots: 5, costPerUser: 200.0 }, + { marginalUserSlots: 100, costPerUser: 20.0 }, + { marginalUserSlots: 1000, costPerUser: 15.0 }, + { marginalUserSlots: 5000, costPerUser: 10.0 }, + { marginalUserSlots: 999999999, costPerUser: 5.0 } ]; - let finalCost = 0.0; - let remainingUsers = (totalNumUsers / 10) - 1; - let pricingTiers = isMonthly ? pricingTiersMonthly : pricingTiersYearly; + var finalCost = 0.0; + var remainingUsers = (totalNumUsers / 10) - 1; + var pricingTiers = isMonthly ? pricingTiersMonthly : pricingTiersYearly; - for (let i = 0; i < pricingTiers.length; i++) { - let tier = pricingTiers[i]; + for (var i = 0; i < pricingTiers.length; i++) { + var tier = pricingTiers[i]; if (tier.marginalUserSlots < remainingUsers) { finalCost += tier.marginalUserSlots * tier.costPerUser; remainingUsers -= tier.marginalUserSlots; @@ -372,5 +287,56 @@ return finalCost; }; + + function updatePricing() { + var amount = parseInt(document.getElementById('amount-input').value) || 0; + + if (amount > 10) { + document.getElementById('monthly-label').textContent = calculateCostFromUsers(amount, true).toFixed(2); + document.getElementById('yearly-label').textContent = calculateCostFromUsers(amount, false).toFixed(2); + document.getElementById('buyYearlyButton').style.display = ''; + document.getElementById('buyMonthlyButton').style.display = ''; + } else { + document.getElementById('monthly-label').textContent = '0.00'; + document.getElementById('yearly-label').textContent = '0.00'; + document.getElementById('buyYearlyButton').style.display = 'none'; + document.getElementById('buyMonthlyButton').style.display = 'none'; + } + } + + $(document).ready(function () { + var slider = document.getElementById('entity-slider'); + var input = document.getElementById('amount-input'); + var isPaddle = @(Model.IsPaddleDepartment ? "true" : "false"); + + // Wire up buy buttons + document.getElementById('buyYearlyButton').addEventListener('click', function (e) { + e.preventDefault(); + isPaddle ? paddleCheckout(36) : stripeCheckout(36); + }); + document.getElementById('buyMonthlyButton').addEventListener('click', function (e) { + e.preventDefault(); + isPaddle ? paddleCheckout(37) : stripeCheckout(37); + }); + + // Sync slider → input + slider.addEventListener('input', function () { + input.value = slider.value; + updatePricing(); + }); + + // Sync input → slider + input.addEventListener('change', function () { + var val = parseInt(input.value) || 20; + if (val < 20) val = 20; + if (val > 2000) val = 2000; + val = Math.ceil(val / 10) * 10; + input.value = val; + slider.value = val; + updatePricing(); + }); + + updatePricing(); + }); } diff --git a/Web/Resgrid.Web/Startup.cs b/Web/Resgrid.Web/Startup.cs index f7f81a0e..0ff3b7fe 100644 --- a/Web/Resgrid.Web/Startup.cs +++ b/Web/Resgrid.Web/Startup.cs @@ -392,7 +392,7 @@ public void ConfigureServices(IServiceCollection services) // Internal app js bundle pipeline.AddJavaScriptBundle("/js/int-bundle.js", - "lib/metisMenu/dist/metisMenu.min.js", "lib/slimScroll/jquery.slimscroll.js", "lib/pace/pace.js", + "lib/metisMenu/dist/metisMenu.min.js", "lib/slimScroll/jquery.slimscroll.js", "lib/select2/dist/js/select2.full.js", "lib/bootstrap-tour/build/js/bootstrap-tour.min.js", "lib/toastr/toastr.min.js", /*"clib/markerwithlabel/markerwithlabel.js",*/ "clib/ujs/jquery-ujs.js", "lib/jquery-validate/dist/jquery.validate.min.js", "lib/jqueryui/jquery-ui.min.js", "lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js", "lib/signalr/dist/browser/signalr.js", "clib/picEdit/js/picedit.min.js",