From b52e0b2b3904916120bbafccabb3096fbfe50f05 Mon Sep 17 00:00:00 2001 From: "e.kireev" Date: Mon, 4 Mar 2024 20:16:41 +0300 Subject: [PATCH] feat(PAYMENTS-17869): QR code component --- src/core/actions/next-action.interface.ts | 4 +- src/core/actions/show-qr-code.action.type.ts | 5 ++ .../web-component-tag-name.enum.ts | 1 + src/core/web-components/web-components.map.ts | 2 + .../headless-checkout/headless-checkout.ts | 34 ++++++----- .../qr-code/qr-code.component.spec.ts | 61 +++++++++++++++++++ .../qr-code/qr-code.component.ts | 31 ++++++++++ src/web-components.ts | 2 + 8 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 src/core/actions/show-qr-code.action.type.ts create mode 100644 src/features/headless-checkout/web-components/qr-code/qr-code.component.spec.ts create mode 100644 src/features/headless-checkout/web-components/qr-code/qr-code.component.ts diff --git a/src/core/actions/next-action.interface.ts b/src/core/actions/next-action.interface.ts index f3f10b5..e11ad76 100644 --- a/src/core/actions/next-action.interface.ts +++ b/src/core/actions/next-action.interface.ts @@ -5,6 +5,7 @@ import { ShowFieldsAction } from './show-fields.action.type'; import { StatusUpdatedAction } from './status-updated.action.type'; import { ThreeDsAction } from './three-ds/three-ds.action.type'; import { SpecialButtonAction } from './special-button.action.type'; +import { ShowQrCodeAction } from './show-qr-code.action.type'; export type NextAction = | CheckStatusAction @@ -13,4 +14,5 @@ export type NextAction = | ShowErrorsAction | RedirectAction | ThreeDsAction - | SpecialButtonAction; + | SpecialButtonAction + | ShowQrCodeAction; diff --git a/src/core/actions/show-qr-code.action.type.ts b/src/core/actions/show-qr-code.action.type.ts new file mode 100644 index 0000000..45d0d38 --- /dev/null +++ b/src/core/actions/show-qr-code.action.type.ts @@ -0,0 +1,5 @@ +import { Action } from './action.interface'; + +export type ShowQrCodeActionType = 'show_qr_code'; + +export type ShowQrCodeAction = Action; diff --git a/src/core/web-components/web-component-tag-name.enum.ts b/src/core/web-components/web-component-tag-name.enum.ts index e874759..db964ca 100644 --- a/src/core/web-components/web-component-tag-name.enum.ts +++ b/src/core/web-components/web-component-tag-name.enum.ts @@ -17,4 +17,5 @@ export enum WebComponentTagName { PaymentFormMessages = 'psdk-payment-form-messages', GooglePayButtonComponent = 'psdk-google-pay-button', ApplePayComponent = 'psdk-apple-pay', + QrCodeComponent = 'psdk-qr-code', } diff --git a/src/core/web-components/web-components.map.ts b/src/core/web-components/web-components.map.ts index e97eed2..a632b54 100644 --- a/src/core/web-components/web-components.map.ts +++ b/src/core/web-components/web-components.map.ts @@ -16,6 +16,7 @@ import { SavedMethodsComponent } from '../../features/headless-checkout/web-comp import { UserBalanceComponent } from '../../features/headless-checkout/web-components/user-balance-component/user-balance-component'; import { PaymentFormMessagesComponent } from '../../features/headless-checkout/web-components/payment-form-messages/payment-form-messages.component'; import { GooglePayButtonComponent } from '../../features/headless-checkout/web-components/pages/google-pay/google-pay-button.component'; +import { QrCodeComponent } from '../../features/headless-checkout/web-components/qr-code/qr-code.component'; import { ApplePayComponent } from '../../features/headless-checkout/web-components/apple-pay/apple-pay.component'; export const webComponents: { @@ -38,5 +39,6 @@ export const webComponents: { [WebComponentTagName.UserBalanceComponent]: UserBalanceComponent, [WebComponentTagName.PaymentFormMessages]: PaymentFormMessagesComponent, [WebComponentTagName.GooglePayButtonComponent]: GooglePayButtonComponent, + [WebComponentTagName.QrCodeComponent]: QrCodeComponent, [WebComponentTagName.ApplePayComponent]: ApplePayComponent, }; diff --git a/src/features/headless-checkout/headless-checkout.ts b/src/features/headless-checkout/headless-checkout.ts index 60ebdc0..7e641bc 100644 --- a/src/features/headless-checkout/headless-checkout.ts +++ b/src/features/headless-checkout/headless-checkout.ts @@ -55,7 +55,7 @@ export class HeadlessCheckout { onCoreEvent: ( eventName: EventName, handler: Handler, - callback: (value?: T) => void + callback: (value?: T) => void, ): (() => void) => { return this.postMessagesClient.listen(eventName, handler, callback); }, @@ -84,7 +84,7 @@ export class HeadlessCheckout { } this.formSpy.formWasInit = true; this.formStatus = FormStatus.active; - }) + }), ) as Promise
; }, @@ -96,12 +96,12 @@ export class HeadlessCheckout { if (nextAction) { callbackFn(nextAction); } - } + }, ); }, onFieldsStatusChange: ( - callbackFn: (fieldsStatus: FormFieldsStatus) => void + callbackFn: (fieldsStatus: FormFieldsStatus) => void, ): void => { this.postMessagesClient.listen( EventName.formFieldsStatusChanged, @@ -110,7 +110,7 @@ export class HeadlessCheckout { if (fieldsStatus) { callbackFn(fieldsStatus); } - } + }, ); }, @@ -132,6 +132,7 @@ export class HeadlessCheckout { private formStatus: FormStatus = FormStatus.undefined; private isWebView?: boolean; private isSandbox?: boolean; + private isMobile!: boolean; private coreIframe!: HTMLIFrameElement; private errorsSubscription?: () => void; private readonly headlessAppUrl = headlessCheckoutAppUrl; @@ -141,15 +142,17 @@ export class HeadlessCheckout { private readonly postMessagesClient: PostMessagesClient, private readonly localizeService: LocalizeService, private readonly headlessCheckoutSpy: HeadlessCheckoutSpy, - private readonly formSpy: FormSpy + private readonly formSpy: FormSpy, ) {} public async init(environment: { isWebview?: boolean; sandbox?: boolean; + isMobile?: boolean; }): Promise { this.isWebView = environment.isWebview; this.isSandbox = environment.sandbox; + this.isMobile = !!environment.isMobile; await this.localizeService.initDictionaries(); @@ -162,7 +165,7 @@ export class HeadlessCheckout { getErrorHandler, (error) => { throw new Error(error); - } + }, ); } @@ -183,6 +186,7 @@ export class HeadlessCheckout { token, isWebView: this.isWebView, sandbox: this.isSandbox, + isMobile: this.isMobile, }, }, }; @@ -190,7 +194,7 @@ export class HeadlessCheckout { return this.postMessagesClient.send(msg, (message) => setTokenHandler(message, () => { this.headlessCheckoutSpy.appWasInit = true; - }) + }), ); } @@ -200,7 +204,7 @@ export class HeadlessCheckout { name: EventName.setSecureComponentStyles, data: styles, }, - setSecureComponentStylesHandler + setSecureComponentStylesHandler, ); } @@ -215,7 +219,7 @@ export class HeadlessCheckout { return this.postMessagesClient.send( msg, - getFinanceDetailsHandler + getFinanceDetailsHandler, ) as Promise; } @@ -239,7 +243,7 @@ export class HeadlessCheckout { return this.postMessagesClient.send( msg, - getRegularMethodsHandler + getRegularMethodsHandler, ) as Promise; } @@ -259,7 +263,7 @@ export class HeadlessCheckout { return this.postMessagesClient.send( msg, - getQuickMethodsHandler + getQuickMethodsHandler, ) as Promise; } @@ -270,7 +274,7 @@ export class HeadlessCheckout { return this.postMessagesClient.send( msg, - getSavedMethodsHandler + getSavedMethodsHandler, ) as Promise; } @@ -281,7 +285,7 @@ export class HeadlessCheckout { return this.postMessagesClient.send( msg, - getUserBalanceHandler + getUserBalanceHandler, ) as Promise; } @@ -294,7 +298,7 @@ export class HeadlessCheckout { }; return this.postMessagesClient.send(msg, (message) => - getPaymentStatusHandler(message) + getPaymentStatusHandler(message), ) as Promise; } diff --git a/src/features/headless-checkout/web-components/qr-code/qr-code.component.spec.ts b/src/features/headless-checkout/web-components/qr-code/qr-code.component.spec.ts new file mode 100644 index 0000000..46e269a --- /dev/null +++ b/src/features/headless-checkout/web-components/qr-code/qr-code.component.spec.ts @@ -0,0 +1,61 @@ +import { container } from 'tsyringe'; +import { HeadlessCheckoutSpy } from '../../../../core/spy/headless-checkout-spy/headless-checkout-spy'; +import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; +import { noopStub } from '../../../../tests/stubs/noop.stub'; +import { HeadlessCheckout } from '../../headless-checkout'; +import { QrCodeComponent } from './qr-code.component'; + +class HeadlessCheckoutMock { + public events = { + onCoreEvent: noopStub, + }; +} + +function createComponent(): void { + const element = document.createElement(WebComponentTagName.QrCodeComponent); + + (document.getElementById('container')! as HTMLElement).appendChild(element); +} + +describe('QrCodeComponent', () => { + let headlessCheckout: HeadlessCheckoutMock; + let headlessCheckoutSpy: HeadlessCheckoutSpy; + + window.customElements.define( + WebComponentTagName.QrCodeComponent, + QrCodeComponent, + ); + + beforeEach(() => { + document.body.innerHTML = '
'; + + headlessCheckout = new HeadlessCheckoutMock(); + headlessCheckoutSpy = { + listenAppInit: noopStub, + get appWasInit() { + return; + }, + } as unknown as HeadlessCheckoutSpy; + + container.clearInstances(); + + container + .register(HeadlessCheckoutSpy, { + useValue: headlessCheckoutSpy, + }) + .register(HeadlessCheckout, { + useValue: headlessCheckout as unknown as HeadlessCheckout, + }); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('Should create component', () => { + createComponent(); + expect( + document.querySelector(WebComponentTagName.QrCodeComponent), + ).not.toBeNull(); + }); +}); diff --git a/src/features/headless-checkout/web-components/qr-code/qr-code.component.ts b/src/features/headless-checkout/web-components/qr-code/qr-code.component.ts new file mode 100644 index 0000000..663c328 --- /dev/null +++ b/src/features/headless-checkout/web-components/qr-code/qr-code.component.ts @@ -0,0 +1,31 @@ +import { SecureComponentAbstract } from '../../../../core/web-components/secure-component/secure-component.abstract'; +import { EventName } from '../../../../core/event-name.enum'; +import { finishLoadComponentHandler } from '../../post-messages-handlers/finish-load-component.handler'; +import { container } from 'tsyringe'; +import { HeadlessCheckout } from '../../headless-checkout'; + +export class QrCodeComponent extends SecureComponentAbstract { + protected componentName = 'qr-code'; + private readonly headlessCheckout: HeadlessCheckout; + + public constructor() { + super(); + this.headlessCheckout = container.resolve(HeadlessCheckout); + } + + protected connectedCallback(): void { + this.startLoadingComponentHandler(); + + this.headlessCheckout.events.onCoreEvent( + EventName.finishLoadComponent, + finishLoadComponentHandler, + () => this.finishLoadingComponentHandler('qr-code'), + ); + + super.connectedCallback(); + } + + protected getHtml(): string { + return this.getSecureHtml(); + } +} diff --git a/src/web-components.ts b/src/web-components.ts index 4c6408a..88203fa 100644 --- a/src/web-components.ts +++ b/src/web-components.ts @@ -13,6 +13,7 @@ import { CheckboxComponent } from './features/headless-checkout/web-components/c import { UserBalanceComponent } from './features/headless-checkout/web-components/user-balance-component/user-balance-component'; import { PaymentFormMessagesComponent } from './features/headless-checkout/web-components/payment-form-messages/payment-form-messages.component'; import { GooglePayButtonComponent } from './features/headless-checkout/web-components/pages/google-pay/google-pay-button.component'; +import { QrCodeComponent } from './features/headless-checkout/web-components/qr-code/qr-code.component'; import { ApplePayComponent } from './features/headless-checkout/web-components/apple-pay/apple-pay.component'; export { @@ -31,5 +32,6 @@ export { UserBalanceComponent, PaymentFormMessagesComponent, GooglePayButtonComponent, + QrCodeComponent, ApplePayComponent, };