Skip to content

Commit b1d2a84

Browse files
siddy2181mchounimbrian
authored
feature(3DS): Implement show method and return response with enriched nonce (#2452)
* chore: prettier * allow three-domain-secure component * refactor threedomainsecure component to class * correct typo * refactor test for class component * chore: fix lint * chore: fix flow issues * pin flow-remove-types and hermes-parser version due to flow errors * return methods only instead of entire class * modify interface to reflect future state * resolve WIP stash merge * implement isEligible request to API * change sdktoken to idtoken * modify protectedExport to Local or Stage check * change protectedexport to local/stage export * pass transaction context as received * fix flow type errors * linting / flow fixes and skipping test for now * add isEligible test skeleton * check for payer-action rel in links * throw error on API error isntead of false * wip: add test for isEligible * remove comments * additional test for isEligble * remove console logs * add threeDS overlay component * add styling for 3DS iframe overlay * update overlay, save 3ds comp on eligibility to class variable * fix stage url,nit * fix lint * fix lint/typecheck * fix tests * fix tests * fix something please * test with dub in stage * remove port * test api subdomain * add sdkMeta, logs, fix css * wip * pass braintree-version header to gql * fix lint * fix typecheck * skip test * add domain * testing only fix * add allowParentDomain * add globals and enable automatic config * render zoid in parent, pass payerActionUrl prop * fix lint * fix show() response back to merchant * call eligibility api with an lsat * fix response back to the merchant * fix response back to the merchant * console logs cleanup * add tests for interface and api * add test - utils.jsx * add devOnlyExport, cleanup * reverted dist content * fix test for protected export * fix test for protected export * address review comments * remove automatic:true flag tests, make show async * wrap show around native promise to make await-able fix types fix types typecheck * add lsat support for Partner integration * fix lint tests * update tests * add validations for isEligible merchant payload (#2459) * fix validation logic * fix typecheck --------- Co-authored-by: Mervin Choun <[email protected]> Co-authored-by: Brian Tedder <[email protected]>
1 parent 01a491d commit b1d2a84

15 files changed

+1640
-119
lines changed

Diff for: .eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
__HOST__: true,
1919
__PATH__: true,
2020
__COMPONENTS__: true,
21+
$Shape: true,
2122
},
2223

2324
rules: {

Diff for: __sdk__.js

+1
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,6 @@ module.exports = {
9595
},
9696
"three-domain-secure": {
9797
entry: "./src/three-domain-secure/interface",
98+
globals,
9899
},
99100
};

Diff for: src/constants/api.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const HEADERS = {
99

1010
export const ELIGIBLE_PAYMENT_METHODS = "v2/payments/find-eligible-methods";
1111
export const PAYMENT_3DS_VERIFICATION = "v2/payments/payment";
12+
export const AUTH = "/v1/oauth2/token";
1213

1314
export const FPTI_TRANSITION = {
1415
SHOPPER_INSIGHTS_API_INIT: "sdk_shopper_insights_recommended_init",

Diff for: src/three-domain-secure/api.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/* @flow */
2+
import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
3+
import { request } from "@krakenjs/belter/src";
4+
import { getSessionID, getPartnerAttributionID } from "@paypal/sdk-client/src";
5+
6+
import { callRestAPI } from "../lib";
7+
import { HEADERS } from "../constants/api";
8+
9+
type HTTPRequestOptions = {|
10+
// eslint-disable-next-line flowtype/no-weak-types
11+
data: any,
12+
baseURL?: string,
13+
accessToken?: string,
14+
method?: string, // TODO do we have an available type for this in Flow?
15+
|};
16+
17+
interface HTTPClientType {
18+
accessToken: ?string;
19+
baseURL: ?string;
20+
}
21+
22+
type HTTPClientOptions = {|
23+
accessToken: ?string,
24+
baseURL: ?string,
25+
|};
26+
27+
export class HTTPClient implements HTTPClientType {
28+
accessToken: ?string;
29+
baseURL: ?string;
30+
31+
constructor(options?: $Shape<HTTPClientOptions> = {}) {
32+
this.accessToken = options.accessToken;
33+
this.baseURL = options.baseURL;
34+
}
35+
36+
setAccessToken(token: string) {
37+
this.accessToken = token;
38+
}
39+
}
40+
41+
export class RestClient extends HTTPClient {
42+
request({ baseURL, ...rest }: HTTPRequestOptions): ZalgoPromise<{ ... }> {
43+
return callRestAPI({
44+
url: baseURL ?? this.baseURL ?? "",
45+
accessToken: this.accessToken,
46+
...rest,
47+
});
48+
}
49+
authRequest({
50+
baseURL,
51+
accessToken,
52+
...rest
53+
}: HTTPRequestOptions): ZalgoPromise<{ ... }> {
54+
return request({
55+
method: "post",
56+
url: baseURL ?? this.baseURL ?? "",
57+
headers: {
58+
// $FlowIssue
59+
Authorization: `Basic ${accessToken}`,
60+
},
61+
...rest,
62+
}).then(({ body }) => {
63+
if (body && body.error === "invalid_client") {
64+
throw new Error(
65+
`Auth Api invalid client id: \n\n${JSON.stringify(body, null, 4)}`
66+
);
67+
}
68+
69+
if (!body || !body.access_token) {
70+
throw new Error(
71+
`Auth Api response error:\n\n${JSON.stringify(body, null, 4)}`
72+
);
73+
}
74+
75+
return body.access_token;
76+
});
77+
}
78+
}
79+
80+
const GRAPHQL_URI = "/graphql";
81+
82+
type GQLQuery = {|
83+
query: string,
84+
variables: { ... },
85+
|};
86+
87+
export function callGraphQLAPI({
88+
accessToken,
89+
baseURL,
90+
data: query,
91+
headers,
92+
}: {|
93+
accessToken: ?string,
94+
baseURL: string,
95+
data: GQLQuery,
96+
headers: Object, // TODO fix
97+
// eslint-disable-next-line flowtype/no-weak-types
98+
|}): ZalgoPromise<any> {
99+
if (!accessToken) {
100+
throw new Error(
101+
`No access token passed to GraphQL request ${baseURL}${GRAPHQL_URI}`
102+
);
103+
}
104+
105+
const requestHeaders = {
106+
...headers,
107+
[HEADERS.AUTHORIZATION]: `Bearer ${accessToken}`,
108+
[HEADERS.CONTENT_TYPE]: "application/json",
109+
[HEADERS.PARTNER_ATTRIBUTION_ID]: getPartnerAttributionID() ?? "",
110+
[HEADERS.CLIENT_METADATA_ID]: getSessionID(),
111+
};
112+
113+
return request({
114+
method: "post",
115+
url: `${baseURL}${GRAPHQL_URI}`,
116+
headers: requestHeaders,
117+
json: query,
118+
}).then(({ status, body }) => {
119+
// TODO handle body.errors
120+
if (status !== 200) {
121+
throw new Error(`${baseURL}${GRAPHQL_URI} returned status ${status}`);
122+
}
123+
124+
return body;
125+
});
126+
}
127+
128+
export class GraphQLClient extends HTTPClient {
129+
request({
130+
baseURL,
131+
data,
132+
accessToken,
133+
headers,
134+
}: // eslint-disable-next-line flowtype/no-weak-types
135+
any): ZalgoPromise<any> {
136+
return callGraphQLAPI({
137+
accessToken: accessToken ?? this.accessToken,
138+
data,
139+
baseURL: baseURL ?? this.baseURL ?? "",
140+
headers,
141+
});
142+
}
143+
}

Diff for: src/three-domain-secure/api.test.js

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/* @flow */
2+
import { describe, expect, vi } from "vitest";
3+
import { request } from "@krakenjs/belter/src";
4+
5+
import { callRestAPI } from "../lib";
6+
import { HEADERS } from "../constants/api";
7+
8+
import { RestClient, callGraphQLAPI, HTTPClient } from "./api";
9+
10+
vi.mock("@krakenjs/belter/src", async () => {
11+
return {
12+
...(await vi.importActual("@krakenjs/belter/src")),
13+
request: vi.fn(),
14+
};
15+
});
16+
17+
vi.mock("@paypal/sdk-client/src", async () => {
18+
return {
19+
...(await vi.importActual("@paypal/sdk-client/src")),
20+
getSessionID: () => "session_id_123",
21+
getPartnerAttributionID: () => "partner_attr_123",
22+
};
23+
});
24+
25+
vi.mock("../lib", () => ({
26+
callRestAPI: vi.fn(),
27+
}));
28+
29+
describe("API", () => {
30+
const accessToken = "access_token";
31+
const baseURL = "http://test.paypal.com:port";
32+
33+
afterEach(() => {
34+
vi.clearAllMocks();
35+
});
36+
describe("HTTPClient", () => {
37+
it("should set access token and base url in constructor", () => {
38+
const client = new HTTPClient({ accessToken, baseURL });
39+
expect(client.accessToken).toBe(accessToken);
40+
expect(client.baseURL).toBe(baseURL);
41+
});
42+
43+
it("should set access token", () => {
44+
const client = new HTTPClient();
45+
client.setAccessToken(accessToken);
46+
expect(client.accessToken).toBe(accessToken);
47+
});
48+
});
49+
50+
describe("RestClient", () => {
51+
it("should make a REST API call with correct params", () => {
52+
const data = { test: "data" };
53+
const requestOptions = {
54+
data,
55+
baseURL,
56+
};
57+
const client = new RestClient({ accessToken });
58+
client.request(requestOptions);
59+
expect(callRestAPI).toHaveBeenCalledWith({
60+
accessToken,
61+
data,
62+
url: baseURL,
63+
});
64+
});
65+
});
66+
67+
describe("callGraphQLAPI", () => {
68+
const query = '{ "test": "data" }';
69+
const variables = { option: "param1" };
70+
const gqlQuery = { query, variables };
71+
72+
const response = { data: { test: "data" } };
73+
74+
it("should throw error if no access token is provided", () => {
75+
expect(() =>
76+
callGraphQLAPI({
77+
accessToken: null,
78+
baseURL,
79+
data: gqlQuery,
80+
headers: {},
81+
})
82+
).toThrowError(
83+
new Error(
84+
`No access token passed to GraphQL request ${baseURL}/graphql`
85+
)
86+
);
87+
});
88+
89+
it("should make a GraphQL API call with correct params", () => {
90+
vi.mocked(request).mockResolvedValue({
91+
status: 200,
92+
body: response,
93+
});
94+
callGraphQLAPI({
95+
accessToken,
96+
baseURL,
97+
data: gqlQuery,
98+
headers: {},
99+
});
100+
expect(request).toHaveBeenCalledWith({
101+
method: "post",
102+
url: `${baseURL}/graphql`,
103+
headers: {
104+
[HEADERS.AUTHORIZATION]: `Bearer ${accessToken}`,
105+
[HEADERS.CONTENT_TYPE]: "application/json",
106+
[HEADERS.PARTNER_ATTRIBUTION_ID]: "partner_attr_123",
107+
[HEADERS.CLIENT_METADATA_ID]: "session_id_123",
108+
},
109+
json: gqlQuery,
110+
});
111+
});
112+
113+
it("should resolve with response body on success", async () => {
114+
vi.mocked(request).mockResolvedValue({
115+
status: 200,
116+
body: response,
117+
});
118+
const resp = await callGraphQLAPI({
119+
accessToken,
120+
baseURL,
121+
data: gqlQuery,
122+
headers: {},
123+
});
124+
expect(resp).toEqual(response);
125+
});
126+
127+
it("should throw error on error status", async () => {
128+
const status = 400;
129+
vi.mocked(request).mockResolvedValue({
130+
status,
131+
body: { message: "Something went wrong" },
132+
});
133+
134+
try {
135+
await callGraphQLAPI({
136+
accessToken,
137+
baseURL,
138+
data: gqlQuery,
139+
headers: {},
140+
});
141+
} catch (error) {
142+
expect(error.message).toBe(
143+
`${baseURL}/graphql returned status ${status}`
144+
);
145+
}
146+
});
147+
});
148+
});

0 commit comments

Comments
 (0)