From fa4256fda7296e8b97bd3c3e99f16022a4eba4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jasi=C5=84ski?= Date: Fri, 23 Oct 2026 08:49:42 +0200 Subject: [PATCH 1/2] fix: hide express checkout header when no wallet button is displayed --- src/dynamic-checkout/clients/apple-pay.ts | 5 +- src/dynamic-checkout/clients/google-pay.ts | 5 +- .../payment-methods/apple-pay.ts | 8 +- .../payment-methods/google-pay.ts | 3 +- src/dynamic-checkout/views/payment-methods.ts | 18 +++ .../wallet-availability.test.ts | 110 ++++++++++++++++++ 6 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 test/dynamic-checkout/wallet-availability.test.ts diff --git a/src/dynamic-checkout/clients/apple-pay.ts b/src/dynamic-checkout/clients/apple-pay.ts index ce6b8e46..0240b435 100644 --- a/src/dynamic-checkout/clients/apple-pay.ts +++ b/src/dynamic-checkout/clients/apple-pay.ts @@ -14,12 +14,13 @@ module ProcessOut { buttonContainer: HTMLElement, invoiceData: Invoice, getViewContainer: () => HTMLElement, + onButtonDisplayed: () => void, ) { const applePayScript = document.createElement("script") applePayScript.src = applePaySdkUrl applePayScript.onload = () => { buttonContainer.innerHTML = `` - this.initializeApplePay(invoiceData, buttonContainer, getViewContainer) + this.initializeApplePay(invoiceData, buttonContainer, getViewContainer, onButtonDisplayed) } document.head.appendChild(applePayScript) @@ -29,6 +30,7 @@ module ProcessOut { invoiceData: Invoice, buttonContainer: HTMLElement, getViewContainer: () => HTMLElement, + onButtonDisplayed: () => void, ) { const applePayPaymentMethodData = this.getApplePayPaymentMethodData(invoiceData) @@ -38,6 +40,7 @@ module ProcessOut { console.log(err) } else { buttonContainer.classList.add("visible") + onButtonDisplayed() document.querySelector("apple-pay-button").addEventListener("click", () => { this.tokenizeApplePay(invoiceData, getViewContainer) diff --git a/src/dynamic-checkout/clients/google-pay.ts b/src/dynamic-checkout/clients/google-pay.ts index 06ab01b2..fdfd149f 100644 --- a/src/dynamic-checkout/clients/google-pay.ts +++ b/src/dynamic-checkout/clients/google-pay.ts @@ -63,6 +63,7 @@ module ProcessOut { buttonContainer: HTMLElement, invoiceData: Invoice, getViewContainer: () => HTMLElement, + onButtonDisplayed: () => void, ) { const googleClientScript = document.createElement("script") googleClientScript.src = googlePaySdkUrl @@ -81,7 +82,7 @@ module ProcessOut { }) : null - this.initizalizeGooglePay(buttonContainer, getViewContainer, invoiceData) + this.initizalizeGooglePay(buttonContainer, getViewContainer, invoiceData, onButtonDisplayed) } document.head.appendChild(googleClientScript) @@ -91,6 +92,7 @@ module ProcessOut { buttonContainer: HTMLElement, getViewContainer: () => HTMLElement, invoiceData: Invoice, + onButtonDisplayed: () => void, ) { this.prepareIsReadyToPayRequest(invoiceData) this.preparePaymentRequest(invoiceData) @@ -108,6 +110,7 @@ module ProcessOut { }) buttonContainer.appendChild(button) + onButtonDisplayed() } }) .catch(error => diff --git a/src/dynamic-checkout/payment-methods/apple-pay.ts b/src/dynamic-checkout/payment-methods/apple-pay.ts index 66953aed..84672652 100644 --- a/src/dynamic-checkout/payment-methods/apple-pay.ts +++ b/src/dynamic-checkout/payment-methods/apple-pay.ts @@ -10,10 +10,16 @@ module ProcessOut { paymentConfig: DynamicCheckoutPaymentConfig, invoiceData: Invoice, resetContainerHtml: () => HTMLElement, + onButtonDisplayed: () => void, ) { this.applePayClient = new ApplePayClient(processOutInstance, paymentConfig) this.element = this.getApplePayButtonElement() - this.applePayClient.loadButton(this.element, invoiceData, resetContainerHtml) + this.applePayClient.loadButton( + this.element, + invoiceData, + resetContainerHtml, + onButtonDisplayed, + ) } private getApplePayButtonElement() { diff --git a/src/dynamic-checkout/payment-methods/google-pay.ts b/src/dynamic-checkout/payment-methods/google-pay.ts index 54911436..07a61d8a 100644 --- a/src/dynamic-checkout/payment-methods/google-pay.ts +++ b/src/dynamic-checkout/payment-methods/google-pay.ts @@ -10,10 +10,11 @@ module ProcessOut { paymentConfig: DynamicCheckoutPaymentConfig, invoiceData: Invoice, resetContainerHtml: () => HTMLElement, + onButtonDisplayed: () => void, ) { this.googleClient = new GooglePayClient(processOutInstance, paymentConfig) this.element = this.getGooglePayButtonElement() - this.googleClient.loadButton(this.element, invoiceData, resetContainerHtml) + this.googleClient.loadButton(this.element, invoiceData, resetContainerHtml, onButtonDisplayed) } private getGooglePayButtonElement() { diff --git a/src/dynamic-checkout/views/payment-methods.ts b/src/dynamic-checkout/views/payment-methods.ts index 3000295b..cbabb808 100644 --- a/src/dynamic-checkout/views/payment-methods.ts +++ b/src/dynamic-checkout/views/payment-methods.ts @@ -73,6 +73,12 @@ module ProcessOut { } if (expressPaymentMethods.length > 0 || walletPaymentMethods.length > 0) { + // Wallets confirm availability asynchronously, so a wallets-only section + // stays hidden until one of them actually displays its button + if (expressPaymentMethods.length === 0) { + expressCheckoutWrapper.style.display = "none" + } + wrapper.appendChild(expressCheckoutWrapper) } @@ -251,6 +257,7 @@ module ProcessOut { this.paymentConfig, this.paymentConfig.invoiceDetails, this.resetContainerHtml.bind(this), + this.showExpressCheckoutWrapper.bind(this), ) return walletPaymentMethods.push(googlePayPaymentMethod) @@ -267,6 +274,7 @@ module ProcessOut { this.paymentConfig, this.paymentConfig.invoiceDetails, this.resetContainerHtml.bind(this), + this.showExpressCheckoutWrapper.bind(this), ) return walletPaymentMethods.push(applePayPaymentMethod) @@ -354,6 +362,16 @@ module ProcessOut { return this.element } + private showExpressCheckoutWrapper() { + const expressCheckoutWrapper = this.element.querySelector( + ".dco-express-checkout-wrapper", + ) as HTMLElement + + if (expressCheckoutWrapper) { + expressCheckoutWrapper.style.display = "" + } + } + private shouldShowSettingsButton() { let shouldShowSettingsButton = false diff --git a/test/dynamic-checkout/wallet-availability.test.ts b/test/dynamic-checkout/wallet-availability.test.ts new file mode 100644 index 00000000..fd180175 --- /dev/null +++ b/test/dynamic-checkout/wallet-availability.test.ts @@ -0,0 +1,110 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { loadDynamicCheckout } from "../support/loadNamespace" + +let ProcessOut: Record + +beforeEach(() => { + ProcessOut = loadDynamicCheckout().namespace +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +function createPaymentConfig() { + const paymentConfig = new ProcessOut.DynamicCheckoutPaymentConfig({ + projectId: "proj_test", + invoiceId: "iv_test", + }) + + paymentConfig.setInvoiceDetails({ payment_methods: [] }) + + return paymentConfig +} + +describe("Apple Pay button availability", () => { + const invoiceData = { + id: "iv_test", + payment_methods: [{ type: "applepay", applepay: { merchant_id: "merchant.test" } }], + } + + function initializeApplePay(availabilityError: unknown) { + const processOutInstance = { + applePay: { + checkAvailability: (callback: (err: unknown) => void) => callback(availabilityError), + }, + } + const client = new ProcessOut.ApplePayClient(processOutInstance, createPaymentConfig()) + const buttonContainer = { classList: { add: vi.fn() } } + const onButtonDisplayed = vi.fn() + + vi.stubGlobal("document", { querySelector: () => ({ addEventListener: vi.fn() }) }) + vi.spyOn(console, "log").mockImplementation(() => {}) + + client.initializeApplePay(invoiceData, buttonContainer, () => ({}), onButtonDisplayed) + + return { buttonContainer, onButtonDisplayed } + } + + it("reports the button as displayed when Apple Pay is available", () => { + const { buttonContainer, onButtonDisplayed } = initializeApplePay(null) + + expect(buttonContainer.classList.add).toHaveBeenCalledWith("visible") + expect(onButtonDisplayed).toHaveBeenCalledOnce() + }) + + it("does not report the button as displayed when the availability check fails", () => { + const { buttonContainer, onButtonDisplayed } = initializeApplePay(new Error("unavailable")) + + expect(buttonContainer.classList.add).not.toHaveBeenCalled() + expect(onButtonDisplayed).not.toHaveBeenCalled() + }) +}) + +describe("Google Pay button availability", () => { + const invoiceData = { + id: "iv_test", + payment_methods: [{ type: "googlepay", display: { name: "GPay" } }], + } + + async function initializeGooglePay(isReadyToPay: () => Promise) { + const client = new ProcessOut.GooglePayClient({}, createPaymentConfig()) + const buttonContainer = { appendChild: vi.fn() } + const onButtonDisplayed = vi.fn() + + client.prepareIsReadyToPayRequest = () => {} + client.preparePaymentRequest = () => {} + client.googleClient = { isReadyToPay, createButton: () => ({}) } + + client.initizalizeGooglePay(buttonContainer, () => ({}), invoiceData, onButtonDisplayed) + await new Promise(resolve => setTimeout(resolve)) + + return { buttonContainer, onButtonDisplayed } + } + + it("reports the button as displayed when Google Pay is ready to pay", async () => { + const { buttonContainer, onButtonDisplayed } = await initializeGooglePay(() => + Promise.resolve({ result: true }), + ) + + expect(buttonContainer.appendChild).toHaveBeenCalledOnce() + expect(onButtonDisplayed).toHaveBeenCalledOnce() + }) + + it("does not report the button as displayed when Google Pay is not ready to pay", async () => { + const { buttonContainer, onButtonDisplayed } = await initializeGooglePay(() => + Promise.resolve({ result: false }), + ) + + expect(buttonContainer.appendChild).not.toHaveBeenCalled() + expect(onButtonDisplayed).not.toHaveBeenCalled() + }) + + it("does not report the button as displayed when the readiness check fails", async () => { + const { onButtonDisplayed } = await initializeGooglePay(() => + Promise.reject(new Error("unavailable")), + ) + + expect(onButtonDisplayed).not.toHaveBeenCalled() + }) +}) From cfb6999526bbe4d87367a488a3bc781028dbcf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jasi=C5=84ski?= Date: Fri, 23 Oct 2026 08:50:34 +0200 Subject: [PATCH 2/2] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3bd89e6..76bc687e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "processout.js", - "version": "1.9.17", + "version": "1.9.18", "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",