Skip to content

Two merchant portal finds: GrowthChart and MerchantSwitcher โ€‹

  • Status: one fixed, one diagnosed and left alone for routing, 2026-08-27.
  • Source: raised by another session, outside every live lane's territory that day.
  • Related: merchant-defect-fixes.md, the M1 to M5 set from the same portal.

GrowthChart called everything a signup โ€‹

Fixed. The empty state now uses the props the component already takes. โ€‹

  • GrowthChart.jsx takes metric and metricPlural for exactly this purpose, resolves them into a local plural on line 16, and uses them correctly in the summary line and the hover tooltip. The empty branch alone hardcoded "No signups in this window".
  • The one live caller is the merchant Overview ad-views chart, Overview.jsx:354, which passes metric="impression" metricPlural="impressions". So an impressions chart with no data announced that there were no signups.
  • The fix reads No {plural} in this window. plural is already computed above the early return, so nothing was plumbed. The metric = 'signup' default is untouched, so any caller that omits the props keeps today's wording.
  • Verified: eslint clean, and the 124 merchant tests still pass.

The merchant switcher says "Viewing Merchant" instead of the business name โ€‹

NOT fixed. It is not a one-line read, so it is reported for routing. โ€‹

  • MerchantSwitcher.jsx lives in shared/, which was task 4's territory on 2026-08-27, and the standing instruction was to touch it only if the real name was already in scope.
  • It is not. The component has no business name from its caller: MerchantShell.jsx holds only urlMerchantId and user, and renders <MerchantSwitcher /> with no props. The switcher fetches the name itself and falls back to the literal 'Merchant'.

The cause is its own two guards disagreeing under StrictMode. โ€‹

  • The name effect carries both a cancelled flag and a nameFetchInFlight ref that dedupes concurrent requests for the same id.
  • nameFetchInFlight is a ref, so it survives StrictMode's simulated remount, while cancelled does not. The sequence: first mount starts the fetch and marks the id in flight, cleanup sets cancelled = true, the second mount hits the in-flight guard and returns WITHOUT starting a replacement, then the original fetch resolves, sees cancelled, and discards the name it just retrieved.
  • Nothing ever writes names[currentId], so the label stays on the 'Merchant' placeholder for the life of the session. apps/admin/src/main.jsx wraps the app in React.StrictMode, so this is the live path, not a hypothetical.

This is confirmed, not inferred. โ€‹

  • A throwaway probe rendered the component twice with an identical mocked getMerchantData: once bare and once inside <StrictMode>. Bare resolved to "Acme Cafe"; StrictMode never did. The probe was deleted rather than committed, since a real regression test belongs in __tests__/MerchantSwitcher.test.jsx alongside the fix.
  • That existing suite passes today because none of its cases render under StrictMode, which is why the bug reached the browser.

Why the page header on the same screen is fine. โ€‹

  • Overview.jsx:135 fetches the same field from the same function, but with a plain cancelled guard and no in-flight dedupe. StrictMode's second mount simply starts a second fetch, which resolves and sets the name. Two guards are what breaks the switcher; one guard works.

What a fix would have to decide. โ€‹

  • Whether the dedupe ref is worth keeping at all. Dropping it restores the Overview shape and fixes the bug outright, at the cost of one duplicate request per mount in dev.
  • If it is kept, clearing the id from nameFetchInFlight on cleanup would let the remount start its own fetch, but the two guards would still need to agree about which one owns the retry.
  • Either way it needs a browser pass, since the symptom only appears under StrictMode.

Built with VitePress