Skip to content

Commit 0e7a439

Browse files
feat: message hover and click behavior and modal (#2352)
1 parent 0c51b05 commit 0e7a439

File tree

12 files changed

+291
-5
lines changed

12 files changed

+291
-5
lines changed

Diff for: globals.js

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = {
3232
__CARD_FIELD__: "/smart/card-field",
3333
__WALLET__: "/smart/wallet",
3434
__PAYMENT_FIELDS__: "/altpayfields",
35+
__MESSAGE_MODAL__: "https://www.paypalobjects.com/upstream/bizcomponents/js/modal.js",
3536
},
3637
},
3738
};

Diff for: src/declarations.js

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ declare var __PAYPAL_CHECKOUT__: {|
1818
__VENMO__: string,
1919
__WALLET__: string,
2020
__PAYMENT_FIELDS__: string,
21+
__MESSAGE_MODAL__: string,
2122
|},
2223
|};
2324

Diff for: src/ui/buttons/message.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function Message({ markup, position }: MessageProps): ChildType {
2121
if (typeof markup !== "string") {
2222
return (
2323
<div
24-
className={`${messageClassNames} ${CLASS.BUTTON_MESSAGE_RESERVE}`}
24+
class={`${messageClassNames} ${CLASS.BUTTON_MESSAGE_RESERVE}`}
2525
style={`height:${INITIAL_RESERVED_HEIGHT}`}
2626
/>
2727
);

Diff for: src/ui/buttons/props.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ export function normalizeButtonMessage(
760760
}
761761
}
762762

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

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

783-
if (position && !values(MESSAGE_POSITION).includes(position)) {
783+
if (
784+
typeof position !== "undefined" &&
785+
!values(MESSAGE_POSITION).includes(position)
786+
) {
784787
throw new Error(`Invalid position: ${position}`);
785788
}
786789

787-
if (align && !values(MESSAGE_ALIGN).includes(align)) {
790+
if (typeof align !== "undefined" && !values(MESSAGE_ALIGN).includes(align)) {
788791
throw new Error(`Invalid align: ${align}`);
789792
}
790793

Diff for: src/zoid/buttons/component.jsx

+96
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
getRenderedButtons,
9191
getButtonSize,
9292
getButtonExperiments,
93+
getModal,
9394
} from "./util";
9495

9596
export type ButtonsComponent = ZoidComponent<ButtonProps>;
@@ -681,6 +682,101 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
681682
},
682683
},
683684

685+
onMessageClick: {
686+
type: "function",
687+
required: false,
688+
value: ({ props }) => {
689+
return async ({
690+
offerType,
691+
messageType,
692+
offerCountryCode,
693+
creditProductIdentifier,
694+
}) => {
695+
const { message, clientID, merchantID, currency, buttonSessionID } =
696+
props;
697+
const amount = message?.amount;
698+
699+
getLogger()
700+
.info("button_message_click")
701+
.track({
702+
[FPTI_KEY.TRANSITION]: "button_message_click",
703+
[FPTI_KEY.STATE]: "BUTTON_MESSAGE",
704+
[FPTI_KEY.CONTEXT_ID]: buttonSessionID,
705+
[FPTI_KEY.CONTEXT_TYPE]: "button_session_id",
706+
[FPTI_KEY.EVENT_NAME]: "message_click",
707+
// adding temp string here for our sdk constants
708+
button_message_offer_type: offerType,
709+
button_message_credit_product_identifier:
710+
creditProductIdentifier,
711+
button_message_type: messageType,
712+
button_message_position: message?.position,
713+
button_message_align: message?.align,
714+
button_message_color: message?.color,
715+
button_message_offer_country: offerCountryCode,
716+
button_message_amount: amount,
717+
[FPTI_KEY.BUTTON_SESSION_UID]: buttonSessionID,
718+
});
719+
720+
const modalInstance = await getModal(clientID, merchantID);
721+
return modalInstance?.show({
722+
amount,
723+
offer: offerType,
724+
currency,
725+
});
726+
};
727+
},
728+
},
729+
730+
onMessageHover: {
731+
type: "function",
732+
required: false,
733+
value: ({ props }) => {
734+
return () => {
735+
// offerType, messageType, offerCountryCode, and creditProductIdentifier are passed in and may be used in an upcoming message hover logging feature
736+
737+
// lazy loads the modal, to be memoized and executed onMessageClick
738+
const { clientID, merchantID } = props;
739+
return getModal(clientID, merchantID);
740+
};
741+
},
742+
},
743+
744+
onMessageReady: {
745+
type: "function",
746+
required: false,
747+
value: ({ props }) => {
748+
return ({
749+
offerType,
750+
messageType,
751+
offerCountryCode,
752+
creditProductIdentifier,
753+
}) => {
754+
const { message, buttonSessionID } = props;
755+
756+
getLogger()
757+
.info("button_message_render")
758+
.track({
759+
[FPTI_KEY.TRANSITION]: "button_message_render",
760+
[FPTI_KEY.STATE]: "BUTTON_MESSAGE",
761+
[FPTI_KEY.CONTEXT_ID]: buttonSessionID,
762+
[FPTI_KEY.CONTEXT_TYPE]: "button_session_id",
763+
[FPTI_KEY.EVENT_NAME]: "message_render",
764+
// adding temp string here for our sdk constants
765+
button_message_offer_type: offerType,
766+
button_message_credit_product_identifier:
767+
creditProductIdentifier,
768+
button_message_type: messageType,
769+
button_message_posiiton: message?.position,
770+
button_message_align: message?.align,
771+
button_message_color: message?.color,
772+
button_message_offer_country: offerCountryCode,
773+
button_message_amount: message?.amount,
774+
[FPTI_KEY.BUTTON_SESSION_UID]: buttonSessionID,
775+
});
776+
};
777+
},
778+
},
779+
684780
onShippingAddressChange: {
685781
type: "function",
686782
required: false,

Diff for: src/zoid/buttons/util.js

+53
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getElement,
1414
isStandAlone,
1515
once,
16+
memoize,
1617
} from "@krakenjs/belter/src";
1718
import { FUNDING } from "@paypal/sdk-constants/src";
1819
import {
@@ -22,6 +23,8 @@ import {
2223
getFundingEligibility,
2324
getPlatform,
2425
getComponents,
26+
getEnv,
27+
getNamespace,
2528
} from "@paypal/sdk-client/src";
2629
import { getRefinedFundingEligibility } from "@paypal/funding-components/src";
2730

@@ -357,3 +360,53 @@ export function getButtonSize(
357360
}
358361
}
359362
}
363+
364+
function buildModalBundleUrl(): string {
365+
let url = __PAYPAL_CHECKOUT__.__URI__.__MESSAGE_MODAL__;
366+
if (getEnv() === "sandbox") {
367+
url = url.replace("/js/", "/sandbox/");
368+
} else if (getEnv() === "stage" || getEnv() === "local") {
369+
url = url.replace("/js/", "/stage/");
370+
}
371+
return url;
372+
}
373+
374+
export const getModal: (
375+
clientID: string,
376+
merchantID: $ReadOnlyArray<string> | void
377+
) => Object = memoize(async (clientID, merchantID) => {
378+
try {
379+
const namespace = getNamespace();
380+
if (!window[namespace].MessagesModal) {
381+
// eslint-disable-next-line no-restricted-globals, promise/no-native
382+
await new Promise((resolve, reject) => {
383+
const script = document.createElement("script");
384+
script.setAttribute("data-pp-namespace", namespace);
385+
script.src = buildModalBundleUrl();
386+
script.addEventListener("error", (err: Event) => {
387+
reject(err);
388+
});
389+
script.addEventListener("load", () => {
390+
document.body?.removeChild(script);
391+
resolve();
392+
});
393+
document.body?.appendChild(script);
394+
});
395+
}
396+
397+
return window[namespace].MessagesModal({
398+
account: `client-id:${clientID}`,
399+
merchantId: merchantID?.join(",") || undefined,
400+
});
401+
} catch (err) {
402+
// $FlowFixMe flow doesn't seem to understand that the reset function property exists on the function object itself
403+
getModal.reset();
404+
getLogger()
405+
.error("button_message_modal_fetch_error", { err })
406+
.track({
407+
err: err.message || "BUTTON_MESSAGE_MODAL_FETCH_ERROR",
408+
details: err.details,
409+
stack: JSON.stringify(err.stack || err),
410+
});
411+
}
412+
});

Diff for: test/globals.js

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ function getTestGlobals(productionGlobals) {
118118
__CARD_FIELD__: `/base/test/integration/windows/card-field/index.htm`,
119119
__WALLET__: `/base/test/integration/windows/wallet/index.htm`,
120120
__PAYMENT_FIELDS__: `/base/test/integration/windows/paymentfields/index.htm`,
121+
__MESSAGE_MODAL__: `/base/test/integration/windows/button/modal.js`,
121122
},
122123
},
123124

Diff for: test/integration/tests/button/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "./error";
66
import "./drivers";
77
import "./frame";
88
import "./size";
9+
import "./message";
910
import "./multiple";
1011
import "./layout";
1112
import "./style";

Diff for: test/integration/tests/button/message.js

+108
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

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

78
import { CLASS } from "../../../../src/constants";
89
import {
@@ -378,4 +379,111 @@ describe(`paypal button message`, () => {
378379
});
379380
});
380381
});
382+
383+
describe("modal", () => {
384+
it("should ensure data-pp-namespace passes in the namespace", (done) => {
385+
window.paypal
386+
.Buttons({
387+
message: {},
388+
test: {
389+
onRender({ hoverMessage }) {
390+
hoverMessage()
391+
.then(() => {
392+
assert.equal(getNamespace(), window.namespace);
393+
done();
394+
})
395+
.catch(done);
396+
},
397+
},
398+
})
399+
.render("#testContainer");
400+
});
401+
it("should ensure getModal callback with clientID and merchantID is called on hover", (done) => {
402+
window.paypal
403+
.Buttons({
404+
message: {},
405+
test: {
406+
onRender({ hoverMessage }) {
407+
hoverMessage()
408+
.then(() => {
409+
assert.ok(
410+
Object.keys(window.paypal.MessagesModal.mock.calledWith)
411+
.length === 2
412+
);
413+
assert.ok(
414+
typeof window.paypal.MessagesModal.mock.calledWith
415+
.account === "string"
416+
);
417+
assert.ok(
418+
typeof window.paypal.MessagesModal.mock.calledWith
419+
.merchantId === "undefined"
420+
);
421+
done();
422+
})
423+
.catch(done);
424+
},
425+
},
426+
})
427+
.render("#testContainer");
428+
});
429+
it("should ensure getModal calls create a script with modal data and called with amount, offer, and currency from props", (done) => {
430+
const props = { offerType: "PAY_LATER", messageType: "GPL" };
431+
window.paypal
432+
.Buttons({
433+
message: {
434+
amount: 101,
435+
},
436+
test: {
437+
onRender({ clickMessage, hoverMessage }) {
438+
hoverMessage()
439+
.then(() => {
440+
return clickMessage(props).then(() => {
441+
assert.equal(
442+
window.paypal.MessagesModal.mock.show.calledWith.amount,
443+
101
444+
);
445+
assert.equal(
446+
window.paypal.MessagesModal.mock.show.calledWith.offer,
447+
"PAY_LATER"
448+
);
449+
assert.equal(
450+
window.paypal.MessagesModal.mock.show.calledWith.currency,
451+
"USD"
452+
);
453+
done();
454+
});
455+
})
456+
.catch(done);
457+
},
458+
},
459+
})
460+
.render("#testContainer");
461+
});
462+
it("should ensure getModal calls utilize a single modal instance, not creating multiple modals", (done) => {
463+
const props = { offerType: "PAY_LATER", messageType: "GPL" };
464+
window.paypal
465+
.Buttons({
466+
message: {
467+
amount: 101,
468+
},
469+
test: {
470+
onRender({ clickMessage, hoverMessage }) {
471+
hoverMessage()
472+
.then(() => {
473+
return clickMessage(props).then(() => {
474+
return hoverMessage().then(() => {
475+
return clickMessage(props).then(() => {
476+
assert.equal(window.paypal.MessagesModal.mock.calls, 1);
477+
done();
478+
});
479+
});
480+
});
481+
})
482+
.catch(done);
483+
},
484+
},
485+
})
486+
.render("#testContainer");
487+
});
488+
});
381489
});

Diff for: test/integration/windows/button/index.jsx

+6
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,11 @@ if (onRender) {
236236
click() {
237237
getElement(".paypal-button", document).click();
238238
},
239+
hoverMessage(): ZalgoPromise<void> | void {
240+
return window.xprops.onMessageHover();
241+
},
242+
clickMessage({ offerType, messageType }): ZalgoPromise<void> | void {
243+
return window.xprops.onMessageClick({ offerType, messageType });
244+
},
239245
});
240246
}

0 commit comments

Comments
 (0)