diff --git a/public/hyperswitch/icons/solid.svg b/public/hyperswitch/icons/solid.svg index d3644766f..b4809a191 100644 --- a/public/hyperswitch/icons/solid.svg +++ b/public/hyperswitch/icons/solid.svg @@ -1402,19 +1402,37 @@ License) d="M320 448v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V120c0-13.255 10.745-24 24-24h72v296c0 30.879 25.121 56 56 56h168zm0-344V0H152c-13.255 0-24 10.745-24 24v368c0 13.255 10.745 24 24 24h272c13.255 0 24-10.745 24-24V128H344c-13.2 0-24-10.8-24-24zm120.971-31.029L375.029 7.029A24 24 0 0 0 358.059 0H352v96h96v-6.059a24 24 0 0 0-7.029-16.97z" > - + - - - - - - + + + + + + - - - + + + @@ -8636,17 +8654,15 @@ License) width="20" height="21" > - + - @@ -8871,30 +8887,85 @@ License) /> - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/entryPoints/hyperswitch/HyperswitchAtom.res b/src/entryPoints/hyperswitch/HyperswitchAtom.res index ad077a330..621d19702 100644 --- a/src/entryPoints/hyperswitch/HyperswitchAtom.res +++ b/src/entryPoints/hyperswitch/HyperswitchAtom.res @@ -12,7 +12,7 @@ let featureFlagAtom: Recoil.recoilAtom = Recoil.at ) let paypalAccountStatusAtom: Recoil.recoilAtom = Recoil.atom(. "paypalAccountStatusAtom", - PayPalFlowTypes.Account_not_found, + PayPalFlowTypes.Connect_paypal_landing, ) let userPermissionAtom: Recoil.recoilAtom = Recoil.atom(. "userPermissionAtom", diff --git a/src/screens/HyperSwitch/Connectors/ConnectPayPalFlow/ConnectPayPal.res b/src/screens/HyperSwitch/Connectors/ConnectPayPalFlow/ConnectPayPal.res new file mode 100644 index 000000000..e8f189ed2 --- /dev/null +++ b/src/screens/HyperSwitch/Connectors/ConnectPayPalFlow/ConnectPayPal.res @@ -0,0 +1,521 @@ +let h3Leading2TextClass = `${HSwitchUtils.getTextClass( + ~textVariant=H3, + ~h3TextVariant=Leading_2, + (), + )} text-grey-700` +let p1RegularTextClass = `${HSwitchUtils.getTextClass( + ~textVariant=P1, + ~paragraphTextVariant=Regular, + (), + )} text-grey-700 opacity-50` + +let p1MediumTextClass = `${HSwitchUtils.getTextClass( + ~textVariant=P1, + ~paragraphTextVariant=Medium, + (), + )} text-grey-700` +let p2RedularTextClass = `${HSwitchUtils.getTextClass( + ~textVariant=P2, + ~paragraphTextVariant=Regular, + (), + )} text-grey-700 opacity-50` + +let preRequisiteList = [ + "You need to grant all the permissions to create and receive payments", + "Confirm your email id once PayPal sends you the mail", +] + +module PayPalCreateNewAccountModal = { + @react.component + let make = (~butttonDisplayText, ~actionUrl, ~setScreenState) => { + let initializePayPalWindow = () => { + try { + Window.payPalCreateAccountWindow() + } catch { + | Js.Exn.Error(e) => + switch Js.Exn.message(e) { + | Some(message) => setScreenState(_ => PageLoaderWrapper.Error(message)) + | None => setScreenState(_ => PageLoaderWrapper.Error("Failed to load paypal window!")) + } + } + } + + React.useEffect0(() => { + initializePayPalWindow() + None + }) + + + + {butttonDisplayText->React.string} + + + + } +} +module ManualSetupScreen = { + @react.component + let make = ( + ~connector, + ~connectorAccountFields, + ~selectedConnector, + ~connectorMetaDataFields, + ~connectorWebHookDetails, + ~connectorLabelDetailField, + ) => { +
+ ConnectorUtils.getConnectorNameTypeFromString} + connectorAccountFields + selectedConnector + connectorMetaDataFields + connectorWebHookDetails + connectorLabelDetailField + /> +
+ } +} + +module LandingScreen = { + @react.component + let make = (~configuartionType, ~setConfigurationType) => { + let getBlockColor = value => + configuartionType === value ? "border border-blue-700 bg-blue-700 bg-opacity-10 " : "border" + +
+
+

+ {"Do you have a PayPal business account?"->React.string} +

+
+ {PayPalFlowUtils.listChoices + ->Array.mapWithIndex((items, index) => { +
string_of_int} + className={`p-6 flex flex-col gap-4 rounded-md cursor-pointer ${items.variantType->getBlockColor} rounded-md`} + onClick={_ => setConfigurationType(_ => items.variantType)}> +
+
+

{items.displayText->React.string}

+
+ +
+
+

{items.choiceDescription->React.string}

+
+
+ }) + ->React.array} +
+
+
+ } +} +module ErrorPage = { + @react.component + let make = (~setupAccountStatus, ~actionUrl, ~getPayPalStatus, ~setScreenState) => { + open UIUtils + let errorPageDetails = setupAccountStatus->PayPalFlowUtils.getPageDetailsForAutomatic + +
+
+ +
+ String.length > 0}> +

+ {errorPageDetails.headerText->React.string} +

+
+ String.length > 0}> +

{errorPageDetails.subText->React.string}

+
+
+
+ +
+ Option.isSome}> + Option.getOr("")} + actionUrl + setScreenState + /> + +
+
+ } +} +module RedirectionToPayPalFlow = { + @react.component + let make = (~getPayPalStatus, ~profileId) => { + open APIUtils + open PayPalFlowTypes + + let url = RescriptReactRouter.useUrl() + let path = url.path->List.toArray->Array.joinWith("/") + let connectorId = url.path->List.toArray->LogicUtils.getValueFromArray(1, "") + let updateDetails = useUpdateMethod(~showErrorToast=false, ()) + let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Loading) + let (actionUrl, setActionUrl) = React.useState(_ => "") + + let getRedirectPaypalWindowUrl = async _ => { + open LogicUtils + try { + setScreenState(_ => PageLoaderWrapper.Loading) + let returnURL = `${HSwitchGlobalVars.hyperSwitchFEPrefix}/${path}?name=paypal&is_back=true&is_simplified_paypal=true&profile_id=${profileId}` + + let body = PayPalFlowUtils.generatePayPalBody( + ~connectorId={connectorId}, + ~returnUrl=Some(returnURL), + (), + ) + let url = `${getURL(~entityName=PAYPAL_ONBOARDING, ~methodType=Post, ())}/action_url` + + let response = await updateDetails(url, body, Post, ()) + let actionURL = + response->getDictFromJsonObject->getDictfromDict("paypal")->getString("action_url", "") + setActionUrl(_ => actionURL) + setScreenState(_ => PageLoaderWrapper.Success) + } catch { + | _ => setScreenState(_ => PageLoaderWrapper.Error("")) + } + } + let setupAccountStatus = Recoil.useRecoilValueFromAtom(HyperswitchAtom.paypalAccountStatusAtom) + + React.useEffect0(() => { + getRedirectPaypalWindowUrl()->ignore + None + }) + + {switch setupAccountStatus { + | Redirecting_to_paypal => +
+

+ {"Sign in / Sign up to auto-configure your credentials & webhooks"->React.string} +

+
+

+ {"Things to keep in mind while signing up"->React.string} +

+ {preRequisiteList + ->Array.mapWithIndex((item, index) => +

+ {`${(index + 1)->string_of_int}. ${item}`->React.string} +

+ ) + ->React.array} +
+
+ +
+
+ | _ => + }} +
+ } +} + +@react.component +let make = ( + ~connector, + ~isUpdateFlow, + ~setInitialValues, + ~initialValues, + ~setCurrentStep, + ~getPayPalStatus, +) => { + open LogicUtils + let url = RescriptReactRouter.useUrl() + let showToast = ToastState.useShowToast() + let showPopUp = PopUpState.useShowPopUp() + let updateConnectorAccountDetails = PayPalFlowUtils.useDeleteConnectorAccountDetails() + let deleteTrackingDetails = PayPalFlowUtils.useDeleteTrackingDetails() + + let (setupAccountStatus, setSetupAccountStatus) = Recoil.useRecoilState( + HyperswitchAtom.paypalAccountStatusAtom, + ) + let connectorValue = isUpdateFlow + ? url.path->List.toArray->getValueFromArray(1, "") + : url.search->getDictFromUrlSearchParams->Dict.get("connectorId")->Option.getOr("") + + let (connectorId, setConnectorId) = React.useState(_ => connectorValue) + + let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Success) + let (configuartionType, setConfigurationType) = React.useState(_ => PayPalFlowTypes.NotSelected) + + let selectedConnector = + connector->ConnectorUtils.getConnectorNameTypeFromString->ConnectorUtils.getConnectorInfo + let defaultBusinessProfile = Recoil.useRecoilValueFromAtom(HyperswitchAtom.businessProfilesAtom) + + let activeBusinessProfile = + defaultBusinessProfile->MerchantAccountUtils.getValueFromBusinessProfile + + let updatedInitialVal = React.useMemo1(() => { + let initialValuesToDict = initialValues->getDictFromJsonObject + if !isUpdateFlow { + initialValuesToDict->Dict.set( + "connector_label", + initialValues + ->getDictFromJsonObject + ->getString("connector_label", "paypal_default") + ->JSON.Encode.string, + ) + initialValuesToDict->Dict.set( + "profile_id", + activeBusinessProfile.profile_id->JSON.Encode.string, + ) + + setInitialValues(_ => initialValuesToDict->JSON.Encode.object) + initialValuesToDict->JSON.Encode.object + } else { + initialValues + } + }, [initialValues]) + + let setConnectorAsActive = values => { + // sets the status as active and diabled as false + let dictOfInitialValues = values->getDictFromJsonObject + dictOfInitialValues->Dict.set("disabled", false->JSON.Encode.bool) + dictOfInitialValues->Dict.set("status", "active"->JSON.Encode.string) + setInitialValues(_ => dictOfInitialValues->JSON.Encode.object) + } + + let updateConnectorDetails = async values => { + try { + setScreenState(_ => Loading) + let res = await updateConnectorAccountDetails( + values, + connectorId, + connector, + isUpdateFlow, + true, + "inactive", + ) + if configuartionType === Manual { + setConnectorAsActive(res) + } else { + setInitialValues(_ => res) + } + let connectorId = res->getDictFromJsonObject->getString("merchant_connector_id", "") + setConnectorId(_ => connectorId) + setScreenState(_ => Success) + RescriptReactRouter.replace(`/connectors/${connectorId}?name=paypal`) + } catch { + | Js.Exn.Error(e) => + switch Js.Exn.message(e) { + | Some(message) => Js.Exn.raiseError(message) + | None => Js.Exn.raiseError("") + } + } + } + + let validateMandatoryFieldForPaypal = values => { + let errors = Dict.make() + let valuesFlattenJson = values->JsonFlattenUtils.flattenObject(true) + let profileId = valuesFlattenJson->getString("profile_id", "") + if profileId->String.length === 0 { + Dict.set(errors, "Profile Id", `Please select your business profile`->JSON.Encode.string) + } + errors->JSON.Encode.object + } + + let handleChangeAuthType = async values => { + try { + // This will only be called whenever there is a update flow + // And whenever we are changing the flow from Manual to Automatic or vice-versa + // To check if the flow is changed we are using auth type (BodyKey for Manual and SignatureKey for Automatic) + // It deletes the old tracking id associated with the connector id and deletes the connector credentials + setScreenState(_ => Loading) + let _ = await deleteTrackingDetails(connectorId, connector) + let _ = await updateConnectorDetails(values) + + switch configuartionType { + | Automatic => setSetupAccountStatus(._ => Redirecting_to_paypal) + | Manual | _ => setCurrentStep(_ => ConnectorTypes.IntegFields) + } + setScreenState(_ => Success) + } catch { + | Js.Exn.Error(_) => setScreenState(_ => Error("Unable to change the configuartion")) + } + } + + let handleOnSubmit = async (values, _) => { + open PayPalFlowUtils + open ConnectorUtils + try { + let authType = initialValues->getAuthTypeFromConnectorDetails + + // create flow + if !isUpdateFlow { + switch configuartionType { + | Automatic => { + await updateConnectorDetails(values) + setSetupAccountStatus(._ => Redirecting_to_paypal) + } + + | Manual | _ => { + setConnectorAsActive(values) + setCurrentStep(_ => ConnectorTypes.IntegFields) + } + } + } // update flow if body type is changed + else if ( + authType !== + PayPalFlowUtils.getBodyType(isUpdateFlow, configuartionType) + ->String.toLowerCase + ->ConnectorUtils.mapAuthType + ) { + showPopUp({ + popUpType: (Warning, WithIcon), + heading: "Warning changing configuration", + description: React.string(`Modifying the configuration will result in the loss of existing details associated with this connector. Are you certain you want to continue?`), + handleConfirm: { + text: "Proceed", + onClick: {_ => handleChangeAuthType(values)->ignore}, + }, + handleCancel: {text: "Cancel"}, + }) + } else { + // update flow if body type is not changed + switch configuartionType { + | Automatic => setCurrentStep(_ => ConnectorTypes.PaymentMethods) + | Manual | _ => { + setConnectorAsActive(values) + setCurrentStep(_ => ConnectorTypes.IntegFields) + } + } + } + } catch { + | Js.Exn.Error(e) => + switch Js.Exn.message(e) { + | Some(message) => { + let errMsg = message->parseIntoMyData + if errMsg.code->Option.getOr("")->String.includes("HE_01") { + showToast( + ~message="This configuration already exists for the connector. Please try with a different country or label under advanced settings.", + ~toastType=ToastState.ToastError, + (), + ) + + setCurrentStep(_ => ConnectorTypes.AutomaticFlow) + setSetupAccountStatus(._ => Connect_paypal_landing) + setScreenState(_ => Success) + } else { + showToast( + ~message="Failed to Save the Configuration!", + ~toastType=ToastState.ToastError, + (), + ) + setScreenState(_ => Error(message)) + } + } + + | None => setScreenState(_ => Error("Failed to Fetch!")) + } + } + Js.Nullable.null + } + + let proceedButton = switch setupAccountStatus { + | Redirecting_to_paypal + | Account_not_found + | Payments_not_receivable + | Ppcp_custom_denied + | More_permissions_needed + | Email_not_verified => +