Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable payment methods cloning to another profile of same merchant #1875

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ tax_processor=true
transaction_view=false
x_feature_route=false
tenant_user=false
clone_payment_methods=false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add dev_ as prefix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated @JeevaRamu0104

[default.merchant_config]
[default.merchant_config.new_analytics]
org_ids=[]
Expand Down
13 changes: 13 additions & 0 deletions src/Recoils/HyperswitchAtom.res
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,16 @@ let moduleListRecoil: Recoil.recoilAtom<array<UserManagementTypes.userModuleType
"moduleListRecoil",
[],
)

let paymentMethodsClonedAtom: Recoil.recoilAtom<
array<ConnectorTypes.paymentMethodEnabled>,
> = Recoil.atom("paymentMethodsClonedAtom", [])

let retainCloneModalAtom: Recoil.recoilAtom<bool> = Recoil.atom("retainCloneModalAtom", false)

let cloneModalButtonStateAtom: Recoil.recoilAtom<Button.buttonState> = Recoil.atom(
"cloneModalButtonStateAtom",
Button.Disabled,
)

let cloneConnectorAtom: Recoil.recoilAtom<string> = Recoil.atom("cloneConnectorAtom", "")
2 changes: 2 additions & 0 deletions src/entryPoints/FeatureFlagUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type featureFlag = {
transactionView: bool,
xFeatureRoute: bool,
tenantUser: bool,
clonePaymentMethods: bool,
}

let featureFlagType = (featureFlags: JSON.t) => {
Expand Down Expand Up @@ -89,6 +90,7 @@ let featureFlagType = (featureFlags: JSON.t) => {
transactionView: dict->getBool("transaction_view", false),
xFeatureRoute: dict->getBool("x_feature_route", false),
tenantUser: dict->getBool("tenant_user", false),
clonePaymentMethods: dict->getBool("clone_payment_methods", false),
}
typedFeatureFlag
}
Expand Down
27 changes: 26 additions & 1 deletion src/entryPoints/HyperSwitchApp.res
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -43,6 +45,17 @@ let make = () => {
let hyperSwitchAppSidebars = SidebarValues.useGetSidebarValues(~isReconEnabled)
sessionExpired := false

React.useEffect(() => {
if retainCloneModal == true {
PritishBudhiraja marked this conversation as resolved.
Show resolved Hide resolved
setShowModal(_ => true)
setScreenState(_ => PageLoaderWrapper.Custom)
} else {
setShowModal(_ => false)
setScreenState(_ => PageLoaderWrapper.Success)
}
None
}, [retainCloneModal])

let setUpDashboard = async () => {
try {
// NOTE: Treat groupACL map similar to screenstate
Expand All @@ -55,6 +68,9 @@ let make = () => {
| list{"unauthorized"} => RescriptReactRouter.push(appendDashboardPath(~url="/home"))
| _ => ()
}
if retainCloneModal {
setScreenState(_ => PageLoaderWrapper.Custom)
}
PritishBudhiraja marked this conversation as resolved.
Show resolved Hide resolved
setDashboardPageState(_ => #HOME)
} catch {
| _ => setScreenState(_ => PageLoaderWrapper.Error("Failed to setup dashboard!"))
Expand All @@ -78,6 +94,9 @@ let make = () => {
if userGroupACL->Option.isSome {
setScreenState(_ => PageLoaderWrapper.Success)
}
if retainCloneModal {
setScreenState(_ => PageLoaderWrapper.Custom)
}
None
}, [userGroupACL])

Expand All @@ -89,6 +108,9 @@ let make = () => {
</RenderIf>
<ProfileSwitch />
</div>

let customUI = <CloneConnectorPaymentMethods.ClonePaymentMethodsModal setShowModal showModal />

<>
<div>
{switch dashboardPageState {
Expand All @@ -106,7 +128,10 @@ let make = () => {
/>
</RenderIf>
<PageLoaderWrapper
screenState={screenState} sectionHeight="!h-screen w-full" showLogoutButton=true>
screenState={screenState}
customUI
sectionHeight="!h-screen w-full"
showLogoutButton=true>
<div
className="flex relative flex-col flex-1 bg-hyperswitch_background dark:bg-black overflow-scroll md:overflow-x-hidden">
<div className="border-b shadow hyperswitch_box_shadow ">
Expand Down
143 changes: 143 additions & 0 deletions src/screens/Connectors/CloneConnectorPaymentMethods.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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 onNextClick = _ => {
RescriptReactRouter.push(
GlobalVars.appendDashboardPath(~url=`/connectors/new?name=${cloneConnector}`),
)
setRetainCloneModal(_ => false)
showToast(
~toastType=ToastSuccess,
~message="Payment Methods Cloned Successfully",
~autoClose=true,
)
}

let modalBody = {
<div className="m-4 p-2">
<div className="pt-3 m-3 flex justify-between">
<CardUtils.CardHeader
heading="Clone Payment Method"
subHeading="Select the target profile where you want to clone payment methods"
customSubHeadingStyle="w-full !max-w-none pr-10"
/>
<div
className="h-fit"
onClick={_ => {
setShowModal(_ => false)
setRetainCloneModal(_ => false)
}}>
<Icon name="modal-close-icon" className="cursor-pointer" size=30 />
</div>
</div>
<div className="m-4 flex flex-col gap-2">
<p className="text-md font-medium leading-7 text-gray-700">
{"Target Profile"->React.string}
</p>
<RenderIf condition={retainCloneModal}>
<div className="w-48">
<ProfileSwitch
showSwitchModal=false setButtonState showHeading=false customMargin="mt-8"
/>
</div>
</RenderIf>
<div className="flex justify-center my-4">
<Button text="Next" onClick={_ => onNextClick()} buttonState buttonType={Primary} />
</div>
</div>
</div>
}

<div>
<Modal
showModal
closeOnOutsideClick=true
setShowModal
childClass="p-0"
borderBottom=true
modalClass="w-full max-w-xl mx-auto my-auto dark:!bg-jp-gray-lightgray_background">
{modalBody}
</Modal>
</div>
}
}

@react.component
let make = (~connectorID, ~connectorName) => {
open APIUtils
open ConnectorUtils
let getURL = useGetURL()
let fetchDetails = useGetMethod()
let showToast = ToastState.useShowToast()
let (initialValues, setInitialValues) = React.useState(_ => JSON.Encode.null)
let (paymentMethodsEnabled, setPaymentMethods) = React.useState(_ =>
Dict.make()->JSON.Encode.object->getPaymentMethodEnabled
)
let setPaymentMethodsClone = Recoil.useSetRecoilState(HyperswitchAtom.paymentMethodsClonedAtom)
let setRetainCloneModal = Recoil.useSetRecoilState(HyperswitchAtom.retainCloneModalAtom)
let setCloneConnector = Recoil.useSetRecoilState(HyperswitchAtom.cloneConnectorAtom)
let (showModal, setShowModal) = React.useState(_ => false)

let setPaymentMethodDetails = async () => {
try {
initialValues->setConnectorPaymentMethods(setPaymentMethods)->ignore
} catch {
| _ => showToast(~message="Failed to Clone Payment methods", ~toastType=ToastError)
}
}

React.useEffect(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this useEffect can we write everything inside the getConnectorDetails function itself

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah moved

if initialValues != JSON.Encode.null {
setPaymentMethodDetails()->ignore
}
None
}, [initialValues])

React.useEffect(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

if paymentMethodsEnabled->Array.length > 0 {
let paymentMethodsClone =
paymentMethodsEnabled
->Identity.genericTypeToJson
->JSON.stringify
->LogicUtils.safeParse
->getPaymentMethodEnabled
setPaymentMethodsClone(_ => paymentMethodsClone)

setShowModal(_ => true)
setRetainCloneModal(_ => true)
}
None
}, [paymentMethodsEnabled])

let getConnectorDetails = async () => {
try {
let connectorUrl = getURL(~entityName=CONNECTOR, ~methodType=Get, ~id=Some(connectorID))
let json = await fetchDetails(connectorUrl)
setInitialValues(_ => json)
} catch {
| _ => Exn.raiseError("Something went wrong")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if the exception is throwed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated and showed a toast error in such case

}
}

let handleCloneClick = e => {
e->ReactEvent.Mouse.stopPropagation
getConnectorDetails()->ignore
setCloneConnector(_ => connectorName)
}
<>
<div className="flex" onClick={handleCloneClick}>
<p> {"Clone"->React.string} </p>
<img alt="copy" src={`/assets/CopyToClipboard.svg`} />
</div>
<ClonePaymentMethodsModal showModal setShowModal />
</>
}
2 changes: 2 additions & 0 deletions src/screens/Connectors/ConnectorList.res
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ let make = (~isPayoutFlow=false) => {
entity={ConnectorTableUtils.connectorEntity(
`${entityPrefix}connectors`,
~authorization=userHasAccess(~groupAccess=ConnectorsManage),
~isPayoutFlow,
~isCloningEnabled=featureFlagDetails.clonePaymentMethods,
)}
currrentFetchCount={filteredConnectorData->Array.length}
collapseTableRow=false
Expand Down
17 changes: 17 additions & 0 deletions src/screens/Connectors/ConnectorPaymentMethod.res
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ let make = (
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 updateDetails = value => {
setPaymentMethods(_ => value->Array.copy)
Expand Down Expand Up @@ -101,6 +104,20 @@ let make = (
Nullable.null
}

React.useEffect(() => {
if paymentMethodsClone->Array.length > 0 {
let clonedData =
paymentMethodsClone
->Identity.genericTypeToJson
->JSON.stringify
->LogicUtils.safeParse
PritishBudhiraja marked this conversation as resolved.
Show resolved Hide resolved
->getPaymentMethodEnabled
setPaymentMethods(_ => clonedData)
setPaymentMethodsClone(_ => [])
}
None
}, [paymentMethodsClone])

<PageLoaderWrapper screenState>
<Form onSubmit initialValues={initialValues}>
<div className="flex flex-col">
Expand Down
27 changes: 18 additions & 9 deletions src/screens/Connectors/ConnectorTableUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ type colType =
| TestMode
| Status
| Disabled
| Actions
| ProfileId
| ProfileName
| ConnectorLabel
| PaymentMethods
| MerchantConnectorId
| Actions

let defaultColumns = [
Name,
Expand All @@ -21,10 +21,11 @@ let defaultColumns = [
Status,
Disabled,
TestMode,
Actions,
PaymentMethods,
]

let defaultPaymentColumns = defaultColumns->Array.concat([Actions])
PritishBudhiraja marked this conversation as resolved.
Show resolved Hide resolved

let getConnectorObjectFromListViaId = (
connectorList: array<ConnectorTypes.connectorPayload>,
mca_id: string,
Expand All @@ -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 =>
Expand Down Expand Up @@ -93,10 +94,6 @@ let getTableCell = (~connectorType: ConnectorTypes.connector=Processor) => {
"",
)
| ConnectorLabel => Text(connector.connector_label)

// | Actions =>
// Table.CustomCell(<ConnectorActions connector_id={connector.merchant_connector_id} />, "")
| Actions => Table.CustomCell(<div />, "")
| PaymentMethods =>
Table.CustomCell(
<div>
Expand All @@ -108,6 +105,13 @@ let getTableCell = (~connectorType: ConnectorTypes.connector=Processor) => {
"",
)
| MerchantConnectorId => DisplayCopyCell(connector.merchant_connector_id)
| Actions =>
CustomCell(
<CloneConnectorPaymentMethods
connectorID=connector.merchant_connector_id connectorName=connector.connector_name
/>,
"",
)
}
}
getCell
Expand All @@ -125,11 +129,16 @@ let getPreviouslyConnectedList: JSON.t => array<connectorPayload> = json => {
LogicUtils.getArrayDataFromJson(json, ConnectorListMapper.getProcessorPayloadType)
}

let connectorEntity = (path: string, ~authorization: CommonAuthTypes.authorization) => {
let connectorEntity = (
path: string,
~authorization: CommonAuthTypes.authorization,
~isPayoutFlow=false,
~isCloningEnabled=false,
) => {
EntityType.makeEntity(
~uri=``,
~getObjects=getPreviouslyConnectedList,
~defaultColumns,
~defaultColumns={isCloningEnabled && !isPayoutFlow ? defaultPaymentColumns : defaultColumns},
~getHeading,
~getCell=getTableCell(~connectorType=Processor),
~dataKey="",
Expand Down
Loading
Loading