Specification: Phase 3.6 — SPA Integration
Wire the design system image components to the production backend through TanStack Query hooks (FD-01 typed provider pattern). All new code follows FD-02 hybrid structure. Entity persist stays in Redux (FD-05).
Follow the typescript-coding skill for TypeScript conventions, the
ui-component skill for component wiring patterns, and the
unit-tests-frontend skill for Jest + React Testing Library test conventions.
Entry Criteria
Section titled “Entry Criteria”- Phase 3.4 complete:
@arda-cards/design-systempublished with lifecycle types and updated components - Phase 3.5 complete: BFF routes implemented and tested
@arda-cards/api-proxypublished (Phase 3.1)- Phase 3.0 legacy cleanup committed
Source: SPA specification, upload-component-backend-analysis.md, goal.md Phase 4
T-1: Dependencies and Provider Setup
Section titled “T-1: Dependencies and Provider Setup”- Add
@tanstack/react-queryand@tanstack/react-query-devtoolstopackage.json - Bump
@arda-cards/design-systemto the most recent published version (from Phase 3.4) - Bump
@arda-cards/api-proxyto the most recent published version (from Phase 3.1). Note:api-proxyis a BFF-only dependency — it is used bysrc/server/routes, never by SPA code (constraint 12). - Create
src/providers/query-provider.tsxwithQueryClientconfiguration:staleTime: 5minretry: 1refetchOnWindowFocus: false
- Add
QueryClientProviderto the app provider stack inlayout.tsx, alongside the existingReduxProviderandAuthProvider - Full checks pass after this task before proceeding
T-2: SPA API Functions Layer (src/api/image-upload.ts)
Section titled “T-2: SPA API Functions Layer (src/api/image-upload.ts)”New architectural layer (FD-02). Plain async functions calling BFF routes via
fetch(). No TanStack imports. No api-proxy imports — that package is
BFF-only (constraint 12).
Type ownership: Request and response types (ImageUploadRequest,
ImageUploadResponse) are re-exported from @arda-cards/api-proxy/reference/item
through a shared types barrel. The SPA API layer imports these types for
compile-time validation against the Backend contract. Since api-proxy is
listed as a dependency of arda-frontend-app (for BFF usage), re-exporting
the types from a shared barrel does not introduce a new dependency.
Rate limit error handling: The BFF returns machine-readable error codes
for rate limit rejections (e.g., { code: "RATE_LIMITED", retryAfterMs })
rather than user-facing text. The SPA API layer is responsible for
translating these codes into user-facing error messages appropriate for
display in the UI (e.g., in ImageDropZone).
Functions:
getImageUploadUrl(request: ImageUploadUrlRequest): Promise<ImageUploadUrlResponse>— POST/api/image-uploaduploadToS3(uploadUrl, formFields, imageBlob, onProgress?): Promise<void>— XHR withupload.onprogressfor future progress supportcheckReachability(url: string): Promise<boolean>— HEAD direct, BFF fallback on CORS errorfetchExternalImage(url: string): Promise<Blob>— POST/api/storage/fetch-urlrefreshCdnCookies(): Promise<void>— POST/api/storage/cdn-cookies
Unit tests for each function with mocked fetch.
T-3: TanStack Mutation Hooks (src/hooks/image-upload/)
Section titled “T-3: TanStack Mutation Hooks (src/hooks/image-upload/)”FD-01 typed provider implementations backed by TanStack Query.
useImageUpload()— mutation that orchestrates:getImageUploadUrl→uploadToS3→ returncdnUrl. Always JPEGcontentType(SPA-FR-021). Invalidates['items']on success. Single upload enforcement (SPA-FR-020): mutation is disabled while another is pending.useCheckReachability()— mutation wrappingcheckReachability()useFetchExternalImage()— mutation wrappingfetchExternalImage()- Query key factory:
imageKeysobject
Unit tests for each hook using @testing-library/react-hooks with a
QueryClientProvider wrapper.
T-4: CDN Cookie Query Hook (src/hooks/cdn/)
Section titled “T-4: CDN Cookie Query Hook (src/hooks/cdn/)”useCdnCookies(enabled?: boolean)— query with:refetchInterval: derived from the sharedAwsNaming.cookieRefreshIntervalMsconstant (default ~15 min = 50% of 30-min cookie TTL). The value is obtained viaresolveAwsNaming().cookieRefreshIntervalMs, ensuring the SPA refresh interval stays coupled to the server-side cookie TTL configuration.refetchIntervalInBackground: truestaleTime:cookieRefreshIntervalMs * 0.67(approximately 2/3 of the refresh interval), obtained from the sameresolveAwsNaming()call.retry: 2
useRefreshCdnCookies()— returns a function that invalidates['image', 'cdn-cookies']for imperative refresh (tenant switch, 403 recovery)
Unit tests: refetchInterval configured; imperative refresh triggers invalidation.
T-5: CDN Cookie Provider (src/providers/cdn-cookie-provider.tsx)
Section titled “T-5: CDN Cookie Provider (src/providers/cdn-cookie-provider.tsx)”- React Context providing
{ isReady: boolean, refreshNow: () => void } - Uses
useCdnCookies()anduseRefreshCdnCookies()internally - Added to the provider stack at app level
- Unit test: context values are accessible by children
T-6: Wiring Hooks (src/hooks/image-upload/use-item-image-upload-dialog.ts)
Section titled “T-6: Wiring Hooks (src/hooks/image-upload/use-item-image-upload-dialog.ts)”Bridge hooks that connect TanStack mutations to component callback prop signatures.
useItemImageUploadDialog()— returns:{ handleUpload, handleCheckReachability, isUploading, uploadError, resetUpload }matching theImageUploadDialogonUpload/onCheckReachabilityprop signatureshandleUpload: (blob: Blob) => Promise<string>— wrapsuseImageUpload().mutateAsync()handleCheckReachability: (url: string) => Promise<boolean>— wrapsuseCheckReachability().mutateAsync()
T-7: ITEM_IMAGE_CONFIG (src/constants/item-image-config.ts)
Section titled “T-7: ITEM_IMAGE_CONFIG (src/constants/item-image-config.ts)”ITEM_IMAGE_CONFIG: ImageFieldConfigwith:aspectRatio: 1:1acceptedFormats: [jpeg, png, webp, heic, heif]maxSizeBytes: 10 MBminDimensionPx: 200entityTypeDisplayName: "Item"propertyDisplayName: "Product Image"
T-7b: Shared CDN Image Recovery Hook (src/hooks/cdn/use-image-with-cdn-recovery.ts)
Section titled “T-7b: Shared CDN Image Recovery Hook (src/hooks/cdn/use-image-with-cdn-recovery.ts)”Shared hook providing 403 recovery for all image display contexts (grid,
form, inspector). Keeps ImageDisplay pure (FD-01) while ensuring consistent
recovery behavior across the app.
interface CdnImageState { /** Current image src — includes a cache-bust key after recovery. */ src: string | null; /** True while a cookie refresh is in progress after a 403. */ isRecovering: boolean; /** Called by ImageDisplay's onError prop. */ handleError: () => void;}
function useImageWithCdnRecovery( cdnUrl: string | null, options?: { maxRetries?: number },): CdnImageState;Behavior:
- Returns
cdnUrlassrcon initial render. - When
handleErroris called (image failed to load):- If the URL is a CDN URL: calls
refreshNow()fromCdnCookieContext, setsisRecovering: true, then updatessrcwith a cache-bust query param (?_r=<timestamp>) to force the browser to re-request the image with fresh cookies. The CDN host is derived fromNEXT_PUBLIC_INFRASTRUCTUREandNEXT_PUBLIC_PARTITIONenvironment variables using the same naming convention as the server-sideresolveAwsNaming()utility:${partition.toLowerCase()}.${infrastructure.toLowerCase()}.assets.arda.cards. The hook checksnew URL(cdnUrl).hostname === derivedCdnHost. This is computed once at module load time and reused across all hook instances. - If the URL is not a CDN URL: does nothing (the error is not cookie-related).
- If the URL is a CDN URL: calls
- Maximum one retry per URL per mount (configurable via
maxRetries, default 1). After exhausting retries, stops recovering — the error state inImageDisplaypersists. - Resets retry count when
cdnUrlchanges (new image).
Unit tests:
- CDN URL error triggers
refreshNow()and updatessrc - Non-CDN URL error does not trigger refresh
- Maximum retries respected — no infinite refresh loop
isRecoveringis true during refresh, false after- New
cdnUrlresets retry count
T-8: Grid Integration
Section titled “T-8: Grid Integration”Modify files under src/components/items/ in place:
- Import
ImageCellDisplayfrom@arda-cards/design-system/canary - Import
createImageCellEditorfactory from the design system - Update item grid column definitions:
- Image column
cellRenderer:ImageCellDisplay, withcellRendererParams: { config: ITEM_IMAGE_CONFIG } - Image column
cellEditor:createImageCellEditor({ imageConfig: ITEM_IMAGE_CONFIG, useImageUpload: ..., useCheckReachability: ... }) - Image column
editable: true
- Image column
- Import
ImageHoverPreviewfor the hover popover (~500 ms delay) - Wire
useImageWithCdnRecovery(T-7b) into the grid image cell renderer to handle 403 recovery. The hook’shandleErroris passed as theonErrorprop toImageCellDisplay. The grid wrapper batches recovery across multiple cells via TanStack Query’s deduplication of the cookie refresh query.
T-9: Form Integration
Section titled “T-9: Form Integration”Modify src/components/items/ItemFormPanel.tsx in place:
- Import
ImageFormFieldfrom@arda-cards/design-system/canary - Import
useItemImageUploadDialogwiring hook - Replace the disabled dropzone placeholder with
ImageFormFieldwired to the upload dialog - Add Item form: optional image field using
ITEM_IMAGE_CONFIG - Edit Item form: support change and remove; remove sends
imageUrl: nullvia the existing ReduxupdateItem()thunk (FD-05) - Entity persist after upload:
onConfirmreceivesImageUploadResult, setsform.imageUrl = result.imageUrl; form submission uses the existing Redux thunk - Wire
useImageWithCdnRecovery(T-7b) intoImageFormField’s image display to handle 403 recovery in form contexts. The hook’ssrcandhandleErrorare passed through to the underlyingImageDisplay.
T-10: Unit Tests
Section titled “T-10: Unit Tests”- Hook tests with
QueryClientProviderwrapper in test-utils - API function tests with mocked
fetch - Wiring hook tests
- Form integration tests (component render with mocked hooks)
- Grid integration tests (column definition verification)
T-11: Documentation checks and commit
Section titled “T-11: Documentation checks and commit”Update session log / byproducts in the documentation worktree.
# In the documentation worktreemake pr-checksCommit documentation changes referencing Phase 3.6.
Exit Criteria
Section titled “Exit Criteria”- All tasks complete with passing unit tests
- Full local checks pass:
Terminal window # arda-frontend-appnpm run lintnpx tsc --noEmitnpx jest --no-coverage --watchAll=false --forceExitnpm run buildTerminal window # documentationmake pr-checks - Image upload flow works end-to-end in mock mode (
npm run dev:mock) - No regressions in existing tests
- Documentation worktree: changes committed
STOP: Review SPA integration before proceeding to Phase 3.7.
Open Questions and Decisions
Section titled “Open Questions and Decisions”| # | Question | Options | Recommendation | Decision |
|---|---|---|---|---|
| 1 | 403 recovery wiring: component-level vs. grid-level | See trade-off analysis below | B — grid wrapper | Agreed |
| 2 | QueryClient in test-utils | A: shared wrapper in test-utils; B: per-test QueryClient | A — shared wrapper is consistent | Agreed |
Open Question #1: 403 Recovery Trade-offs
Section titled “Open Question #1: 403 Recovery Trade-offs”When a CDN image returns 403 (expired cookie), the app needs to refresh cookies and retry the image load. The question is where this logic lives.
Option A — Component-level (ImageDisplay calls refreshNow()):
ImageDisplay(design system) imports and usesuseCdnCookieContext()internally. On<img>onErrorwith 403, it callsrefreshNow(), waits, then retries by changing the<img>key.- Pro: Self-contained — every
ImageDisplayanywhere in the app handles 403 automatically. Works in grids, forms, detail panels, cards. - Con:
ImageDisplaygains a dependency onCdnCookieContext— a React Context defined inarda-frontend-app. This violates FD-01: design system components must not depend on app-specific contexts or TanStack Query. The component would no longer be portable. - Con: The
onErrorevent on<img>does not provide the HTTP status code — you only know the image failed, not whether it was a 403 vs. a genuine 404 or network error. The component would have to assume any error might be a stale cookie and try refreshing, which could cause unnecessary refresh storms. - Con: Multiple
ImageDisplayinstances failing simultaneously (e.g., a grid with 20 thumbnails) would each triggerrefreshNow()independently, though the cookie query deduplication in TanStack handles this.
Option B — Grid wrapper handles 403 and retries (decided):
- The grid integration code in
arda-frontend-app(T-8) wraps image rendering with 403 awareness. On image error, the grid-level wrapper callsrefreshNow()fromCdnCookieContext, then triggers a re-render of affected image cells. - Pro:
ImageDisplaystays pure (FD-01 compliant) — no context dependency, no retry logic, no awareness of CDN cookies. It renders what it’s given and reports errors viaonErrorprop. - Pro: The grid wrapper can batch the retry — one cookie refresh for all failed images, then re-render all affected rows.
- Pro: The wrapper has more context — it knows the error came from a CDN URL (vs. an external URL or a data URI), so it can decide whether cookie refresh is the right action.
- Mitigated: 403 recovery for non-grid contexts (
ImageFormField,ImageInspectorOverlay) is handled by the shareduseImageWithCdnRecoveryhook (T-7b). All image display contexts in the app use this hook, providing consistent recovery behavior without violating FD-01. - Con: Slightly more wiring code in the app (a shared hook that
intercepts
ImageDisplay’sonError).
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved