Skip to content

feat: decouple dashboard bootstrap from merchant account API#4903

Open
kanikabansal08 wants to merge 1 commit into
mainfrom
feat/dashboard-bootstrap-lightweight-merchant-details
Open

feat: decouple dashboard bootstrap from merchant account API#4903
kanikabansal08 wants to merge 1 commit into
mainfrom
feat/dashboard-bootstrap-lightweight-merchant-details

Conversation

@kanikabansal08

Copy link
Copy Markdown
Collaborator

Type of Change

  • New feature
  • Refactoring

Description

Replaces the merchant-account API dependency at dashboard bootstrap with the new
lightweight user merchant-details endpoint, so roles without the account
resource can load the dashboard.

  • Add USER_MERCHANT_DETAILS endpoint (v1: user/merchant_details,
    v2: v2/users/merchant-details) + a lightweight fetch hook returning only
    product_type / merchant_account_type.
  • Bootstrap (HyperSwitchApp) now uses the lightweight endpoint to set the
    active product — no account permission required.
  • Full merchant account is fetched lazily only where its fields are needed
    (WebSDK, PaymentLinkThemeConfigurator, home) via useMerchantDetails.
  • Home "Credentials and Keys" card is gated by account access, and its fetch is
    guarded so non-account users no longer hit the merchant-account API on home.
  • Home "Connect Processors" button is now an ACLButton gated by ConnectorsView.

Motivation and Context

Dashboard load was coupled to Account-level permissions because bootstrap called
the full merchant-account API just to read a couple of signals. That leaked
account access into roles that didn't need it. This migrates only the bootstrap
path to the low-privilege endpoint.

How did you test it?

  • npx rescript compiles clean.
  • Manual: dashboard loads for a role without the account resource; account-bearing
    roles still see keys card / SDK publishable key; non-account roles no longer
    trigger the merchant-account call on home; "Connect Processors" disabled without
    connector access.

Where to test it?

  • INTEG
  • SANDBOX

Backend Dependency

  • Yes

Backend PR URL: juspay/hyperswitch#12160

Feature Flag

  • No feature flag changes

Checklist

  • I ran npm run re:build
  • I reviewed submitted code

🤖 Generated with Claude Code

Replace the merchant-account API dependency at dashboard bootstrap with the
lightweight user merchant details endpoint, so groups without the account
resource can load the dashboard.

- Add USER_MERCHANT_DETAILS endpoint (v1: user/merchant_details,
  v2: v2/users/merchant-details) and a lightweight fetch hook that only
  reads product_type and merchant_account_type
- Bootstrap now uses the lightweight endpoint for the active product
- Full merchant account is fetched lazily, only where needed, via
  useMerchantDetails (guarded by account access)
- Gate the homepage credentials/keys card by account access and guard the
  fetch so non-account users no longer hit the merchant account API
- Guard the homepage "Connect Processors" button with connector access

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kanikabansal08 kanikabansal08 requested a review from a team as a code owner June 4, 2026 09:03
@semanticdiff-com

Copy link
Copy Markdown

Review changes with  SemanticDiff

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Summary

Classification: Feature — Decouple dashboard bootstrap from merchant account API
Risk Level: Low
Files Changed: 9 files


Verification Against Control Center Checklist

Tier 1 — High Signal:

Naming ConsistencyUSER_MERCHANT_DETAILS follows the existing entity naming convention; useFetchUserMerchantDetails clearly distinguishes from useFetchMerchantDetails

Feature Flag Gating — Proper ACL gating on "Connect Processors" button (ConnectorsView) and "Credentials and Keys" card (MerchantDetailsView + AccountView)

Reuse Existing Utilities — Uses existing hasAnyGroupAccess pattern from GroupACLHooks; follows established useMerchantDetails lazy-fetch pattern

Error Handling — Catch block in useFetchUserMerchantDetails properly propagates errors via Exn.raiseError

Tier 2 — Medium Signal:

Blast Radius Checked — Changes to merchantDetailsValueAtom are localized; the lazy fetch pattern doesn't affect existing direct Recoil consumers

Effect DependenciesuseMerchantDetails effect has proper dependency array with shouldFetch and empty array for mount-only behavior


Minor Observation

🔍 Silent error swallow in useMerchantDetails effect

In MerchantDetailsHook.res, lines 82-84:

} catch {
| _ => ()
}

The error is silently discarded when fetchMerchantDetails fails. Consider logging the error for observability:

| err => {
    logger.error("Failed to load merchant details", err)
}

However, given this is guarded by showErrorToast=false and the caller explicitly controls error display, this is acceptable if intentional.


Positive Notes

  • Clean separation between lightweight USER_MERCHANT_DETAILS endpoint and full MERCHANT_ACCOUNT endpoint
  • Proper V1/V2 API version handling in both endpoints
  • The lazy fetch pattern (useMerchantDetails) correctly avoids unnecessary API calls when publishable_key is already populated

Verdict:Approve — Clean implementation following established patterns with proper ACL gating.

@XyneSpaces

Copy link
Copy Markdown
Collaborator

💡 Review Finding - useEffect Dependencies

In MerchantDetailsHook.res, the useMerchantDetails hook's useEffect has an empty dependency array ([]). If shouldFetch becomes true after initial render (e.g., permissions are granted during the session), the effect won't re-run to fetch merchant details.

Suggestion: Consider adding shouldFetch and version to the dependency array, or document why reactive refetching isn't needed.

@XyneSpaces

Copy link
Copy Markdown
Collaborator

💡 Review Finding - Error Context

In useFetchUserMerchantDetails, the catch block raises a generic error. Consider including which API version (V1/V2) failed to aid debugging.

Suggested Fix:

| Exn.Error(e) => {
    let versionStr = switch version { | V1 => "V1" | V2 => "V2" }
    let err = Exn.message(e)->Option.getOr(`Failed to fetch user merchant details (${versionStr})!`)
    Exn.raiseError(err)
  }

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Summary

Classification: Feature — Dashboard Bootstrap Decoupling
Risk Level: Medium (bootstrap path changes)
Files Changed: 9 files (+91/-13 lines)


Verification Against Control Center Checklist

Tier 1 — High Signal:

Naming ConsistencyuseFetchUserMerchantDetails follows useFetchMerchantDetails pattern; USER_MERCHANT_DETAILS matches entity naming convention
Reuse Existing Utilities — Uses existing ACLButton, EnableMerchantDetailsContext patterns
Feature Flag Gating — No feature flag needed for this permission decoupling
Blast Radius — Changes localized to bootstrap path; graceful degradation for existing flows

Tier 2 — Medium Signal:

Error Handling — Proper error states in useFetchUserMerchantDetails with setScreenState(Error(...))
Prop ConsistencyuseMerchantDetails(~shouldFetch) hook properly forwards boolean param


Findings

🔍 ~shouldFetch parameter naming

let useMerchantDetails = (~shouldFetch) => {

Consider a more descriptive name like ~lazy or ~fetchImmediately to better convey intent. shouldFetch is vague — does it mean "should fetch now" or "should fetch at all"?

Fix: Rename to ~fetchImmediately or document the parameter with a comment.


💡 Redundant useEffect in HomeUtils.res

React.useEffect(() => {
  if isAccessAllowedForAccount {
    merchantDetails.fetchMerchantDetails()->ignore
  }
  None
}, [isAccessAllowedForAccount])

This effect fetches when isAccessAllowedForAccount becomes true. Verify this doesn't cause double-fetching if the component re-renders while isAccessAllowedForAccount remains true.

Fix: Consider adding a fetched flag or use useMemo to ensure single fetch:

let _ = React.useMemo(() => {
  if isAccessAllowedForAccount {
    merchantDetails.fetchMerchantDetails()->ignore
  }
}, [isAccessAllowedForAccount])

⚠️ productType mapping from API response

productType: Dict.get(merchantDict, "product_type")

The API returns "product_type" but the variable is named productType (camelCase). Verify the API field name matches exactly — ReScript dict access is case-sensitive.


Positive Notes

  • Clean separation of concerns: lightweight bootstrap vs full merchant details
  • Proper ACL gating on Credentials and Keys card
  • Good use of ACLButton for Connectors button
  • Fallback to full merchant details preserved in existing consumers

Verdict:Approve — Clean decoupling of dashboard bootstrap from full merchant account API. Minor suggestions on naming and effect handling.

@@ -30,4 +61,26 @@ let useFetchMerchantDetails = () => {
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 Parameter naming suggestion

~shouldFetch is ambiguous — does it mean "fetch now" or "should ever fetch"? Consider ~fetchImmediately or ~lazy for clarity.

@@ -147,8 +148,9 @@ module ControlCenter = {
subHeading="Get a head start by connecting with 20+ gateways, payment methods, and networks."
/>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 Potential double-fetch on re-render

This useEffect fetches whenever isAccessAllowedForAccount is true. If the component re-renders while permission remains granted, the fetch re-executes. Consider guarding with a fetched state or using useMemo for one-time fetch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants