-
Notifications
You must be signed in to change notification settings - Fork 570
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
536 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/* @flow */ | ||
|
||
import { getMerchantID } from "@paypal/sdk-client/src"; | ||
|
||
import { getButtonsComponent } from "../zoid/buttons"; | ||
|
||
import { | ||
buildHostedButtonCreateOrder, | ||
buildHostedButtonOnApprove, | ||
getHostedButtonDetails, | ||
renderForm, | ||
} from "./utils"; | ||
import type { | ||
HostedButtonsComponent, | ||
HostedButtonsComponentProps, | ||
HostedButtonsInstance, | ||
} from "./types"; | ||
|
||
export const getHostedButtonsComponent = (): HostedButtonsComponent => { | ||
function HostedButtons({ | ||
hostedButtonId, | ||
}: HostedButtonsComponentProps): HostedButtonsInstance { | ||
const Buttons = getButtonsComponent(); | ||
const render = (selector) => { | ||
// The SDK supports mutiple merchant IDs, but hosted buttons only | ||
// have one merchant id as a query parameter to the SDK script. | ||
// https://github.com/paypal/paypal-sdk-client/blob/c58e35f8f7adbab76523eb25b9c10543449d2d29/src/script.js#L144 | ||
const merchantId = getMerchantID()[0]; | ||
|
||
getHostedButtonDetails({ hostedButtonId }).then( | ||
({ html, htmlScript, style }) => { | ||
renderForm({ html, htmlScript, selector }); | ||
|
||
// $FlowFixMe | ||
Buttons({ | ||
style, | ||
hostedButtonId, | ||
onInit(data, actions) { | ||
// disable the button, listen for input changes, | ||
// and enable the button when the form is valid | ||
// using actions.disable() and actions.enable() | ||
window[`__pp_form_fields_${hostedButtonId}`]?.onInit?.( | ||
data, | ||
actions | ||
); | ||
}, | ||
onClick(data, actions) { | ||
// render form errors, if present | ||
window[`__pp_form_fields_${hostedButtonId}`]?.onClick?.( | ||
data, | ||
actions | ||
); | ||
}, | ||
createOrder: buildHostedButtonCreateOrder({ | ||
hostedButtonId, | ||
merchantId, | ||
}), | ||
onApprove: buildHostedButtonOnApprove({ | ||
hostedButtonId, | ||
merchantId, | ||
}), | ||
}).render(selector); | ||
} | ||
); | ||
}; | ||
return { | ||
render, | ||
}; | ||
} | ||
return HostedButtons; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* @flow */ | ||
|
||
import { describe, test, expect, vi } from "vitest"; | ||
import { request } from "@krakenjs/belter/src"; | ||
import { ZalgoPromise } from "@krakenjs/zalgo-promise"; | ||
|
||
import { getButtonsComponent } from "../zoid/buttons"; | ||
|
||
import { getHostedButtonsComponent } from "."; | ||
|
||
vi.mock("@krakenjs/belter/src", async () => { | ||
return { | ||
...(await vi.importActual("@krakenjs/belter/src")), | ||
request: vi.fn(), | ||
}; | ||
}); | ||
|
||
vi.mock("@paypal/sdk-client/src", async () => { | ||
return { | ||
...(await vi.importActual("@paypal/sdk-client/src")), | ||
getSDKHost: () => "example.com", | ||
getClientID: () => "client_id_123", | ||
getMerchantID: () => ["merchant_id_123"], | ||
}; | ||
}); | ||
|
||
vi.mock("../zoid/buttons", async () => { | ||
return { | ||
...(await vi.importActual("../zoid/buttons")), | ||
getButtonsComponent: vi.fn(), | ||
}; | ||
}); | ||
|
||
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", | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
|
||
describe("HostedButtons", () => { | ||
test("paypal.Buttons calls getHostedButtonDetails and invokes v5 of the SDK", () => { | ||
const Buttons = vi.fn(() => ({ render: vi.fn() })); | ||
// $FlowIssue | ||
getButtonsComponent.mockImplementationOnce(() => Buttons); | ||
const HostedButtons = getHostedButtonsComponent(); | ||
// $FlowIssue | ||
request.mockImplementationOnce(() => | ||
ZalgoPromise.resolve(getHostedButtonDetailsResponse) | ||
); | ||
HostedButtons({ | ||
hostedButtonId: "B1234567890", | ||
}).render("#example"); | ||
expect(Buttons).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
hostedButtonId: "B1234567890", | ||
hostedButtonType: "NO_CODE_FIXED_PRICE", | ||
}) | ||
); | ||
expect.assertions(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/* @flow */ | ||
|
||
import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; | ||
|
||
export type HostedButtonsComponentProps = {| | ||
hostedButtonId: string, | ||
|}; | ||
|
||
export type GetCallbackProps = {| | ||
hostedButtonId: string, | ||
merchantId: string, | ||
|}; | ||
|
||
export type HostedButtonsInstance = {| | ||
render: (string | HTMLElement) => void, | ||
|}; | ||
|
||
export type HostedButtonDetailsParams = | ||
(HostedButtonsComponentProps) => ZalgoPromise<{| | ||
html: string, | ||
htmlScript: string, | ||
style: {| | ||
layout: string, | ||
shape: string, | ||
color: string, | ||
label: string, | ||
|}, | ||
|}>; | ||
|
||
export type ButtonVariables = $ReadOnlyArray<{| | ||
name: string, | ||
value: string, | ||
|}>; | ||
|
||
export type CreateOrder = (data: {| | ||
paymentSource: string, | ||
|}) => ZalgoPromise<string>; | ||
|
||
export type OnApprove = (data: {| | ||
orderID: string, | ||
paymentSource: string, | ||
|}) => ZalgoPromise<mixed>; | ||
|
||
export type CreateAccessToken = (clientID: string) => ZalgoPromise<string>; | ||
|
||
export type HostedButtonsComponent = | ||
(HostedButtonsComponentProps) => HostedButtonsInstance; | ||
|
||
export type RenderForm = {| | ||
html: string, | ||
htmlScript: string, | ||
selector: string | HTMLElement, | ||
|}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/* @flow */ | ||
|
||
import { request, memoize, popup } from "@krakenjs/belter/src"; | ||
import { getSDKHost, getClientID } from "@paypal/sdk-client/src"; | ||
|
||
import { DEFAULT_POPUP_SIZE } from "../zoid/checkout"; | ||
|
||
import type { | ||
ButtonVariables, | ||
CreateAccessToken, | ||
CreateOrder, | ||
GetCallbackProps, | ||
HostedButtonDetailsParams, | ||
OnApprove, | ||
RenderForm, | ||
} from "./types"; | ||
|
||
const entryPoint = "SDK"; | ||
const baseUrl = `https://${getSDKHost()}`; | ||
const apiUrl = baseUrl.replace("www", "api"); | ||
|
||
const getHeaders = (accessToken?: string) => ({ | ||
...(accessToken && { Authorization: `Bearer ${accessToken}` }), | ||
"Content-Type": "application/json", | ||
"PayPal-Entry-Point": entryPoint, | ||
}); | ||
|
||
export const createAccessToken: CreateAccessToken = memoize<CreateAccessToken>( | ||
(clientId) => { | ||
return request({ | ||
url: `${apiUrl}/v1/oauth2/token`, | ||
method: "POST", | ||
body: "grant_type=client_credentials", | ||
headers: { | ||
Authorization: `Basic ${btoa(clientId)}`, | ||
"Content-Type": "application/json", | ||
}, | ||
}).then((response) => response.body.access_token); | ||
} | ||
); | ||
|
||
const getButtonVariable = (variables: ButtonVariables, key: string): string => | ||
variables?.find((variable) => variable.name === key)?.value ?? ""; | ||
|
||
const getFundingSource = (paymentSource) => { | ||
if (paymentSource === "credit") { | ||
return `CARD`; | ||
} | ||
return paymentSource.toUpperCase(); | ||
}; | ||
|
||
export const getHostedButtonDetails: HostedButtonDetailsParams = ({ | ||
hostedButtonId, | ||
}) => { | ||
return request({ | ||
url: `${baseUrl}/ncp/api/form-fields/${hostedButtonId}`, | ||
headers: getHeaders(), | ||
}).then(({ body }) => { | ||
const variables = body.button_details.link_variables; | ||
return { | ||
style: { | ||
layout: getButtonVariable(variables, "layout"), | ||
shape: getButtonVariable(variables, "shape"), | ||
color: getButtonVariable(variables, "color"), | ||
label: getButtonVariable(variables, "button_text"), | ||
}, | ||
html: body.html, | ||
htmlScript: body.html_script, | ||
}; | ||
}); | ||
}; | ||
|
||
/** | ||
* Attaches form fields (html) to the given selector, and | ||
* initializes window.__pp_form_fields (htmlScript). | ||
*/ | ||
export const renderForm = ({ | ||
html, | ||
htmlScript, | ||
selector, | ||
}: RenderForm): void => { | ||
const elm = | ||
typeof selector === "string" ? document.querySelector(selector) : selector; | ||
if (elm) { | ||
elm.innerHTML = html + htmlScript; | ||
const newScriptEl = document.createElement("script"); | ||
const oldScriptEl = elm.querySelector("script"); | ||
newScriptEl.innerHTML = oldScriptEl?.innerHTML ?? ""; | ||
oldScriptEl?.parentNode?.replaceChild(newScriptEl, oldScriptEl); | ||
} | ||
}; | ||
|
||
export const buildHostedButtonCreateOrder = ({ | ||
hostedButtonId, | ||
merchantId, | ||
}: GetCallbackProps): CreateOrder => { | ||
return (data) => { | ||
const userInputs = | ||
window[`__pp_form_fields_${hostedButtonId}`]?.getUserInputs?.() || {}; | ||
return createAccessToken(getClientID()).then((accessToken) => { | ||
return request({ | ||
url: `${apiUrl}/v1/checkout/links/${hostedButtonId}/create-context`, | ||
headers: getHeaders(accessToken), | ||
method: "POST", | ||
body: JSON.stringify({ | ||
entry_point: entryPoint, | ||
funding_source: getFundingSource(data.paymentSource), | ||
merchant_id: merchantId, | ||
...userInputs, | ||
}), | ||
}).then(({ body }) => { | ||
return body.context_id; | ||
}); | ||
}); | ||
}; | ||
}; | ||
|
||
export const buildHostedButtonOnApprove = ({ | ||
hostedButtonId, | ||
merchantId, | ||
}: GetCallbackProps): OnApprove => { | ||
return (data) => { | ||
return createAccessToken(getClientID()).then((accessToken) => { | ||
return request({ | ||
url: `${apiUrl}/v1/checkout/links/${hostedButtonId}/pay`, | ||
headers: getHeaders(accessToken), | ||
method: "POST", | ||
body: JSON.stringify({ | ||
entry_point: entryPoint, | ||
merchant_id: merchantId, | ||
context_id: data.orderID, | ||
}), | ||
}).then((response) => { | ||
// The "Debit or Credit Card" button does not open a popup | ||
// so we need to open a new popup for buyers who complete | ||
// a checkout via "Debit or Credit Card". | ||
if (data.paymentSource === "card") { | ||
const url = `${baseUrl}/ncp/payment/${hostedButtonId}/${data.orderID}`; | ||
popup(url, { | ||
width: DEFAULT_POPUP_SIZE.WIDTH, | ||
height: DEFAULT_POPUP_SIZE.HEIGHT, | ||
}); | ||
} | ||
return response; | ||
}); | ||
}); | ||
}; | ||
}; |
Oops, something went wrong.