diff --git a/packages/core/src/payment/create-payment-strategy-registry.ts b/packages/core/src/payment/create-payment-strategy-registry.ts index dad59b6339..8f9465b6d5 100644 --- a/packages/core/src/payment/create-payment-strategy-registry.ts +++ b/packages/core/src/payment/create-payment-strategy-registry.ts @@ -27,7 +27,6 @@ import { SpamProtectionActionCreator, SpamProtectionRequestSender, } from '../spam-protection'; -import { StoreCreditActionCreator, StoreCreditRequestSender } from '../store-credit'; import createPaymentStrategyRegistryV2 from './create-payment-strategy-registry-v2'; import PaymentActionCreator from './payment-action-creator'; @@ -56,7 +55,6 @@ import { import { CBAMPGSPaymentStrategy, CBAMPGSScriptLoader } from './strategies/cba-mpgs'; import { ConvergePaymentStrategy } from './strategies/converge'; import { MasterpassPaymentStrategy, MasterpassScriptLoader } from './strategies/masterpass'; -import { MonerisPaymentStrategy } from './strategies/moneris'; import { OpyPaymentStrategy, OpyScriptLoader } from './strategies/opy'; import { PaypalExpressPaymentStrategy, PaypalScriptLoader } from './strategies/paypal'; import { @@ -94,9 +92,6 @@ export default function createPaymentStrategyRegistry( new OrderRequestSender(requestSender), checkoutValidator, ); - const storeCreditActionCreator = new StoreCreditActionCreator( - new StoreCreditRequestSender(requestSender), - ); const paymentHumanVerificationHandler = new PaymentHumanVerificationHandler( createSpamProtection(createScriptLoader()), ); @@ -242,18 +237,6 @@ export default function createPaymentStrategyRegistry( ), ); - registry.register( - PaymentStrategyType.MONERIS, - () => - new MonerisPaymentStrategy( - hostedFormFactory, - store, - orderActionCreator, - paymentActionCreator, - storeCreditActionCreator, - ), - ); - registry.register( PaymentStrategyType.OPY, () => diff --git a/packages/core/src/payment/payment-request-options.ts b/packages/core/src/payment/payment-request-options.ts index e5bea80919..1f0b966659 100644 --- a/packages/core/src/payment/payment-request-options.ts +++ b/packages/core/src/payment/payment-request-options.ts @@ -9,7 +9,6 @@ import { } from './strategies/braintree'; import { DigitalRiverPaymentInitializeOptions } from './strategies/digitalriver'; import { MasterpassPaymentInitializeOptions } from './strategies/masterpass'; -import { MonerisPaymentInitializeOptions } from './strategies/moneris'; import { OpyPaymentInitializeOptions } from './strategies/opy'; import { PaypalExpressPaymentInitializeOptions } from './strategies/paypal'; import { SquarePaymentInitializeOptions } from './strategies/square'; @@ -78,12 +77,6 @@ export interface BasePaymentInitializeOptions extends PaymentRequestOptions { */ masterpass?: MasterpassPaymentInitializeOptions; - /** - * The options that are required to initialize the Moneris payment method. - * They can be omitted unless you need to support Moneris. - */ - moneris?: MonerisPaymentInitializeOptions; - /** * The options that are required to initialize the Opy payment * method. They can be omitted unless you need to support Opy. diff --git a/packages/core/src/payment/strategies/moneris/index.ts b/packages/core/src/payment/strategies/moneris/index.ts deleted file mode 100644 index b491e3e8fd..0000000000 --- a/packages/core/src/payment/strategies/moneris/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as MonerisPaymentInitializeOptions } from './moneris-payment-initialize-options'; -export { default as MonerisPaymentStrategy } from './moneris-payment-strategy'; -export { default as MonerisStylingProps } from './moneris'; diff --git a/packages/core/src/payment/strategies/moneris/moneris-payment-strategy.ts b/packages/core/src/payment/strategies/moneris/moneris-payment-strategy.ts deleted file mode 100644 index 16ab2af0a0..0000000000 --- a/packages/core/src/payment/strategies/moneris/moneris-payment-strategy.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { isEmpty, map, omitBy } from 'lodash'; - -import { isHostedInstrumentLike } from '../../'; -import { CheckoutStore, InternalCheckoutSelectors } from '../../../checkout'; -import { - InvalidArgumentError, - MissingDataError, - MissingDataErrorType, - NotInitializedError, - NotInitializedErrorType, -} from '../../../common/error/errors'; -import { HostedForm, HostedFormFactory, HostedFormOptions } from '../../../hosted-form'; -import { OrderActionCreator, OrderPaymentRequestBody, OrderRequestBody } from '../../../order'; -import { OrderFinalizationNotRequiredError } from '../../../order/errors'; -import { StoreCreditActionCreator } from '../../../store-credit'; -import { PaymentArgumentInvalidError } from '../../errors'; -import isVaultedInstrument from '../../is-vaulted-instrument'; -import PaymentActionCreator from '../../payment-action-creator'; -import { PaymentInitializeOptions } from '../../payment-request-options'; -import PaymentStrategy from '../payment-strategy'; - -import MonerisStylingProps, { - MoneriesHostedFieldsQueryParams, - MonerisInitializationData, - MonerisResponseData, -} from './moneris'; -import MonerisPaymentInitializeOptions from './moneris-payment-initialize-options'; - -const IFRAME_NAME = 'moneris-payment-iframe'; -const RESPONSE_SUCCESS_CODE = '001'; - -export default class MonerisPaymentStrategy implements PaymentStrategy { - private _iframe?: HTMLIFrameElement; - private _initializeOptions?: MonerisPaymentInitializeOptions; - private _windowEventListener?: (response: MessageEvent) => void; - - private _hostedForm?: HostedForm; - - constructor( - private _hostedFormFactory: HostedFormFactory, - private _store: CheckoutStore, - private _orderActionCreator: OrderActionCreator, - private _paymentActionCreator: PaymentActionCreator, - private _storeCreditActionCreator: StoreCreditActionCreator, - ) {} - - async initialize(options: PaymentInitializeOptions): Promise { - const state = this._store.getState(); - - const { moneris: monerisOptions, methodId } = options; - - if (!methodId) { - throw new InvalidArgumentError( - 'Unable to initialize payment because "methodId" argument is not provided.', - ); - } - - if (!monerisOptions) { - throw new InvalidArgumentError( - 'Unable to initialize payment because "options.moneris" argument is not provided.', - ); - } - - this._initializeOptions = monerisOptions; - - const { config, initializationData } = - state.paymentMethods.getPaymentMethodOrThrow(methodId); - - if (!initializationData?.profileId) { - throw new MissingDataError(MissingDataErrorType.MissingPaymentMethod); - } - - if (monerisOptions.form && this._shouldShowTSVHostedForm(methodId)) { - this._hostedForm = await this._mountCardVerificationfields(monerisOptions.form); - } - - if (!this._iframe) { - this._iframe = this._createIframe( - monerisOptions.containerId, - initializationData, - !!config.testMode, - ); - } - - return Promise.resolve(this._store.getState()); - } - - async execute( - payload: OrderRequestBody, - options?: PaymentInitializeOptions, - ): Promise { - const { payment, ...order } = payload; - - if (!payment) { - throw new PaymentArgumentInvalidError(['payment']); - } - - const { isStoreCreditApplied: useStoreCredit } = this._store - .getState() - .checkout.getCheckoutOrThrow(); - - if (useStoreCredit !== undefined) { - await this._store.dispatch( - this._storeCreditActionCreator.applyStoreCredit(useStoreCredit), - ); - } - - await this._store.dispatch(this._orderActionCreator.submitOrder(order, options)); - - if (payment.paymentData && isVaultedInstrument(payment.paymentData)) { - return this._executeWithVaulted(payment); - } - - return this._executeWithCC(payment); - } - - finalize(): Promise { - return Promise.reject(new OrderFinalizationNotRequiredError()); - } - - deinitialize(): Promise { - if (this._hostedForm) { - this._hostedForm.detach(); - } - - if (this._windowEventListener) { - window.removeEventListener('message', this._windowEventListener); - this._windowEventListener = undefined; - } - - if (this._iframe && this._iframe.parentNode) { - this._iframe.parentNode.removeChild(this._iframe); - this._iframe = undefined; - } - - return Promise.resolve(this._store.getState()); - } - - private async _executeWithCC( - payment: OrderPaymentRequestBody, - ): Promise { - const { - paymentMethods: { getPaymentMethodOrThrow }, - } = this._store.getState(); - const paymentMethod = getPaymentMethodOrThrow(payment.methodId); - - const testMode = paymentMethod.config.testMode; - const paymentData = payment.paymentData || {}; - const instrumentSettings = isHostedInstrumentLike(paymentData) - ? paymentData - : { shouldSaveInstrument: false, shouldSetAsDefaultInstrument: false }; - - const { shouldSaveInstrument, shouldSetAsDefaultInstrument } = instrumentSettings; - - const nonce = await new Promise((resolve, reject) => { - if (!this._iframe) { - throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); - } - - const frameref = this._iframe.contentWindow; - - frameref?.postMessage('tokenize', this._monerisURL(!!testMode)); - - this._windowEventListener = (response: MessageEvent) => { - if (typeof response.data !== 'string') { - return; - } - - try { - resolve(this._handleMonerisResponse(response)); - } catch (error) { - reject(error); - } - }; - - window.addEventListener('message', this._windowEventListener); - }); - - if (nonce !== undefined) { - return this._store.dispatch( - this._paymentActionCreator.submitPayment({ - methodId: payment.methodId, - paymentData: { nonce, shouldSaveInstrument, shouldSetAsDefaultInstrument }, - }), - ); - } - - return this._store.getState(); - } - - private async _executeWithVaulted( - payment: OrderPaymentRequestBody, - ): Promise { - if (this._hostedForm) { - const form = this._hostedForm; - - if (!form) { - throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); - } - - await form.validate(); - await form.submit(payment); - - return this._store.dispatch(this._orderActionCreator.loadCurrentOrder()); - } - - return this._store.dispatch(this._paymentActionCreator.submitPayment(payment)); - } - - private _shouldShowTSVHostedForm(methodId: string): boolean { - return this._isHostedPaymentFormEnabled(methodId) && this._isHostedFieldAvailable(); - } - - private _isHostedPaymentFormEnabled(methodId: string): boolean { - const { - paymentMethods: { getPaymentMethodOrThrow }, - } = this._store.getState(); - const paymentMethod = getPaymentMethodOrThrow(methodId); - - return Boolean(paymentMethod.config.isHostedFormEnabled); - } - - private _isHostedFieldAvailable(): boolean { - const options = this._getInitializeOptions(); - const definedFields = omitBy(options.form?.fields, isEmpty); - - return !isEmpty(definedFields); - } - - private _getInitializeOptions(): MonerisPaymentInitializeOptions { - if (!this._initializeOptions) { - throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); - } - - return this._initializeOptions; - } - - private async _mountCardVerificationfields( - formOptions: HostedFormOptions, - ): Promise { - const { config } = this._store.getState(); - const bigpayBaseUrl = config.getStoreConfig()?.paymentSettings.bigpayBaseUrl; - - if (!bigpayBaseUrl) { - throw new MissingDataError(MissingDataErrorType.MissingCheckoutConfig); - } - - const form = this._hostedFormFactory.create(bigpayBaseUrl, formOptions); - - await form.attach(); - - return form; - } - - private _createIframe( - containerId: string, - initializationData: MonerisInitializationData, - testMode: boolean, - style?: MonerisStylingProps, - ): HTMLIFrameElement { - const container = document.getElementById(containerId); - - if (!container) { - throw new InvalidArgumentError('Unable to create iframe without valid container ID.'); - } - - const iframe = document.createElement('iframe'); - const monerisQueryParams: MoneriesHostedFieldsQueryParams = { - id: initializationData.profileId, - pmmsg: true, - display_labels: 1, - enable_exp: 1, - enable_cvd: 1, - css_body: - style?.cssBody || - 'font-family: Arial, Helvetica,sans-serif;background: transparent;', - css_textbox: - style?.cssTextbox || - 'border-radius:4px;border: 2px solid rgb(00,00,00);width: 100%;font-weight: 600;padding: 8px 8px;outline: 0;', - css_textbox_pan: style?.cssTextboxCardNumber || 'width: 240px;', - css_textbox_exp: - style?.cssTextboxExpiryDate || 'margin-bottom: 0;width: calc(30% - 12px);', - css_textbox_cvd: style?.cssTextboxCVV || 'margin-bottom: 0;width: calc(30% - 12px);', - css_input_label: - style?.cssInputLabel || - 'font-size: 10px;position: relative;top: 8px;left: 6px;background: rgb(255,255,255);padding: 3px 2px;color: rgb(66,66,66);font-weight: 600;z-index: 2;', - pan_label: initializationData.creditCardLabel || 'Credit Card Number', - exp_label: initializationData.expiryDateLabel || 'Expiration', - cvd_label: initializationData.cvdLabel || 'CVD', - }; - - const queryString = map(monerisQueryParams, (value, key) => `${key}=${value}`).join('&'); - - iframe.width = '100%'; - iframe.height = '100%'; - iframe.name = IFRAME_NAME; - iframe.id = IFRAME_NAME; - iframe.style.border = 'none'; - iframe.src = `${this._monerisURL(testMode)}?${queryString}`; - - container.appendChild(iframe); - - return iframe; - } - - private _handleMonerisResponse(response: MessageEvent): string { - const monerisResponse: MonerisResponseData = JSON.parse(response.data); - - if (monerisResponse.responseCode[0] !== RESPONSE_SUCCESS_CODE) { - throw new Error(monerisResponse.errorMessage); - } - - return monerisResponse.dataKey; - } - - private _monerisURL(testMode: boolean): string { - return `https://${testMode ? 'esqa' : 'www3'}.moneris.com/HPPtoken/index.php`; - } -} diff --git a/packages/moneris-integration/.eslintrc.json b/packages/moneris-integration/.eslintrc.json new file mode 100644 index 0000000000..460e57bb34 --- /dev/null +++ b/packages/moneris-integration/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off" + } + }, + { + "files": ["*.test.ts"], + "rules": { + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/consistent-type-assertions": "off" + } + } + ] +} diff --git a/packages/moneris-integration/README.md b/packages/moneris-integration/README.md new file mode 100644 index 0000000000..1ade0a53a4 --- /dev/null +++ b/packages/moneris-integration/README.md @@ -0,0 +1,12 @@ +# moneris-integration + +This package contains the integration layer for the [Moneris](https://www.moneris.com/) provider. +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test moneris-integration` to execute the unit tests via [Jest](https://jestjs.io). + +## Running lint + +Run `nx lint moneris-integration` to execute the lint via [ESLint](https://eslint.org/). diff --git a/packages/moneris-integration/jest.config.js b/packages/moneris-integration/jest.config.js new file mode 100644 index 0000000000..049af9fc15 --- /dev/null +++ b/packages/moneris-integration/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + displayName: 'moneris-integration', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + diagnostics: false, + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + setupFilesAfterEnv: ['../../jest-setup.js'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/packages/moneris-integration', +}; diff --git a/packages/moneris-integration/project.json b/packages/moneris-integration/project.json new file mode 100644 index 0000000000..9b3b4f20b5 --- /dev/null +++ b/packages/moneris-integration/project.json @@ -0,0 +1,23 @@ +{ + "root": "packages/moneris-integration", + "sourceRoot": "packages/moneris-integration/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/moneris-integration/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/packages/moneris-integration"], + "options": { + "jestConfig": "packages/moneris-integration/jest.config.js", + "passWithNoTests": true + } + } + }, + "tags": ["scope:integration"] +} diff --git a/packages/moneris-integration/src/create-moneris-payment-strategy.test.ts b/packages/moneris-integration/src/create-moneris-payment-strategy.test.ts new file mode 100644 index 0000000000..135110ce7c --- /dev/null +++ b/packages/moneris-integration/src/create-moneris-payment-strategy.test.ts @@ -0,0 +1,19 @@ +import { PaymentIntegrationService } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { PaymentIntegrationServiceMock } from '@bigcommerce/checkout-sdk/payment-integrations-test-utils'; + +import createMonerisPaymentStrategy from './create-moneris-payment-strategy'; +import MonerisPaymentStrategy from './moneris-payment-strategy'; + +describe('createMonerisPaymentStrategy', () => { + let paymentIntegrationService: PaymentIntegrationService; + + beforeEach(() => { + paymentIntegrationService = new PaymentIntegrationServiceMock(); + }); + + it('instantiates Moneris payment strategy', () => { + const strategy = createMonerisPaymentStrategy(paymentIntegrationService); + + expect(strategy).toBeInstanceOf(MonerisPaymentStrategy); + }); +}); diff --git a/packages/moneris-integration/src/create-moneris-payment-strategy.ts b/packages/moneris-integration/src/create-moneris-payment-strategy.ts new file mode 100644 index 0000000000..757ab172bb --- /dev/null +++ b/packages/moneris-integration/src/create-moneris-payment-strategy.ts @@ -0,0 +1,14 @@ +import { + PaymentStrategyFactory, + toResolvableModule, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import MonerisPaymentStrategy from './moneris-payment-strategy'; + +const createMonerisPaymentStrategy: PaymentStrategyFactory = ( + paymentIntegrationService, +) => { + return new MonerisPaymentStrategy(paymentIntegrationService); +}; + +export default toResolvableModule(createMonerisPaymentStrategy, [{ id: 'moneris' }]); diff --git a/packages/moneris-integration/src/index.ts b/packages/moneris-integration/src/index.ts new file mode 100644 index 0000000000..bdfd8b1805 --- /dev/null +++ b/packages/moneris-integration/src/index.ts @@ -0,0 +1 @@ +export { default as createMonerisPaymentStrategy } from './create-moneris-payment-strategy'; diff --git a/packages/core/src/payment/strategies/moneris/moneris-payment-initialize-options.ts b/packages/moneris-integration/src/moneris-payment-initialize-options.ts similarity index 86% rename from packages/core/src/payment/strategies/moneris/moneris-payment-initialize-options.ts rename to packages/moneris-integration/src/moneris-payment-initialize-options.ts index 1e9dcc1be2..b2c88b915e 100644 --- a/packages/core/src/payment/strategies/moneris/moneris-payment-initialize-options.ts +++ b/packages/moneris-integration/src/moneris-payment-initialize-options.ts @@ -1,4 +1,4 @@ -import { HostedFormOptions } from '../../../hosted-form'; +import { HostedFormOptions } from '@bigcommerce/checkout-sdk/payment-integration-api'; import MonerisStylingProps from './moneris'; @@ -41,3 +41,7 @@ export default interface MonerisPaymentInitializeOptions { */ form?: HostedFormOptions; } + +export interface WithMonerisPaymentInitializeOptions { + moneris?: MonerisPaymentInitializeOptions; +} diff --git a/packages/moneris-integration/src/moneris-payment-method.mock.ts b/packages/moneris-integration/src/moneris-payment-method.mock.ts new file mode 100644 index 0000000000..14c372f258 --- /dev/null +++ b/packages/moneris-integration/src/moneris-payment-method.mock.ts @@ -0,0 +1,23 @@ +import { PaymentMethod } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +export function getMoneris(): PaymentMethod { + return { + id: 'moneris', + gateway: '', + logoUrl: '', + method: 'moneris', + supportedCards: [], + config: { + isHostedFormEnabled: false, + displayName: 'Moneris', + testMode: false, + }, + type: 'PAYMENT_TYPE_API', + initializationData: { + profileId: 'ABC123', + creditCardLabel: 'Credit Card', + expiryDateLabel: 'Expiration Date', + cvdLabel: 'CVV', + }, + }; +} diff --git a/packages/core/src/payment/strategies/moneris/moneris-payment-strategy.spec.ts b/packages/moneris-integration/src/moneris-payment-strategy.test.ts similarity index 64% rename from packages/core/src/payment/strategies/moneris/moneris-payment-strategy.spec.ts rename to packages/moneris-integration/src/moneris-payment-strategy.test.ts index d8a04b7ac5..06ec5fd105 100644 --- a/packages/core/src/payment/strategies/moneris/moneris-payment-strategy.spec.ts +++ b/packages/moneris-integration/src/moneris-payment-strategy.test.ts @@ -1,106 +1,44 @@ -import { createClient as createPaymentClient } from '@bigcommerce/bigpay-client'; -import { Action, createAction } from '@bigcommerce/data-store'; -import { createRequestSender, RequestSender } from '@bigcommerce/request-sender'; -import { createScriptLoader } from '@bigcommerce/script-loader'; import { merge } from 'lodash'; -import { Observable, of } from 'rxjs'; import { Checkout, - CheckoutRequestSender, - CheckoutStore, - CheckoutValidator, - createCheckoutStore, - InternalCheckoutSelectors, -} from '../../../checkout'; -import { getCheckout, getCheckoutStoreState } from '../../../checkout/checkouts.mock'; -import { + HostedFieldType, + HostedForm, InvalidArgumentError, MissingDataError, NotInitializedError, -} from '../../../common/error/errors'; -import { HostedFieldType, HostedForm, HostedFormFactory } from '../../../hosted-form'; -import { - LoadOrderSucceededAction, - OrderActionCreator, - OrderActionType, + OrderFinalizationNotRequiredError, OrderRequestBody, - OrderRequestSender, - SubmitOrderAction, -} from '../../../order'; -import { OrderFinalizationNotRequiredError } from '../../../order/errors'; -import { getOrderRequestBody } from '../../../order/internal-orders.mock'; -import { getOrder } from '../../../order/orders.mock'; -import { PaymentMethod, PaymentRequestSender } from '../../../payment'; -import { getMoneris } from '../../../payment/payment-methods.mock'; -import { createSpamProtection, PaymentHumanVerificationHandler } from '../../../spam-protection'; + PaymentArgumentInvalidError, + PaymentInitializeOptions, + PaymentIntegrationService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; import { - StoreCreditActionCreator, - StoreCreditActionType, - StoreCreditRequestSender, -} from '../../../store-credit'; -import { PaymentArgumentInvalidError } from '../../errors'; -import PaymentActionCreator from '../../payment-action-creator'; -import { PaymentActionType, SubmitPaymentAction } from '../../payment-actions'; -import { PaymentInitializeOptions, PaymentRequestOptions } from '../../payment-request-options'; -import PaymentRequestTransformer from '../../payment-request-transformer'; + getCheckout, + getOrderRequestBody, + PaymentIntegrationServiceMock, +} from '@bigcommerce/checkout-sdk/payment-integrations-test-utils'; +import { WithMonerisPaymentInitializeOptions } from './moneris-payment-initialize-options'; +import { getMoneris } from './moneris-payment-method.mock'; import MonerisPaymentStrategy from './moneris-payment-strategy'; import { getHostedFormInitializeOptions, getOrderRequestBodyVaultedCC } from './moneris.mock'; describe('MonerisPaymentStrategy', () => { const containerId = 'moneris_iframe_container'; const iframeId = 'moneris-payment-iframe'; - let applyStoreCreditAction: Observable; let checkoutMock: Checkout; let container: HTMLDivElement; - let formFactory: HostedFormFactory; let initializeOptions: PaymentInitializeOptions; - let options: PaymentRequestOptions; - let orderActionCreator: OrderActionCreator; + let options: PaymentInitializeOptions; let payload: OrderRequestBody; - let paymentActionCreator: PaymentActionCreator; - let paymentHumanVerificationHandler: PaymentHumanVerificationHandler; let paymentMethodMock: PaymentMethod; - let paymentRequestTransformer: PaymentRequestTransformer; - let paymentRequestSender: PaymentRequestSender; - let requestSender: RequestSender; - let store: CheckoutStore; - let storeCreditActionCreator: StoreCreditActionCreator; let strategy: MonerisPaymentStrategy; - let submitPaymentAction: Observable; - - let submitOrderAction: Observable; + let paymentIntegrationService: PaymentIntegrationService; beforeEach(() => { - store = createCheckoutStore(getCheckoutStoreState()); - requestSender = createRequestSender(); - - orderActionCreator = new OrderActionCreator( - new OrderRequestSender(requestSender), - new CheckoutValidator(new CheckoutRequestSender(requestSender)), - ); - - paymentRequestTransformer = new PaymentRequestTransformer(); - paymentRequestSender = new PaymentRequestSender(createPaymentClient()); - paymentHumanVerificationHandler = new PaymentHumanVerificationHandler( - createSpamProtection(createScriptLoader()), - ); - paymentActionCreator = new PaymentActionCreator( - paymentRequestSender, - orderActionCreator, - paymentRequestTransformer, - paymentHumanVerificationHandler, - ); - - applyStoreCreditAction = of(createAction(StoreCreditActionType.ApplyStoreCreditRequested)); paymentMethodMock = getMoneris(); - submitOrderAction = of(createAction(OrderActionType.SubmitOrderRequested)); - submitPaymentAction = of(createAction(PaymentActionType.SubmitPaymentRequested)); - - storeCreditActionCreator = new StoreCreditActionCreator( - new StoreCreditRequestSender(requestSender), - ); initializeOptions = { methodId: 'moneris', @@ -119,56 +57,42 @@ describe('MonerisPaymentStrategy', () => { }, }); + paymentIntegrationService = new PaymentIntegrationServiceMock(); + container = document.createElement('div'); container.setAttribute('id', containerId); document.body.appendChild(container); jest.spyOn(document, 'getElementById'); jest.spyOn(document, 'createElement'); - jest.spyOn(store, 'dispatch'); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(orderActionCreator, 'submitOrder').mockReturnValue(submitOrderAction); + jest.spyOn(paymentIntegrationService, 'submitOrder').mockImplementation(jest.fn()); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(paymentActionCreator, 'submitPayment').mockReturnValue(submitPaymentAction); + jest.spyOn(paymentIntegrationService, 'submitPayment').mockImplementation(jest.fn()); - jest.spyOn(store.getState().checkout, 'getCheckoutOrThrow').mockReturnValue(checkoutMock); + jest.spyOn(paymentIntegrationService.getState(), 'getCheckoutOrThrow').mockReturnValue( + checkoutMock, + ); - jest.spyOn(store.getState().paymentMethods, 'getPaymentMethodOrThrow').mockReturnValue( + jest.spyOn(paymentIntegrationService.getState(), 'getPaymentMethodOrThrow').mockReturnValue( paymentMethodMock, ); - jest.spyOn(storeCreditActionCreator, 'applyStoreCredit').mockReturnValue( - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - applyStoreCreditAction, - ); + jest.spyOn(paymentIntegrationService, 'applyStoreCredit').mockImplementation(jest.fn()); jest.spyOn(window, 'removeEventListener'); - formFactory = new HostedFormFactory(store); - strategy = new MonerisPaymentStrategy( - formFactory, - store, - orderActionCreator, - paymentActionCreator, - storeCreditActionCreator, - ); + strategy = new MonerisPaymentStrategy(paymentIntegrationService); }); afterEach(() => { document.body.removeChild(container); + jest.clearAllMocks(); }); describe('#initialize', () => { it('successfully initializes moneris strategy and creates the iframe', async () => { - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); expect(document.getElementById).toHaveBeenCalledWith(containerId); expect(document.createElement).toHaveBeenCalledWith('iframe'); @@ -185,7 +109,7 @@ describe('MonerisPaymentStrategy', () => { it('initializes moneris iframe and sets src to the live environment', async () => { paymentMethodMock.config.testMode = false; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -196,7 +120,7 @@ describe('MonerisPaymentStrategy', () => { it('initializes moneris iframe and sets src to the test environment', async () => { paymentMethodMock.config.testMode = true; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -207,7 +131,7 @@ describe('MonerisPaymentStrategy', () => { it('initialize moneris iframe and sets data labels from initialization data', async () => { paymentMethodMock.config.testMode = true; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -221,7 +145,7 @@ describe('MonerisPaymentStrategy', () => { paymentMethodMock.config.testMode = true; paymentMethodMock.initializationData = { profileId: 'ABC123' }; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -234,7 +158,7 @@ describe('MonerisPaymentStrategy', () => { it('initialize moneris iframe and sets expiry field', async () => { paymentMethodMock.config.testMode = true; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -245,7 +169,7 @@ describe('MonerisPaymentStrategy', () => { it('initialize moneris iframe and sets cvv field', async () => { paymentMethodMock.config.testMode = true; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -256,7 +180,7 @@ describe('MonerisPaymentStrategy', () => { it('initialize moneris iframe and sets labels enabled', async () => { paymentMethodMock.config.testMode = true; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -267,7 +191,7 @@ describe('MonerisPaymentStrategy', () => { it('initialize moneris iframe and sets hosted fields css', async () => { paymentMethodMock.config.testMode = true; - await expect(strategy.initialize(initializeOptions)).resolves.toEqual(store.getState()); + await strategy.initialize(initializeOptions); const iframe = document.getElementById(iframeId) as HTMLIFrameElement; @@ -319,9 +243,8 @@ describe('MonerisPaymentStrategy', () => { }; window.postMessage(JSON.stringify(mockMonerisIframeMessage), '*'); - await expect(promise).rejects.toThrow(new Error('expected error message')); - expect(paymentActionCreator.submitPayment).not.toHaveBeenCalled(); + expect(paymentIntegrationService.submitPayment).not.toHaveBeenCalled(); }); it('successfully executes moneris strategy and submits payment', async () => { @@ -335,7 +258,6 @@ describe('MonerisPaymentStrategy', () => { }; checkoutMock.isStoreCreditApplied = true; - await strategy.initialize(initializeOptions); const promise = strategy.execute(payload, options); @@ -350,11 +272,9 @@ describe('MonerisPaymentStrategy', () => { }; window.postMessage(JSON.stringify(mockMonerisIframeMessage), '*'); - await promise; - - expect(storeCreditActionCreator.applyStoreCredit).toHaveBeenCalledWith(true); - expect(paymentActionCreator.submitPayment).toHaveBeenCalledWith(expectedPayment); + expect(paymentIntegrationService.applyStoreCredit).toHaveBeenCalledWith(true); + expect(paymentIntegrationService.submitPayment).toHaveBeenCalledWith(expectedPayment); }); it('submits payment with vaulted card', async () => { @@ -370,7 +290,6 @@ describe('MonerisPaymentStrategy', () => { await strategy.initialize(initializeOptions); const pendingExecution = strategy.execute(getOrderRequestBodyVaultedCC(), options); - const mockMonerisIframeMessage = { responseCode: ['001'], errorMessage: null, @@ -379,10 +298,8 @@ describe('MonerisPaymentStrategy', () => { }; window.postMessage(JSON.stringify(mockMonerisIframeMessage), '*'); - await pendingExecution; - - expect(paymentActionCreator.submitPayment).toHaveBeenCalledWith(expectedPayment); + expect(paymentIntegrationService.submitPayment).toHaveBeenCalledWith(expectedPayment); }); it('Moneris returns an object instead of a stringify JSON', async () => { @@ -398,7 +315,6 @@ describe('MonerisPaymentStrategy', () => { await strategy.initialize(initializeOptions); const pendingExecution = strategy.execute(getOrderRequestBodyVaultedCC(), options); - const mockMonerisIframeMessage = { responseCode: ['001'], errorMessage: null, @@ -407,10 +323,8 @@ describe('MonerisPaymentStrategy', () => { }; window.postMessage(mockMonerisIframeMessage, '*'); - await pendingExecution; - - expect(paymentActionCreator.submitPayment).toHaveBeenCalledWith(expectedPayment); + expect(paymentIntegrationService.submitPayment).toHaveBeenCalledWith(expectedPayment); }); it('submits payment and sends shouldSaveInstrument and shouldSetAsDefaultInstrument if provided', async () => { @@ -431,7 +345,6 @@ describe('MonerisPaymentStrategy', () => { await strategy.initialize(initializeOptions); const pendingExecution = strategy.execute(vaultingPayload, options); - const mockMonerisIframeMessage = { responseCode: ['001'], errorMessage: null, @@ -440,10 +353,8 @@ describe('MonerisPaymentStrategy', () => { }; window.postMessage(JSON.stringify(mockMonerisIframeMessage), '*'); - await pendingExecution; - - expect(paymentActionCreator.submitPayment).toHaveBeenCalledWith(expectedPayment); + expect(paymentIntegrationService.submitPayment).toHaveBeenCalledWith(expectedPayment); }); it('submits payment with intrument if provided', async () => { @@ -459,89 +370,74 @@ describe('MonerisPaymentStrategy', () => { await strategy.initialize(initializeOptions); await strategy.execute(vaultingPayload, options); - - expect(paymentActionCreator.submitPayment).toHaveBeenCalledWith(expectedPayment); + expect(paymentIntegrationService.submitPayment).toHaveBeenCalledWith(expectedPayment); }); it('fails to executes moneris strategy when payment is not provided', async () => { payload.payment = undefined; await strategy.initialize(initializeOptions); - await expect(strategy.execute(payload)).rejects.toThrow(PaymentArgumentInvalidError); - - expect(paymentActionCreator.submitPayment).not.toHaveBeenCalled(); + expect(paymentIntegrationService.submitPayment).not.toHaveBeenCalled(); }); it('fails to executes moneris strategy when the strategy is not previously initialized', async () => { await expect(strategy.execute(payload)).rejects.toThrow(NotInitializedError); - - expect(paymentActionCreator.submitPayment).not.toHaveBeenCalled(); + expect(paymentIntegrationService.submitPayment).not.toHaveBeenCalled(); }); }); describe('When Hosted Form is enabled', () => { - let form: Pick; - let initializeOptions: PaymentInitializeOptions; - let loadOrderAction: Observable; - let state: InternalCheckoutSelectors; + let form: HostedForm; + let hostedFormInitializeOptions: PaymentInitializeOptions & + WithMonerisPaymentInitializeOptions; beforeEach(() => { form = { attach: jest.fn(() => Promise.resolve()), - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - submit: jest.fn(() => Promise.resolve()), + submit: jest.fn(), validate: jest.fn(() => Promise.resolve()), detach: jest.fn(), + getBin: jest.fn(), + getCardType: jest.fn(), }; - initializeOptions = getHostedFormInitializeOptions(); - loadOrderAction = of(createAction(OrderActionType.LoadOrderSucceeded, getOrder())); - state = store.getState(); - - jest.spyOn(state.paymentMethods, 'getPaymentMethodOrThrow').mockReturnValue( - merge(getMoneris(), { config: { isHostedFormEnabled: true } }), - ); - - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(orderActionCreator, 'loadCurrentOrder').mockReturnValue(loadOrderAction); + hostedFormInitializeOptions = getHostedFormInitializeOptions(); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(formFactory, 'create').mockReturnValue(form); + jest.spyOn( + paymentIntegrationService.getState(), + 'getPaymentMethodOrThrow', + ).mockReturnValue(merge(getMoneris(), { config: { isHostedFormEnabled: true } })); + jest.spyOn(paymentIntegrationService, 'loadCurrentOrder').mockImplementation(jest.fn()); + jest.spyOn(paymentIntegrationService, 'createHostedForm').mockReturnValue(form); }); it('creates hosted form', async () => { - await strategy.initialize(initializeOptions); + await strategy.initialize(hostedFormInitializeOptions); - expect(formFactory.create).toHaveBeenCalledWith( + expect(paymentIntegrationService.createHostedForm).toHaveBeenCalledWith( 'https://bigpay.integration.zone', - initializeOptions.moneris?.form, + hostedFormInitializeOptions.moneris?.form, ); }); it('attaches hosted form to container', async () => { - await strategy.initialize(initializeOptions); + await strategy.initialize(hostedFormInitializeOptions); expect(form.attach).toHaveBeenCalled(); }); it('submits payment data with hosted form', async () => { - const payload = getOrderRequestBodyVaultedCC(); + payload = getOrderRequestBodyVaultedCC(); - await strategy.initialize(initializeOptions); + await strategy.initialize(hostedFormInitializeOptions); await strategy.execute(payload); expect(form.submit).toHaveBeenCalledWith(payload.payment); }); it('validates user input before submitting data', async () => { - const payload = getOrderRequestBodyVaultedCC(); + payload = getOrderRequestBodyVaultedCC(); - await strategy.initialize(initializeOptions); + await strategy.initialize(hostedFormInitializeOptions); await strategy.execute(payload); expect(form.validate).toHaveBeenCalled(); @@ -550,7 +446,7 @@ describe('MonerisPaymentStrategy', () => { it('does not submit payment data with hosted form if validation fails', async () => { jest.spyOn(form, 'validate').mockRejectedValue(new Error()); - await strategy.initialize(initializeOptions); + await strategy.initialize(hostedFormInitializeOptions); await expect(strategy.execute(getOrderRequestBodyVaultedCC())).rejects.toThrow(); expect(form.submit).not.toHaveBeenCalled(); @@ -579,17 +475,17 @@ describe('MonerisPaymentStrategy', () => { }, }; - const payload = getOrderRequestBodyVaultedCC(); + payload = getOrderRequestBodyVaultedCC(); await strategy.initialize(noFieldPayload); await strategy.execute(payload); - expect(paymentActionCreator.submitPayment).toHaveBeenCalledWith(expectedPayment); + expect(paymentIntegrationService.submitPayment).toHaveBeenCalledWith(expectedPayment); expect(form.submit).not.toHaveBeenCalled(); }); it('should detach hostedForm on Deinitialize', async () => { - await strategy.initialize(initializeOptions); + await strategy.initialize(hostedFormInitializeOptions); await strategy.deinitialize(); expect(form.detach).toHaveBeenCalled(); @@ -613,20 +509,16 @@ describe('MonerisPaymentStrategy', () => { }; window.postMessage(JSON.stringify(mockMonerisIframeMessage), '*'); - await expect(promise).rejects.toThrow(new Error('expected error message')); - - expect(await strategy.deinitialize()).toEqual(store.getState()); + await strategy.deinitialize(); expect(window.removeEventListener).toHaveBeenCalledWith( 'message', expect.any(Function), ); }); - it('deinitializes strategy and removes the iframe if it exists', async () => { await strategy.initialize(initializeOptions); - - expect(await strategy.deinitialize()).toEqual(store.getState()); + await strategy.deinitialize(); expect(container.childElementCount).toBe(0); }); }); diff --git a/packages/moneris-integration/src/moneris-payment-strategy.ts b/packages/moneris-integration/src/moneris-payment-strategy.ts new file mode 100644 index 0000000000..47db0d305a --- /dev/null +++ b/packages/moneris-integration/src/moneris-payment-strategy.ts @@ -0,0 +1,304 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { isEmpty, map, omitBy } from 'lodash'; + +import { + HostedForm, + HostedFormOptions, + InvalidArgumentError, + isHostedInstrumentLike, + isVaultedInstrument, + MissingDataError, + MissingDataErrorType, + NotInitializedError, + NotInitializedErrorType, + OrderFinalizationNotRequiredError, + OrderPaymentRequestBody, + OrderRequestBody, + PaymentArgumentInvalidError, + PaymentInitializeOptions, + PaymentIntegrationSelectors, + PaymentIntegrationService, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import MonerisStylingProps, { + MoneriesHostedFieldsQueryParams, + MonerisInitializationData, + MonerisResponseData, +} from './moneris'; +import MonerisPaymentInitializeOptions, { + WithMonerisPaymentInitializeOptions, +} from './moneris-payment-initialize-options'; + +const IFRAME_NAME = 'moneris-payment-iframe'; +const RESPONSE_SUCCESS_CODE = '001'; + +export default class MonerisPaymentStrategy { + private iframe?: HTMLIFrameElement; + private initializeOptions?: MonerisPaymentInitializeOptions; + private windowEventListener?: (response: MessageEvent) => void; + + private hostedForm?: HostedForm; + constructor(private paymentIntegrationService: PaymentIntegrationService) {} + + async initialize( + options: PaymentInitializeOptions & WithMonerisPaymentInitializeOptions, + ): Promise { + const state = this.paymentIntegrationService.getState(); + + const { moneris: monerisOptions, methodId } = options; + + if (!methodId) { + throw new InvalidArgumentError( + 'Unable to initialize payment because "methodId" argument is not provided.', + ); + } + + if (!monerisOptions) { + throw new InvalidArgumentError( + 'Unable to initialize payment because "options.moneris" argument is not provided.', + ); + } + + this.initializeOptions = monerisOptions; + + const { config, initializationData } = + state.getPaymentMethodOrThrow(methodId); + + if (!initializationData?.profileId) { + throw new MissingDataError(MissingDataErrorType.MissingPaymentMethod); + } + + if (monerisOptions.form && this.shouldShowTSVHostedForm(methodId)) { + this.hostedForm = await this.mountCardVerificationfields(monerisOptions.form); + } + + if (!this.iframe) { + this.iframe = this.createIframe( + monerisOptions.containerId, + initializationData, + !!config.testMode, + ); + } + + return Promise.resolve(); + } + + async execute(payload: OrderRequestBody, options?: PaymentInitializeOptions): Promise { + const { payment, ...order } = payload; + + if (!payment) { + throw new PaymentArgumentInvalidError(['payment']); + } + + const { isStoreCreditApplied: useStoreCredit } = this.paymentIntegrationService + .getState() + .getCheckoutOrThrow(); + + if (useStoreCredit) { + await this.paymentIntegrationService.applyStoreCredit(useStoreCredit); + } + + await this.paymentIntegrationService.submitOrder(order, options); + + if (payment.paymentData && isVaultedInstrument(payment.paymentData)) { + await this.executeWithVaulted(payment); + + return; + } + + return this.executeWithCC(payment); + } + + finalize(): Promise { + return Promise.reject(new OrderFinalizationNotRequiredError()); + } + + deinitialize(): Promise { + if (this.hostedForm) { + this.hostedForm.detach(); + } + + if (this.windowEventListener) { + window.removeEventListener('message', this.windowEventListener); + this.windowEventListener = undefined; + } + + if (this.iframe && this.iframe.parentNode) { + this.iframe.parentNode.removeChild(this.iframe); + this.iframe = undefined; + } + + return Promise.resolve(); + } + + private async executeWithCC(payment: OrderPaymentRequestBody): Promise { + const state = this.paymentIntegrationService.getState(); + const paymentMethod = state.getPaymentMethodOrThrow(payment.methodId); + + const testMode = paymentMethod.config.testMode; + const paymentData = payment.paymentData || {}; + const instrumentSettings = isHostedInstrumentLike(paymentData) + ? paymentData + : { shouldSaveInstrument: false, shouldSetAsDefaultInstrument: false }; + + const { shouldSaveInstrument, shouldSetAsDefaultInstrument } = instrumentSettings; + + const nonce = await new Promise((resolve, reject) => { + if (!this.iframe) { + throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); + } + + const frameref: Window | null = this.iframe.contentWindow; + + if (frameref === null) { + throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); + } + + frameref.postMessage('tokenize', this.monerisURL(!!testMode)); + + this.windowEventListener = (response: MessageEvent) => { + if (typeof response.data !== 'string') { + return; + } + + try { + resolve(this.handleMonerisResponse(response)); + } catch (error) { + reject(error); + } + }; + + window.addEventListener('message', this.windowEventListener); + }); + + if (nonce !== undefined) { + await this.paymentIntegrationService.submitPayment({ + methodId: payment.methodId, + paymentData: { nonce, shouldSaveInstrument, shouldSetAsDefaultInstrument }, + }); + } + } + + private async executeWithVaulted( + payment: OrderPaymentRequestBody, + ): Promise { + if (this.hostedForm) { + const form = this.hostedForm; + + await form.validate(); + await form.submit(payment); + + return this.paymentIntegrationService.loadCurrentOrder(); + } + + return this.paymentIntegrationService.submitPayment(payment); + } + + private shouldShowTSVHostedForm(methodId: string): boolean { + return this.isHostedPaymentFormEnabled(methodId) && this.isHostedFieldAvailable(); + } + + private isHostedPaymentFormEnabled(methodId: string): boolean { + const paymentMethod = this.paymentIntegrationService + .getState() + .getPaymentMethodOrThrow(methodId); + + return Boolean(paymentMethod.config.isHostedFormEnabled); + } + + private isHostedFieldAvailable(): boolean { + const options = this.getInitializeOptions(); + const definedFields = omitBy(options.form?.fields, isEmpty); + + return !isEmpty(definedFields); + } + + private getInitializeOptions(): MonerisPaymentInitializeOptions { + if (!this.initializeOptions) { + throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); + } + + return this.initializeOptions; + } + + private async mountCardVerificationfields(formOptions: HostedFormOptions): Promise { + const bigpayBaseUrl = this.paymentIntegrationService.getState().getStoreConfig() + ?.paymentSettings.bigpayBaseUrl; + + if (!bigpayBaseUrl) { + throw new MissingDataError(MissingDataErrorType.MissingCheckoutConfig); + } + + const form = this.paymentIntegrationService.createHostedForm(bigpayBaseUrl, formOptions); + + await form.attach(); + + return form; + } + + private createIframe( + containerId: string, + initializationData: MonerisInitializationData, + testMode: boolean, + style?: MonerisStylingProps, + ): HTMLIFrameElement { + const container = document.getElementById(containerId); + + if (!container) { + throw new InvalidArgumentError('Unable to create iframe without valid container ID.'); + } + + const iframe: HTMLIFrameElement = document.createElement('iframe'); + const monerisQueryParams: MoneriesHostedFieldsQueryParams = { + id: initializationData.profileId, + pmmsg: true, + display_labels: 1, + enable_exp: 1, + enable_cvd: 1, + css_body: + style?.cssBody || + 'font-family: Arial, Helvetica,sans-serif;background: transparent;', + css_textbox: + style?.cssTextbox || + 'border-radius:4px;border: 2px solid rgb(00,00,00);width: 100%;font-weight: 600;padding: 8px 8px;outline: 0;', + css_textbox_pan: style?.cssTextboxCardNumber || 'width: 240px;', + css_textbox_exp: + style?.cssTextboxExpiryDate || 'margin-bottom: 0;width: calc(30% - 12px);', + css_textbox_cvd: style?.cssTextboxCVV || 'margin-bottom: 0;width: calc(30% - 12px);', + css_input_label: + style?.cssInputLabel || + 'font-size: 10px;position: relative;top: 8px;left: 6px;background: rgb(255,255,255);padding: 3px 2px;color: rgb(66,66,66);font-weight: 600;z-index: 2;', + pan_label: initializationData.creditCardLabel || 'Credit Card Number', + exp_label: initializationData.expiryDateLabel || 'Expiration', + cvd_label: initializationData.cvdLabel || 'CVD', + }; + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const queryString = map(monerisQueryParams, (value, key) => `${key}=${value}`).join('&'); + + iframe.width = '100%'; + iframe.height = '100%'; + iframe.name = IFRAME_NAME; + iframe.id = IFRAME_NAME; + iframe.style.border = 'none'; + iframe.src = `${this.monerisURL(testMode)}?${queryString}`; + + container.appendChild(iframe); + + return iframe; + } + + private handleMonerisResponse(response: MessageEvent): string { + const monerisResponse: MonerisResponseData = JSON.parse(response.data); + + if (monerisResponse.responseCode[0] !== RESPONSE_SUCCESS_CODE) { + throw new Error(monerisResponse.errorMessage); + } + + return monerisResponse.dataKey; + } + + private monerisURL(testMode: boolean): string { + return `https://${testMode ? 'esqa' : 'www3'}.moneris.com/HPPtoken/index.php`; + } +} diff --git a/packages/core/src/payment/strategies/moneris/moneris.mock.ts b/packages/moneris-integration/src/moneris.mock.ts similarity index 85% rename from packages/core/src/payment/strategies/moneris/moneris.mock.ts rename to packages/moneris-integration/src/moneris.mock.ts index baa65af95a..91b0ad0a07 100644 --- a/packages/core/src/payment/strategies/moneris/moneris.mock.ts +++ b/packages/moneris-integration/src/moneris.mock.ts @@ -1,6 +1,8 @@ -import { HostedFieldType } from '../../../hosted-form'; -import { OrderRequestBody } from '../../../order'; -import { PaymentInitializeOptions } from '../../payment-request-options'; +import { + HostedFieldType, + OrderRequestBody, + PaymentInitializeOptions, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; export function getHostedFormInitializeOptions(): PaymentInitializeOptions { return { diff --git a/packages/core/src/payment/strategies/moneris/moneris.ts b/packages/moneris-integration/src/moneris.ts similarity index 96% rename from packages/core/src/payment/strategies/moneris/moneris.ts rename to packages/moneris-integration/src/moneris.ts index eb17633c70..c9065a75bf 100644 --- a/packages/core/src/payment/strategies/moneris/moneris.ts +++ b/packages/moneris-integration/src/moneris.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ /** * A set of stringified CSS to apply to Moneris' IFrame fields. * CSS attributes should be converted to string. diff --git a/packages/moneris-integration/tsconfig.json b/packages/moneris-integration/tsconfig.json new file mode 100644 index 0000000000..8ff1666639 --- /dev/null +++ b/packages/moneris-integration/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.base.json", +} \ No newline at end of file diff --git a/packages/moneris-integration/tsconfig.lib.json b/packages/moneris-integration/tsconfig.lib.json new file mode 100644 index 0000000000..ba5447ffb1 --- /dev/null +++ b/packages/moneris-integration/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/moneris-integration/tsconfig.spec.json b/packages/moneris-integration/tsconfig.spec.json new file mode 100644 index 0000000000..2c1fb69d9f --- /dev/null +++ b/packages/moneris-integration/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 92d643e142..fb2a7ee11b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -85,6 +85,9 @@ "@bigcommerce/checkout-sdk/mollie-integration": [ "packages/mollie-integration/src/index.ts" ], + "@bigcommerce/checkout-sdk/moneris-integration": [ + "packages/moneris-integration/src/index.ts" + ], "@bigcommerce/checkout-sdk/no-payment-integration": [ "packages/no-payment-integration/src/index.ts" ], diff --git a/workspace.json b/workspace.json index f9cffd7dd2..d6b9883198 100644 --- a/workspace.json +++ b/workspace.json @@ -27,6 +27,7 @@ "klarna-integration": "packages/klarna-integration", "legacy-integration": "packages/legacy-integration", "mollie-integration": "packages/mollie-integration", + "moneris-integration": "packages/moneris-integration", "no-payment-integration": "packages/no-payment-integration", "offline-integration": "packages/offline-integration", "offsite-integration": "packages/offsite-integration",