diff --git a/CHANGELOG.md b/CHANGELOG.md index b311c1fe7..3d0281313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## [0.101.3](https://github.com/juspay/hyperswitch-web/compare/v0.101.2...v0.101.3) (2024-11-07) + +## [0.101.2](https://github.com/juspay/hyperswitch-web/compare/v0.101.1...v0.101.2) (2024-11-07) + +## [0.101.1](https://github.com/juspay/hyperswitch-web/compare/v0.101.0...v0.101.1) (2024-11-07) + +# [0.101.0](https://github.com/juspay/hyperswitch-web/compare/v0.100.1...v0.101.0) (2024-11-07) + + +### Features + +* added dynamic fields support for sepa bank transfer ([#775](https://github.com/juspay/hyperswitch-web/issues/775)) ([52d2d15](https://github.com/juspay/hyperswitch-web/commit/52d2d15ed2b2b8ed364a81fe9bbd3dfbd64c0cec)) + +## [0.100.1](https://github.com/juspay/hyperswitch-web/compare/v0.100.0...v0.100.1) (2024-11-06) + +# [0.100.0](https://github.com/juspay/hyperswitch-web/compare/v0.99.6...v0.100.0) (2024-11-06) + + +### Features + +* add dynamic fields support for affirm ([#776](https://github.com/juspay/hyperswitch-web/issues/776)) ([e55e533](https://github.com/juspay/hyperswitch-web/commit/e55e533a197efad11c9b8cceb4b06c672a365dfe)) + +## [0.99.6](https://github.com/juspay/hyperswitch-web/compare/v0.99.5...v0.99.6) (2024-11-06) + ## [0.99.5](https://github.com/juspay/hyperswitch-web/compare/v0.99.4...v0.99.5) (2024-11-06) ## [0.99.4](https://github.com/juspay/hyperswitch-web/compare/v0.99.3...v0.99.4) (2024-11-05) diff --git a/cypress-tests/cypress/e2e/external-3DS-netcetera-e2e-test.cy.ts b/cypress-tests/cypress/e2e/external-3DS-netcetera-e2e-test.cy.ts new file mode 100644 index 000000000..333ceb201 --- /dev/null +++ b/cypress-tests/cypress/e2e/external-3DS-netcetera-e2e-test.cy.ts @@ -0,0 +1,81 @@ +import * as testIds from "../../../src/Utilities/TestUtils.bs"; +import { getClientURL, netceteraChallengeTestCard, createPaymentBody, changeObjectKeyValue, connectorProfileIdMapping, connectorEnum } from "../support/utils"; +describe("External 3DS using Netcetera Checks", () => { + let getIframeBody: () => Cypress.Chainable<JQuery<HTMLBodyElement>>; + const publishableKey = Cypress.env('HYPERSWITCH_PUBLISHABLE_KEY') + const secretKey = Cypress.env('HYPERSWITCH_SECRET_KEY') + changeObjectKeyValue(createPaymentBody, "profile_id", connectorProfileIdMapping.get(connectorEnum.NETCETERA)) + changeObjectKeyValue(createPaymentBody, "request_external_three_ds_authentication", true) + changeObjectKeyValue(createPaymentBody, "authentication_type", "three_ds") + let iframeSelector = + "#orca-payment-element-iframeRef-orca-elements-payment-element-payment-element"; + + beforeEach(() => { + getIframeBody = () => cy.iframe(iframeSelector); + cy.createPaymentIntent(secretKey, createPaymentBody).then(() => { + cy.getGlobalState("clientSecret").then((clientSecret) => { + + cy.visit(getClientURL(clientSecret, publishableKey)); + }); + + }) + }); + + + it("title rendered correctly", () => { + cy.contains("Hyperswitch Unified Checkout").should("be.visible"); + }); + + it("orca-payment-element iframe loaded", () => { + cy.get( + "#orca-payment-element-iframeRef-orca-elements-payment-element-payment-element" + ) + .should("be.visible") + .its("0.contentDocument") + .its("body"); + }); + + + it('If the user completes the challenge, the payment should be successful.', () => { + getIframeBody().find(`[data-testid=${testIds.addNewCardIcon}]`).click() + getIframeBody().find(`[data-testid=${testIds.cardNoInputTestId}]`).type(netceteraChallengeTestCard) + getIframeBody().find(`[data-testid=${testIds.expiryInputTestId}]`).type("0444") + cy.wait(1000) + getIframeBody().find(`[data-testid=${testIds.cardCVVInputTestId}]`).type("1234") + getIframeBody().get("#submit").click(); + cy.wait(4000) + + cy.nestedIFrame("#threeDsAuthFrame", ($body) => { + cy.wrap($body).find('#otp') + .type('1234') + + cy.wrap($body).find('#sendOtp') + .click() + cy.contains("Thanks for your order!").should("be.visible"); + }) + + }) + + it('If the user closes the challenge, the payment should fail.', () => { + getIframeBody().find(`[data-testid=${testIds.addNewCardIcon}]`).click() + getIframeBody().find(`[data-testid=${testIds.cardNoInputTestId}]`).type(netceteraChallengeTestCard) + getIframeBody().find(`[data-testid=${testIds.expiryInputTestId}]`).type("0444") + cy.wait(1000) + getIframeBody().find(`[data-testid=${testIds.cardCVVInputTestId}]`).type("1234") + getIframeBody().get("#submit").click(); + cy.wait(4000) + + cy.nestedIFrame("#threeDsAuthFrame", ($body) => { + cy.wrap($body) + .find('#cancel') + .click() + cy.contains("Payment Failed!").should("be.visible"); + }) + }) + + +}) + + + + diff --git a/cypress-tests/cypress/support/commands.ts b/cypress-tests/cypress/support/commands.ts index fe84834c0..ba44cde2b 100644 --- a/cypress-tests/cypress/support/commands.ts +++ b/cypress-tests/cypress/support/commands.ts @@ -173,3 +173,11 @@ Cypress.Commands.add("createPaymentIntent", (secretKey: string, createPaymentBod Cypress.Commands.add("getGlobalState", (key: any) => { return globalState[key]; }); + +Cypress.Commands.add("nestedIFrame", (selector, callback) => { + cy.iframe("#orca-fullscreen").find(selector).should("exist").should("be.visible").then(($ele) => { + const $body = + $ele.contents().find('body') + callback($body); + }) +}); \ No newline at end of file diff --git a/cypress-tests/cypress/support/types.ts b/cypress-tests/cypress/support/types.ts index d1db388d1..1ad709b50 100644 --- a/cypress-tests/cypress/support/types.ts +++ b/cypress-tests/cypress/support/types.ts @@ -26,6 +26,7 @@ declare global { ): Chainable<JQuery<HTMLElement>> createPaymentIntent(secretKey: string, createPaymentBody: Record<string, any>): Chainable<Response<any>> getGlobalState(key: string): Chainable<Response<any>> + nestedIFrame(selector: string, callback: (body: Chainable<JQuery<HTMLElement>>) => void): Chainable<void>; } } } \ No newline at end of file diff --git a/cypress-tests/cypress/support/utils.ts b/cypress-tests/cypress/support/utils.ts index 496d55606..18a484a01 100644 --- a/cypress-tests/cypress/support/utils.ts +++ b/cypress-tests/cypress/support/utils.ts @@ -6,15 +6,17 @@ export const getClientURL = (clientSecret, publishableKey) => { return `${CLIENT_BASE_URL}?isCypressTestMode=true&clientSecret=${clientSecret}&publishableKey=${publishableKey}`; } -export const enum connectorEnum{ +export const enum connectorEnum { TRUSTPAY, ADYEN, - STRIPE + STRIPE, + NETCETERA } export const connectorProfileIdMapping = new Map<connectorEnum, string>([ [connectorEnum.TRUSTPAY, "pro_eP323T9e4ApKpilWBfPA"], [connectorEnum.ADYEN, "pro_Kvqzu8WqBZsT1OjHlCj4"], [connectorEnum.STRIPE, "pro_5fVcCxU8MFTYozgtf0P8"], + [connectorEnum.NETCETERA, "pro_h9VHXnJx8s6W4KSZfSUL"] ]); export const createPaymentBody = { @@ -78,7 +80,7 @@ export const createPaymentBody = { } -export const changeObjectKeyValue = (object: Record<string, any>, key: string, value: string) => { +export const changeObjectKeyValue = (object: Record<string, any>, key: string, value: boolean | string) => { object[key] = value } @@ -128,3 +130,4 @@ export const adyenTestCard = "4917610000000000"; export const bluesnapTestCard = "4000000000001091"; export const amexTestCard = "378282246310005" export const visaTestCard = "4242424242424242"; +export const netceteraChallengeTestCard = "348638267931507"; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index facd283d8..76d00eb7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "orca-payment-page", - "version": "0.99.5", + "version": "0.101.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "orca-payment-page", - "version": "0.99.5", + "version": "0.101.3", "hasInstallScript": true, "dependencies": { "@glennsl/rescript-fetch": "^0.2.0", diff --git a/package.json b/package.json index e87738c56..d6b56eb7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orca-payment-page", - "version": "0.99.5", + "version": "0.101.3", "main": "index.js", "private": true, "dependencies": { diff --git a/src/Components/DynamicFields.res b/src/Components/DynamicFields.res index ee1fe6e3d..2c6f9ea30 100644 --- a/src/Components/DynamicFields.res +++ b/src/Components/DynamicFields.res @@ -1,4 +1,3 @@ -open RecoilAtoms @react.component let make = ( ~paymentType, @@ -12,7 +11,9 @@ let make = ( ~cvcProps=None, ~isBancontact=false, ) => { + open DynamicFieldsUtils open Utils + open RecoilAtoms let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) React.useEffect(() => { @@ -41,10 +42,8 @@ let make = ( paymentMethodTypes.required_fields ->Array.concat(creditRequiredFields) - ->DynamicFieldsUtils.removeRequiredFieldsDuplicates - } else if ( - PaymentMethodsRecord.dynamicFieldsEnabledPaymentMethods->Array.includes(paymentMethodType) - ) { + ->removeRequiredFieldsDuplicates + } else if dynamicFieldsEnabledPaymentMethods->Array.includes(paymentMethodType) { paymentMethodTypes.required_fields } else { [] @@ -52,9 +51,7 @@ let make = ( }, (paymentMethod, paymentMethodTypes.required_fields, paymentMethodType)) let requiredFields = React.useMemo(() => { - requiredFieldsWithBillingDetails->DynamicFieldsUtils.removeBillingDetailsIfUseBillingAddress( - billingAddress, - ) + requiredFieldsWithBillingDetails->removeBillingDetailsIfUseBillingAddress(billingAddress) }, [requiredFieldsWithBillingDetails]) let isAllStoredCardsHaveName = React.useMemo(() => { @@ -69,7 +66,7 @@ let make = ( ~isSavedCardFlow, ~isAllStoredCardsHaveName, ) - ->DynamicFieldsUtils.updateDynamicFields(billingAddress) + ->updateDynamicFields(billingAddress) ->Belt.SortArray.stableSortBy(PaymentMethodsRecord.sortPaymentMethodFields) //<...>// }, (requiredFields, isAllStoredCardsHaveName, isSavedCardFlow)) @@ -255,7 +252,7 @@ let make = ( } } - DynamicFieldsUtils.useRequiredFieldsEmptyAndValid( + useRequiredFieldsEmptyAndValid( ~requiredFields, ~fieldsArr, ~countryNames, @@ -268,14 +265,14 @@ let make = ( ~cvcNumber, ) - DynamicFieldsUtils.useSetInitialRequiredFields( + useSetInitialRequiredFields( ~requiredFields={ billingAddress.usePrefilledValues === Auto ? requiredFieldsWithBillingDetails : requiredFields }, ~paymentMethodType, ) - DynamicFieldsUtils.useRequiredFieldsBody( + useRequiredFieldsBody( ~requiredFields, ~paymentMethodType, ~cardNumber, @@ -286,7 +283,7 @@ let make = ( ~setRequiredFieldsBody, ) - let submitCallback = DynamicFieldsUtils.useSubmitCallback() + let submitCallback = useSubmitCallback() useSubmitPaymentData(submitCallback) let bottomElement = <InfoElement /> @@ -307,11 +304,11 @@ let make = ( } let dynamicFieldsToRenderOutsideBilling = React.useMemo(() => { - fieldsArr->Array.filter(DynamicFieldsUtils.isFieldTypeToRenderOutsideBilling) + fieldsArr->Array.filter(isFieldTypeToRenderOutsideBilling) }, [fieldsArr]) let dynamicFieldsToRenderInsideBilling = React.useMemo(() => { - fieldsArr->Array.filter(field => !(field->DynamicFieldsUtils.isFieldTypeToRenderOutsideBilling)) + fieldsArr->Array.filter(field => !(field->isFieldTypeToRenderOutsideBilling)) }, [fieldsArr]) let isInfoElementPresent = dynamicFieldsToRenderInsideBilling->Array.includes(InfoElement) diff --git a/src/Components/SavedMethods.res b/src/Components/SavedMethods.res index ae2dbdf1f..377deb887 100644 --- a/src/Components/SavedMethods.res +++ b/src/Components/SavedMethods.res @@ -147,11 +147,7 @@ let make = ( | _ => // TODO - To be replaced with proper error message intent( - ~bodyArr=savedPaymentMethodBody - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict, + ~bodyArr=savedPaymentMethodBody->mergeAndFlattenToTuples(requiredFieldsBody), ~confirmParam=confirm.confirmParams, ~handleUserError=false, ~manualRetry=isManualRetryEnabled, @@ -168,11 +164,7 @@ let make = ( | _ => // TODO - To be replaced with proper error message intent( - ~bodyArr=savedPaymentMethodBody - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict, + ~bodyArr=savedPaymentMethodBody->mergeAndFlattenToTuples(requiredFieldsBody), ~confirmParam=confirm.confirmParams, ~handleUserError=false, ~manualRetry=isManualRetryEnabled, @@ -180,11 +172,7 @@ let make = ( } | _ => intent( - ~bodyArr=savedPaymentMethodBody - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict, + ~bodyArr=savedPaymentMethodBody->mergeAndFlattenToTuples(requiredFieldsBody), ~confirmParam=confirm.confirmParams, ~handleUserError=false, ~manualRetry=isManualRetryEnabled, diff --git a/src/Hooks/Fetcher.res b/src/Hooks/Fetcher.res deleted file mode 100644 index ebd789fdb..000000000 --- a/src/Hooks/Fetcher.res +++ /dev/null @@ -1,22 +0,0 @@ -type match -type pathname = {match: match} -type url = {pathname: pathname} -@new external url: string => url = "URL" - -let useFetcher = fileName => { - let _url = RescriptReactRouter.useUrl() - let (optionalJson, setJson) = React.useState(() => None) - React.useEffect(() => { - open Promise - Fetch.get(`${Window.Location.hostname}/json/${fileName}.json`) - ->then(Fetch.Response.json) - ->thenResolve(json => { - setJson(_ => Some(json)) - }) - ->ignore - - None - }, [fileName]) - - optionalJson -} diff --git a/src/PaymentMethodCollectElement.res b/src/PaymentMethodCollectElement.res index 8a3679d25..366750c6a 100644 --- a/src/PaymentMethodCollectElement.res +++ b/src/PaymentMethodCollectElement.res @@ -74,7 +74,7 @@ let make = (~integrateError, ~logger) => { setTimeout(() => { clearInterval(interval) // Append query params and redirect - let url = PaymentHelpers.urlSearch(returnUrl) + let url = URLModule.makeUrl(returnUrl) url.searchParams.set("payout_id", options.payoutId) url.searchParams.set("status", statusInfo.status->getPayoutStatusString) Utils.openUrl(url.href) diff --git a/src/Payments/ACHBankTransfer.res b/src/Payments/ACHBankTransfer.res index c4665addb..c7fc5b913 100644 --- a/src/Payments/ACHBankTransfer.res +++ b/src/Payments/ACHBankTransfer.res @@ -10,7 +10,8 @@ let make = (~paymentType: CardThemeType.mode) => { let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), BankTransfer) let (email, _) = Recoil.useLoggedRecoilState(userEmailAddress, "email", loggerState) let setComplete = Recoil.useSetRecoilState(fieldsComplete) - let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) + + let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) let complete = email.value != "" && email.isValid->Option.getOr(false) let empty = email.value == "" @@ -27,9 +28,12 @@ let make = (~paymentType: CardThemeType.mode) => { let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper if confirm.doSubmit { if complete { - let (connectors, _) = paymentMethodListValue->PaymentUtils.getConnectors(BankTransfer(ACH)) + let bodyArr = + PaymentBody.dynamicPaymentBody("bank_transfer", "ach")->mergeAndFlattenToTuples( + requiredFieldsBody, + ) intent( - ~bodyArr=PaymentBody.achBankTransferBody(~email=email.value, ~connectors), + ~bodyArr, ~confirmParam=confirm.confirmParams, ~handleUserError=false, ~iframeId, @@ -43,7 +47,9 @@ let make = (~paymentType: CardThemeType.mode) => { useSubmitPaymentData(submitCallback) <div className="flex flex-col animate-slowShow" style={gridGap: themeObj.spacingTab}> - <EmailPaymentInput paymentType /> + <DynamicFields + paymentType paymentMethod="bank_transfer" paymentMethodType="ach" setRequiredFieldsBody + /> <Surcharge paymentMethod="bank_transfer" paymentMethodType="ach" /> <InfoElement /> </div> diff --git a/src/Payments/CardPayment.res b/src/Payments/CardPayment.res index da4da5c98..05c540660 100644 --- a/src/Payments/CardPayment.res +++ b/src/Payments/CardPayment.res @@ -163,11 +163,7 @@ let make = ( if validFormat && (showFields || isBancontact) { intent( ~bodyArr={ - (isBancontact ? banContactBody : cardBody) - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict + (isBancontact ? banContactBody : cardBody)->mergeAndFlattenToTuples(requiredFieldsBody) }, ~confirmParam=confirm.confirmParams, ~handleUserError=false, diff --git a/src/Payments/KlarnaSDK.res b/src/Payments/KlarnaSDK.res index a53771a7b..ea7ab5303 100644 --- a/src/Payments/KlarnaSDK.res +++ b/src/Payments/KlarnaSDK.res @@ -100,11 +100,7 @@ let make = (~sessionObj: SessionsType.token) => { ) let body = { - klarnaSDKBody - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict + klarnaSDKBody->mergeAndFlattenToTuples(requiredFieldsBody) } res.approved diff --git a/src/Payments/PaymentMethodsRecord.res b/src/Payments/PaymentMethodsRecord.res index d235c6bec..050542ba3 100644 --- a/src/Payments/PaymentMethodsRecord.res +++ b/src/Payments/PaymentMethodsRecord.res @@ -661,27 +661,6 @@ let getFieldType = (dict, isBancontact) => { } } -let dynamicFieldsEnabledPaymentMethods = [ - "crypto_currency", - "debit", - "credit", - "blik", - "google_pay", - "apple_pay", - "bancontact_card", - "open_banking_uk", - "eps", - "ideal", - "sofort", - "pix_transfer", - "giropay", - "local_bank_transfer_transfer", - "afterpay_clearpay", - "mifinity", - "upi_collect", - "sepa", -] - let getIsBillingField = requiredFieldType => { switch requiredFieldType { | AddressLine1 diff --git a/src/Payments/PaymentMethodsWrapper.res b/src/Payments/PaymentMethodsWrapper.res index 857a796e8..cb4cf0627 100644 --- a/src/Payments/PaymentMethodsWrapper.res +++ b/src/Payments/PaymentMethodsWrapper.res @@ -84,12 +84,7 @@ let make = (~paymentType: CardThemeType.mode, ~paymentMethodName: string) => { phoneNumber.countryCode->Option.getOr("") ++ phoneNumber.value, ), ~paymentExperience=paymentFlow, - ) - ->Dict.fromArray - ->JSON.Encode.object - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict + )->mergeAndFlattenToTuples(requiredFieldsBody) intent( ~bodyArr=body, diff --git a/src/Payments/PaypalSDKHelpers.res b/src/Payments/PaypalSDKHelpers.res index 96603bf72..d50d0a33a 100644 --- a/src/Payments/PaypalSDKHelpers.res +++ b/src/Payments/PaypalSDKHelpers.res @@ -1,5 +1,4 @@ open PaypalSDKTypes -open Promise open Utils open TaxCalculation @@ -11,13 +10,13 @@ let loadPaypalSDK = ( ~isManualRetryEnabled, ~paymentMethodListValue, ~isGuestCustomer, - ~postSessionTokens: PaymentHelpers.paymentIntent, + ~postSessionTokens: PaymentHelpersTypes.paymentIntent, ~options: PaymentType.options, ~publishableKey, ~paymentMethodTypes, ~stateJson, - ~confirm: PaymentHelpers.paymentIntent, - ~completeAuthorize: PaymentHelpers.completeAuthorize, + ~confirm: PaymentHelpersTypes.paymentIntent, + ~completeAuthorize: PaymentHelpersTypes.completeAuthorize, ~handleCloseLoader, ~areOneClickWalletsRendered: ( RecoilAtoms.areOneClickWalletsRendered => RecoilAtoms.areOneClickWalletsRendered @@ -57,10 +56,10 @@ let loadPaypalSDK = ( style: buttonStyle, fundingSource: paypal["FUNDING"]["PAYPAL"], createOrder: () => { - Utils.makeOneClickHandlerPromise(sdkHandleIsThere)->then(result => { + makeOneClickHandlerPromise(sdkHandleIsThere)->Promise.then(result => { let result = result->JSON.Decode.bool->Option.getOr(false) if result { - Utils.messageParentWindow([ + messageParentWindow([ ("fullscreen", true->JSON.Encode.bool), ("param", "paymentloader"->JSON.Encode.string), ("iframeId", iframeId->JSON.Encode.string), @@ -84,9 +83,9 @@ let loadPaypalSDK = ( ~handleUserError=true, ~intentCallback=val => { val - ->Utils.getDictFromJson - ->Utils.getDictFromDict("nextActionData") - ->Utils.getString("order_id", "") + ->getDictFromJson + ->getDictFromDict("nextActionData") + ->getString("order_id", "") ->resolve }, ~manualRetry=isManualRetryEnabled, @@ -99,8 +98,7 @@ let loadPaypalSDK = ( publishableKey, }, ~handleUserError=true, - ~intentCallback=val => - val->Utils.getDictFromJson->Utils.getString("orderId", "")->resolve, + ~intentCallback=val => val->getDictFromJson->getString("orderId", "")->resolve, ~manualRetry=isManualRetryEnabled, ) } @@ -111,7 +109,7 @@ let loadPaypalSDK = ( ~eventName=PAYPAL_SDK_FLOW, ~paymentMethod="PAYPAL", ) - resolve("") + Promise.resolve("") } }) }, @@ -141,23 +139,23 @@ let loadPaypalSDK = ( ~sessionId=data->getDictFromJson->Dict.get("orderID"), ) } else { - Js.Json.null->Js.Promise.resolve + JSON.Encode.null->Promise.resolve } }, onApprove: (_data, actions) => { if !options.readOnly { actions.order.get() - ->then(val => { + ->Promise.then(val => { let purchaseUnit = val - ->Utils.getDictFromJson - ->Utils.getArray("purchase_units") + ->getDictFromJson + ->getArray("purchase_units") ->Array.get(0) ->Option.flatMap(JSON.Decode.object) ->Option.getOr(Dict.make()) let payerDetails = val - ->Utils.getDictFromJson + ->getDictFromJson ->Dict.get("payer") ->Option.flatMap(JSON.Decode.object) ->Option.getOr(Dict.make()) @@ -172,7 +170,7 @@ let loadPaypalSDK = ( let (connectors, _) = paymentMethodListValue->PaymentUtils.getConnectors(Wallets(Paypal(SDK))) - let orderId = val->getDictFromJson->Utils.getString("id", "") + let orderId = val->getDictFromJson->getString("id", "") let body = PaymentBody.paypalSdkBody(~token=orderId, ~connectors) let modifiedPaymentBody = PaymentUtils.appendedCustomerAcceptance( ~isGuestCustomer, @@ -183,8 +181,8 @@ let loadPaypalSDK = ( let bodyArr = requiredFieldsBody ->JSON.Encode.object - ->Utils.unflattenObject - ->Utils.getArrayOfTupleFromDict + ->unflattenObject + ->getArrayOfTupleFromDict let confirmBody = bodyArr->Array.concatMany([modifiedPaymentBody]) Promise.make((_resolve, _) => { @@ -234,7 +232,7 @@ let loadBraintreePaypalSdk = ( ~iframeId, ~paymentMethodListValue, ~isGuestCustomer, - ~intent: PaymentHelpers.paymentIntent, + ~intent: PaymentHelpersTypes.paymentIntent, ~options: PaymentType.options, ~orderDetails, ~publishableKey, @@ -251,8 +249,8 @@ let loadBraintreePaypalSdk = ( ~eventName=PAYPAL_SDK_FLOW, ~paymentMethod="PAYPAL", ) - Utils.makeOneClickHandlerPromise(sdkHandleOneClickConfirmPayment) - ->then(result => { + makeOneClickHandlerPromise(sdkHandleOneClickConfirmPayment) + ->Promise.then(result => { let result = result->JSON.Decode.bool->Option.getOr(false) if result { braintree.client.create({authorization: token}, (clientErr, clientInstance) => { @@ -276,7 +274,7 @@ let loadBraintreePaypalSdk = ( fundingSource: paypal["FUNDING"]["PAYPAL"], createBillingAgreement: () => { //Paypal Clicked - Utils.messageParentWindow([ + messageParentWindow([ ("fullscreen", true->JSON.Encode.bool), ("param", "paymentloader"->JSON.Encode.string), ("iframeId", iframeId->JSON.Encode.string), @@ -301,12 +299,7 @@ let loadBraintreePaypalSdk = ( ~statesList=stateJson, ) - let paypalBody = - body - ->Utils.getJsonFromArrayOfJson - ->Utils.flattenObject(true) - ->Utils.mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->Utils.getArrayOfTupleFromDict + let paypalBody = body->mergeAndFlattenToTuples(requiredFieldsBody) let modifiedPaymentBody = PaymentUtils.appendedCustomerAcceptance( ~isGuestCustomer, @@ -345,7 +338,7 @@ let loadBraintreePaypalSdk = ( ) })->ignore } - resolve() + Promise.resolve() }) ->ignore } diff --git a/src/Payments/PreMountLoader.res b/src/Payments/PreMountLoader.res index 8f976ca43..1c6ecbc57 100644 --- a/src/Payments/PreMountLoader.res +++ b/src/Payments/PreMountLoader.res @@ -1,64 +1,72 @@ -@react.component -let make = ( - ~sessionId, - ~publishableKey, - ~clientSecret, - ~endpoint, - ~ephemeralKey, - ~hyperComponentName: Types.hyperComponentName, - ~merchantHostname, - ~customPodUri, -) => { - open Utils - let (paymentMethodsResponseSent, setPaymentMethodsResponseSent) = React.useState(_ => false) - let ( - customerPaymentMethodsResponseSent, - setCustomerPaymentMethodsResponseSent, - ) = React.useState(_ => false) - let (savedPaymentMethodsResponseSent, setSavedPaymentMethodsResponseSent) = React.useState(_ => - false - ) - let (sessionTokensResponseSent, setSessionTokensResponseSent) = React.useState(_ => false) - let logger = HyperLogger.make( - ~sessionId, - ~source=Loader, - ~merchantId=publishableKey, - ~clientSecret, - ) +let sendPromiseData = (promise, key) => { + let executePromise = async () => { + let response = try { + await promise + } catch { + | _ => JSON.Encode.null + } + Utils.messageParentWindow([("response", response), ("data", key->JSON.Encode.string)]) + } + executePromise()->ignore +} + +let useMessageHandler = getPromisesAndHandler => { + React.useEffect(_ => { + let (promises, messageHandler) = getPromisesAndHandler() + let setupMessageListener = _ => { + Utils.messageParentWindow([("preMountLoaderIframeMountedCallback", true->JSON.Encode.bool)]) + Window.addEventListener("message", messageHandler) + } + + let cleanupMessageListener = _ => { + Utils.messageParentWindow([("preMountLoaderIframeUnMount", true->JSON.Encode.bool)]) + Window.removeEventListener("message", messageHandler) + } + + setupMessageListener() + + let executeAllPromises = async () => { + try { + let _ = await Promise.all(promises) + } catch { + | error => Console.error2("Error in message handler:", error) + } + cleanupMessageListener() + } + executeAllPromises()->ignore - let ( - paymentMethodsResponse, - customerPaymentMethodsResponse, - sessionTokensResponse, - savedPaymentMethodsResponse, - ) = React.useMemo0(() => { - let paymentMethodsResponse = switch hyperComponentName { - | Elements => - PaymentHelpers.fetchPaymentMethodList( + Some(cleanupMessageListener) + }, []) +} + +module PreMountLoaderForElements = { + @react.component + let make = ( + ~logger, + ~publishableKey, + ~clientSecret, + ~endpoint, + ~merchantHostname, + ~customPodUri, + ) => { + useMessageHandler(() => { + let paymentMethodsPromise = PaymentHelpers.fetchPaymentMethodList( ~clientSecret, ~publishableKey, ~logger, ~customPodUri, ~endpoint, ) - | _ => JSON.Encode.null->Promise.resolve - } - let customerPaymentMethodsResponse = switch hyperComponentName { - | Elements => - PaymentHelpers.fetchCustomerPaymentMethodList( + let customerPaymentMethodsPromise = PaymentHelpers.fetchCustomerPaymentMethodList( ~clientSecret, ~publishableKey, ~optLogger=Some(logger), ~customPodUri, ~endpoint, ) - | _ => JSON.Encode.null->Promise.resolve - } - let sessionTokensResponse = switch hyperComponentName { - | Elements => - PaymentHelpers.fetchSessions( + let sessionTokensPromise = PaymentHelpers.fetchSessions( ~clientSecret, ~publishableKey, ~optLogger=Some(logger), @@ -66,101 +74,78 @@ let make = ( ~endpoint, ~merchantHostname, ) - | _ => JSON.Encode.null->Promise.resolve - } - let savedPaymentMethodsResponse = switch hyperComponentName { - | PaymentMethodsManagementElements => - PaymentHelpers.fetchSavedPaymentMethodList( + let messageHandler = (ev: Window.event) => { + open Utils + let dict = ev.data->safeParse->getDictFromJson + if dict->isKeyPresentInDict("sendPaymentMethodsResponse") { + paymentMethodsPromise->sendPromiseData("payment_methods") + } else if dict->isKeyPresentInDict("sendCustomerPaymentMethodsResponse") { + customerPaymentMethodsPromise->sendPromiseData("customer_payment_methods") + } else if dict->isKeyPresentInDict("sendSessionTokensResponse") { + sessionTokensPromise->sendPromiseData("session_tokens") + } + } + + let promises = [paymentMethodsPromise, customerPaymentMethodsPromise, sessionTokensPromise] + (promises, messageHandler) + }) + + React.null + } +} + +module PreMountLoaderForPMMElements = { + @react.component + let make = (~logger, ~endpoint, ~ephemeralKey, ~customPodUri) => { + useMessageHandler(() => { + let savedPaymentMethodsPromise = PaymentHelpers.fetchSavedPaymentMethodList( ~ephemeralKey, ~optLogger=Some(logger), ~customPodUri, ~endpoint, ) - | _ => JSON.Encode.null->Promise.resolve - } - ( - paymentMethodsResponse, - customerPaymentMethodsResponse, - sessionTokensResponse, - savedPaymentMethodsResponse, - ) - }) - - let sendPromiseData = (promise, key) => { - open Promise - promise - ->then(res => { - messageParentWindow([("response", res), ("data", key->JSON.Encode.string)]) - switch key { - | "payment_methods" => setPaymentMethodsResponseSent(_ => true) - | "session_tokens" => setSessionTokensResponseSent(_ => true) - | "customer_payment_methods" => setCustomerPaymentMethodsResponseSent(_ => true) - | "saved_payment_methods" => setSavedPaymentMethodsResponseSent(_ => true) - | _ => () + let messageHandler = (ev: Window.event) => { + open Utils + let dict = ev.data->safeParse->getDictFromJson + if dict->isKeyPresentInDict("sendSavedPaymentMethodsResponse") { + savedPaymentMethodsPromise->sendPromiseData("saved_payment_methods") + } } - resolve() - }) - ->catch(_err => { - messageParentWindow([("response", JSON.Encode.null), ("data", key->JSON.Encode.string)]) - resolve() + + let promises = [savedPaymentMethodsPromise] + (promises, messageHandler) }) - ->ignore - } - let handle = (ev: Window.event) => { - let json = ev.data->safeParse - let dict = json->Utils.getDictFromJson - if dict->Dict.get("sendPaymentMethodsResponse")->Option.isSome { - paymentMethodsResponse->sendPromiseData("payment_methods") - } else if dict->Dict.get("sendCustomerPaymentMethodsResponse")->Option.isSome { - customerPaymentMethodsResponse->sendPromiseData("customer_payment_methods") - } else if dict->Dict.get("sendSessionTokensResponse")->Option.isSome { - sessionTokensResponse->sendPromiseData("session_tokens") - } else if dict->Dict.get("sendSavedPaymentMethodsResponse")->Belt.Option.isSome { - savedPaymentMethodsResponse->sendPromiseData("saved_payment_methods") - } + React.null } +} - React.useEffect0(() => { - Window.addEventListener("message", handle) - messageParentWindow([("preMountLoaderIframeMountedCallback", true->JSON.Encode.bool)]) - Some( - () => { - Window.removeEventListener("message", handle) - }, - ) - }) - - React.useEffect4(() => { - let handleUnmount = () => { - messageParentWindow([("preMountLoaderIframeUnMount", true->JSON.Encode.bool)]) - Window.removeEventListener("message", handle) - } - - switch hyperComponentName { - | Elements => - if ( - paymentMethodsResponseSent && - customerPaymentMethodsResponseSent && - sessionTokensResponseSent - ) { - handleUnmount() - } - | PaymentMethodsManagementElements => - if savedPaymentMethodsResponseSent { - handleUnmount() - } - } - - None - }, ( - paymentMethodsResponseSent, - customerPaymentMethodsResponseSent, - sessionTokensResponseSent, - savedPaymentMethodsResponseSent, - )) +@react.component +let make = ( + ~sessionId, + ~publishableKey, + ~clientSecret, + ~endpoint, + ~ephemeralKey, + ~hyperComponentName: Types.hyperComponentName, + ~merchantHostname, + ~customPodUri, +) => { + let logger = HyperLogger.make( + ~sessionId, + ~source=Loader, + ~merchantId=publishableKey, + ~clientSecret, + ) - React.null + switch hyperComponentName { + | Elements => + <PreMountLoaderForElements + logger publishableKey clientSecret endpoint merchantHostname customPodUri + /> + | PaymentMethodsManagementElements => + <PreMountLoaderForPMMElements logger endpoint ephemeralKey customPodUri /> + } } diff --git a/src/Payments/SepaBankTransfer.res b/src/Payments/SepaBankTransfer.res index fd88fb89b..ca543425e 100644 --- a/src/Payments/SepaBankTransfer.res +++ b/src/Payments/SepaBankTransfer.res @@ -4,27 +4,17 @@ open Utils @react.component let make = (~paymentType) => { let {iframeId} = Recoil.useRecoilValueFromAtom(keys) - let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom) - let {config, themeObj, localeString} = Recoil.useRecoilValueFromAtom(configAtom) - let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), BankTransfer) + let {themeObj} = Recoil.useRecoilValueFromAtom(configAtom) let isManualRetryEnabled = Recoil.useRecoilValueFromAtom(isManualRetryEnabled) - let (country, setCountry) = React.useState(_ => "France") + let setComplete = Recoil.useSetRecoilState(fieldsComplete) let (email, _) = Recoil.useLoggedRecoilState(userEmailAddress, "email", loggerState) let (fullName, _) = Recoil.useLoggedRecoilState(userFullName, "fullName", loggerState) - let showAddressDetails = PaymentType.getShowAddressDetails( - ~billingDetails=fields.billingDetails, - ~logger=loggerState, - ) - let countryNames = Utils.getCountryNames(Country.country) - let setComplete = Recoil.useSetRecoilState(fieldsComplete) - let clientCountryCode = - Country.country - ->Array.find(item => item.countryName == country) - ->Option.getOr(Country.defaultTimeZone) + let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), BankTransfer) + + let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) let complete = email.value != "" && fullName.value != "" && email.isValid->Option.getOr(false) let empty = email.value == "" || fullName.value == "" - let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="bank_transfer") @@ -38,14 +28,13 @@ let make = (~paymentType) => { let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper if confirm.doSubmit { if complete { - let (connectors, _) = paymentMethodListValue->PaymentUtils.getConnectors(BankTransfer(Sepa)) + let bodyArr = + PaymentBody.dynamicPaymentBody("bank_transfer", "sepa")->mergeAndFlattenToTuples( + requiredFieldsBody, + ) + intent( - ~bodyArr=PaymentBody.sepaBankTransferBody( - ~email=email.value, - ~name=fullName.value, - ~country=clientCountryCode.isoAlpha2, - ~connectors, - ), + ~bodyArr, ~confirmParam=confirm.confirmParams, ~handleUserError=false, ~iframeId, @@ -55,25 +44,13 @@ let make = (~paymentType) => { postFailedSubmitResponse(~errortype="validation_error", ~message="Please enter all fields") } } - }, (email, fullName, country, isManualRetryEnabled)) + }, (email, fullName, isManualRetryEnabled)) useSubmitPaymentData(submitCallback) - let updatedOptionsArrayForCountry = - countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray - <div className="flex flex-col animate-slowShow" style={gridGap: themeObj.spacingTab}> - <EmailPaymentInput paymentType /> - <FullNamePaymentInput paymentType={paymentType} /> - <RenderIf condition={showAddressDetails.country == Auto}> - <DropdownField - appearance=config.appearance - fieldName=localeString.countryLabel - value=country - setValue=setCountry - disabled=false - options=updatedOptionsArrayForCountry - /> - </RenderIf> + <DynamicFields + paymentType paymentMethod="bank_transfer" paymentMethodType="sepa" setRequiredFieldsBody + /> <Surcharge paymentMethod="bank_transfer" paymentMethodType="sepa" /> <InfoElement /> </div> diff --git a/src/Utilities/ApplePayHelpers.res b/src/Utilities/ApplePayHelpers.res index b337e3ede..3a342cacd 100644 --- a/src/Utilities/ApplePayHelpers.res +++ b/src/Utilities/ApplePayHelpers.res @@ -7,7 +7,7 @@ let processPayment = ( ~isThirdPartyFlow=false, ~isGuestCustomer, ~paymentMethodListValue=PaymentMethodsRecord.defaultList, - ~intent: PaymentHelpers.paymentIntent, + ~intent: PaymentHelpersTypes.paymentIntent, ~options: PaymentType.options, ~publishableKey, ~isManualRetryEnabled, @@ -61,11 +61,7 @@ let getApplePayFromResponse = ( let bodyDict = PaymentBody.applePayBody(~token, ~connectors) - bodyDict - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict + bodyDict->mergeAndFlattenToTuples(requiredFieldsBody) } let startApplePaySession = ( @@ -261,11 +257,7 @@ let useHandleApplePayResponse = ( let bodyArr = if isWallet { applePayBody } else { - applePayBody - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict + applePayBody->mergeAndFlattenToTuples(requiredFieldsBody) } processPayment( diff --git a/src/Utilities/DynamicFieldsUtils.res b/src/Utilities/DynamicFieldsUtils.res index 380d087ed..4861d6f45 100644 --- a/src/Utilities/DynamicFieldsUtils.res +++ b/src/Utilities/DynamicFieldsUtils.res @@ -1,5 +1,28 @@ open RecoilAtoms +let dynamicFieldsEnabledPaymentMethods = [ + "crypto_currency", + "debit", + "credit", + "blik", + "google_pay", + "apple_pay", + "bancontact_card", + "open_banking_uk", + "eps", + "ideal", + "sofort", + "pix_transfer", + "giropay", + "local_bank_transfer_transfer", + "afterpay_clearpay", + "mifinity", + "upi_collect", + "sepa", + "affirm", + "ach", +] + let getName = (item: PaymentMethodsRecord.required_fields, field: RecoilAtomTypes.field) => { let fieldNameArr = field.value->String.split(" ") let requiredFieldsArr = item.required_field->String.split(".") diff --git a/src/Utilities/GooglePayHelpers.res b/src/Utilities/GooglePayHelpers.res index 1f1118e57..8552941d3 100644 --- a/src/Utilities/GooglePayHelpers.res +++ b/src/Utilities/GooglePayHelpers.res @@ -53,17 +53,13 @@ let getGooglePayBodyFromResponse = ( ) } - gPayBody - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict + gPayBody->mergeAndFlattenToTuples(requiredFieldsBody) } let processPayment = ( ~body: array<(string, JSON.t)>, ~isThirdPartyFlow=false, - ~intent: PaymentHelpers.paymentIntent, + ~intent: PaymentHelpersTypes.paymentIntent, ~options: PaymentType.options, ~publishableKey, ~isManualRetryEnabled, @@ -123,11 +119,7 @@ let useHandleGooglePayResponse = ( let googlePayBody = if isWallet { body } else { - body - ->getJsonFromArrayOfJson - ->flattenObject(true) - ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) - ->getArrayOfTupleFromDict + body->mergeAndFlattenToTuples(requiredFieldsBody) } processPayment( @@ -151,12 +143,7 @@ let useHandleGooglePayResponse = ( }, (paymentMethodTypes, stateJson, isManualRetryEnabled, requiredFieldsBody, isWallet)) } -let handleGooglePayClicked = ( - ~sessionObj, - ~componentName, - ~iframeId, - ~readOnly, -) => { +let handleGooglePayClicked = (~sessionObj, ~componentName, ~iframeId, ~readOnly) => { let paymentDataRequest = GooglePayType.getPaymentDataFromSession(~sessionObj, ~componentName) messageParentWindow([ ("fullscreen", true->JSON.Encode.bool), @@ -183,12 +170,7 @@ let useSubmitCallback = (~isWallet, ~sessionObj, ~componentName) => { let json = ev.data->safeParse let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper if confirm.doSubmit && areRequiredFieldsValid && !areRequiredFieldsEmpty { - handleGooglePayClicked( - ~sessionObj, - ~componentName, - ~iframeId, - ~readOnly=options.readOnly, - ) + handleGooglePayClicked(~sessionObj, ~componentName, ~iframeId, ~readOnly=options.readOnly) } else if areRequiredFieldsEmpty { postFailedSubmitResponse( ~errortype="validation_error", diff --git a/src/Utilities/PaymentBody.res b/src/Utilities/PaymentBody.res index 2a5a8e75e..eaa5c76bb 100644 --- a/src/Utilities/PaymentBody.res +++ b/src/Utilities/PaymentBody.res @@ -654,21 +654,6 @@ let epsBody = (~name, ~bankName) => [ ), ] -let achBankTransferBody = (~email, ~connectors) => [ - ("payment_method", "bank_transfer"->JSON.Encode.string), - ("connector", connectors->Utils.getArrofJsonString->JSON.Encode.array), - ("payment_method_type", "ach"->JSON.Encode.string), - ( - "payment_method_data", - [ - ("billing", [("email", email->JSON.Encode.string)]->Utils.getJsonFromArrayOfJson), - ( - "bank_transfer", - [("ach_bank_transfer", Dict.make()->JSON.Encode.object)]->Utils.getJsonFromArrayOfJson, - ), - ]->Utils.getJsonFromArrayOfJson, - ), -] let bacsBankTransferBody = (~email, ~name, ~connectors) => { let (firstName, lastName) = name->Utils.getFirstAndLastNameFromFullName @@ -698,38 +683,6 @@ let bacsBankTransferBody = (~email, ~name, ~connectors) => { ] } -let sepaBankTransferBody = (~email, ~name, ~country, ~connectors) => { - let (firstName, lastName) = name->Utils.getFirstAndLastNameFromFullName - - [ - ("payment_method", "bank_transfer"->JSON.Encode.string), - ("connector", connectors->Utils.getArrofJsonString->JSON.Encode.array), - ("payment_method_type", "sepa"->JSON.Encode.string), - ( - "payment_method_data", - [ - ( - "billing", - [ - ("email", email->JSON.Encode.string), - ( - "address", - [ - ("first_name", firstName), - ("last_name", lastName), - ("country", country->JSON.Encode.string), - ]->Utils.getJsonFromArrayOfJson, - ), - ]->Utils.getJsonFromArrayOfJson, - ), - ( - "bank_transfer", - [("sepa_bank_transfer", Dict.make()->JSON.Encode.object)]->Utils.getJsonFromArrayOfJson, - ), - ]->Utils.getJsonFromArrayOfJson, - ), - ] -} let blikBody = (~blikCode) => [ ("payment_method", "bank_redirect"->JSON.Encode.string), ("payment_method_type", "blik"->JSON.Encode.string), @@ -977,16 +930,30 @@ let appendRedirectPaymentMethods = [ ] let appendBankeDebitMethods = ["sepa"] +let appendBankTransferMethods = ["sepa", "ach"] -let appendPaymentMethodExperience = (paymentMethodType, isQrPaymentMethod) => +let getPaymentMethodSuffix = (~paymentMethodType, ~paymentMethod, ~isQrPaymentMethod) => { if isQrPaymentMethod { - paymentMethodType ++ "_qr" + Some("qr") } else if appendRedirectPaymentMethods->Array.includes(paymentMethodType) { - paymentMethodType ++ "_redirect" - } else if appendBankeDebitMethods->Array.includes(paymentMethodType) { - paymentMethodType ++ "_bank_debit" + Some("redirect") + } else if ( + appendBankeDebitMethods->Array.includes(paymentMethodType) && paymentMethod == "bank_debit" + ) { + Some("bank_debit") + } else if ( + appendBankTransferMethods->Array.includes(paymentMethodType) && paymentMethod == "bank_transfer" + ) { + Some("bank_transfer") } else { - paymentMethodType + None + } +} + +let appendPaymentMethodExperience = (~paymentMethod, ~paymentMethodType, ~isQrPaymentMethod) => + switch getPaymentMethodSuffix(~paymentMethodType, ~paymentMethod, ~isQrPaymentMethod) { + | Some(suffix) => `${paymentMethodType}_${suffix}` + | None => paymentMethodType } let paymentExperiencePaymentMethods = ["affirm"] @@ -1010,7 +977,7 @@ let dynamicPaymentBody = (paymentMethod, paymentMethodType, ~isQrPaymentMethod=f paymentMethod, [ ( - paymentMethodType->appendPaymentMethodExperience(isQrPaymentMethod), + appendPaymentMethodExperience(~paymentMethod, ~paymentMethodType, ~isQrPaymentMethod), Dict.make()->JSON.Encode.object, ), ]->Utils.getJsonFromArrayOfJson, diff --git a/src/Utilities/PaymentHelpers.res b/src/Utilities/PaymentHelpers.res index 08ce312c5..86dc02bc4 100644 --- a/src/Utilities/PaymentHelpers.res +++ b/src/Utilities/PaymentHelpers.res @@ -1,15 +1,8 @@ open Utils open Identity - -@val @scope(("window", "parent", "location")) external href: string = "href" - -type searchParams = {set: (string, string) => unit} -type url = {searchParams: searchParams, href: string} -@new external urlSearch: string => url = "URL" - +open PaymentHelpersTypes open LoggerUtils -type payment = - Card | BankTransfer | BankDebits | KlarnaRedirect | Gpay | Applepay | Paypal | Paze | Other +open URLModule let getPaymentType = paymentMethodType => switch paymentMethodType { @@ -25,23 +18,6 @@ let getPaymentType = paymentMethodType => let closePaymentLoaderIfAny = () => messageParentWindow([("fullscreen", false->JSON.Encode.bool)]) -type paymentIntent = ( - ~handleUserError: bool=?, - ~bodyArr: array<(string, JSON.t)>, - ~confirmParam: ConfirmType.confirmParams, - ~iframeId: string=?, - ~isThirdPartyFlow: bool=?, - ~intentCallback: Core__JSON.t => unit=?, - ~manualRetry: bool=?, -) => unit - -type completeAuthorize = ( - ~handleUserError: bool=?, - ~bodyArr: array<(string, JSON.t)>, - ~confirmParam: ConfirmType.confirmParams, - ~iframeId: string=?, -) => unit - let retrievePaymentIntent = ( clientSecret, headers, @@ -359,10 +335,10 @@ let rec intentCall = ( ) ->then(res => { let statusCode = res->Fetch.Response.status->Int.toString - let url = urlSearch(confirmParam.return_url) + let url = makeUrl(confirmParam.return_url) url.searchParams.set("payment_intent_client_secret", clientSecret) url.searchParams.set("status", "failed") - messageParentWindow([("confirmParams", confirmParam->Identity.anyTypeToJson)]) + messageParentWindow([("confirmParams", confirmParam->anyTypeToJson)]) if statusCode->String.charAt(0) !== "2" { res @@ -504,7 +480,7 @@ let rec intentCall = ( | _ => intent.payment_method_type } - let url = urlSearch(confirmParam.return_url) + let url = makeUrl(confirmParam.return_url) url.searchParams.set("payment_intent_client_secret", clientSecret) url.searchParams.set("status", intent.status) @@ -516,7 +492,7 @@ let rec intentCall = ( | (Paypal, false) => if !isPaymentSession { if isCallbackUsedVal->Option.getOr(false) { - Utils.handleOnCompleteDoThisMessage() + handleOnCompleteDoThisMessage() } else { closePaymentLoaderIfAny() } @@ -524,7 +500,7 @@ let rec intentCall = ( postSubmitResponse(~jsonData=data, ~url=url.href) } else if confirmParam.redirect === Some("always") { if isCallbackUsedVal->Option.getOr(false) { - Utils.handleOnCompleteDoThisMessage() + handleOnCompleteDoThisMessage() } else { handleOpenUrl(url.href) } @@ -534,7 +510,7 @@ let rec intentCall = ( | _ => if isCallbackUsedVal->Option.getOr(false) { closePaymentLoaderIfAny() - Utils.handleOnCompleteDoThisMessage() + handleOnCompleteDoThisMessage() } else { handleOpenUrl(url.href) } @@ -719,7 +695,7 @@ let rec intentCall = ( ->getString("open_banking_session_token", "") ->JSON.Encode.string, ), - ("pmAuthConnectorArray", ["plaid"]->Identity.anyTypeToJson), + ("pmAuthConnectorArray", ["plaid"]->anyTypeToJson), ("publishableKey", confirmParam.publishableKey->JSON.Encode.string), ("clientSecret", clientSecret->JSON.Encode.string), ("isForceSync", true->JSON.Encode.bool), @@ -846,7 +822,7 @@ let rec intentCall = ( ->catch(err => { Promise.make((resolve, _) => { try { - let url = urlSearch(confirmParam.return_url) + let url = makeUrl(confirmParam.return_url) url.searchParams.set("payment_intent_client_secret", clientSecret) url.searchParams.set("status", "failed") let exceptionMessage = err->formatException @@ -982,7 +958,7 @@ let rec maskPayload = payloadJson => { let (key, value) = entry (key, maskPayload(value)) }) - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson | Array(arr) => arr->Array.map(maskPayload)->JSON.Encode.array | String(valueStr) => valueStr->maskStr->JSON.Encode.string @@ -1049,10 +1025,10 @@ let usePaymentIntent = (optLogger, paymentType) => { let (key, value) = header (key, value->JSON.Encode.string) }) - ->Utils.getJsonFromArrayOfJson, + ->getJsonFromArrayOfJson, ), ] - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson ->JSON.stringify switch paymentType { | Card => @@ -1115,7 +1091,7 @@ let usePaymentIntent = (optLogger, paymentType) => { bodyArr->Array.concat(broswerInfo()), mandatePaymentType->PaymentBody.paymentTypeBody, ]) - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson ->JSON.stringify callIntent(bodyStr) } @@ -1126,7 +1102,7 @@ let usePaymentIntent = (optLogger, paymentType) => { ->Array.concat( bodyArr->Array.concatMany([PaymentBody.mandateBody(mandatePaymentType), broswerInfo()]), ) - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson ->JSON.stringify callIntent(bodyStr) } @@ -1206,7 +1182,7 @@ let useCompleteAuthorize = (optLogger: option<HyperLogger.loggerMake>, paymentTy let bodyStr = [("client_secret", clientSecret->JSON.Encode.string)] ->Array.concatMany([bodyArr, browserInfo()]) - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson ->JSON.stringify let completeAuthorize = () => { @@ -1738,7 +1714,7 @@ let callAuthLink = ( let metaData = [ ("linkToken", data->getDictFromJson->getString("link_token", "")->JSON.Encode.string), - ("pmAuthConnectorArray", pmAuthConnectorsArr->Identity.anyTypeToJson), + ("pmAuthConnectorArray", pmAuthConnectorsArr->anyTypeToJson), ("publishableKey", publishableKey->JSON.Encode.string), ("clientSecret", clientSecret->Option.getOr("")->JSON.Encode.string), ("isForceSync", false->JSON.Encode.bool), @@ -2157,10 +2133,10 @@ let usePostSessionTokens = ( let (key, value) = header (key, value->JSON.Encode.string) }) - ->Utils.getJsonFromArrayOfJson, + ->getJsonFromArrayOfJson, ), ] - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson ->JSON.stringify switch paymentType { | Card => @@ -2218,7 +2194,7 @@ let usePostSessionTokens = ( bodyArr->Array.concat(broswerInfo()), mandatePaymentType->PaymentBody.paymentTypeBody, ]) - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson ->JSON.stringify callIntent(bodyStr) } @@ -2229,7 +2205,7 @@ let usePostSessionTokens = ( ->Array.concat( bodyArr->Array.concatMany([PaymentBody.mandateBody(mandatePaymentType), broswerInfo()]), ) - ->Utils.getJsonFromArrayOfJson + ->getJsonFromArrayOfJson ->JSON.stringify callIntent(bodyStr) } diff --git a/src/Utilities/PaymentHelpersTypes.res b/src/Utilities/PaymentHelpersTypes.res new file mode 100644 index 000000000..64ff04258 --- /dev/null +++ b/src/Utilities/PaymentHelpersTypes.res @@ -0,0 +1,19 @@ +type payment = + Card | BankTransfer | BankDebits | KlarnaRedirect | Gpay | Applepay | Paypal | Paze | Other + +type paymentIntent = ( + ~handleUserError: bool=?, + ~bodyArr: array<(string, JSON.t)>, + ~confirmParam: ConfirmType.confirmParams, + ~iframeId: string=?, + ~isThirdPartyFlow: bool=?, + ~intentCallback: Core__JSON.t => unit=?, + ~manualRetry: bool=?, +) => unit + +type completeAuthorize = ( + ~handleUserError: bool=?, + ~bodyArr: array<(string, JSON.t)>, + ~confirmParam: ConfirmType.confirmParams, + ~iframeId: string=?, +) => unit diff --git a/src/Utilities/URLModule.res b/src/Utilities/URLModule.res new file mode 100644 index 000000000..f956b52e9 --- /dev/null +++ b/src/Utilities/URLModule.res @@ -0,0 +1,5 @@ +type match +type pathname = {match: match} +type searchParams = {set: (string, string) => unit} +type url = {searchParams: searchParams, href: string, pathname: pathname} +@new external makeUrl: string => url = "URL" diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index 50f09d979..45c43cc0d 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -257,7 +257,7 @@ let useWindowSize = () => { let (size, setSize) = React.useState(_ => (0, 0)) React.useLayoutEffect1(() => { let updateSize = () => { - setSize(_ => (Window.windowInnerWidth, Window.windowInnerHeight)) + setSize(_ => (Window.innerWidth, Window.innerHeight)) } Window.addEventListener("resize", updateSize) updateSize() @@ -1419,6 +1419,14 @@ let getFirstAndLastNameFromFullName = fullName => { (firstName, lastNameJson) } +let isKeyPresentInDict = (dict, key) => dict->Dict.get(key)->Option.isSome let checkIsTestCardWildcard = val => ["1111222233334444"]->Array.includes(val) let minorUnitToString = val => (val->Int.toFloat /. 100.)->Float.toString + +let mergeAndFlattenToTuples = (body, requiredFieldsBody) => + body + ->getJsonFromArrayOfJson + ->flattenObject(true) + ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) + ->getArrayOfTupleFromDict diff --git a/src/Window.res b/src/Window.res index ebb0570d6..f2a17a768 100644 --- a/src/Window.res +++ b/src/Window.res @@ -1,33 +1,7 @@ type window type parent type document -@val external window: window = "window" -@val @scope("window") external windowInnerHeight: int = "innerHeight" -@val @scope("window") external windowInnerWidth: int = "innerWidth" - -@val @scope("window") -external windowParent: window = "parent" type style - -@val external parent: window = "parent" -@get external cardNumberElement: window => option<window> = "cardNumber" -@get external cardCVCElement: window => option<window> = "cardCvc" -@get external cardExpiryElement: window => option<window> = "cardExpiry" -@get external value: Dom.element => 'a = "value" - -@val @scope("document") external createElement: string => Dom.element = "createElement" -@set external windowOnload: (window, unit => unit) => unit = "onload" - -@get external fullscreen: window => option<window> = "fullscreen" - -@get external document: window => document = "document" -@get external parentNode: Dom.element => Dom.element = "parentNode" -@val @scope("document") -external querySelector: string => Nullable.t<Dom.element> = "querySelector" -@val @scope("document") -external querySelectorAll: string => array<Dom.element> = "querySelectorAll" -@send external getAttribute: (Dom.element, string) => option<string> = "getAttribute" - type eventData = { elementType: string, clickTriggered: bool, @@ -38,76 +12,62 @@ type eventData = { oneClickConfirmTriggered: bool, } type loaderEvent = {key: string, data: eventData} -@set external innerHTML: (Dom.element, string) => unit = "innerHTML" type event = {key: string, data: string, origin: string} -@val @scope("window") -external addEventListener: (string, _ => unit) => unit = "addEventListener" +type date = {now: unit => string} +type body +type packageJson = {version: string} + +/* External Declarations */ +@val external window: window = "window" +@val @scope("window") external innerHeight: int = "innerHeight" +@val @scope("window") external innerWidth: int = "innerWidth" +@val @scope("window") external windowParent: window = "parent" +@val external parent: window = "parent" +@val @scope("document") external createElement: string => Dom.element = "createElement" +@val @scope("document") external querySelector: string => Nullable.t<Dom.element> = "querySelector" +@val @scope("document") external querySelectorAll: string => array<Dom.element> = "querySelectorAll" +@module("/package.json") @val external packageJson: packageJson = "default" +@val @scope("document") external body: body = "body" +@val @scope("window") external getHyper: Nullable.t<Types.hyperInstance> = "HyperMethod" +@val @scope("window") external addEventListener: (string, _ => unit) => unit = "addEventListener" @val @scope("window") external removeEventListener: (string, 'ev => unit) => unit = "removeEventListener" +@val @scope("window") external btoa: string => string = "btoa" +@new external date: date = "Date" +@get external value: Dom.element => 'a = "value" + +/* External Methods */ +@scope("window") @get external cardNumberElement: window => option<window> = "cardNumber" +@get external cardCVCElement: window => option<window> = "cardCvc" +@get external cardExpiryElement: window => option<window> = "cardExpiry" +@get external document: window => document = "document" +@get external fullscreen: window => option<window> = "fullscreen" +@get external frames: window => {..} = "frames" +@get external name: window => string = "name" +@get external contentWindow: Dom.element => Dom.element = "contentWindow" +@get external style: Dom.element => style = "style" +@send external getAttribute: (Dom.element, string) => option<string> = "getAttribute" @send external postMessage: (Dom.element, string, string) => unit = "postMessage" @send external postMessageJSON: (Dom.element, JSON.t, string) => unit = "postMessage" -@send -external getElementById: (document, string) => Nullable.t<Dom.element> = "getElementById" -@get -external frames: window => {..} = "frames" -@get external name: window => string = "name" -@get -external contentWindow: Dom.element => Dom.element = "contentWindow" -@get -external style: Dom.element => style = "style" -@set external setTransition: (style, string) => unit = "transition" -@set external setHeight: (style, string) => unit = "height" +@send external getElementById: (document, string) => Nullable.t<Dom.element> = "getElementById" +@send external preventDefault: (event, unit) => unit = "preventDefault" +@send external appendChild: (body, Dom.element) => unit = "appendChild" +@send external remove: Dom.element => unit = "remove" +@send external setAttribute: (Dom.element, string, string) => unit = "setAttribute" @send external paymentRequest: (JSON.t, JSON.t, JSON.t) => JSON.t = "PaymentRequest" @send external click: Dom.element => unit = "click" - -let sendPostMessage = (element, message) => { - element->postMessage(message->JSON.Encode.object->JSON.stringify, GlobalVars.targetOrigin) -} - -let sendPostMessageJSON = (element, message) => { - element->postMessageJSON(message, GlobalVars.targetOrigin) -} - -let iframePostMessage = (iframeRef: nullable<Dom.element>, message) => { - switch iframeRef->Nullable.toOption { - | Some(ref) => - try { - ref - ->contentWindow - ->sendPostMessage(message) - } catch { - | _ => () - } - | None => Console.error("This element does not exist or is not mounted yet.") - } -} - -@send external preventDefault: (event, unit) => unit = "preventDefault" - -type date = {now: unit => string} -@new external date: date = "Date" +@set external innerHTML: (Dom.element, string) => unit = "innerHTML" @set external className: (Dom.element, string) => unit = "className" @set external id: (Dom.element, string) => unit = "id" -@send external setAttribute: (Dom.element, string, string) => unit = "setAttribute" @set external elementSrc: (Dom.element, string) => unit = "src" -type body - -@val @scope("document") -external body: body = "body" -@send external appendChild: (body, Dom.element) => unit = "appendChild" -@send external remove: Dom.element => unit = "remove" - @set external elementOnload: (Dom.element, unit => unit) => unit = "onload" @set external elementOnerror: (Dom.element, exn => unit) => unit = "onerror" -@val @scope("window") -external getHyper: Nullable.t<Types.hyperInstance> = "HyperMethod" -@set -external setHyper: (window, Types.hyperInstance) => unit = "HyperMethod" - -type packageJson = {version: string} -@module("/package.json") @val external packageJson: packageJson = "default" -let version = packageJson.version +@set external setTransition: (style, string) => unit = "transition" +@set external setHeight: (style, string) => unit = "height" +@set external windowOnload: (window, unit => unit) => unit = "onload" +@set external setHyper: (window, Types.hyperInstance) => unit = "HyperMethod" +/* Module Definitions */ module Navigator = { @val @scope("navigator") external browserName: string = "appName" @@ -174,17 +134,41 @@ module Element = { @get external clientWidth: Dom.element => int = "clientWidth" } -@val @scope("window") -external btoa: string => string = "btoa" +/* Helper Functions */ +let sendPostMessage = (element, message) => { + element->postMessage(message->JSON.Encode.object->JSON.stringify, GlobalVars.targetOrigin) +} + +let sendPostMessageJSON = (element, message) => { + element->postMessageJSON(message, GlobalVars.targetOrigin) +} + +let iframePostMessage = (iframeRef: nullable<Dom.element>, message) => { + switch iframeRef->Nullable.toOption { + | Some(ref) => + try { + ref + ->contentWindow + ->sendPostMessage(message) + } catch { + | _ => () + } + | None => Console.error("This element does not exist or is not mounted yet.") + } +} + +/* Version Handling */ +let version = packageJson.version +/* URL Handling */ let hrefWithoutSearch = Location.origin ++ Location.pathname +/* Environment Flags */ let isSandbox = Location.hostname === "beta.hyperswitch.io" - let isInteg = Location.hostname === "dev.hyperswitch.io" - let isProd = Location.hostname === "checkout.hyperswitch.io" +/* iFrame Detection */ let isIframed = () => try { Location.href !== Top.Location.href @@ -200,6 +184,7 @@ let isIframed = () => } } +/* Root Hostname Retrieval */ let getRootHostName = () => switch isIframed() { | true => @@ -219,22 +204,5 @@ let getRootHostName = () => | false => Location.hostname } -let replaceRootHref = (href: string) => { - Location.replace(href) - // switch isIframed() { - // | true => - // try { - // Top.Location.replace(href) - // } catch { - // | e => { - // Js.Console.error3( - // "Failed to redirect root document", - // e, - // `Using [window.location.replace] for redirection`, - // ) - // Location.replace(href) - // } - // } - // | false => Location.replace(href) - // } -} +/* Redirect Handling */ +let replaceRootHref = (href: string) => Location.replace(href)