diff --git a/package.json b/package.json index 4b6112a..faa35f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "processout.js", - "version": "1.9.10", + "version": "1.9.11", "description": "ProcessOut.js is a JavaScript library for ProcessOut's payment processing API.", "scripts": { "build:processout": "tsc -p src/processout && uglifyjs --compress --keep-fnames --ie8 dist/processout.js -o dist/processout.js", diff --git a/src/dynamic-checkout/payment-methods/apm.ts b/src/dynamic-checkout/payment-methods/apm.ts index 1c8bdaa..c83a695 100644 --- a/src/dynamic-checkout/payment-methods/apm.ts +++ b/src/dynamic-checkout/payment-methods/apm.ts @@ -176,7 +176,7 @@ module ProcessOut { ) }, error => { - if (error.code === "customer.canceled") { + if (isCustomerCancellationError(error)) { this.resetContainerHtml().appendChild( new DynamicCheckoutPaymentCancelledView(this.processOutInstance, this.paymentConfig) .element, @@ -364,7 +364,7 @@ module ProcessOut { ) }, error => { - if (error.code === "customer.canceled") { + if (isCustomerCancellationError(error)) { this.resetContainerHtml().appendChild( new DynamicCheckoutPaymentCancelledView( this.processOutInstance, diff --git a/src/dynamic-checkout/payment-methods/card.ts b/src/dynamic-checkout/payment-methods/card.ts index 7dfb18c..9e6405c 100644 --- a/src/dynamic-checkout/payment-methods/card.ts +++ b/src/dynamic-checkout/payment-methods/card.ts @@ -220,7 +220,7 @@ module ProcessOut { } private handleCardPaymentError(error) { - if (error.code === "customer.canceled") { + if (isCustomerCancellationError(error)) { this.resetContainerHtml().appendChild( new DynamicCheckoutPaymentCancelledView(this.processOutInstance, this.paymentConfig) .element, diff --git a/src/dynamic-checkout/payment-methods/saved-apm.ts b/src/dynamic-checkout/payment-methods/saved-apm.ts index efb0998..3a25ec2 100644 --- a/src/dynamic-checkout/payment-methods/saved-apm.ts +++ b/src/dynamic-checkout/payment-methods/saved-apm.ts @@ -161,7 +161,7 @@ module ProcessOut { ) }, error => { - if (error.code === "customer.canceled") { + if (isCustomerCancellationError(error)) { this.resetContainerHtml().appendChild( new DynamicCheckoutPaymentCancelledView( this.processOutInstance, @@ -255,7 +255,7 @@ module ProcessOut { } private handlePaymentError(error) { - if (error.code === "customer.canceled") { + if (isCustomerCancellationError(error)) { this.resetContainerHtml().appendChild( new DynamicCheckoutPaymentCancelledView(this.processOutInstance, this.paymentConfig) .element, diff --git a/src/dynamic-checkout/payment-methods/saved-card.ts b/src/dynamic-checkout/payment-methods/saved-card.ts index 5a4c239..efef504 100644 --- a/src/dynamic-checkout/payment-methods/saved-card.ts +++ b/src/dynamic-checkout/payment-methods/saved-card.ts @@ -107,7 +107,7 @@ module ProcessOut { } private handlePaymentError(error) { - if (error.code === "customer.canceled") { + if (isCustomerCancellationError(error)) { this.resetContainerHtml().appendChild( new DynamicCheckoutPaymentCancelledView(this.processOutInstance, this.paymentConfig) .element, diff --git a/src/dynamic-checkout/references.ts b/src/dynamic-checkout/references.ts index 24f1b84..af2174e 100644 --- a/src/dynamic-checkout/references.ts +++ b/src/dynamic-checkout/references.ts @@ -38,6 +38,7 @@ /// /// /// +/// /// /// /// diff --git a/src/dynamic-checkout/utils/errors.ts b/src/dynamic-checkout/utils/errors.ts new file mode 100644 index 0000000..85934ac --- /dev/null +++ b/src/dynamic-checkout/utils/errors.ts @@ -0,0 +1,11 @@ +/// + +module ProcessOut { + // The SDK raises "customer.canceled" for cancellations it detects itself + // (overlay cancel, tab/window closed), while cancellations that happen on a + // gateway's hosted page come back from the API as "customer.cancelled". + // Both must be treated as a customer cancellation. + export function isCustomerCancellationError(error: { code?: string }): boolean { + return error.code === "customer.canceled" || error.code === "customer.cancelled" + } +} diff --git a/test/dynamic-checkout/errors.test.ts b/test/dynamic-checkout/errors.test.ts new file mode 100644 index 0000000..3121ac2 --- /dev/null +++ b/test/dynamic-checkout/errors.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest" +import { loadNamespaceFile } from "../support/loadNamespace" + +const { isCustomerCancellationError } = loadNamespaceFile("src/dynamic-checkout/utils/errors.ts") + +describe("isCustomerCancellationError", () => { + it("matches the SDK-internal cancellation code", () => { + expect(isCustomerCancellationError({ code: "customer.canceled" })).toBe(true) + }) + + it("matches the API cancellation code raised by gateway-hosted pages", () => { + expect(isCustomerCancellationError({ code: "customer.cancelled" })).toBe(true) + }) + + it("rejects other error codes", () => { + expect(isCustomerCancellationError({ code: "customer.popup-blocked" })).toBe(false) + expect(isCustomerCancellationError({ code: "card.declined" })).toBe(false) + expect(isCustomerCancellationError({})).toBe(false) + }) +}) diff --git a/test/support/loadNamespace.ts b/test/support/loadNamespace.ts index 546c1ce..d998956 100644 --- a/test/support/loadNamespace.ts +++ b/test/support/loadNamespace.ts @@ -56,3 +56,16 @@ export function loadApmUtils(navigator: FakeNavigator = {}): Record run(moduleShim, moduleShim.exports, navigator) return moduleShim.exports } + +/** + * Load a self-contained namespace source file and return its `ProcessOut` + * namespace. Only suitable for files with no dependencies on the rest of + * the bundle or on browser globals. + */ +export function loadNamespaceFile(relativePath: string): Record { + const js = compile(relativePath) + const moduleShim = { exports: {} as Record } + const run = new Function("module", "exports", `${js}\nmodule.exports = ProcessOut;`) + run(moduleShim, moduleShim.exports) + return moduleShim.exports +}