Skip to content

[Due for payment 2026-11-02] [$500] Upgrade react-native-vision-camera from V4 to V5 #102514

Closed

Description

@mountiny

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 gesture
  • src/pages/iou/request/step/IOURequestStepScan/captureReceipt/index.ts — the takePhoto versus takeSnapshot split, the riskiest piece
  • src/components/AttachmentPicker/AttachmentCamera.tsx — the only useCameraDevices() and CameraRuntimeError consumer, and the only takePhoto call without a path
  • src/pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/Camera.tsx — carries the iOS video={true} workaround that exists only to keep takeSnapshot working
  • src/pages/iou/request/step/IOURequestStepScan/components/CameraViewport.tsx

Type-only or format-only changes:

  • src/pages/iou/request/step/IOURequestStepScan/components/Camera/index.native.tsx
  • src/pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/types.ts
  • src/pages/iou/request/step/IOURequestStepScan/captureReceipt/types.ts
  • src/pages/iou/request/step/IOURequestStepScan/getCameraAspectRatio.ts
  • src/pages/iou/request/step/IOURequestStepScan/hooks/useCameraInitTelemetry.ts
  • src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx
  • src/libs/getVideoResolutionFormatFilter/types.ts

Also touched:

  • Delete patches/react-native-vision-camera/
  • __mocks__/react-native-vision-camera.ts, plus the inline jest.mock calls in 7 test files under tests/ui/
  • android/gradle.properties — check whether disableFrameProcessors still exists in V5
  • ios/Podfile.lock and Mobile-Expensify/iOS/Podfile.lock — both pin VisionCamera (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.ts and contributingGuides/HYBRID_APP.md

Risks

  1. V5 may not fix the crash. Upstream #4026, the detachFromFigCaptureSession abort, was closed on 2026-08-05 by PR #4134, but two people reported it still firing on 5.2.2 on 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 — NSGenericException on an unbalanced configuration — and the V5 code that throws it no longer exists, so this is a caveat rather than a blocker.
  2. getVideoResolutionFormatFilter disappears. Its only job is building FormatFilter values. Constraints replace it, so the index.ios.ts / index.android.ts split probably goes away.
  3. Two pod installs. App and the Mobile-Expensify submodule both pin VisionCamera (4.7.2).
  4. 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-image and get App Deployer approval
  • Migrate the 4 useCameraFormat call sites to the Constraints API
  • Migrate takePhoto and takeSnapshot to capturePhotoToFile and 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

Issue OwnerCurrent Issue Owner: @mallenexpensify
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. added
    InternalRequires API changes or must be handled by Expensify staff
    on Sep 29, 2026
  2. changed the title [-]Upgrade react-native-vision-camera from V4 to V5[/-] [+][$500] Upgrade react-native-vision-camera from V4 to V5[/+] on Sep 29, 2026
  3. melvin-bot commented on Sep 29, 2026

    @melvin-bot

    ⚠️ Could not update price automatically because there is no linked Upwork Job ID. The BZ team member will need to update the price manually in Upwork.

  4. sharabai commented on Sep 29, 2026

    @sharabai
    Contributor

    I'll start working on the upgrade!

  5. sharabai commented on Sep 29, 2026

    @sharabai
    Contributor

    @mountiny heads up, V5 needs react-native-nitro-image as a peer dependency, so I opened a library request for it here: #102564

  6. mrousavy commented on Oct 1, 2026

    @mrousavy
    Member

    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)

  7. 33 remaining items

  8. situchan commented on Nov 2, 2026

    @situchan
    Contributor

    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

    1. Open Create expense > Scan.
    2. Verify the camera opens at the same framing as the native Camera app at 1x, not the 0.5x ultra-wide.
    3. Verify the viewfinder is 4:3 and does not jump after the camera starts.
    4. Hold the phone in portrait, flash off, and tap the shutter.
    5. Verify the confirmation page opens with the receipt upright and sharp, with no "Camera error" alert.
    6. iOS: verify no shutter sound plays.
    7. Android: wait a few seconds on the confirmation page and verify the receipt preview refreshes to the full-resolution photo and stays upright.
    8. Go back to Scan with the receipt close to the camera. Verify the viewfinder doesn't show a frozen or blurry old frame and the live camera is sharp right away.
    9. Turn the flash on and capture again. Verify the flash fires, the photo is saved and, unless the phone is muted, the shutter sound plays.
    10. Capture with the phone held in landscape (Android). Verify the photo is saved.
    11. Tap the viewfinder to focus, including two quick taps. Verify the focus indicator shows and no error appears.
    12. Turn on multi-scan, take three captures in a row and submit. Verify all three receipts are attached.
    13. Submit an expense from a scanned receipt and verify the receipt shows on the expense.

    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 👎

  9. removed
    Skip PaymentSkips all the payment process steps for an issue. Use it for fixed-price projects.
    on Nov 2, 2026
  10. situchan commented on Nov 2, 2026

    @situchan
    Contributor

    2 PRs linked to this issue:

    Total: $750


    ⚠️ Looks like this issue was linked to a Deploy Blocker here

    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):

    Two testers were able to reproduce the issue on the following devices:
    Samsung Galaxy S25 FE, Android 16
    Samsung Galaxy S23 FE, Android 16
    Two other testers couldn't reproduce the issue on their devices:
    Samsung Galaxy A50, Android 11
    Samsung Galaxy A54, Android 16

    There was no other regressions.
    So I suggest not to apply regression penalty.

    @AndrewGable please confirm the amount. Thanks

  11. AndrewGable commented on Nov 2, 2026

    @AndrewGable
    Contributor

    $750 works for me 👍

  12. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    on Nov 2, 2026
  13. changed the title [-][$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[/+] on Nov 2, 2026
  14. melvin-bot commented on Nov 2, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  15. melvin-bot commented on Nov 2, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  16. mallenexpensify commented on Nov 5, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @situchan due $750 via NewDot

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionInternalRequires API changes or must be handled by Expensify staffTaskWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions