Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds prop update listening to modal browser zoid polyfill #1161

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
57 changes: 57 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,59 @@ 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;
}
Comment on lines +117 to +131
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a belter util we can leverage that meets our needs here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

belter has uniqueID that returns uid_${randomID}_${timeID}. Considerations are that the existing postMessenger message ids have a format that is different from that (32 alphanumeric vs the above format in hex); however, there is no validation for nor true consumer of the id we will generate here. Would you like me to use this belter uniqueID?

Copy link
Contributor Author

@danzhaaspaypal danzhaaspaypal Mar 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed this in the other (more recent) PR on this line since the file was moved out of utils


export function validateProps(updatedProps) {
const validatedProps = {};
Object.entries(updatedProps).forEach(entry => {
const [k, v] = entry;
if (k === 'offerTypes') {
validatedProps.offer = validate.offer({ props: { offer: v } });
} else {
validatedProps[k] = validate[k]({ props: { [k]: v } });
}
Comment on lines +136 to +141
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we guaranteed to only pass in props that we have validation for? Should we account for the scenario where validate[k] is undefined which would throw an error here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. See #1170 for this fix

});
return validatedProps;
}

export function sendEventAck(eventId, trustedOrigin) {
// skip this step if running in test env because jest's target windows don't support postMessage
if (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;
}
Comment on lines +153 to +159
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let targetWindow;
const popupCheck = window.parent === window;
if (popupCheck) {
targetWindow = window.opener;
} else {
targetWindow = window.parent;
}
const targetWindow = window.parent === window ? window.opener : window.parent;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed here in other PR


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()
},
trustedOrigin
);
}
43 changes: 35 additions & 8 deletions src/components/modal/v2/lib/zoid-polyfill.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
/* global Android */
import { isAndroidWebview, isIosWebview, getPerformance } from '@krakenjs/belter/src';
import { getOrCreateDeviceID, logger } from '../../../../utils';
import { isIframe } from './utils';
import { isIframe, validateProps, sendEventAck } from './utils';

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 handleBrowserEvents(initialProps, propListeners, updatedPropsEvent) {
const {
origin: eventOrigin,
data: { eventName, id, eventPayload: newProps }
} = updatedPropsEvent;
const clientOrigin = decodeURIComponent(initialProps.origin);

if (eventOrigin === clientOrigin && eventName === 'PROPS_UPDATE' && newProps && typeof newProps === 'object') {
// send event ack so PostMessenger will stop reposting event
sendEventAck(id, clientOrigin);
const validProps = validateProps(newProps);
updateProps(validProps, propListeners);
}
}

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

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

window.addEventListener(
'message',
event => {
handleBrowserEvents(props, 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 @@ -139,11 +170,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
Loading
Loading