Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/components/modal/v2/lib/postMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { uniqueID } from '@krakenjs/belter/src';

// these constants are defined in PostMessenger
const POSTMESSENGER_EVENT_TYPES = {
ACK: 'ack',
MESSAGE: 'message'
};
const POSTMESSENGER_ACK_PAYLOAD = {
ok: 'true'
};
// these constants should maintain parity with MESSAGE_MODAL_EVENT_NAMES in core-web-sdk
export const POSTMESSENGER_EVENT_NAMES = {
CALCULATE: 'paypal-messages-modal-calculate',
CLOSE: 'paypal-messages-modal-close',
SHOW: 'paypal-messages-modal-show'
};

export function sendEvent(payload, trustedOrigin) {
if (!trustedOrigin) {
return;
}

const isTest = process.env.NODE_ENV === 'test';
const targetWindow = !isTest && window.parent === window ? window.opener : window.parent;

targetWindow.postMessage(payload, trustedOrigin);
}

// This function provides data security by preventing accidentally exposing sensitive data; we are adding
// an extra layer of validation here by only allowing explicitly approved fields to be included
function createSafePayload(unscreenedPayload) {
const allowedFields = [
'linkName' // close event
];

const safePayload = {};
if (unscreenedPayload) {
const entries = Object.entries(unscreenedPayload);
entries.forEach(entry => {
const [key, value] = entry;
if (allowedFields.includes(key)) {
safePayload[key] = value;
} else {
console.warn(`modal hook payload param should be allowlisted if secure: ${key}`);

Check warning on line 44 in src/components/modal/v2/lib/postMessage.js

View workflow job for this annotation

GitHub Actions / Lint and Unit Tests

Unexpected console statement
}
});
}

return safePayload;
}

export function createPostMessengerEvent(typeArg, eventName, eventPayloadArg) {
let type;
let eventPayload;

if (typeArg === 'ack') {
type = POSTMESSENGER_EVENT_TYPES.ACK;
eventPayload = POSTMESSENGER_ACK_PAYLOAD;
} else if (typeArg === 'message') {
type = POSTMESSENGER_EVENT_TYPES.MESSAGE;
// createSafePayload, only call this if a payload is sent
eventPayload = createSafePayload(eventPayloadArg);
}

return {
eventName,
id: uniqueID(),
type,
eventPayload: eventPayload || {}
};
}
16 changes: 16 additions & 0 deletions src/components/modal/v2/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
({
Expand Down Expand Up @@ -112,3 +113,18 @@ export function formatDateByCountry(country) {
}
return currentDate.toLocaleDateString('en-GB', options);
}

export function validateProps(updatedProps) {
const validatedProps = {};
Object.entries(updatedProps).forEach(entry => {
const [k, v] = entry;
if (k === 'offerType') {
validatedProps.offer = validate.offer({ props: { offer: v } });
} else if (!Object.keys(validate).includes(k)) {
validatedProps[k] = v;
} else {
validatedProps[k] = validate[k]({ props: { [k]: v } });
}
});
return validatedProps;
}
92 changes: 74 additions & 18 deletions src/components/modal/v2/lib/zoid-polyfill.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,56 @@
/* global Android */
import { isAndroidWebview, isIosWebview, getPerformance } from '@krakenjs/belter/src';
import { getOrCreateDeviceID, logger } from '../../../../utils';
import { isIframe } from './utils';
import { validateProps, isIframe } from './utils';
import { sendEvent, createPostMessengerEvent, POSTMESSENGER_EVENT_NAMES } from './postMessage';

const IOS_INTERFACE_NAME = 'paypalMessageModalCallbackHandler';
const ANDROID_INTERFACE_NAME = 'paypalMessageModalCallbackHandler';

function updateProps(newProps, propListeners) {
Array.from(propListeners.values()).forEach(listener => {
listener({ ...window.xprops, ...newProps });
});
Object.assign(window.xprops, newProps);
}

export function handlePropsUpdateEvent(propListeners, updatedPropsEvent) {
const {
data: { eventPayload: newProps }
} = updatedPropsEvent;
if (newProps && typeof newProps === 'object') {
const validProps = validateProps(newProps);
updateProps(validProps, propListeners);
}
}

export function logModalClose(linkName) {
logger.track({
index: '1',
et: 'CLICK',
event_type: 'modal_close',
page_view_link_name: linkName
});
}

export function handleBrowserEvents(clientOrigin, propListeners, event) {
const {
origin: eventOrigin,
data: { eventName, id }
} = event;
if (eventOrigin !== clientOrigin) {
return;
}
if (eventName === 'PROPS_UPDATE') {
handlePropsUpdateEvent(propListeners, event);
}
if (eventName === 'MODAL_CLOSED') {
logModalClose(event.data.eventPayload.linkName);
}
// send event ack with original event id so PostMessenger will stop reposting event
sendEvent(createPostMessengerEvent('ack', id), clientOrigin);
}

const getAccount = (merchantId, clientId, payerId) => {
if (merchantId) {
return merchantId;
Expand All @@ -20,9 +65,23 @@ const getAccount = (merchantId, clientId, payerId) => {
};

const setupBrowser = props => {
const propListeners = new Set();

let trustedOrigin = decodeURIComponent(props.origin || '');
if (isIframe && document.referrer && !process.env.NODE_ENV === 'test') {
trustedOrigin = new window.URL(document.referrer).origin;
}

window.addEventListener(
'message',
event => {
handleBrowserEvents(trustedOrigin, propListeners, event);
},
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;
Expand Down Expand Up @@ -79,6 +138,7 @@ const setupBrowser = props => {
});
},
onCalculate: ({ value }) => {
sendEvent(createPostMessengerEvent('message', POSTMESSENGER_EVENT_NAMES.CALCULATE), trustedOrigin);
logger.track({
index: '1',
et: 'CLICK',
Expand All @@ -89,6 +149,7 @@ const setupBrowser = props => {
});
},
onShow: () => {
sendEvent(createPostMessengerEvent('message', POSTMESSENGER_EVENT_NAMES.SHOW), trustedOrigin);
logger.track({
index: '1',
et: 'CLIENT_IMPRESSION',
Expand All @@ -97,16 +158,15 @@ const setupBrowser = props => {
});
},
onClose: ({ linkName }) => {
if (isIframe && document.referrer) {
const targetOrigin = new window.URL(document.referrer).origin;
window.parent.postMessage('paypal-messages-modal-close', targetOrigin);
}
logger.track({
index: '1',
et: 'CLICK',
event_type: 'modal_close',
page_view_link_name: linkName
});
const eventPayload = {
linkName
// for data security, also add new params to createSafePayload in ./postMessage.js
};
sendEvent(
createPostMessengerEvent('message', POSTMESSENGER_EVENT_NAMES.CLOSE, eventPayload),
trustedOrigin
);
logModalClose(linkName);
},
// Overridable defaults
integrationType: __MESSAGES__.__TARGET__,
Expand Down Expand Up @@ -139,11 +199,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);
updateProps(newProps, propListeners);
}
}
};
Expand Down
2 changes: 0 additions & 2 deletions src/components/modal/v2/parts/Container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ const Container = ({ children }) => {
useEffect(() => {
if (transitionState === 'CLOSED') {
contentWrapperRef.current.scrollTop = 0;
} else if (transitionState === 'OPEN') {
window.focus();
}
}, [transitionState]);

Expand Down
25 changes: 24 additions & 1 deletion tests/unit/spec/src/components/modal/v2/lib/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { formatDateByCountry } from 'src/components/modal/v2/lib/utils';
import { formatDateByCountry, validateProps } from 'src/components/modal/v2/lib/utils';

describe('Date function should return correct date format based on country', () => {
it('US country date should be formatted MM/DD/YYYY', () => {
Expand All @@ -14,3 +14,26 @@ describe('Date function should return correct date format based on country', ()
expect(result).toMatch(expectedFormat);
});
});

describe('validateProps', () => {
it('validates amount, contextualComponents, and offerType, and preserves value of other props', () => {
const propsToFix = {
amount: '10',
offerType: 'PAY_LATER_SHORT_TERM, PAY_LATER_LONG_TERM',
contextualComponents: 'paypal_button'
};
const propsToPreserve = {
itemSkus: ['123', '456'],
presentationMode: 'auto'
};

const output = validateProps({ ...propsToFix, ...propsToPreserve });

const fixedPropOutputValues = {
amount: 10,
offer: 'PAY_LATER_LONG_TERM,PAY_LATER_SHORT_TERM',
contextualComponents: 'PAYPAL_BUTTON'
};
expect(output).toMatchObject({ ...fixedPropOutputValues, ...propsToPreserve });
});
});
Loading
Loading