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

feat: implement show() method for 3DS Fastlane #2448

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
17b79af
add threeDS overlay component
siddy2181 Oct 25, 2024
d464fbc
add styling for 3DS iframe overlay
siddy2181 Oct 28, 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
e2921d5
Merge branch 'feature/DTPPCPSDK-2660-3ds-eligibility' of https://gith…
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
dfa0504
Merge branch 'feature/DTPPCPSDK-2660-3ds-eligibility' of https://gith…
siddy2181 Nov 6, 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
bb9ad0d
update overlay, save 3ds comp on eligibility to class variable
siddy2181 Nov 7, 2024
bb62853
Merge branch 'feature/DTPPCPSDK-2660-3ds-eligibility' into feature/DT…
siddy2181 Nov 7, 2024
ccac5fd
fix stage url,nit
siddy2181 Nov 7, 2024
3176b3c
Merge branch 'feature/DTPPCPSDK-2662-show' of https://github.com/payp…
siddy2181 Nov 7, 2024
53ced83
fix lint
siddy2181 Nov 8, 2024
28b76fd
fix lint
siddy2181 Nov 8, 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 __sdk__.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,6 @@ module.exports = {
},
"three-domain-secure": {
entry: "./src/three-domain-secure/interface",
globals,
},
};
2 changes: 1 addition & 1 deletion dist/button.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/test/button.js

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion src/lib/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { isSameDomain } from "@krakenjs/cross-domain-utils/src";
import { supportsPopups } from "@krakenjs/belter/src";
import { isPayPalDomain } from "@paypal/sdk-client/src";
import { getEnv, isPayPalDomain } from "@paypal/sdk-client/src";
import { ENV } from "@paypal/sdk-constants/src";

export function allowIframe(): boolean {
if (!isPayPalDomain()) {
Expand All @@ -28,3 +29,13 @@ export function allowIframe(): boolean {
export const protectedExport = (unprotectedExport) =>
isPayPalDomain() ? unprotectedExport : undefined;
/* eslint-enable no-confusing-arrow */

// $FlowIssue
export const localOrStageExport = (unprotectedExport) => {
const env = getEnv();
if (env === ENV.LOCAL || env === ENV.STAGE) {
return unprotectedExport;
} else {
return undefined;
}
};
134 changes: 120 additions & 14 deletions src/three-domain-secure/component.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
/* @flow */
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-restricted-globals, promise/no-native */
import { type LoggerType } from "@krakenjs/beaver-logger/src";
import { type ZoidComponent } from "@krakenjs/zoid/src";
import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
import { create, type ZoidComponent } from "@krakenjs/zoid/src";
import { FPTI_KEY } from "@paypal/sdk-constants/src";

import { ValidationError } from "../lib";

type SdkConfig = {|
sdkToken: ?string,
|};
import type {
requestData,
responseBody,
Request,
MerchantPayloadData,
SdkConfig,
threeDSResponse,
} from "./types";
import { getThreeDomainSecureComponent } from "./utils";

const parseSdkConfig = ({ sdkConfig, logger }): SdkConfig => {
if (!sdkConfig.sdkToken) {
if (!sdkConfig.authenticationToken) {
throw new ValidationError(
`script data attribute sdk-client-token is required but was not passed`
);
Expand All @@ -23,32 +31,130 @@ const parseSdkConfig = ({ sdkConfig, logger }): SdkConfig => {

return sdkConfig;
};

const parseMerchantPayload = ({
merchantPayload,
}: {|
merchantPayload: MerchantPayloadData,
|}): requestData => {
const { threeDSRequested, amount, currency, nonce, transactionContext } =
merchantPayload;

return {
intent: "THREE_DS_VERIFICATION",
payment_source: {
card: {
single_use_token: nonce,
verification_method: threeDSRequested
? "SCA_ALWAYS"
: "SCA_WHEN_REQUIRED",
},
},
amount: {
currency_code: currency,
value: amount,
},
...transactionContext,
};
};

export interface ThreeDomainSecureComponentInterface {
isEligible(): ZalgoPromise<boolean>;
show(): ZoidComponent<void>;
isEligible(): Promise<boolean>;
show(): ZalgoPromise<threeDSResponse>;
}
export class ThreeDomainSecureComponent {
logger: LoggerType;
request: Request;
sdkConfig: SdkConfig;

authenticationURL: string;
threeDSIframe: ZoidComponent<void>;
constructor({
logger,
request,
sdkConfig,
}: {|
logger: LoggerType,
request: Request,
sdkConfig: SdkConfig,
|}) {
this.logger = logger;
this.request = request;
this.sdkConfig = parseSdkConfig({ sdkConfig, logger });
}

isEligible(): ZalgoPromise<boolean> {
return new ZalgoPromise((resolve) => {
resolve(false);
});
async isEligible(merchantPayload: MerchantPayloadData): Promise<boolean> {
// eslint-disable-next-line no-console
console.log("Entered IsEligible");

const data = parseMerchantPayload({ merchantPayload });
const idToken = merchantPayload.idToken;
try {
// $FlowFixMe
const { status, links } = await this.request<requestData, responseBody>({
method: "POST",
url: `https://te-fastlane-3ds.qa.paypal.com:12326/v2/payments/payment`,
data,
accessToken: idToken, // this.sdkConfig.authenticationToken,
});

let responseStatus = false;

if (status === "PAYER_ACTION_REQUIRED") {
this.authenticationURL = links.find(
(link) => link.rel === "payer-action"
).href;
responseStatus = true;
this.threeDSIframe = getThreeDomainSecureComponent(
this.authenticationURL
);
}
return responseStatus;
} catch (error) {
this.logger.warn(error);
throw error;
}
}

show() {
create({ tag: "", url: "" });
show(): ZalgoPromise<threeDSResponse> {
if (!this.threeDSIframe) {
return new ValidationError(`Ineligible for three domain secure`);
}
const promise = new ZalgoPromise();
const cancelThreeDS = () => {
return ZalgoPromise.try(() => {
// eslint-disable-next-line no-console
console.log("cancelled");
}).then(() => {
// eslint-disable-next-line no-use-before-define
instance.close();
});
};

const instance = this.threeDSIframe({
onSuccess: (data) => {
// const {threeDSRefID, authentication_status, liability_shift } = data;
// let enrichedNonce;
// if(threeDSRefID) {
// enrichedNonce = await updateNonceWith3dsData(threeDSRefID, this.fastlaneNonce)
// }

return promise.resolve(data);
},
onClose: cancelThreeDS,
onError: (err) => {
return promise.reject(
new Error(
`Error with obtaining 3DS contingency, ${JSON.stringify(err)}`
)
);
},
});
const TARGET_ELEMENT = {
BODY: "body",
};
return instance
.renderTo(window.parent, TARGET_ELEMENT.BODY)
.then(() => promise)
.finally(instance.close);
}
}
107 changes: 99 additions & 8 deletions src/three-domain-secure/component.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
/* @flow */
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-restricted-globals, promise/no-native, compat/compat */
import { describe, expect, vi } from "vitest";

import { ThreeDomainSecureComponent } from "./component";

const defaultSdkConfig = {
sdkToken: "sdk-client-token",
authenticationToken: "sdk-client-token",
};

const defaultEligibilityResponse = {
status: "PAYER_ACTION_REQUIRED",
links: [{ href: "https://testurl.com", rel: "payer-action" }],
};

const defaultMerchantPayload = {
amount: "1.00",
currency: "USD",
nonce: "test-nonce",
};

const mockEligibilityRequest = (body = defaultEligibilityResponse) => {
return vi.fn().mockResolvedValue(body);
};

const createThreeDomainSecureComponent = ({
sdkConfig = defaultSdkConfig,
request = mockEligibilityRequest(),
logger = {
info: vi.fn().mockReturnThis(),
warn: vi.fn().mockReturnThis(),
Expand All @@ -18,8 +36,11 @@ const createThreeDomainSecureComponent = ({
},
} = {}) =>
new ThreeDomainSecureComponent({
// $FlowFixMe
sdkConfig,
// $FlowIssue
request,
// $FlowIssue
logger,
});

Expand All @@ -28,17 +49,87 @@ afterEach(() => {
});

describe("three domain secure component - isEligible method", () => {
test("should return false", async () => {
const threeDomainSecuretClient = createThreeDomainSecureComponent();
const eligibility = await threeDomainSecuretClient.isEligible();
test("should return true if payer action required", async () => {
const threeDomainSecureClient = createThreeDomainSecureComponent();
const eligibility = await threeDomainSecureClient.isEligible(
defaultMerchantPayload
);
expect(eligibility).toEqual(true);
});

test("should return false if payer action is not returned", async () => {
const threeDomainSecureClient = createThreeDomainSecureComponent({
request: () =>
Promise.resolve({ ...defaultEligibilityResponse, status: "SUCCESS" }),
});
const eligibility = await threeDomainSecureClient.isEligible(
defaultMerchantPayload
);
expect(eligibility).toEqual(false);
});

test("should assign correct URL to authenticationURL", async () => {
const threeDomainSecureClient = createThreeDomainSecureComponent({
request: () =>
Promise.resolve({
...defaultEligibilityResponse,
links: [
{ href: "https://not-payer-action.com", rel: "not-payer-action" },
...defaultEligibilityResponse.links,
],
}),
});
await threeDomainSecureClient.isEligible(defaultMerchantPayload);
expect(threeDomainSecureClient.authenticationURL).toEqual(
"https://testurl.com"
);
});

test("create payload with correctly parameters", async () => {
const mockedRequest = mockEligibilityRequest();
const threeDomainSecureClient = createThreeDomainSecureComponent({
request: mockedRequest,
});

await threeDomainSecureClient.isEligible(defaultMerchantPayload);

expect(mockedRequest).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
intent: "THREE_DS_VERIFICATION",
payment_source: expect.objectContaining({
card: expect.objectContaining({
single_use_token: defaultMerchantPayload.nonce,
verification_method: "SCA_WHEN_REQUIRED",
}),
}),
amount: expect.objectContaining({
currency_code: defaultMerchantPayload.currency,
value: defaultMerchantPayload.amount,
}),
}),
})
);
});

test("catch errors from the API", async () => {
const mockRequest = vi.fn().mockRejectedValue(new Error("Error with API"));
const threeDomainSecureClient = createThreeDomainSecureComponent({
request: mockRequest,
});

expect.assertions(2);
await expect(() =>
threeDomainSecureClient.isEligible(defaultMerchantPayload)
).rejects.toThrow(new Error("Error with API"));
expect(mockRequest).toHaveBeenCalled();
});
});

describe("three domain descure component - show method", () => {
test.skip("should return a zoid component", () => {
const threeDomainSecuretClient = createThreeDomainSecureComponent();
threeDomainSecuretClient.show();
test.todo("should return a zoid component", () => {
const threeDomainSecureClient = createThreeDomainSecureComponent();
threeDomainSecureClient.show();
// create test for zoid component
});
});
Expand All @@ -49,7 +140,7 @@ describe("three domain secure component - initialization", () => {
createThreeDomainSecureComponent({
sdkConfig: {
...defaultSdkConfig,
sdkToken: "",
authenticationToken: "",
},
})
).toThrowError(
Expand Down
19 changes: 13 additions & 6 deletions src/three-domain-secure/interface.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/* @flow */
import { getLogger, getSDKToken } from "@paypal/sdk-client/src";
import {
getLogger,
getPayPalAPIDomain,
getSDKToken,
} from "@paypal/sdk-client/src";

import { callRestAPI } from "../lib";
import type { LazyExport } from "../types";
import { protectedExport } from "../lib";

import {
ThreeDomainSecureComponent,
Expand All @@ -14,13 +18,16 @@ export const ThreeDomainSecureClient: LazyExport<ThreeDomainSecureComponentInter
__get__: () => {
const threeDomainSecureInstance = new ThreeDomainSecureComponent({
logger: getLogger(),
// $FlowIssue ZalgoPromise vs Promise
request: callRestAPI,
sdkConfig: {
sdkToken: getSDKToken(),
authenticationToken: getSDKToken(),
paypalApiDomain: getPayPalAPIDomain(),
},
});
return protectedExport({
isEligible: () => threeDomainSecureInstance.isEligible(),
return {
isEligible: (payload) => threeDomainSecureInstance.isEligible(payload),
show: () => threeDomainSecureInstance.show(),
});
};
},
};
Loading
Loading