Skip to content

Add PayPal App Switch Overlay #2484

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 6 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all 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/three-domain-secure/utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
12 changes: 12 additions & 0 deletions src/ui/buttons/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,15 @@ export type ButtonExtensions = {|
resume: () => void,
|};

type ShowPayPalAppSwitchOverlay = {|
Copy link
Contributor

Choose a reason for hiding this comment

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

we also need to accept the universal link to render the continue link to app.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added that logic in the focus function in setupOverlays

I can update the focus function to take the args props.redirect & url.href and call props.redirect(url.href) from inside the overlay component

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ravishekhar , I attempted a solution that took the url as an argument and ran into issues running the karma tests with the redirect call now being a part of overlay.jsx

With the Karma framework, there is no way to mock location.href or location.redirect. When the redirect is called, the karma test runner is interrupted and the tests fail.

focus: () => void,
close: () => void,
|};

type HidePayPalAppSwitchOverlay = {|
close: () => void,
|};

export type ButtonProps = {|
// app switch properties
appSwitchWhenAvailable: string,
Expand All @@ -515,6 +524,9 @@ export type ButtonProps = {|
// Not passed to child iframe
visibilityChangeHandler: () => void,

showPayPalAppSwitchOverlay: (args: ShowPayPalAppSwitchOverlay) => void,
hidePayPalAppSwitchOverlay: (args: HidePayPalAppSwitchOverlay) => void,

fundingSource?: ?$Values<typeof FUNDING>,
intent: $Values<typeof INTENT>,
createOrder: CreateOrder,
Expand Down
File renamed without changes.
113 changes: 113 additions & 0 deletions src/ui/overlay/paypal-app-switch/overlay.jsx
Original file line number Diff line number Diff line change
@@ -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<void>,
focus: () => ZalgoPromise<void>,
|};

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 (
<div
id={uid}
name={uid}
onRender={setupShowAnimation()}
class="paypal-checkout-sandbox"
>
<style nonce={nonce}>{getSandboxStyle({ uid })}</style>
<iframe
title="PayPal Checkout Overlay"
name={overlayIframeName}
scrolling="no"
class="paypal-checkout-sandbox-iframe"
>
<html>
<body>
<div
dir="auto"
id={uid}
onClick={focusCheckout}
class="paypal-overlay-context-popup paypal-checkout-overlay"
>
<a
href="#"
class="paypal-checkout-close"
onClick={closeCheckout}
aria-label="close"
role="button"
/>
<div class="paypal-checkout-modal">
<div class="paypal-checkout-logo" dir="ltr">
<PayPalRebrandLogo logoColor={LOGO_COLOR.WHITE} />
</div>
{content.windowMessage && (
<div class="paypal-checkout-message">
{content.windowMessage}
</div>
)}
{content.continueMessage && (
<div class="paypal-checkout-continue">
{/* This handler should be guarded with e.stopPropagation.
This will stop the event from bubbling up to the overlay click handler
and causing unexpected behavior. */}
<a onClick={focusCheckout} href="#">
{content.continueMessage}
</a>
</div>
)}
</div>
<style nonce={nonce}>{getContainerStyle({ uid })}</style>
</div>
</body>
</html>
</iframe>
</div>
);
}
167 changes: 167 additions & 0 deletions src/ui/overlay/paypal-app-switch/style.jsx
Original file line number Diff line number Diff line change
@@ -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);
}
`;
}
3 changes: 3 additions & 0 deletions src/ui/overlay/three-domain-secure/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* @flow */

export * from "./overlay";
38 changes: 37 additions & 1 deletion src/zoid/buttons/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -303,6 +304,41 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
required: false,
},

showPayPalAppSwitchOverlay: {
type: "function",
queryParam: false,
value:
({ props: { buttonSessionID } }) =>
({ close, focus }) => {
const overlay = (
<PayPalAppSwitchOverlay
buttonSessionID={buttonSessionID}
close={close}
focus={focus}
/>
).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,
Expand Down Expand Up @@ -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,
},
});
Expand Down
1 change: 1 addition & 0 deletions test/integration/tests/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ import "./clone";
import "./renderOrder";
import "./nonce";
import "./eligibility";
import "./paypalAppSwitchOverlay";
Loading