Skip to content

[Due for payment 2026-10-07] Receipt camera autofocus is worse than Expensify Classic — most captures grab an unconverged preview frame #100537

Closed

Description

@MelvinBot

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:

  1. The default shutter path captures a preview frame, not a still photo. captureReceipt/index.ts:6-21 calls takePhoto() only when flash is on or the device is in landscape; otherwise it falls through to takeSnapshot({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 that takePhoto() performs. Having to rescan three or more times is exactly what that produces.

  2. No focus mode is ever configured. useNativeCamera.ts:40-42 sets up the device but never requests a focus mode, and focusCamera calls camera.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.

  3. Format selection ignores the autofocus system. Camera/index.native.tsx:93-99 ranks formats by aspect ratio and resolution only. There is no autoFocusSystem: '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.

  4. 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 at device.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

Issue OwnerCurrent Issue Owner: @mkhutornyi

Activity

  1. OlimpiaZurek commented on Sep 17, 2026

    @OlimpiaZurek
    Contributor

    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 in Camera/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-L42 and render at neutralZoom in CameraViewport.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.

  2. AndrewGable commented on Sep 17, 2026

    @AndrewGable
    Contributor

    Sounds great, thank you @OlimpiaZurek

  3. OlimpiaZurek commented on Sep 18, 2026

    @OlimpiaZurek
    Contributor

    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.

  4. mkhutornyi commented on Sep 18, 2026

    @mkhutornyi
    Contributor

    @AndrewGable Can I be assigned here too since i'll be reviewing similar native camera issue?

  5. OlimpiaZurek commented on Sep 22, 2026

    @OlimpiaZurek
    Contributor

    @mkhutornyi PR's ready for review: #101562

  6. 22 remaining items

  7. mkhutornyi commented on Oct 22, 2026

    @mkhutornyi
    Contributor

    Not overdue

  8. AndrewGable commented on Oct 23, 2026

    @AndrewGable
    Contributor

    @OlimpiaZurek @mkhutornyi are we done here?

  9. OlimpiaZurek commented on Oct 26, 2026

    @OlimpiaZurek
    Contributor

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

  10. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed
    Skip PaymentSkips all the payment process steps for an issue. Use it for fixed-price projects.
    on Oct 26, 2026
  11. melvin-bot commented on Oct 26, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  12. AndrewGable commented on Oct 26, 2026

    @AndrewGable
    Contributor

    Awesome work 🎉

  13. mallenexpensify commented on Oct 26, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @mkhutornyi due $250 via NewDot

    @mkhutornyi Do we need a test case or C+ checklist here?

  14. mkhutornyi commented on Oct 26, 2026

    @mkhutornyi
    Contributor

    No need test case. Camera improvement

  15. trjExpensify commented on Nov 6, 2026

    @trjExpensify
    Contributor

    approved $250 for @mkhutornyi

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 productionDailyKSv2ImprovementItem broken or needs improvement.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions