Skip to content

Commit 01a491d

Browse files
feat: app switch resume flow (#2458)
* feat: app switch resume flow * remove passing down the storage id in query params * update bundle size * rename to Pixel Co-authored-by: Shane Brunson <[email protected]> * Update src/lib/appSwitchResume.js Co-authored-by: Shane Brunson <[email protected]> --------- Co-authored-by: Shane Brunson <[email protected]>
1 parent 890d4b0 commit 01a491d

File tree

18 files changed

+855
-2
lines changed

18 files changed

+855
-2
lines changed

Diff for: .bundlemonrc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"path": "size.min.js",
66
"compression": "gzip",
77
"maxPercentIncrease": 3.6,
8-
"maxSize": "79kb"
8+
"maxSize": "81kb"
99
}
1010
],
1111
"reportOutput": [

Diff for: globals.js

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module.exports = {
2323
__URI__: {
2424
__CHECKOUT__: "/checkoutnow",
2525
__BUTTONS__: "/smart/buttons",
26+
__PIXEL__: "/smart/pixel",
2627
__MENU__: "/smart/menu",
2728
__QRCODE__: "/smart/qrcode",
2829
__VENMO__: "/smart/checkout/venmo/popup",

Diff for: src/constants/misc.js

+6
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ export const ATTRIBUTE = {
1717
};
1818

1919
export const DEFAULT = ("default": "default");
20+
21+
export const APP_SWITCH_RETURN_HASH = {
22+
ONAPPROVE: ("onApprove": "onApprove"),
23+
ONCANCEL: ("onCancel": "onCancel"),
24+
ONERROR: ("onError": "onError"),
25+
};

Diff for: src/declarations.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ declare var __PAYPAL_CHECKOUT__: {|
88
__REMEMBERED_FUNDING__: $ReadOnlyArray<$Values<typeof FUNDING>>,
99
__URI__: {|
1010
__BUTTONS__: string,
11+
__PIXEL__: string,
1112
__CHECKOUT__: string,
1213
__CARD_FIELDS__: string,
1314
__CARD_FIELD__: string,

Diff for: src/interface/button.js

+6
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ import {
3333
getModalComponent,
3434
type ModalComponent,
3535
} from "../zoid/modal/component";
36+
import { getPixelComponent, type PixelComponent } from "../zoid/pixel";
3637

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

42+
export const Pixel: LazyExport<PixelComponent> = {
43+
__get__: () => getPixelComponent(),
44+
};
45+
4146
export const Checkout: LazyProtectedExport<CheckoutComponent> = {
4247
__get__: () => protectedExport(getCheckoutComponent()),
4348
};
@@ -93,6 +98,7 @@ export const destroyAll: LazyProtectedExport<typeof destroyComponents> = {
9398
export function setup() {
9499
getButtonsComponent();
95100
getCheckoutComponent();
101+
getPixelComponent();
96102
}
97103

98104
export function destroy(err?: mixed) {

Diff for: src/lib/appSwitchResume.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* @flow */
2+
import { FUNDING } from "@paypal/sdk-constants/src";
3+
4+
import { APP_SWITCH_RETURN_HASH } from "../constants";
5+
6+
export type AppSwitchResumeParams = {|
7+
orderID?: ?string,
8+
buttonSessionID: string,
9+
payerID?: ?string,
10+
billingToken?: ?string,
11+
vaultSetupToken?: ?string,
12+
paymentID?: ?string,
13+
subscriptionID?: ?string,
14+
fundingSource?: ?$Values<typeof FUNDING>,
15+
checkoutState: "onApprove" | "onCancel" | "onError",
16+
|};
17+
18+
export function getAppSwitchResumeParams(): AppSwitchResumeParams | null {
19+
const urlHash = String(window.location.hash).replace("#", "");
20+
const isPostApprovalAction = [
21+
APP_SWITCH_RETURN_HASH.ONAPPROVE,
22+
APP_SWITCH_RETURN_HASH.ONCANCEL,
23+
APP_SWITCH_RETURN_HASH.ONERROR,
24+
].includes(urlHash);
25+
if (!isPostApprovalAction) {
26+
return null;
27+
}
28+
// eslint-disable-next-line compat/compat
29+
const search = new URLSearchParams(window.location.search);
30+
const orderID = search.get("orderID");
31+
const payerID = search.get("payerID");
32+
const buttonSessionID = search.get("buttonSessionID");
33+
const billingToken = search.get("billingToken");
34+
const paymentID = search.get("paymentID");
35+
const subscriptionID = search.get("subscriptionID");
36+
const vaultSetupToken = search.get("vaultSetupToken");
37+
const fundingSource = search.get("fundingSource");
38+
if (buttonSessionID) {
39+
const params: AppSwitchResumeParams = {
40+
orderID,
41+
buttonSessionID,
42+
payerID,
43+
billingToken,
44+
paymentID,
45+
subscriptionID,
46+
// URLSearchParams get returns as string,
47+
// but below code excepts a value from list of string.
48+
// $FlowIgnore[incompatible-type]
49+
fundingSource,
50+
vaultSetupToken,
51+
// the isPostApprovalAction already ensures
52+
// that the function will exit if url hash is not one of supported values.
53+
// $FlowIgnore[incompatible-type]
54+
checkoutState: urlHash,
55+
};
56+
return params;
57+
}
58+
return null;
59+
}
60+
61+
export function isAppSwitchResumeFlow(): boolean {
62+
return Boolean(getAppSwitchResumeParams());
63+
}

Diff for: src/lib/appSwithResume.test.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/* @flow */
2+
3+
import { vi, describe, expect } from "vitest";
4+
5+
import {
6+
isAppSwitchResumeFlow,
7+
getAppSwitchResumeParams,
8+
} from "./appSwitchResume";
9+
10+
describe("app switch resume flow", () => {
11+
afterEach(() => {
12+
vi.clearAllMocks();
13+
vi.unstubAllGlobals();
14+
});
15+
const buttonSessionID = "uid_button_session_123444";
16+
const orderID = "EC-1223114";
17+
const fundingSource = "paypal";
18+
19+
test("should test fetching resume params when its non resume flow", () => {
20+
const params = getAppSwitchResumeParams();
21+
22+
expect.assertions(2);
23+
expect(params).toEqual(null);
24+
expect(isAppSwitchResumeFlow()).toEqual(false);
25+
});
26+
27+
test("should test fetching resume params when parameters are correctly passed", () => {
28+
vi.spyOn(window, "location", "get").mockReturnValue({
29+
hash: "#onApprove",
30+
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}`,
31+
});
32+
33+
const params = getAppSwitchResumeParams();
34+
35+
expect.assertions(2);
36+
expect(params).toEqual({
37+
billingToken: null,
38+
buttonSessionID,
39+
checkoutState: "onApprove",
40+
fundingSource,
41+
orderID,
42+
payerID: null,
43+
paymentID: null,
44+
subscriptionID: null,
45+
vaultSetupToken: null,
46+
});
47+
expect(isAppSwitchResumeFlow()).toEqual(true);
48+
});
49+
50+
test("should test fetching resume params with invalid callback passed", () => {
51+
vi.spyOn(window, "location", "get").mockReturnValue({
52+
hash: "#Unknown",
53+
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}`,
54+
});
55+
56+
const params = getAppSwitchResumeParams();
57+
58+
expect.assertions(2);
59+
expect(params).toEqual(null);
60+
expect(isAppSwitchResumeFlow()).toEqual(false);
61+
});
62+
63+
test("should test null fetching resume params with invalid callback passed", () => {
64+
vi.spyOn(window, "location", "get").mockReturnValue({
65+
hash: "#Unknown",
66+
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}`,
67+
});
68+
69+
const params = getAppSwitchResumeParams();
70+
71+
expect.assertions(2);
72+
expect(params).toEqual(null);
73+
expect(isAppSwitchResumeFlow()).toEqual(false);
74+
});
75+
76+
test("should test fetching resume params when parameters are correctly passed", () => {
77+
vi.spyOn(window, "location", "get").mockReturnValue({
78+
hash: "#onApprove",
79+
search: `buttonSessionID=${buttonSessionID}&orderID=${orderID}&fundingSource=${fundingSource}&billingToken=BA-124&payerID=PP-122&paymentID=PAY-123&subscriptionID=I-1234&vaultSetupToken=VA-3`,
80+
});
81+
82+
const params = getAppSwitchResumeParams();
83+
84+
expect.assertions(2);
85+
expect(params).toEqual({
86+
billingToken: "BA-124",
87+
buttonSessionID,
88+
checkoutState: "onApprove",
89+
fundingSource,
90+
orderID,
91+
payerID: "PP-122",
92+
paymentID: "PAY-123",
93+
subscriptionID: "I-1234",
94+
vaultSetupToken: "VA-3",
95+
});
96+
expect(isAppSwitchResumeFlow()).toEqual(true);
97+
});
98+
});

Diff for: src/lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export * from "./errors";
55
export * from "./isRTLLanguage";
66
export * from "./security";
77
export * from "./session";
8+
export * from "./appSwitchResume";
89
export * from "./perceived-latency-instrumentation";

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

+5
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,11 @@ export type PrerenderDetails = {|
496496

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

499+
export type ButtonExtensions = {|
500+
hasReturned: () => boolean,
501+
resume: () => void,
502+
|};
503+
499504
export type ButtonProps = {|
500505
// app switch properties
501506
appSwitchWhenAvailable: string,

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

+45-1
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,17 @@ import {
7474
sessionState,
7575
logLatencyInstrumentationPhase,
7676
prepareInstrumentationPayload,
77+
isAppSwitchResumeFlow,
78+
getAppSwitchResumeParams,
7779
} from "../../lib";
7880
import {
7981
normalizeButtonStyle,
8082
normalizeButtonMessage,
8183
type ButtonProps,
84+
type ButtonExtensions,
8285
} from "../../ui/buttons/props";
8386
import { isFundingEligible } from "../../funding";
87+
import { getPixelComponent } from "../pixel";
8488
import { CLASS } from "../../constants";
8589

8690
import { containerTemplate } from "./container";
@@ -96,7 +100,12 @@ import {
96100
getModal,
97101
} from "./util";
98102

99-
export type ButtonsComponent = ZoidComponent<ButtonProps>;
103+
export type ButtonsComponent = ZoidComponent<
104+
ButtonProps,
105+
void,
106+
void,
107+
ButtonExtensions
108+
>;
100109

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

108117
domain: getPayPalDomainRegex(),
118+
getExtensions: (parent) => {
119+
return {
120+
hasReturned: () => {
121+
return isAppSwitchResumeFlow();
122+
},
123+
resume: () => {
124+
const resumeFlowParams = getAppSwitchResumeParams();
125+
if (!resumeFlowParams) {
126+
throw new Error("Resume Flow is not supported.");
127+
}
128+
getLogger().metricCounter({
129+
namespace: "resume_flow.init.count",
130+
event: "info",
131+
dimensions: {
132+
orderID: Boolean(resumeFlowParams.orderID),
133+
vaultSessionID: Boolean(resumeFlowParams.vaultSetupToken),
134+
billingToken: Boolean(resumeFlowParams.billingToken),
135+
payerID: Boolean(resumeFlowParams.payerID),
136+
},
137+
});
138+
const resumeComponent = getPixelComponent();
139+
const parentProps = parent.getProps();
140+
resumeComponent({
141+
onApprove: parentProps.onApprove,
142+
// $FlowIgnore[incompatible-call]
143+
onError: parentProps.onError,
144+
// $FlowIgnore[prop-missing] onCancel is incorrectly declared as oncancel in button props
145+
onCancel: parentProps.onCancel,
146+
onClick: parentProps.onClick,
147+
onComplete: parentProps.onComplete,
148+
resumeFlowParams,
149+
}).render("body");
150+
},
151+
};
152+
},
109153

110154
autoResize: {
111155
width: false,

0 commit comments

Comments
 (0)