From 92b257a18bc916bd044881c53ec82d5a63d91397 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Fri, 10 Apr 2026 20:02:18 -0700 Subject: [PATCH 1/3] RE1-T112 Fixes --- .../User/Views/Shared/_UserLayout.cshtml | 11 - .../SelectRegistrationPlan.cshtml | 380 ++++++++---------- Web/Resgrid.Web/Startup.cs | 2 +- 3 files changed, 166 insertions(+), 227 deletions(-) diff --git a/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml b/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml index 4ce641c75..e0743208e 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/SelectRegistrationPlan.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml index fda41ebba..acbdba2db 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,55 @@ 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 -
-
-
- - - -
-
-
+
+

Entities

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

- .00 -
+
+ 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 +179,35 @@ @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) { + swal({ title: "Purchase Error", text: "Error redirecting to Stripe: " + result.error.message, 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 +216,13 @@ }).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; } 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) { @@ -279,88 +233,33 @@ } }); } else { - 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(); + swal({ title: "Cannot Purchase", text: "Please select more than 10 entities to purchase a plan.", icon: "warning", buttons: true, dangerMode: false }); } } - 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: 1000, 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 +271,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); + document.getElementById('yearly-label').textContent = calculateCostFromUsers(amount, false); + document.getElementById('buyYearlyButton').style.display = ''; + document.getElementById('buyMonthlyButton').style.display = ''; + } else { + document.getElementById('monthly-label').textContent = '0'; + document.getElementById('yearly-label').textContent = '0'; + 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 f7f81a0eb..0ff3b7fe7 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", From f0b081396d9d21cefc0acba22ff4b24c3a6c1514 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Fri, 10 Apr 2026 20:56:47 -0700 Subject: [PATCH 2/3] RE1-T112 PR#324 fixes --- .../User/Views/Subscription/Index.cshtml | 2 +- .../SelectRegistrationPlan.cshtml | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml index 4d1fdc4b2..2fa1b8595 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 acbdba2db..a4c6ae52a 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml @@ -99,13 +99,14 @@
-

Entities

- Users or Units sold in packs of 10 +

+ Users or Units sold in packs of 10
- +
- Entities + + Entities
@@ -115,7 +116,7 @@ Monthly ($):
- 0.00 + 0.00
@@ -123,7 +124,7 @@ Yearly ($):
- 0.00 + 0.00

@@ -194,9 +195,15 @@ }).done(function (data) { if (data && data.SessionId) { stripe.redirectToCheckout({ sessionId: data.SessionId }).then(function (result) { - swal({ title: "Purchase Error", text: "Error redirecting to Stripe: " + result.error.message, icon: "error", buttons: true, dangerMode: false }); + 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 }); @@ -220,6 +227,11 @@ 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 }] @@ -230,7 +242,11 @@ } 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 }); @@ -250,7 +266,7 @@ { marginalUserSlots: 5, costPerUser: 200.0 }, { marginalUserSlots: 100, costPerUser: 20.0 }, { marginalUserSlots: 1000, costPerUser: 15.0 }, - { marginalUserSlots: 1000, costPerUser: 10.0 }, + { marginalUserSlots: 5000, costPerUser: 10.0 }, { marginalUserSlots: 999999999, costPerUser: 5.0 } ]; @@ -276,13 +292,13 @@ var amount = parseInt(document.getElementById('amount-input').value) || 0; if (amount > 10) { - document.getElementById('monthly-label').textContent = calculateCostFromUsers(amount, true); - document.getElementById('yearly-label').textContent = calculateCostFromUsers(amount, false); + 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'; - document.getElementById('yearly-label').textContent = '0'; + 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'; } From 82482bf7e2075484e07f0cdb39b446fea66470de Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Fri, 10 Apr 2026 21:05:01 -0700 Subject: [PATCH 3/3] RE1-T112 PR#324 fixes --- .../Areas/User/Controllers/SubscriptionController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs b/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs index b903341bf..b1c3ee0e7 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);