From f0b5eebf36793273ea7d83f0f9f46557b1c82adb Mon Sep 17 00:00:00 2001 From: Mervin Choun <38122192+mchoun@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:11:32 -0500 Subject: [PATCH] add eligibility check and protect export for stage and local (#2451) * 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 * change name of export to devEnvOnly * move url to constants file * resolve dist merge conflict --------- Co-authored-by: Shraddha Shah --- src/constants/api.js | 1 + src/lib/security.js | 13 ++- src/three-domain-secure/component.jsx | 132 ++++++++++++++++++++-- src/three-domain-secure/component.test.js | 107 ++++++++++++++++-- src/three-domain-secure/interface.js | 17 ++- 5 files changed, 248 insertions(+), 22 deletions(-) diff --git a/src/constants/api.js b/src/constants/api.js index a2a5760707..427248c7d0 100644 --- a/src/constants/api.js +++ b/src/constants/api.js @@ -8,6 +8,7 @@ export const HEADERS = { }; export const ELIGIBLE_PAYMENT_METHODS = "v2/payments/find-eligible-methods"; +export const PAYMENT_3DS_VERIFICATION = "v2/payments/payment"; export const FPTI_TRANSITION = { SHOPPER_INSIGHTS_API_INIT: "sdk_shopper_insights_recommended_init", diff --git a/src/lib/security.js b/src/lib/security.js index 467b59c58f..a1dd3bf7b2 100644 --- a/src/lib/security.js +++ b/src/lib/security.js @@ -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()) { @@ -28,3 +29,13 @@ export function allowIframe(): boolean { export const protectedExport = (unprotectedExport) => isPayPalDomain() ? unprotectedExport : undefined; /* eslint-enable no-confusing-arrow */ + +// $FlowIssue +export const devEnvOnlyExport = (unprotectedExport) => { + const env = getEnv(); + if (env === ENV.LOCAL || env === ENV.STAGE) { + return unprotectedExport; + } else { + return undefined; + } +}; diff --git a/src/three-domain-secure/component.jsx b/src/three-domain-secure/component.jsx index d2404429d9..37fb3e9e20 100644 --- a/src/three-domain-secure/component.jsx +++ b/src/three-domain-secure/component.jsx @@ -1,17 +1,81 @@ /* @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 { 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"; +import { PAYMENT_3DS_VERIFICATION } from "../constants/api"; + +type MerchantPayloadData = {| + amount: string, + currency: string, + nonce: string, + threeDSRequested?: boolean, + transactionContext?: Object, +|}; + +// eslint-disable-next-line no-undef +type Request = ({| + method?: string, + url: string, + // eslint-disable-next-line no-undef + data: TRequestData, + accessToken: ?string, + // eslint-disable-next-line no-undef +|}) => Promise; + +type requestData = {| + intent: "THREE_DS_VERIFICATION", + payment_source: {| + card: {| + single_use_token: string, + verification_method: string, + |}, + |}, + amount: {| + currency_code: string, + value: string, + |}, + transaction_context?: {| + soft_descriptor?: string, + |}, +|}; + +type responseBody = {| + payment_id: string, + status: string, + intent: string, + payment_source: {| + card: {| + last_digits: string, + type: string, + name: string, + expiry: string, + |}, + |}, + amount: {| + currency_code: string, + value: string, + |}, + transaction_context: {| + soft_descriptor: string, + |}, + links: $ReadOnlyArray<{| + href: string, + rel: string, + method: string, + |}>, +|}; type SdkConfig = {| - sdkToken: ?string, + authenticationToken: ?string, + paypalApiDomain: string, |}; 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` ); @@ -23,29 +87,81 @@ 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; + isEligible(): Promise; show(): ZoidComponent; } export class ThreeDomainSecureComponent { logger: LoggerType; + request: Request; sdkConfig: SdkConfig; + authenticationURL: string; constructor({ logger, + request, sdkConfig, }: {| logger: LoggerType, + request: Request, sdkConfig: SdkConfig, |}) { this.logger = logger; + this.request = request; this.sdkConfig = parseSdkConfig({ sdkConfig, logger }); } - isEligible(): ZalgoPromise { - return new ZalgoPromise((resolve) => { - resolve(false); - }); + async isEligible(merchantPayload: MerchantPayloadData): Promise { + const data = parseMerchantPayload({ merchantPayload }); + try { + // $FlowFixMe + const { status, links } = await this.request({ + method: "POST", + url: `${this.sdkConfig.paypalApiDomain}/${PAYMENT_3DS_VERIFICATION}`, + data, + accessToken: this.sdkConfig.authenticationToken, + }); + + let responseStatus = false; + + if (status === "PAYER_ACTION_REQUIRED") { + this.authenticationURL = links.find( + (link) => link.rel === "payer-action" + ).href; + responseStatus = true; + } + return responseStatus; + } catch (error) { + this.logger.warn(error); + throw error; + } } show() { diff --git a/src/three-domain-secure/component.test.js b/src/three-domain-secure/component.test.js index 1b50794a4e..14a698cdeb 100644 --- a/src/three-domain-secure/component.test.js +++ b/src/three-domain-secure/component.test.js @@ -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(), @@ -18,8 +36,11 @@ const createThreeDomainSecureComponent = ({ }, } = {}) => new ThreeDomainSecureComponent({ + // $FlowFixMe sdkConfig, // $FlowIssue + request, + // $FlowIssue logger, }); @@ -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 }); }); @@ -49,7 +140,7 @@ describe("three domain secure component - initialization", () => { createThreeDomainSecureComponent({ sdkConfig: { ...defaultSdkConfig, - sdkToken: "", + authenticationToken: "", }, }) ).toThrowError( diff --git a/src/three-domain-secure/interface.js b/src/three-domain-secure/interface.js index 81fe993f2b..7289818127 100644 --- a/src/three-domain-secure/interface.js +++ b/src/three-domain-secure/interface.js @@ -1,8 +1,12 @@ /* @flow */ -import { getLogger, getSDKToken } from "@paypal/sdk-client/src"; +import { + getLogger, + getPayPalAPIDomain, + getUserIDToken, +} from "@paypal/sdk-client/src"; +import { callRestAPI, devEnvOnlyExport } from "../lib"; import type { LazyExport } from "../types"; -import { protectedExport } from "../lib"; import { ThreeDomainSecureComponent, @@ -14,12 +18,15 @@ export const ThreeDomainSecureClient: LazyExport { const threeDomainSecureInstance = new ThreeDomainSecureComponent({ logger: getLogger(), + // $FlowIssue ZalgoPromise vs Promise + request: callRestAPI, sdkConfig: { - sdkToken: getSDKToken(), + authenticationToken: getUserIDToken(), + paypalApiDomain: getPayPalAPIDomain(), }, }); - return protectedExport({ - isEligible: () => threeDomainSecureInstance.isEligible(), + return devEnvOnlyExport({ + isEligible: (payload) => threeDomainSecureInstance.isEligible(payload), show: () => threeDomainSecureInstance.show(), }); },