From 07c96127d60c0f598cb1ae6aadd95c9ce887d350 Mon Sep 17 00:00:00 2001 From: Sagnik Mitra <83326850+ImSagnik007@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:38:16 +0530 Subject: [PATCH] refactor: Paypal Flow Refactor (#736) Co-authored-by: Pritish Budhiraja --- src/Components/SavedPaymentManagement.res | 2 +- src/Payments/PaypalSDK.res | 10 +- src/Payments/PaypalSDKHelpers.res | 111 +++++++++--- src/Utilities/PaymentHelpers.res | 198 +++++++++++++++++++++- src/Utilities/PaymentUtils.res | 2 +- src/orca-log-catcher/OrcaLogger.res | 4 + 6 files changed, 296 insertions(+), 31 deletions(-) diff --git a/src/Components/SavedPaymentManagement.res b/src/Components/SavedPaymentManagement.res index f492b4b24..f213bef33 100644 --- a/src/Components/SavedPaymentManagement.res +++ b/src/Components/SavedPaymentManagement.res @@ -9,7 +9,7 @@ let make = (~savedMethods: array, ~setSavedMethods) let logger = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom) let removeSavedMethod = ( - savedMethods: array, + savedMethods: array, paymentMethodId, ) => { savedMethods->Array.filter(savedMethod => { diff --git a/src/Payments/PaypalSDK.res b/src/Payments/PaypalSDK.res index 1abbc2886..952c304b9 100644 --- a/src/Payments/PaypalSDK.res +++ b/src/Payments/PaypalSDK.res @@ -16,9 +16,11 @@ let make = (~sessionObj: SessionsType.token, ~paymentType: CardThemeType.mode) = let token = sessionObj.token let orderDetails = sessionObj.orderDetails->getOrderDetails(paymentType) - let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), Paypal) - let isManualRetryEnabled = Recoil.useRecoilValueFromAtom(RecoilAtoms.isManualRetryEnabled) + let intent = PaymentHelpers.usePostSessionTokens(Some(loggerState), Paypal, Wallet) + let confirm = PaymentHelpers.usePaymentIntent(Some(loggerState), Paypal) + let sessions = Recoil.useRecoilValueFromAtom(RecoilAtoms.sessions) let completeAuthorize = PaymentHelpers.useCompleteAuthorize(Some(loggerState), Paypal) + let isManualRetryEnabled = Recoil.useRecoilValueFromAtom(RecoilAtoms.isManualRetryEnabled) let checkoutScript = Window.document(Window.window)->Window.getElementById("braintree-checkout")->Nullable.toOption let clientScript = @@ -84,18 +86,20 @@ let make = (~sessionObj: SessionsType.token, ~paymentType: CardThemeType.mode) = ~iframeId, ~paymentMethodListValue, ~isGuestCustomer, - ~intent, + ~postSessionTokens=intent, ~isManualRetryEnabled, ~options, ~publishableKey, ~paymentMethodTypes, ~stateJson, + ~confirm, ~completeAuthorize, ~handleCloseLoader, ~areOneClickWalletsRendered, ~setIsCompleted, ~isCallbackUsedVal, ~sdkHandleIsThere, + ~sessions, ) }) Window.body->Window.appendChild(paypalScript) diff --git a/src/Payments/PaypalSDKHelpers.res b/src/Payments/PaypalSDKHelpers.res index e05a477b7..0b4bc071b 100644 --- a/src/Payments/PaypalSDKHelpers.res +++ b/src/Payments/PaypalSDKHelpers.res @@ -1,27 +1,30 @@ open PaypalSDKTypes open Promise +open Utils let loadPaypalSDK = ( ~loggerState: OrcaLogger.loggerMake, - ~sdkHandleOneClickConfirmPayment, + ~sdkHandleOneClickConfirmPayment as _, ~buttonStyle, ~iframeId, ~isManualRetryEnabled, ~paymentMethodListValue, ~isGuestCustomer, - ~intent: PaymentHelpers.paymentIntent, + ~postSessionTokens: PaymentHelpers.paymentIntent, ~options: PaymentType.options, ~publishableKey, ~paymentMethodTypes, ~stateJson, + ~confirm: PaymentHelpers.paymentIntent, ~completeAuthorize: PaymentHelpers.completeAuthorize, ~handleCloseLoader, ~areOneClickWalletsRendered: ( RecoilAtoms.areOneClickWalletsRendered => RecoilAtoms.areOneClickWalletsRendered ) => unit, ~setIsCompleted, - ~isCallbackUsedVal: bool, + ~isCallbackUsedVal as _: bool, ~sdkHandleIsThere: bool, + ~sessions: PaymentType.loadType, ) => { loggerState.setLogInfo( ~value="Paypal SDK Button Clicked", @@ -31,6 +34,23 @@ let loadPaypalSDK = ( let paypalWrapper = GooglePayType.getElementById(Utils.document, "paypal-button") paypalWrapper.innerHTML = "" setIsCompleted(_ => true) + let paypalNextAction = switch sessions { + | Loaded(data) => + data + ->getDictFromJson + ->getOptionalArrayFromDict("session_token") + ->Option.flatMap(arr => { + arr->Array.find(ele => ele->getDictFromJson->getString("connector", "") == "paypal") + }) + ->Option.flatMap(ele => { + ele + ->getDictFromJson + ->getDictFromDict("sdk_next_action") + ->getOptionString("next_action") + }) + ->Option.getOr("") + | _ => "" + } paypal["Buttons"]({ style: buttonStyle, fundingSource: paypal["FUNDING"]["PAYPAL"], @@ -52,17 +72,36 @@ let loadPaypalSDK = ( ~body, ) Promise.make((resolve, _) => { - intent( - ~bodyArr=modifiedPaymentBody, - ~confirmParam={ - return_url: options.wallets.walletReturnUrl, - publishableKey, - }, - ~handleUserError=true, - ~intentCallback=val => - val->Utils.getDictFromJson->Utils.getString("orderId", "")->resolve, - ~manualRetry=isManualRetryEnabled, - ) + if paypalNextAction == "post_session_tokens" { + postSessionTokens( + ~bodyArr=modifiedPaymentBody, + ~confirmParam={ + return_url: options.wallets.walletReturnUrl, + publishableKey, + }, + ~handleUserError=true, + ~intentCallback=val => { + val + ->Utils.getDictFromJson + ->Utils.getDictFromDict("nextActionData") + ->Utils.getString("order_id", "") + ->resolve + }, + ~manualRetry=isManualRetryEnabled, + ) + } else { + confirm( + ~bodyArr=modifiedPaymentBody, + ~confirmParam={ + return_url: options.wallets.walletReturnUrl, + publishableKey, + }, + ~handleUserError=true, + ~intentCallback=val => + val->Utils.getDictFromJson->Utils.getString("orderId", "")->resolve, + ~manualRetry=isManualRetryEnabled, + ) + } }) } else { loggerState.setLogInfo( @@ -100,22 +139,46 @@ let loadPaypalSDK = ( ~statesList=stateJson, ) + let (connectors, _) = + paymentMethodListValue->PaymentUtils.getConnectors(Wallets(Paypal(SDK))) + + let orderId = val->Utils.getDictFromJson->Utils.getString("id", "") + let body = PaymentBody.paypalSdkBody(~token=orderId, ~connectors) + let modifiedPaymentBody = PaymentUtils.appendedCustomerAcceptance( + ~isGuestCustomer, + ~paymentType=paymentMethodListValue.payment_type, + ~body, + ) + let bodyArr = requiredFieldsBody ->JSON.Encode.object ->Utils.unflattenObject ->Utils.getArrayOfTupleFromDict - completeAuthorize( - ~bodyArr, - ~confirmParam={ - return_url: options.wallets.walletReturnUrl, - publishableKey, - }, - ~handleUserError=true, - ) - - resolve() + let confirmBody = bodyArr->Array.concatMany([modifiedPaymentBody]) + Promise.make((_resolve, _) => { + if paypalNextAction == "post_session_tokens" { + confirm( + ~bodyArr=confirmBody, + ~confirmParam={ + return_url: options.wallets.walletReturnUrl, + publishableKey, + }, + ~handleUserError=true, + ~manualRetry=true, + ) + } else { + completeAuthorize( + ~bodyArr, + ~confirmParam={ + return_url: options.wallets.walletReturnUrl, + publishableKey, + }, + ~handleUserError=true, + ) + } + }) }) ->ignore } diff --git a/src/Utilities/PaymentHelpers.res b/src/Utilities/PaymentHelpers.res index 981517c51..0fa7d7b5e 100644 --- a/src/Utilities/PaymentHelpers.res +++ b/src/Utilities/PaymentHelpers.res @@ -321,12 +321,15 @@ let rec intentCall = ( let isConfirm = uri->String.includes("/confirm") let isCompleteAuthorize = uri->String.includes("/complete_authorize") + let isPostSessionTokens = uri->String.includes("/post_session_tokens") let (eventName: OrcaLogger.eventName, initEventName: OrcaLogger.eventName) = switch ( isConfirm, isCompleteAuthorize, + isPostSessionTokens, ) { - | (true, _) => (CONFIRM_CALL, CONFIRM_CALL_INIT) - | (_, true) => (COMPLETE_AUTHORIZE_CALL, COMPLETE_AUTHORIZE_CALL_INIT) + | (true, _, _) => (CONFIRM_CALL, CONFIRM_CALL_INIT) + | (_, true, _) => (COMPLETE_AUTHORIZE_CALL, COMPLETE_AUTHORIZE_CALL_INIT) + | (_, _, true) => (POST_SESSION_TOKENS_CALL, POST_SESSION_TOKENS_CALL_INIT) | _ => (RETRIEVE_CALL, RETRIEVE_CALL_INIT) } logApi( @@ -763,6 +766,17 @@ let rec intentCall = ( resolve(failedSubmitResponse) } } + } else if intent.status == "requires_payment_method" { + if intent.nextAction.type_ === "invoke_sdk_client" { + let nextActionData = + intent.nextAction.next_action_data->Option.getOr(JSON.Encode.null) + let response = + [ + ("orderId", intent.connectorTransactionId->JSON.Encode.string), + ("nextActionData", nextActionData), + ]->getJsonFromArrayOfJson + resolve(response) + } } else if intent.status == "processing" { if intent.nextAction.type_ == "third_party_sdk_session_token" { let session_token = switch intent.nextAction.session_token { @@ -2072,3 +2086,183 @@ let calculateTax = ( JSON.Encode.null->resolve }) } + +let usePostSessionTokens = ( + optLogger, + paymentType: payment, + paymentMethod: PaymentMethodCollectTypes.paymentMethod, +) => { + open RecoilAtoms + open Promise + let url = RescriptReactRouter.useUrl() + let paymentTypeFromUrl = + CardUtils.getQueryParamsDictforKey(url.search, "componentName")->CardThemeType.getPaymentMode + let customPodUri = Recoil.useRecoilValueFromAtom(customPodUri) + let paymentMethodList = Recoil.useRecoilValueFromAtom(paymentMethodList) + let keys = Recoil.useRecoilValueFromAtom(keys) + + let setIsManualRetryEnabled = Recoil.useSetRecoilState(isManualRetryEnabled) + ( + ~handleUserError=false, + ~bodyArr: array<(string, JSON.t)>, + ~confirmParam: ConfirmType.confirmParams, + ~iframeId=keys.iframeId, + ~isThirdPartyFlow=false, + ~intentCallback=_ => (), + ~manualRetry as _=false, + ) => { + switch keys.clientSecret { + | Some(clientSecret) => + let paymentIntentID = clientSecret->getPaymentId + let headers = [ + ("Content-Type", "application/json"), + ("api-key", confirmParam.publishableKey), + ("X-Client-Source", paymentTypeFromUrl->CardThemeType.getPaymentModeToStrMapper), + ] + let body = [ + ("client_secret", clientSecret->JSON.Encode.string), + ("payment_id", paymentIntentID->JSON.Encode.string), + ("payment_method_type", (paymentType :> string)->JSON.Encode.string), + ("payment_method", (paymentMethod :> string)->JSON.Encode.string), + ] + + let endpoint = ApiEndpoint.getApiEndPoint( + ~publishableKey=confirmParam.publishableKey, + ~isConfirmCall=isThirdPartyFlow, + ) + let uri = `${endpoint}/payments/${paymentIntentID}/post_session_tokens` + + let callIntent = body => { + let contentLength = body->String.length->Int.toString + let maskedPayload = + body->safeParseOpt->Option.getOr(JSON.Encode.null)->maskPayload->JSON.stringify + let loggerPayload = + [ + ("payload", maskedPayload->JSON.Encode.string), + ( + "headers", + headers + ->Array.map(header => { + let (key, value) = header + (key, value->JSON.Encode.string) + }) + ->Utils.getJsonFromArrayOfJson, + ), + ] + ->Utils.getJsonFromArrayOfJson + ->JSON.stringify + switch paymentType { + | Card => + handleLogging( + ~optLogger, + ~internalMetadata=loggerPayload, + ~value=contentLength, + ~eventName=PAYMENT_ATTEMPT, + ~paymentMethod="CARD", + ) + | _ => + bodyArr->Array.forEach(((str, json)) => { + if str === "payment_method_type" { + handleLogging( + ~optLogger, + ~value=contentLength, + ~internalMetadata=loggerPayload, + ~eventName=PAYMENT_ATTEMPT, + ~paymentMethod=json->getStringFromJson(""), + ) + } + () + }) + } + + intentCall( + ~fetchApi, + ~uri, + ~headers, + ~bodyStr=body, + ~confirmParam: ConfirmType.confirmParams, + ~clientSecret, + ~optLogger, + ~handleUserError, + ~paymentType, + ~iframeId, + ~fetchMethod=#POST, + ~setIsManualRetryEnabled, + ~customPodUri, + ~sdkHandleOneClickConfirmPayment=keys.sdkHandleOneClickConfirmPayment, + ~counter=0, + ) + ->then(val => { + intentCallback(val) + resolve() + }) + ->ignore + } + + let broswerInfo = BrowserSpec.broswerInfo + let intentWithoutMandate = mandatePaymentType => { + let bodyStr = + body + ->Array.concatMany([ + bodyArr->Array.concat(broswerInfo()), + mandatePaymentType->PaymentBody.paymentTypeBody, + ]) + ->Utils.getJsonFromArrayOfJson + ->JSON.stringify + callIntent(bodyStr) + } + + let intentWithMandate = mandatePaymentType => { + let bodyStr = + body + ->Array.concat( + bodyArr->Array.concatMany([PaymentBody.mandateBody(mandatePaymentType), broswerInfo()]), + ) + ->Utils.getJsonFromArrayOfJson + ->JSON.stringify + callIntent(bodyStr) + } + + switch paymentMethodList { + | LoadError(data) + | Loaded(data) => + let paymentList = data->getDictFromJson->PaymentMethodsRecord.itemToObjMapper + let mandatePaymentType = + paymentList.payment_type->PaymentMethodsRecord.paymentTypeToStringMapper + if paymentList.payment_methods->Array.length > 0 { + switch paymentList.mandate_payment { + | Some(_) => + switch paymentType { + | Card + | Gpay + | Applepay + | KlarnaRedirect + | Paypal + | BankDebits => + intentWithMandate(mandatePaymentType) + | _ => intentWithoutMandate(mandatePaymentType) + } + | None => intentWithoutMandate(mandatePaymentType) + } + } else { + postFailedSubmitResponse( + ~errortype="payment_methods_empty", + ~message="Payment Failed. Try again!", + ) + Console.warn("Please enable atleast one Payment method.") + } + | SemiLoaded => intentWithoutMandate("") + | _ => + postFailedSubmitResponse( + ~errortype="payment_methods_loading", + ~message="Please wait. Try again!", + ) + } + | None => + postFailedSubmitResponse( + ~errortype="post_session_tokens_failed", + ~message="Post Session Tokens failed. Try again!", + ) + } + } +} diff --git a/src/Utilities/PaymentUtils.res b/src/Utilities/PaymentUtils.res index 6422fb090..150978000 100644 --- a/src/Utilities/PaymentUtils.res +++ b/src/Utilities/PaymentUtils.res @@ -478,7 +478,7 @@ let sortCustomerMethodsBasedOnPriority = ( } let getSupportedCardBrands = ( - paymentMethodListValue: OrcaPaymentPage.PaymentMethodsRecord.paymentMethodList, + paymentMethodListValue: PaymentMethodsRecord.paymentMethodList, ) => { let cardPaymentMethod = paymentMethodListValue.payment_methods->Array.find(ele => ele.payment_method === "card") diff --git a/src/orca-log-catcher/OrcaLogger.res b/src/orca-log-catcher/OrcaLogger.res index bdcc4e4cb..82ab3933c 100644 --- a/src/orca-log-catcher/OrcaLogger.res +++ b/src/orca-log-catcher/OrcaLogger.res @@ -84,6 +84,8 @@ type eventName = | DELETE_PAYMENT_METHODS_CALL_INIT | DELETE_PAYMENT_METHODS_CALL | EXTERNAL_TAX_CALCULATION + | POST_SESSION_TOKENS_CALL + | POST_SESSION_TOKENS_CALL_INIT let eventNameToStrMapper = eventName => { switch eventName { @@ -168,6 +170,8 @@ let eventNameToStrMapper = eventName => { | DELETE_PAYMENT_METHODS_CALL_INIT => "DELETE_PAYMENT_METHODS_CALL_INIT" | DELETE_PAYMENT_METHODS_CALL => "DELETE_PAYMENT_METHODS_CALL" | EXTERNAL_TAX_CALCULATION => "EXTERNAL_TAX_CALCULATION" + | POST_SESSION_TOKENS_CALL => "POST_SESSION_TOKENS_CALL" + | POST_SESSION_TOKENS_CALL_INIT => "POST_SESSION_TOKENS_CALL_INIT" } }