Repository navigation
Description
Problem
A customer reported that receipt capture in New Expensify focuses much worse than in Expensify Classic: "Camera takes many out of focus shots now. Have to scan and rescan sometimes more than 3 times… Old Expensify was much better with scanning." Apple checked the phone (iPhone 15) and found no hardware problem, and it's the same phone running both apps.
This is distinct from #99615, which is about receipt image resolution and compression. A bigger encode of a blurry frame is still blurry.
How autofocus works today
Expensify Classic (iOS) — behaviorally: continuous autofocus runs for the whole capture session, every shutter press goes through the full still-photo pipeline with stabilization, a single wide-angle camera is used so there is no lens switching mid-capture, and tap-to-focus does a one-shot focus.
New Expensify — four differences from that, each pinned to code:
-
The default shutter path captures a preview frame, not a still photo.
captureReceipt/index.ts:6-21callstakePhoto()only when flash is on or the device is in landscape; otherwise it falls through totakeSnapshot({quality: 85}). Portrait with flash off is the common case. A snapshot grabs whatever frame the preview pipeline currently holds — it skips the pre-capture focus convergence and stabilization thattakePhoto()performs. Having to rescan three or more times is exactly what that produces. -
No focus mode is ever configured.
useNativeCamera.ts:40-42sets up the device but never requests a focus mode, andfocusCameracallscamera.focus(point)on tap, which switches the device to one-shot AF. Nothing restores continuous AF afterward, so after a single tap the camera stays locked at that focus distance for the rest of the session. -
Format selection ignores the autofocus system.
Camera/index.native.tsx:93-99ranks formats by aspect ratio and resolution only. There is noautoFocusSystem: 'phase-detection'preference, so a contrast-detection format can win — contrast AF hunts noticeably more at close range, which is exactly how receipts are shot. -
Multi-lens virtual device used at
neutralZoom. The device requests both wide-angle and ultra-wide physical cameras (useNativeCamera.ts:40-42) and the preview renders atdevice.neutralZoom(CameraViewport.tsx:117). Close-range receipt framing can sit right at the lens-switch boundary, and each switch restarts autofocus.
Proposed scope
- Measure snapshot vs. photo sharpness handheld at close range, so the tradeoff is decided on data rather than assumption.
- Decide whether to always use
takePhoto(). There is a real speed/sharpness tradeoff here — see [Metrics] ManualShutterToConfirmation - Optimize "Confirm details" page rendering after receipt capture #83503 for the prior discussion. - Restore continuous autofocus after a tap-to-focus, instead of leaving the camera locked.
- Add the
autoFocusSystem: 'phase-detection'preference to format selection. - Test pinning to the single wide-angle device to remove lens-switch AF restarts.
- Consider gating the shutter until autofocus has converged.
Platforms
iOS and Android app.
Slack thread: https://expensify.slack.com/archives/C07NMDKEFMH/p1788739748192919
Activity
OlimpiaZurek commentedon Sep 17, 2026
Update:
Here's what I found digging into the four points.
1. Snapshot capture. Most likely the root cause.- is already being fixed in #99615 (#100592), which captures a real photo alongside the snapshot and swaps it onto the receipt.
2. "No focus mode is ever configured" isn't right. VisionCamera 4.7.2 turns on continuous autofocus itself when the session starts, and its iOS tap-to-focus goes back to continuous once the subject area changes. There's nothing for app code to configure here, so this one can come off the list.
3. Phase-detection preference. Worth trying, iOS only. VisionCamera lets us state a preferred
autoFocusSystem, so it can feed into format selection alongside the existing filters inCamera/index.native.tsx#L93-L99. Android gets nothing from it, since VisionCamera 4.7.2 labels every focus-capable Android camera contrast-detection. I have a draft PR ready: #101434, but it only does anything if an iPhone offers two formats that are identical in resolution and frame rate and differ only in the autofocus column. I can't read the real format list off a simulator, so it needs an ad hoc build before we know whether it changes the selected format at all.4. Multi-lens switching. We request a combined wide/ultra-wide device in
useNativeCamera.ts#L40-L42and render atneutralZoominCameraViewport.tsx#L115, which leaves us sitting near the point where the camera would swap lenses. That's as far as reading the code gets us, and we have no data either way on whether a swap happens during a receipt capture.VisionCamera doesn't expose lens switches to JS, so catching one directly would need native work. Maybe we could add some logging instead, covering what the device reports when the camera opens. I'll check whether that's possible and whether it actually gives us the information we need, and I'll prepare PRs for it if so. It would answer the open question on point 3 at the same time.
AndrewGable commentedon Sep 17, 2026
Sounds great, thank you @OlimpiaZurek
OlimpiaZurek commentedon Sep 18, 2026
Update:
Posted a proposal on Slack for extra camera logging, with a draft PR: #101562.
I think we should get this data first, before we change anything about focus. Both causes depend on the phone's camera hardware, and that is different on every model, so it's better to have input from more devices.
After that, we can go back to the phase-detection draft: #101434.
mkhutornyi commentedon Sep 18, 2026
@AndrewGable Can I be assigned here too since i'll be reviewing similar native camera issue?
OlimpiaZurek commentedon Sep 22, 2026
@mkhutornyi PR's ready for review: #101562
22 remaining items
mkhutornyi commentedon Oct 22, 2026
Not overdue
AndrewGable commentedon Oct 23, 2026
@OlimpiaZurek @mkhutornyi are we done here?
OlimpiaZurek commentedon Oct 26, 2026
@AndrewGable yes, we’re done here. The VisionCamera v5 upgrade (#102898) fixes the root cause on iOS: every scan is now a real photo instead of a blurry preview frame.
melvin-bot commentedon Oct 26, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)AndrewGable commentedon Oct 26, 2026
Awesome work 🎉
mallenexpensify commentedon Oct 26, 2026
Payment Summary
Contributor+: @mkhutornyi due $250 via NewDot
@mkhutornyi Do we need a test case or C+ checklist here?
mkhutornyi commentedon Oct 26, 2026
No need test case. Camera improvement
trjExpensify commentedon Nov 6, 2026
approved $250 for @mkhutornyi