From 40b7330c077dd49a9182cb6ee0905c30a27b9efc Mon Sep 17 00:00:00 2001 From: "a.kornienko" Date: Thu, 7 Mar 2024 15:53:41 +0300 Subject: [PATCH] feat(PAYMENTS-18338): add apple-pay redirect integration --- index.html | 92 ++++++++++ legal-component.html | 37 ++++ partner.html | 112 ++++++++++++ src/core/event-name.enum.ts | 2 + .../open-apple-pay-page.guard.spec.ts | 14 ++ .../apple-pay/open-apple-pay-page.guard.ts | 12 ++ .../headless-checkout.spec.ts | 26 +-- .../headless-checkout/headless-checkout.ts | 17 +- .../open-apple-pay-page.handler.spec.ts | 26 +++ .../apple-pay/open-apple-pay-page.handler.ts | 21 +++ .../apple-pay/actions-to-stop-waiting.set.ts | 7 + .../apple-pay-button-classname.const.ts | 1 + .../apple-pay/apple-pay-commands.enum.ts | 4 + .../apple-pay/apple-pay-id.const.ts | 1 + .../apple-pay/apple-pay.component.spec.ts | 12 ++ .../apple-pay/apple-pay.component.ts | 164 ++++++++++++++++++ .../apple-pay/apple-pay.template.ts | 13 +- .../waiting-processing-classname.const.ts | 1 + .../apple-pay/waiting-processing.template.ts | 8 + .../submit-button.component.spec.ts | 17 +- .../submit-button/submit-button.component.ts | 26 ++- 21 files changed, 587 insertions(+), 26 deletions(-) create mode 100644 index.html create mode 100644 legal-component.html create mode 100644 partner.html create mode 100644 src/core/guards/apple-pay/open-apple-pay-page.guard.spec.ts create mode 100644 src/core/guards/apple-pay/open-apple-pay-page.guard.ts create mode 100644 src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.spec.ts create mode 100644 src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.ts create mode 100644 src/features/headless-checkout/web-components/apple-pay/actions-to-stop-waiting.set.ts create mode 100644 src/features/headless-checkout/web-components/apple-pay/apple-pay-button-classname.const.ts create mode 100644 src/features/headless-checkout/web-components/apple-pay/apple-pay-commands.enum.ts create mode 100644 src/features/headless-checkout/web-components/apple-pay/apple-pay-id.const.ts create mode 100644 src/features/headless-checkout/web-components/apple-pay/waiting-processing-classname.const.ts create mode 100644 src/features/headless-checkout/web-components/apple-pay/waiting-processing.template.ts diff --git a/index.html b/index.html new file mode 100644 index 0000000..bf1dc70 --- /dev/null +++ b/index.html @@ -0,0 +1,92 @@ + + + + + + + Document + + + +

Partner

+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/legal-component.html b/legal-component.html new file mode 100644 index 0000000..f3da16a --- /dev/null +++ b/legal-component.html @@ -0,0 +1,37 @@ + + + + + + + Document + + + + +

Partner

+
+ + + + diff --git a/partner.html b/partner.html new file mode 100644 index 0000000..58121e6 --- /dev/null +++ b/partner.html @@ -0,0 +1,112 @@ + + + + + + + Document + + + +

Partner

+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/event-name.enum.ts b/src/core/event-name.enum.ts index 45ed4c4..17d36cb 100644 --- a/src/core/event-name.enum.ts +++ b/src/core/event-name.enum.ts @@ -28,4 +28,6 @@ export const enum EventName { finishLoadComponent = 'finishLoadComponent', getFormStatus = 'getFormStatus', applePayError = 'applePayError', + openApplePayPage = 'openApplePayPage', + submitApplePayForm = 'submitApplePayForm', } diff --git a/src/core/guards/apple-pay/open-apple-pay-page.guard.spec.ts b/src/core/guards/apple-pay/open-apple-pay-page.guard.spec.ts new file mode 100644 index 0000000..3dfd458 --- /dev/null +++ b/src/core/guards/apple-pay/open-apple-pay-page.guard.spec.ts @@ -0,0 +1,14 @@ +import 'reflect-metadata'; +import { EventName } from '../../event-name.enum'; +import { isOpenApplePayPageEventMessage } from './open-apple-pay-page.guard'; + +describe('Event message type guard', () => { + it('Should return true', () => { + expect( + isOpenApplePayPageEventMessage({ name: EventName.openApplePayPage }) + ).toBeTruthy(); + }); + it('Should return false', () => { + expect(isOpenApplePayPageEventMessage({})).toBeFalsy(); + }); +}); diff --git a/src/core/guards/apple-pay/open-apple-pay-page.guard.ts b/src/core/guards/apple-pay/open-apple-pay-page.guard.ts new file mode 100644 index 0000000..b62dc0c --- /dev/null +++ b/src/core/guards/apple-pay/open-apple-pay-page.guard.ts @@ -0,0 +1,12 @@ +import { Message } from '../../message.interface'; +import { isEventMessage } from '../event-message.guard'; +import { EventName } from '../../event-name.enum'; + +export const isOpenApplePayPageEventMessage = ( + messageData: unknown +): messageData is Message<{ redirectUrl: string } | null | undefined> => { + if (isEventMessage(messageData)) { + return messageData.name === EventName.openApplePayPage; + } + return false; +}; diff --git a/src/features/headless-checkout/headless-checkout.spec.ts b/src/features/headless-checkout/headless-checkout.spec.ts index 90a9fb7..31b67e1 100644 --- a/src/features/headless-checkout/headless-checkout.spec.ts +++ b/src/features/headless-checkout/headless-checkout.spec.ts @@ -8,6 +8,7 @@ import { LocalizeService } from '../../core/i18n/localize.service'; import { getFinanceDetailsHandler } from './post-messages-handlers/get-finance-details.handler'; import { FormStatus } from '../../core/status/form-status.enum'; import { noopStub } from '../../tests/stubs/noop.stub'; +import { headlessCheckoutAppUrl } from './environment'; const mockMessage: Message = { name: EventName.initPayment, @@ -19,7 +20,6 @@ const mockHandler: Handler = (): null => { }; class MockIframeElement { - public src = ''; public width = ''; public height = ''; public style = { border: '' }; @@ -46,6 +46,13 @@ describe('HeadlessCheckout', () => { beforeEach(() => { windowService = window; + spyOn(windowService, 'addEventListener').and.callFake( + (name: string, handlerWrapper: EventListenerOrEventListenerObject) => { + (handlerWrapper as (message: MessageEvent) => void)({ + origin: headlessCheckoutAppUrl, + } as MessageEvent); + } + ); postMessagesClient = { init: stub, send: stub, @@ -75,9 +82,8 @@ describe('HeadlessCheckout', () => { const spy = spyOn(postMessagesClient, 'init'); spyOn(windowService.document.body, 'appendChild'); spyOn(windowService.document, 'createElement').and.callFake( - () => new MockIframeElement() as unknown as HTMLIFrameElement, + () => new MockIframeElement() as unknown as HTMLIFrameElement ); - await headlessCheckout.init({ isWebview: false }); expect(spy).toHaveBeenCalled(); }); @@ -85,7 +91,7 @@ describe('HeadlessCheckout', () => { it('Should init localization', () => { const localizeSpy = spyOn( localizeService, - 'initDictionaries', + 'initDictionaries' ).and.resolveTo(); void headlessCheckout.init({ isWebview: false }); @@ -104,7 +110,7 @@ describe('HeadlessCheckout', () => { headlessCheckout.events.onCoreEvent( EventName.initPayment, mockHandler, - stub, + stub ); expect(spy).toHaveBeenCalled(); }); @@ -113,7 +119,7 @@ describe('HeadlessCheckout', () => { const spy = spyOn(postMessagesClient, 'listen'); spyOn(windowService.document.body, 'appendChild'); spyOn(windowService.document, 'createElement').and.callFake( - () => new MockIframeElement() as unknown as HTMLIFrameElement, + () => new MockIframeElement() as unknown as HTMLIFrameElement ); await headlessCheckout.init({ isWebview: false }); @@ -145,7 +151,7 @@ describe('HeadlessCheckout', () => { await headlessCheckout.getFinanceDetails(); expect(spy).toHaveBeenCalledWith( { name: EventName.financeDetails }, - getFinanceDetailsHandler, + getFinanceDetailsHandler ); }); @@ -166,7 +172,7 @@ describe('HeadlessCheckout', () => { spyOn(windowService.customElements, 'get').and.returnValue(undefined); spyOn(windowService.document.body, 'appendChild'); spyOn(windowService.document, 'createElement').and.callFake( - () => new MockIframeElement() as unknown as HTMLIFrameElement, + () => new MockIframeElement() as unknown as HTMLIFrameElement ); await headlessCheckout.init({ isWebview: false }); @@ -175,11 +181,11 @@ describe('HeadlessCheckout', () => { it('Should web components not be redefined', async () => { spyOn(windowService.customElements, 'get').and.returnValue( - CustomElementMock, + CustomElementMock ); spyOn(windowService.document.body, 'appendChild'); spyOn(windowService.document, 'createElement').and.callFake( - () => new MockIframeElement() as unknown as HTMLIFrameElement, + () => new MockIframeElement() as unknown as HTMLIFrameElement ); const spy = spyOn(window.customElements, 'define'); diff --git a/src/features/headless-checkout/headless-checkout.ts b/src/features/headless-checkout/headless-checkout.ts index 60ebdc0..6be3e24 100644 --- a/src/features/headless-checkout/headless-checkout.ts +++ b/src/features/headless-checkout/headless-checkout.ts @@ -68,6 +68,7 @@ export class HeadlessCheckout { * @returns {Form} form details */ init: async (configuration: FormConfiguration): Promise
=> { + this._formConfiguration = configuration; this.formStatus = FormStatus.pending; const msg: Message = { @@ -128,6 +129,9 @@ export class HeadlessCheckout { this.formStatus = FormStatus.active; }, }; + public get formConfiguration(): FormConfiguration | undefined { + return this._formConfiguration; + } private formStatus: FormStatus = FormStatus.undefined; private isWebView?: boolean; @@ -135,6 +139,7 @@ export class HeadlessCheckout { private coreIframe!: HTMLIFrameElement; private errorsSubscription?: () => void; private readonly headlessAppUrl = headlessCheckoutAppUrl; + private _formConfiguration?: FormConfiguration; public constructor( private readonly window: Window, @@ -307,10 +312,18 @@ export class HeadlessCheckout { this.coreIframe.src = `${this.headlessAppUrl}/core`; this.coreIframe.name = 'core'; this.window.document.body.appendChild(this.coreIframe); + return this.listenCoreIframeLoading(); + } + + private async listenCoreIframeLoading(): Promise { return new Promise((resolve) => { - this.coreIframe.onload = () => { - resolve(); + const handler = (event: MessageEvent): void => { + if (event.origin === this.headlessAppUrl) { + resolve(); + this.window.removeEventListener('message', handler); + } }; + this.window.addEventListener('message', handler); }); } diff --git a/src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.spec.ts b/src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.spec.ts new file mode 100644 index 0000000..64bff38 --- /dev/null +++ b/src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.spec.ts @@ -0,0 +1,26 @@ +import { Message } from '../../../../core/message.interface'; +import { EventName } from '../../../../core/event-name.enum'; +import { openApplePayPageHandler } from './open-apple-pay-page.handler'; + +const mockMessage: Message<{ redirectUrl: string } | null | undefined> = { + name: EventName.openApplePayPage, + data: { + redirectUrl: 'url', + }, +}; + +describe('openApplePayPageHandler', () => { + it('Should handle data', () => { + expect(openApplePayPageHandler(mockMessage)).toEqual({ + isHandled: true, + value: { + redirectUrl: 'url', + }, + }); + }); + it('Should return null', () => { + expect( + openApplePayPageHandler({ name: EventName.getSavedMethods }) + ).toBeNull(); + }); +}); diff --git a/src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.ts b/src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.ts new file mode 100644 index 0000000..886ce7e --- /dev/null +++ b/src/features/headless-checkout/post-messages-handlers/apple-pay/open-apple-pay-page.handler.ts @@ -0,0 +1,21 @@ +import { Handler } from '../../../../core/post-messages-client/handler.type'; +import { Message } from '../../../../core/message.interface'; +import { isOpenApplePayPageEventMessage } from '../../../../core/guards/apple-pay/open-apple-pay-page.guard'; + +export const openApplePayPageHandler: Handler< + { redirectUrl: string } | null | undefined +> = ( + message: Message +): { + isHandled: boolean; + value: { redirectUrl: string } | null | undefined; +} | null => { + if (!isOpenApplePayPageEventMessage(message)) { + return null; + } + + return { + isHandled: true, + value: message.data, + }; +}; diff --git a/src/features/headless-checkout/web-components/apple-pay/actions-to-stop-waiting.set.ts b/src/features/headless-checkout/web-components/apple-pay/actions-to-stop-waiting.set.ts new file mode 100644 index 0000000..1de631a --- /dev/null +++ b/src/features/headless-checkout/web-components/apple-pay/actions-to-stop-waiting.set.ts @@ -0,0 +1,7 @@ +import { NextAction } from '../../../../core/actions/next-action.interface'; + +export const actionsToStopWaiting: Set = new Set([ + 'show_fields', + 'status_updated', + 'show_errors', +]); diff --git a/src/features/headless-checkout/web-components/apple-pay/apple-pay-button-classname.const.ts b/src/features/headless-checkout/web-components/apple-pay/apple-pay-button-classname.const.ts new file mode 100644 index 0000000..60a201a --- /dev/null +++ b/src/features/headless-checkout/web-components/apple-pay/apple-pay-button-classname.const.ts @@ -0,0 +1 @@ +export const applePayButtonClassname = 'psdk-apple-pay'; diff --git a/src/features/headless-checkout/web-components/apple-pay/apple-pay-commands.enum.ts b/src/features/headless-checkout/web-components/apple-pay/apple-pay-commands.enum.ts new file mode 100644 index 0000000..ffdff35 --- /dev/null +++ b/src/features/headless-checkout/web-components/apple-pay/apple-pay-commands.enum.ts @@ -0,0 +1,4 @@ +export enum ApplePayCommands { + applePayReturn = 'apple-pay-return', + applePaySendToken = 'apple-pay-send-token', +} diff --git a/src/features/headless-checkout/web-components/apple-pay/apple-pay-id.const.ts b/src/features/headless-checkout/web-components/apple-pay/apple-pay-id.const.ts new file mode 100644 index 0000000..b9f622b --- /dev/null +++ b/src/features/headless-checkout/web-components/apple-pay/apple-pay-id.const.ts @@ -0,0 +1 @@ +export const applePayId = 3175; diff --git a/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.spec.ts b/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.spec.ts index 7cae836..3d69c5f 100644 --- a/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.spec.ts +++ b/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.spec.ts @@ -5,6 +5,7 @@ import { FormSpy } from '../../../../core/spy/form-spy/form-spy'; import { HeadlessCheckout } from '../../headless-checkout'; import { ApplePayComponent } from './apple-pay.component'; import { EventName } from '../../../../core/event-name.enum'; +import { PostMessagesClient } from '../../../../core/post-messages-client/post-messages-client'; function createComponent(): HTMLElement { const element = document.createElement(WebComponentTagName.ApplePayComponent); @@ -14,6 +15,7 @@ function createComponent(): HTMLElement { describe('ApplePayComponent', () => { let headlessCheckout: HeadlessCheckout; + let postMessagesClient: PostMessagesClient; let formSpy: FormSpy; window.customElements.define( @@ -40,6 +42,10 @@ describe('ApplePayComponent', () => { }, } as unknown as FormSpy; + postMessagesClient = { + send: noopStub, + } as unknown as PostMessagesClient; + container.clearInstances(); container @@ -48,6 +54,12 @@ describe('ApplePayComponent', () => { }) .register(HeadlessCheckout, { useValue: headlessCheckout, + }) + .register(PostMessagesClient, { + useValue: postMessagesClient, + }) + .register(Window, { + useValue: window, }); }); diff --git a/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.ts b/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.ts index 66272df..8ba4ecd 100644 --- a/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.ts +++ b/src/features/headless-checkout/web-components/apple-pay/apple-pay.component.ts @@ -10,16 +10,34 @@ import i18next from 'i18next'; import { FormSpy } from '../../../../core/spy/form-spy/form-spy'; import { finishLoadComponentHandler } from '../../post-messages-handlers/finish-load-component.handler'; import { headlessCheckoutAppUrl } from '../../environment'; +import { openApplePayPageHandler } from '../../post-messages-handlers/apple-pay/open-apple-pay-page.handler'; +import { Message } from '../../../../core/message.interface'; +import { ApplePayCommands } from './apple-pay-commands.enum'; +import { PostMessagesClient } from '../../../../core/post-messages-client/post-messages-client'; +import { nextActionHandler } from '../../post-messages-handlers/next-action.handler'; +import { NextAction } from '../../../../core/actions/next-action.interface'; +import { getWaitingProcessingTemplate } from './waiting-processing.template'; +import { waitingProcessingClassname } from './waiting-processing-classname.const'; +import { applePayButtonClassname } from './apple-pay-button-classname.const'; +import { actionsToStopWaiting } from './actions-to-stop-waiting.set'; export class ApplePayComponent extends SecureComponentAbstract { protected componentName: string | null = 'pages/apple-pay'; private readonly headlessCheckout: HeadlessCheckout; + private readonly postMessagesClient: PostMessagesClient; private readonly formSpy: FormSpy; + private readonly window: Window; + private readonly listenApplePayWindowCloseDelay = 100; + private applePayWindow?: Window | null; + private listenApplePayWindowCloseTimeout?: ReturnType; + private isWaitingPayment = false; public constructor() { super(); this.headlessCheckout = container.resolve(HeadlessCheckout); + this.postMessagesClient = container.resolve(PostMessagesClient); this.formSpy = container.resolve(FormSpy); + this.window = container.resolve(Window); this.headlessCheckout.events.onCoreEvent( EventName.applePayError, @@ -31,6 +49,16 @@ export class ApplePayComponent extends SecureComponentAbstract { } ); + this.headlessCheckout.events.onCoreEvent( + EventName.openApplePayPage, + openApplePayPageHandler, + (res) => { + if (res?.redirectUrl) { + this.openRedirectPage(res.redirectUrl); + } + } + ); + this.headlessCheckout.events.onCoreEvent( EventName.finishLoadComponent, finishLoadComponentHandler, @@ -77,6 +105,142 @@ export class ApplePayComponent extends SecureComponentAbstract { const errorElement = document.createElement('p'); errorElement.classList.add('apple-pay-error'); errorElement.textContent = i18next.t(errorMessage); + errorsContainer.innerHTML = ''; errorsContainer.append(errorElement); } + + private setupWaitingPayment(isWaiting: boolean): void { + this.isWaitingPayment = isWaiting; + if (isWaiting) { + this.drawWaitingElement(); + this.hidePayButton(); + return; + } + this.removeWaitingElement(); + this.showPayButton(); + } + + private hidePayButton(): void { + const applePayButton: HTMLElement | null = this.querySelector( + `.${applePayButtonClassname}` + ); + if (applePayButton) { + applePayButton.style.visibility = 'hidden'; + applePayButton.style.position = 'absolute'; + } + } + + private showPayButton(): void { + const applePayButton: HTMLElement | null = this.querySelector( + `.${applePayButtonClassname}` + ); + if (applePayButton) { + applePayButton.style.visibility = 'visible'; + applePayButton.style.position = 'static'; + } + } + + private drawWaitingElement(): void { + const waitingProcessingWrapper = document.createElement('div'); + waitingProcessingWrapper.classList.add(waitingProcessingClassname); + waitingProcessingWrapper.innerHTML = getWaitingProcessingTemplate( + i18next.t('status.processing.title'), + i18next.t('status.processing.description') + ); + this.append(waitingProcessingWrapper); + } + + private removeWaitingElement(): void { + const waitingProcessingWrapper = this.querySelector( + `.${waitingProcessingClassname}` + ); + if (waitingProcessingWrapper) { + waitingProcessingWrapper.remove(); + } + } + + private openRedirectPage(redirectUrl: string): void { + this.setupWaitingPayment(true); + this.applePayWindow = this.window.open(redirectUrl); + this.listenApplePayWindowCloseEvent(); + this.window.addEventListener('message', this.handleApplePayWindowMessages); + } + + private listenApplePayWindowCloseEvent(): void { + if (!this.applePayWindow) { + return; + } + + this.listenApplePayWindowCloseTimeout = setTimeout(() => { + if (this.applePayWindow?.closed) { + console.log( + 'listenApplePayWindowCloseTimeout', + this.listenApplePayWindowCloseTimeout + ); + this.destroyApplePayWindow(); + + void this.submitForm(); + } else { + this.listenApplePayWindowCloseEvent(); + } + }, this.listenApplePayWindowCloseDelay); + } + + private readonly handleApplePayWindowMessages = ( + event: MessageEvent + ): void => { + const message = JSON.parse(event.data) as { + command: string; + data: Message['data']; + }; + if (message.command === ApplePayCommands.applePayReturn) { + this.destroyApplePayWindow(); + void this.submitForm(); + return; + } + if (message.command === ApplePayCommands.applePaySendToken) { + this.destroyApplePayWindow(); + void this.submitForm( + (message.data as { applePayToken: string }).applePayToken + ); + return; + } + }; + + private destroyApplePayWindow(stopLoading?: boolean): void { + this.window.focus(); + if (this.applePayWindow && !this.applePayWindow.closed) { + this.applePayWindow.close(); + } + if (this.listenApplePayWindowCloseTimeout) { + clearTimeout(this.listenApplePayWindowCloseTimeout); + } + this.window.removeEventListener( + 'message', + this.handleApplePayWindowMessages + ); + + if (stopLoading) { + this.setupWaitingPayment(false); + } + } + + private async submitForm(tokenNotice?: string): Promise { + const msg: Message<{ tokenNotice?: string }> = { + name: EventName.submitApplePayForm, + data: { + tokenNotice, + }, + }; + + return this.postMessagesClient.send(msg, (message) => { + return nextActionHandler(message, (data: unknown) => { + if ((data as NextAction)?.type) { + const nextAction = data as NextAction; + this.setupWaitingPayment(!actionsToStopWaiting.has(nextAction.type)); + } + return true; + }); + }) as Promise; + } } diff --git a/src/features/headless-checkout/web-components/apple-pay/apple-pay.template.ts b/src/features/headless-checkout/web-components/apple-pay/apple-pay.template.ts index 2ebca1f..c9db2aa 100644 --- a/src/features/headless-checkout/web-components/apple-pay/apple-pay.template.ts +++ b/src/features/headless-checkout/web-components/apple-pay/apple-pay.template.ts @@ -1,18 +1,13 @@ -import { TextComponentConfig } from '../text-component/text-component.config.interface'; import { errorsHtmlWrapperClassName } from './errors-html-wrapper-classname.const'; - -export interface CardNumberComponentData extends TextComponentConfig { - secureHtml: string; - isCardIconShown: boolean; -} +import { applePayButtonClassname } from './apple-pay-button-classname.const'; export const getApplePayComponentTemplate = ( secureHtml: string, error?: string ): string => { return ` -
+
${error ?? ''}
+
${secureHtml} -
-
${error ?? ''}
`; +
`; }; diff --git a/src/features/headless-checkout/web-components/apple-pay/waiting-processing-classname.const.ts b/src/features/headless-checkout/web-components/apple-pay/waiting-processing-classname.const.ts new file mode 100644 index 0000000..a48f8fa --- /dev/null +++ b/src/features/headless-checkout/web-components/apple-pay/waiting-processing-classname.const.ts @@ -0,0 +1 @@ +export const waitingProcessingClassname = 'apple-pay-waiting-processing'; diff --git a/src/features/headless-checkout/web-components/apple-pay/waiting-processing.template.ts b/src/features/headless-checkout/web-components/apple-pay/waiting-processing.template.ts new file mode 100644 index 0000000..b07c180 --- /dev/null +++ b/src/features/headless-checkout/web-components/apple-pay/waiting-processing.template.ts @@ -0,0 +1,8 @@ +export const getWaitingProcessingTemplate = ( + title: string, + description: string +): string => { + return ` +

${title}

+

${description}

`; +}; diff --git a/src/features/headless-checkout/web-components/submit-button/submit-button.component.spec.ts b/src/features/headless-checkout/web-components/submit-button/submit-button.component.spec.ts index 2df4cae..52069fc 100644 --- a/src/features/headless-checkout/web-components/submit-button/submit-button.component.spec.ts +++ b/src/features/headless-checkout/web-components/submit-button/submit-button.component.spec.ts @@ -4,10 +4,11 @@ import { PostMessagesClient } from '../../../../core/post-messages-client/post-m import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; import { HeadlessCheckout } from '../../headless-checkout'; import { noopStub } from '../../../../tests/stubs/noop.stub'; +import { FormSpy } from '../../../../core/spy/form-spy/form-spy'; function createComponent(): void { const element = document.createElement( - WebComponentTagName.SubmitButtonComponent, + WebComponentTagName.SubmitButtonComponent ); element.setAttribute('text', 'Pay Now'); element.setAttribute('id', 'test'); @@ -17,10 +18,11 @@ function createComponent(): void { describe('SubmitButtonComponent', () => { let postMessagesClient: PostMessagesClient; let headlessCheckout: HeadlessCheckout; + let formSpy: FormSpy; window.customElements.define( WebComponentTagName.SubmitButtonComponent, - SubmitButtonComponent, + SubmitButtonComponent ); beforeEach(() => { @@ -38,9 +40,19 @@ describe('SubmitButtonComponent', () => { }, } as unknown as HeadlessCheckout; + formSpy = { + listenFormInit: noopStub, + get formWasInit() { + return; + }, + } as unknown as FormSpy; + container.clearInstances(); container + .register(FormSpy, { + useValue: formSpy, + }) .register(PostMessagesClient, { useValue: postMessagesClient, }) @@ -51,6 +63,7 @@ describe('SubmitButtonComponent', () => { afterEach(() => { document.body.innerHTML = ''; + spyOnProperty(formSpy, 'formWasInit').and.returnValue(true); }); it('Should set text for button', () => { diff --git a/src/features/headless-checkout/web-components/submit-button/submit-button.component.ts b/src/features/headless-checkout/web-components/submit-button/submit-button.component.ts index cfb1d54..39810c4 100644 --- a/src/features/headless-checkout/web-components/submit-button/submit-button.component.ts +++ b/src/features/headless-checkout/web-components/submit-button/submit-button.component.ts @@ -9,10 +9,13 @@ import { submitButtonHandler } from './submit-button.handler'; import { Message } from '../../../../core/message.interface'; import { Handler } from '../../../../core/post-messages-client/handler.type'; import { isSubmitButtonLoadingMessage } from '../../../../core/guards/submit-button-loading-message.guard'; +import { FormSpy } from '../../../../core/spy/form-spy/form-spy'; +import { applePayId } from '../apple-pay/apple-pay-id.const'; export class SubmitButtonComponent extends WebComponentAbstract { private readonly postMessagesClient: PostMessagesClient; private readonly headlessCheckout: HeadlessCheckout; + private readonly formSpy: FormSpy; private get elementRef(): HTMLButtonElement { return this.querySelector('button')! as HTMLButtonElement; @@ -22,12 +25,24 @@ export class SubmitButtonComponent extends WebComponentAbstract { super(); this.postMessagesClient = container.resolve(PostMessagesClient); this.headlessCheckout = container.resolve(HeadlessCheckout); + this.formSpy = container.resolve(FormSpy); } public static get observedAttributes(): string[] { return [SubmitButtonAttributes.isLoading, SubmitButtonAttributes.text]; } + protected connectedCallback(): void { + this.startLoadingComponentHandler(); + + if (!this.formSpy.formWasInit) { + this.formSpy.listenFormInit(() => this.connectedCallback()); + return; + } + + this.render(); + } + protected render(): void { this.removeAllEventListeners(); this.innerHTML = this.getHtml(); @@ -40,7 +55,7 @@ export class SubmitButtonComponent extends WebComponentAbstract { void this.postMessagesClient.send( { name: EventName.getFormStatus }, - this.loadingHandler, + this.loadingHandler ); let isCheckedFieldStatuses = false; @@ -66,7 +81,7 @@ export class SubmitButtonComponent extends WebComponentAbstract { void this.postMessagesClient.send( { name: EventName.submitForm }, - submitButtonHandler, + submitButtonHandler ); this.render(); @@ -75,6 +90,11 @@ export class SubmitButtonComponent extends WebComponentAbstract { } protected getHtml(): string { + if ( + this.headlessCheckout.formConfiguration?.paymentMethodId === applePayId + ) { + return ''; + } const text = this.getAttribute(SubmitButtonAttributes.text) ?? ''; const isLoading = !!this.getAttribute(SubmitButtonAttributes.isLoading); @@ -82,7 +102,7 @@ export class SubmitButtonComponent extends WebComponentAbstract { } private readonly loadingHandler: Handler = ( - message: Message, + message: Message ): { isHandled: boolean } | null => { if (!isSubmitButtonLoadingMessage(message)) return null;