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 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d1e4576
feat: enable payment methods cloning to another profile of same merchant
Dec 6, 2024
0b77bf4
feat: enable payment methods cloning to another profile of same merchant
Dec 6, 2024
03bcd7f
feat: enable payment methods cloning to another profile of same merchant
Dec 6, 2024
8264901
chore: merge main and resolve conflict
Dec 9, 2024
f2ccb35
chore: address comment
Dec 10, 2024
9de87bd
chore: css changes
Dec 19, 2024
28f9da4
chore: css changes
Dec 19, 2024
9811c8d
chore: added banner to indicate cloned Payment methods
Dec 20, 2024
2d34aff
chore: added banner to indicate cloned Payment methods
Dec 20, 2024
aaf8978
chore: merge main and resolve conflict
Dec 23, 2024
a2a35d7
chore: css changes for banner
Dec 23, 2024
90fba7c
chore: merged main and resolved conflicts
Jan 2, 2025
1c9ed7b
chore: merged main and resolved conflicts
Jan 2, 2025
c7e02f3
chore: merged main and resolved conflicts
Jan 2, 2025
7065ab9
chore: label update
Jan 3, 2025
77c2ebb
chore: address comment
Jan 6, 2025
52d26c7
chore: change action cell value
Jan 6, 2025
f49abbc
Merge branch 'main' of https://github.com/juspay/hyperswitch-control-…
Jan 6, 2025
454edf6
Merge branch 'main' into clone-payment-methods-feature
JeevaRamu0104 Jan 8, 2025
258efb7
chore: address comment
Jan 10, 2025
bbe7bd5
chore: minor fixes
Jan 10, 2025
47f5cf9
chore: refactored code
Jan 10, 2025
ad7fb8a
chore: merged main and resolved conflicts
Jan 13, 2025
dcbfd36
chore: merged main
Jan 23, 2025
308e87e
chore: refactor
Jan 31, 2025
bea8c51
chore: merged
Jan 31, 2025
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 @@ -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=[]
Expand Down
20 changes: 20 additions & 0 deletions src/Recoils/HyperswitchAtom.res
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,23 @@ let moduleListRecoil: Recoil.recoilAtom<array<UserManagementTypes.userModuleType
"moduleListRecoil",
[],
)

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

let metaDataClonedAtom: Recoil.recoilAtom<JSON.t> = Recoil.atom(
"metaDataClonedAtom",
JSON.Encode.null,
)

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 @@ -46,6 +46,7 @@ type featureFlag = {
clickToPay: bool,
devThemeFeature: bool,
devReconv2Product: bool,
devClonePaymentMethods: bool,
}

let featureFlagType = (featureFlags: JSON.t) => {
Expand Down Expand Up @@ -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),
}
}

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 @@ -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
Expand All @@ -56,6 +69,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 @@ -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])

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

let customUI = <CloneConnectorPaymentMethods.ClonePaymentMethodsModal setShowModal showModal />

<>
<div>
{switch dashboardPageState {
Expand All @@ -110,7 +132,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
157 changes: 157 additions & 0 deletions src/screens/Connectors/CloneConnectorPaymentMethods.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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 && showModal}>
<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 (metaData, setMetaData) = React.useState(_ => JSON.Encode.null)
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 setPaymentMethodDetails = async () => {
try {
initialValues->setConnectorPaymentMethods(setPaymentMethods, setMetaData)->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)
setMetaDataClone(_ => metaData)
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 onClick={handleCloneClick}>
<ToolTip
description="Clone Payment Methods"
toolTipFor={<Icon name="clone" size=15 />}
toolTipPosition=Top
/>
</div>
<ClonePaymentMethodsModal showModal setShowModal />
</>
}
20 changes: 20 additions & 0 deletions src/screens/Connectors/ConnectorUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,26 @@ let defaultSelectAllCards = (
}
}

let setConnectorPaymentMethods = async (initialValues, setPaymentMethods, setMetaData) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please write this function inside the clone module itself

open LogicUtils
try {
let json = Window.getResponsePayload(initialValues)
let metaData = json->getDictFromJsonObject->getJsonObjectFromDict("metadata")
let paymentMethodEnabled =
json
->getDictFromJsonObject
->getJsonObjectFromDict("payment_methods_enabled")
->getPaymentMethodEnabled
setPaymentMethods(_ => paymentMethodEnabled)
setMetaData(_ => metaData)
} catch {
| Exn.Error(e) => {
let err = Exn.message(e)->Option.getOr("Something went wrong")
Exn.raiseError(err)
}
}
}

let getConnectorPaymentMethodDetails = async (
~initialValues,
~setPaymentMethods,
Expand Down
37 changes: 36 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 = (~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 = (~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 @@ -207,6 +209,14 @@ let make = (~showStepIndicator=true, ~showBreadCrumb=true) => {
isButton=true
/>

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

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 @@ -216,7 +226,29 @@ 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 {
setIsClonePMFlow(_ => false)
}
_ =>
RescriptReactRouter.push(
GlobalVars.appendDashboardPath(~url="/connectors"),
)
},
},
handleCancel: {
text: "No, don't go back",
},
}),
}
: {
title: "Processor",
Expand All @@ -237,6 +269,9 @@ let make = (~showStepIndicator=true, ~showBreadCrumb=true) => {
bannerType=Warning
/>
</RenderIf>
<RenderIf condition={isClonePMFlow && featureFlagDetails.devClonePaymentMethods}>
{infoBanner}
</RenderIf>
<div
className="bg-white rounded-lg border h-3/4 overflow-scroll shadow-boxShadowMultiple show-scrollbar">
{switch currentStep {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ let make = () => {
entity={ConnectorTableUtils.connectorEntity(
"connectors",
~authorization=userHasAccess(~groupAccess=ConnectorsManage),
~isCloningEnabled=featureFlagDetails.devClonePaymentMethods,
)}
currrentFetchCount={filteredConnectorData->Array.length}
collapseTableRow=false
Expand Down
Loading
Loading