No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!257
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "253-android-profile-picture"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two related Android-only bugs in the universal client's profile-picture flow. Bundled together because they share
apps/client/app/(app)/profile.tsxand you can't verify the display fix without the upload working first.Fix 1 — upload fails on Android (#253)
Root cause:
fetchPickedAsBlob(uri)calledfetch(uri)to read the picker URI. React Native's whatwg-fetch polyfill does NOT understandfile:///schemes — every Android upload bubbled up as "Upload failed. Try again." without ever reaching the server.Fix: new
buildPictureFormData()helper that branches onPlatform.OS. On web it keeps the existingblob:/data:fetch path. On native it appends the RN-idiomatic file-descriptor{ uri, name, type }shape to FormData, which RN's fetch polyfill recognises and streams off disk at send time.useUpdateProfilePicture()'s mutation signature changes fromBlob | FiletoFormDataso each platform builds the body in the shape its fetch understands.Fix 2 — set picture doesn't render on Android (#256)
Root cause: the avatar's
<Image>does NOT go through the typed client's URL-rewriter middleware (apiClient.ts'sonRequest). On Android and inside the Tauri shell the bundle is loaded off-origin from the API, so a relative/api/profile/pictureresolved againsttauri://localhostor Expo Go's bundle URL — garbage. Headers were attached but the URL itself was wrong.Fix: new
pictureSrc.tshelper that splices the runtime server URL in front of the path on native + Tauri and attaches the bearer token viasource.headers. PWA stays relative — cookie covers auth. Missing server URL or bearer → return null so the consumer falls back to initials rather than racing a doomed image fetch. Added a syncgetCachedAccessToken()tolib/auth/storage.tsso the avatar source resolves without asetStateround-trip; the in-memory cache is populated by the boot-time auth-header path.Test plan
pnpm install --frozen-lockfilepnpm -F @carol/api typecheck/lint/testpnpm -F @carol/api-client typecheck/lint/test/checkpnpm -F @carol/client typecheck/lint/test/export:webbuildPictureFormData(Platform.OS web vs android) and all three branches ofgetProfilePictureSource(no picture / PWA relative / off-origin absolute + headers + missing-piece fallback).Closes #253.
Closes #256.
Android's `expo-image-picker` returns a `file:///` URI for the picked image, and React Native's whatwg-fetch polyfill cannot `fetch()` that scheme — every Android upload bubbled up as "Upload failed. Try again." without reaching the server. Build the multipart body via the RN-idiomatic file-descriptor shape: form.append("file", { uri, name, type } as never) which RN recognises and streams off disk at send time. Web keeps its existing `blob:` / `data:` fetch path, gated on `Platform.OS`. The helper lives next to the existing picker validation so the screen just calls `buildPictureFormData(picked)` and passes the FormData to the mutation. `useUpdateProfilePicture()` accepts FormData directly (was `Blob | File`) so each platform builds the body in the shape its fetch understands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>The avatar's `<Image>` does NOT run through the typed client's URL-rewriter middleware in `apiClient.ts`, so a relative `/api/profile/picture` resolved against `tauri://localhost` (Flatpak) or Expo Go's bundle URL — garbage. The previous code passed only the relative path plus headers; the platform image loader had no real URL to dial. Build the source explicitly per platform in a small `pictureSrc.ts` helper: - PWA (same-origin web): `{ uri: "/api/profile/picture?v=…" }` — cookie covers auth. - Off-origin (Android + Tauri): splice the runtime server URL in front and attach the bearer via `source.headers`. Missing URL or bearer → return null so the consumer falls back to initials rather than racing a doomed image fetch. Drop the previous async bearer-resolution `useEffect` from the Avatar in favour of a sync read against the in-memory access-token cache (`getCachedAccessToken()`, mirror of the SecureStore-cache fix). The cache is populated by the boot-time auth-header path, so by the time the profile screen renders the bearer is available without a `setState` round-trip. The old `avatarUrl.ts` helper went unreferenced and is removed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.