diff --git a/Core/Resgrid.Config/PaymentProviderConfig.cs b/Core/Resgrid.Config/PaymentProviderConfig.cs index 58758646..b8d69c38 100644 --- a/Core/Resgrid.Config/PaymentProviderConfig.cs +++ b/Core/Resgrid.Config/PaymentProviderConfig.cs @@ -1,9 +1,12 @@ using System; +using System.Text.RegularExpressions; namespace Resgrid.Config { public static class PaymentProviderConfig { + private static readonly Regex PaddleClientTokenRegex = new Regex("^(test|live)_[a-zA-Z0-9]{27}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); + #if DEBUG public static bool IsTestMode = true; #else @@ -139,17 +142,34 @@ public static string GetPaddlePTT10UserAddonPackageId() public static string GetPaddleEnvironment() { if (IsTestMode) - return PaddleTestEnvironment; + return NormalizeConfigValue(PaddleTestEnvironment).ToLowerInvariant(); else - return PaddleProductionEnvironment; + return NormalizeConfigValue(PaddleProductionEnvironment).ToLowerInvariant(); } public static string GetPaddleClientToken() { if (IsTestMode) - return PaddleTestClientToken; + return NormalizeConfigValue(PaddleTestClientToken); else - return PaddleProductionClientToken; + return NormalizeConfigValue(PaddleProductionClientToken); + } + + public static bool IsValidPaddleEnvironment(string environment) + { + var normalizedEnvironment = NormalizeConfigValue(environment).ToLowerInvariant(); + return normalizedEnvironment == "sandbox" || normalizedEnvironment == "production"; + } + + public static bool IsValidPaddleClientToken(string token) + { + var normalizedToken = NormalizeConfigValue(token); + return !string.IsNullOrWhiteSpace(normalizedToken) && PaddleClientTokenRegex.IsMatch(normalizedToken); + } + + private static string NormalizeConfigValue(string value) + { + return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); } } } diff --git a/Tests/Resgrid.Tests/Config/PaymentProviderConfigTests.cs b/Tests/Resgrid.Tests/Config/PaymentProviderConfigTests.cs new file mode 100644 index 00000000..9e9aad3c --- /dev/null +++ b/Tests/Resgrid.Tests/Config/PaymentProviderConfigTests.cs @@ -0,0 +1,62 @@ +using FluentAssertions; +using NUnit.Framework; +using Resgrid.Config; + +namespace Resgrid.Tests.Config +{ + [TestFixture] + public class PaymentProviderConfigTests + { + [Test] + public void should_trim_and_normalize_paddle_environment() + { + var originalIsTestMode = PaymentProviderConfig.IsTestMode; + var originalProductionEnvironment = PaymentProviderConfig.PaddleProductionEnvironment; + + try + { + PaymentProviderConfig.IsTestMode = false; + PaymentProviderConfig.PaddleProductionEnvironment = " Production "; + + PaymentProviderConfig.GetPaddleEnvironment().Should().Be("production"); + PaymentProviderConfig.IsValidPaddleEnvironment(" sandbox ").Should().BeTrue(); + PaymentProviderConfig.IsValidPaddleEnvironment("invalid").Should().BeFalse(); + } + finally + { + PaymentProviderConfig.IsTestMode = originalIsTestMode; + PaymentProviderConfig.PaddleProductionEnvironment = originalProductionEnvironment; + } + } + + [Test] + public void should_trim_paddle_client_token() + { + var originalIsTestMode = PaymentProviderConfig.IsTestMode; + var originalProductionClientToken = PaymentProviderConfig.PaddleProductionClientToken; + + try + { + PaymentProviderConfig.IsTestMode = false; + PaymentProviderConfig.PaddleProductionClientToken = " live_7d279f61a3499fed520f7cd8c08 "; + + PaymentProviderConfig.GetPaddleClientToken().Should().Be("live_7d279f61a3499fed520f7cd8c08"); + } + finally + { + PaymentProviderConfig.IsTestMode = originalIsTestMode; + PaymentProviderConfig.PaddleProductionClientToken = originalProductionClientToken; + } + } + + [TestCase("live_7d279f61a3499fed520f7cd8c08", true)] + [TestCase("test_4s7gd50ap72ms92nnsa20ma61lt", true)] + [TestCase(" paddletoken_live_1940dc25e601d953fce733eeddfAty ", false)] + [TestCase("live_7d279f61a3499fed520f7cd8c08/", false)] + [TestCase("", false)] + public void should_validate_documented_paddle_client_token_format(string token, bool expectedResult) + { + PaymentProviderConfig.IsValidPaddleClientToken(token).Should().Be(expectedResult); + } + } +} diff --git a/Web/Resgrid.Web.Tts/k8s/deployment.yaml b/Web/Resgrid.Web.Tts/k8s/deployment.yaml index a4f44374..cd4d6003 100644 --- a/Web/Resgrid.Web.Tts/k8s/deployment.yaml +++ b/Web/Resgrid.Web.Tts/k8s/deployment.yaml @@ -2,49 +2,83 @@ apiVersion: v1 kind: ConfigMap metadata: name: resgrid-tts-config + labels: + app.kubernetes.io/name: resgrid-tts + app.kubernetes.io/part-of: resgrid + app.kubernetes.io/managed-by: fleet data: - tts-s3-endpoint: https://minio.example.com - tts-s3-region: us-east-1 - tts-s3-bucket: tts-audio - tts-s3-public-base-url: "" - tts-default-voice: en-us - tts-default-speed: "175" - tts-playback-base-url: "" - tts-playback-memory-cache-minutes: "60" - tts-playback-cache-control-seconds: "86400" - tts-max-concurrent-generations: "4" - tts-temp-directory: /tmp/resgrid-tts - tts-pregenerated-prompts: Press 1 for yes;Press 2 for no;Invalid option;Please try again - tts-static-prompt-refresh-interval-minutes: "1440" + RESGRID__TtsConfig__ServiceBaseUrl: https://tts.example.com + RESGRID__TtsConfig__PlaybackBaseUrl: "" + RESGRID__TtsConfig__S3Endpoint: https://minio.example.com + RESGRID__TtsConfig__S3Region: us-east-1 + RESGRID__TtsConfig__S3Bucket: tts-audio + RESGRID__TtsConfig__S3PublicBaseUrl: "" + RESGRID__TtsConfig__S3UseSsl: "true" + RESGRID__TtsConfig__S3ForcePathStyle: "true" + RESGRID__TtsConfig__S3UsePresignedUrls: "true" + RESGRID__TtsConfig__S3PresignedUrlExpiryMinutes: "60" + RESGRID__TtsConfig__DefaultVoice: en-us + RESGRID__TtsConfig__DefaultSpeed: "175" + RESGRID__TtsConfig__MaxConcurrentGenerations: "4" + RESGRID__TtsConfig__MaxTextLength: "1000" + RESGRID__TtsConfig__EspeakExecutable: /usr/bin/espeak-ng + RESGRID__TtsConfig__FfmpegExecutable: /usr/bin/ffmpeg + RESGRID__TtsConfig__TempDirectory: /tmp/resgrid-tts + RESGRID__TtsConfig__CachePrefix: tts + RESGRID__TtsConfig__PlaybackMemoryCacheMinutes: "60" + RESGRID__TtsConfig__PlaybackCacheControlSeconds: "86400" + RESGRID__TtsConfig__WarmupEnabled: "true" + RESGRID__TtsConfig__StaticPromptRefreshIntervalMinutes: "1440" + RESGRID__TtsConfig__PreGeneratedPrompts: >- + Press 1 for yes;Press 2 for no;Invalid option;Please try again;Please stay on the line;This call has been closed. Goodbye.;You have been marked responding to the scene, goodbye.;Sorry, that was not a valid selection.;Hello, this is Resgrid calling with your verification code.;That was your Resgrid verification code. Goodbye.;Thank you for calling Resgrid, automated personnel system. The number you called is not tied to an active department or the department doesn't have this feature enabled. Goodbye.;We couldn't complete your verification call. Please request a new code and try again. Goodbye.;Please select from the following options.;To list current active calls, press 1.;To list current user statuses, press 2.;To list current unit statuses, press 3.;To list upcoming calendar events, press 4.;To list upcoming shifts, press 5.;To set your current status, press 6.;To set your current staffing level, press 7.;Press 0 to repeat. Press 1 to respond to the scene.;Press 0 to go back to the main menu.;Invalid status selection, goodbye.;No status selection made, goodbye.;Invalid staffing selection. Returning to the main menu.;No staffing selection made. Returning to the main menu.;Thank you. Your response has been recorded. + RESGRID__TtsConfig__RateLimitPermitLimit: "60" + RESGRID__TtsConfig__RateLimitQueueLimit: "10" + RESGRID__TtsConfig__RateLimitWindowSeconds: "60" --- apiVersion: v1 kind: Secret metadata: name: resgrid-tts-secrets + labels: + app.kubernetes.io/name: resgrid-tts + app.kubernetes.io/part-of: resgrid + app.kubernetes.io/managed-by: fleet type: Opaque stringData: - tts-s3-access-key: change-me - tts-s3-secret-key: change-me - tts-static-prompt-admin-key: change-me - tts-redis-connection-string: change-me + RESGRID__TtsConfig__S3AccessKey: change-me + RESGRID__TtsConfig__S3SecretKey: change-me + RESGRID__TtsConfig__StaticPromptAdminKey: change-me + RESGRID__CacheConfig__RedisConnectionString: change-me --- apiVersion: apps/v1 kind: Deployment metadata: name: resgrid-tts + labels: + app.kubernetes.io/name: resgrid-tts + app.kubernetes.io/part-of: resgrid + app.kubernetes.io/managed-by: fleet spec: replicas: 2 revisionHistoryLimit: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 selector: matchLabels: - app: resgrid-tts + app.kubernetes.io/name: resgrid-tts template: metadata: labels: - app: resgrid-tts + app.kubernetes.io/name: resgrid-tts + app.kubernetes.io/part-of: resgrid spec: securityContext: fsGroup: 10001 + seccompProfile: + type: RuntimeDefault containers: - name: resgrid-tts image: resgridllc/resgridwebtts:latest @@ -53,93 +87,15 @@ spec: - name: http containerPort: 8080 env: + - name: ASPNETCORE_ENVIRONMENT + value: Production - name: ASPNETCORE_URLS value: http://+:8080 - - name: RESGRID__TtsConfig__S3Endpoint - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-s3-endpoint - - name: RESGRID__TtsConfig__S3Region - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-s3-region - - name: RESGRID__TtsConfig__S3Bucket - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-s3-bucket - - name: RESGRID__TtsConfig__S3PublicBaseUrl - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-s3-public-base-url - - name: RESGRID__TtsConfig__S3AccessKey - valueFrom: - secretKeyRef: - name: resgrid-tts-secrets - key: tts-s3-access-key - - name: RESGRID__TtsConfig__S3SecretKey - valueFrom: - secretKeyRef: - name: resgrid-tts-secrets - key: tts-s3-secret-key - - name: RESGRID__TtsConfig__DefaultVoice - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-default-voice - - name: RESGRID__TtsConfig__DefaultSpeed - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-default-speed - - name: RESGRID__TtsConfig__PlaybackBaseUrl - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-playback-base-url - - name: RESGRID__TtsConfig__PlaybackMemoryCacheMinutes - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-playback-memory-cache-minutes - - name: RESGRID__TtsConfig__PlaybackCacheControlSeconds - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-playback-cache-control-seconds - - name: RESGRID__TtsConfig__MaxConcurrentGenerations - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-max-concurrent-generations - - name: RESGRID__TtsConfig__TempDirectory - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-temp-directory - - name: RESGRID__TtsConfig__PreGeneratedPrompts - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-pregenerated-prompts - - name: RESGRID__TtsConfig__StaticPromptRefreshIntervalMinutes - valueFrom: - configMapKeyRef: - name: resgrid-tts-config - key: tts-static-prompt-refresh-interval-minutes - - name: RESGRID__TtsConfig__StaticPromptAdminKey - valueFrom: - secretKeyRef: - name: resgrid-tts-secrets - key: tts-static-prompt-admin-key - - name: RESGRID__CacheConfig__RedisConnectionString - valueFrom: - secretKeyRef: - name: resgrid-tts-secrets - key: tts-redis-connection-string + envFrom: + - configMapRef: + name: resgrid-tts-config + - secretRef: + name: resgrid-tts-secrets readinessProbe: httpGet: path: /health @@ -147,6 +103,7 @@ spec: initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 + failureThreshold: 3 livenessProbe: httpGet: path: /health @@ -154,6 +111,15 @@ spec: initialDelaySeconds: 15 periodSeconds: 20 timeoutSeconds: 2 + failureThreshold: 3 + startupProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 18 resources: requests: cpu: 100m @@ -167,21 +133,65 @@ spec: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 + capabilities: + drop: + - ALL volumeMounts: - name: temp-storage mountPath: /tmp/resgrid-tts volumes: - name: temp-storage - emptyDir: {} + ephemeral: + volumeClaimTemplate: + metadata: + labels: + app.kubernetes.io/name: resgrid-tts + app.kubernetes.io/component: temp-storage + spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 2Gi --- apiVersion: v1 kind: Service metadata: name: resgrid-tts + labels: + app.kubernetes.io/name: resgrid-tts + app.kubernetes.io/part-of: resgrid + app.kubernetes.io/managed-by: fleet spec: + type: ClusterIP selector: - app: resgrid-tts + app.kubernetes.io/name: resgrid-tts ports: - name: http port: 80 targetPort: http +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: resgrid-tts + labels: + app.kubernetes.io/name: resgrid-tts + app.kubernetes.io/part-of: resgrid + app.kubernetes.io/managed-by: fleet + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "60" + nginx.ingress.kubernetes.io/proxy-send-timeout: "60" +spec: + rules: + - host: tts.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: resgrid-tts + port: + number: 80 diff --git a/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs b/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs index c841a7dd..3720c5eb 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs @@ -78,6 +78,40 @@ private static bool ShouldUsePaddleForSubscriptionFlow(Payment currentPayment, s return Config.PaymentProviderConfig.IsPaddleActive(); } + private static (string PaddleEnvironment, string PaddleClientToken, bool CanInitializePaddleCheckout, string PaddleConfigurationError) GetPaddleCheckoutConfiguration(bool isPaddleDepartment) + { + if (!isPaddleDepartment) + return (string.Empty, string.Empty, false, null); + + var paddleEnvironment = Config.PaymentProviderConfig.GetPaddleEnvironment(); + var paddleClientToken = Config.PaymentProviderConfig.GetPaddleClientToken(); + var canInitializePaddleCheckout = + Config.PaymentProviderConfig.IsValidPaddleEnvironment(paddleEnvironment) + && Config.PaymentProviderConfig.IsValidPaddleClientToken(paddleClientToken); + + return ( + paddleEnvironment, + paddleClientToken, + canInitializePaddleCheckout, + isPaddleDepartment && !canInitializePaddleCheckout + ? GetPaddleConfigurationError(paddleEnvironment, paddleClientToken) + : null); + } + + private static string GetPaddleConfigurationError(string paddleEnvironment, string paddleClientToken) + { + if (string.IsNullOrWhiteSpace(paddleClientToken)) + return "Paddle checkout is not configured. A valid client-side token is required."; + + if (!Config.PaymentProviderConfig.IsValidPaddleClientToken(paddleClientToken)) + return "Paddle checkout is misconfigured. The configured client-side token must use Paddle's documented live_... or test_... format."; + + if (!Config.PaymentProviderConfig.IsValidPaddleEnvironment(paddleEnvironment)) + return "Paddle checkout is misconfigured. The configured environment must be sandbox or production."; + + return null; + } + [HttpGet] [Authorize] public async Task SelectRegistrationPlan(string discountCode = null) @@ -94,8 +128,11 @@ public async Task SelectRegistrationPlan(string discountCode = nu var paddleCustomerId = await _departmentSettingsService.GetPaddleCustomerIdForDepartmentAsync(DepartmentId); bool isPaddleDepartment = ShouldUsePaddleForSubscriptionFlow(currentPayment, paddleCustomerId); model.IsPaddleDepartment = isPaddleDepartment; - model.PaddleEnvironment = Config.PaymentProviderConfig.GetPaddleEnvironment(); - model.PaddleClientToken = Config.PaymentProviderConfig.GetPaddleClientToken(); + var paddleCheckoutConfiguration = GetPaddleCheckoutConfiguration(isPaddleDepartment); + model.PaddleEnvironment = paddleCheckoutConfiguration.PaddleEnvironment; + model.PaddleClientToken = paddleCheckoutConfiguration.PaddleClientToken; + model.CanInitializePaddleCheckout = paddleCheckoutConfiguration.CanInitializePaddleCheckout; + model.PaddleConfigurationError = paddleCheckoutConfiguration.PaddleConfigurationError; return View(model); } @@ -249,8 +286,11 @@ public async Task Index() if (isPaddleDepartment) { model.PaddleCustomer = paddleCustomerId; - model.PaddleEnvironment = Config.PaymentProviderConfig.GetPaddleEnvironment(); - model.PaddleClientToken = Config.PaymentProviderConfig.GetPaddleClientToken(); + var paddleCheckoutConfiguration = GetPaddleCheckoutConfiguration(isPaddleDepartment); + model.PaddleEnvironment = paddleCheckoutConfiguration.PaddleEnvironment; + model.PaddleClientToken = paddleCheckoutConfiguration.PaddleClientToken; + model.CanInitializePaddleCheckout = paddleCheckoutConfiguration.CanInitializePaddleCheckout; + model.PaddleConfigurationError = paddleCheckoutConfiguration.PaddleConfigurationError; } else { diff --git a/Web/Resgrid.Web/Areas/User/Models/Subscription/SelectRegistrationPlanView.cs b/Web/Resgrid.Web/Areas/User/Models/Subscription/SelectRegistrationPlanView.cs index dfab5b0e..eacc774f 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Subscription/SelectRegistrationPlanView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Subscription/SelectRegistrationPlanView.cs @@ -7,6 +7,8 @@ public class SelectRegistrationPlanView public string PaddleEnvironment { get; set; } public bool IsPaddleDepartment { get; set; } public string PaddleClientToken { get; set; } + public bool CanInitializePaddleCheckout { get; set; } + public string PaddleConfigurationError { get; set; } public string DiscountCode { get; set; } } } diff --git a/Web/Resgrid.Web/Areas/User/Models/Subscription/SubscriptionView.cs b/Web/Resgrid.Web/Areas/User/Models/Subscription/SubscriptionView.cs index d6c767e5..6ba707e7 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Subscription/SubscriptionView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Subscription/SubscriptionView.cs @@ -47,6 +47,8 @@ public class SubscriptionView : BaseUserModel public string PaddleEnvironment { get; set; } public bool IsPaddleDepartment { get; set; } public string PaddleClientToken { get; set; } + public bool CanInitializePaddleCheckout { get; set; } + public string PaddleConfigurationError { get; set; } public string IsEntitiesTabActive() diff --git a/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml index b9d06884..0cb7a4e2 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml @@ -1,4 +1,5 @@ -@using Resgrid.Framework +@using Newtonsoft.Json +@using Resgrid.Framework @using Resgrid.Model @using Resgrid.Config @model Resgrid.Web.Areas.User.Models.Subscription.SubscriptionView @@ -10,6 +11,10 @@ var location = InfoConfig.Locations.Find(l => l.Name == locationName) ?? InfoConfig.Locations[0]; var isEU = locationName.StartsWith("EU", StringComparison.OrdinalIgnoreCase); var currencySymbol = isEU ? "\u20AC" : "$"; + var jsStringSettings = new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeHtml }; + var paddleEnvironmentJson = Html.Raw(JsonConvert.SerializeObject(Model.PaddleEnvironment ?? "", jsStringSettings)); + var paddleClientTokenJson = Html.Raw(JsonConvert.SerializeObject(Model.PaddleClientToken ?? "", jsStringSettings)); + var paddleConfigurationErrorJson = Html.Raw(JsonConvert.SerializeObject(Model.PaddleConfigurationError ?? "", jsStringSettings)); } @section Styles @@ -510,13 +515,13 @@ @section Scripts { - @if (Model.IsPaddleDepartment) { + @if (Model.IsPaddleDepartment && Model.CanInitializePaddleCheckout) { - } else { + } else if (!Model.IsPaddleDepartment) { } @@ -524,6 +529,8 @@ @if (!Model.IsPaddleDepartment) { var stripe = Stripe('@Model.StripeKey'); } + var paddleReady = @(Model.CanInitializePaddleCheckout ? "true" : "false"); + var paddleConfigurationError = @paddleConfigurationErrorJson; function stripeCheckout(id) { const amount = slider == 1 ? val : $("#amount").val(); @@ -577,6 +584,11 @@ } function paddleCheckout(id) { + if (!paddleReady) { + swal({ title: "Checkout Unavailable", text: paddleConfigurationError || "Paddle checkout is not configured correctly. Please contact support.", icon: "error", buttons: true, dangerMode: false }); + return; + } + const amount = slider == 1 ? val : $("#amount").val(); const minAmount = IS_EU ? 10 : 20; diff --git a/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml index da75edaa..2983b22e 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml @@ -1,3 +1,4 @@ +@using Newtonsoft.Json @using Resgrid.Config @model Resgrid.Web.Areas.User.Models.Subscription.SelectRegistrationPlanView @{ @@ -9,6 +10,10 @@ var isEU = locationName.StartsWith("EU", StringComparison.OrdinalIgnoreCase); var currencySymbol = isEU ? "\u20AC" : "$"; var minEntities = isEU ? 10 : 20; + var jsStringSettings = new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeHtml }; + var paddleEnvironmentJson = Html.Raw(JsonConvert.SerializeObject(Model.PaddleEnvironment ?? "", jsStringSettings)); + var paddleClientTokenJson = Html.Raw(JsonConvert.SerializeObject(Model.PaddleClientToken ?? "", jsStringSettings)); + var paddleConfigurationErrorJson = Html.Raw(JsonConvert.SerializeObject(Model.PaddleConfigurationError ?? "", jsStringSettings)); } @section Styles @@ -127,6 +132,13 @@ } + @if (Model.IsPaddleDepartment && !Model.CanInitializePaddleCheckout && !string.IsNullOrWhiteSpace(Model.PaddleConfigurationError)) + { +
+ Paddle Checkout Unavailable: @Model.PaddleConfigurationError +
+ } +
Need help choosing? Contact us at @SystemBehaviorConfig.ContactUsUrl and we can help find the right plan for your organization.
@@ -139,13 +151,13 @@ @section Scripts { - @if (Model.IsPaddleDepartment) { + @if (Model.IsPaddleDepartment && Model.CanInitializePaddleCheckout) { - } else { + } else if (!Model.IsPaddleDepartment) { } @@ -153,9 +165,11 @@ @if (!Model.IsPaddleDepartment) { var stripe = Stripe('@Model.StripeKey'); } - var discountCode = '@Html.Raw(Model.DiscountCode ?? "")'; + var discountCode = @Html.Raw(JsonConvert.SerializeObject(Model.DiscountCode ?? "", jsStringSettings)); var IS_EU = @(isEU ? "true" : "false"); var EU_MULTIPLIER = 1.25; + var paddleReady = @(Model.CanInitializePaddleCheckout ? "true" : "false"); + var paddleConfigurationError = @paddleConfigurationErrorJson; function stripeCheckout(id) { var amount = parseInt(document.getElementById('amount-input').value) || 0; @@ -187,6 +201,11 @@ } function paddleCheckout(id) { + if (!paddleReady) { + swal({ title: "Checkout Unavailable", text: paddleConfigurationError || "Paddle checkout is not configured correctly. Please contact support.", icon: "error", buttons: true, dangerMode: false }); + return; + } + var amount = parseInt(document.getElementById('amount-input').value) || 0; var minAmount = @minEntities;