diff --git a/config/config.toml b/config/config.toml index 3391aeb50..f25db7784 100644 --- a/config/config.toml +++ b/config/config.toml @@ -61,6 +61,7 @@ dev_recon_v2_product=false dev_org_sidebar=false dev_recovery_v2_product=false maintainence_alert="" +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..de56e38bb 100644 --- a/src/Recoils/HyperswitchAtom.res +++ b/src/Recoils/HyperswitchAtom.res @@ -2,6 +2,14 @@ type accessMapping = { groups: Map.t, resources: Map.t, } +type clonedConnectorData = { + paymentMethods: array, + metaData: JSON.t, +} +let defaultConnectorData = { + paymentMethods: [], + metaData: JSON.Encode.null, +} let ompDefaultValue: OMPSwitchTypes.ompListTypes = {id: "", name: ""} let merchantDetailsValueAtom: Recoil.recoilAtom = Recoil.atom( "merchantDetailsValue", @@ -76,3 +84,19 @@ let moduleListRecoil: Recoil.recoilAtom = Recoil.atom( + "clonedConnectorData", + defaultConnectorData, +) + +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 26d0be748..bc04bd6f2 100644 --- a/src/entryPoints/FeatureFlagUtils.res +++ b/src/entryPoints/FeatureFlagUtils.res @@ -48,6 +48,7 @@ type featureFlag = { forceCookies: bool, authenticationAnalytics: bool, devOrgSidebar: bool, + devClonePaymentMethods: bool, } let featureFlagType = (featureFlags: JSON.t) => { @@ -98,6 +99,7 @@ let featureFlagType = (featureFlags: JSON.t) => { forceCookies: dict->getBool("force_cookies", false), authenticationAnalytics: dict->getBool("authentication_analytics", false), devOrgSidebar: dict->getBool("dev_org_sidebar", false), + devClonePaymentMethods: dict->getBool("dev_clone_payment_methods", false), } } diff --git a/src/entryPoints/HyperSwitchApp.res b/src/entryPoints/HyperSwitchApp.res index 61e6b4b25..4f3d7593c 100644 --- a/src/entryPoints/HyperSwitchApp.res +++ b/src/entryPoints/HyperSwitchApp.res @@ -23,6 +23,9 @@ let make = () => { let {getThemesJson} = React.useContext(ThemeProvider.themeContext) let {devThemeFeature, devOrgSidebar} = HyperswitchAtom.featureFlagAtom->Recoil.useRecoilValueFromAtom + let retainCloneModal = Recoil.useRecoilValueFromAtom(HyperswitchAtom.retainCloneModalAtom) + let (showModal, setShowModal) = React.useState(_ => false) + let { fetchMerchantSpecificConfig, useIsFeatureEnabledForMerchant, @@ -59,6 +62,17 @@ let make = () => { } } + 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 @@ -72,6 +86,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!")) @@ -96,10 +113,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]) @@ -111,6 +131,9 @@ let make = () => { + + let customUI = + <>
{switch dashboardPageState { @@ -134,7 +157,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..d5d1d8f10 --- /dev/null +++ b/src/screens/Connectors/CloneConnectorPaymentMethods.res @@ -0,0 +1,148 @@ +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 setClonedConnectorData = Recoil.useSetRecoilState(HyperswitchAtom.clonedConnectorData) + 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 + + let clonedData: HyperswitchAtom.clonedConnectorData = { + paymentMethods: paymentMethodsClone, + metaData, + } + setClonedConnectorData((_): HyperswitchAtom.clonedConnectorData => clonedData) + 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 209899f63..59f8bcedc 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,8 @@ 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 setClonedConnectorData = Recoil.useSetRecoilState(HyperswitchAtom.clonedConnectorData) let isUpdateFlow = switch url.path->HSwitchUtils.urlPath { | list{"connectors", "new"} => false @@ -207,6 +210,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 +227,30 @@ 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 { + setClonedConnectorData(_ => HyperswitchAtom.defaultConnectorData) + setIsClonePMFlow(_ => false) + } + _ => + RescriptReactRouter.push( + GlobalVars.appendDashboardPath(~url="/connectors"), + ) + }, + }, + handleCancel: { + text: "No, don't go back", + }, + }), } : { title: "Processor", @@ -237,6 +271,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 9690ac8cd..62389e94d 100644 --- a/src/screens/Connectors/PaymentProcessor/ConnectorList.res +++ b/src/screens/Connectors/PaymentProcessor/ConnectorList.res @@ -131,6 +131,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 c5adc48fe..c73d57be0 100644 --- a/src/screens/Connectors/PaymentProcessor/ConnectorPaymentMethod.res +++ b/src/screens/Connectors/PaymentProcessor/ConnectorPaymentMethod.res @@ -16,6 +16,9 @@ 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 (clonedConnectorData, setClonedConnectorData) = Recoil.useRecoilState( + HyperswitchAtom.clonedConnectorData, + ) let updateDetails = value => { setPaymentMethods(_ => value->Array.copy) @@ -73,6 +76,7 @@ let make = (~setCurrentStep, ~connector, ~setInitialValues, ~initialValues, ~isU setInitialValues(_ => response) setScreenState(_ => Success) setCurrentStep(_ => ConnectorTypes.SummaryAndTest) + setClonedConnectorData(_ => HyperswitchAtom.defaultConnectorData) showToast( ~message=!isUpdateFlow ? "Connector Created Successfully!" : "Details Updated!", ~toastType=ToastSuccess, @@ -94,6 +98,19 @@ let make = (~setCurrentStep, ~connector, ~setInitialValues, ~initialValues, ~isU Nullable.null } + React.useEffect(() => { + if clonedConnectorData.paymentMethods->Array.length > 0 { + let paymentMethodCloned = + clonedConnectorData.paymentMethods->Identity.genericTypeToJson->getPaymentMethodEnabled + + let metaDataCloned = clonedConnectorData.metaData + + setMetaData(_ => metaDataCloned) + setPaymentMethods(_ => paymentMethodCloned) + } + None + }, [clonedConnectorData]) +
diff --git a/src/screens/Connectors/PaymentProcessor/ConnectorPreview.res b/src/screens/Connectors/PaymentProcessor/ConnectorPreview.res index c0423d2c6..4941809de 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/OMPSwitch/ProfileSwitch.res b/src/screens/OMPSwitch/ProfileSwitch.res index 2f1c84cc6..29e359eec 100644 --- a/src/screens/OMPSwitch/ProfileSwitch.res +++ b/src/screens/OMPSwitch/ProfileSwitch.res @@ -120,7 +120,12 @@ module NewProfileCreationModal = { } @react.component -let make = () => { +let make = ( + ~showSwitchModal=true, + ~setButtonState=_ => (), + ~showHeading=true, + ~customMargin="", +) => { open APIUtils open LogicUtils open OMPSwitchUtils @@ -156,9 +161,11 @@ let make = () => { let profileSwitch = async value => { try { setShowSwitchingProfile(_ => true) + setButtonState(_ => Button.Disabled) let _ = await internalSwitch(~expectedProfileId=Some(value)) RescriptReactRouter.replace(GlobalVars.extractModulePath(url)) setShowSwitchingProfile(_ => false) + setButtonState(_ => Button.Normal) } catch { | _ => { showToast(~message="Failed to switch profile", ~toastType=ToastError) @@ -187,6 +194,9 @@ let make = () => { let toggleChevronState = () => { setArrow(prev => !prev) } + + let heading = showHeading ? "Profile" : "" + let updatedProfileList: array< OMPSwitchTypes.ompListTypesCustom, > = profileList->Array.mapWithIndex((item, i) => { @@ -207,13 +217,13 @@ let make = () => { deselectDisable=true customButtonStyle="!rounded-md" options={updatedProfileList->generateDropdownOptionsCustomComponent} - marginTop="mt-14" + marginTop={customMargin->isNonEmptyString ? customMargin : "mt-14"} hideMultiSelectButtons=true addButton=false searchable=true customStyle="w-fit" baseComponent={} bottomComponent={} customDropdownOuterClass="!border-none " @@ -226,10 +236,12 @@ let make = () => { - + + + }