Skip to content

feat: message hover and click behavior and modal #2352

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

Merged
merged 17 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion src/ui/buttons/message.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function Message({ markup, position }: MessageProps): ChildType {
if (typeof markup !== "string") {
return (
<div
className={`${messageClassNames} ${CLASS.BUTTON_MESSAGE_RESERVE}`}
class={`${messageClassNames} ${CLASS.BUTTON_MESSAGE_RESERVE}`}
style={`height:${INITIAL_RESERVED_HEIGHT}`}
/>
);
Expand Down
11 changes: 7 additions & 4 deletions src/ui/buttons/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ export function normalizeButtonMessage(
}
}

if (offer) {
if (typeof offer !== "undefined") {
if (!Array.isArray(offer)) {
throw new TypeError(
`Expected message.offer to be an array of strings, got: ${String(
Expand All @@ -776,15 +776,18 @@ export function normalizeButtonMessage(
}
}

if (color && !values(MESSAGE_COLOR).includes(color)) {
if (typeof color !== "undefined" && !values(MESSAGE_COLOR).includes(color)) {
throw new Error(`Invalid color: ${color}`);
}

if (position && !values(MESSAGE_POSITION).includes(position)) {
if (
typeof position !== "undefined" &&
!values(MESSAGE_POSITION).includes(position)
) {
throw new Error(`Invalid position: ${position}`);
}

if (align && !values(MESSAGE_ALIGN).includes(align)) {
if (typeof align !== "undefined" && !values(MESSAGE_ALIGN).includes(align)) {
throw new Error(`Invalid align: ${align}`);
}

Expand Down
46 changes: 46 additions & 0 deletions src/zoid/buttons/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
getRenderedButtons,
getButtonSize,
getButtonExperiments,
getModal,
} from "./util";

export type ButtonsComponent = ZoidComponent<ButtonProps>;
Expand Down Expand Up @@ -681,6 +682,51 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
},
},

onMessageHover: {
type: "function",
required: false,
value: ({ props }) => {
return () => {
// lazy loads the modal, to be memoized and executed onMessageClick
const { clientID, merchantID } = props;
return getModal(clientID, merchantID);
};
},
},

onMessageClick: {
type: "function",
required: false,
value: ({ props }) => {
return async ({ offerType, messageType }) => {
const { message, clientID, merchantID, currency, buttonSessionID } =
props;
const amount = message?.amount || undefined;

const modalInstance = await getModal(clientID, merchantID);

getLogger()
.info("button_message_clicked")
.track({
[FPTI_KEY.EVENT_NAME]: "message_click",
// [FPTI_KEY.BUTTON_MESSAGE_OFFER_TYPE]: offerType,
"button message type fpti key placeholder": messageType,
// [FPTI_KEY.BUTTON_MESSAGE_POSITION]: message.position,
// [FPTI_KEY.BUTTON_MESSAGE_ALIGN]: message.align,
// [FPTI_KEY.BUTTON_MESSAGE_COLOR]: message.color,
// [FPTI_KEY.AMOUNT]: amount,
[FPTI_KEY.BUTTON_SESSION_UID]: buttonSessionID,
});

return modalInstance.show({
amount,
offer: offerType?.join(",") || undefined,
currency,
});
};
},
},

onShippingAddressChange: {
type: "function",
required: false,
Expand Down
60 changes: 60 additions & 0 deletions src/zoid/buttons/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getElement,
isStandAlone,
once,
memoize,
} from "@krakenjs/belter/src";
import { FUNDING } from "@paypal/sdk-constants/src";
import {
Expand All @@ -22,6 +23,8 @@ import {
getFundingEligibility,
getPlatform,
getComponents,
getEnv,
getNamespace,
} from "@paypal/sdk-client/src";
import { getRefinedFundingEligibility } from "@paypal/funding-components/src";

Expand Down Expand Up @@ -357,3 +360,60 @@ export function getButtonSize(
}
}
}

export const getModal: (
clientID: string,
merchantID: $ReadOnlyArray<string> | void
) => Object = memoize(async (clientID, merchantID) => {
try {
const namespace = getNamespace();
if (!window[namespace].MessagesModal) {
const modalBundleUrl = () => {
let envPiece;
switch (getEnv()) {
case "test":
return "/base/test/integration/windows/button/modal.js";
case "local":
case "stage":
envPiece = "stage";
break;
case "sandbox":
envPiece = "sandbox";
break;
case "production":
default:
envPiece = "js";
}
return `https://www.paypalobjects.com/upstream/bizcomponents/${envPiece}/modal.js`;
};

// eslint-disable-next-line no-restricted-globals, promise/no-native
await new Promise((resolve, reject) => {
const script = document.createElement("script");
script.setAttribute("data-pp-namespace", namespace);
script.src = modalBundleUrl();
script.addEventListener("error", (err: Event) => {
reject(err);
});
script.addEventListener("load", () => {
document.body?.removeChild(script);
resolve();
});
document.body?.appendChild(script);
});
}

return window[namespace].MessagesModal({
account: `client-id:${clientID}`,
merchantId: merchantID?.join(",") || undefined,
});
} catch (err) {
getLogger()
.info("button_message_modal_fetch_error")
.track({
err: err.message || "BUTTON_MESSAGE_MODAL_FETCH_ERROR",
details: err.details,
stack: JSON.stringify(err.stack || err),
});
}
});
1 change: 1 addition & 0 deletions test/integration/tests/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "./error";
import "./drivers";
import "./frame";
import "./size";
import "./message";
import "./multiple";
import "./layout";
import "./style";
Expand Down
108 changes: 108 additions & 0 deletions test/integration/tests/button/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { wrapPromise, getElement } from "@krakenjs/belter/src";
import { FUNDING } from "@paypal/sdk-constants/src";
import { getNamespace } from "@paypal/sdk-client/src";

import { CLASS } from "../../../../src/constants";
import {
Expand Down Expand Up @@ -378,4 +379,111 @@ describe(`paypal button message`, () => {
});
});
});

describe("modal", () => {
it("should ensure data-pp-namespace passes in the namespace", (done) => {
window.paypal
.Buttons({
message: {},
test: {
onRender({ hoverMessage }) {
hoverMessage()
.then(() => {
assert.equal(getNamespace(), window.namespace);
done();
})
.catch(done);
},
},
})
.render("#testContainer");
});
it("should ensure getModal callback with clientID and merchantID is called on hover", (done) => {
window.paypal
.Buttons({
message: {},
test: {
onRender({ hoverMessage }) {
hoverMessage()
.then(() => {
assert.ok(
Object.keys(window.paypal.MessagesModal.mock.calledWith)
.length === 2
);
assert.ok(
typeof window.paypal.MessagesModal.mock.calledWith
.account === "string"
);
assert.ok(
typeof window.paypal.MessagesModal.mock.calledWith
.merchantId === "undefined"
);
done();
})
.catch(done);
},
},
})
.render("#testContainer");
});
it("should ensure getModal calls create a script with modal data and called with amount, offer, and currency from props", (done) => {
const props = { offerType: ["PAY_LATER"], messageType: "GPL" };
window.paypal
.Buttons({
message: {
amount: 101,
},
test: {
onRender({ clickMessage, hoverMessage }) {
hoverMessage()
.then(() => {
return clickMessage(props).then(() => {
assert.equal(
window.paypal.MessagesModal.mock.show.calledWith.amount,
101
);
assert.equal(
window.paypal.MessagesModal.mock.show.calledWith.offer,
"PAY_LATER"
);
assert.equal(
window.paypal.MessagesModal.mock.show.calledWith.currency,
"USD"
);
done();
});
})
.catch(done);
},
},
})
.render("#testContainer");
});
it("should ensure getModal calls utilize a single modal instance, not creating multiple modals", (done) => {
const props = { offerType: ["PAY_LATER"], messageType: "GPL" };
window.paypal
.Buttons({
message: {
amount: 101,
},
test: {
onRender({ clickMessage, hoverMessage }) {
hoverMessage()
.then(() => {
return clickMessage(props).then(() => {
return hoverMessage().then(() => {
return clickMessage(props).then(() => {
assert.equal(window.paypal.MessagesModal.mock.calls, 1);
done();
});
});
});
})
.catch(done);
},
},
})
.render("#testContainer");
});
});
});
6 changes: 6 additions & 0 deletions test/integration/windows/button/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,5 +236,11 @@ if (onRender) {
click() {
getElement(".paypal-button", document).click();
},
hoverMessage(): ZalgoPromise<void> | void {
return window.xprops.onMessageHover();
},
clickMessage({ offerType, messageType }): ZalgoPromise<void> | void {
return window.xprops.onMessageClick({ offerType, messageType });
},
});
}
22 changes: 22 additions & 0 deletions test/integration/windows/button/modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* @flow */
const namespace = document.currentScript?.getAttribute("data-pp-namespace");

// function createMockFn(callback = () => {}) {
// function mockFn(...args) {
// mockFn.calls.push(args);
// callback(...args)
// }
// }

window.namespace = namespace;
window[namespace].MessagesModal = (config) => {
window[namespace].MessagesModal.mock = {};
window[namespace].MessagesModal.mock.calls =
(window[namespace].MessagesModal.mock.calls ?? 0) + 1;
window[namespace].MessagesModal.mock.calledWith = config;
return {
show: (config2) => {
window[namespace].MessagesModal.mock.show = { calledWith: config2 };
},
};
};
Loading