diff --git a/config/config.toml b/config/config.toml index 61e94b68f..d0a51494a 100644 --- a/config/config.toml +++ b/config/config.toml @@ -54,6 +54,7 @@ x_feature_route=false tenant_user=false dev_click_to_pay=false dev_recon_v2_product=false +dev_clone_payment_methods=false [default.merchant_config] [default.merchant_config.new_analytics] org_ids=[] diff --git a/src/Recoils/HyperswitchAtom.res b/src/Recoils/HyperswitchAtom.res index c94c0df0f..7afc41b29 100644 --- a/src/Recoils/HyperswitchAtom.res +++ b/src/Recoils/HyperswitchAtom.res @@ -76,3 +76,23 @@ let moduleListRecoil: Recoil.recoilAtom, +> = Recoil.atom("paymentMethodsClonedAtom", []) + +let metaDataClonedAtom: Recoil.recoilAtom = Recoil.atom( + "metaDataClonedAtom", + JSON.Encode.null, +) + +let retainCloneModalAtom: Recoil.recoilAtom = Recoil.atom("retainCloneModalAtom", false) + +let cloneModalButtonStateAtom: Recoil.recoilAtom = Recoil.atom( + "cloneModalButtonStateAtom", + Button.Normal, +) + +let cloneConnectorAtom: Recoil.recoilAtom = Recoil.atom("cloneConnectorAtom", "") + +let isClonePMFlow: Recoil.recoilAtom = Recoil.atom("isClonePMFlow", false) diff --git a/src/entryPoints/FeatureFlagUtils.res b/src/entryPoints/FeatureFlagUtils.res index ceefb5d7c..1540665d6 100644 --- a/src/entryPoints/FeatureFlagUtils.res +++ b/src/entryPoints/FeatureFlagUtils.res @@ -46,6 +46,7 @@ type featureFlag = { clickToPay: bool, devThemeFeature: bool, devReconv2Product: bool, + devClonePaymentMethods: bool, } let featureFlagType = (featureFlags: JSON.t) => { @@ -94,6 +95,7 @@ let featureFlagType = (featureFlags: JSON.t) => { tenantUser: dict->getBool("tenant_user", false), devThemeFeature: dict->getBool("dev_theme_feature", false), devReconv2Product: dict->getBool("dev_recon_v2_product", false), + devClonePaymentMethods: dict->getBool("dev_clone_payment_methods", false), } } diff --git a/src/entryPoints/HyperSwitchApp.res b/src/entryPoints/HyperSwitchApp.res index 2b35e80a5..8438825cc 100644 --- a/src/entryPoints/HyperSwitchApp.res +++ b/src/entryPoints/HyperSwitchApp.res @@ -18,6 +18,8 @@ let make = () => { let merchantDetailsTypedValue = Recoil.useRecoilValueFromAtom(merchantDetailsValueAtom) let featureFlagDetails = featureFlagAtom->Recoil.useRecoilValueFromAtom let (userGroupACL, setuserGroupACL) = Recoil.useRecoilState(userGroupACLAtom) + let retainCloneModal = Recoil.useRecoilValueFromAtom(HyperswitchAtom.retainCloneModalAtom) + let (showModal, setShowModal) = React.useState(_ => false) let { fetchMerchantSpecificConfig, @@ -44,6 +46,17 @@ let make = () => { let reconSidebars = HSReconSidebarValues.useGetReconSideBar() sessionExpired := false + React.useEffect(() => { + if retainCloneModal { + setShowModal(_ => true) + setScreenState(_ => PageLoaderWrapper.Custom) + } else { + setShowModal(_ => false) + setScreenState(_ => PageLoaderWrapper.Success) + } + None + }, [retainCloneModal]) + let setUpDashboard = async () => { try { // NOTE: Treat groupACL map similar to screenstate @@ -56,6 +69,9 @@ let make = () => { | list{"unauthorized"} => RescriptReactRouter.push(appendDashboardPath(~url="/home")) | _ => () } + if retainCloneModal { + setScreenState(_ => PageLoaderWrapper.Custom) + } setDashboardPageState(_ => #HOME) } catch { | _ => setScreenState(_ => PageLoaderWrapper.Error("Failed to setup dashboard!")) @@ -75,10 +91,13 @@ let make = () => { None }, (featureFlagDetails.mixpanel, path)) - React.useEffect1(() => { + React.useEffect(() => { if userGroupACL->Option.isSome { setScreenState(_ => PageLoaderWrapper.Success) } + if retainCloneModal { + setScreenState(_ => PageLoaderWrapper.Custom) + } None }, [userGroupACL]) @@ -90,6 +109,9 @@ let make = () => { + + let customUI = + <>
{switch dashboardPageState { @@ -110,7 +132,7 @@ let make = () => { /> + screenState customUI sectionHeight="!h-screen w-full" showLogoutButton=true>
diff --git a/src/screens/Connectors/CloneConnectorPaymentMethods.res b/src/screens/Connectors/CloneConnectorPaymentMethods.res new file mode 100644 index 000000000..24885f362 --- /dev/null +++ b/src/screens/Connectors/CloneConnectorPaymentMethods.res @@ -0,0 +1,145 @@ +module ClonePaymentMethodsModal = { + @react.component + let make = (~setShowModal, ~showModal) => { + let showToast = ToastState.useShowToast() + let (retainCloneModal, setRetainCloneModal) = Recoil.useRecoilState( + HyperswitchAtom.retainCloneModalAtom, + ) + let cloneConnector = Recoil.useRecoilValueFromAtom(HyperswitchAtom.cloneConnectorAtom) + let (buttonState, setButtonState) = Recoil.useRecoilState( + HyperswitchAtom.cloneModalButtonStateAtom, + ) + let setIsClonePMFlow = Recoil.useSetRecoilState(HyperswitchAtom.isClonePMFlow) + + let onNextClick = _ => { + RescriptReactRouter.push( + GlobalVars.appendDashboardPath(~url=`/connectors/new?name=${cloneConnector}`), + ) + setRetainCloneModal(_ => false) + showToast( + ~toastType=ToastSuccess, + ~message="Payment Methods Cloned Successfully", + ~autoClose=true, + ) + setIsClonePMFlow(_ => true) + } + + let modalBody = { + <> +
+ +
{ + setShowModal(_ => false) + setRetainCloneModal(_ => false) + }}> + +
+
+
+
+
+

+ {"Select the target profile where you want to clone payment methods"->React.string} +

+
+

{"Target Profile"->React.string}

+ +
+ +
+
+
+
+
+
+
+
+ + } + +
+ + {modalBody} + +
+ } +} + +@react.component +let make = (~connectorID, ~connectorName) => { + open APIUtils + open ConnectorUtils + open LogicUtils + let getURL = useGetURL() + let fetchDetails = useGetMethod() + let showToast = ToastState.useShowToast() + let setPaymentMethodsClone = Recoil.useSetRecoilState(HyperswitchAtom.paymentMethodsClonedAtom) + let setMetaDataClone = Recoil.useSetRecoilState(HyperswitchAtom.metaDataClonedAtom) + let setRetainCloneModal = Recoil.useSetRecoilState(HyperswitchAtom.retainCloneModalAtom) + let setCloneConnector = Recoil.useSetRecoilState(HyperswitchAtom.cloneConnectorAtom) + let (showModal, setShowModal) = React.useState(_ => false) + + let getConnectorDetails = async () => { + try { + let connectorUrl = getURL(~entityName=CONNECTOR, ~methodType=Get, ~id=Some(connectorID)) + let response = await fetchDetails(connectorUrl) + let json = Window.getResponsePayload(response) + let metaData = json->getDictFromJsonObject->getJsonObjectFromDict("metadata") + let paymentMethodEnabled = + json + ->getDictFromJsonObject + ->getJsonObjectFromDict("payment_methods_enabled") + ->getPaymentMethodEnabled + + if paymentMethodEnabled->Array.length > 0 { + let paymentMethodsClone = + paymentMethodEnabled + ->Identity.genericTypeToJson + ->JSON.stringify + ->LogicUtils.safeParse + ->getPaymentMethodEnabled + setPaymentMethodsClone(_ => paymentMethodsClone) + setMetaDataClone(_ => metaData) + setShowModal(_ => true) + setRetainCloneModal(_ => true) + } + } catch { + | _ => + showToast( + ~message="Unable to fetch Payment Methods. Please try cloning again.", + ~toastType=ToastError, + ) + } + } + + let handleCloneClick = e => { + e->ReactEvent.Mouse.stopPropagation + getConnectorDetails()->ignore + setCloneConnector(_ => connectorName) + } + <> +
+ } + toolTipPosition=Top + /> +
+ + +} diff --git a/src/screens/Connectors/PaymentProcessor/ConnectorHome.res b/src/screens/Connectors/PaymentProcessor/ConnectorHome.res index df03d44b2..c5b959d61 100644 --- a/src/screens/Connectors/PaymentProcessor/ConnectorHome.res +++ b/src/screens/Connectors/PaymentProcessor/ConnectorHome.res @@ -62,6 +62,7 @@ let make = (~showStepIndicator=true, ~showBreadCrumb=true) => { let updateDetails = useUpdateMethod() let featureFlagDetails = HyperswitchAtom.featureFlagAtom->Recoil.useRecoilValueFromAtom let showToast = ToastState.useShowToast() + let showPopUp = PopUpState.useShowPopUp() let connector = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("name", "") let connectorTypeFromName = connector->getConnectorNameTypeFromString let profileIdFromUrl = @@ -71,6 +72,9 @@ let make = (~showStepIndicator=true, ~showBreadCrumb=true) => { let (initialValues, setInitialValues) = React.useState(_ => Dict.make()->JSON.Encode.object) let (currentStep, setCurrentStep) = React.useState(_ => ConnectorTypes.IntegFields) let fetchDetails = useGetMethod() + let (isClonePMFlow, setIsClonePMFlow) = Recoil.useRecoilState(HyperswitchAtom.isClonePMFlow) + let setPaymentMethodsClone = Recoil.useSetRecoilState(HyperswitchAtom.paymentMethodsClonedAtom) + let setMetaDataClone = Recoil.useSetRecoilState(HyperswitchAtom.metaDataClonedAtom) let isUpdateFlow = switch url.path->HSwitchUtils.urlPath { | list{"connectors", "new"} => false @@ -207,6 +211,14 @@ let make = (~showStepIndicator=true, ~showBreadCrumb=true) => { isButton=true /> + let infoBanner = + + + let warningText = `You have not yet completed configuring your ${connector->LogicUtils.snakeToTitle} connector. Are you sure you want to go back?` +
@@ -216,7 +228,31 @@ let make = (~showStepIndicator=true, ~showBreadCrumb=true) => { ? { title: "Processor", link: "/connectors", - warning: `You have not yet completed configuring your ${connector->LogicUtils.snakeToTitle} connector. Are you sure you want to go back?`, + onClick: _ => + showPopUp({ + popUpType: (Warning, WithIcon), + heading: "Heads up!", + description: { + React.string(warningText) + }, + handleConfirm: { + text: "Yes, go back", + onClick: { + if isClonePMFlow { + setMetaDataClone(_ => JSON.Encode.null) + setPaymentMethodsClone(_ => []) + setIsClonePMFlow(_ => false) + } + _ => + RescriptReactRouter.push( + GlobalVars.appendDashboardPath(~url="/connectors"), + ) + }, + }, + handleCancel: { + text: "No, don't go back", + }, + }), } : { title: "Processor", @@ -237,6 +273,9 @@ let make = (~showStepIndicator=true, ~showBreadCrumb=true) => { bannerType=Warning /> + + {infoBanner} +
{switch currentStep { diff --git a/src/screens/Connectors/PaymentProcessor/ConnectorList.res b/src/screens/Connectors/PaymentProcessor/ConnectorList.res index 11a58ea13..392100fd4 100644 --- a/src/screens/Connectors/PaymentProcessor/ConnectorList.res +++ b/src/screens/Connectors/PaymentProcessor/ConnectorList.res @@ -135,6 +135,7 @@ let make = () => { entity={ConnectorTableUtils.connectorEntity( "connectors", ~authorization=userHasAccess(~groupAccess=ConnectorsManage), + ~isCloningEnabled=featureFlagDetails.devClonePaymentMethods, )} currrentFetchCount={filteredConnectorData->Array.length} collapseTableRow=false diff --git a/src/screens/Connectors/PaymentProcessor/ConnectorPaymentMethod.res b/src/screens/Connectors/PaymentProcessor/ConnectorPaymentMethod.res index 70f191eb4..f84acf6b1 100644 --- a/src/screens/Connectors/PaymentProcessor/ConnectorPaymentMethod.res +++ b/src/screens/Connectors/PaymentProcessor/ConnectorPaymentMethod.res @@ -16,6 +16,10 @@ let make = (~setCurrentStep, ~connector, ~setInitialValues, ~initialValues, ~isU let connectorID = initialValues->getDictFromJsonObject->getOptionString("merchant_connector_id") let (screenState, setScreenState) = React.useState(_ => Loading) let updateAPIHook = useUpdateMethod(~showErrorToast=false) + let (paymentMethodsClone, setPaymentMethodsClone) = Recoil.useRecoilState( + HyperswitchAtom.paymentMethodsClonedAtom, + ) + let (metaDataClone, setMetaDataClone) = Recoil.useRecoilState(HyperswitchAtom.metaDataClonedAtom) let updateDetails = value => { setPaymentMethods(_ => value->Array.copy) @@ -73,6 +77,8 @@ let make = (~setCurrentStep, ~connector, ~setInitialValues, ~initialValues, ~isU setInitialValues(_ => response) setScreenState(_ => Success) setCurrentStep(_ => ConnectorTypes.SummaryAndTest) + setMetaDataClone(_ => JSON.Encode.null) + setPaymentMethodsClone(_ => []) showToast( ~message=!isUpdateFlow ? "Connector Created Successfully!" : "Details Updated!", ~toastType=ToastSuccess, @@ -94,6 +100,20 @@ let make = (~setCurrentStep, ~connector, ~setInitialValues, ~initialValues, ~isU Nullable.null } + React.useEffect(() => { + if paymentMethodsClone->Array.length > 0 { + let clonedData = + paymentMethodsClone + ->Identity.genericTypeToJson + ->JSON.stringify + ->safeParse + ->getPaymentMethodEnabled + setMetaData(_ => metaDataClone) + setPaymentMethods(_ => clonedData) + } + None + }, [paymentMethodsClone]) +
diff --git a/src/screens/Connectors/PaymentProcessor/ConnectorPreview.res b/src/screens/Connectors/PaymentProcessor/ConnectorPreview.res index cf186c4c6..3490a39ea 100644 --- a/src/screens/Connectors/PaymentProcessor/ConnectorPreview.res +++ b/src/screens/Connectors/PaymentProcessor/ConnectorPreview.res @@ -343,6 +343,8 @@ let make = ( let connector = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("name", "") let {setShowFeedbackModal} = React.useContext(GlobalProvider.defaultContext) let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Success) + let (isClonePMFlow, setIsClonePMFlow) = Recoil.useRecoilState(HyperswitchAtom.isClonePMFlow) + let connectorInfoDict = connectorInfo->LogicUtils.getDictFromJsonObject let connectorInfo = connectorInfo->LogicUtils.getDictFromJsonObject->ConnectorListMapper.getProcessorPayloadType @@ -431,6 +433,9 @@ let make = ( if isFeedbackModalToBeOpen { setShowFeedbackModal(_ => true) } + if isClonePMFlow { + setIsClonePMFlow(_ => false) + } RescriptReactRouter.push(GlobalVars.appendDashboardPath(~url="/connectors")) }} text="Done" diff --git a/src/screens/Connectors/PaymentProcessor/ConnectorTableUtils.res b/src/screens/Connectors/PaymentProcessor/ConnectorTableUtils.res index 112194ac3..93f3f4766 100644 --- a/src/screens/Connectors/PaymentProcessor/ConnectorTableUtils.res +++ b/src/screens/Connectors/PaymentProcessor/ConnectorTableUtils.res @@ -5,12 +5,12 @@ type colType = | TestMode | Status | Disabled - | Actions | ProfileId | ProfileName | ConnectorLabel | PaymentMethods | MerchantConnectorId + | Actions let defaultColumns = [ Name, @@ -21,10 +21,11 @@ let defaultColumns = [ Status, Disabled, TestMode, - Actions, PaymentMethods, ] +let defaultPaymentColumns = [...defaultColumns, Actions] + let getConnectorObjectFromListViaId = ( connectorList: array, mca_id: string, @@ -47,13 +48,13 @@ let getHeading = colType => { | TestMode => Table.makeHeaderInfo(~key="test_mode", ~title="Test Mode") | Status => Table.makeHeaderInfo(~key="status", ~title="Integration status") | Disabled => Table.makeHeaderInfo(~key="disabled", ~title="Disabled") - | Actions => Table.makeHeaderInfo(~key="actions", ~title="") | ProfileId => Table.makeHeaderInfo(~key="profile_id", ~title="Profile Id") | MerchantConnectorId => Table.makeHeaderInfo(~key="merchant_connector_id", ~title="Merchant Connector Id") | ProfileName => Table.makeHeaderInfo(~key="profile_name", ~title="Profile Name") | ConnectorLabel => Table.makeHeaderInfo(~key="connector_label", ~title="Connector Label") | PaymentMethods => Table.makeHeaderInfo(~key="payment_methods", ~title="Payment Methods") + | Actions => Table.makeHeaderInfo(~key="actions", ~title="Actions") } } let connectorStatusStyle = connectorStatus => @@ -93,10 +94,6 @@ let getTableCell = (~connectorType: ConnectorTypes.connector=Processor) => { "", ) | ConnectorLabel => Text(connector.connector_label) - - // | Actions => - // Table.CustomCell(, "") - | Actions => Table.CustomCell(
, "") | PaymentMethods => Table.CustomCell(
@@ -108,6 +105,13 @@ let getTableCell = (~connectorType: ConnectorTypes.connector=Processor) => { "", ) | MerchantConnectorId => DisplayCopyCell(connector.merchant_connector_id) + | Actions => + CustomCell( + , + "", + ) } } getCell @@ -125,11 +129,15 @@ let getPreviouslyConnectedList: JSON.t => array = json => { LogicUtils.getArrayDataFromJson(json, ConnectorListMapper.getProcessorPayloadType) } -let connectorEntity = (path: string, ~authorization: CommonAuthTypes.authorization) => { +let connectorEntity = ( + path: string, + ~authorization: CommonAuthTypes.authorization, + ~isCloningEnabled=false, +) => { EntityType.makeEntity( ~uri=``, ~getObjects=getPreviouslyConnectedList, - ~defaultColumns, + ~defaultColumns={isCloningEnabled ? defaultPaymentColumns : defaultColumns}, ~getHeading, ~getCell=getTableCell(~connectorType=Processor), ~dataKey="", diff --git a/src/screens/Helpers/CardUtils.res b/src/screens/Helpers/CardUtils.res index 1d6eb74ae..37e3c91cd 100644 --- a/src/screens/Helpers/CardUtils.res +++ b/src/screens/Helpers/CardUtils.res @@ -7,7 +7,7 @@ module CardHeader = { image | None => React.null }} -
+
{heading->React.string}
diff --git a/src/screens/OMPSwitch/MerchantSwitch.res b/src/screens/OMPSwitch/MerchantSwitch.res index 5e44a52e1..fddf4a4ae 100644 --- a/src/screens/OMPSwitch/MerchantSwitch.res +++ b/src/screens/OMPSwitch/MerchantSwitch.res @@ -181,7 +181,7 @@ let make = () => { let customStyle = "text-blue-500 bg-white dark:bg-black hover:bg-jp-gray-100 text-nowrap w-full" let addItemBtnStyle = "border border-t-0 w-full" let customScrollStyle = "max-h-72 overflow-scroll px-1 pt-1 border border-b-0" - let dropdownContainerStyle = "rounded-md border border-1 w-[15rem]" + let dropdownContainerStyle = "rounded-md border border-1 w-[14rem] max-w-[20rem]" let subHeading = {currentOMPName(merchantList, merchantId)} diff --git a/src/screens/OMPSwitch/OMPSwitchHelper.res b/src/screens/OMPSwitch/OMPSwitchHelper.res index d406cb9cb..b537a68c1 100644 --- a/src/screens/OMPSwitch/OMPSwitchHelper.res +++ b/src/screens/OMPSwitch/OMPSwitchHelper.res @@ -28,8 +28,9 @@ module ListBaseComp = { let paddingHeading = isDarkBg ? "pl-2" : "" let endValue = isDarkBg ? 23 : 15 + let maxLength = isDarkBg ? 23 : 15 - let subHeadingElement = if subHeading->String.length > 15 { + let subHeadingElement = if subHeading->String.length > maxLength { { +let make = ( + ~showSwitchModal=true, + ~setButtonState=_ => (), + ~showHeading=true, + ~customMargin="", +) => { open APIUtils open LogicUtils open OMPSwitchUtils @@ -152,13 +157,15 @@ let make = () => { let customStyle = "text-blue-500 bg-white dark:bg-black hover:bg-jp-gray-100 text-nowrap w-full" let addItemBtnStyle = "border border-t-0 w-full" let customScrollStyle = "max-h-72 overflow-scroll px-1 pt-1 border border-b-0" - let dropdownContainerStyle = "rounded-md border border-1 w-[15rem]" + let dropdownContainerStyle = "rounded-md border border-1 w-[14rem] max-w-[20rem]" let profileSwitch = async value => { try { setShowSwitchingProfile(_ => true) + setButtonState(_ => Button.Disabled) let _ = await profileSwitch(~expectedProfileId=value, ~currentProfileId=profileId) RescriptReactRouter.replace(GlobalVars.extractModulePath(url)) setShowSwitchingProfile(_ => false) + setButtonState(_ => Button.Normal) } catch { | _ => { showToast(~message="Failed to switch profile", ~toastType=ToastError) @@ -188,6 +195,8 @@ let make = () => { setArrow(prev => !prev) } + let heading = showHeading ? "Profile" : "" + <> { deselectDisable=true customButtonStyle="!rounded-md" options={profileList->generateDropdownOptions} - marginTop="mt-14" + marginTop={customMargin->isNonEmptyString ? customMargin : "mt-14"} hideMultiSelectButtons=true addButton=false searchable=true customStyle="absolute w-fit left-0" baseComponent={} baseComponentCustomStyle="bg-white" bottomComponent={} @@ -218,10 +227,12 @@ let make = () => { - + + + }