Skip to content

[PM-39361] fix: Restart card scanner after camera interruption or reopen#2847

Merged
fedemkr merged 5 commits into
mainfrom
PM-39361/fix-card-scanner-unresponsive
Jul 22, 2026
Merged

[PM-39361] fix: Restart card scanner after camera interruption or reopen#2847
fedemkr merged 5 commits into
mainfrom
PM-39361/fix-card-scanner-unresponsive

Conversation

@fedemkr

@fedemkr fedemkr commented Jun 30, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-39361

📔 Objective

Fixes a blank camera screen when opening the card scanner after the Add Card view was closed while the scanner was still open, or after the device locks and unlocks while the app is on the Add Card screen.

Changes in CardScannerView.swift:

  • Replace try? startScanning() with explicit error handling; on failure a new
    onScannerUnavailable callback is fired (dispatched after the current SwiftUI
    update pass to avoid re-entrant state mutations).
  • Implement dataScanner(_:becameUnavailableWithError:) in the Coordinator to
    forward runtime camera-interruption events through the same callback.
  • Add @Environment(\.scenePhase) observation in CardScannerWrapperView; when
    the scene becomes .active while scanning is supposed to be running, a
    stop-then-restart cycle is triggered.
  • Retry up to 2 times with a 300 ms delay before auto-dismissing the sheet, so
    the user is never left with a permanently blank screen.

@fedemkr fedemkr added the ai-review Request a Claude code review label Jun 30, 2026
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context t:bug Change Type - Bug labels Jun 30, 2026
@fedemkr
fedemkr marked this pull request as ready for review June 30, 2026 21:48
@fedemkr
fedemkr requested review from a team and matt-livefront as code owners June 30, 2026 21:48
@fedemkr fedemkr added the enhancement New feature or request label Jun 30, 2026
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the card scanner restart logic in CardScannerView.swift and the new Coordinator unit tests in CardScannerViewTests.swift. The latest commit resolves the two concerns raised earlier in this PR: the retry-budget double-counting (foreground resumes now use a dedicated resumeScanningAfterForeground() path that does not consume the failure budget) and the scenePhase unreliability under UIKit hosting (replaced with a UIApplication.willEnterForegroundNotification observer). The Coordinator initializer signature change is fully propagated to all internal call sites, and the dispatch-after-update-pass handling of startScanning() failures is coherent and within scope.

Code Review Details

No new blocking findings.

Previously raised concerns are now resolved:

  • Retry-budget double-counting — foreground transitions no longer consume the failure retry budget (resumeScanningAfterForeground()).
  • scenePhase unreliability under UIHostingController — replaced with a willEnterForegroundNotification observer.
  • DispatchQueue.main.async rationale (CardScannerView.swift:172) — answered by the author and resolved.

The minor body overlap between restartScanning() and resumeScanningAfterForeground() is intentional divergence (budget/dismiss vs. no-budget) and does not warrant a change. Test coverage appropriately targets the Coordinator delegate path and documents why the @State-driven wrapper logic is not unit-tested.

Comment on lines 65 to 87
.onChange(of: scenePhase) { newPhase in
if newPhase == .active, isScanning {
restartScanning()
}
}
}

// MARK: Private

/// Stops scanning, waits 300 ms for the camera to fully release, then restarts.
/// After two retries the sheet is dismissed so the user is never left with a blank screen.
private func restartScanning() {
guard scannerRetryCount < 2 else {
dismiss()
return
}
scannerRetryCount += 1
isScanning = false
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(300))
isScanning = true
}
}

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.

⚠️ IMPORTANT: Benign foreground transitions consume the retry budget and can auto-dismiss a working scanner.

Details and fix

scannerRetryCount is shared between two very different triggers: genuine scanner failures (onScannerUnavailable) and routine scenePhase == .active transitions (line 66). It only resets on .onAppear, which does not re-fire when the app is backgrounded and foregrounded while the sheet stays presented.

Trace:

After two normal app-switches the sheet closes on the next foreground even though the camera is fine. A successful restart also never decrements the counter, so each recovery permanently shrinks the budget available for a later real failure.

Consider resetting scannerRetryCount = 0 on a successful restart (e.g. after isScanning = true settles), or only counting onScannerUnavailable-driven restarts toward the cap rather than scene-phase restarts.

fedemkr added 2 commits July 17, 2026 13:00
…annerView

Replace scenePhase (unreliable in this UIKit-hosted app) with a
UIApplication.willEnterForegroundNotification observer, and stop
foreground-triggered restarts from consuming the same retry budget
as genuine scanner failures.
@fedemkr
fedemkr requested a review from matt-livefront July 17, 2026 19:37
.onChange(of: scenePhase) { newPhase in
if newPhase == .active, isScanning {
restartScanning()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matt-livefront I wasn't sure whether this would be a good idea to put it directly here. It seems more like SwiftUI but less testable so if needed I can move it to the processor and do the logic somehow there.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok given that we would have used scene phase here and it's for a minor and self-contained use.

matt-livefront
matt-livefront previously approved these changes Jul 21, 2026

Copilot AI 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.

Pull request overview

This PR aims to prevent the card-scanner sheet from getting stuck on a blank camera view after interruptions (e.g., device lock/unlock, leaving/reopening the sheet) by adding explicit scanner-unavailable handling and a controlled restart strategy.

Changes:

  • Adds an onScannerUnavailable callback that is triggered when startScanning() throws or when VisionKit reports the scanner becoming unavailable at runtime.
  • Implements a stop→delay→restart cycle with a capped retry budget, plus a foreground-triggered restart path.
  • Adds unit tests for the coordinator’s forwarding of becameUnavailableWithError to the new callback.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditCardItem/CardScannerView.swift Adds scanner-unavailable callback wiring, restart/retry logic, and foreground resumption handling.
BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditCardItem/CardScannerViewTests.swift Adds tests ensuring runtime-unavailable delegate events trigger onScannerUnavailable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +84 to +89
scannerRetryCount += 1
isScanning = false
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(300))
isScanning = true
}
Comment on lines +65 to +69
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
// `scenePhase` is driven by SwiftUI's own `Scene`/`App` lifecycle and is unreliable
// for views hosted in a UIKit app via `UIHostingController`, so foreground transitions
// are observed directly via `UIApplication` notifications instead.
if isScanning {
Comment on lines +77 to +85
/// Stops scanning, waits 300 ms for the camera to fully release, then restarts.
/// After two retries the sheet is dismissed so the user is never left with a blank screen.
private func restartScanning() {
guard scannerRetryCount < 2 else {
dismiss()
return
}
scannerRetryCount += 1
isScanning = false
@github-actions

Copy link
Copy Markdown
Contributor

Warning

@fedemkr Uploading code coverage report failed. Please check the "Upload to codecov.io" step of Process Test Reports job for more details.

@fedemkr
fedemkr merged commit faa246a into main Jul 22, 2026
15 checks passed
@fedemkr
fedemkr deleted the PM-39361/fix-card-scanner-unresponsive branch July 22, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review app:password-manager Bitwarden Password Manager app context enhancement New feature or request t:bug Change Type - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants