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 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 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 @@ -52,6 +52,7 @@ tax_processor=true
x_feature_route=false
tenant_user=false
dev_click_to_pay=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
15 changes: 15 additions & 0 deletions src/Recoils/HyperswitchAtom.res
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,18 @@ 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.Normal,
)

let cloneConnectorAtom: Recoil.recoilAtom<string> = Recoil.atom("cloneConnectorAtom", "")

let isClonePMFlow: Recoil.recoilAtom<bool> = Recoil.atom("isClonePMFlow", false)
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 = {
xFeatureRoute: bool,
tenantUser: bool,
clickToPay: bool,
clonePaymentMethods: bool,
}

let featureFlagType = (featureFlags: JSON.t) => {
Expand Down Expand Up @@ -90,6 +91,7 @@ let featureFlagType = (featureFlags: JSON.t) => {
taxProcessor: dict->getBool("tax_processor", false),
xFeatureRoute: dict->getBool("x_feature_route", false),
tenantUser: dict->getBool("tenant_user", false),
clonePaymentMethods: dict->getBool("clone_payment_methods", false),
}
}

Expand Down
26 changes: 24 additions & 2 deletions 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 {
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 @@ -74,10 +90,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])

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,7 @@ let make = () => {
/>
</RenderIf>
<PageLoaderWrapper
screenState={screenState} sectionHeight="!h-screen w-full" showLogoutButton=true>
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
152 changes: 152 additions & 0 deletions src/screens/Connectors/CloneConnectorPaymentMethods.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 = {
<>
<div className="pt-3 m-3 flex justify-between">
<CardUtils.CardHeader
heading="Clone Payment Methods"
subHeading=""
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>
<hr />
<div>
<div className="flex flex-col gap-2 py-10 text-sm leading-7 text-gray-600 mx-3">
<p>
{"Select the target profile where you want to clone payment methods"->React.string}
</p>
<div>
<p> {"Target Profile"->React.string} </p>
<RenderIf condition={retainCloneModal}>
<div className="w-48">
<ProfileSwitch
showSwitchModal=false setButtonState showHeading=false customMargin="mt-8"
/>
</div>
</RenderIf>
</div>
</div>
<hr className="mt-4" />
<div className="flex justify-end my-4 mr-4">
<Button text="Next" onClick={_ => onNextClick()} buttonState buttonType={Primary} />
</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(() => {
if initialValues != JSON.Encode.null {
setPaymentMethodDetails()->ignore
}
None
}, [initialValues])

React.useEffect(() => {
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")
}
}

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 />
</>
}
19 changes: 19 additions & 0 deletions src/screens/Connectors/ConnectorUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,25 @@ let defaultSelectAllCards = (
}
}

let setConnectorPaymentMethods = async (initialValues, setPaymentMethods) => {
open LogicUtils
try {
let json = Window.getResponsePayload(initialValues)

let paymentMethodEnabled =
json
->getDictFromJsonObject
->getJsonObjectFromDict("payment_methods_enabled")
->getPaymentMethodEnabled
setPaymentMethods(_ => paymentMethodEnabled)
} catch {
| Exn.Error(e) => {
let err = Exn.message(e)->Option.getOr("Something went wrong")
Exn.raiseError(err)
}
}
}

let getConnectorPaymentMethodDetails = async (
initialValues,
setPaymentMethods,
Expand Down
33 changes: 32 additions & 1 deletion src/screens/Connectors/PaymentProcessor/ConnectorHome.res
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ let make = (~isPayoutFlow=false, ~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 =
Expand All @@ -71,6 +72,7 @@ let make = (~isPayoutFlow=false, ~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 isUpdateFlow = switch url.path->HSwitchUtils.urlPath {
| list{"connectors", "new"} => false
Expand Down Expand Up @@ -211,6 +213,13 @@ let make = (~isPayoutFlow=false, ~showStepIndicator=true, ~showBreadCrumb=true)
isButton=true
/>

let infoBanner =
<HSwitchUtils.WarningArea
warningText="This connector contains Cloned Payment Methods from source profile."
/>

let warningText = `You have not yet completed configuring your ${connector->LogicUtils.snakeToTitle} connector. Are you sure you want to go back?`

<PageLoaderWrapper screenState customUI={customUiForPaypal}>
<div className="flex flex-col gap-10 overflow-scroll h-full w-full">
<RenderIf condition={showBreadCrumb}>
Expand All @@ -220,7 +229,26 @@ let make = (~isPayoutFlow=false, ~showStepIndicator=true, ~showBreadCrumb=true)
? {
title,
link,
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 {
setIsClonePMFlow(_ => false)
}
_ => RescriptReactRouter.push(GlobalVars.appendDashboardPath(~url=link))
},
},
handleCancel: {
text: "No, don't go back",
},
}),
}
: {
title,
Expand All @@ -240,6 +268,9 @@ let make = (~isPayoutFlow=false, ~showStepIndicator=true, ~showBreadCrumb=true)
warningText="This is a test connector and will not be reflected on your payment processor dashboard."
/>
</RenderIf>
<RenderIf condition={isClonePMFlow && featureFlagDetails.clonePaymentMethods}>
{infoBanner}
</RenderIf>
<div
className="bg-white rounded-lg border h-3/4 overflow-scroll shadow-boxShadowMultiple show-scrollbar">
{switch currentStep {
Expand Down
2 changes: 2 additions & 0 deletions src/screens/Connectors/PaymentProcessor/ConnectorList.res
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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
Loading
Loading