feat(client): profile picture upload via expo-image-picker (#217) #248
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!248
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "217-profile-picture-upload"
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?
Summary
expo-image-pickerinto the Profile screen's "Change picture" / "Remove picture" actions, replacing the read-only "manage from web" hint with a working flow on web and Android.apps/client/lib/apiClient.ts): every outbound body was going throughrequest.text(), which silently corrupts non-UTF-8 bytes inside a multipart payload. The rewriter now picksrequest.arrayBuffer()when it seesmultipart/form-data. Extracted asapps/client/lib/apiClientBody.tswith a regression test that confirms the image's raw bytes survive the clone byte-for-byte (tests/apiClientBody.test.ts).useUpdateProfilePicture/useDeleteProfilePicturehooks to@carol/api-client(useUploadProfilePicturerenamed — wasn't used anywhere); both patch the profile bundle queryKey on success.Multipart through the off-origin rewriter
The off-origin path (Android + Tauri Flatpak) clones each outgoing
Requestso it can splice the runtime server URL in front of the relative path. The clone reads the body explicitly because RN'swhatwg-fetchpolyfill doesn't reliably transfer a stream vianew Request(url, request). Before this PR, the body was always read via.text(). For JSON that's fine; for multipart it isn't — the0xFE/0xFF-class bytes inside an image part get mangled intoU+FFFD(the UTF-8 replacement char) on the read-back. The fix is a content-type check +.arrayBuffer(). The new test asserts the original bytes appear verbatim in the cloned body.Files changed
apps/client/package.json— addexpo-image-picker@~56.0.18(SDK 56 dep matrix pin).apps/client/app/(app)/profile.tsx— PictureCard now exposes Change/Remove actions, inline confirm on Remove, error states, real avatar render via<Image>with bearer-header fallback.apps/client/lib/apiClient.ts+apps/client/lib/apiClientBody.ts— multipart-aware body extraction.apps/client/lib/profile/pictureUpload.ts— local MIME/byte validation matching the API's 5 MB cap.apps/client/lib/profile/avatarUrl.ts— composes the?v=<picture_uploaded_at>cache-busted URL.apps/client/README.md— documents the Android manifest entriesexpo-image-pickerbrings in (READ_MEDIA_IMAGESfor Android 13+,READ_EXTERNAL_STORAGEfor ≤ 12) and the local validation thresholds.packages/api-client/src/hooks/picture.ts— hooks now alsoinvalidateQuerieson the profile bundle.packages/i18n/messages/en.json— newprofile.picture.*keys for the action labels, confirm copy, error states.Test plan
pnpm install --frozen-lockfilepnpm -F @carol/api typecheckpnpm -F @carol/api lintpnpm -F @carol/api test(591 passed)pnpm -F @carol/api openapi:checkpnpm -F @carol/api-client typecheck/lint/test(23 passed) /checkpnpm -F @carol/client typecheck/lint/test(54 passed)pnpm -F @carol/client export:webCloses #217.
🤖 Generated with Claude Code
📊 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.