From 14c6ef7fb6d30b2317798c8c0b1861d402058bca Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Fri, 24 Apr 2026 19:16:56 -0700 Subject: [PATCH] RE1-T115 Paddle --- .../Controllers/SubscriptionController.cs | 8 ++ .../User/Views/Subscription/Index.cshtml | 108 ++++++++++++++---- .../SelectRegistrationPlan.cshtml | 95 ++++++++++++--- 3 files changed, 178 insertions(+), 33 deletions(-) diff --git a/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs b/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs index 38b7ba09..6497b183 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs @@ -797,6 +797,14 @@ public async Task GetPaddleCheckout(int id, int count, string dis var user = _usersService.GetUserById(UserId); var checkout = await _subscriptionsService.CreatePaddleCheckoutForSub(DepartmentId, paddleCustomerId, paddleProductId, plan.PlanId, user.Email, department.Name, count, discountCode); + if (checkout == null || (string.IsNullOrWhiteSpace(checkout.TransactionId) && string.IsNullOrWhiteSpace(checkout.PriceId))) + { + if (plan.PlanId == 36 || plan.PlanId == 37) + return StatusCode(StatusCodes.Status502BadGateway, "Paddle checkout could not be created because the billing service did not return a transaction or price for this product-based entity plan."); + + return StatusCode(StatusCodes.Status502BadGateway, "Paddle checkout could not be created because the billing service did not return checkout data."); + } + bool hasActiveSub = false; if (!string.IsNullOrWhiteSpace(paddleCustomerId)) { diff --git a/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml index 456a4102..eee60984 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml @@ -516,20 +516,7 @@ @section Scripts { - @if (Model.IsPaddleDepartment && Model.CanInitializePaddleCheckout) { - - - } else if (!Model.IsPaddleDepartment) { + @if (!Model.IsPaddleDepartment) { } @@ -538,7 +525,80 @@ var stripe = Stripe('@Model.StripeKey'); } var paddleReady = @(Model.CanInitializePaddleCheckout ? "true" : "false"); + var paddleEnvironment = @paddleEnvironmentJson; + var paddleClientToken = @paddleClientTokenJson; + var paddleCustomerId = @paddleCustomerJson; var paddleConfigurationError = @paddleConfigurationErrorJson; + var paddleInitializationPromise = null; + + function getAjaxErrorMessage(xhr, fallbackMessage) { + if (xhr && xhr.responseText) { + return xhr.responseText; + } + + return fallbackMessage; + } + + function initializePaddle() { + if (window.resgridPaddleInitialized) { + return window.Paddle; + } + + Paddle.Environment.set(paddleEnvironment); + + var paddleInitializeOptions = { token: paddleClientToken }; + if (/^ctm_/.test(paddleCustomerId)) { + paddleInitializeOptions.pwCustomer = { id: paddleCustomerId }; + } + + Paddle.Initialize(paddleInitializeOptions); + window.resgridPaddleInitialized = true; + + return window.Paddle; + } + + function ensurePaddleInitialized() { + if (!paddleReady) { + return Promise.reject(new Error(paddleConfigurationError || "Paddle checkout is not configured correctly. Please contact support.")); + } + + if (window.resgridPaddleInitialized && window.Paddle) { + return Promise.resolve(window.Paddle); + } + + if (paddleInitializationPromise) { + return paddleInitializationPromise; + } + + paddleInitializationPromise = new Promise(function (resolve, reject) { + var completeInitialization = function () { + try { + resolve(initializePaddle()); + } catch (error) { + paddleInitializationPromise = null; + reject(error); + } + }; + + if (window.Paddle) { + completeInitialization(); + return; + } + + var script = document.createElement('script'); + script.src = 'https://cdn.paddle.com/paddle/v2/paddle.js'; + script.async = true; + script.onload = completeInitialization; + script.onerror = function () { + paddleInitializationPromise = null; + reject(new Error("Unable to load Paddle checkout. Please try again.")); + }; + + document.head.appendChild(script); + }); + + return paddleInitializationPromise; + } function stripeCheckout(id) { const amount = slider == 1 ? val : $("#amount").val(); @@ -615,9 +675,13 @@ } if (data.TransactionId) { - Paddle.Checkout.open({ - settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id }, - transactionId: data.TransactionId + ensurePaddleInitialized().then(function () { + Paddle.Checkout.open({ + settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id }, + transactionId: data.TransactionId + }); + }).catch(function (error) { + swal({ title: "Checkout Error", text: (error && error.message) || "Unable to initialize Paddle checkout. Please try again.", icon: "error", buttons: true, dangerMode: false }); }); return; } @@ -640,12 +704,16 @@ checkoutSettings.customer = { id: data.CustomerId }; } - Paddle.Checkout.open(checkoutSettings); + ensurePaddleInitialized().then(function () { + Paddle.Checkout.open(checkoutSettings); + }).catch(function (error) { + swal({ title: "Checkout Error", text: (error && error.message) || "Unable to initialize Paddle checkout. Please try again.", 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 }); + }).fail(function (xhr) { + swal({ title: "Checkout Error", text: getAjaxErrorMessage(xhr, "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 entities to purchase a plan.", icon: "warning", buttons: true, dangerMode: false }); diff --git a/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml index 1e4916d4..25b35f69 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml @@ -151,13 +151,7 @@ @section Scripts { - @if (Model.IsPaddleDepartment && Model.CanInitializePaddleCheckout) { - - - } else if (!Model.IsPaddleDepartment) { + @if (!Model.IsPaddleDepartment) { } @@ -169,7 +163,73 @@ var IS_EU = @(isEU ? "true" : "false"); var EU_MULTIPLIER = 1.25; var paddleReady = @(Model.CanInitializePaddleCheckout ? "true" : "false"); + var paddleEnvironment = @paddleEnvironmentJson; + var paddleClientToken = @paddleClientTokenJson; var paddleConfigurationError = @paddleConfigurationErrorJson; + var paddleInitializationPromise = null; + + function getAjaxErrorMessage(xhr, fallbackMessage) { + if (xhr && xhr.responseText) { + return xhr.responseText; + } + + return fallbackMessage; + } + + function initializePaddle() { + if (window.resgridPaddleInitialized) { + return window.Paddle; + } + + Paddle.Environment.set(paddleEnvironment); + Paddle.Initialize({ token: paddleClientToken }); + window.resgridPaddleInitialized = true; + + return window.Paddle; + } + + function ensurePaddleInitialized() { + if (!paddleReady) { + return Promise.reject(new Error(paddleConfigurationError || "Paddle checkout is not configured correctly. Please contact support.")); + } + + if (window.resgridPaddleInitialized && window.Paddle) { + return Promise.resolve(window.Paddle); + } + + if (paddleInitializationPromise) { + return paddleInitializationPromise; + } + + paddleInitializationPromise = new Promise(function (resolve, reject) { + var completeInitialization = function () { + try { + resolve(initializePaddle()); + } catch (error) { + paddleInitializationPromise = null; + reject(error); + } + }; + + if (window.Paddle) { + completeInitialization(); + return; + } + + var script = document.createElement('script'); + script.src = 'https://cdn.paddle.com/paddle/v2/paddle.js'; + script.async = true; + script.onload = completeInitialization; + script.onerror = function () { + paddleInitializationPromise = null; + reject(new Error("Unable to load Paddle checkout. Please try again.")); + }; + + document.head.appendChild(script); + }); + + return paddleInitializationPromise; + } function stripeCheckout(id) { var amount = parseInt(document.getElementById('amount-input').value) || 0; @@ -223,9 +283,13 @@ return; } if (data.TransactionId) { - Paddle.Checkout.open({ - settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id }, - transactionId: data.TransactionId + ensurePaddleInitialized().then(function () { + Paddle.Checkout.open({ + settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id }, + transactionId: data.TransactionId + }); + }).catch(function (error) { + swal({ title: "Checkout Error", text: (error && error.message) || "Unable to initialize Paddle checkout. Please try again.", icon: "error", buttons: true, dangerMode: false }); }); return; } @@ -244,12 +308,17 @@ if (data.CustomerId) { checkoutSettings.customer = { id: data.CustomerId }; } - Paddle.Checkout.open(checkoutSettings); + + ensurePaddleInitialized().then(function () { + Paddle.Checkout.open(checkoutSettings); + }).catch(function (error) { + swal({ title: "Checkout Error", text: (error && error.message) || "Unable to initialize Paddle checkout. Please try again.", 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 }); + }).fail(function (xhr) { + swal({ title: "Checkout Error", text: getAjaxErrorMessage(xhr, "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 entities to purchase a plan.", icon: "warning", buttons: true, dangerMode: false });