Repository navigation
Description
Problem
VisionCamera V4 keeps producing iOS crashes and hangs in HybridApp, and our local patches have not stopped them.
| Sentry | GH issue | Signature | Events / users | Last seen |
|---|---|---|---|---|
| APP-7FJ | HybridApp iOS Fatal App Hang at main | Fatal App Hang at main; one bucket is the AVCaptureSession lock |
2448 / 447 | 2026-09-27, 9.4.91-3 |
| APP-JB3 | AVCaptureSession stopRunning crash still active | NSGenericException from CameraSession.checkIsActive |
91 / 91 | 2026-09-27, 9.4.90-2 |
We already carry a local patch for both signatures — patches/react-native-vision-camera/react-native-vision-camera+4.7.2+001+fix-ios-camera-teardown-hang-and-config-leak.patch, added in Fix: patch react-native-vision-camera to avoid main thread hang and Fix unbalanced capture session configuration. APP-JB3 survived it.
The patch's own details.md already states it will be dropped when we move to V5. V4 is no longer supported upstream, so no further fix will land there: upstream closed PR #3664 as obsolete after the V5 rewrite.
@chrispader recommended the upgrade in this comment and this one. V5 is a Nitro Modules rewrite that deletes roughly 3,000 lines of hand-written JSI/C++, which removes a whole class of threading bugs.
Proposal
Upgrade react-native-vision-camera from ^4.7.2 to 5.2.3 and delete the local patch.
What we already have in place
| Requirement | App today | Status |
|---|---|---|
react-native-nitro-modules |
0.37.1 |
V5 5.2.3 is built against 0.37.0. Compatible. |
react-native-worklets (Software Mansion) |
0.11.1 |
V5 switched its default worklets runtime to this. Already correct. |
react-native |
0.86.0 |
V5 5.2.3 is built against 0.85.3. |
react-native-reanimated |
4.5.5 |
Optional peer. |
react-native-worklets-core |
not installed | V5 drops it. Nothing to do. |
react-native-nitro-image |
missing | New required peer. 0.15.2, also on nitro 0.37.0. |
Breaking changes that hit us
Confirmed against the published react-native-vision-camera@5.2.3 TypeScript sources.
| V4 | V5 | Where it bites |
|---|---|---|
useCameraFormat plus the format prop |
Removed. New Constraints API: constraints={[...]} |
4 call sites. The biggest single change. |
photo / video boolean props |
outputs={[photoOutput, videoOutput]} from usePhotoOutput() / useVideoOutput() |
NavigationAwareCamera/Camera.tsx, CameraViewport.tsx, AttachmentCamera.tsx |
camera.takePhoto({path}) |
photoOutput.capturePhoto() returns an in-memory Photo. capturePhotoToFile({filePath}) also exists and keeps the path-based flow. |
captureReceipt/index.ts, AttachmentCamera.tsx, IOURequestStepOdometerImage/index.native.tsx |
camera.takeSnapshot({quality, path}) |
takeSnapshot(): Promise<Image> on the preview ref. No quality, no path, returns an in-memory image. |
captureReceipt/index.ts — needs a write to disk |
camera.focus(point) |
camera.focusTo(viewPoint, options?) |
src/hooks/useNativeCamera.ts |
CameraRuntimeError |
Gone. onError?: (error: Error) only, with no .code. |
AttachmentCamera.tsx, src/libs/telemetry/ReceiptObservability.ts |
photoQualityBalance="quality" |
qualityPrioritization on the photo output config |
NavigationAwareCamera/Camera.tsx, AttachmentCamera.tsx |
androidPreviewViewType="texture-view" |
Gone. Closest equivalent is implementationMode on the preview. |
CameraViewport.tsx |
device.neutralZoom |
Gone. Closest equivalent is device.zoomLensSwitchFactors. |
AttachmentCamera.tsx, CameraViewport.tsx |
CodeScanner moved to its own package |
Not used. No impact. | — |
| Frame processor plugins are now Nitro Modules | Not used, and disableFrameProcessors=true on Android. No impact. |
— |
Migration surface
Eleven files import the package. There are no deep imports anywhere.
Core changes:
src/hooks/useNativeCamera.ts—useCameraDevice,hasFlash,supportsFocus,focus(), the tap to focus gesturesrc/pages/iou/request/step/IOURequestStepScan/captureReceipt/index.ts— thetakePhotoversustakeSnapshotsplit, the riskiest piecesrc/components/AttachmentPicker/AttachmentCamera.tsx— the onlyuseCameraDevices()andCameraRuntimeErrorconsumer, and the onlytakePhotocall without apathsrc/pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/Camera.tsx— carries the iOSvideo={true}workaround that exists only to keeptakeSnapshotworkingsrc/pages/iou/request/step/IOURequestStepScan/components/CameraViewport.tsx
Type-only or format-only changes:
src/pages/iou/request/step/IOURequestStepScan/components/Camera/index.native.tsxsrc/pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/types.tssrc/pages/iou/request/step/IOURequestStepScan/captureReceipt/types.tssrc/pages/iou/request/step/IOURequestStepScan/getCameraAspectRatio.tssrc/pages/iou/request/step/IOURequestStepScan/hooks/useCameraInitTelemetry.tssrc/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsxsrc/libs/getVideoResolutionFormatFilter/types.ts
Also touched:
- Delete
patches/react-native-vision-camera/ __mocks__/react-native-vision-camera.ts, plus the inlinejest.mockcalls in 7 test files undertests/ui/android/gradle.properties— check whetherdisableFrameProcessorsstill exists in V5ios/Podfile.lockandMobile-Expensify/iOS/Podfile.lock— both pinVisionCamera (4.7.2)at the same checksum, so pod install runs in both- Stale comments in
src/libs/ReceiptStorage/index.native.ts,src/libs/telemetry/ReceiptObservability.tsandcontributingGuides/HYBRID_APP.md
Risks
- V5 may not fix the crash. Upstream #4026, the
detachFromFigCaptureSessionabort, was closed on 2026-08-05 by PR #4134, but two people reported it still firing on5.2.2on 2026-08-12 and 2026-08-14. #4156, the same abort with two sessions sharing an output, has been open since 2026-08-17. Our own signature is different —NSGenericExceptionon an unbalanced configuration — and the V5 code that throws it no longer exists, so this is a caveat rather than a blocker. getVideoResolutionFormatFilterdisappears. Its only job is buildingFormatFiltervalues. Constraints replace it, so theindex.ios.ts/index.android.tssplit probably goes away.- Two pod installs. App and the
Mobile-Expensifysubmodule both pinVisionCamera (4.7.2). - We use no frame processors. The Nitro rewrite's riskiest surface does not apply to us, which lowers the overall risk of this upgrade.
Checklist
- File a New Library Request for
react-native-nitro-imageand get App Deployer approval - Migrate the 4
useCameraFormatcall sites to the Constraints API - Migrate
takePhotoandtakeSnapshottocapturePhotoToFileand the new preview snapshot - Drop
patches/react-native-vision-camera/ - Update the jest mock and the 7 inline test mocks
- Verify receipt scan, attachment camera and odometer capture on iOS and Android, both standalone and HybridApp
- Watch APP-7FJ and APP-JB3 for two weeks after the release reaches production
Related
- HybridApp iOS Fatal App Hang at main
- AVCaptureSession stopRunning crash still active
- VisionCamera V5 release notes
- What's new in VisionCamera V5
Issue Owner
Current Issue Owner: @mallenexpensifyIssue Owner
Current Issue Owner: @mallenexpensifyMetadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
[-]Upgrade react-native-vision-camera from V4 to V5[/-][+][$500] Upgrade react-native-vision-camera from V4 to V5[/+]melvin-bot commentedon Sep 29, 2026
sharabai commentedon Sep 29, 2026
I'll start working on the upgrade!
sharabai commentedon Sep 29, 2026
@mountiny heads up, V5 needs
react-native-nitro-imageas a peer dependency, so I opened a library request for it here: #102564mrousavy commentedon Oct 1, 2026
Nice stuff! Ping me if you have any questions. Also, I'm sure @chrispader can help with this as he's from Margelo and works closely on Nitro stuff with us.
Re
takeSnapshot()being dropped: V5 dropped snapshots only on iOS - on Android those are still supported.The reason for that is that snapshots were never a thing on iOS; I faked them by always storing one full sized frame of the Video Output in memory. So if you enabled snapshots, VisionCamera V4 silently attached a full Video Output to the stream that was just used for eventually capturing a snapshot.
It is significantly better resource-wise to just capture a photo and not attach a Video Output on iOS. So V5 should reduce memory usage and give better perf on iOS if you just use normal capturePhoto then - on iOS this is super fast anyways (e.g. qualityPrioritization can get you close to the fake snapshot speed from before).
On Android, you can still use takeSnapshot just like before. There, it makes a difference since capturing photos can be quite slow on Android Cameras in general.
If you really insist on using the snapshot approach you can just add a Frame Output and just always store the latest Frame in memory, then on your shutter press just convert the latest stored Frame to a Image. That's it. But again; adding a Video Output might be more overhead over time than just a few more milliseconds for a real photo (on iOS)
33 remaining items
situchan commentedon Nov 2, 2026
Contributor+ Checklist:
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: upstream refactor
Link to the comment on the Issue:
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion:
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
Regression Test Proposal
Test:
Receipt scan
Attachment camera
14. In any chat, tap + > Take photo.
15. Verify the back camera opens at 1x. Capture and verify the attachment preview is upright.
16. Flip to the front camera and capture again.
17. Close the camera while a capture is in progress and verify nothing breaks.
Odometer
18. Create a distance expense with the odometer option and take the start photo.
19. Verify the cropped photo is upright and saved.
20. Go back to the odometer camera and verify the viewfinder is sharp, with no frozen old frame.
Camera teardown (the dropped iOS patch)
21. Open Scan and leave the screen right away. Repeat a few times.
22. Tap the shutter and leave the screen during the capture. Repeat a few times.
23. Tap the shutter and leave the confirmation page immediately. Repeat a few times.
24. Verify the app doesn't hang or crash, and the camera works when you come back.
Other
25. Background the app while on Scan and return. Verify the camera resumes.
26. Deny camera permission and open Scan. Verify the permission prompt shows.
Do we agree 👍 or 👎
situchan commentedon Nov 2, 2026
2 PRs linked to this issue:
Total: $750
This was true but I was not able to reproduce the bug so couldn't catch it earlier.
This happened only on some specific android devices. QA team also reproduced in some devices, not in another devices - #104941 (comment):
There was no other regressions.
So I suggest not to apply regression penalty.
@AndrewGable please confirm the amount. Thanks
AndrewGable commentedon Nov 2, 2026
$750 works for me 👍
[-][$500] Upgrade react-native-vision-camera from V4 to V5[/-][+][Due for payment 2026-11-02] [$500] Upgrade react-native-vision-camera from V4 to V5[/+]melvin-bot commentedon Nov 2, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Nov 2, 2026
Payment Summary
Resolving PRs:
fix: Skip the camera error alert when the capture is cancelled by leaving the screen #103892
Upgrade react-native-vision-camera from v4 to v5 #102898
Contributor: @OlimpiaZurek does not require payment (Contractor)
Contributor: @sharabai does not require payment (Contractor)
Reviewer: @situchan owed $500 via NewDot
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Nov 5, 2026
Payment Summary
Contributor+: @situchan due $750 via NewDot