diff --git a/package.json b/package.json
index a3bd89e..76bc687 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",
diff --git a/src/dynamic-checkout/clients/apple-pay.ts b/src/dynamic-checkout/clients/apple-pay.ts
index ce6b8e4..0240b43 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 06ab01b..fdfd149 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 66953ae..8467265 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 5491143..07a61d8 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 3000295..cbabb80 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 0000000..fd18017
--- /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()
+ })
+})