Skip to content

Commit 86c001c

Browse files
feat: adds prop update listening to modal browser zoid polyfill (#1161)
1 parent 06970de commit 86c001c

File tree

6 files changed

+440
-120
lines changed

6 files changed

+440
-120
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { uniqueID } from '@krakenjs/belter/src';
2+
3+
// these constants are defined in PostMessenger
4+
const POSTMESSENGER_EVENT_TYPES = {
5+
ACK: 'ack',
6+
MESSAGE: 'message'
7+
};
8+
const POSTMESSENGER_ACK_PAYLOAD = {
9+
ok: 'true'
10+
};
11+
// these constants should maintain parity with MESSAGE_MODAL_EVENT_NAMES in core-web-sdk
12+
export const POSTMESSENGER_EVENT_NAMES = {
13+
CALCULATE: 'paypal-messages-modal-calculate',
14+
CLOSE: 'paypal-messages-modal-close',
15+
SHOW: 'paypal-messages-modal-show'
16+
};
17+
18+
export function sendEvent(payload, trustedOrigin) {
19+
if (!trustedOrigin) {
20+
return;
21+
}
22+
23+
const isTest = process.env.NODE_ENV === 'test';
24+
const targetWindow = !isTest && window.parent === window ? window.opener : window.parent;
25+
26+
targetWindow.postMessage(payload, trustedOrigin);
27+
}
28+
29+
// This function provides data security by preventing accidentally exposing sensitive data; we are adding
30+
// an extra layer of validation here by only allowing explicitly approved fields to be included
31+
function createSafePayload(unscreenedPayload) {
32+
const allowedFields = [
33+
'linkName' // close event
34+
];
35+
36+
const safePayload = {};
37+
if (unscreenedPayload) {
38+
const entries = Object.entries(unscreenedPayload);
39+
entries.forEach(entry => {
40+
const [key, value] = entry;
41+
if (allowedFields.includes(key)) {
42+
safePayload[key] = value;
43+
} else {
44+
console.warn(`modal hook payload param should be allowlisted if secure: ${key}`);
45+
}
46+
});
47+
}
48+
49+
return safePayload;
50+
}
51+
52+
export function createPostMessengerEvent(typeArg, eventName, eventPayloadArg) {
53+
let type;
54+
let eventPayload;
55+
56+
if (typeArg === 'ack') {
57+
type = POSTMESSENGER_EVENT_TYPES.ACK;
58+
eventPayload = POSTMESSENGER_ACK_PAYLOAD;
59+
} else if (typeArg === 'message') {
60+
type = POSTMESSENGER_EVENT_TYPES.MESSAGE;
61+
// createSafePayload, only call this if a payload is sent
62+
eventPayload = createSafePayload(eventPayloadArg);
63+
}
64+
65+
return {
66+
eventName,
67+
id: uniqueID(),
68+
type,
69+
eventPayload: eventPayload || {}
70+
};
71+
}

src/components/modal/v2/lib/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import objectEntries from 'core-js-pure/stable/object/entries';
22
import arrayFrom from 'core-js-pure/stable/array/from';
33
import { isIosWebview, isAndroidWebview } from '@krakenjs/belter/src';
44
import { request, memoize, ppDebug } from '../../../../utils';
5+
import validate from '../../../../library/zoid/message/validation';
56

67
export const getContent = memoize(
78
({
@@ -112,3 +113,18 @@ export function formatDateByCountry(country) {
112113
}
113114
return currentDate.toLocaleDateString('en-GB', options);
114115
}
116+
117+
export function validateProps(updatedProps) {
118+
const validatedProps = {};
119+
Object.entries(updatedProps).forEach(entry => {
120+
const [k, v] = entry;
121+
if (k === 'offerType') {
122+
validatedProps.offer = validate.offer({ props: { offer: v } });
123+
} else if (!Object.keys(validate).includes(k)) {
124+
validatedProps[k] = v;
125+
} else {
126+
validatedProps[k] = validate[k]({ props: { [k]: v } });
127+
}
128+
});
129+
return validatedProps;
130+
}

src/components/modal/v2/lib/zoid-polyfill.js

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,56 @@
11
/* global Android */
22
import { isAndroidWebview, isIosWebview, getPerformance } from '@krakenjs/belter/src';
33
import { getOrCreateDeviceID, logger } from '../../../../utils';
4-
import { isIframe } from './utils';
4+
import { validateProps, isIframe } from './utils';
5+
import { sendEvent, createPostMessengerEvent, POSTMESSENGER_EVENT_NAMES } from './postMessage';
56

67
const IOS_INTERFACE_NAME = 'paypalMessageModalCallbackHandler';
78
const ANDROID_INTERFACE_NAME = 'paypalMessageModalCallbackHandler';
89

10+
function updateProps(newProps, propListeners) {
11+
Array.from(propListeners.values()).forEach(listener => {
12+
listener({ ...window.xprops, ...newProps });
13+
});
14+
Object.assign(window.xprops, newProps);
15+
}
16+
17+
export function handlePropsUpdateEvent(propListeners, updatedPropsEvent) {
18+
const {
19+
data: { eventPayload: newProps }
20+
} = updatedPropsEvent;
21+
if (newProps && typeof newProps === 'object') {
22+
const validProps = validateProps(newProps);
23+
updateProps(validProps, propListeners);
24+
}
25+
}
26+
27+
export function logModalClose(linkName) {
28+
logger.track({
29+
index: '1',
30+
et: 'CLICK',
31+
event_type: 'modal_close',
32+
page_view_link_name: linkName
33+
});
34+
}
35+
36+
export function handleBrowserEvents(clientOrigin, propListeners, event) {
37+
const {
38+
origin: eventOrigin,
39+
data: { eventName, id }
40+
} = event;
41+
if (eventOrigin !== clientOrigin) {
42+
return;
43+
}
44+
if (eventName === 'PROPS_UPDATE') {
45+
handlePropsUpdateEvent(propListeners, event);
46+
}
47+
if (eventName === 'MODAL_CLOSED') {
48+
logModalClose(event.data.eventPayload.linkName);
49+
}
50+
// send event ack with original event id so PostMessenger will stop reposting event
51+
sendEvent(createPostMessengerEvent('ack', id), clientOrigin);
52+
}
53+
954
const getAccount = (merchantId, clientId, payerId) => {
1055
if (merchantId) {
1156
return merchantId;
@@ -20,9 +65,23 @@ const getAccount = (merchantId, clientId, payerId) => {
2065
};
2166

2267
const setupBrowser = props => {
68+
const propListeners = new Set();
69+
70+
let trustedOrigin = decodeURIComponent(props.origin || '');
71+
if (isIframe && document.referrer && !process.env.NODE_ENV === 'test') {
72+
trustedOrigin = new window.URL(document.referrer).origin;
73+
}
74+
75+
window.addEventListener(
76+
'message',
77+
event => {
78+
handleBrowserEvents(trustedOrigin, propListeners, event);
79+
},
80+
false
81+
);
82+
2383
window.xprops = {
24-
// We will never recieve new props via this integration style
25-
onProps: () => {},
84+
onProps: listener => propListeners.add(listener),
2685
// TODO: Verify these callbacks are instrumented correctly
2786
onReady: ({ products, meta }) => {
2887
const { clientId, payerId, merchantId, offer, partnerAttributionId } = props;
@@ -79,6 +138,7 @@ const setupBrowser = props => {
79138
});
80139
},
81140
onCalculate: ({ value }) => {
141+
sendEvent(createPostMessengerEvent('message', POSTMESSENGER_EVENT_NAMES.CALCULATE), trustedOrigin);
82142
logger.track({
83143
index: '1',
84144
et: 'CLICK',
@@ -89,6 +149,7 @@ const setupBrowser = props => {
89149
});
90150
},
91151
onShow: () => {
152+
sendEvent(createPostMessengerEvent('message', POSTMESSENGER_EVENT_NAMES.SHOW), trustedOrigin);
92153
logger.track({
93154
index: '1',
94155
et: 'CLIENT_IMPRESSION',
@@ -97,16 +158,15 @@ const setupBrowser = props => {
97158
});
98159
},
99160
onClose: ({ linkName }) => {
100-
if (isIframe && document.referrer) {
101-
const targetOrigin = new window.URL(document.referrer).origin;
102-
window.parent.postMessage('paypal-messages-modal-close', targetOrigin);
103-
}
104-
logger.track({
105-
index: '1',
106-
et: 'CLICK',
107-
event_type: 'modal_close',
108-
page_view_link_name: linkName
109-
});
161+
const eventPayload = {
162+
linkName
163+
// for data security, also add new params to createSafePayload in ./postMessage.js
164+
};
165+
sendEvent(
166+
createPostMessengerEvent('message', POSTMESSENGER_EVENT_NAMES.CLOSE, eventPayload),
167+
trustedOrigin
168+
);
169+
logModalClose(linkName);
110170
},
111171
// Overridable defaults
112172
integrationType: __MESSAGES__.__TARGET__,
@@ -139,11 +199,7 @@ const setupWebview = props => {
139199
window.actions = {
140200
updateProps: newProps => {
141201
if (newProps && typeof newProps === 'object') {
142-
Array.from(propListeners.values()).forEach(listener => {
143-
listener({ ...window.xprops, ...newProps });
144-
});
145-
146-
Object.assign(window.xprops, newProps);
202+
updateProps(newProps, propListeners);
147203
}
148204
}
149205
};

src/components/modal/v2/parts/Container.jsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ const Container = ({ children }) => {
4545
useEffect(() => {
4646
if (transitionState === 'CLOSED') {
4747
contentWrapperRef.current.scrollTop = 0;
48-
} else if (transitionState === 'OPEN') {
49-
window.focus();
5048
}
5149
}, [transitionState]);
5250

tests/unit/spec/src/components/modal/v2/lib/utils.test.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { formatDateByCountry } from 'src/components/modal/v2/lib/utils';
1+
import { formatDateByCountry, validateProps } from 'src/components/modal/v2/lib/utils';
22

33
describe('Date function should return correct date format based on country', () => {
44
it('US country date should be formatted MM/DD/YYYY', () => {
@@ -14,3 +14,26 @@ describe('Date function should return correct date format based on country', ()
1414
expect(result).toMatch(expectedFormat);
1515
});
1616
});
17+
18+
describe('validateProps', () => {
19+
it('validates amount, contextualComponents, and offerType, and preserves value of other props', () => {
20+
const propsToFix = {
21+
amount: '10',
22+
offerType: 'PAY_LATER_SHORT_TERM, PAY_LATER_LONG_TERM',
23+
contextualComponents: 'paypal_button'
24+
};
25+
const propsToPreserve = {
26+
itemSkus: ['123', '456'],
27+
presentationMode: 'auto'
28+
};
29+
30+
const output = validateProps({ ...propsToFix, ...propsToPreserve });
31+
32+
const fixedPropOutputValues = {
33+
amount: 10,
34+
offer: 'PAY_LATER_LONG_TERM,PAY_LATER_SHORT_TERM',
35+
contextualComponents: 'PAYPAL_BUTTON'
36+
};
37+
expect(output).toMatchObject({ ...fixedPropOutputValues, ...propsToPreserve });
38+
});
39+
});

0 commit comments

Comments
 (0)