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: app switch resume flow #2458

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
__URI__: {
__CHECKOUT__: "/checkoutnow",
__BUTTONS__: "/smart/buttons",
__PIXEL__: "/smart/pixel",
__MENU__: "/smart/menu",
__QRCODE__: "/smart/qrcode",
__VENMO__: "/smart/checkout/venmo/popup",
Expand Down
6 changes: 6 additions & 0 deletions src/constants/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ export const ATTRIBUTE = {
};

export const DEFAULT = ("default": "default");

export const APP_SWITCH_RETURN_HASH = {
ONAPPROVE: ("onApprove": "onApprove"),
ONCANCEL: ("onCancel": "onCancel"),
ONERROR: ("onError": "onError"),
};
1 change: 1 addition & 0 deletions src/declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ declare var __PAYPAL_CHECKOUT__: {|
__REMEMBERED_FUNDING__: $ReadOnlyArray<$Values<typeof FUNDING>>,
__URI__: {|
__BUTTONS__: string,
__PIXEL__: string,
__CHECKOUT__: string,
__CARD_FIELDS__: string,
__CARD_FIELD__: string,
Expand Down
6 changes: 6 additions & 0 deletions src/interface/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ import {
getModalComponent,
type ModalComponent,
} from "../zoid/modal/component";
import { getPixelComponent, type PixelComponent } from "../zoid/pixel";

export const Buttons: LazyExport<ButtonsComponent> = {
__get__: () => getButtonsComponent(),
};

export const ResumePixel: LazyExport<PixelComponent> = {
__get__: () => getPixelComponent(),
};

export const Checkout: LazyProtectedExport<CheckoutComponent> = {
__get__: () => protectedExport(getCheckoutComponent()),
};
Expand Down Expand Up @@ -93,6 +98,7 @@ export const destroyAll: LazyProtectedExport<typeof destroyComponents> = {
export function setup() {
getButtonsComponent();
getCheckoutComponent();
getPixelComponent();
}

export function destroy(err?: mixed) {
Expand Down
64 changes: 64 additions & 0 deletions src/lib/appSwitchResume.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* @flow */
import { FUNDING } from "@paypal/sdk-constants/src";

import { APP_SWITCH_RETURN_HASH } from "../constants";

export type AppSwitchResumeParams = {|
orderID?: ?string,
buttonSessionID: string,
payerID?: ?string,
billingToken?: ?string,
vaultSetupToken?: ?string,
paymentID?: ?string,
subscriptionID?: ?string,
fundingSource?: ?$Values<typeof FUNDING>,
checkoutState: "onApprove" | "onCancel" | "onError",
|};

export function getAppSwitchResumeParams(): AppSwitchResumeParams | null {
const urlHash = String(window.location.hash).replace("#", "");
const isPostApprovalAction = [
APP_SWITCH_RETURN_HASH.ONAPPROVE,
APP_SWITCH_RETURN_HASH.ONCANCEL,
APP_SWITCH_RETURN_HASH.ONERROR,
].includes(urlHash);
if (!isPostApprovalAction) {
return null;
}
// eslint-disable-next-line compat/compat
const search = new URLSearchParams(window.location.search);
const orderID = search.get("orderID");
const payerID = search.get("payerID");
const buttonSessionID = search.get("buttonSessionID");
const billingToken = search.get("billingToken");
const paymentID = search.get("paymentID");
const subscriptionID = search.get("subscriptionID");
const vaultSetupToken = search.get("vaultSetupToken");
const fundingSource = search.get("fundingSource");
if (buttonSessionID) {
const params: AppSwitchResumeParams = {
orderID,
buttonSessionID,
payerID,
billingToken,
paymentID,
subscriptionID,
// URLSearchParams get returns as string,
// but below code excepts a value from list of string.
// $FlowIgnore[incompatible-type]
fundingSource,
vaultSetupToken,
// the isPostApprovalAction already ensures
// that the function will exit if url hash is not one of supported values.
// $FlowIgnore[incompatible-type]
checkoutState: urlHash,
};
return params;
}
return null;
}

export function isAppSwitchResumeFlow(): boolean {
const params = getAppSwitchResumeParams();
return params !== null;
}
98 changes: 98 additions & 0 deletions src/lib/appSwithResume.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* @flow */

import { vi, describe, expect } from "vitest";

import {
isAppSwitchResumeFlow,
getAppSwitchResumeParams,
} from "./appSwitchResume";

describe("app switch resume flow", () => {
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
const buttonSessionID = "uid_button_session_123444";
const orderID = "EC-1223114";
const fundingSource = "paypal";

test("should test fetching resume params when its non resume flow", () => {
const params = getAppSwitchResumeParams();

expect.assertions(2);
expect(params).toEqual(null);
expect(isAppSwitchResumeFlow()).toEqual(false);
});

test("should test fetching resume params when parameters are correctly passed", () => {
vi.spyOn(window, "location", "get").mockReturnValue({
hash: "#onApprove",
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}`,
});

const params = getAppSwitchResumeParams();

expect.assertions(2);
expect(params).toEqual({
billingToken: null,
buttonSessionID,
checkoutState: "onApprove",
fundingSource,
orderID,
payerID: null,
paymentID: null,
subscriptionID: null,
vaultSetupToken: null,
});
expect(isAppSwitchResumeFlow()).toEqual(true);
});

test("should test fetching resume params with invalid callback passed", () => {
vi.spyOn(window, "location", "get").mockReturnValue({
hash: "#Unknown",
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}`,
});

const params = getAppSwitchResumeParams();

expect.assertions(2);
expect(params).toEqual(null);
expect(isAppSwitchResumeFlow()).toEqual(false);
});

test("should test null fetching resume params with invalid callback passed", () => {
vi.spyOn(window, "location", "get").mockReturnValue({
hash: "#Unknown",
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}`,
});

const params = getAppSwitchResumeParams();

expect.assertions(2);
expect(params).toEqual(null);
expect(isAppSwitchResumeFlow()).toEqual(false);
});

test("should test fetching resume params when parameters are correctly passed", () => {
vi.spyOn(window, "location", "get").mockReturnValue({
hash: "#onApprove",
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}&billingToken=BA-124&payerID=PP-122&paymentID=PAY-123&subscriptionID=I-1234&vaultSetupToken=VA-3`,
});

const params = getAppSwitchResumeParams();

expect.assertions(2);
expect(params).toEqual({
billingToken: "BA-124",
buttonSessionID,
checkoutState: "onApprove",
fundingSource,
orderID,
payerID: "PP-122",
paymentID: "PAY-123",
subscriptionID: "I-1234",
vaultSetupToken: "VA-3",
});
expect(isAppSwitchResumeFlow()).toEqual(true);
});
});
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from "./errors";
export * from "./isRTLLanguage";
export * from "./security";
export * from "./session";
export * from "./appSwitchResume";
export * from "./perceived-latency-instrumentation";
5 changes: 5 additions & 0 deletions src/ui/buttons/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,11 @@ export type PrerenderDetails = {|

export type GetPrerenderDetails = () => PrerenderDetails | void;

export type ButtonExtensions = {|
hasReturned: () => boolean,
resume: () => void,
|};

export type ButtonProps = {|
// app switch properties
appSwitchWhenAvailable: string,
Expand Down
46 changes: 45 additions & 1 deletion src/zoid/buttons/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ import {
sessionState,
logLatencyInstrumentationPhase,
prepareInstrumentationPayload,
isAppSwitchResumeFlow,
getAppSwitchResumeParams,
} from "../../lib";
import {
normalizeButtonStyle,
normalizeButtonMessage,
type ButtonProps,
type ButtonExtensions,
} from "../../ui/buttons/props";
import { isFundingEligible } from "../../funding";
import { getPixelComponent } from "../pixel";
import { CLASS } from "../../constants";

import { containerTemplate } from "./container";
Expand All @@ -96,7 +100,12 @@ import {
getModal,
} from "./util";

export type ButtonsComponent = ZoidComponent<ButtonProps>;
export type ButtonsComponent = ZoidComponent<
ButtonProps,
void,
void,
ButtonExtensions
>;

export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
const queriedEligibleFunding = [];
Expand All @@ -106,6 +115,41 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
url: () => `${getPayPalDomain()}${__PAYPAL_CHECKOUT__.__URI__.__BUTTONS__}`,

domain: getPayPalDomainRegex(),
getExtensions: (parent) => {
return {
hasReturned: () => {
return isAppSwitchResumeFlow();
},
resume: () => {
const resumeFlowParams = getAppSwitchResumeParams();
if (!resumeFlowParams) {
throw new Error("Resume Flow is not supported.");
}
getLogger().metricCounter({
namespace: "resume_flow.init.count",
event: "info",
dimensions: {
orderID: Boolean(resumeFlowParams.orderID),
vaultSessionID: Boolean(resumeFlowParams.vaultSetupToken),
billingToken: Boolean(resumeFlowParams.billingToken),
payerID: Boolean(resumeFlowParams.payerID),
},
});
const resumeComponent = getPixelComponent();
const parentProps = parent.getProps();
resumeComponent({
onApprove: parentProps.onApprove,
// $FlowIgnore[incompatible-call]
onError: parentProps.onError,
// $FlowIgnore[prop-missing] onCancel is incorrectly declared as oncancel in button props
onCancel: parentProps.onCancel,
onClick: parentProps.onClick,
onComplete: parentProps.onComplete,
resumeFlowParams,
}).render("body");
},
};
},

autoResize: {
width: false,
Expand Down
Loading
Loading