You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
Camera teardown — no hang, no crash
In the LHN, tap the green "+" button.
Tap "Create expense". The Scan tab should open with the camera active.
Immediately tap the back button or swipe back.
Verify that the app returns to the previous screen without freezing and without crashing.
Repeat the open-camera → immediately-go-back flow several times.
Verify that the app remains responsive every time.
Take a receipt photo
Tap "+" → "Create expense".
Wait for the camera preview to appear on the Scan tab.
Take a photo of a receipt or document.
Verify that the photo is captured and the receipt preview appears.
Confirm or discard the expense.
Verify that leaving the camera neither hangs nor crashes the app.
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
privatestaticvarhasInjectedFailure=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 =truethrowCameraError.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 testingconst[forceDirtyPass,setForceDirtyPass]=React.useState(false);React.useEffect(()=>{consttimer=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 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 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: NativePR.test.mp4iOS: 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.
⚠️ 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. ⚠️
@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]
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 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation of Change
CameraSession.configurecallsbeginConfiguration()inside oneif difference.isSessionConfigurationDirtyblock andcommitConfiguration()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, thenoDeviceguard, or any of the device-configuration steps that follow it — skips the commit and leaves the session's configuration counter permanently held, after which everystartRunning()orstopRunning()on that session throwsNSGenericExceptionfor 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 aconfigureduring teardown that reachescheckIsActive, 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 nextconfigure, 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, becauseaudiois never enabled in this app and every throw site inconfigureAudioSessionsits behindif enableAudio.The patch file keeps its
001number and has been renamed fromfix-main-thread-hang-on-iostofix-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
configureto 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:
Start Metro from the App repository root in a separate terminal:
Open
Mobile-Expensify/iOS/Expensify.xcworkspacein Xcode. Do not openExpensify.xcodeprojdirectly 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.Camera teardown — no hang, no crash
Take a receipt photo
The fix itself — reproduce the crash, then confirm it is gone
Expand for the injection steps
The crash needs a
configureto throw while the capture session is already running. Throwing on the mount pass instead leaves the session stopped, andcheckIsActivethen 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 theCameraSessionclass body: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 thevideoprop five seconds after mount:and pass
video={forceDirtyPass ? !IS_VIDEO_REQUIRED_FOR_SNAPSHOT : IS_VIDEO_REQUIRED_FOR_SNAPSHOT}.d. Confirm the crash on
main. Check outmain, 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 withNSGenericException: *** -[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
noDeviceerror is reported throughonConfigureErrorin the Xcode console.f. Remove both injections.
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.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.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.