Dentolize · Canary Build Fix (DHS Integration) Walkthrough
On this pageBusiness viewTechnical view

The Build Fix: Prop Injection

Business view

Think of clinic-web-canary as a separately-built add-on module that the main clinic app loads pieces of. It's built and shipped on its own so new features (DHS integration, ZATCA e-invoicing, custom forms, and others) can move faster than the whole main app's release cycle. For that separation to actually work, the add-on module isn't allowed to reach into the main app's source code directly — it has to receive whatever it needs (like "who's logged in") handed to it explicitly, the same way you'd hand a contractor a key rather than give them your address and let them let themselves in.

The DHS screens didn't follow that rule. Eleven files reached directly into the main app's and mobile app's source folders to grab things like the logged-in user, a search query, or a loading spinner. The main app's type-checker didn't complain, because those imports were explicitly told to just trust it ("type it as any"). But the build tool that packages the add-on module for production isn't the type-checker — it's a bundler (Rollup, via Vite), and it does not trust it. It tried to process one of those directly-imported files, hit JSX syntax it wasn't configured to expect outside the add-on's own project boundary, and failed. The build failure was silent to end users: it didn't crash the main app, it just meant the DHS portion of the add-on module was missing from what got shipped.

This PR removes every one of those direct reaches into the main app's source and replaces them with explicit hand-offs (props) — the same pattern an existing feature in this module (PatientTimeline) already used correctly. It's mechanical, low-risk, and does not change what any screen does — only whether the code for it is actually present in the build.

Technical view

Before

11 files in packages/clinic-web-canary/src/features/DHS* imported 17 symbols from clinic-web and clinic-mobile source paths, e.g.:

import { useUser } from '@dentolize/clinic-web/src/context/userContext'
import { SEARCH_USERS_QUERY } from '@dentolize/clinic-mobile/src/shared/store/queries/appointmentQueries'

packages/clinic-web-canary/src/declaration.d.ts carried 8 declare module '...' statements with no type body, so every one of those imports typed as any. This satisfied tsc, but vite build --lib (which runs Rollup against the canary package alone) does not consult declaration.d.ts — it tries to resolve and transform the actual file. Per the PR description, the production build failed with a JSX parse error at clinic-web/src/context/userContext.js:17, because Rollup, scoped to the canary project root, has no JSX transform configured for a .js file living outside it. Reported build impact: 9 exports shipped, 0 DHS chunks, since the DHS feature was first added.

After

Every cross-package import is gone. packages/clinic-web-canary/src/declaration.d.ts now contains only asset-type shims (*.svg, *.png, etc.) — 0 any-typed module declarations, down from 8. The replacements fall into three patterns:

1. Props, for anything that depends on request/session state. useUser() calls are removed from all 9 canary wrapper components; the host now passes user={user} and the wrapper reads it from props instead:

- const { user } = useUser();
+ const { user } = props;

(e.g. packages/clinic-web-canary/src/features/DHSApprovalDetail/index.tsx, DHSApprovalSubmission/index.tsx, DHSCheckInsurance/index.tsx, DHSIntegrationSettings/index.tsx, DHSManualUpdate/index.tsx). Inside deeper components (DHSApprovalSubmission.tsx, DHSApprovalSubmissionModal.tsx, CheckInsuranceModal.tsx, DHSCheckInsurance.tsx, ReviewStep.tsx) user becomes a required prop threaded down from the wrapper, rather than each level calling its own useUser().

Host side, 5 .js files gained a user={user} attribute (user was already in scope in every case, since each file already calls useUser() for its own purposes):

Host fileMounts
ChartTable.js:355PendingActions (DHSManualUpdate)
ChartTableFooter.js:414DHSApprovalSubmission
PatientProfile.js:162,167DHSCheckEligibility, DHSCheckInsurance
PatientCommonFields.js:734DHSCheckInsurance
SettingsIntegrations.js:52DHSIntegrationSettings

DashboardRouter.js already passed user to DHSApprovalDetailWrapper and needed no change.

2. Canary-owned copies, for small pure code.

  • canSubmitOperation moved from @dentolize/clinic-mobile/.../dhsApprovalStatus to a new

file, packages/clinic-web-canary/src/features/DHS/shared/dhsApprovalStatus.ts. Its header comment flags that it's now one of three manually-synced copies (server, clinic-mobile, clinic-web-canary) — nothing enforces they agree.

  • SEARCH_USERS_QUERY and SEARCH_GLOBAL_CONDITIONS moved from clinic-mobile's

930-line appointment-queries barrel into a canary-owned DHSApprovalSubmission.queries.tsx.

  • useDebounce was repointed from clinic-web/src/shared/hooks/useDebounce to canary's own

src/hooks/useDebounce.ts, which four other canary features already used — no new file needed, just a corrected import path.

  • DashboardLoader — a plain centered <Spin> — gained a canary-owned copy at

packages/clinic-web-canary/src/patterns/DashboardLoader/DashboardLoader.tsx, exported from the package's patterns/index.tsx alongside Meta, PageTitle, ErrorFallback.

3. A string, where a live import was only ever needed as a cache key. PATIENT_DETAILS was previously imported from clinic-mobile's 5,556-line patient-queries barrel purely to pass as an Apollo refetchQueries entry. It's now the literal string 'PATIENT_DETAILS' (useDHSApprovalSubmission.tsx:32). Apollo matches refetch-by-name against the operation name in the query document, not against the imported object identity, so this is functionally equivalent — provided the host's query keeps that exact operation name.

4. A narrower replacement, where the imported component did more than the call site used. DHSStatusPopover (clinic-web, 203 lines + a 485-line style module + a 1,110-line tooth-helper import, because it also renders action buttons for retry/cancel/get-status) is replaced by DHSStatusTag (packages/clinic-web-canary/src/features/DHS/shared/DHSStatusTag.tsx, 121 lines, read-only). The new component's header comment documents why: DHSStatusPopover's action buttons only activate when showActions || hasNphies is true, and the only call site (DHSApprovalDetail.tsx:86) passes neither, so the buttons were always dead code at that call site. The dhs.* translation keys it reads are the host's own — populated into the shared i18next instance by clinic-mobile's translation JSON — and are deliberately not duplicated into a canary namespace.

Verified outcome (per PR description)

MetricBeforeAfter
Cross-package imports170
declare module shims80 (asset types only)
Canary bundle exports9 (no DHS)118 (all DHS features)
DHS lazy chunks emitted07
vite build (canary)Fails — JSX parse errorPasses
clinic-web buildPassesPasses — 0 errors

Known gap: the host side is untyped

The five host call sites that now pass user={user} are .js files with no TypeScript checking. Every hop inside clinic-web-canary is .tsx and enforces DHSUser at compile time, but nothing stops a future edit to one of those five host files from silently dropping user={user}. If that happens, the affected DHS feature receives user: undefined, its internal !user?.permissions?.X guard evaluates false, and the component returns null — no build error, no console error, no crash. The nested case is the least visible: ReviewStep.tsx:196 renders DHSCheckEligibility only if user?.permissions?.checkDhsEligibility, so a dropped prop there means an entire section of a modal silently doesn't appear, three component hops away from where the prop was lost. See For Quality for the specific manual check this implies.