Skip to content

fix: use in-app VisionCamera for chat attachment photos (re-land)#96458

Draft
MelvinBot wants to merge 2 commits into
mainfrom
claude-inAppCameraAttachments-relandV2
Draft

fix: use in-app VisionCamera for chat attachment photos (re-land)#96458
MelvinBot wants to merge 2 commits into
mainfrom
claude-inAppCameraAttachments-relandV2

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

This is a fresh re-land of the in-app VisionCamera flow for chat attachment photos (originally PR #86981, re-implemented in PR #87681, which was reverted). It replaces the external system camera intent with an in-app VisionCamera modal so Expensify stays in the foreground during capture — the original crash (#84018) was the OS reclaiming the backgrounded Expensify process while the system camera was open on Android 16.

Deploy blockers addressed in this re-land

  • #90047 (iOS: in-app camera does not open for avatar) / #90042 (iOS: app loads infinitely when taking a photo with camera permission denied). Root cause: an iOS modal-presentation race. The previous version called close() on the picker popover and then immediately opened the camera Modal, but iOS silently drops a modal that is presented while another modal is still dismissing — so the camera never appeared (leaving the caller hanging). Fixed by deferring the camera launch to onModalHide (so it only presents after the popover has fully dismissed), reusing the exact pattern the gallery/document flow already relies on for the same iOS quirk.

Not verified locally — needs on-device iOS testing before this is marked ready

  • #90119 (iOS: viewfinder blurry/grainy) and #90236 (iOS: app theme does not change while in camera mode) are iOS-runtime rendering behaviors that cannot be reproduced or validated without an iOS device. They are called out here so the reviewer verifies them on a device; if either still reproduces, they likely need a VisionCamera format/preview tweak (#90119) and a change away from the RN Modal for theme propagation (#90236) respectively.

Excluded as unrelated

Local checks run and passing: typecheck-tsgo, lint-changed, and oxfmt. React-compiler compliance will be validated by CI. All manual/on-device testing is left for the human co-author.

Fixed Issues

$ #84018
PROPOSAL: #84018 (comment)

Tests

  • Verify that no errors appear in the JS console

Offline tests

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
- [ ] I verified that similar component doesn't exist in the codebase - [ ] I verified that all props are defined accurately and each prop has a `/** comment above it */` - [ ] I verified that each file is named correctly - [ ] I verified that each component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone - [ ] I verified that the only data being stored in component state is data necessary for rendering and nothing else - [ ] In component if we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes - [ ] For Class Components, any internal methods passed to components event handlers are bound to `this` properly so there are no scoping issues (i.e. for `onClick={this.submit}` the method `this.submit` should be bound to `this` in the constructor) - [ ] I verified that component internal methods bound to `this` are necessary to be bound (i.e. avoid `this.submit = this.submit.bind(this);` if `this.submit` is never passed to a component event handler like `onClick`) - [ ] I verified that all JSX used for rendering exists in the render method - [ ] I verified that each component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions

Screenshots/Videosundefined

Replaces the external system camera intent with an in-app VisionCamera modal
for chat attachment photos (re-land of PR 87681). Routes the camera launch
through onModalHide so the camera modal only presents after the picker popover
has fully dismissed, fixing the iOS modal-presentation race that caused the
camera to not open / the app to load infinitely.

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/es.ts b/src/languages/es.ts
index b5a66776904..5b9aa296e68 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1175,7 +1175,7 @@ const translations: TranslationDeepObject<typeof en> = {
         dropTitle: 'Suéltalo',
         dropMessage: 'Suelta tu archivo aquí',
         flash: 'flash',
-        flipCamera: 'Cambiar cámara',
+        flipCamera: 'Cambiar de cámara',
         multiScan: 'escaneo múltiple',
         shutter: 'obturador',
         gallery: 'galería',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 1705d428725..4f7369240b1 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1215,7 +1215,7 @@ const translations: TranslationDeepObject<typeof en> = {
         dropTitle: 'Lascia perdere',
         dropMessage: 'Rilascia qui il tuo file',
         flash: 'flash',
-        flipCamera: 'Cambia fotocamera',
+        flipCamera: 'Inverti fotocamera',
         multiScan: 'scansione multipla',
         shutter: 'otturatore',
         gallery: 'galleria',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 209479d4d6d..f304a4f123c 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -1198,7 +1198,7 @@ const translations: TranslationDeepObject<typeof en> = {
         dropTitle: '手放して',
         dropMessage: 'ここにファイルをドロップしてください',
         flash: 'フラッシュ',
-        flipCamera: 'カメラ切替',
+        flipCamera: 'カメラを反転',
         multiScan: 'マルチスキャン',
         shutter: 'シャッター',
         gallery: 'ギャラリー',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 2bf3f415309..48682eb22e3 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1214,7 +1214,7 @@ const translations: TranslationDeepObject<typeof en> = {
         dropTitle: 'Laat het los',
         dropMessage: 'Zet je bestand hier neer',
         flash: 'flits',
-        flipCamera: 'Camera wisselen',
+        flipCamera: 'Camera omdraaien',
         multiScan: 'meerscannen',
         shutter: 'sluiter',
         gallery: 'galerij',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index c6429c56266..0861b845200 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1210,7 +1210,7 @@ const translations: TranslationDeepObject<typeof en> = {
         dropTitle: 'Odpuść to',
         dropMessage: 'Upuść tutaj plik',
         flash: 'błysk',
-        flipCamera: 'Przełącz kamerę',
+        flipCamera: 'Obróć kamerę',
         multiScan: 'wielokrotne skanowanie',
         shutter: 'migawka',
         gallery: 'galeria',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 8aca0907787..0ec7a8fd340 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1214,7 +1214,7 @@ const translations: TranslationDeepObject<typeof en> = {
         dropTitle: 'Deixe pra lá',
         dropMessage: 'Solte seu arquivo aqui',
         flash: 'flash',
-        flipCamera: 'Alternar câmera',
+        flipCamera: 'Inverter câmera',
         multiScan: 'escaneamento múltiplo',
         shutter: 'obturador',
         gallery: 'galeria',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index d6332d8b119..f39d6bdb7b9 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1166,7 +1166,7 @@ const translations: TranslationDeepObject<typeof en> = {
         dropTitle: '随它去',
         dropMessage: '将文件拖放到此处',
         flash: '闪光',
-        flipCamera: '切换相机',
+        flipCamera: '切换摄像头',
         multiScan: '多重扫描',
         shutter: '快门',
         gallery: '图库',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
...rc/components/Icon/chunks/expensify-icons.chunk.ts 0.00% <ø> (ø)
src/hooks/useNativeCamera.ts 73.33% <52.94%> (+14.31%) ⬆️
src/components/AttachmentPicker/index.native.tsx 19.23% <17.39%> (-0.10%) ⬇️
...c/components/AttachmentPicker/AttachmentCamera.tsx 74.35% <74.35%> (ø)
... and 10 files with indirect coverage changes

Move the cameraRef.current access into a module-level helper so React Compiler
does not flag ref access during render. OXC previously bailed on the whole file
(no memoization on web) while Babel still memoized it, producing a divergence.

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the failing React Compiler Compliance check and pushed to the branch.

Root cause: useTapToFocusGesture read cameraRef.current (a ref passed in as a parameter) inside the hook body. React Compiler treats that as "Cannot access refs during render". OXC bailed on the whole file (so it wasn't memoized on web), while Babel still memoized useNativeCamera — that mismatch is exactly the "new memoization divergence" the check blocks.

Fix: Moved the .current dereference into a module-level focusCameraAtPoint(cameraRef, point) helper, which React Compiler doesn't analyze. The hook now only passes the ref object through (which is allowed) and the focusCamera callback still closes over the live ref, so runtime behavior is unchanged. Both compilers now compile and memoize the file → no divergence.

I also removed the now-stale useNativeCamera.ts / react-hooks/refs entry from eslint.seatbelt.tsv (the fix eliminated that grandfathered violation; leaving the entry would fail the frozen seatbelt check in CI).

Verification
  • npm run react-compiler-compliance-check check src/hooks/useNativeCamera.ts → exit 0 (no divergence)
  • ./scripts/lint.sh src/hooks/useNativeCamera.ts → ✔ Lint done
  • npm run typecheck-tsgo → exit 0
  • npm run fmt applied

The other failing check (checklist) is a human PR checklist, not a code failure.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

All the code checks are green now — React Compiler Compliance, ESLint, Oxfmt, typecheck, and every unit test pass after my earlier fix.

The only remaining failing check is PR Author Checklist, which is a human checklist, not a code failure. There's nothing for me to fix here — ShridharGoel, please tick the remaining boxes in the PR body's author checklist and it'll go green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants