From c417065e2be5613d7eee4aea8cfaa813f5626251 Mon Sep 17 00:00:00 2001 From: "a.kornienko" Date: Thu, 19 Oct 2023 10:30:54 +0300 Subject: [PATCH] feat(PAYMENTS-16646): add phone control --- README.md | 2 +- .../web-component-tag-name.enum.ts | 1 + src/core/web-components/web-components.map.ts | 2 + .../payment-form/form-controls-tags.list.ts | 6 +++ ...et-web-component-by-field-name.function.ts | 11 +++++ .../payment-form-fields.service.spec.ts | 10 ++-- .../payment-form-fields.service.ts | 3 +- .../payment-form.component.spec.ts | 14 +++--- .../payment-form/payment-form.component.ts | 22 ++++++--- .../web-components-fields-names.map.ts | 6 +++ .../payment-methods.component.spec.ts | 46 +++++++++---------- .../phone-component-attributes.enum.ts | 4 ++ .../phone-component/phone.component.ts | 30 ++++++++++++ .../text-component/text.component.spec.ts | 18 ++++---- src/web-components.ts | 2 + 15 files changed, 125 insertions(+), 52 deletions(-) create mode 100644 src/features/headless-checkout/web-components/payment-form/form-controls-tags.list.ts create mode 100644 src/features/headless-checkout/web-components/payment-form/get-web-component-by-field-name.function.ts create mode 100644 src/features/headless-checkout/web-components/payment-form/web-components-fields-names.map.ts create mode 100644 src/features/headless-checkout/web-components/phone-component/phone-component-attributes.enum.ts create mode 100644 src/features/headless-checkout/web-components/phone-component/phone.component.ts diff --git a/README.md b/README.md index 5be8952..42a41eb 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ Using SDK components is straightforward: you only need to paste the HTML tag of | **Component** | **Selector** | **Status** | | --------------------- | ------------ | ---------- | | Text Component | psdk-text | ✅ | -| Phone Component | ❔ | 🕑 | +| Phone Component | psdk-phone | ✅ | | Card Number Component | ❔ | 🕑 | ![SDK secure componentscheme](./readme_images/secure_component_scheme.png 'SDK secure componentscheme') 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 0cdbbdc..3ba089e 100644 --- a/src/core/web-components/web-component-tag-name.enum.ts +++ b/src/core/web-components/web-component-tag-name.enum.ts @@ -7,4 +7,5 @@ export enum WebComponentTagName { LegalComponent = 'psdk-legal', PaymentFormComponent = 'psdk-payment-form', StatusComponent = 'psdk-status', + PhoneComponent = 'psdk-phone', } diff --git a/src/core/web-components/web-components.map.ts b/src/core/web-components/web-components.map.ts index 66a6f29..ba4d032 100644 --- a/src/core/web-components/web-components.map.ts +++ b/src/core/web-components/web-components.map.ts @@ -7,6 +7,7 @@ import { FinanceDetailsComponent } from '../../features/headless-checkout/web-co import { PriceTextComponent } from '../../features/headless-checkout/web-components/finance-details/price-text/price-text.component'; import { WebComponentTagName } from './web-component-tag-name.enum'; import { PaymentFormComponent } from '../../features/headless-checkout/web-components/payment-form/payment-form.component'; +import { PhoneComponent } from '../../features/headless-checkout/web-components/phone-component/phone.component'; export const webComponents: { [key in WebComponentTagName]: CustomElementConstructor; @@ -19,4 +20,5 @@ export const webComponents: { [WebComponentTagName.LegalComponent]: LegalComponent, [WebComponentTagName.StatusComponent]: StatusComponent, [WebComponentTagName.PaymentFormComponent]: PaymentFormComponent, + [WebComponentTagName.PhoneComponent]: PhoneComponent, }; diff --git a/src/features/headless-checkout/web-components/payment-form/form-controls-tags.list.ts b/src/features/headless-checkout/web-components/payment-form/form-controls-tags.list.ts new file mode 100644 index 0000000..f65aaff --- /dev/null +++ b/src/features/headless-checkout/web-components/payment-form/form-controls-tags.list.ts @@ -0,0 +1,6 @@ +import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; + +export const formControlsTags = [ + WebComponentTagName.TextComponent, + WebComponentTagName.PhoneComponent, +]; diff --git a/src/features/headless-checkout/web-components/payment-form/get-web-component-by-field-name.function.ts b/src/features/headless-checkout/web-components/payment-form/get-web-component-by-field-name.function.ts new file mode 100644 index 0000000..d2e55ff --- /dev/null +++ b/src/features/headless-checkout/web-components/payment-form/get-web-component-by-field-name.function.ts @@ -0,0 +1,11 @@ +import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; +import { webComponentsFieldsNamesMap } from './web-components-fields-names.map'; + +export const getWebComponentByFieldName = ( + name: string +): WebComponentTagName => { + const webComponent: undefined | WebComponentTagName = + webComponentsFieldsNamesMap[name]; + + return webComponent ?? WebComponentTagName.TextComponent; +}; diff --git a/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.spec.ts b/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.spec.ts index 3435b28..1b80840 100644 --- a/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.spec.ts +++ b/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.spec.ts @@ -8,7 +8,7 @@ const getTextInputElements = (names: string[]): NodeListOf => { const mockContainer = document.createElement('div'); for (const name of names) { const mockElement = document.createElement( - WebComponentTagName.TextComponent, + WebComponentTagName.TextComponent ); mockElement.setAttribute('name', name); mockContainer.appendChild(mockElement); @@ -34,14 +34,14 @@ describe('PaymentFormFieldsManager', () => { it('Should create missed fields and append them into body', () => { const textInputElements = getTextInputElements([]); spyOn(windowService.document, 'querySelectorAll').and.returnValue( - textInputElements, + textInputElements ); const missedFields = ['card']; const mockBody = { appendChild: noopStub, } as unknown as HTMLElement; spyOn(windowService.document, 'querySelector').and.returnValue( - mockBody as unknown as Element, + mockBody as unknown as Element ); const spy = spyOn(mockBody, 'appendChild'); paymentFormFieldsManager.createMissedFields(missedFields, mockBody); @@ -54,7 +54,7 @@ describe('PaymentFormFieldsManager', () => { remove: noopStub, }; spyOn(windowService.document, 'querySelector').and.returnValue( - mockElement as unknown as Element, + mockElement as unknown as Element ); const spy = spyOn(mockElement, 'remove'); paymentFormFieldsManager.removeExtraFields(extraFields); @@ -67,7 +67,7 @@ describe('PaymentFormFieldsManager', () => { element.removeAttribute('name'); }); spyOn(windowService.document, 'querySelectorAll').and.returnValue( - textInputElements, + textInputElements ); const spy = spyOn(textInputElements[0], 'remove'); paymentFormFieldsManager.removeEmptyNameFields(); diff --git a/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.ts b/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.ts index 2a29d67..b9dff13 100644 --- a/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.ts +++ b/src/features/headless-checkout/web-components/payment-form/payment-form-fields.service.ts @@ -1,5 +1,6 @@ import { injectable } from 'tsyringe'; import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; +import { getWebComponentByFieldName } from './get-web-component-by-field-name.function'; @injectable() export class PaymentFormFieldsService { @@ -13,7 +14,7 @@ export class PaymentFormFieldsService { for (const name of missedFieldsNames) { const formElement = this.window.document.createElement( - WebComponentTagName.TextComponent + getWebComponentByFieldName(name) ); formElement.setAttribute('name', name); documentFragment.append(formElement); diff --git a/src/features/headless-checkout/web-components/payment-form/payment-form.component.spec.ts b/src/features/headless-checkout/web-components/payment-form/payment-form.component.spec.ts index c7b7b72..b4ccc90 100644 --- a/src/features/headless-checkout/web-components/payment-form/payment-form.component.spec.ts +++ b/src/features/headless-checkout/web-components/payment-form/payment-form.component.spec.ts @@ -10,7 +10,7 @@ import { PostMessagesClient } from '../../../../core/post-messages-client/post-m function createComponent(): void { const element = document.createElement( - WebComponentTagName.PaymentFormComponent, + WebComponentTagName.PaymentFormComponent ); element.setAttribute('id', 'test'); (document.getElementById('container')! as HTMLElement).appendChild(element); @@ -29,7 +29,7 @@ const getTextInputElements = (names: string[]): NodeListOf => { const mockContainer = document.createElement('div'); for (const name of names) { const mockElement = document.createElement( - WebComponentTagName.TextComponent, + WebComponentTagName.TextComponent ); mockElement.setAttribute('name', name); mockContainer.appendChild(mockElement); @@ -47,7 +47,7 @@ describe('PaymentFormComponent', () => { window.customElements.define( WebComponentTagName.PaymentFormComponent, - PaymentFormComponent, + PaymentFormComponent ); beforeEach(() => { @@ -109,7 +109,7 @@ describe('PaymentFormComponent', () => { it('Should create component', () => { createComponent(); expect( - document.querySelector(WebComponentTagName.PaymentFormComponent), + document.querySelector(WebComponentTagName.PaymentFormComponent) ).toBeDefined(); }); @@ -139,7 +139,7 @@ describe('PaymentFormComponent', () => { const textInputElements = getTextInputElements(['zip']); spyOn(windowService.document, 'querySelectorAll').and.returnValue( - textInputElements, + textInputElements ); const spy = spyOn(paymentFormFieldsManager, 'createMissedFields'); @@ -164,12 +164,12 @@ describe('PaymentFormComponent', () => { const textInputElements = getTextInputElements(['zip', 'zip']); spyOn(windowService.document, 'querySelectorAll').and.returnValue( - textInputElements, + textInputElements ); const spy = spyOn(paymentFormFieldsManager, 'removeExtraFields'); createComponent(); - expect(spy).toHaveBeenCalledWith(['zip']); + expect(spy).toHaveBeenCalledWith(['zip', 'zip', 'zip']); }); }); diff --git a/src/features/headless-checkout/web-components/payment-form/payment-form.component.ts b/src/features/headless-checkout/web-components/payment-form/payment-form.component.ts index 567024c..2c48f10 100644 --- a/src/features/headless-checkout/web-components/payment-form/payment-form.component.ts +++ b/src/features/headless-checkout/web-components/payment-form/payment-form.component.ts @@ -8,6 +8,7 @@ import { getMissedFieldsNames } from './get-missed-fields-names.function'; import { getInvalidFieldsNames } from './get-invalid-fields-names.function'; import { PaymentFormFieldsService } from './payment-form-fields.service'; import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; +import { formControlsTags } from './form-controls-tags.list'; export class PaymentFormComponent extends WebComponentAbstract { private readonly headlessCheckout: HeadlessCheckout; @@ -90,12 +91,21 @@ export class PaymentFormComponent extends WebComponentAbstract { } private getExistsControlsNames(): Array { - const formInputs = this.window.document.querySelectorAll( - WebComponentTagName.TextComponent - ); - return Array.from(formInputs).map((formInput) => - formInput.getAttribute('name') - ); + const existsControlsNames: Array = []; + + formControlsTags.forEach((tag: WebComponentTagName) => { + const formInputs = this.window.document.querySelectorAll(tag); + + const controlsNames = Array.from(formInputs).map((formInput) => + formInput.getAttribute('name') + ); + + controlsNames.forEach((name: string | null) => + existsControlsNames.push(name) + ); + }); + + return existsControlsNames; } private logMissedFields(missedFieldsNames: string[]): void { diff --git a/src/features/headless-checkout/web-components/payment-form/web-components-fields-names.map.ts b/src/features/headless-checkout/web-components/payment-form/web-components-fields-names.map.ts new file mode 100644 index 0000000..a3735a7 --- /dev/null +++ b/src/features/headless-checkout/web-components/payment-form/web-components-fields-names.map.ts @@ -0,0 +1,6 @@ +import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; + +export const webComponentsFieldsNamesMap: { [k: string]: WebComponentTagName } = + { + phone: WebComponentTagName.PhoneComponent, + }; diff --git a/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.spec.ts b/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.spec.ts index cbcb748..e44d5f6 100644 --- a/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.spec.ts +++ b/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.spec.ts @@ -10,7 +10,7 @@ import { HeadlessCheckout } from '../../headless-checkout'; function createComponent(): void { const element = document.createElement( - WebComponentTagName.PaymentMethodsComponent, + WebComponentTagName.PaymentMethodsComponent ); element.setAttribute('id', 'test'); (document.getElementById('container')! as HTMLElement).appendChild(element); @@ -40,7 +40,7 @@ describe('PaymentMethodsComponent', () => { window.customElements.define( WebComponentTagName.PaymentMethodsComponent, - PaymentMethodsComponent, + PaymentMethodsComponent ); beforeEach(() => { @@ -75,16 +75,16 @@ describe('PaymentMethodsComponent', () => { it('Should create component', () => { createComponent(); expect( - document.querySelector(WebComponentTagName.PaymentMethodsComponent), + document.querySelector(WebComponentTagName.PaymentMethodsComponent) ).toBeDefined(); }); it('Should load payment methods', () => { const spy = spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([]), + Promise.resolve([]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); expect(spy).toHaveBeenCalled(); @@ -92,12 +92,12 @@ describe('PaymentMethodsComponent', () => { it('Should load payment methods after init', () => { const spy = spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([]), + Promise.resolve([]) ); const appWasInitSpy = spyOnProperty( headlessCheckoutSpy, 'appWasInit', - 'get', + 'get' ); const listenAppInitSpy = spyOn(headlessCheckoutSpy, 'listenAppInit'); listenAppInitSpy.and.callFake((callback: () => void) => { @@ -111,10 +111,10 @@ describe('PaymentMethodsComponent', () => { it('Should not load payment methods', () => { const spy = spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([]), + Promise.resolve([]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - false, + false ); createComponent(); expect(spy).not.toHaveBeenCalled(); @@ -122,10 +122,10 @@ describe('PaymentMethodsComponent', () => { it('Should load payment methods twice if change country', () => { const spy = spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([]), + Promise.resolve([]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); document @@ -136,10 +136,10 @@ describe('PaymentMethodsComponent', () => { it('Should draw 2 payment methods', async () => { spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([mockVisiblePaymentMethod, mockVisiblePaymentMethod]), + Promise.resolve([mockVisiblePaymentMethod, mockVisiblePaymentMethod]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); @@ -150,10 +150,10 @@ describe('PaymentMethodsComponent', () => { it('Should draw 1 payment methods', async () => { spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([mockNotVisiblePaymentMethod, mockVisiblePaymentMethod]), + Promise.resolve([mockNotVisiblePaymentMethod, mockVisiblePaymentMethod]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); @@ -166,10 +166,10 @@ describe('PaymentMethodsComponent', () => { Promise.resolve([ mockNotVisiblePaymentMethod, mockNotVisiblePaymentMethod, - ]), + ]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); @@ -179,10 +179,10 @@ describe('PaymentMethodsComponent', () => { it('Should dispatch custom event', async () => { spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([mockVisiblePaymentMethod]), + Promise.resolve([mockVisiblePaymentMethod]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); @@ -200,10 +200,10 @@ describe('PaymentMethodsComponent', () => { it('Should search methods', async () => { spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([mockVisiblePaymentMethod, mockQiwiPaymentMethod]), + Promise.resolve([mockVisiblePaymentMethod, mockQiwiPaymentMethod]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); @@ -218,10 +218,10 @@ describe('PaymentMethodsComponent', () => { it('Should draw no methods', async () => { spyOn(headlessCheckout, 'getRegularMethods').and.returnValue( - Promise.resolve([mockVisiblePaymentMethod, mockQiwiPaymentMethod]), + Promise.resolve([mockVisiblePaymentMethod, mockQiwiPaymentMethod]) ); spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue( - true, + true ); createComponent(); diff --git a/src/features/headless-checkout/web-components/phone-component/phone-component-attributes.enum.ts b/src/features/headless-checkout/web-components/phone-component/phone-component-attributes.enum.ts new file mode 100644 index 0000000..1f23b8c --- /dev/null +++ b/src/features/headless-checkout/web-components/phone-component/phone-component-attributes.enum.ts @@ -0,0 +1,4 @@ +export enum PhoneComponentAttributes { + name = 'name', + showFlags = 'showflags', +} diff --git a/src/features/headless-checkout/web-components/phone-component/phone.component.ts b/src/features/headless-checkout/web-components/phone-component/phone.component.ts new file mode 100644 index 0000000..86c6499 --- /dev/null +++ b/src/features/headless-checkout/web-components/phone-component/phone.component.ts @@ -0,0 +1,30 @@ +import { PhoneComponentAttributes } from './phone-component-attributes.enum'; +import { headlessCheckoutAppUrl } from '../../environment'; +import { TextComponent } from '../text-component/text.component'; + +export class PhoneComponent extends TextComponent { + public static get observedAttributes(): string[] { + return [PhoneComponentAttributes.showFlags]; + } + + protected connectedCallback(): void { + super.connectedCallback(); + if (!this.getAttribute(PhoneComponentAttributes.name)) { + this.setAttribute(PhoneComponentAttributes.name, 'phone'); + } + } + + protected getSecureHtml(): string { + if (!this.componentName) { + throw new Error('Component name is required'); + } + + let src = `${headlessCheckoutAppUrl}/secure-components/${this.componentName}`; + const showFlags = this.getAttribute(PhoneComponentAttributes.showFlags); + + if (showFlags) { + src += '?showFlags=true'; + } + return ``; + } +} diff --git a/src/features/headless-checkout/web-components/text-component/text.component.spec.ts b/src/features/headless-checkout/web-components/text-component/text.component.spec.ts index c1192a0..ab6b6a0 100644 --- a/src/features/headless-checkout/web-components/text-component/text.component.spec.ts +++ b/src/features/headless-checkout/web-components/text-component/text.component.spec.ts @@ -59,7 +59,7 @@ describe('TextComponent', () => { window.customElements.define( WebComponentTagName.TextComponent, - TextComponent, + TextComponent ); beforeEach(() => { @@ -116,7 +116,7 @@ describe('TextComponent', () => { it('Should not render error element for valid field state', (done) => { let element: HTMLElement = document.createElement('div'); - let callback: (fieldsStatus: FormFieldsStatus) => void = () => {}; + let callback: (fieldsStatus: FormFieldsStatus) => void = noopStub; spyOnProperty(formSpy, 'formWasInit').and.returnValue(true); spyOn(postMessagesClient, 'send').and.resolveTo({ name: fieldName, @@ -124,7 +124,7 @@ describe('TextComponent', () => { spyOn(headlessCheckout.form, 'onFieldsStatusChange').and.callFake( (callbackFn) => { setTimeout(() => (callback = callbackFn)); - }, + } ); element = createComponent(); @@ -138,7 +138,7 @@ describe('TextComponent', () => { it('Should not render error element if no current field state', (done) => { let element: HTMLElement = document.createElement('div'); - let callback: (fieldsStatus: FormFieldsStatus) => void = () => {}; + let callback: (fieldsStatus: FormFieldsStatus) => void = noopStub; spyOnProperty(formSpy, 'formWasInit').and.returnValue(true); spyOn(postMessagesClient, 'send').and.resolveTo({ name: fieldName, @@ -146,7 +146,7 @@ describe('TextComponent', () => { spyOn(headlessCheckout.form, 'onFieldsStatusChange').and.callFake( (callbackFn) => { setTimeout(() => (callback = callbackFn)); - }, + } ); element = createComponent(); @@ -160,7 +160,7 @@ describe('TextComponent', () => { it('Should render error element', (done) => { let element: HTMLElement = document.createElement('div'); - let callback: (fieldsStatus: FormFieldsStatus) => void = () => {}; + let callback: (fieldsStatus: FormFieldsStatus) => void = noopStub; spyOnProperty(formSpy, 'formWasInit').and.returnValue(true); spyOn(postMessagesClient, 'send').and.resolveTo({ name: fieldName, @@ -168,7 +168,7 @@ describe('TextComponent', () => { spyOn(headlessCheckout.form, 'onFieldsStatusChange').and.callFake( (callbackFn) => { setTimeout(() => (callback = callbackFn)); - }, + } ); element = createComponent(); @@ -182,7 +182,7 @@ describe('TextComponent', () => { it('Should remove error element', (done) => { let element: HTMLElement = document.createElement('div'); - let callback: (fieldsStatus: FormFieldsStatus) => void = () => {}; + let callback: (fieldsStatus: FormFieldsStatus) => void = noopStub; spyOnProperty(formSpy, 'formWasInit').and.returnValue(true); spyOn(postMessagesClient, 'send').and.resolveTo({ name: fieldName, @@ -190,7 +190,7 @@ describe('TextComponent', () => { spyOn(headlessCheckout.form, 'onFieldsStatusChange').and.callFake( (callbackFn) => { setTimeout(() => (callback = callbackFn)); - }, + } ); element = createComponent(); diff --git a/src/web-components.ts b/src/web-components.ts index 60866c6..c52f150 100644 --- a/src/web-components.ts +++ b/src/web-components.ts @@ -5,6 +5,7 @@ import { LegalComponent } from './features/headless-checkout/web-components/lega import { StatusComponent } from './features/headless-checkout/web-components/status/status.component'; import { FinanceDetailsComponent } from './features/headless-checkout/web-components/finance-details/finance-details.component'; import { PriceTextComponent } from './features/headless-checkout/web-components/finance-details/price-text/price-text.component'; +import { PhoneComponent } from './features/headless-checkout/web-components/phone-component/phone.component'; export { SubmitButtonComponent, @@ -14,4 +15,5 @@ export { FinanceDetailsComponent, LegalComponent, StatusComponent, + PhoneComponent, };