diff --git a/config/config.toml b/config/config.toml index f19518d3c..5a599d6a7 100644 --- a/config/config.toml +++ b/config/config.toml @@ -11,6 +11,7 @@ agreement_url="" agreement_version="" apple_pay_certificate_url="" mixpanel_token="" +recon_iframe_url="" [default.features] test_live_toggle=false is_live_mode=false @@ -38,4 +39,5 @@ configure_pmts=false branding=false totp=false live_users_counter=false -granularity=false \ No newline at end of file +granularity=false +recon_v2=false \ No newline at end of file diff --git a/src/entryPoints/FeatureFlagUtils.res b/src/entryPoints/FeatureFlagUtils.res index abab138f6..48d18a8b0 100644 --- a/src/entryPoints/FeatureFlagUtils.res +++ b/src/entryPoints/FeatureFlagUtils.res @@ -28,6 +28,7 @@ type featureFlag = { totp: bool, liveUsersCounter: bool, granularity: bool, + reconV2: bool, } let featureFlagType = (featureFlags: JSON.t) => { @@ -63,6 +64,7 @@ let featureFlagType = (featureFlags: JSON.t) => { totp: dict->getBool("totp", false), liveUsersCounter: dict->getBool("live_users_counter", false), granularity: dict->getBool("granularity", false), + reconV2: dict->getBool("recon_v2", false), } typedFeatureFlag } diff --git a/src/entryPoints/HyperSwitchApp.res b/src/entryPoints/HyperSwitchApp.res index 8f7fcd08c..c5590e4b7 100644 --- a/src/entryPoints/HyperSwitchApp.res +++ b/src/entryPoints/HyperSwitchApp.res @@ -462,6 +462,16 @@ let make = () => { + | list{"upload-files"} + | list{"run-recon"} + | list{"recon-analytics"} + | list{"reports"} + | list{"config-settings"} + | list{"file-processor"} => + + urlPath} /> + + | list{"sdk"} => diff --git a/src/entryPoints/HyperSwitchEntry.res b/src/entryPoints/HyperSwitchEntry.res index d7029730b..4ca4d430f 100644 --- a/src/entryPoints/HyperSwitchEntry.res +++ b/src/entryPoints/HyperSwitchEntry.res @@ -40,6 +40,7 @@ module HyperSwitchEntryComponent = { ->getString("apple_pay_certificate_url", "") ->getNonEmptyString, agreementVersion: dict->getString("agreement_version", "")->getNonEmptyString, + reconIframeUrl: dict->getString("recon_iframe_url", "")->getNonEmptyString, } DOMUtils.window._env_ = value configureFavIcon(value.faviconUrl)->ignore diff --git a/src/entryPoints/SidebarValues.res b/src/entryPoints/SidebarValues.res index caa00a049..f46052fbb 100644 --- a/src/entryPoints/SidebarValues.res +++ b/src/entryPoints/SidebarValues.res @@ -466,7 +466,59 @@ let developers = (isDevelopersEnabled, userRole, systemMetrics, ~permissionJson) : emptyComponent } -let reconTag = (recon, isReconEnabled) => +let uploadReconFiles = { + SubLevelLink({ + name: "Upload Recon Files", + link: `/upload-files`, + access: Access, + searchOptions: [("Upload recon files", "")], + }) +} + +let runRecon = { + SubLevelLink({ + name: "Run Recon", + link: `/run-recon`, + access: Access, + searchOptions: [("Run recon", "")], + }) +} + +let reconAnalytics = { + SubLevelLink({ + name: "Analytics", + link: `/recon-analytics`, + access: Access, + searchOptions: [("Recon analytics", "")], + }) +} +let reconReports = { + SubLevelLink({ + name: "Reports", + link: `reports`, + access: Access, + searchOptions: [("Recon reports", "")], + }) +} + +let reconConfigurator = { + SubLevelLink({ + name: "Configurator", + link: `config-settings`, + access: Access, + searchOptions: [("Recon configurator", "")], + }) +} +let reconFileProcessor = { + SubLevelLink({ + name: "File Processor", + link: `file-processor`, + access: Access, + searchOptions: [("Recon file processor", "")], + }) +} + +let reconTag = (recon, isReconEnabled) => { recon ? Link({ name: "Reconcilation", @@ -475,6 +527,25 @@ let reconTag = (recon, isReconEnabled) => access: Access, }) : emptyComponent +} + +let reconAndSettlement = (recon_v2, isReconEnabled) => { + recon_v2 && isReconEnabled + ? Section({ + name: "Recon And Settlement", + icon: "recon", + showSection: true, + links: [ + uploadReconFiles, + runRecon, + reconAnalytics, + reconReports, + reconConfigurator, + reconFileProcessor, + ], + }) + : emptyComponent +} let useGetSidebarValues = (~isReconEnabled: bool) => { let {user_role: userRole} = @@ -497,6 +568,7 @@ let useGetSidebarValues = (~isReconEnabled: bool) => { quickStart, disputeAnalytics, configurePmts, + reconV2, } = featureFlagDetails let sidebar = [ @@ -518,6 +590,7 @@ let useGetSidebarValues = (~isReconEnabled: bool) => { ), default->workflow(isSurchargeEnabled, ~permissionJson, ~isPayoutEnabled=payOut), recon->reconTag(isReconEnabled), + reconV2->reconAndSettlement(isReconEnabled), default->developers(userRole, systemMetrics, ~permissionJson), settings( ~isSampleDataEnabled=sampleData, diff --git a/src/entryPoints/configs/HyperSwitchConfigTypes.res b/src/entryPoints/configs/HyperSwitchConfigTypes.res index abd6a8e48..3df8a99bf 100644 --- a/src/entryPoints/configs/HyperSwitchConfigTypes.res +++ b/src/entryPoints/configs/HyperSwitchConfigTypes.res @@ -7,6 +7,7 @@ type urlConfig = { agreementUrl: option, agreementVersion: option, applePayCertificateUrl: option, + reconIframeUrl: option, } type customStyle = { diff --git a/src/libraries/Window.res b/src/libraries/Window.res index ffa2f4cfe..6e5ed83d6 100644 --- a/src/libraries/Window.res +++ b/src/libraries/Window.res @@ -2,6 +2,8 @@ type t type listener<'ev> = 'ev => unit +type event = {data: string} + @val @scope("window") external parent: 't = "parent" diff --git a/src/screens/Recon/ReconModule.res b/src/screens/Recon/ReconModule.res new file mode 100644 index 000000000..0a5497e2a --- /dev/null +++ b/src/screens/Recon/ReconModule.res @@ -0,0 +1,95 @@ +@react.component +let make = (~urlList) => { + open APIUtils + open LogicUtils + + let getURL = useGetURL() + let fetchDetails = useGetMethod() + let (redirectToken, setRedirectToken) = React.useState(_ => "") + let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Loading) + let (iframeLoaded, setIframeLoaded) = React.useState(_ => false) + let iframeRef = React.useRef(Js.Nullable.null) + + let getReconToken = async () => { + try { + let url = getURL(~entityName=RECON, ~reconType=#TOKEN, ~methodType=Get, ()) + let res = await fetchDetails(url) + let token = res->LogicUtils.getDictFromJsonObject->LogicUtils.getString("token", "") + setRedirectToken(_ => token) + setScreenState(_ => PageLoaderWrapper.Success) + } catch { + | _ => setScreenState(_ => PageLoaderWrapper.Error("Something went wrong!")) + } + } + + let redirectUrl = switch urlList { + | list{"upload-files"} + | list{"run-recon"} + | list{"reports"} + | list{"config-settings"} + | list{"file-processor"} => + urlList->List.toArray->Array.joinWithUnsafe("/") + | list{"recon-analytics"} => "analytics" + | _ => "" + } + + React.useEffect2(() => { + getReconToken()->ignore + None + }, (iframeLoaded, redirectUrl)) + + <> + { + switch iframeRef.current->Js.Nullable.toOption { + | Some(iframeEl) => { + let tokenDict = [("token", redirectToken->JSON.Encode.string)]->Dict.fromArray + let dict = + [ + ("eventType", "AuthenticationDetails"->JSON.Encode.string), + ("payload", tokenDict->JSON.Encode.object), + ]->Dict.fromArray + iframeEl->IframeUtils.iframePostMessage(dict) + } + + | None => () + } + + {if redirectToken->isNonEmptyString { + + { + setIframeLoaded(_ => true) + }} + id="recon-module" + className="h-full w-full" + src={`${Window.env.reconIframeUrl->Option.getOr("")}/${redirectUrl}`} + height="100%" + width="100%" + ref={iframeRef->ReactDOM.Ref.domRef} + /> + + } else { + + + + {"If you encounter any errors, please refresh the page to resolve the issue."->React.string} + + { + getReconToken()->ignore + }} + /> + + + }} + + } + > +} diff --git a/src/utils/IframeUtils.res b/src/utils/IframeUtils.res new file mode 100644 index 000000000..792573edb --- /dev/null +++ b/src/utils/IframeUtils.res @@ -0,0 +1,24 @@ +@val external document: 'a = "document" +type window +type parent +@val external window: window = "window" +@val @scope("window") external iframeParent: parent = "parent" +type event = {data: string} +@get +external contentWindow: Dom.element => Dom.element = "contentWindow" + +@send external postMessageToParent: (parent, JSON.t, string) => unit = "postMessage" +let handlePostMessage = (~targetOrigin="*", messageArr) => { + iframeParent->postMessageToParent(messageArr->Dict.fromArray->JSON.Encode.object, targetOrigin) +} + +@send external postMessageToChildren: (Dom.element, string, string) => unit = "postMessage" +let sendPostMessage = (element, message, ~targetOrigin="*") => { + element->postMessageToChildren(message->JSON.Encode.object->JSON.stringify, targetOrigin) +} + +let iframePostMessage = (iframeRef: Dom.element, message) => { + iframeRef + ->contentWindow + ->sendPostMessage(message) +} diff --git a/tailwind.config.js b/tailwind.config.js index 01e010596..bc68ccb0b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -47,6 +47,7 @@ module.exports = { "50-rem": "50rem", "93-per": "93%", "80-vh": "80vh", + "85-vh": "85vh", "30-vh": "30vh", "40-vh": "40vh", "75-vh": "75vh",