Skip to content

Fix: patch react-native-vision-camera to avoid AVCaptureSession configuration crash - #97499

Merged
mountiny merged 6 commits into
Expensify:mainfrom
software-mansion-labs:fix/react-native-vision-camera/patch/prev-patch-fix
Aug 13, 2026
Merged

Fix: patch react-native-vision-camera to avoid AVCaptureSession configuration crash#97499
mountiny merged 6 commits into
Expensify:mainfrom
software-mansion-labs:fix/react-native-vision-camera/patch/prev-patch-fix

Conversation

@sharabai

@sharabai sharabai commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

CameraSession.configure calls beginConfiguration() inside one if difference.isSessionConfigurationDirty block and commitConfiguration() inside a second block further down, so that input, output and device changes batch into a single hardware update. Any throw between the two — configureDevice, configureOutputs, the noDevice guard, or any of the device-configuration steps that follow it — skips the commit and leaves the session's configuration counter permanently held, after which every startRunning() or stopRunning() on that session throws NSGenericException for the rest of its lifetime.

The leak is an upstream defect that predates our patch, but #95984 made it reachable: the deactivateCameraSession() call it added issues a configure during teardown that reaches checkIsActive, which turns a previously latent leak into a crash on the way out of the camera screen. This extends the existing patch to track whether the configuration was opened and commit it on the error path, leaving the success-path batching untouched. Committing a partially-applied configuration is the better of the two outcomes — it self-corrects on the next configure, whereas today the session stays wedged permanently. Closing the upstream hole is preferred to reverting #95984, which would reintroduce the teardown hangs it fixed. The change is scoped to the video session: the audio block in the same function has the identical defect but is unreachable here, because audio is never enabled in this app and every throw site in configureAudioSession sits behind if enableAudio.

The patch file keeps its 001 number and has been renamed from fix-main-thread-hang-on-ios to fix-ios-camera-teardown-hang-and-config-leak, since it now carries both fixes.

Fixed Issues

$ #97103

Tests

Prerequisite: this must be tested with a HybridApp build on a physical iOS device, because the Simulator has no real camera.

This crash cannot be triggered by ordinary camera use. It requires an earlier configure to have failed while the session was running, which is why Sentry shows only four events. Tests 1–2 below are regression coverage for the flows this patch touches; test 3 is the only one that actually exercises the fix, and it needs a temporary code injection.

From the App repository root, initialize the Mobile-Expensify submodule and install the HybridApp dependencies and Pods:

git submodule update --init
npm install
npm run pod-install

Start Metro from the App repository root in a separate terminal:

npm run start

Open Mobile-Expensify/iOS/Expensify.xcworkspace in Xcode. Do not open Expensify.xcodeproj directly because that does not include the Pods project. Select the Expensify Dev scheme, its default Debug configuration, and the connected physical device, then build and run with Cmd+R. Follow HOW_TO_BUILD_APP_ON_PHYSICAL_IOS_DEVICE.md if development code signing needs to be configured.

  1. Camera teardown — no hang, no crash

    1. In the LHN, tap the green "+" button.
    2. Tap "Create expense". The Scan tab should open with the camera active.
    3. Immediately tap the back button or swipe back.
    4. Verify that the app returns to the previous screen without freezing and without crashing.
    5. Repeat the open-camera → immediately-go-back flow several times.
    6. Verify that the app remains responsive every time.
  2. Take a receipt photo

    1. Tap "+" → "Create expense".
    2. Wait for the camera preview to appear on the Scan tab.
    3. Take a photo of a receipt or document.
    4. Verify that the photo is captured and the receipt preview appears.
    5. Confirm or discard the expense.
    6. Verify that leaving the camera neither hangs nor crashes the app.
  3. The fix itself — reproduce the crash, then confirm it is gone

Expand for the injection steps

The crash needs a configure to throw while the capture session is already running. Throwing on the mount pass instead leaves the session stopped, and checkIsActive then early-returns at teardown, so nothing crashes and the test proves nothing.

a. In node_modules/react-native-vision-camera/ios/Core/CameraSession.swift, add to the CameraSession class body:

  // TEMPORARY — remove after testing
  private static var hasInjectedFailure = false

b. In the same file, immediately before the if difference.isSessionConfigurationDirty { ... commitConfiguration() ... } block:

        // TEMPORARY — remove after testing
        if difference.isSessionConfigurationDirty, self.captureSession.isRunning, !CameraSession.hasInjectedFailure {
          CameraSession.hasInjectedFailure = true
          throw CameraError.device(.noDevice)
        }

c. Nothing on the Scan screen naturally produces a session-dirty pass mid-session, so force one from JS. In src/pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/Camera.tsx, flip the video prop five seconds after mount:

    // TEMPORARY — remove after testing
    const [forceDirtyPass, setForceDirtyPass] = React.useState(false);
    React.useEffect(() => {
        const timer = setTimeout(() => setForceDirtyPass(true), 5000);
        return () => clearTimeout(timer);
    }, []);

and pass video={forceDirtyPass ? !IS_VIDEO_REQUIRED_FOR_SNAPSHOT : IS_VIDEO_REQUIRED_FOR_SNAPSHOT}.

d. Confirm the crash on main. Check out main, keep the injections, rebuild, then: open "+" → "Create expense", confirm the camera preview is live (a black preview means the mount pass failed and the run is invalid), wait ~6 seconds, then navigate to the confirmation step. Verify the app crashes with NSGenericException: *** -[AVCaptureSession stopRunning] stopRunning may not be called between calls to beginConfiguration and commitConfiguration.

e. Confirm the fix. Check out this branch, keep the injections, rebuild, repeat the same sequence. Verify that no crash occurs, teardown completes, and the injected noDevice error is reported through onConfigureError in the Xcode console.

f. Remove both injections.

  • Verify that no errors appear in the JS console

Offline tests

N/A — this patch only changes the native iOS camera-session lifecycle and does not involve network requests or persisted data.

QA Steps

Same as tests 1–2, on a physical iOS device. Test 3 requires editing native source and is developer-only.

  • 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

N/A — this patch only changes native iOS camera code.

Android: mWeb Chrome

N/A — this patch only changes native iOS camera code.

iOS: Native
PR.test.mp4
iOS: mWeb Safari

N/A — this patch only changes native iOS camera code.

MacOS: Chrome / Safari

N/A — this patch only changes native iOS camera code.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR is possibly changing native code and/or updating libraries, it may cause problems with HybridApp. Please check if any patch updates are required in the HybridApp repo and run an AdHoc build to verify that HybridApp will not break. Ask Contributor Plus for help if you are not sure how to handle this. ⚠️

@sharabai sharabai changed the title patch react-native-vision-camera to avoid AVCaptureSession configuration crash Fix: patch react-native-vision-camera to avoid AVCaptureSession configuration crash Jul 30, 2026
@sharabai

Copy link
Copy Markdown
Contributor Author

I already mentioned this in the issue, but just to make sure everyone sees it: I'll be OOO starting tomorrow, and @GCyganek will take over.

@sharabai
sharabai marked this pull request as ready for review July 30, 2026 18:34
@sharabai
sharabai requested a review from a team as a code owner July 30, 2026 18:34
@melvin-bot
melvin-bot Bot requested review from ahmedGaber93 and removed request for a team July 30, 2026 18:34
@melvin-bot

melvin-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

@ahmedGaber93 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@ahmedGaber93

Copy link
Copy Markdown
Contributor

This is a follow-up PR, @ZhenjaHorbach will review it.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

bump @ZhenjaHorbach for reviewing ^.

@ZhenjaHorbach

ZhenjaHorbach commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Oh
Sorry
Don't have this issue in K2 😅

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@mountiny
Can you please assign me and run a build here?

@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.

@github-actions

Copy link
Copy Markdown
Contributor

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

@mountiny
mountiny requested review from ZhenjaHorbach and removed request for ahmedGaber93 August 11, 2026 13:02
@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@GCyganek
Can you update the branch to the latest version of main, please?

@github-actions

This comment has been minimized.

@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.

@GCyganek

Copy link
Copy Markdown
Contributor

Unlucky with the builds here I see, I merged main again, ios partched artifcats script is updated so let's hope now it will work

@ZhenjaHorbach

ZhenjaHorbach commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@mountiny
Can you please run an adhoc build?

@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.

@quinthar quinthar removed the #quality label Aug 13, 2026
@ZhenjaHorbach

ZhenjaHorbach commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The 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
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
2026-08-13.11.23.40.mp4
iOS: mWeb Safari
MacOS: Chrome / Safari

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

LGTM!

@melvin-bot
melvin-bot Bot requested a review from mountiny August 13, 2026 09:24

@mountiny mountiny 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 for the fix and testing

@mountiny mountiny closed this Aug 13, 2026
@mountiny mountiny reopened this Aug 13, 2026
@mountiny
mountiny merged commit 4ad11ab into Expensify:main Aug 13, 2026
33 of 35 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

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

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.

7 participants