@paypal/checkout-components
Advanced tools
Comparing version 5.0.316 to 5.0.317
{ | ||
"name": "@paypal/checkout-components", | ||
"version": "5.0.316", | ||
"version": "5.0.317", | ||
"description": "PayPal Checkout components, for integrating checkout products.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
/* @flow */ | ||
import { getLogger } from "@paypal/sdk-client/src"; | ||
import { getButtonsComponent } from "../zoid/buttons"; | ||
@@ -13,6 +10,7 @@ | ||
getMerchantID, | ||
shouldRenderSDKButtons, | ||
getFlexDirection, | ||
appendButtonContainer, | ||
getButtonColor, | ||
applyContainerStyles, | ||
renderStandaloneButton, | ||
renderDefaultButton, | ||
getDefaultButtonOptions, | ||
} from "./utils"; | ||
@@ -23,2 +21,3 @@ import type { | ||
HostedButtonsInstance, | ||
HostedButtonOptions, | ||
} from "./types"; | ||
@@ -30,3 +29,2 @@ | ||
hostedButtonId, | ||
fundingSources = [], | ||
}: HostedButtonsComponentProps): HostedButtonsInstance { | ||
@@ -36,5 +34,11 @@ const Buttons = getButtonsComponent(); | ||
const merchantId = getMerchantID(); | ||
const { html, htmlScript, style } = await getHostedButtonDetails({ | ||
const { | ||
html, | ||
htmlScript, | ||
style, | ||
version, | ||
preferences, | ||
buttonContainerId, | ||
} = await getHostedButtonDetails({ | ||
hostedButtonId, | ||
fundingSources, | ||
}); | ||
@@ -54,2 +58,3 @@ | ||
}); | ||
const onApprove = buildHostedButtonOnApprove({ | ||
@@ -61,3 +66,3 @@ enableDPoP, | ||
const buttonOptions = { | ||
const buttonOptions: HostedButtonOptions = { | ||
createOrder, | ||
@@ -72,26 +77,21 @@ hostedButtonId, | ||
if (shouldRenderSDKButtons(fundingSources)) { | ||
if (version === "2") { | ||
const { flexDirection } = getFlexDirection({ ...style }); | ||
appendButtonContainer({ flexDirection, selector }); | ||
applyContainerStyles({ flexDirection, buttonContainerId }); | ||
// Only render 2 buttons max | ||
// This will be refactored in https://paypal.atlassian.net/browse/DTPPCPSDK-2112 when NCPS team updates their API response | ||
fundingSources.slice(0, 2).forEach((fundingSource, index) => { | ||
// $FlowFixMe | ||
const standaloneButton = Buttons({ | ||
...buttonOptions, | ||
fundingSource, | ||
style: { | ||
...style, | ||
color: getButtonColor(style.color, fundingSource), | ||
}, | ||
}); | ||
if (standaloneButton.isEligible()) { | ||
standaloneButton.render( | ||
index === 0 ? "#ncp-primary-button" : "#ncp-secondary-button" | ||
); | ||
preferences?.buttonPreferences.forEach((fundingSource) => { | ||
if (fundingSource === "default") { | ||
const eligibleDefaultButtons = getDefaultButtonOptions(preferences); | ||
renderDefaultButton({ | ||
eligibleDefaultButtons, | ||
buttonContainerId, | ||
buttonOptions, | ||
}); | ||
} else { | ||
getLogger().error(`ncps_standalone_${fundingSource}_ineligible`); | ||
renderStandaloneButton({ | ||
fundingSource, | ||
buttonContainerId, | ||
buttonOptions, | ||
}); | ||
} | ||
@@ -98,0 +98,0 @@ }); |
@@ -9,2 +9,4 @@ /* @flow */ | ||
import { renderDefaultButton, renderStandaloneButton } from "./utils"; | ||
import { getHostedButtonsComponent } from "."; | ||
@@ -35,40 +37,52 @@ | ||
const getHostedButtonDetailsResponse = { | ||
body: { | ||
button_details: { | ||
link_variables: [ | ||
{ | ||
name: "shape", | ||
value: "rect", | ||
}, | ||
{ | ||
name: "layout", | ||
value: "vertical", | ||
}, | ||
{ | ||
name: "color", | ||
value: "gold", | ||
}, | ||
{ | ||
name: "button_text", | ||
value: "paypal", | ||
}, | ||
{ | ||
name: "button_type", | ||
value: "FIXED_PRICE", | ||
}, | ||
{ | ||
name: "tagline", | ||
value: "true", | ||
}, | ||
{ | ||
name: "height", | ||
value: "40", | ||
}, | ||
], | ||
}, | ||
vi.mock("./utils.js", async () => { | ||
return { | ||
...(await vi.importActual("./utils.js")), | ||
renderStandaloneButton: vi.fn(), | ||
renderDefaultButton: vi.fn(), | ||
}; | ||
}); | ||
const baseLinkVariables = [ | ||
{ | ||
name: "shape", | ||
value: "rect", | ||
}, | ||
}; | ||
{ | ||
name: "layout", | ||
value: "vertical", | ||
}, | ||
{ | ||
name: "color", | ||
value: "gold", | ||
}, | ||
{ | ||
name: "button_text", | ||
value: "paypal", | ||
}, | ||
{ | ||
name: "button_type", | ||
value: "FIXED_PRICE", | ||
}, | ||
{ | ||
name: "tagline", | ||
value: "true", | ||
}, | ||
{ | ||
name: "height", | ||
value: "40", | ||
}, | ||
]; | ||
describe("HostedButtons", () => { | ||
const hostedButtonId = "B1234567890"; | ||
describe("HostedButtons v1", () => { | ||
const hostedButtonDetailsResponse = { | ||
body: { | ||
button_details: { | ||
link_variables: baseLinkVariables, | ||
}, | ||
}, | ||
}; | ||
test("paypal.Buttons calls getHostedButtonDetails and invokes v5 of the SDK", async () => { | ||
@@ -82,11 +96,11 @@ const Buttons = vi.fn(() => ({ render: vi.fn() })); | ||
// eslint-disable-next-line compat/compat | ||
Promise.resolve(getHostedButtonDetailsResponse) | ||
Promise.resolve(hostedButtonDetailsResponse) | ||
); | ||
await HostedButtons({ | ||
hostedButtonId: "B1234567890", | ||
fundingSources: [], | ||
hostedButtonId, | ||
}).render("#example"); | ||
expect(Buttons).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
hostedButtonId: "B1234567890", | ||
hostedButtonId, | ||
style: expect.objectContaining({ tagline: true }), | ||
@@ -99,40 +113,2 @@ }) | ||
describe("NCP V2", () => { | ||
beforeEach(() => { | ||
const containerId = "#container-id"; | ||
const selector = document.createElement("div"); | ||
selector.setAttribute("id", containerId.slice(1)); | ||
vi.spyOn(document, "querySelector").mockReturnValue(selector); | ||
}); | ||
test("paypal.Buttons calls getHostedButtonDetails, invokes v5 of the SDK", async () => { | ||
const renderMock = vi.fn(); | ||
const Buttons = vi.fn(() => ({ | ||
render: renderMock, | ||
isEligible: vi.fn(() => true), | ||
})); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementationOnce(() => Buttons); | ||
const HostedButtons = getHostedButtonsComponent(); | ||
// $FlowIssue | ||
request.mockImplementationOnce(() => | ||
// eslint-disable-next-line compat/compat | ||
Promise.resolve(getHostedButtonDetailsResponse) | ||
); | ||
await HostedButtons({ | ||
hostedButtonId: "B1234567890", | ||
fundingSources: ["paypal", "venmo"], | ||
}).render("#example"); | ||
expect(Buttons).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
hostedButtonId: "B1234567890", | ||
}) | ||
); | ||
expect(Buttons).toHaveBeenCalledTimes(2); | ||
expect(renderMock).toHaveBeenCalledTimes(2); | ||
expect.assertions(3); | ||
}); | ||
}); | ||
test("only eligible buttons are rendered", async () => { | ||
@@ -151,15 +127,15 @@ const renderMock = vi.fn(); | ||
// eslint-disable-next-line compat/compat | ||
Promise.resolve(getHostedButtonDetailsResponse) | ||
Promise.resolve(hostedButtonDetailsResponse) | ||
); | ||
await HostedButtons({ | ||
hostedButtonId: "B1234567890", | ||
fundingSources: ["paypal", "venmo"], | ||
hostedButtonId, | ||
}).render("#example"); | ||
expect(Buttons).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
hostedButtonId: "B1234567890", | ||
hostedButtonId, | ||
}) | ||
); | ||
expect(Buttons).toHaveBeenCalledTimes(2); | ||
expect(renderMock).toHaveBeenCalledTimes(0); | ||
expect(Buttons).toHaveBeenCalledTimes(1); | ||
expect(renderMock).toHaveBeenCalledTimes(1); | ||
expect.assertions(3); | ||
@@ -215,7 +191,8 @@ }); | ||
await HostedButtons({ | ||
hostedButtonId: "B1234567890", | ||
hostedButtonId, | ||
}).render("#example"); | ||
expect(Buttons).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
hostedButtonId: "B1234567890", | ||
hostedButtonId, | ||
style: expect.objectContaining({ tagline: false }), | ||
@@ -229,2 +206,66 @@ }) | ||
}); | ||
describe("HostedButtons v2", () => { | ||
const hostedButtonDetailsResponse = { | ||
body: { | ||
version: "2", | ||
button_details: { | ||
link_variables: baseLinkVariables, | ||
js_sdk_container_id: "spb-container", | ||
preferences: { | ||
button_preferences: ["paypal", "default"], | ||
eligible_funding_methods: ["paypal", "venmo", "paylater"], | ||
}, | ||
}, | ||
}, | ||
}; | ||
beforeEach(() => { | ||
vi.restoreAllMocks(); | ||
const selector = document.createElement("div"); | ||
selector.id = | ||
hostedButtonDetailsResponse.body.button_details.js_sdk_container_id; | ||
vi.spyOn(document, "querySelector").mockReturnValue(selector); | ||
}); | ||
test("paypal.HostedButtons calls renderStandaloneButton & renderDefaultButton accordingly", async () => { | ||
const renderMock = vi.fn(); | ||
const Buttons = vi.fn(() => ({ | ||
render: renderMock, | ||
isEligible: vi.fn(() => false), | ||
})); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementation(() => Buttons); | ||
const HostedButtons = getHostedButtonsComponent(); | ||
// $FlowIssue | ||
request.mockImplementationOnce(() => | ||
// eslint-disable-next-line compat/compat | ||
Promise.resolve(hostedButtonDetailsResponse) | ||
); | ||
await HostedButtons({ | ||
hostedButtonId, | ||
}).render("#example"); | ||
expect(renderStandaloneButton).toHaveBeenCalledTimes(1); | ||
expect(renderDefaultButton).toHaveBeenCalledTimes(1); | ||
expect(renderStandaloneButton).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
fundingSource: "paypal", | ||
}) | ||
); | ||
expect(renderDefaultButton).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
eligibleDefaultButtons: ["venmo", "paylater"], | ||
}) | ||
); | ||
}); | ||
}); | ||
/* eslint-enable no-restricted-globals, promise/no-native */ |
/* @flow */ | ||
/* eslint-disable no-restricted-globals, promise/no-native */ | ||
import { FUNDING } from "@paypal/sdk-constants/src"; | ||
@@ -8,2 +9,14 @@ export type Color = string; | ||
export type CreateOrder = (data: {| | ||
paymentSource: string, | ||
|}) => Promise<string | void>; | ||
export type OnApprove = (data: {| | ||
orderID: string, | ||
paymentSource: string, | ||
|}) => Promise<mixed>; | ||
type OnInit = (data: mixed, actions: mixed) => void; | ||
type OnClick = (data: mixed, actions: mixed) => void; | ||
export type FundingSources = string; | ||
@@ -23,2 +36,57 @@ export interface GetFlexDirection { | ||
export type ApplyButtonStylesProps = {| | ||
flexDirection: FlexDirection, | ||
buttonContainerId: string, | ||
|}; | ||
export type HostedButtonStyles = {| | ||
layout: string, | ||
shape: string, | ||
color: string, | ||
label: string, | ||
height: ?number, | ||
tagline: boolean, | ||
|}; | ||
export type HostedButtonOptions = {| | ||
createOrder: CreateOrder, | ||
onApprove: OnApprove, | ||
onClick: OnClick, | ||
onInit: OnInit, | ||
style: HostedButtonStyles, | ||
hostedButtonId: string, | ||
merchantId?: string, | ||
|}; | ||
export type ButtonPreferences = $ReadOnlyArray< | ||
$Values<typeof FUNDING> | "default" | ||
>; | ||
export type HostedButtonPreferences = {| | ||
buttonPreferences: ButtonPreferences, | ||
eligibleFundingMethods: $ReadOnlyArray<$Values<typeof FUNDING>>, | ||
|}; | ||
export type NcpResponsePreferences = {| | ||
button_preferences: ButtonPreferences, | ||
eligible_funding_methods: $ReadOnlyArray<$Values<typeof FUNDING>>, | ||
|}; | ||
export type GetButtonsProps = {| | ||
fundingSource: FundingSources, | ||
buttonOptions: HostedButtonOptions, | ||
|}; | ||
export type RenderStandaloneButtonProps = {| | ||
fundingSource: FundingSources, | ||
buttonContainerId: string, | ||
buttonOptions: HostedButtonOptions, | ||
|}; | ||
export type RenderDefaultButtonProps = {| | ||
eligibleDefaultButtons: $ReadOnlyArray<FundingSources>, | ||
buttonContainerId: string, | ||
buttonOptions: HostedButtonOptions, | ||
|}; | ||
export type HostedButtonsComponentProps = {| | ||
@@ -39,9 +107,2 @@ hostedButtonId: string, | ||
export type EligibleHostedButtons = "paypal" | "venmo" | "paylater"; | ||
export type HostedButtonPreferences = {| | ||
buttonPreferences: $ReadOnlyArray<EligibleHostedButtons & "default">, | ||
eligibleFundingMethods: $ReadOnlyArray<EligibleHostedButtons>, | ||
|}; | ||
export type HostedButtonDetailsParams = | ||
@@ -51,12 +112,5 @@ (HostedButtonsComponentProps) => Promise<{| | ||
htmlScript: string, | ||
style: {| | ||
layout: string, | ||
shape: string, | ||
color: string, | ||
label: string, | ||
height: ?number, | ||
tagline: boolean, | ||
|}, | ||
style: HostedButtonStyles, | ||
version: ?string, | ||
buttonContainerId: ?string, | ||
buttonContainerId: string, | ||
preferences?: HostedButtonPreferences, | ||
@@ -70,11 +124,2 @@ |}>; | ||
export type CreateOrder = (data: {| | ||
paymentSource: string, | ||
|}) => Promise<string | void>; | ||
export type OnApprove = (data: {| | ||
orderID: string, | ||
paymentSource: string, | ||
|}) => Promise<mixed>; | ||
export type CreateAccessToken = ({| | ||
@@ -94,6 +139,6 @@ clientId: string, | ||
|}) => {| | ||
onInit: (data: mixed, actions: mixed) => void, | ||
onClick: (data: mixed, actions: mixed) => void, | ||
onInit: OnInit, | ||
onClick: OnClick, | ||
|}; | ||
/* eslint-enable no-restricted-globals, promise/no-native */ |
@@ -5,2 +5,3 @@ /* @flow */ | ||
import { | ||
getLogger, | ||
buildDPoPHeaders, | ||
@@ -12,3 +13,7 @@ getSDKHost, | ||
import { FUNDING } from "@paypal/sdk-constants/src"; | ||
import { SUPPORTED_FUNDING_SOURCES } from "@paypal/funding-components/src"; | ||
import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; | ||
import { getButtonsComponent, type ButtonsComponent } from "../zoid/buttons"; | ||
import type { | ||
@@ -24,5 +29,11 @@ ButtonVariables, | ||
GetFlexDirection, | ||
BuildButtonContainerArgs, | ||
Color, | ||
FundingSources, | ||
ApplyButtonStylesProps, | ||
HostedButtonPreferences, | ||
NcpResponsePreferences, | ||
ButtonPreferences, | ||
GetButtonsProps, | ||
RenderStandaloneButtonProps, | ||
RenderDefaultButtonProps, | ||
} from "./types"; | ||
@@ -47,2 +58,3 @@ | ||
if (merchantIds.length > 1) { | ||
getLogger().error("ncps_multiple_merchant_ids", { merchantIds }); | ||
throw new Error("Multiple merchant-ids are not supported."); | ||
@@ -84,2 +96,37 @@ } | ||
export const getButtonPreferences = ({ | ||
button_preferences: buttonPreferences, | ||
eligible_funding_methods: eligibleFundingMethods, | ||
}: NcpResponsePreferences): HostedButtonPreferences => { | ||
if (!buttonPreferences?.length || !eligibleFundingMethods?.length) { | ||
const preferences = { | ||
buttonPreferences, | ||
eligibleFundingMethods, | ||
}; | ||
getLogger().error("ncps_missing_preferences", { preferences }); | ||
throw new Error( | ||
`Expected preferences to be populated, received: ${JSON.stringify({ | ||
preferences, | ||
})}` | ||
); | ||
} | ||
return { | ||
// Remove any buttons that are not included in eligibleFundingMethods. | ||
// If the funding method is "default", we want to keep it in the preferences, and decide which | ||
// button should be rendered in its place in renderStandaloneButton() | ||
buttonPreferences: buttonPreferences.filter( | ||
(fundingMethod) => | ||
eligibleFundingMethods.includes(fundingMethod) || | ||
fundingMethod === "default" | ||
), | ||
// Sort the eligible funding methods returned from /ncp/api/form-fields in the order that they would appear in the smart stack. | ||
eligibleFundingMethods: SUPPORTED_FUNDING_SOURCES.filter((fundingMethod) => | ||
eligibleFundingMethods.includes(fundingMethod) | ||
), | ||
}; | ||
}; | ||
const getButtonVariable = (variables: ButtonVariables, key: string): string => | ||
@@ -98,4 +145,10 @@ variables?.find((variable) => variable.name === key)?.value ?? ""; | ||
const { body } = response; | ||
const { link_variables: variables, preferences } = body.button_details; | ||
const { | ||
link_variables: variables, | ||
js_sdk_container_id: buttonContainerId, | ||
preferences, | ||
} = body.button_details; | ||
const shouldIncludePreferences = preferences && body.version === "2"; | ||
return { | ||
@@ -111,10 +164,7 @@ style: { | ||
version: body.version, | ||
buttonContainerId: body.button_container_id, | ||
buttonContainerId: buttonContainerId || "spb-container", | ||
html: body.html, | ||
htmlScript: body.html_script, | ||
...(preferences && { | ||
preferences: { | ||
buttonPreferences: preferences.button_preferences, | ||
eligibleFundingMethods: preferences.eligible_funding_methods, | ||
}, | ||
...(shouldIncludePreferences && { | ||
preferences: getButtonPreferences(preferences), | ||
}), | ||
@@ -260,12 +310,12 @@ }; | ||
export function getFlexDirection({ | ||
export const getFlexDirection = ({ | ||
layout, | ||
}: GetFlexDirectionArgs): GetFlexDirection { | ||
return { flexDirection: layout === "horizontal" ? "row" : "column" }; | ||
} | ||
}: GetFlexDirectionArgs): GetFlexDirection => ({ | ||
flexDirection: layout === "horizontal" ? "row" : "column", | ||
}); | ||
export function getButtonColor( | ||
export const getButtonColor = ( | ||
color: Color, | ||
fundingSource: FundingSources | ||
): Color { | ||
): Color => { | ||
const colorMap = { | ||
@@ -300,39 +350,108 @@ gold: { | ||
return colorMap[color][fundingSource]; | ||
} | ||
}; | ||
export function shouldRenderSDKButtons( | ||
fundingSources: $ReadOnlyArray<FundingSources> | ||
): boolean { | ||
return Boolean(fundingSources.length); | ||
} | ||
export function appendButtonContainer({ | ||
export const applyContainerStyles = ({ | ||
flexDirection, | ||
selector, | ||
}: BuildButtonContainerArgs) { | ||
const elm = getElementFromSelector(selector); | ||
buttonContainerId, | ||
}: ApplyButtonStylesProps): void => { | ||
const buttonContainer = document.querySelector(`#${buttonContainerId}`); | ||
if (!elm) { | ||
throw new Error("PayPal button container selector was not found"); | ||
if (!buttonContainer) { | ||
getLogger().error("ncps_button_container_missing", { | ||
buttonContainerId, | ||
}); | ||
throw new Error(`Element with id ${buttonContainerId} not found.`); | ||
} | ||
const buttonContainer = document.createElement("div"); | ||
buttonContainer.style.flexDirection = flexDirection; | ||
}; | ||
buttonContainer.setAttribute( | ||
"style", | ||
`display: flex; flex-wrap: nowrap; gap: 16px; max-width: 750px; flex-direction: ${flexDirection}` | ||
/** | ||
* Filters out all eligible funding methods that are already specified in button preferences | ||
*/ | ||
export const getDefaultButtonOptions = ({ | ||
buttonPreferences, | ||
eligibleFundingMethods, | ||
}: HostedButtonPreferences): ButtonPreferences => { | ||
return eligibleFundingMethods.filter( | ||
(fundingSource: string) => !buttonPreferences.includes(fundingSource) | ||
); | ||
}; | ||
const primaryButton = document.createElement("div"); | ||
primaryButton.setAttribute("id", `ncp-primary-button`); | ||
primaryButton.setAttribute("style", "flex-grow: 1"); | ||
/** | ||
* Gets buttons component instance. | ||
*/ | ||
export const getButtons = ({ | ||
fundingSource, | ||
buttonOptions, | ||
}: GetButtonsProps): ButtonsComponent => { | ||
const Buttons = getButtonsComponent(); | ||
const secondaryButton = document.createElement("div"); | ||
secondaryButton.setAttribute("id", `ncp-secondary-button`); | ||
secondaryButton.setAttribute("style", "flex-grow: 1"); | ||
const { style } = buttonOptions; | ||
buttonContainer.appendChild(primaryButton); | ||
buttonContainer.appendChild(secondaryButton); | ||
// $FlowFixMe | ||
return Buttons({ | ||
...buttonOptions, | ||
fundingSource, | ||
style: { | ||
...style, | ||
// $FlowFixMe | ||
color: getButtonColor(style.color, fundingSource), | ||
}, | ||
}); | ||
}; | ||
elm?.appendChild(buttonContainer); | ||
} | ||
/** | ||
* Handles logic for each specified button preference. | ||
*/ | ||
export const renderStandaloneButton = ({ | ||
fundingSource, | ||
buttonContainerId, | ||
buttonOptions, | ||
}: RenderStandaloneButtonProps): ZalgoPromise<void> | void => { | ||
const standaloneButton = getButtons({ | ||
fundingSource, | ||
buttonOptions, | ||
}); | ||
// $FlowFixMe | ||
if (standaloneButton.isEligible()) { | ||
// $FlowFixMe | ||
return standaloneButton.render(`#${buttonContainerId}`); | ||
} | ||
getLogger().error(`ncps_standalone_${fundingSource}_ineligible`); | ||
}; | ||
/** | ||
* Handles logic for "default" button preference. | ||
*/ | ||
export const renderDefaultButton = ({ | ||
eligibleDefaultButtons, | ||
buttonContainerId, | ||
buttonOptions, | ||
}: RenderDefaultButtonProps): void => { | ||
const eligibleButtons = [...eligibleDefaultButtons]; | ||
// If we exhaust all default options, we don't render any button. | ||
while (eligibleButtons.length) { | ||
const fundingSource = eligibleButtons[0]; | ||
const standaloneButton = getButtons({ | ||
fundingSource, | ||
buttonOptions, | ||
}); | ||
// If the funding source is eligible, render button & return to end loop. | ||
// $FlowFixMe | ||
if (standaloneButton.isEligible()) { | ||
// $FlowFixMe | ||
return standaloneButton.render(`#${buttonContainerId}`); | ||
} | ||
// If funding source is ineligible, log error and move to next funding option. | ||
getLogger().error(`ncps_standalone_${fundingSource}_ineligible`); | ||
eligibleButtons.shift(); | ||
} | ||
}; |
@@ -5,3 +5,6 @@ /* @flow */ | ||
import { request } from "@krakenjs/belter/src"; | ||
import { getLogger } from "@paypal/sdk-client/src"; | ||
import { getButtonsComponent } from "../zoid/buttons"; | ||
import { | ||
@@ -14,5 +17,7 @@ buildHostedButtonCreateOrder, | ||
getButtonColor, | ||
shouldRenderSDKButtons, | ||
appendButtonContainer, | ||
getElementFromSelector, | ||
getButtonPreferences, | ||
renderStandaloneButton, | ||
applyContainerStyles, | ||
renderDefaultButton, | ||
} from "./utils"; | ||
@@ -33,5 +38,15 @@ | ||
getMerchantID: () => ["merchant_id_123"], | ||
getLogger: vi.fn(() => ({ | ||
error: vi.fn(), | ||
})), | ||
}; | ||
}); | ||
vi.mock("../zoid/buttons", async () => { | ||
return { | ||
...(await vi.importActual("../zoid/buttons")), | ||
getButtonsComponent: vi.fn(), | ||
}; | ||
}); | ||
const accessToken = "AT1234567890"; | ||
@@ -103,2 +118,3 @@ const hostedButtonId = "B1234567890"; | ||
}, | ||
js_sdk_container_id: "spb-container", | ||
}, | ||
@@ -563,34 +579,4 @@ version: "2", | ||
test("shouldRenderSDKButtons", () => { | ||
expect(shouldRenderSDKButtons([])).toBe(false); | ||
expect(shouldRenderSDKButtons(["paypal"])).toBe(true); | ||
expect(shouldRenderSDKButtons(["paypal", "venmo"])).toBe(true); | ||
}); | ||
test("buildButtonContainer", () => { | ||
const containerId = "#container-id"; | ||
const selector = document.createElement("div"); | ||
selector.setAttribute("id", containerId.slice(1)); | ||
vi.spyOn(document, "querySelector").mockReturnValueOnce(selector); | ||
expect(() => | ||
appendButtonContainer({ flexDirection: "row", selector: containerId }) | ||
).not.toThrow(); | ||
expect(() => | ||
appendButtonContainer({ flexDirection: "row", selector }) | ||
).not.toThrow(); | ||
expect(() => | ||
appendButtonContainer({ | ||
flexDirection: "row", | ||
selector: `${containerId}-not-found`, | ||
}) | ||
).toThrow("PayPal button container selector was not found"); | ||
}); | ||
test("getElementFromSelector", () => { | ||
const containerId = "#container-id"; | ||
const containerId = "container-id"; | ||
const selector = document.createElement("div"); | ||
@@ -610,2 +596,232 @@ | ||
describe("getButtonPreferences", () => { | ||
test("returns all button preferences if all are eligible", () => { | ||
const params = { | ||
button_preferences: ["paypal", "venmo"], | ||
eligible_funding_methods: ["paypal", "venmo", "paylater"], | ||
}; | ||
const preferences = getButtonPreferences(params); | ||
expect(preferences.buttonPreferences).toEqual(["paypal", "venmo"]); | ||
}); | ||
test("removes any button preferences not in the eligible funding methods", () => { | ||
const params = { | ||
button_preferences: ["paypal", "venmo"], | ||
eligible_funding_methods: ["paypal", "paylater"], | ||
}; | ||
const preferences = getButtonPreferences(params); | ||
expect(preferences.buttonPreferences).toEqual(["paypal"]); | ||
}); | ||
test("sorts eligible funding methods according to SUPPORTED_FUNDING_SOURCES", () => { | ||
const params = { | ||
button_preferences: ["paypal", "venmo"], | ||
eligible_funding_methods: ["paylater", "venmo", "paypal"], | ||
}; | ||
const preferences = getButtonPreferences(params); | ||
expect(preferences.eligibleFundingMethods).toEqual([ | ||
"paypal", | ||
"venmo", | ||
"paylater", | ||
]); | ||
}); | ||
test("doesn't filter out 'default' in button preferences", () => { | ||
const params = { | ||
button_preferences: ["paypal", "default"], | ||
eligible_funding_methods: ["paylater", "venmo", "paypal"], | ||
}; | ||
const preferences = getButtonPreferences(params); | ||
expect(preferences.buttonPreferences).toEqual(["paypal", "default"]); | ||
}); | ||
test("logs & throws error if the input is bad", () => { | ||
const errorMock = vi.fn(); | ||
// $FlowIssue | ||
getLogger.mockImplementation(() => ({ error: errorMock })); | ||
const params = { | ||
button_preferences: [], | ||
eligible_funding_methods: [], | ||
}; | ||
const shouldThrowError = () => getButtonPreferences(params); | ||
expect(shouldThrowError).toThrowError(); | ||
expect(errorMock).toBeCalledTimes(1); | ||
}); | ||
}); | ||
describe("applyContainerStyles", () => { | ||
const buttonContainerId = "button-container"; | ||
const params = { flexDirection: "vertical", buttonContainerId }; | ||
test("successfully applies styles to container", () => { | ||
const buttonContainer = document.createElement("div"); | ||
buttonContainer.id = buttonContainerId; | ||
vi.spyOn(document, "querySelector").mockReturnValueOnce(buttonContainer); | ||
applyContainerStyles(params); | ||
expect(buttonContainer?.style.length).toBeTruthy(); | ||
}); | ||
test("throws error if button container cannot be found", () => { | ||
// Intentionally not setting up the button container to throw the error | ||
const shouldThrowError = () => applyContainerStyles(params); | ||
expect(shouldThrowError).toThrowError( | ||
`Element with id ${buttonContainerId} not found.` | ||
); | ||
}); | ||
}); | ||
describe("render buttons", () => { | ||
const containerId = "container-id"; | ||
const expectedContainerId = `#${containerId}`; | ||
const renderMock = vi.fn(); | ||
const errorMock = vi.fn(); | ||
const baseParams = { | ||
buttonContainerId: containerId, | ||
buttonOptions: { | ||
createOrder: vi.fn(), | ||
onApprove: vi.fn(), | ||
onClick: vi.fn(), | ||
onInit: vi.fn(), | ||
style: { | ||
color: "gold", | ||
layout: "", | ||
shape: "", | ||
height: 40, | ||
label: "", | ||
tagline: true, | ||
}, | ||
hostedButtonId: "", | ||
}, | ||
}; | ||
beforeEach(() => { | ||
vi.resetAllMocks(); | ||
// $FlowIssue | ||
getLogger.mockImplementation(() => ({ error: errorMock })); | ||
}); | ||
describe("renderStandaloneButton", () => { | ||
test("renders button if eligible", () => { | ||
const Buttons = vi.fn(() => ({ | ||
render: renderMock, | ||
isEligible: vi.fn(() => true), | ||
})); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementationOnce(() => Buttons); | ||
renderStandaloneButton({ | ||
...baseParams, | ||
fundingSource: "paypal", | ||
}); | ||
expect(renderMock).toHaveBeenCalledTimes(1); | ||
expect(renderMock).toHaveBeenCalledWith(expectedContainerId); | ||
expect(Buttons).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
fundingSource: "paypal", | ||
}) | ||
); | ||
}); | ||
test("does not render button if button is ineligible", () => { | ||
const Buttons = vi.fn(() => ({ | ||
render: renderMock, | ||
isEligible: vi.fn(() => false), | ||
})); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementationOnce(() => Buttons); | ||
renderStandaloneButton({ | ||
...baseParams, | ||
fundingSource: "venmo", | ||
}); | ||
expect(renderMock).toHaveBeenCalledTimes(0); | ||
expect(errorMock).toHaveBeenCalledWith( | ||
"ncps_standalone_venmo_ineligible" | ||
); | ||
}); | ||
}); | ||
describe("renderDefaultButton", () => { | ||
test("renders the first eligible button", () => { | ||
const Buttons = vi.fn(() => ({ | ||
render: renderMock, | ||
isEligible: vi.fn(() => true), | ||
})); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementation(() => Buttons); | ||
renderDefaultButton({ | ||
...baseParams, | ||
eligibleDefaultButtons: ["venmo", "paylater"], | ||
}); | ||
expect(renderMock).toHaveBeenCalledWith(expectedContainerId); | ||
expect(errorMock).toHaveBeenCalledTimes(0); | ||
}); | ||
test("renders the next eligible button if button fails Buttons().isEligible() check", () => { | ||
const Buttons = vi.fn(({ fundingSource }) => ({ | ||
render: renderMock, | ||
isEligible: vi.fn(() => fundingSource === "paylater"), | ||
})); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementation(() => Buttons); | ||
renderDefaultButton({ | ||
...baseParams, | ||
eligibleDefaultButtons: ["venmo", "paylater"], | ||
}); | ||
expect(errorMock).toHaveBeenCalledTimes(1); | ||
expect(errorMock).toHaveBeenCalledWith( | ||
"ncps_standalone_venmo_ineligible" | ||
); | ||
expect(renderMock).toHaveBeenCalledWith(expectedContainerId); | ||
}); | ||
test("does not render any button if all fail Buttons().isEligible()", () => { | ||
const Buttons = vi.fn(() => ({ | ||
render: renderMock, | ||
isEligible: vi.fn(() => false), | ||
})); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementation(() => Buttons); | ||
renderDefaultButton({ | ||
...baseParams, | ||
eligibleDefaultButtons: ["venmo", "paylater"], | ||
}); | ||
expect(errorMock).toHaveBeenCalledTimes(2); | ||
expect(errorMock).toHaveBeenCalledWith( | ||
"ncps_standalone_venmo_ineligible" | ||
); | ||
expect(errorMock).toHaveBeenCalledWith( | ||
"ncps_standalone_paylater_ineligible" | ||
); | ||
expect(renderMock).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
}); | ||
/* eslint-enable no-restricted-globals, promise/no-native */ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1189710
16141