diff --git a/package-lock.json b/package-lock.json index 7b4e922..0b98384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-n": "^15.7.0", "eslint-plugin-promise": "^6.1.1", "eslint-webpack-plugin": "^4.0.1", + "file-loader": "^6.2.0", "husky": "^8.0.3", "karma": "^6.4.2", "karma-chrome-launcher": "^3.2.0", @@ -4311,6 +4312,26 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", diff --git a/package.json b/package.json index 4060097..74e9b7b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "eslint-plugin-n": "^15.7.0", "eslint-plugin-promise": "^6.1.1", "eslint-webpack-plugin": "^4.0.1", + "file-loader": "^6.2.0", "husky": "^8.0.3", "karma": "^6.4.2", "karma-chrome-launcher": "^3.2.0", diff --git a/src/assets/images.d.ts b/src/assets/images.d.ts index bff9471..5073e14 100644 --- a/src/assets/images.d.ts +++ b/src/assets/images.d.ts @@ -1 +1,2 @@ declare module '*.svg'; +declare module '*.png'; diff --git a/src/assets/statuses/failed.png b/src/assets/statuses/failed.png new file mode 100644 index 0000000..f8f38b6 Binary files /dev/null and b/src/assets/statuses/failed.png differ diff --git a/src/assets/statuses/success.png b/src/assets/statuses/success.png new file mode 100644 index 0000000..263e6c4 Binary files /dev/null and b/src/assets/statuses/success.png differ diff --git a/src/core/status/status.enum.ts b/src/core/status/status.enum.ts index c275545..5556c5e 100644 --- a/src/core/status/status.enum.ts +++ b/src/core/status/status.enum.ts @@ -7,5 +7,4 @@ export enum StatusEnum { done = 'done', error = 'error', canceled = 'canceled', - unknown = 'unknown', } 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 ac9918f..81c798b 100644 --- a/src/core/web-components/web-component-tag-name.enum.ts +++ b/src/core/web-components/web-component-tag-name.enum.ts @@ -3,4 +3,5 @@ export enum WebComponentTagName { SubmitButtonComponent = 'psdk-submit-button', PaymentMethodsComponent = 'psdk-payment-methods', LegalComponent = 'psdk-legal', + StatusComponent = 'psdk-status', } diff --git a/src/core/web-components/web-components.map.ts b/src/core/web-components/web-components.map.ts index ec4472e..ad5ce2e 100644 --- a/src/core/web-components/web-components.map.ts +++ b/src/core/web-components/web-components.map.ts @@ -1,8 +1,9 @@ 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'; import { LegalComponent } from '../../features/headless-checkout/web-components/legal/legal.component'; +import { StatusComponent } from '../../features/headless-checkout/web-components/status/status.component'; +import { WebComponentTagName } from './web-component-tag-name.enum'; export const webComponents: { [key in WebComponentTagName]: CustomElementConstructor; @@ -11,4 +12,5 @@ export const webComponents: { [WebComponentTagName.SubmitButtonComponent]: SubmitButtonComponent, [WebComponentTagName.PaymentMethodsComponent]: PaymentMethodsComponent, [WebComponentTagName.LegalComponent]: LegalComponent, + [WebComponentTagName.StatusComponent]: StatusComponent, }; diff --git a/src/features/headless-checkout/headless-checkout.ts b/src/features/headless-checkout/headless-checkout.ts index 7a3d599..c1610ec 100644 --- a/src/features/headless-checkout/headless-checkout.ts +++ b/src/features/headless-checkout/headless-checkout.ts @@ -14,7 +14,6 @@ import { Form } from '../../core/form/form.interface'; import { NextAction } from '../../core/actions/next-action.interface'; import { FormSpy } from '../../core/spy/form-spy/form-spy'; import { Status } from '../../core/status/status.interface'; -import { StatusEnum } from '../../core/status/status.enum'; import { getErrorHandler } from './post-messages-handlers/error.handler'; import { initFormHandler } from './post-messages-handlers/init-form.handler'; import { getQuickMethodsHandler } from './post-messages-handlers/get-quick-methods.handler'; @@ -85,30 +84,10 @@ export class HeadlessCheckout { } ); }, - - getStatus: async (): Promise => { - const msg: Message = { - name: EventName.getPaymentStatus, - }; - - const status = await this.postMessagesClient.send( - msg, - (message) => getPaymentStatusHandler(message) - ); - - if (!status) { - return { - statusState: StatusEnum.unknown, - statusMessage: 'Unknown status', - group: 'unknown', - }; - } - - return status; - }, }; private isWebView?: boolean; + private isSandbox?: boolean; private coreIframe!: HTMLIFrameElement; private errorsSubscription?: () => void; private readonly headlessAppUrl = headlessCheckoutAppUrl; @@ -121,8 +100,12 @@ export class HeadlessCheckout { private readonly formSpy: FormSpy ) {} - public async init(environment: { isWebview: boolean }): Promise { + public async init(environment: { + isWebview?: boolean; + sandbox?: boolean; + }): Promise { this.isWebView = environment.isWebview; + this.isSandbox = environment.sandbox; await this.localizeService.initDictionaries(); @@ -155,6 +138,7 @@ export class HeadlessCheckout { configuration: { token, isWebView: this.isWebView, + sandbox: this.isSandbox, }, }, }; @@ -228,6 +212,16 @@ export class HeadlessCheckout { ) as Promise; } + public async getStatus(): Promise { + const msg: Message = { + name: EventName.getPaymentStatus, + }; + + return this.postMessagesClient.send(msg, (message) => + getPaymentStatusHandler(message) + ) as Promise; + } + private async setupCoreIframe(): Promise { this.coreIframe = this.window.document.createElement('iframe'); this.coreIframe.width = '0px'; diff --git a/src/features/headless-checkout/web-components/legal/secure-connection.component.template.ts b/src/features/headless-checkout/web-components/legal/secure-connection.component.template.ts index fa21ed0..7739b0b 100644 --- a/src/features/headless-checkout/web-components/legal/secure-connection.component.template.ts +++ b/src/features/headless-checkout/web-components/legal/secure-connection.component.template.ts @@ -1,6 +1,7 @@ -import { LegalComponentConfig } from './legal-component.config.interface'; -import logo from '../../../../assets/icons/logo.svg'; import i18next from 'i18next'; +import logo from '../../../../assets/icons/logo.svg'; +import { LegalComponentConfig } from './legal-component.config.interface'; + export const getSecureConnectionTemplate = ( secureConnection?: LegalComponentConfig['secureConnection'] ): string => { diff --git a/src/features/headless-checkout/web-components/status/status.component.config.interface.ts b/src/features/headless-checkout/web-components/status/status.component.config.interface.ts new file mode 100644 index 0000000..4ecb020 --- /dev/null +++ b/src/features/headless-checkout/web-components/status/status.component.config.interface.ts @@ -0,0 +1,6 @@ +export interface StatusComponentConfig { + title: string; + image: string | null; + description: string; + showDescription: boolean; +} diff --git a/src/features/headless-checkout/web-components/status/status.component.template.ts b/src/features/headless-checkout/web-components/status/status.component.template.ts new file mode 100644 index 0000000..4554403 --- /dev/null +++ b/src/features/headless-checkout/web-components/status/status.component.template.ts @@ -0,0 +1,28 @@ +import { StatusComponentConfig } from './status.component.config.interface'; + +export const getStatusComponentTemplate = ( + statusConfig: StatusComponentConfig +): string => { + return ` +
+ ${ + statusConfig.image + ? ` +
+ ${statusConfig.title} +
` + : '' + } + +
+

${statusConfig.title}

+
+ + ${ + statusConfig.showDescription + ? `

${statusConfig.description}

` + : '' + } +
+ `; +}; diff --git a/src/features/headless-checkout/web-components/status/status.component.ts b/src/features/headless-checkout/web-components/status/status.component.ts new file mode 100644 index 0000000..5810c8e --- /dev/null +++ b/src/features/headless-checkout/web-components/status/status.component.ts @@ -0,0 +1,105 @@ +import { container } from 'tsyringe'; +import i18next from 'i18next'; +import { WebComponentAbstract } from '../../../../core/web-components/web-component.abstract'; +import { Status } from '../../../../core/status/status.interface'; +import { StatusEnum } from '../../../../core/status/status.enum'; +import { HeadlessCheckout } from '../../headless-checkout'; +import { getStatusComponentTemplate } from './status.component.template'; +import { StatusComponentConfig } from './status.component.config.interface'; +import successImage from '../../../../assets/statuses/success.png'; +import failedImage from '../../../../assets/statuses/failed.png'; + +export class StatusComponent extends WebComponentAbstract { + private readonly headlessCheckout: HeadlessCheckout; + + private statusConfig: StatusComponentConfig | null = null; + + public constructor() { + super(); + + this.headlessCheckout = container.resolve(HeadlessCheckout); + } + + protected connectedCallback(): void { + void this.getStatus(); + + this.headlessCheckout.form.onNextAction((nextAction) => { + if (nextAction.type === 'status_updated') { + void this.getStatus(); + } + }); + } + + protected getHtml(): string { + if (this.statusConfig) { + return getStatusComponentTemplate(this.statusConfig); + } + + return ''; + } + + private statusLoadedHandler( + statusConfig: StatusComponentConfig | null + ): void { + this.statusConfig = statusConfig; + + this.render(); + } + + private async getStatus(): Promise { + const status = await this.headlessCheckout.getStatus(); + + const statusConfig = this.getStatusConfig(status); + + this.statusLoadedHandler(statusConfig); + } + + private getStatusConfig(status: Status): StatusComponentConfig | null { + if (!status) { + return null; + } + + const isProcessing = [ + StatusEnum.processing, + StatusEnum.created, + StatusEnum.held, + ].includes(status.statusState); + const isError = + [StatusEnum.canceled, StatusEnum.error].includes(status.statusState) || + status.isCancelUser; + const isSuccess = + status.statusState === StatusEnum.done ?? + status.statusState === StatusEnum.authorized; + + if (isProcessing) { + return { + image: null, + title: i18next.t('status.processing.title'), + description: i18next.t('status.processing.description'), + showDescription: true, + }; + } + + if (isError) { + return { + image: failedImage, + title: i18next.t('status.error.title'), + description: '', + showDescription: false, + }; + } + + if (isSuccess) { + return { + image: successImage, + title: i18next.t('status.success.title'), + description: i18next.t('status.success.description', { + email: status.email, + }), + showDescription: !!status.email, + }; + } + + return null; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 5a3a115..75f77f8 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -8,7 +8,12 @@ "privacy-policy": "Privacy policy", "refund-policy": "Refund policy", "sctl-indications": "SCTL Indications", - "secure-connection": "Secure connection" + "secure-connection": "Secure connection", + "status.processing.title": "Processing payment", + "status.processing.description": "Waiting for payment to complete...", + "status.error.title": "Payment failed", + "status.success.title": "Payment successful", + "status.success.description": "We sent your receipt to {{email}}" } } } diff --git a/src/web-components.ts b/src/web-components.ts index f7e44a8..5416fe5 100644 --- a/src/web-components.ts +++ b/src/web-components.ts @@ -2,10 +2,12 @@ import { SubmitButtonComponent } from './features/headless-checkout/web-componen 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'; +import { StatusComponent } from './features/headless-checkout/web-components/status/status.component'; export { SubmitButtonComponent, TextComponent, PaymentMethodsComponent, LegalComponent, + StatusComponent, }; diff --git a/webpack.config.js b/webpack.config.js index 5ff190c..6dccfe5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,6 +25,11 @@ const config = { type: 'asset', use: 'svgo-loader', }, + { + test: /\.png$/, + type: 'asset', + use: 'file-loader', + }, ], }, resolve: {