From ecf1a927acdad188683c18c1eb4e11d4c4f221f3 Mon Sep 17 00:00:00 2001 From: Dan Haas Date: Tue, 10 Dec 2024 13:11:13 -0500 Subject: [PATCH 1/7] adds browser new prop event listener --- src/components/modal/v2/lib/zoid-polyfill.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/modal/v2/lib/zoid-polyfill.js b/src/components/modal/v2/lib/zoid-polyfill.js index 5e6f0e69b4..64c79bf38b 100644 --- a/src/components/modal/v2/lib/zoid-polyfill.js +++ b/src/components/modal/v2/lib/zoid-polyfill.js @@ -7,9 +7,23 @@ const IOS_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; const ANDROID_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; const setupBrowser = props => { + const propListeners = new Set(); + + window.addEventListener( + 'message', + newProps => { + if (newProps && typeof newProps === 'object') { + Array.from(propListeners.values()).forEach(listener => { + listener({ ...window.xprops, ...newProps }); + }); + Object.assign(window.xprops, newProps); + } + }, + false + ); + 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; From 71f28c8e32f11dac872d370791f4fe079be674eb Mon Sep 17 00:00:00 2001 From: Dan Haas Date: Tue, 7 Jan 2025 17:17:58 -0500 Subject: [PATCH 2/7] adds prop update listening to modal browser zoid polyfill --- src/components/modal/v2/lib/utils.js | 30 +++ src/components/modal/v2/lib/zoid-polyfill.js | 94 ++++++- .../modal/v2/lib/zoid-polyfill.test.js | 238 +++++++++++------- 3 files changed, 269 insertions(+), 93 deletions(-) diff --git a/src/components/modal/v2/lib/utils.js b/src/components/modal/v2/lib/utils.js index 0d42b61cbf..d8d4d18b46 100644 --- a/src/components/modal/v2/lib/utils.js +++ b/src/components/modal/v2/lib/utils.js @@ -2,6 +2,7 @@ import objectEntries from 'core-js-pure/stable/object/entries'; import arrayFrom from 'core-js-pure/stable/array/from'; import { isIosWebview, isAndroidWebview } from '@krakenjs/belter/src'; import { request, memoize, ppDebug } from '../../../../utils'; +import validate from '../../../../library/zoid/message/validation'; export const getContent = memoize( ({ @@ -112,3 +113,32 @@ export function formatDateByCountry(country) { } return currentDate.toLocaleDateString('en-GB', options); } + +export function createUUID() { + // crypto.randomUUID() is only available in HTTPS secure environments and modern browsers + if (typeof crypto !== 'undefined' && crypto && crypto.randomUUID instanceof Function) { + return crypto.randomUUID(); + } + + const validChars = '0123456789abcdefghijklmnopqrstuvwxyz'; + const stringLength = 32; + let randomId = ''; + for (let index = 0; index < stringLength; index++) { + const randomIndex = Math.floor(Math.random() * validChars.length); + randomId += validChars.charAt(randomIndex); + } + return randomId; +} + +export function validateUpdatedProps(updatedProps) { + const validatedProps = {}; + Object.entries(updatedProps).forEach(entry => { + const [k, v] = entry; + if (k === 'offerType') { + validatedProps.offer = validate.offer({ props: { offer: v } }); + } else { + validatedProps[k] = validate[k]({ props: { [k]: v } }); + } + }); + return validatedProps; +} diff --git a/src/components/modal/v2/lib/zoid-polyfill.js b/src/components/modal/v2/lib/zoid-polyfill.js index 64c79bf38b..3cfff945a7 100644 --- a/src/components/modal/v2/lib/zoid-polyfill.js +++ b/src/components/modal/v2/lib/zoid-polyfill.js @@ -1,24 +1,100 @@ /* global Android */ import { isAndroidWebview, isIosWebview, getPerformance } from '@krakenjs/belter/src'; import { getOrCreateDeviceID, logger } from '../../../../utils'; -import { isIframe } from './utils'; +import { isIframe, createUUID, validateUpdatedProps } from './utils'; const IOS_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; const ANDROID_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; +export function updateBrowserProps(initialProps, propListeners, newPropsEvent) { + const { origin, eventName, id, eventPayload: updatedProps } = newPropsEvent.data; + if ( + // verify the event is coming from the merchant page and is the correct event + origin === initialProps.origin && + eventName === 'PROPS_UPDATE' && + updatedProps && + typeof updatedProps === 'object' + ) { + // send ack so PostMessenger will stop trying to resend message + postMessage( + { + eventName: id, + eventPayload: { ok: true }, + id: createUUID() + }, + // TODO: resolve Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('https://www.msmaster.qa.paypal.com') does not match the recipient window's origin ('https://localhost:8443'). + initialProps.origin + ); + + const validatedProps = validateUpdatedProps(updatedProps); + Array.from(propListeners.values()).forEach(listener => { + listener({ ...window.xprops, ...validatedProps }); + }); + + console.debug( + 'LearnMore message ack', + { + eventName: id, + eventPayload: { ok: true } + }, + initialProps.origin + ); + } +} + const setupBrowser = props => { const propListeners = new Set(); window.addEventListener( 'message', - newProps => { - if (newProps && typeof newProps === 'object') { - Array.from(propListeners.values()).forEach(listener => { - listener({ ...window.xprops, ...newProps }); - }); - Object.assign(window.xprops, newProps); - } - }, + event => updateBrowserProps(props, propListeners, event), + // event => { + // console.log('LearnMore message event.data', event.data); + // const { eventName, eventPayload: updatedProps, id } = event.data; + // if ( + // // verify the event is coming from the merchant page + // event.data.origin === props.origin && + // eventName === 'PROPS_UPDATE' && + // updatedProps && + // typeof updatedProps === 'object' + // ) { + // Array.from(propListeners.values()).forEach(listener => { + // listener({ ...window.xprops, ...updatedProps }); + // }); + + // let validatedProps; + // Object.entries(updatedProps).forEach(entry => { + // const [k, v] = entry; + // if (k === 'offerType') { + // validatedProps.offer = validate.offer({ props: { offer: v } }); + // } else { + // validatedProps[k] = validate[k]({ props: { [k]: v } }); + // } + // }); + + // Object.assign(window.xprops, validatedProps); + + // console.log( + // 'LearnMore message ack', + // { + // eventName: id, + // eventPayload: { ok: true }, + // id: createUUID() + // }, + // props.origin + // ); + + // // send back ack so PostMessenger won't retry sending message unnecessarily + // postMessage( + // { + // eventName: id, + // eventPayload: { ok: true }, + // id: createUUID() + // }, + // props.origin + // ); + // } + // }, false ); 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..407adea316 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,13 +1,19 @@ -import zoidPolyfill from 'src/components/modal/v2/lib/zoid-polyfill'; +import zoidPolyfill, { updateBrowserProps } from 'src/components/modal/v2/lib/zoid-polyfill'; import { logger } from 'src/utils'; // Mock all of utils because the `stats` util that would be included has a side-effect call to logger.track -jest.mock('src/utils', () => ({ - logger: { - track: jest.fn(), - addMetaBuilder: jest.fn() - } -})); +jest.mock('src/utils', () => { + const originalModule = jest.requireActual('@krakenjs/belter/src'); + + return { + ...originalModule, + logger: { + track: jest.fn(), + addMetaBuilder: jest.fn(), + warn: jest.fn() + } + }; +}); jest.mock('@krakenjs/belter/src', () => { const originalModule = jest.requireActual('@krakenjs/belter/src'); @@ -25,9 +31,21 @@ jest.mock('@krakenjs/belter/src', () => { }) }; }); -jest.mock('src/components/modal/v2/lib/utils', () => ({ - isIframe: true -})); +jest.mock('src/components/modal/v2/lib/utils', () => { + const originalModule = jest.requireActual('src/components/modal/v2/lib/utils'); + + return { + ...originalModule, + isIframe: true + }; +}); + +const addEventListenerSpy = jest.fn(); +const addEventListener = window.addEventListener.bind(window); +window.addEventListener = (...args) => { + addEventListenerSpy(...args); + addEventListener(...args); +}; const mockLoadUrl = (url, { platform = 'web' } = {}) => { delete window.location; @@ -73,13 +91,14 @@ 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(); }); afterEach(() => { logger.track.mockClear(); + addEventListenerSpy.mockClear(); }); test('window.xprops initalized', () => { expect(window.actions).toBeUndefined(); @@ -177,7 +196,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 +335,145 @@ 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', () => { + 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", - }, - ], - "name": "onReady", - } - `); - postMessage.mockClear(); + "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&origin=http://example.com' + ); + + zoidPolyfill(); + + expect(window.xprops).toEqual( + expect.objectContaining({ + onProps: expect.any(Function) + }) + ); + + // verify event listener was added + expect(addEventListenerSpy).toHaveBeenCalledTimes(1); + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function), false); + + const newPropsEvent = { + data: { + origin: 'http://example.com', + eventName: 'PROPS_UPDATE', + eventPayload: { + amount: 1000, + offerType: ['PAY_LATER_LONG_TERM', 'PAY_LATER_SHORT_TERM'] + } + } + }; + + // jest doesn't support calling postMessage, so we cannot use the event listener above + // instead we will manually verify that updateBrowserProps works as intended + const propListeners = new Set(); + const onPropsCallback = jest.fn(); + propListeners.add(onPropsCallback); + updateBrowserProps(window.xprops, propListeners, newPropsEvent); + + // subscribeCallback({ + // amount: 1000 + // }); + + expect(onPropsCallback).toHaveBeenCalledTimes(1); + expect(onPropsCallback).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: 'client_1', + logoType: 'inline', + amount: 1000, + offer: 'PAY_LATER_LONG_TERM,PAY_LATER_SHORT_TERM' + }) + ); + }); }); 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(); From 701bdd8f40cc0643a80e104077eee791894e6b43 Mon Sep 17 00:00:00 2001 From: Dan Haas Date: Mon, 13 Jan 2025 15:04:26 -0500 Subject: [PATCH 3/7] fixes postMessage and refactors a bit --- src/components/modal/v2/lib/utils.js | 29 +++- src/components/modal/v2/lib/zoid-polyfill.js | 127 ++++++------------ .../modal/v2/lib/zoid-polyfill.test.js | 104 +++++++------- 3 files changed, 128 insertions(+), 132 deletions(-) diff --git a/src/components/modal/v2/lib/utils.js b/src/components/modal/v2/lib/utils.js index d8d4d18b46..5700589d63 100644 --- a/src/components/modal/v2/lib/utils.js +++ b/src/components/modal/v2/lib/utils.js @@ -130,7 +130,7 @@ export function createUUID() { return randomId; } -export function validateUpdatedProps(updatedProps) { +export function validateProps(updatedProps) { const validatedProps = {}; Object.entries(updatedProps).forEach(entry => { const [k, v] = entry; @@ -142,3 +142,30 @@ export function validateUpdatedProps(updatedProps) { }); return validatedProps; } + +export function sendEventAck(eventId) { + // skip this step if running in test env because jest's target windows don't support postMessage + if (window.process?.env?.NODE_ENV === 'test') { + return; + } + + // target window selection depends on if checkout window is in popup or modal iframe + let targetWindow; + const popupCheck = window.parent === window; + if (popupCheck) { + targetWindow = window.opener; + } else { + targetWindow = window.parent; + } + + targetWindow.postMessage( + { + // PostMessenger stops reposting an event when it receives an eventName which matches the id in the message it sent and type 'ack' + eventName: eventId, + type: 'ack', + eventPayload: { ok: true }, + id: createUUID() + }, + '*' + ); +} diff --git a/src/components/modal/v2/lib/zoid-polyfill.js b/src/components/modal/v2/lib/zoid-polyfill.js index 3cfff945a7..c834af998a 100644 --- a/src/components/modal/v2/lib/zoid-polyfill.js +++ b/src/components/modal/v2/lib/zoid-polyfill.js @@ -1,44 +1,30 @@ /* global Android */ import { isAndroidWebview, isIosWebview, getPerformance } from '@krakenjs/belter/src'; import { getOrCreateDeviceID, logger } from '../../../../utils'; -import { isIframe, createUUID, validateUpdatedProps } from './utils'; +import { isIframe, validateProps, sendEventAck } from './utils'; const IOS_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; const ANDROID_INTERFACE_NAME = 'paypalMessageModalCallbackHandler'; -export function updateBrowserProps(initialProps, propListeners, newPropsEvent) { - const { origin, eventName, id, eventPayload: updatedProps } = newPropsEvent.data; - if ( - // verify the event is coming from the merchant page and is the correct event - origin === initialProps.origin && - eventName === 'PROPS_UPDATE' && - updatedProps && - typeof updatedProps === 'object' - ) { - // send ack so PostMessenger will stop trying to resend message - postMessage( - { - eventName: id, - eventPayload: { ok: true }, - id: createUUID() - }, - // TODO: resolve Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('https://www.msmaster.qa.paypal.com') does not match the recipient window's origin ('https://localhost:8443'). - initialProps.origin - ); - - const validatedProps = validateUpdatedProps(updatedProps); - Array.from(propListeners.values()).forEach(listener => { - listener({ ...window.xprops, ...validatedProps }); - }); +function listenAndAssignProps(newProps, propListeners) { + Array.from(propListeners.values()).forEach(listener => { + listener({ ...window.xprops, ...newProps }); + }); + Object.assign(window.xprops, newProps); +} - console.debug( - 'LearnMore message ack', - { - eventName: id, - eventPayload: { ok: true } - }, - initialProps.origin - ); +export function validateAndUpdateBrowserProps(initialProps, propListeners, updatedPropsEvent) { + const { + origin, + data: { eventName, id, eventPayload: newProps } + } = updatedPropsEvent; + // verify the event is coming from the merchant page + const eventOriginCheck = origin === decodeURIComponent(initialProps.origin); + if (eventOriginCheck && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') { + // send event ack so PostMessenger will stop reposting event + sendEventAck(id); + const validProps = validateProps(newProps); + listenAndAssignProps(validProps, propListeners, true); } } @@ -47,57 +33,30 @@ const setupBrowser = props => { window.addEventListener( 'message', - event => updateBrowserProps(props, propListeners, event), - // event => { - // console.log('LearnMore message event.data', event.data); - // const { eventName, eventPayload: updatedProps, id } = event.data; - // if ( - // // verify the event is coming from the merchant page - // event.data.origin === props.origin && - // eventName === 'PROPS_UPDATE' && - // updatedProps && - // typeof updatedProps === 'object' - // ) { - // Array.from(propListeners.values()).forEach(listener => { - // listener({ ...window.xprops, ...updatedProps }); - // }); - - // let validatedProps; - // Object.entries(updatedProps).forEach(entry => { - // const [k, v] = entry; - // if (k === 'offerType') { - // validatedProps.offer = validate.offer({ props: { offer: v } }); - // } else { - // validatedProps[k] = validate[k]({ props: { [k]: v } }); - // } - // }); - - // Object.assign(window.xprops, validatedProps); - - // console.log( - // 'LearnMore message ack', - // { - // eventName: id, - // eventPayload: { ok: true }, - // id: createUUID() - // }, - // props.origin - // ); - - // // send back ack so PostMessenger won't retry sending message unnecessarily - // postMessage( - // { - // eventName: id, - // eventPayload: { ok: true }, - // id: createUUID() - // }, - // props.origin - // ); - // } - // }, + event => { + validateAndUpdateBrowserProps(props, propListeners, event); + }, false ); + // window.addEventListener( + // 'message', + // event => { + // const { + // origin, + // data: { eventName, id, eventPayload: newProps } + // } = event; + // // verify the event is coming from the merchant page + // const eventOriginCheck = origin === decodeURIComponent(props.origin); + // if (eventOriginCheck && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') { + // // send event ack so PostMessenger will stop reposting event + // sendEventAck(id); + // listenAndAssignProps(newProps, propListeners, true); + // } + // }, + // false + // ); + window.xprops = { onProps: listener => propListeners.add(listener), // TODO: Verify these callbacks are instrumented correctly @@ -216,11 +175,7 @@ const setupWebview = props => { window.actions = { updateProps: newProps => { if (newProps && typeof newProps === 'object') { - Array.from(propListeners.values()).forEach(listener => { - listener({ ...window.xprops, ...newProps }); - }); - - Object.assign(window.xprops, newProps); + listenAndAssignProps(newProps, propListeners, false); } } }; 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 407adea316..647f01075d 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,4 +1,4 @@ -import zoidPolyfill, { updateBrowserProps } from 'src/components/modal/v2/lib/zoid-polyfill'; +import zoidPolyfill, { validateAndUpdateBrowserProps } from 'src/components/modal/v2/lib/zoid-polyfill'; import { logger } from 'src/utils'; // Mock all of utils because the `stats` util that would be included has a side-effect call to logger.track @@ -419,54 +419,68 @@ describe('zoidPollyfill', () => { `); 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&origin=http://example.com' - ); - - zoidPolyfill(); - - expect(window.xprops).toEqual( - expect.objectContaining({ - onProps: expect.any(Function) - }) - ); - - // verify event listener was added - expect(addEventListenerSpy).toHaveBeenCalledTimes(1); - expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function), false); - - const newPropsEvent = { - data: { + describe('browser', () => { + beforeAll(() => { + mockLoadUrl( + 'https://localhost.paypal.com:8080/credit-presentment/lander/modal?client_id=client_1&logo_type=inline&amount=500&devTouchpoint=true&origin=http://example.com' + ); + zoidPolyfill(); + }); + afterEach(() => { + logger.track.mockClear(); + addEventListenerSpy.mockClear(); + }); + test('event listener is added', () => { + expect(window.xprops).toEqual( + expect.objectContaining({ + onProps: expect.any(Function) + }) + ); + + expect(addEventListenerSpy).toHaveBeenCalledTimes(1); + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function), false); + }); + test('validateAndUpdateBrowserProps updates props when values are valid', () => { + // jest doesn't support calling postMessage, so we cannot use the event listener above + // instead we will manually verify that validateAndUpdateBrowserProps works as intended + const newPropsEvent = { origin: 'http://example.com', - eventName: 'PROPS_UPDATE', - eventPayload: { - amount: 1000, - offerType: ['PAY_LATER_LONG_TERM', 'PAY_LATER_SHORT_TERM'] + data: { + eventName: 'PROPS_UPDATE', + eventPayload: { + amount: 1000, + offerType: ['PAY_LATER_LONG_TERM', 'PAY_LATER_SHORT_TERM'] + } } - } - }; - - // jest doesn't support calling postMessage, so we cannot use the event listener above - // instead we will manually verify that updateBrowserProps works as intended - const propListeners = new Set(); - const onPropsCallback = jest.fn(); - propListeners.add(onPropsCallback); - updateBrowserProps(window.xprops, propListeners, newPropsEvent); + }; + + const propListeners = new Set(); + const onPropsCallback = jest.fn(); + propListeners.add(onPropsCallback); + validateAndUpdateBrowserProps(window.xprops, propListeners, newPropsEvent); + + expect(onPropsCallback).toHaveBeenCalledTimes(1); + expect(onPropsCallback).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: 'client_1', + logoType: 'inline', + amount: 1000, + offer: 'PAY_LATER_LONG_TERM,PAY_LATER_SHORT_TERM' + }) + ); + }); + test('validateAndUpdateBrowserProps handles unrelated events with no data', () => { + const unrelatedEvent = { + data: {} + }; - // subscribeCallback({ - // amount: 1000 - // }); + const propListeners = new Set(); + const onPropsCallback = jest.fn(); + propListeners.add(onPropsCallback); + validateAndUpdateBrowserProps(window.xprops, propListeners, unrelatedEvent); - expect(onPropsCallback).toHaveBeenCalledTimes(1); - expect(onPropsCallback).toHaveBeenCalledWith( - expect.objectContaining({ - clientId: 'client_1', - logoType: 'inline', - amount: 1000, - offer: 'PAY_LATER_LONG_TERM,PAY_LATER_SHORT_TERM' - }) - ); + expect(onPropsCallback).toHaveBeenCalledTimes(0); + }); }); }); From a0c0b0b2d2264cf95a3224a760fdd8d09a5c0124 Mon Sep 17 00:00:00 2001 From: Dan Haas Date: Mon, 13 Jan 2025 15:10:42 -0500 Subject: [PATCH 4/7] removes comments --- src/components/modal/v2/lib/zoid-polyfill.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/components/modal/v2/lib/zoid-polyfill.js b/src/components/modal/v2/lib/zoid-polyfill.js index c834af998a..19bcb0a5bc 100644 --- a/src/components/modal/v2/lib/zoid-polyfill.js +++ b/src/components/modal/v2/lib/zoid-polyfill.js @@ -39,24 +39,6 @@ const setupBrowser = props => { false ); - // window.addEventListener( - // 'message', - // event => { - // const { - // origin, - // data: { eventName, id, eventPayload: newProps } - // } = event; - // // verify the event is coming from the merchant page - // const eventOriginCheck = origin === decodeURIComponent(props.origin); - // if (eventOriginCheck && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') { - // // send event ack so PostMessenger will stop reposting event - // sendEventAck(id); - // listenAndAssignProps(newProps, propListeners, true); - // } - // }, - // false - // ); - window.xprops = { onProps: listener => propListeners.add(listener), // TODO: Verify these callbacks are instrumented correctly From 3f5908f48558d24abb4ee8e6e16b6126c5cd7b92 Mon Sep 17 00:00:00 2001 From: Dan Haas Date: Mon, 13 Jan 2025 15:34:15 -0500 Subject: [PATCH 5/7] adds trusted origin to event ack postMessage --- src/components/modal/v2/lib/utils.js | 4 ++-- src/components/modal/v2/lib/zoid-polyfill.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/modal/v2/lib/utils.js b/src/components/modal/v2/lib/utils.js index 5700589d63..b1fefc37a1 100644 --- a/src/components/modal/v2/lib/utils.js +++ b/src/components/modal/v2/lib/utils.js @@ -143,7 +143,7 @@ export function validateProps(updatedProps) { return validatedProps; } -export function sendEventAck(eventId) { +export function sendEventAck(eventId, trustedOrigin) { // skip this step if running in test env because jest's target windows don't support postMessage if (window.process?.env?.NODE_ENV === 'test') { return; @@ -166,6 +166,6 @@ export function sendEventAck(eventId) { eventPayload: { ok: true }, id: createUUID() }, - '*' + trustedOrigin ); } diff --git a/src/components/modal/v2/lib/zoid-polyfill.js b/src/components/modal/v2/lib/zoid-polyfill.js index 19bcb0a5bc..7a00748948 100644 --- a/src/components/modal/v2/lib/zoid-polyfill.js +++ b/src/components/modal/v2/lib/zoid-polyfill.js @@ -15,16 +15,16 @@ function listenAndAssignProps(newProps, propListeners) { export function validateAndUpdateBrowserProps(initialProps, propListeners, updatedPropsEvent) { const { - origin, + origin: eventOrigin, data: { eventName, id, eventPayload: newProps } } = updatedPropsEvent; - // verify the event is coming from the merchant page - const eventOriginCheck = origin === decodeURIComponent(initialProps.origin); - if (eventOriginCheck && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') { + const merchantOrigin = decodeURIComponent(initialProps.origin); + + if (eventOrigin === merchantOrigin && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') { // send event ack so PostMessenger will stop reposting event - sendEventAck(id); + sendEventAck(id, merchantOrigin); const validProps = validateProps(newProps); - listenAndAssignProps(validProps, propListeners, true); + listenAndAssignProps(validProps, propListeners); } } @@ -157,7 +157,7 @@ const setupWebview = props => { window.actions = { updateProps: newProps => { if (newProps && typeof newProps === 'object') { - listenAndAssignProps(newProps, propListeners, false); + listenAndAssignProps(newProps, propListeners); } } }; From 7da92bbd9d58e3148997fde78297e2b2b5fea49d Mon Sep 17 00:00:00 2001 From: Dan Haas Date: Wed, 15 Jan 2025 16:42:27 -0500 Subject: [PATCH 6/7] corrects offerTypes param name --- src/components/modal/v2/lib/utils.js | 2 +- .../unit/spec/src/components/modal/v2/lib/zoid-polyfill.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/modal/v2/lib/utils.js b/src/components/modal/v2/lib/utils.js index b1fefc37a1..3088ea0c05 100644 --- a/src/components/modal/v2/lib/utils.js +++ b/src/components/modal/v2/lib/utils.js @@ -134,7 +134,7 @@ export function validateProps(updatedProps) { const validatedProps = {}; Object.entries(updatedProps).forEach(entry => { const [k, v] = entry; - if (k === 'offerType') { + if (k === 'offerTypes') { validatedProps.offer = validate.offer({ props: { offer: v } }); } else { validatedProps[k] = validate[k]({ props: { [k]: v } }); 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 647f01075d..dc21f03ec8 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 @@ -449,7 +449,7 @@ describe('zoidPollyfill', () => { eventName: 'PROPS_UPDATE', eventPayload: { amount: 1000, - offerType: ['PAY_LATER_LONG_TERM', 'PAY_LATER_SHORT_TERM'] + offerTypes: ['PAY_LATER_LONG_TERM', 'PAY_LATER_SHORT_TERM'] } } }; From 8f938f5648e6159bd6ecd930dcabdf8c06e7826e Mon Sep 17 00:00:00 2001 From: Dan Haas Date: Wed, 22 Jan 2025 11:19:11 -0500 Subject: [PATCH 7/7] addresses comments --- src/components/modal/v2/lib/utils.js | 2 +- src/components/modal/v2/lib/zoid-polyfill.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/modal/v2/lib/utils.js b/src/components/modal/v2/lib/utils.js index 3088ea0c05..d3a5bffe0e 100644 --- a/src/components/modal/v2/lib/utils.js +++ b/src/components/modal/v2/lib/utils.js @@ -145,7 +145,7 @@ export function validateProps(updatedProps) { export function sendEventAck(eventId, trustedOrigin) { // skip this step if running in test env because jest's target windows don't support postMessage - if (window.process?.env?.NODE_ENV === 'test') { + if (process.env.NODE_ENV === 'test') { return; } diff --git a/src/components/modal/v2/lib/zoid-polyfill.js b/src/components/modal/v2/lib/zoid-polyfill.js index 7a00748948..8e15e39663 100644 --- a/src/components/modal/v2/lib/zoid-polyfill.js +++ b/src/components/modal/v2/lib/zoid-polyfill.js @@ -18,11 +18,11 @@ export function validateAndUpdateBrowserProps(initialProps, propListeners, updat origin: eventOrigin, data: { eventName, id, eventPayload: newProps } } = updatedPropsEvent; - const merchantOrigin = decodeURIComponent(initialProps.origin); + const clientOrigin = decodeURIComponent(initialProps.origin); - if (eventOrigin === merchantOrigin && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') { + if (eventOrigin === clientOrigin && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') { // send event ack so PostMessenger will stop reposting event - sendEventAck(id, merchantOrigin); + sendEventAck(id, clientOrigin); const validProps = validateProps(newProps); listenAndAssignProps(validProps, propListeners); }