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

feature(3DS): Implement show method and return response with enriched nonce #2452

Open
wants to merge 64 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
2a9545d
chore: prettier
mchoun Oct 9, 2024
dd3c570
allow three-domain-secure component
mchoun Oct 10, 2024
673d98d
refactor threedomainsecure component to class
mchoun Oct 15, 2024
71c8127
correct typo
mchoun Oct 16, 2024
62afa53
refactor test for class component
mchoun Oct 16, 2024
89020d4
chore: fix lint
mchoun Oct 16, 2024
93e1b36
chore: fix flow issues
mchoun Oct 16, 2024
c718c95
pin flow-remove-types and hermes-parser version due to flow errors
mchoun Oct 17, 2024
295d091
return methods only instead of entire class
mchoun Oct 18, 2024
4fd9cef
modify interface to reflect future state
mchoun Oct 21, 2024
89be40e
resolve WIP stash merge
mchoun Oct 21, 2024
1e06a6d
implement isEligible request to API
mchoun Oct 24, 2024
58db542
Merge branch 'main' into feature/DTPPCPSDK-2660-3ds-eligibility
mchoun Oct 24, 2024
8cadea9
change sdktoken to idtoken
mchoun Oct 31, 2024
3d3fe62
modify protectedExport to Local or Stage check
mchoun Oct 31, 2024
149fa12
change protectedexport to local/stage export
mchoun Oct 31, 2024
5ae9ced
pass transaction context as received
siddy2181 Nov 1, 2024
b3aa99f
fix flow type errors
mchoun Nov 1, 2024
714509c
linting / flow fixes and skipping test for now
mchoun Nov 1, 2024
dba7fc4
add isEligible test skeleton
mchoun Nov 4, 2024
7e193e2
check for payer-action rel in links
mchoun Nov 5, 2024
68bc87d
throw error on API error isntead of false
mchoun Nov 5, 2024
ed1879f
wip: add test for isEligible
mchoun Nov 5, 2024
bb1fe54
remove comments
mchoun Nov 6, 2024
04124f1
additional test for isEligble
mchoun Nov 6, 2024
1fa80ab
remove console logs
mchoun Nov 6, 2024
ab27dab
add threeDS overlay component
siddy2181 Oct 25, 2024
2819404
add styling for 3DS iframe overlay
siddy2181 Oct 28, 2024
28f0a16
update overlay, save 3ds comp on eligibility to class variable
siddy2181 Nov 7, 2024
9bd1803
fix stage url,nit
siddy2181 Nov 7, 2024
139db48
fix lint
siddy2181 Nov 8, 2024
dd42464
fix lint/typecheck
siddy2181 Nov 8, 2024
811872d
fix tests
siddy2181 Nov 8, 2024
1f4bbc7
fix tests
siddy2181 Nov 8, 2024
a26e760
fix something please
siddy2181 Nov 8, 2024
e7a8f01
test with dub in stage
siddy2181 Nov 12, 2024
f56383c
remove port
siddy2181 Nov 12, 2024
a814d6d
test api subdomain
siddy2181 Nov 13, 2024
62ead51
add sdkMeta, logs, fix css
siddy2181 Nov 15, 2024
5cf3f99
Merge branch 'main' into fastlane-3ds-e2e
siddy2181 Nov 15, 2024
58581a5
wip
imbrian Nov 15, 2024
13993a1
pass braintree-version header to gql
imbrian Nov 18, 2024
c74de53
fix lint
siddy2181 Nov 18, 2024
f0163ce
fix typecheck
siddy2181 Nov 18, 2024
9155667
skip test
siddy2181 Nov 18, 2024
a6e4620
add domain
siddy2181 Nov 21, 2024
ea7b44f
Merge branch 'main' of https://github.com/paypal/paypal-checkout-comp…
siddy2181 Nov 21, 2024
0f42cd5
testing only fix
siddy2181 Nov 21, 2024
a29fbcd
add allowParentDomain
siddy2181 Nov 22, 2024
b768595
add globals and enable automatic config
siddy2181 Dec 4, 2024
a843e7e
render zoid in parent, pass payerActionUrl prop
siddy2181 Dec 4, 2024
a34ea3a
fix lint
siddy2181 Dec 5, 2024
009d5d8
fix show() response back to merchant
siddy2181 Dec 2, 2024
53f155f
call eligibility api with an lsat
siddy2181 Dec 2, 2024
9093e4c
fix response back to the merchant
siddy2181 Dec 5, 2024
5b0fb8b
fix response back to the merchant
siddy2181 Dec 6, 2024
4792f9c
console logs cleanup
siddy2181 Dec 6, 2024
67542a5
add tests for interface and api
siddy2181 Dec 9, 2024
650e451
add test - utils.jsx
siddy2181 Dec 10, 2024
a83adb6
add devOnlyExport, cleanup
siddy2181 Dec 10, 2024
7e7a6a7
reverted dist content
siddy2181 Dec 10, 2024
99836cd
fix test for protected export
siddy2181 Dec 10, 2024
9d345fb
fix test for protected export
siddy2181 Dec 11, 2024
5073359
address review comments
siddy2181 Dec 12, 2024
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 .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
__HOST__: true,
__PATH__: true,
__COMPONENTS__: true,
$Shape: true,
},

rules: {
Expand Down
2 changes: 2 additions & 0 deletions __sdk__.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ module.exports = {
entry: "./src/shopper-insights/interface",
},
"three-domain-secure": {
globals,
automatic: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to automatic: true anymore, correct? I believe we were testing this when we were running into the zoid xprops issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe, having automatic: true wouldn't hurt. It would make this component available automatically when created. My understanding was that it will prevent collision with the old 3DS component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

automatic: true will return this component for everyone that loads the JS SDK. The exported setup() method is what will automatically create the zoid component when the component is loaded.

This will probably be fine for now since this component is still protected but we should remove this before we allow this in Production. If we don't, paypal.ThreeDomainSecureClient will be available to everyone even if they don't pass three-domain-secure into the components query param.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably remove it now because even though its hidden, it will show up in all bundles served. I'm sure its super small but might as well not ship javascript if we don't have to

entry: "./src/three-domain-secure/interface",
},
};
3 changes: 3 additions & 0 deletions src/lib/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ export const devEnvOnlyExport = (unprotectedExport) => {
return undefined;
}
};
// TODO: Remove after testing
// $FlowIssue
export const payPayDomainRegEx = /\.paypal\.(com|cn)(:\d+)?$/; // eslint-disable-line security/detect-unsafe-regex
siddy2181 marked this conversation as resolved.
Show resolved Hide resolved
114 changes: 114 additions & 0 deletions src/three-domain-secure/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* @flow */
import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
import { request } from "@krakenjs/belter/src";
import { getSessionID, getPartnerAttributionID } from "@paypal/sdk-client/src";

import { callRestAPI } from "../lib";
import { HEADERS } from "../constants/api";

type HTTPRequestOptions = {|
// eslint-disable-next-line flowtype/no-weak-types
data: any,
baseURL?: string,
accessToken?: string,
method?: string, // TODO do we have an available type for this in Flow?
|};

interface HTTPClientType {
accessToken: ?string;
baseURL: ?string;
}

type HTTPClientOptions = {|
accessToken: ?string,
baseURL: ?string,
|};

export class HTTPClient implements HTTPClientType {
accessToken: ?string;
baseURL: ?string;

constructor(options?: $Shape<HTTPClientOptions> = {}) {
this.accessToken = options.accessToken;
this.baseURL = options.baseURL;
}

setAccessToken(token: string) {
this.accessToken = token;
}
}

export class RestClient extends HTTPClient {
request({ baseURL, ...rest }: HTTPRequestOptions): ZalgoPromise<{ ... }> {
return callRestAPI({
url: baseURL ?? this.baseURL ?? "",
accessToken: this.accessToken,
...rest,
});
}
}

const GRAPHQL_URI = "/graphql";

type GQLQuery = {|
query: string,
variables: { ... },
|};

export function callGraphQLAPI({
accessToken,
baseURL,
data: query,
headers,
}: {|
accessToken: ?string,
baseURL: string,
data: GQLQuery,
headers: Object, // TODO fix
// eslint-disable-next-line flowtype/no-weak-types
|}): ZalgoPromise<any> {
if (!accessToken) {
throw new Error(
`No access token passed to GraphQL request ${baseURL}${GRAPHQL_URI}`
);
}

const requestHeaders = {
...headers,
[HEADERS.AUTHORIZATION]: `Bearer ${accessToken}`,
[HEADERS.CONTENT_TYPE]: "application/json",
[HEADERS.PARTNER_ATTRIBUTION_ID]: getPartnerAttributionID() ?? "",
[HEADERS.CLIENT_METADATA_ID]: getSessionID(),
};

return request({
method: "post",
url: `${baseURL}${GRAPHQL_URI}`,
headers: requestHeaders,
json: query,
}).then(({ status, body }) => {
// TODO handle body.errors
if (status !== 200) {
throw new Error(`${baseURL}${GRAPHQL_URI} returned status ${status}`);
}

return body;
});
}

export class GraphQLClient extends HTTPClient {
request({
baseURL,
data,
accessToken,
headers,
}: // eslint-disable-next-line flowtype/no-weak-types
any): ZalgoPromise<any> {
return callGraphQLAPI({
accessToken: accessToken ?? this.accessToken,
data,
baseURL: baseURL ?? this.baseURL ?? "",
headers,
});
}
}
169 changes: 169 additions & 0 deletions src/three-domain-secure/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/* @flow */
import { describe, expect, vi } from "vitest";
import { request } from "@krakenjs/belter/src";

import { callRestAPI } from "../lib";
import { HEADERS } from "../constants/api";

import { RestClient, GraphQLClient, callGraphQLAPI, HTTPClient } from "./api";

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")),
getSessionID: () => "session_id_123",
getPartnerAttributionID: () => "partner_attr_123",
};
});

vi.mock("../lib", () => ({
callRestAPI: vi.fn(),
}));

describe("API", () => {
const accessToken = "access_token";
const baseURL = "http://localhost.paypal.com:8080";

afterEach(() => {
vi.clearAllMocks();
});
describe("HTTPClient", () => {
it("should set access token and base url in constructor", () => {
const client = new HTTPClient({ accessToken, baseURL });
expect(client.accessToken).toBe(accessToken);
expect(client.baseURL).toBe(baseURL);
});

it("should set access token", () => {
const client = new HTTPClient();
client.setAccessToken(accessToken);
expect(client.accessToken).toBe(accessToken);
});
});

describe("RestClient", () => {
it("should make a REST API call with correct params", () => {
const data = { test: "data" };
const requestOptions = {
data,
baseURL,
};
const client = new RestClient({ accessToken });
client.request(requestOptions);
expect(callRestAPI).toHaveBeenCalledWith({
accessToken,
data,
url: baseURL,
});
});
});

describe("GraphQLClient", () => {
const query = { test: "data" };
const data = { query };
const headers = { "Content-Type": "application/json" };

it.skip("should make a GraphQL API call with correct params", () => {
vi.spyOn({ callGraphQLAPI }, "callGraphQLAPI").mockResolvedValue({
data: { test: "data" },
});
const client = new GraphQLClient({ accessToken, baseURL });
client.request({ data, headers }).then(() => {
expect(callGraphQLAPI).toHaveBeenCalledWith({
accessToken,
baseURL,
data,
headers,
});
});
});
});

describe("callGraphQLAPI", () => {
const query = '{ "test": "data" }';
const variables = { option: "param1" };
const gqlQuery = { query, variables };

const response = { data: { test: "data" } };

it("should throw error if no access token is provided", () => {
expect(() =>
callGraphQLAPI({
accessToken: null,
baseURL,
data: gqlQuery,
headers: {},
})
).toThrowError(
new Error(
`No access token passed to GraphQL request ${baseURL}/graphql`
)
);
});

it("should make a GraphQL API call with correct params", () => {
vi.mocked(request).mockResolvedValue({
status: 200,
body: response,
});
callGraphQLAPI({
accessToken,
baseURL,
data: gqlQuery,
headers: {},
});
expect(request).toHaveBeenCalledWith({
method: "post",
url: `${baseURL}/graphql`,
headers: {
[HEADERS.AUTHORIZATION]: `Bearer ${accessToken}`,
[HEADERS.CONTENT_TYPE]: "application/json",
[HEADERS.PARTNER_ATTRIBUTION_ID]: "partner_attr_123",
[HEADERS.CLIENT_METADATA_ID]: "session_id_123",
},
json: gqlQuery,
});
});

it("should resolve with response body on success", async () => {
vi.mocked(request).mockResolvedValue({
status: 200,
body: response,
});
const resp = await callGraphQLAPI({
accessToken,
baseURL,
data: gqlQuery,
headers: {},
});
expect(resp).toEqual(response);
});

it("should throw error on error status", async () => {
const status = 400;
vi.mocked(request).mockResolvedValue({
status,
body: { message: "Something went wrong" },
});

try {
await callGraphQLAPI({
accessToken,
baseURL,
data: gqlQuery,
headers: {},
});
} catch (error) {
expect(error.message).toBe(
`${baseURL}/graphql returned status ${status}`
);
}
});
});
});
Loading
Loading