diff --git a/.eslintrc b/.eslintrc index c559b35178..6c7eed1ad7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,5 @@ { - "extends": ["airbnb", "prettier"], + "extends": ["airbnb", "prettier", "plugin:import/typescript"], "parser": "@babel/eslint-parser", "env": { "browser": true, @@ -39,7 +39,8 @@ "server": "./server", "utils": "./utils" } - } + }, + "typescript": {} } } } diff --git a/package.json b/package.json index 61e08cd13c..ecfb5ce829 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@paypal/sdk-client": "^4.0.166", "@paypal/sdk-constants": "^1.0.118", "@paypal/sdk-logos": "^2.0.0", + "@paypalcorp/web-sdk-postmessenger": "^0.2.6", "core-js-pure": "3.31.1" }, "devDependencies": { @@ -81,6 +82,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-babel-module": "^5.3.1", "eslint-import-resolver-jest": "^3.0.2", + "eslint-import-resolver-typescript": "^3.7.0", "eslint-plugin-import": "2.25.4", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.2.1", diff --git a/src/components/modal/v2/lib/zoid-polyfill.js b/src/components/modal/v2/lib/zoid-polyfill.js index 5e6f0e69b4..959128c045 100644 --- a/src/components/modal/v2/lib/zoid-polyfill.js +++ b/src/components/modal/v2/lib/zoid-polyfill.js @@ -1,15 +1,44 @@ /* global Android */ import { isAndroidWebview, isIosWebview, getPerformance } from '@krakenjs/belter/src'; +import PostMessenger from '@paypalcorp/web-sdk-postmessenger/src'; import { getOrCreateDeviceID, logger } from '../../../../utils'; import { isIframe } from './utils'; const IOS_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; const ANDROID_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; +const channelConfigObj = { + // where to send or recieve the messages + target: window, // TODO: determine window or window.top? + // restricted list of destinations to send or recieve from + allowedOrigins: [], // string[] + // dump debug information + debug: console.log.bind(console), // just a simple place to log info, + // time to wait for message ack + timeout: 2000, + // number of times to retry failed messages + retries: 3 +}; + const setupBrowser = props => { + // setup communication layer with v6 modal wrapper + const channel = new PostMessenger(channelConfigObj); + + const propListeners = new Set(); + + channel.subscribe('PROPS_UPDATE', newProps => { + if (newProps && typeof newProps === 'object') { + Array.from(propListeners.values()).forEach(listener => { + listener({ ...window.xprops, ...newProps }); + }); + + Object.assign(window.xprops, newProps); + } + console.log('Reciever got PROPS_UPDATE data =', newProps); + }); + window.xprops = { - // We will never recieve new props via this integration style - onProps: () => {}, + onProps: listener => propListeners.add(listener), // TODO: Verify these callbacks are instrumented correctly onReady: ({ products, meta }) => { const { clientId, payerId, merchantId, offer, partnerAttributionId } = props; diff --git a/tests/unit/spec/src/components/modal/v2/lib/hooks/currency.test.js b/tests/unit/spec/src/components/modal/v2/lib/hooks/currency.test.js index 5830d2a597..864f6f005c 100644 --- a/tests/unit/spec/src/components/modal/v2/lib/hooks/currency.test.js +++ b/tests/unit/spec/src/components/modal/v2/lib/hooks/currency.test.js @@ -1,4 +1,4 @@ -import { currencyFormat } from 'src/components/modal/v2/lib/'; +import { currencyFormat } from 'src/components/modal/v2/lib/hooks/currency'; describe('currency format', () => { test('array test', () => { diff --git a/tests/unit/spec/src/components/modal/v2/lib/zoid-polyfill.test.js b/tests/unit/spec/src/components/modal/v2/lib/zoid-polyfill.test.js index f70b6c43f6..490bc16022 100644 --- a/tests/unit/spec/src/components/modal/v2/lib/zoid-polyfill.test.js +++ b/tests/unit/spec/src/components/modal/v2/lib/zoid-polyfill.test.js @@ -1,3 +1,4 @@ +import { subscribeMockFn } from '@paypalcorp/web-sdk-postmessenger/src'; import zoidPolyfill from 'src/components/modal/v2/lib/zoid-polyfill'; import { logger } from 'src/utils'; @@ -25,6 +26,16 @@ jest.mock('@krakenjs/belter/src', () => { }) }; }); +jest.mock('@paypalcorp/web-sdk-postmessenger/src', () => { + const subscribeMock = jest.fn(); + return { + __esModule: true, + subscribeMockFn: subscribeMock, + default: jest.fn().mockImplementation(() => ({ + subscribe: subscribeMock + })) + }; +}); jest.mock('src/components/modal/v2/lib/utils', () => ({ isIframe: true })); @@ -73,7 +84,7 @@ describe('zoidPollyfill', () => { describe('sets up xprops for browser', () => { beforeAll(() => { mockLoadUrl( - 'https://localhost.paypal.com:8080/credit-presentment/native/message?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true' + 'https://localhost.paypal.com:8080/credit-presentment/lander/modal?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true' ); zoidPolyfill(); @@ -177,7 +188,7 @@ describe('zoidPollyfill', () => { test('sets up xprops for webview', () => { mockLoadUrl( - 'https://localhost.paypal.com:8080/credit-presentment/native/message?client_id=client_1&logo_type=inline&amount=500&dev_touchpoint=true', + 'https://localhost.paypal.com:8080/credit-presentment/native/modal?client_id=client_1&logo_type=inline&amount=500&dev_touchpoint=true', { platform: 'ios' } @@ -316,94 +327,167 @@ describe('zoidPollyfill', () => { postMessage.mockClear(); }); - test('notifies when props update', () => { - mockLoadUrl( - 'https://localhost.paypal.com:8080/credit-presentment/native/message?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true', - { - platform: 'android' - } - ); - const postMessage = global.Android.paypalMessageModalCallbackHandler; + describe('notifies when props update', () => { + afterEach(() => { + subscribeMockFn.mockClear(); + }); + test('webview', () => { + mockLoadUrl( + 'https://localhost.paypal.com:8080/credit-presentment/native/modal?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true', + { + platform: 'android' + } + ); + const postMessage = global.Android.paypalMessageModalCallbackHandler; - zoidPolyfill(); + zoidPolyfill(); - expect(window.actions).toEqual( - expect.objectContaining({ - updateProps: expect.any(Function) - }) - ); - expect(window.xprops).toEqual( - expect.objectContaining({ - onProps: expect.any(Function) - }) - ); + expect(window.actions).toEqual( + expect.objectContaining({ + updateProps: expect.any(Function) + }) + ); + expect(window.xprops).toEqual( + expect.objectContaining({ + onProps: expect.any(Function) + }) + ); - const onPropsCallback = jest.fn(); + const onPropsCallback = jest.fn(); - window.xprops.onProps(onPropsCallback); - window.actions.updateProps({ amount: 1000 }); + window.xprops.onProps(onPropsCallback); + window.actions.updateProps({ amount: 1000 }); - expect(onPropsCallback).toHaveBeenCalledTimes(1); - expect(onPropsCallback).toHaveBeenCalledWith( - expect.objectContaining({ - clientId: 'client_1', - logoType: 'inline', - amount: 1000 - }) - ); + expect(onPropsCallback).toHaveBeenCalledTimes(1); + expect(onPropsCallback).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: 'client_1', + logoType: 'inline', + amount: 1000 + }) + ); - window.actions.updateProps({ offer: 'TEST' }); + window.actions.updateProps({ offer: 'TEST' }); - expect(onPropsCallback).toHaveBeenCalledTimes(2); - expect(onPropsCallback).toHaveBeenCalledWith( - expect.objectContaining({ - clientId: 'client_1', - logoType: 'inline', - amount: 1000, - offer: 'TEST' - }) - ); + expect(onPropsCallback).toHaveBeenCalledTimes(2); + expect(onPropsCallback).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: 'client_1', + logoType: 'inline', + amount: 1000, + offer: 'TEST' + }) + ); - window.xprops.onReady({ - products: ['PRODUCT_1', 'PRODUCT_2'], - meta: { - trackingDetails: { - fdata: '123abc', - credit_product_identifiers: ['PAY_LATER_LONG_TERM_US'], - offer_country_code: 'US', - extra_field: 'should not be present' + window.xprops.onReady({ + products: ['PRODUCT_1', 'PRODUCT_2'], + meta: { + trackingDetails: { + fdata: '123abc', + credit_product_identifiers: ['PAY_LATER_LONG_TERM_US'], + offer_country_code: 'US', + extra_field: 'should not be present' + } } - } - }); + }); - expect(postMessage).toHaveBeenCalledTimes(1); - expect(postMessage.mock.calls[0][0]).toEqual(expect.any(String)); - expect(JSON.parse(postMessage.mock.calls[0][0])).toMatchInlineSnapshot(` - Object { - "args": Array [ + expect(postMessage).toHaveBeenCalledTimes(1); + expect(postMessage.mock.calls[0][0]).toEqual(expect.any(String)); + expect(JSON.parse(postMessage.mock.calls[0][0])).toMatchInlineSnapshot(` Object { - "__shared__": Object { - "credit_product_identifiers": Array [ - "PAY_LATER_LONG_TERM_US", - ], - "fdata": "123abc", - "offer_country_code": "US", - }, - "event_type": "modal_rendered", - "render_duration": "50", - "request_duration": "100", + "args": Array [ + Object { + "__shared__": Object { + "credit_product_identifiers": Array [ + "PAY_LATER_LONG_TERM_US", + ], + "fdata": "123abc", + "offer_country_code": "US", + }, + "event_type": "modal_rendered", + "render_duration": "50", + "request_duration": "100", + }, + ], + "name": "onReady", + } + `); + postMessage.mockClear(); + }); + test('browser', () => { + mockLoadUrl( + 'https://localhost.paypal.com:8080/credit-presentment/lander/modal?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true' + ); + + zoidPolyfill(); + + expect(window.xprops).toEqual( + expect.objectContaining({ + onProps: expect.any(Function) + }) + ); + + const onPropsCallback = jest.fn(); + + window.xprops.onProps(onPropsCallback); + + expect(subscribeMockFn).toHaveBeenCalledTimes(1); + expect(subscribeMockFn).toHaveBeenCalledWith('PROPS_UPDATE', expect.any(Function)); + + subscribeMockFn( + { + amount: 1000 }, - ], - "name": "onReady", - } - `); - postMessage.mockClear(); + window.origin + ); + + const subscribeCallback = subscribeMockFn.mock.calls[0][1]; + + subscribeCallback({ + amount: 1000 + }); + + expect(onPropsCallback).toHaveBeenCalledTimes(1); + expect(onPropsCallback).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: 'client_1', + logoType: 'inline', + amount: 1000 + }) + ); + + subscribeCallback({ + offerTypes: ['TEST'] + }); + + expect(onPropsCallback).toHaveBeenCalledTimes(2); + expect(onPropsCallback).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: 'client_1', + logoType: 'inline', + amount: 1000, + offerTypes: ['TEST'] + }) + ); + + window.xprops.onReady({ + products: ['PRODUCT_1', 'PRODUCT_2'], + meta: { + trackingDetails: { + fdata: '123abc', + credit_product_identifiers: ['PAY_LATER_LONG_TERM_US'], + offer_country_code: 'US', + extra_field: 'should not be present' + } + } + }); + }); }); describe('communication with parent window on onClose ', () => { beforeAll(() => { mockLoadUrl( - 'https://localhost.paypal.com:8080/credit-presentment/native/message?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true' + 'https://localhost.paypal.com:8080/credit-presentment/native/modal?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true' ); zoidPolyfill(); const postMessage = jest.fn();