diff --git a/src/core/spy/form-spy/form-spy.spec.ts b/src/core/spy/form-spy/form-spy.spec.ts new file mode 100644 index 0000000..21d337b --- /dev/null +++ b/src/core/spy/form-spy/form-spy.spec.ts @@ -0,0 +1,49 @@ +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { FormSpy } from './form-spy'; + +class Observer { + public formInitHandler(): void { + return; + } +} + +describe('FormSpy', () => { + let formSpy: FormSpy; + + beforeEach(() => { + container.clearInstances(); + formSpy = container.createChildContainer().resolve(FormSpy); + }); + + it('Should return true for init form', () => { + formSpy.formWasInit = true; + expect(formSpy.formWasInit).toBeTrue(); + }); + + it('Should call formWasInitHandler', () => { + const observer = new Observer(); + const spy = spyOn(observer, 'formInitHandler'); + formSpy.listenFormInit(observer.formInitHandler); + formSpy.formWasInit = true; + expect(spy).toHaveBeenCalled(); + }); + + it('Should call formWasInitHandler once', () => { + const observer = new Observer(); + const spy = spyOn(observer, 'formInitHandler'); + formSpy.listenFormInit(observer.formInitHandler); + formSpy.formWasInit = true; + formSpy.formWasInit = true; + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('Should call appWasInitHandler once', () => { + const observer = new Observer(); + const spy = spyOn(observer, 'formInitHandler'); + formSpy.listenFormInit(observer.formInitHandler); + formSpy.formWasInit = true; + formSpy.formWasInit = false; + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/core/spy/form-spy/form-spy.ts b/src/core/spy/form-spy/form-spy.ts new file mode 100644 index 0000000..be2be54 --- /dev/null +++ b/src/core/spy/form-spy/form-spy.ts @@ -0,0 +1,29 @@ +import { singleton } from 'tsyringe'; + +@singleton() +export class FormSpy { + private _formWasInit = false; + private readonly _callbacks: Array<() => void> = []; + + public set formWasInit(value: boolean) { + if (this._formWasInit === value) { + return; + } + this._formWasInit = value; + if (value) { + this.formWasInitHandler(); + } + } + + public get formWasInit(): boolean { + return this._formWasInit; + } + + public listenFormInit(callback: () => void): void { + this._callbacks.push(callback); + } + + private formWasInitHandler(): void { + this._callbacks.forEach((callback) => callback()); + } +} diff --git a/src/core/headless-checkout-spy/headless-checkout-spy.spec.ts b/src/core/spy/headless-checkout-spy/headless-checkout-spy.spec.ts similarity index 100% rename from src/core/headless-checkout-spy/headless-checkout-spy.spec.ts rename to src/core/spy/headless-checkout-spy/headless-checkout-spy.spec.ts diff --git a/src/core/headless-checkout-spy/headless-checkout-spy.ts b/src/core/spy/headless-checkout-spy/headless-checkout-spy.ts similarity index 100% rename from src/core/headless-checkout-spy/headless-checkout-spy.ts rename to src/core/spy/headless-checkout-spy/headless-checkout-spy.ts diff --git a/src/core/web-components/secure-component/secure-component.abstract.ts b/src/core/web-components/secure-component/secure-component.abstract.ts index 01e71c6..fa8e35d 100644 --- a/src/core/web-components/secure-component/secure-component.abstract.ts +++ b/src/core/web-components/secure-component/secure-component.abstract.ts @@ -2,9 +2,13 @@ import { WebComponentAbstract } from '../web-component.abstract'; import { headlessCheckoutAppUrl } from '../../../features/headless-checkout/environment'; export abstract class SecureComponentAbstract extends WebComponentAbstract { - protected abstract componentName: string; + protected componentName: string | null = null; protected getSecureHtml(): string { + if (!this.componentName) { + throw new Error('Component name is required'); + } + return ``; } } 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 7234557..ac9918f 100644 --- a/src/core/web-components/web-component-tag-name.enum.ts +++ b/src/core/web-components/web-component-tag-name.enum.ts @@ -1,5 +1,5 @@ export enum WebComponentTagName { - CardNumberComponent = 'psdk-card-number', + TextComponent = 'psdk-text-component', SubmitButtonComponent = 'psdk-submit-button', PaymentMethodsComponent = 'psdk-payment-methods', LegalComponent = 'psdk-legal', diff --git a/src/core/web-components/web-components.map.ts b/src/core/web-components/web-components.map.ts index 62cc671..ec4472e 100644 --- a/src/core/web-components/web-components.map.ts +++ b/src/core/web-components/web-components.map.ts @@ -1,4 +1,4 @@ -import { CardNumberComponent } from '../../features/headless-checkout/web-components/card-number/card-number.component'; +import { TextComponent } from '../../features/headless-checkout/web-components/text-component/text.component'; import { SubmitButtonComponent } from '../../features/headless-checkout/web-components/submit-button/submit-button.component'; import { WebComponentTagName } from './web-component-tag-name.enum'; import { PaymentMethodsComponent } from '../../features/headless-checkout/web-components/payment-methods/payment-methods.component'; @@ -7,7 +7,7 @@ import { LegalComponent } from '../../features/headless-checkout/web-components/ export const webComponents: { [key in WebComponentTagName]: CustomElementConstructor; } = { - [WebComponentTagName.CardNumberComponent]: CardNumberComponent, + [WebComponentTagName.TextComponent]: TextComponent, [WebComponentTagName.SubmitButtonComponent]: SubmitButtonComponent, [WebComponentTagName.PaymentMethodsComponent]: PaymentMethodsComponent, [WebComponentTagName.LegalComponent]: LegalComponent, diff --git a/src/features/headless-checkout/headless-checkout.ts b/src/features/headless-checkout/headless-checkout.ts index e0100c6..ef748c3 100644 --- a/src/features/headless-checkout/headless-checkout.ts +++ b/src/features/headless-checkout/headless-checkout.ts @@ -14,13 +14,14 @@ import { SavedMethod } from '../../core/saved-method.interface'; import { getSavedMethodsHandler } from './post-messages-handlers/get-saved-methods.handler'; import { UserBalance } from '../../core/user-balance.interface'; import { getUserBalanceHandler } from './post-messages-handlers/get-user-balance.handler'; -import { HeadlessCheckoutSpy } from '../../core/headless-checkout-spy/headless-checkout-spy'; +import { HeadlessCheckoutSpy } from '../../core/spy/headless-checkout-spy/headless-checkout-spy'; import { getRegularMethodsHandler } from './post-messages-handlers/get-regular-methods.handler'; import { FormConfiguration } from '../../core/form/form-configuration.interface'; import { initFormHandler } from './post-messages-handlers/init-form.handler'; import { Form } from '../../core/form/form.interface'; import { NextAction } from '../../core/actions/next-action.interface'; import { nextActionHandler } from './post-messages-handlers/next-action.handler'; +import { FormSpy } from '../../core/spy/form-spy/form-spy'; @singleton() export class HeadlessCheckout { @@ -65,9 +66,8 @@ export class HeadlessCheckout { }, }; - return this.postMessagesClient.send
( - msg, - initFormHandler + return this.postMessagesClient.send(msg, (message) => + initFormHandler(message, () => (this.formSpy.formWasInit = true)) ) as Promise; }, @@ -93,7 +93,8 @@ export class HeadlessCheckout { private readonly window: Window, private readonly postMessagesClient: PostMessagesClient, private readonly localizeService: LocalizeService, - private readonly headlessCheckoutSpy: HeadlessCheckoutSpy + private readonly headlessCheckoutSpy: HeadlessCheckoutSpy, + private readonly formSpy: FormSpy ) {} public async init(environment: { isWebview: boolean }): Promise { @@ -210,6 +211,7 @@ export class HeadlessCheckout { this.coreIframe.style.border = 'none'; this.coreIframe.style.position = 'absolute'; this.coreIframe.src = `${this.headlessAppUrl}/core`; + this.coreIframe.name = 'core'; this.window.document.body.appendChild(this.coreIframe); return new Promise((resolve) => { this.coreIframe.onload = () => { diff --git a/src/features/headless-checkout/post-messages-handlers/init-form.handler.ts b/src/features/headless-checkout/post-messages-handlers/init-form.handler.ts index 119d90c..f3d237b 100644 --- a/src/features/headless-checkout/post-messages-handlers/init-form.handler.ts +++ b/src/features/headless-checkout/post-messages-handlers/init-form.handler.ts @@ -4,9 +4,13 @@ import { Message } from '../../../core/message.interface'; import { Handler } from '../../../core/post-messages-client/handler.type'; export const initFormHandler: Handler = ( - message: Message + message: Message, + callback?: () => void ): { isHandled: boolean; value?: Form } | null => { if (isInitFormEventMessage(message)) { + if (typeof callback === 'function') { + callback(); + } return { isHandled: true, value: message.data, diff --git a/src/features/headless-checkout/web-components/card-number/card-number.component.ts b/src/features/headless-checkout/web-components/card-number/card-number.component.ts deleted file mode 100644 index 2c75dfc..0000000 --- a/src/features/headless-checkout/web-components/card-number/card-number.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SecureComponentAbstract } from '../../../../core/web-components/secure-component/secure-component.abstract'; - -export class CardNumberComponent extends SecureComponentAbstract { - protected componentName = 'card-number'; - - public constructor() { - super(); - } - - protected getHtml(): string { - return ` -
${this.getSecureHtml()} - `; - } -} diff --git a/src/features/headless-checkout/web-components/legal/legal.component.spec.ts b/src/features/headless-checkout/web-components/legal/legal.component.spec.ts index 419914d..b41a039 100644 --- a/src/features/headless-checkout/web-components/legal/legal.component.spec.ts +++ b/src/features/headless-checkout/web-components/legal/legal.component.spec.ts @@ -1,6 +1,6 @@ import { container } from 'tsyringe'; import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; -import { HeadlessCheckoutSpy } from '../../../../core/headless-checkout-spy/headless-checkout-spy'; +import { HeadlessCheckoutSpy } from '../../../../core/spy/headless-checkout-spy/headless-checkout-spy'; import { noopStub } from '../../../../tests/stubs/noop.stub'; import { HeadlessCheckout } from '../../headless-checkout'; import { LegalComponent } from './legal.component'; diff --git a/src/features/headless-checkout/web-components/legal/legal.component.ts b/src/features/headless-checkout/web-components/legal/legal.component.ts index 90d8338..5a1e7a9 100644 --- a/src/features/headless-checkout/web-components/legal/legal.component.ts +++ b/src/features/headless-checkout/web-components/legal/legal.component.ts @@ -5,7 +5,7 @@ import { EventName } from '../../../../core/event-name.enum'; import { HeadlessCheckout } from '../../headless-checkout'; import { getLegalComponentTemplate } from './legal.component.tempate'; import { Message } from '../../../../core/message.interface'; -import { HeadlessCheckoutSpy } from '../../../../core/headless-checkout-spy/headless-checkout-spy'; +import { HeadlessCheckoutSpy } from '../../../../core/spy/headless-checkout-spy/headless-checkout-spy'; import { getLegalComponentConfigHandler } from '../../post-messages-handlers/get-legal-component-config.handler'; import { LegalComponentConfig } from './legal-component.config.interface'; import { isEventMessage } from '../../../../core/guards/event-message.guard'; 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 b30c88a..71a46e4 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 @@ -1,6 +1,6 @@ import { container } from 'tsyringe'; import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; -import { HeadlessCheckoutSpy } from '../../../../core/headless-checkout-spy/headless-checkout-spy'; +import { HeadlessCheckoutSpy } from '../../../../core/spy/headless-checkout-spy/headless-checkout-spy'; import { PaymentMethodsComponent } from './payment-methods.component'; import { noopStub } from '../../../../tests/stubs/noop.stub'; import { PaymentMethodsAttributes } from './payment-methods-attributes.enum'; diff --git a/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.ts b/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.ts index b09f537..ad0911e 100644 --- a/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.ts +++ b/src/features/headless-checkout/web-components/payment-methods/payment-methods.component.ts @@ -5,7 +5,7 @@ import { getPaymentMethodTemplate } from './payment-method.template'; import { filterPaymentMethods } from './filter-payment-methods.function'; import { PaymentMethodsAttributes } from './payment-methods-attributes.enum'; import { PaymentMethodsEvents } from './payment-methods-events.enum'; -import { HeadlessCheckoutSpy } from '../../../../core/headless-checkout-spy/headless-checkout-spy'; +import { HeadlessCheckoutSpy } from '../../../../core/spy/headless-checkout-spy/headless-checkout-spy'; import { HeadlessCheckout } from '../../headless-checkout'; export class PaymentMethodsComponent extends WebComponentAbstract { diff --git a/src/features/headless-checkout/web-components/text-component/text-component-attributes.enum.ts b/src/features/headless-checkout/web-components/text-component/text-component-attributes.enum.ts new file mode 100644 index 0000000..bea4498 --- /dev/null +++ b/src/features/headless-checkout/web-components/text-component/text-component-attributes.enum.ts @@ -0,0 +1,3 @@ +export enum TextComponentAttributes { + name = 'name', +} diff --git a/src/features/headless-checkout/web-components/card-number/card-number.component.spec.ts b/src/features/headless-checkout/web-components/text-component/text.component.spec.ts similarity index 75% rename from src/features/headless-checkout/web-components/card-number/card-number.component.spec.ts rename to src/features/headless-checkout/web-components/text-component/text.component.spec.ts index a7bf179..fdb8bbc 100644 --- a/src/features/headless-checkout/web-components/card-number/card-number.component.spec.ts +++ b/src/features/headless-checkout/web-components/text-component/text.component.spec.ts @@ -1,18 +1,17 @@ import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum'; -import { CardNumberComponent } from './card-number.component'; +import { TextComponent } from './text.component'; function createComponent(): void { - const element = document.createElement( - WebComponentTagName.CardNumberComponent - ); + const element = document.createElement(WebComponentTagName.TextComponent); + element.setAttribute('name', 'zip'); element.setAttribute('id', 'test'); (document.getElementById('container')! as HTMLElement).appendChild(element); } describe('HeadlessCheckout', () => { window.customElements.define( - WebComponentTagName.CardNumberComponent, - CardNumberComponent + WebComponentTagName.TextComponent, + TextComponent ); beforeEach(() => { diff --git a/src/features/headless-checkout/web-components/text-component/text.component.ts b/src/features/headless-checkout/web-components/text-component/text.component.ts new file mode 100644 index 0000000..9199928 --- /dev/null +++ b/src/features/headless-checkout/web-components/text-component/text.component.ts @@ -0,0 +1,41 @@ +import { SecureComponentAbstract } from '../../../../core/web-components/secure-component/secure-component.abstract'; +import { TextComponentAttributes } from './text-component-attributes.enum'; +import { container } from 'tsyringe'; +import { FormSpy } from '../../../../core/spy/form-spy/form-spy'; + +export class TextComponent extends SecureComponentAbstract { + private readonly formSpy: FormSpy; + public constructor() { + super(); + this.formSpy = container.resolve(FormSpy); + } + + public static get observedAttributes(): string[] { + return [TextComponentAttributes.name]; + } + + protected connectedCallback(): void { + if (!this.formSpy.formWasInit) { + this.formSpy.listenFormInit(() => this.connectedCallback()); + return; + } + + const inputName = this.getAttribute(TextComponentAttributes.name); + if (!inputName) { + return; + } + + this.componentName = `text-input/${inputName}`; + super.render(); + } + + protected attributeChangedCallback(): void { + this.connectedCallback(); + } + + protected getHtml(): string { + return ` + ${this.getSecureHtml()} + `; + } +} diff --git a/src/web-components.ts b/src/web-components.ts index f83dbd2..f7e44a8 100644 --- a/src/web-components.ts +++ b/src/web-components.ts @@ -1,11 +1,11 @@ import { SubmitButtonComponent } from './features/headless-checkout/web-components/submit-button/submit-button.component'; -import { CardNumberComponent } from './features/headless-checkout/web-components/card-number/card-number.component'; +import { TextComponent } from './features/headless-checkout/web-components/text-component/text.component'; import { PaymentMethodsComponent } from './features/headless-checkout/web-components/payment-methods/payment-methods.component'; import { LegalComponent } from './features/headless-checkout/web-components/legal/legal.component'; export { SubmitButtonComponent, - CardNumberComponent, + TextComponent, PaymentMethodsComponent, LegalComponent, };