Skip to content

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

Merged
mountiny merged 28 commits into
mainfrom
claude-inAppCameraAttachments-relandV2
Sep 17, 2026
Merged

mountiny merged 28 commits into
mainfrom
claude-inAppCameraAttachments-relandV2

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Slack reference: https://expensify.slack.com/archives/C05LX9D6E07/p1772818699627849

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

These changes are Android/iOS Native only (VisionCamera is native). There is no in-app camera on web/desktop, so run the steps below on a native build.

  1. Open any chat (a 1:1 DM or a workspace/room chat).
  2. Tap the + (attachment) button next to the composer.
  3. In the attachment picker, tap Take photo.
  4. Verify the in-app camera opens as a modal (Expensify stays in the foreground — the OS/system camera app does NOT launch).
  5. If prompted, grant camera permission and verify the camera preview then appears.
  6. Tap the shutter button to capture a photo.
  7. Verify the captured photo opens in the attachment preview/confirmation screen and can be sent to the chat.
  8. Re-open the camera and tap the flip camera icon — verify it switches between the front and back cameras.
  9. Tap the flash toggle — verify it toggles on/off (on a device with a flash).
  10. Tap the close (X) button — verify the camera modal dismisses and returns you to the chat with nothing attached.
  11. Permission-denied path (iOS regression check for #90042): Deny camera permission (or disable it in system settings), then tap Take photo again. Verify the in-app permission prompt/screen is shown and the app does not hang on an infinite loading state.
  12. Modal-race regression check (#90047): Repeat step 3 several times in quick succession and verify the camera opens reliably every time (it should never silently fail to appear).
  13. Verify that no errors appear in the JS console.
  • Verify that no errors appear in the JS console

Offline tests

The in-app camera capture is a purely local operation and works fully offline:

  1. Turn off your network connection.
  2. Open a chat, tap +Take photo, and verify the in-app camera still opens and captures a photo normally.
  3. Verify the captured photo is queued/attached; it uploads and sends once the connection is restored (standard offline-attachment behavior).

QA Steps

Android/iOS Native only.

  1. Open any chat (a 1:1 DM or a workspace/room chat).
  2. Tap the + (attachment) button next to the composer.
  3. In the attachment picker, tap Take photo.
  4. Verify the in-app camera opens.
  5. If prompted, grant camera permission and verify the camera preview then appears.
  6. Tap the shutter button to capture a photo.
  7. Verify the captured photo opens in the attachment preview/confirmation screen and can be sent to the chat.
  8. Re-open the camera flow and tap the flip camera icon — verify it switches between the front and back cameras.
  9. Tap the flash toggle — verify it toggles on/off (on a device with a flash).
  10. Tap the close (X) button — verify the camera modal dismisses and returns you to the chat with nothing attached.
  11. Permission-denied path: Deny camera permission (or disable it in system settings), then tap Take photo again.
  12. Verify the in-app permission prompt/screen is shown.
  13. Verify the camera works fine on the profile picture/avatar flow also.
  14. Ensure things like focus and viewfinder work as expected.
  • 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
- [x] I verified that similar component doesn't exist in the codebase - [x] I verified that all props are defined accurately and each prop has a `/** comment above it */` - [x] I verified that each file is named correctly - [x] 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 - [x] I verified that the only data being stored in component state is data necessary for rendering and nothing else - [x] 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 - [x] 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) - [x] 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`) - [x] I verified that all JSX used for rendering exists in the render method - [x] 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

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

This comment has been minimized.

@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/useIsPlatformMuted.ts 100.00% <100.00%> (ø)
...c/libs/getVideoResolutionFormatFilter/index.ios.ts 100.00% <100.00%> (ø)
...bs/getVideoResolutionFormatFilter/index.android.ts 0.00% <0.00%> (ø)
src/libs/getVideoResolutionFormatFilter/index.ts 0.00% <0.00%> (ø)
src/libs/telemetry/ReceiptObservability.ts 83.75% <60.00%> (-1.59%) ⬇️
...c/components/AttachmentPicker/AttachmentCamera.tsx 91.20% <91.20%> (ø)
src/hooks/useNativeCamera.ts 59.57% <40.74%> (+1.24%) ⬆️
src/components/AttachmentPicker/index.native.tsx 23.67% <24.48%> (+0.44%) ⬆️
... and 9 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.

@ShridharGoel

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6bd1224c6f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/components/AttachmentPicker/AttachmentCamera.tsx
Comment thread src/components/AttachmentPicker/AttachmentCamera.tsx
@MelvinBot

Copy link
Copy Markdown
Contributor Author

There's no code-related check failure to fix here — every automated CI check is passing (typecheck, ESLint, Oxfmt, React Compiler, unit tests, builds, and perf tests all green).

The only two failing checks are:

  • PR Author Checklist / checklist
  • PR Reviewer Checklist / checklist

These aren't code failures. They fail because the checklist boxes in the PR body aren't ticked — those items attest to human actions (manual testing on all platforms, no console errors, reviewer sign-off, etc.), so they can only be completed by a person, not by pushing a commit.

To clear them:

  • Author checklist: go through the PR Author Checklist section and check each box after you've done the corresponding step (in particular the on-device iOS testing this re-land still needs, per the PR description, plus filling in the Tests/QA Steps sections — or adding [No QA] to the title).
  • Reviewer checklist: this is completed by the assigned reviewer during review.

I intentionally did not auto-tick these boxes — doing so would falsely attest that manual/on-device testing was performed.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Nothing has changed since my previous comment — there's still no code-related check failure to fix. Every automated CI check is passing.

The only two failing checks are PR Author Checklist / checklist and PR Reviewer Checklist / checklist. These fail because the checklist boxes in the PR body aren't ticked — those items attest to human actions (manual/on-device testing, no console errors, reviewer sign-off, filling in Tests/QA Steps or adding [No QA] to the title). They can only be cleared by a person, not by pushing a commit, and I won't auto-tick them since that would falsely attest testing was done.

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

Copy link
Copy Markdown
Contributor Author

The typecheck failure isn't caused by the camera code itself — this branch is 8409 commits behind main, and both errors are drift that only appears when CI type-checks the PR merged with main. Fixing them requires merging main into the branch (and regenerating translations), which I'm not doing automatically because it's a large, judgment-heavy merge with conflicts in auto-generated files — see below.

The two type errors

  1. AttachmentCamera.tsx:247-252 passes reasonAttributes to ActivityIndicator, but main fully reverted the skeleton-span telemetry — on main, ActivityIndicator no longer accepts reasonAttributes (only extraLoadingContext) and src/libs/telemetry/useSkeletonSpan.ts no longer exists. AttachmentCamera.tsx is a new file only on this branch, so it's the one spot still using the removed API after the merge.

  2. src/languages/el.ts (the Greek locale, added on main after this branch was cut) is missing flipCamera, which the branch added to en.ts:1287. Since en.ts is the source of truth, every locale must define it. el.ts isn't on this branch at all, so it can only be fixed after main is merged in.

What you'll need to do

  1. git merge main into claude-inAppCameraAttachments-relandV2. Expect conflicts in the locale files and the icon chunk — these are generated, so regenerate rather than hand-merging (npm run generateTranslations for locales after resolving en.ts). Note main also added el and removed pt-BR, so the locale set has changed.
  2. Drop the reasonAttributes prop from the ActivityIndicator in AttachmentCamera.tsx (the skeleton-span telemetry it relied on no longer exists on main). Confirm this is the intended behavior for the re-land — if you want that loading telemetry back, it's a separate re-land of the reverted feature, not part of this PR.
  3. Re-run npm run typecheck and confirm all locales (including el) define the new camera keys (flipCamera, flash, multiScan, shutter, gallery, takePhoto).
Why I didn't push a fix
  • A minimal edit can't be verified in isolation: on the branch as-is, ActivityIndicator still requires reasonAttributes, so removing it from AttachmentCamera only type-checks after main is merged (which is what CI actually tests). And el.ts doesn't exist on this branch, so its missing key can't be added without the merge.
  • The merge is 8409 commits and its conflicts land in auto-generated files (all locale files, expensify-icons.chunk.ts) that should be regenerated with the project tooling, not resolved by hand.
  • The merge reverts an app-wide feature (skeleton-span telemetry) that the camera code depends on — how the loading indicator should behave post-revert is a call for the author to make.

The other two failing checks (PR Reviewer Checklist, Check independent approval) are human/review gates, not code issues.

@ShridharGoel

Copy link
Copy Markdown
Contributor

@MelvinBot Merge the latest main

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the failing TypeScript Checks / typecheck. The PR was green except for the two ignore-listed checklist checks and typecheck, which broke once the branch was merged against the latest main — both errors were merge-with-main interactions, so I merged main into the branch and resolved them:

  1. AttachmentCamera.tsx(251)reasonAttributes no longer valid on ActivityIndicator. main removed the skeleton-span telemetry props (reasonAttributes / useSkeletonSpan) from ActivityIndicator, but this PR's AttachmentCamera still passed reasonAttributes. Removed that prop (no other file in the tree references it anymore).
  2. el.ts(1229) — missing flipCamera. main added the Greek locale after this branch was cut. This PR adds the receipt.flipCamera key to the en type (source of truth), so every locale must define it — el.ts was the only one missing it. Added flipCamera: 'Εναλλαγή κάμερας'.

Verified locally after the merge: npm run typecheck (tsc — the exact failing gate) and npm run typecheck-tsgo both pass with exit 0.

The remaining red checks are only PR Reviewer Checklist / checklist and Check independent approval, which are human/review steps, not code failures.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

There's nothing to fix in code here. The Author/Reviewer checklist checks now pass, and the remaining in-progress checks (typecheck, perf tests) aren't failing.

The only failing check is Verify peer review / Check independent approval. This isn't a code failure — it's a process gate that stays red until the PR gets an independent approving review from an eligible reviewer. It clears itself once a reviewer approves; there's no commit I can push to satisfy it.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The branch is already up to date with the latest main — no merge was needed.

main (tip a1f71ac4) was already merged into this branch in commit f702d473af4 ("Merge main into claude-inAppCameraAttachments-relandV2"), with a follow-up typecheck fix in 71c2d26928d. The branch is currently 0 commits behind origin/main, so there's nothing further to merge or push.

@github-actions

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@ShridharGoel

Copy link
Copy Markdown
Contributor

@MelvinBot Add testing steps in the description

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Added testing steps to the PR description — filled in the Tests, Offline tests, and QA Steps sections.

Since this is an Android/iOS Native-only change (VisionCamera is native — there's no in-app camera on web/desktop), I based the steps on the actual flow in the diff:

  • Open a chat → +Take photo → verify the in-app camera modal opens (Expensify stays in the foreground; the system camera app must not launch).
  • Grant permission → capture with the shutter → verify the photo reaches the attachment preview and can be sent.
  • Verify the flip camera, flash, and close (X) controls.
  • Regression checks for the deploy blockers this re-land targets:
    • Expensify/App#90042 — deny permission, re-open the camera, and confirm the app shows the permission screen and does not hang on an infinite load.
    • Expensify/App#90047 — open the camera repeatedly in quick succession and confirm it opens reliably every time (the iOS modal-dismiss race).
    • Expensify/App#84018 — background the app while the camera is open and return; confirm no crash.

Please review and adjust the wording as needed before checking the boxes.

@ShridharGoel

Copy link
Copy Markdown
Contributor

@MelvinBot Let's stop passing passing path to takePhoto, so that this uses a temporary path. What do you think?

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Agree — we should drop path here.

Per VisionCamera's TakePhotoOptions docs, when path is omitted "VisionCamera will use the device's temporary directory." That's exactly what we want for a chat capture that's handed straight to the attachment flow — the temp dir is OS-managed and auto-cleaned.

Three reasons this is the right call:

  1. Semantics / storage location. getReceiptsUploadFolderPath() points at persistent app storage — DownloadDir on Android, DocumentDir on iOS — under the receipts subfolder. Writing chat photos there is semantically wrong and leaves clutter in persistent storage (on Android, potentially user-visible in Downloads).

  2. It removes a latent failure. takePhoto throws if the given directory doesn't exist. The receipts flow only gets away with passing that path because ReceiptStorage.adopt mkdirs the folder. This camera flow never creates it and doesn't go through adopt — so for a user who's never scanned a receipt, the first capture at AttachmentCamera.tsx:158 could throw and get swallowed by the .catch (the shutter would silently do nothing). Using the default temp dir sidesteps that since it always exists.

  3. Nothing downstream needs the receipts folder — onCapture just reads photo.path/derives the filename, which works with any path.

Concretely: remove const path = getReceiptsUploadFolderPath(); at AttachmentCamera.tsx:155, drop the path key from the takePhoto({...}) call, and remove the now-unused import at AttachmentCamera.tsx:19.

Want me to push that change? Reply @MelvinBot make that change and I'll commit it (note: since this is my own PR I can push once you confirm you're good with it).

@ShridharGoel

Copy link
Copy Markdown
Contributor

@MelvinBot make that change

… folder

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

Copy link
Copy Markdown
Contributor

🚧 robertjchen has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@ShridharGoel

Copy link
Copy Markdown
Contributor

@huult Tested

Screen.Recording.2026-09-16.at.1.10.26.AM.mov

@mountiny

Copy link
Copy Markdown
Contributor

@ShridharGoel conflicts

@ShridharGoel

Copy link
Copy Markdown
Contributor

@MelvinBot Fix conflicts

@huult

huult commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot Test on native Android

@ShridharGoel Do you still need this?

Resolves a conflict in config/eslint/eslint.seatbelt.tsv: main added the
useMoneyReportHeaderStatusBar entry while this branch removed the
useNativeCamera react-hooks/refs entry (the refactor here eliminated that
violation). Kept both changes.
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Conflicts are resolved and pushed — the PR is MERGEABLE again as of fd7c3b9.

There was one conflicted file: config/eslint/eslint.seatbelt.tsv:497. Both sides edited adjacent lines in the same sorted block:

  • main added a useMoneyReportHeaderStatusBar.ts baseline entry.
  • This branch removed the useNativeCamera.ts / react-hooks/refs entry, because the refactor here replaced the manual askForPermissions callback and ref usage in src/hooks/useNativeCamera.ts, which eliminated that violation.

I kept both changes: main's new entry stays, and the now-stale useNativeCamera.ts entry stays removed. main never touched useNativeCamera.ts, so the merged file is exactly this branch's version and the removal is still correct.

Verification

Ran against the merged tree:

Check Result
npm run typecheck ✅ pass (all 5 projects)
npm run lint-changed ✅ pass — no violations, and lint did not rewrite the seatbelt TSV, confirming the resolved baseline matches reality
npm run fmt ✅ no changes
tests/ui/components/AttachmentCameraTest.tsx ✅ 9/9 pass
tests/ui/IOURequestStepScanTest.tsx ✅ 2/2 pass
tests/ui/ScanSkipConfirmationTest.tsx ✅ 1/1 pass
flipCamera key in all 11 src/languages/*.ts ✅ present in each (files auto-merged cleanly)

On the React Compiler check: running it in strict check mode flags pre-existing "Cannot access refs during render" errors in src/components/AttachmentPicker/index.native.tsx. This is not a regression from the merge — the merge left that file byte-identical to the pre-merge tip, and React Compiler Compliance passed on that tip in CI, which runs the baseline-aware check-changed rule. I could not run check-changed locally because this environment sets GITHUB_BASE_REF to an empty string, which defeats its ?? 'main' fallback.

The merge brought in a react-native-pager-view 8.0.0 → 9.0.4 bump. I could not run npm install in this environment, so the checks above ran against the pre-bump node_modules. That package is unrelated to the files this PR touches, and CI will install the merged lockfile.

Note that mergeStateStatus is still BLOCKED, but that is the Check independent approval gate waiting on a review — not a conflict. No other checks were failing.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

That Android run already happened — results are in my earlier comment from 09-15, against a2eb5fd.

Whether it's still needed is ShridharGoel's call, but the relevant fact is that nothing in the camera path has changed since that run. The only commit added since is the main merge (fd7c3b9), and it touched no camera, AttachmentPicker, or VisionCamera file. The one modal change it pulled in is src/components/Modal/BaseModal.tsx:306, which adds an opt-in shouldShowBackdrop prop defaulting to false — it only affects backdrop opacity and nothing in the mount/unmount path.

So a re-run would exercise the same code as both that run and ShridharGoel's adhoc-build video, which was also built from a2eb5fd.

If the open question is specifically whether the step-12 freeze reproduces, a fresh emulator run wouldn't add much — that run already flagged emulator-specific caveats (single camera device, synthetic feed). A physical device would be the more informative check.

How I verified the merge left the camera path alone

fd7c3b9 is a merge of a2eb5fd (previous branch tip) and 89728eb (main). Diffing the two:

  • 551 files changed, none matching camera, attachmentpicker, or vision.
  • Modal-adjacent files changed: Modal/BaseModal.tsx and Modal/types.ts only — a 9-line addition introducing the shouldShowBackdrop prop. Because it defaults to false, !shouldShowBackdrop && (...) reduces to the previous (...), so existing behavior is byte-for-byte equivalent.
  • src/components/AttachmentPicker/, src/hooks/useNativeCamera.ts, src/hooks/useIsPlatformMuted.ts, src/libs/getVideoResolutionFormatFilter/, and __mocks__/react-native-vision-camera.ts are all untouched by the merge.

Note the GitHub compare API caps its file list at 300, so I diffed the two parent commits locally against a full clone to get all 551.


view run

@huult huult left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@robertjchen robertjchen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, let's ship it! :shipit:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not gonna block on this here, but can you make sure to follow the Given, When, Then comments in the test files?

@mountiny
mountiny merged commit 04fb105 into main Sep 17, 2026
46 of 48 checks passed
@mountiny
mountiny deleted the claude-inAppCameraAttachments-relandV2 branch September 17, 2026 20:24
@github-actions

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.82-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 cancelled 🔪
🍎 iOS 🍎 cancelled 🔪

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Yes — help site changes are required. Draft PR: Docs updates for E/A#96458 in-app camera for chat attachment photos (labeled HelpDot, assigned to ShridharGoel).

Why: this PR gives chat attachments a brand-new in-app camera screen — permission prompt with Continue, plus flash, shutter, flip camera, and close controls. The help site never documented any of it. Chat-Features-and-Moderation-Tools.md described the whole mobile flow as "Use the message bar to type a message, add an attachment, or insert an emoji" — it never even named Take photo.

What the docs PR changes (one file):

  • Names the mobile picker options in UI order: Take photo, Choose from gallery, Choose file.
  • Adds ## How to take a photo to attach to a chat message covering the permission prompt and each camera control, including that flash and flip camera are hidden when the device lacks a flash or second camera.
  • States the platform split: in-app camera is native only; on web Add attachment opens the file picker.
  • Fixes the composer placeholder to Write something....
How labels were verified
  • Live web UI: composer placeholder is Write something...; the + menu shows Create expense, Track distance, Split expense, Pay Expensify, Assign task, Add attachment — no Take photo, confirming the in-app camera is native only.
  • Native picker order from index.native.tsx:291-312: unshift puts Take photo first, then Choose from gallery, then Choose file.
  • Control positions from AttachmentCamera.tsx:216-330: close is top-right (justifyContentEnd), then flash / shutter / flip camera left-to-right.
  • The new flipCamera string is an accessibility label, not visible text, so it's documented as an icon.

Reviewed, no change needed: Attach-and-edit-receipts-on-expenses.md, Create-an-Expense.md, Track-Expenses.md, Track-Personal-Expenses.md — those cover the receipt-scanning camera, which already ran in-app and isn't changed here.

@ShridharGoel, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #101518 was identified to be related to this PR.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.86-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

MelvinBot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Yes, a help site update was required — but it already exists, so I did not open a duplicate.

This PR is user-visible on mobile: Take photo in a chat now opens Expensify's in-app camera (flash, shutter, flip camera, close) instead of handing off to the device camera app, and Expensify itself asks for camera access. The only article covering the chat composer, docs/articles/new-expensify/chat/Chat-Features-and-Moderation-Tools.md, described the mobile attachment flow only as "add an attachment" and said nothing about the camera.

That exact gap is already closed by [No QA] Docs updates for E/A#96458 in-app camera for chat attachment photos (Expensify/App#101478) — open, labeled HelpDot, assigned to ShridharGoel, and already approved by you. A second PR would duplicate it.

What I verified against the code and the live UI

Checked every label in #101478 against main and a running web build, per HELPSITE_NAMING_CONVENTIONS.md:

  • Add attachment, Send, and placeholder Write something... — confirmed in the live web UI (actions menu reads exactly Create expense, Track distance, Split expense, Pay Expensify, Assign task, Add attachment).
  • Picker order Take photoChoose from galleryChoose filesrc/components/AttachmentPicker/index.native.tsx:291-313.
  • Close icon in the top-right, and flash left / shutter center / flip camera right — src/components/AttachmentPicker/AttachmentCamera.tsx:228-320.
  • Flash and flip camera hidden when the device lacks a flash or a second camera — hasFlash / canFlipCamera in the same file.

All of it matches what #101478 already says.

One optional accuracy nit for #101478: its step 7 reads "Review the photo in the preview, then send it to the chat." The preview has a real button (common.sendSend), so the naming conventions would have it read "Review the photo in the preview, then tap Send."

@ShridharGoel, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review (it is already marked Ready for review and approved, so there may be nothing left to do here).


view run · no recording available

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.89-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Re-checked: a help site change is required, it already exists, and it is still accurate — so I did not open a duplicate PR.

[No QA] Docs updates for E/A#96458 in-app camera for chat attachment photos is open, labeled HelpDot, assigned to ShridharGoel, and approved. It documents the mobile flow this PR introduced — plus iconAdd attachmentTake photo opening Expensify's in-app camera (flash, shutter, flip camera, close) instead of the device camera app — in docs/articles/new-expensify/chat/Chat-Features-and-Moderation-Tools.md. No other article under docs/articles describes the chat camera flow, so nothing else needs updating.

What changed since my last check

Nothing that affects the docs:

One optional nit for #101478 (unchanged from before): its step 7 reads "Review the photo in the preview, then send it to the chat." The preview has a real button (common.sendSend), so per HELPSITE_NAMING_CONVENTIONS.md it should read "Review the photo in the preview, then tap Send."

@ShridharGoel, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review — note it is already marked Ready for review and approved, so there is likely nothing left to do.


view run

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/luacmartins in version: 9.4.89-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 failure ❌

Bundle Size Analysis (Sentry):

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.

9 participants