diff --git a/src/three-domain-secure/utils.jsx b/src/three-domain-secure/utils.jsx index beb0a666d..e73f68688 100644 --- a/src/three-domain-secure/utils.jsx +++ b/src/three-domain-secure/utils.jsx @@ -12,7 +12,7 @@ import { getPayPalDomainRegex, } from "@paypal/sdk-client/src"; -import { Overlay } from "../ui/overlay"; +import { Overlay } from "../ui/overlay/three-domain-secure"; import type { TDSProps } from "./types"; diff --git a/src/ui/buttons/props.js b/src/ui/buttons/props.js index 6a770936e..7fc00cbf8 100644 --- a/src/ui/buttons/props.js +++ b/src/ui/buttons/props.js @@ -501,6 +501,15 @@ export type ButtonExtensions = {| resume: () => void, |}; +type ShowPayPalAppSwitchOverlay = {| + focus: () => void, + close: () => void, +|}; + +type HidePayPalAppSwitchOverlay = {| + close: () => void, +|}; + export type ButtonProps = {| // app switch properties appSwitchWhenAvailable: string, @@ -515,6 +524,9 @@ export type ButtonProps = {| // Not passed to child iframe visibilityChangeHandler: () => void, + showPayPalAppSwitchOverlay: (args: ShowPayPalAppSwitchOverlay) => void, + hidePayPalAppSwitchOverlay: (args: HidePayPalAppSwitchOverlay) => void, + fundingSource?: ?$Values, intent: $Values, createOrder: CreateOrder, diff --git a/src/ui/overlay/index.jsx b/src/ui/overlay/paypal-app-switch/index.jsx similarity index 100% rename from src/ui/overlay/index.jsx rename to src/ui/overlay/paypal-app-switch/index.jsx diff --git a/src/ui/overlay/paypal-app-switch/overlay.jsx b/src/ui/overlay/paypal-app-switch/overlay.jsx new file mode 100644 index 000000000..ce1ec9999 --- /dev/null +++ b/src/ui/overlay/paypal-app-switch/overlay.jsx @@ -0,0 +1,113 @@ +/* @flow */ +/** @jsx node */ + +import { animate, noop } from "@krakenjs/belter/src"; +import { node, type ElementNode } from "@krakenjs/jsx-pragmatic/src"; +import { LOGO_COLOR, PayPalRebrandLogo } from "@paypal/sdk-logos/src"; +import { type ZalgoPromise } from "@krakenjs/zalgo-promise/src"; + +import { getContainerStyle, getSandboxStyle } from "./style"; + +type OverlayProps = {| + buttonSessionID: string, + close: () => ZalgoPromise, + focus: () => ZalgoPromise, +|}; + +export function PayPalAppSwitchOverlay({ + close, + focus, + buttonSessionID, +}: OverlayProps): ElementNode { + const uid = `paypal-overlay-${buttonSessionID}`; + const overlayIframeName = `__paypal_checkout_sandbox_${uid}__`; + const nonce = ""; + const content = { + windowMessage: "To finish, go back to the PayPal app.", + continueMessage: "Return to PayPal", + }; + + function closeCheckout(e) { + e.preventDefault(); + e.stopPropagation(); + const overlay = document.getElementsByName(uid)?.[0]; + + animate(overlay, "hide-container", noop); + close(); + + if (overlay) { + // the delay is to allow the animation time to run + setTimeout(() => { + overlay.remove(); + }, 300); + } + } + + function focusCheckout(e) { + e.preventDefault(); + e.stopPropagation(); + + focus(); + } + + const setupShowAnimation = () => (el) => { + animate(el, "show-container", noop); + }; + + return ( +
+ + +
+ ); +} diff --git a/src/ui/overlay/paypal-app-switch/style.jsx b/src/ui/overlay/paypal-app-switch/style.jsx new file mode 100644 index 000000000..0c2933525 --- /dev/null +++ b/src/ui/overlay/paypal-app-switch/style.jsx @@ -0,0 +1,167 @@ +/* @flow */ + +export function getSandboxStyle({ uid }: {| uid: string |}): string { + return ` + #${uid}.paypal-checkout-sandbox { + display: block; + position: fixed; + top: 0; + left: 0; + + width: 100%; + height: 100%; + width: 100vw; + height: 100vh; + max-width: 100%; + max-height: 100%; + min-width: 100%; + min-height: 100%; + + z-index: 2147483647; + + animation-duration: 0.3s; + animation-iteration-count: 1; + animation-fill-mode: forwards !important; + opacity: 0; + } + + #${uid}.paypal-checkout-sandbox .paypal-checkout-sandbox-iframe { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + @keyframes show-container { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + @keyframes hide-container { + from { + opacity: 1; + } + + 50% { + opacity: 1; + } + + to { + opacity: 0; + } + } + `; +} + +export function getContainerStyle({ uid }: {| uid: string |}): string { + return ` + #${uid} { + position: absolute; + z-index: 2147483647; + top: 0; + left: 0; + width: 100%; + height: 100%; + + transform: translate3d(0, 0, 0); + + background: radial-gradient(84.48% 50% at 50% 50%, #000 0%, rgba(0, 0, 0, 0.75) 100%); + + color: #fff; + } + + #${uid} a { + color: #fff; + } + + #${uid} .paypal-checkout-close:before, + #${uid} .paypal-checkout-close:after { + background-color: #fff; + } + + #${uid} a { + text-decoration: none; + } + + #${uid} .paypal-checkout-modal { + font-family: PayPalPlain-Regular, system-ui, -apple-system, Roboto, "Segoe UI", Helvetica-Neue, Helvetica, Arial, sans-serif; + font-size: 14px; + text-align: center; + + box-sizing: border-box; + width: 100%; + max-width: 350px; + top: 50%; + left: 50%; + position: absolute; + transform: translateX(-50%) translateY(-50%); + text-align: center; + } + + #${uid}.paypal-overlay-loading .paypal-checkout-message, #${uid}.paypal-overlay-loading .paypal-checkout-continue { + display: none; + } + + #${uid} .paypal-checkout-modal .paypal-checkout-logo { + cursor: pointer; + margin-bottom: 8px; + display: inline-block; + } + + #${uid} .paypal-checkout-modal .paypal-checkout-logo img { + height: 44px; + } + + #${uid} .paypal-checkout-modal .paypal-checkout-message { + font-size: 14px; + line-height: 18px; + padding: 8px 16px; + } + + #${uid} .paypal-checkout-modal .paypal-checkout-continue { + font-size: 14px; + line-height: 18px; + padding: 8px 0; + font-weight: bold; + } + + #${uid} .paypal-checkout-modal .paypal-checkout-continue a { + border-bottom: 1px solid white; + } + + #${uid} .paypal-checkout-close { + position: absolute; + right: 16px; + top: 16px; + width: 24px; + height: 24px; + } + + #${uid}.paypal-overlay-loading .paypal-checkout-close { + display: none; + } + + #${uid} .paypal-checkout-close:before, .paypal-checkout-close:after { + position: absolute; + left: 11px; + content: ' '; + height: 24px; + width: 2px; + } + + #${uid} .paypal-checkout-close:before { + transform: rotate(45deg); + } + + #${uid} .paypal-checkout-close:after { + transform: rotate(-45deg); + } + `; +} diff --git a/src/ui/overlay/three-domain-secure/index.jsx b/src/ui/overlay/three-domain-secure/index.jsx new file mode 100644 index 000000000..34d9db853 --- /dev/null +++ b/src/ui/overlay/three-domain-secure/index.jsx @@ -0,0 +1,3 @@ +/* @flow */ + +export * from "./overlay"; diff --git a/src/ui/overlay/overlay.jsx b/src/ui/overlay/three-domain-secure/overlay.jsx similarity index 100% rename from src/ui/overlay/overlay.jsx rename to src/ui/overlay/three-domain-secure/overlay.jsx diff --git a/src/ui/overlay/style.jsx b/src/ui/overlay/three-domain-secure/style.jsx similarity index 100% rename from src/ui/overlay/style.jsx rename to src/ui/overlay/three-domain-secure/style.jsx diff --git a/src/zoid/buttons/component.jsx b/src/zoid/buttons/component.jsx index 882d5cd4a..545805b11 100644 --- a/src/zoid/buttons/component.jsx +++ b/src/zoid/buttons/component.jsx @@ -88,6 +88,7 @@ import { import { isFundingEligible } from "../../funding"; import { getPixelComponent } from "../pixel"; import { CLASS } from "../../constants"; +import { PayPalAppSwitchOverlay } from "../../ui/overlay/paypal-app-switch/overlay"; import { containerTemplate } from "./container"; import { PrerenderedButtons } from "./prerender"; @@ -303,6 +304,41 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => { required: false, }, + showPayPalAppSwitchOverlay: { + type: "function", + queryParam: false, + value: + ({ props: { buttonSessionID } }) => + ({ close, focus }) => { + const overlay = ( + + ).render(dom({ doc: document })); + + document.body?.appendChild(overlay); + }, + }, + + hidePayPalAppSwitchOverlay: { + type: "function", + queryParam: false, + value: + ({ props: { buttonSessionID } }) => + ({ close }) => { + const overlay = document.getElementsByName( + `paypal-overlay-${buttonSessionID}` + )?.[0]; + + if (overlay) { + close(); + overlay.remove(); + } + }, + }, + redirect: { type: "function", sendToChild: true, @@ -358,7 +394,7 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => { eventName: "paypal-visibilitychange", payload: { url: window.location.href, - // eslint-disable-next-line compat/compat + visibilityState: document.visibilityState, }, }); diff --git a/test/integration/tests/button/index.js b/test/integration/tests/button/index.js index 3fa83eed6..f7ed91cee 100644 --- a/test/integration/tests/button/index.js +++ b/test/integration/tests/button/index.js @@ -17,3 +17,4 @@ import "./clone"; import "./renderOrder"; import "./nonce"; import "./eligibility"; +import "./paypalAppSwitchOverlay"; diff --git a/test/integration/tests/button/paypalAppSwitchOverlay.js b/test/integration/tests/button/paypalAppSwitchOverlay.js new file mode 100644 index 000000000..75e7c834e --- /dev/null +++ b/test/integration/tests/button/paypalAppSwitchOverlay.js @@ -0,0 +1,215 @@ +/* @flow */ +/* eslint max-lines: 0 */ + +import { once, noop } from "@krakenjs/belter/src"; + +import { + createTestContainer, + destroyTestContainer, + getElementRecursive, + assert, +} from "../common"; + +describe(`PayPal app switch overlay`, () => { + beforeEach(() => { + createTestContainer(); + }); + + afterEach(() => { + destroyTestContainer(); + }); + + it("should call showPayPalAppSwitchOverlay and show the overlay", (done) => { + done = once(done); + + window.paypal + .Buttons({ + test: { + flow: "popup", + action: "init", + onRender({ xprops }) { + xprops.showPayPalAppSwitchOverlay({ + close: noop, + focus: noop, + }); + assert.ok(getElementRecursive(".paypal-logo-paypal-rebrand")); + assert.ok(getElementRecursive(".paypal-logo-color-white")); + assert.ok(getElementRecursive(".paypal-checkout-message")); + assert.ok(getElementRecursive(".paypal-checkout-continue")); + + xprops.hidePayPalAppSwitchOverlay({ close: noop }); + + done(); + }, + }, + onApprove(): void { + return done(new Error("Expected onApprove to not be called")); + }, + onCancel(): void { + return done(new Error("Expected onCancel to not be called")); + }, + }) + .render("#testContainer"); + }); + + it("should call showPayPalAppSwitchOverlay then show the overlay and call focus when continue is clicked", (done) => { + done = once(done); + + window.paypal + .Buttons({ + test: { + flow: "popup", + action: "init", + onRender({ xprops }) { + let focusCalled = false; + + xprops.showPayPalAppSwitchOverlay({ + close: noop, + focus: () => { + focusCalled = true; + }, + }); + + getElementRecursive(".paypal-checkout-continue").click(); + + if (!focusCalled) { + done(new Error("Expected focus function to be called")); + } + + xprops.hidePayPalAppSwitchOverlay({ close: noop }); + + done(); + }, + }, + onApprove(): void { + return done(new Error("Expected onApprove to not be called")); + }, + onCancel(): void { + return done(new Error("Expected onCancel to not be called")); + }, + }) + .render("#testContainer"); + }); + + it("should remove the overlay when hidePayPalAppSwitchOverlay", (done) => { + done = once(done); + + window.paypal + .Buttons({ + test: { + flow: "popup", + action: "init", + onRender({ xprops }) { + let closeCalled = false; + const close = () => { + closeCalled = true; + }; + + xprops.showPayPalAppSwitchOverlay({ + close, + focus: noop, + }); + + if (closeCalled) { + done( + new Error("Expected close function to not be called on render") + ); + } + + xprops.hidePayPalAppSwitchOverlay({ close }); + + if (!closeCalled) { + done(new Error("Expected close function to be called")); + } + + // timeout is to allow time for animation to run for overlay removal + setTimeout(() => { + try { + if (getElementRecursive(".paypal-checkout-sandbox")) { + done( + new Error( + "Expected overlay to be removed from dom after close was called" + ) + ); + } + } catch { + // an error will be thrown if the overlay is not found, which means overlay was removed successfully + done(); + } + }, 300); + + done(); + }, + }, + onApprove(): void { + return done(new Error("Expected onApprove to not be called")); + }, + onCancel(): void { + return done(new Error("Expected onCancel to not be called")); + }, + }) + .render("#testContainer"); + }); + + it("should call showPayPalAppSwitchOverlay then show the app switch overlay and call close when X is clicked", (done) => { + done = once(done); + + window.paypal + .Buttons({ + test: { + flow: "popup", + action: "init", + onRender({ xprops }) { + let closeCalled = false; + xprops.showPayPalAppSwitchOverlay({ + close: () => { + closeCalled = true; + }, + focus: noop, + }); + + getElementRecursive(".paypal-checkout-close").click(); + + if (!closeCalled) { + done(new Error("Expected close function to be called")); + } + + // timeout is to allow time for animation to run for overlay removal + setTimeout(() => { + try { + if (getElementRecursive(".paypal-checkout-sandbox")) { + done( + new Error( + "Expected overlay to be removed from dom after close was called" + ) + ); + } + } catch { + // an error will be thrown if the overlay is not found, which means overlay was removed successfully + } + + done(); + }, 300); + }, + }, + onApprove(): void { + return done(new Error("Expected onApprove to not be called")); + }, + onCancel(): void { + return done(new Error("Expected onCancel to not be called")); + }, + }) + .render("#testContainer"); + }); +}); + +// describe.only(`paypal button props - PayPal app switch overlay`, () => { +// beforeEach(() => { +// createTestContainer(); +// }); + +// afterEach(() => { +// destroyTestContainer(); +// }); + +// });