diff --git a/__sdk__.js b/__sdk__.js index 9dd73d4343..0759a7714f 100644 --- a/__sdk__.js +++ b/__sdk__.js @@ -62,6 +62,10 @@ module.exports = { entry: "./src/interface/wallet", globals, }, + connect: { + entry: "./src/connect/interface", + globals + }, // @deprecated - renamed to payment-fields to be removed fields: { entry: "./src/interface/fields", diff --git a/package.json b/package.json index 4c6824be4b..a02550f39c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paypal/checkout-components", - "version": "5.0.286", + "version": "5.0.288", "description": "PayPal Checkout components, for integrating checkout products.", "main": "index.js", "scripts": { @@ -29,7 +29,7 @@ "release": "./scripts/publish.sh", "start": "npm run webpack -- --progress --watch", "test": "npm run format:check && npm run test:unit && npm run jest-ssr && npm run karma && npm run jest-screenshot", - "test:unit": "vitest run", + "test:unit": "vitest", "percy-screenshot": "npx playwright install && babel-node ./test/percy/server/createButtonConfigs.js && percy exec -- playwright test --config=./test/percy/playwright.config.js --reporter=dot --pass-with-no-tests", "typecheck": "npm run flow-typed && npm run flow", "version": "./scripts/version.sh", @@ -109,6 +109,7 @@ "@krakenjs/zoid": "^10.3.1", "@paypal/common-components": "^1.0.35", "@paypal/funding-components": "^1.0.31", + "@paypal/connect-loader-component": "^1.1.0", "@paypal/sdk-client": "^4.0.176", "@paypal/sdk-constants": "^1.0.133", "@paypal/sdk-logos": "^2.2.6" diff --git a/src/connect/component.jsx b/src/connect/component.jsx new file mode 100644 index 0000000000..4c616a5a9a --- /dev/null +++ b/src/connect/component.jsx @@ -0,0 +1,93 @@ +/* @flow */ +import { loadAxo } from "@paypal/connect-loader-component"; +import { + getClientID, + getClientMetadataID, + getUserIDToken, + getLogger, +} from "@paypal/sdk-client/src"; + +const sendCountMetric = ({ + dimensions, + event = "unused", + name, + value = 1, +}: {| + event?: string, + name: string, + value?: number, + dimensions: { + [string]: mixed, + }, + // $FlowIssue return type +|}) => + getLogger().metric({ + dimensions, + metricEventName: event, + metricNamespace: name, + metricValue: value, + metricType: "counter", + }); + +// $FlowFixMe +export const getConnectComponent = async (merchantProps) => { + sendCountMetric({ + name: "pp.app.paypal_sdk.connect.init.count", + dimensions: {}, + }); + + const cmid = getClientMetadataID(); + const clientID = getClientID(); + const userIdToken = getUserIDToken(); + const { metadata } = merchantProps; + + let loadResult = {}; + try { + loadResult = await loadAxo({ + platform: "PPCP", + btSdkVersion: "3.97.3-connect-alpha.6.1", + minified: false, + metadata, + }); + } catch (error) { + sendCountMetric({ + name: "pp.app.paypal_sdk.connect.init.error.count", + event: "error", + dimensions: { + errorName: "connect_load_error", + }, + }); + + throw new Error(error); + } + + try { + const connect = await window.braintree.connect.create({ + ...loadResult.metadata, // returns a localeURL for assets + ...merchantProps, // AXO specific props + platformOptions: { + platform: "PPCP", + userIdToken, + clientID, + clientMetadataID: cmid, + }, + }); + + sendCountMetric({ + name: "pp.app.paypal_sdk.connect.init.success.count", + event: "success", + dimensions: {}, + }); + + return connect; + } catch (error) { + sendCountMetric({ + name: "pp.app.paypal_sdk.connect.init.error.count", + event: "error", + dimensions: { + errorName: "connect_init_error", + }, + }); + throw new Error(error); + } +}; diff --git a/src/connect/component.test.js b/src/connect/component.test.js new file mode 100644 index 0000000000..20551b4ef4 --- /dev/null +++ b/src/connect/component.test.js @@ -0,0 +1,83 @@ +/* @flow */ + +import { + getClientID, + getClientMetadataID, + getUserIDToken, + getLogger, +} from "@paypal/sdk-client/src"; +import { loadAxo } from "@paypal/connect-loader-component"; +import { describe, expect, test, vi } from "vitest"; + +import { getConnectComponent } from "./component"; + +vi.mock("@paypal/sdk-client/src", () => { + return { + getClientID: vi.fn(() => "mock-client-id"), + getClientMetadataID: vi.fn(() => "mock-cmid"), + getUserIDToken: vi.fn(() => "mock-uid"), + getLogger: vi.fn(() => ({ metric: vi.fn() })), + }; +}); + +vi.mock("@paypal/connect-loader-component", () => { + return { + loadAxo: vi.fn(), + }; +}); + +describe("getConnectComponent: returns ConnectComponent", () => { + const mockAxoMetadata = { someData: "data" }; + const mockProps = { someProp: "value" }; + beforeEach(() => { + vi.clearAllMocks(); + window.braintree = { + connect: { + create: vi.fn(), + }, + }; + + loadAxo.mockResolvedValue({ metadata: mockAxoMetadata }); + }); + + test("loadAxo and window.braintree.connect.create are called with proper data", async () => { + await getConnectComponent(mockProps); + + expect(getClientID).toHaveBeenCalled(); + expect(getClientMetadataID).toHaveBeenCalled(); + expect(getUserIDToken).toHaveBeenCalled(); + expect(loadAxo).toHaveBeenCalled(); + + expect(window.braintree.connect.create).toHaveBeenCalledWith({ + ...mockAxoMetadata, + ...mockProps, + platformOptions: { + platform: "PPCP", + clientID: "mock-client-id", + clientMetadataID: "mock-cmid", + userIdToken: "mock-uid", + }, + }); + expect(getLogger).toBeCalledTimes(2); + }); + + test("loadAxo failure is handled", async () => { + const errorMessage = "Something went wrong"; + loadAxo.mockRejectedValue(errorMessage); + + await expect(() => getConnectComponent(mockProps)).rejects.toThrow( + errorMessage + ); + expect(getLogger).toHaveBeenCalledTimes(2); + }); + + test("connect create failure is handled", async () => { + const expectedError = "create failed"; + window.braintree.connect.create.mockRejectedValue(expectedError); + + await expect(() => getConnectComponent(mockProps)).rejects.toThrow( + expectedError + ); + expect(getLogger).toBeCalledTimes(2); + }); +}); diff --git a/src/connect/interface.js b/src/connect/interface.js new file mode 100644 index 0000000000..208cfede13 --- /dev/null +++ b/src/connect/interface.js @@ -0,0 +1,16 @@ +/* eslint-disable flowtype/no-weak-types */ +/* @flow */ +// flow-disable + +import { getConnectComponent } from "./component"; + +type ConnectComponent = (merchantProps: any) => ConnectComponent; +// $FlowFixMe +export const Connect: ConnectComponent = async ( + merchantProps: any +): ConnectComponent => { + // $FlowFixMe + return await getConnectComponent(merchantProps); +}; + +/* eslint-enable flowtype/no-weak-types */ diff --git a/src/connect/interface.test.js b/src/connect/interface.test.js new file mode 100644 index 0000000000..50918157ae --- /dev/null +++ b/src/connect/interface.test.js @@ -0,0 +1,19 @@ +/* @flow */ + +import { describe, expect, vi } from "vitest"; + +import { getConnectComponent } from "./component"; +import { Connect } from "./interface"; + +describe("interface.js", () => { + vi.mock("./component", () => { + return { + getConnectComponent: vi.fn(), + }; + }); + it("should call getConnectComponent with merchant props", async () => { + const merchantProps = { props: "someProps" }; + await Connect(merchantProps); + expect(getConnectComponent).toBeCalledWith(merchantProps); + }); +}); diff --git a/test/declarations.js b/test/declarations.js index 721ee09b44..b93b617393 100644 --- a/test/declarations.js +++ b/test/declarations.js @@ -2,8 +2,11 @@ /* eslint import/unambiguous: 0 */ declare var jest; +declare var after: Function; declare var afterAll: Function; +declare var beforeAll: Function; declare var beforeEach: Function; +declare var afterEach: Function; declare var it: Function; declare var describe: Function; declare var test: Function;