feat: added camera rotation in add photo sheet.#1094
Conversation
📝 WalkthroughWalkthroughThe photo capture component now supports switching between front and rear cameras. It identifies and caches the primary rear camera, coordinates asynchronous stream changes, safely stops obsolete streams, and adds an in-capture camera switch button. ChangesPhoto capture camera switching
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CaptureMode
participant AddPhotoSheet
participant MediaDevices
participant VideoPreview
CaptureMode->>AddPhotoSheet: flipCamera()
AddPhotoSheet->>MediaDevices: enumerateDevices() and getUserMedia()
MediaDevices-->>AddPhotoSheet: selected camera stream
AddPhotoSheet->>VideoPreview: bind stream
AddPhotoSheet->>MediaDevices: stop superseded stream tracks
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
infrastructure/eid-wallet/src/routes/(app)/personal/components/AddPhotoSheet.svelte (2)
124-128: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCached-device lookup has no fallback on stale deviceId.
If
backDeviceIdbecomes stale (device disconnected/re-enumerated), thisgetUserMediathrows and is caught only instartStream's generic handler, which always shows the permission dialog — misleading the user even though the real cause is a missing device, not a denied permission. Consider catching this specific failure, clearingbackDeviceId, and re-running the full probe as a fallback.♻️ Proposed fix
if (backDeviceId) { - return navigator.mediaDevices.getUserMedia({ - video: { deviceId: { exact: backDeviceId } }, - }); + try { + return await navigator.mediaDevices.getUserMedia({ + video: { deviceId: { exact: backDeviceId } }, + }); + } catch { + // Cached device is no longer valid — clear and re-probe below. + backDeviceId = undefined; + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infrastructure/eid-wallet/src/routes/`(app)/personal/components/AddPhotoSheet.svelte around lines 124 - 128, Update the cached-device branch in the media-probing function around backDeviceId to catch getUserMedia failures caused by the stale device, clear backDeviceId, and rerun the full device probe. Ensure the fallback result is handled by the existing startStream flow so stale-device errors do not reach the generic permission-denied handler.
158-172: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winProbing loop doesn't check for supersession.
Once started, the torch-probe loop runs to completion (or exhaustion) even if the sheet is closed or the user flips again mid-probe. It doesn't consult
streamToken/isOpenbetween iterations, so each candidate device is still opened and momentarily activates the hardware/camera indicator after the request that started it is no longer relevant. Adding an early-exit check would release the camera sooner in that case.♻️ Proposed fix
for (const cam of candidates) { + if (facing !== "environment") break; // supersession would be checked via a passed-in token try { const probe = await navigator.mediaDevices.getUserMedia({🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infrastructure/eid-wallet/src/routes/`(app)/personal/components/AddPhotoSheet.svelte around lines 158 - 172, Update the camera probing loop in the torch-probe function to check the current streamToken and isOpen state before opening each candidate and stop probing immediately when the request is superseded or the sheet is closed. Preserve existing cleanup for probes that were opened and the current behavior for valid active requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@infrastructure/eid-wallet/src/routes/`(app)/personal/components/AddPhotoSheet.svelte:
- Around line 52-60: Update AddPhotoSheet.svelte so facingMode defaults to the
front camera on mobile, using the environment camera only as the desktop
fallback, and revise its comment accordingly; at chooseCamera, remove the
unconditional environment reset so the user’s selected camera persists across
opens. Apply these changes at the facingMode declaration (lines 52-60) and
chooseCamera (lines 212-224).
- Around line 102-184: Update openCameraStream so iOS/WKWebView rear-camera
selection does not depend on getCapabilities().torch or the multi-camera probing
heuristic. Detect the iOS environment and use an iOS-safe strategy, such as the
established native camera/torch integration, or bypass torch probing and select
the appropriate rear camera deterministically; preserve the existing torch-based
selection for supported non-iOS platforms.
---
Nitpick comments:
In
`@infrastructure/eid-wallet/src/routes/`(app)/personal/components/AddPhotoSheet.svelte:
- Around line 124-128: Update the cached-device branch in the media-probing
function around backDeviceId to catch getUserMedia failures caused by the stale
device, clear backDeviceId, and rerun the full device probe. Ensure the fallback
result is handled by the existing startStream flow so stale-device errors do not
reach the generic permission-denied handler.
- Around line 158-172: Update the camera probing loop in the torch-probe
function to check the current streamToken and isOpen state before opening each
candidate and stop probing immediately when the request is superseded or the
sheet is closed. Preserve existing cleanup for probes that were opened and the
current behavior for valid active requests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7366f2e5-2e83-4d40-9c5d-1e43ae02b848
📒 Files selected for processing (1)
infrastructure/eid-wallet/src/routes/(app)/personal/components/AddPhotoSheet.svelte
| // Which camera to use. The back camera is the default; users can flip to | ||
| // the front (selfie) camera from the viewfinder. On desktop "environment" | ||
| // is advisory and resolves to the only available camera anyway. | ||
| let facingMode = $state<"user" | "environment">("environment"); | ||
| // Bumped on every (re)start so a slow getUserMedia belonging to a | ||
| // superseded request (e.g. a rapid flip) can detect it lost the race | ||
| // and release its stream instead of binding a stale one. | ||
| let streamToken = 0; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Default camera and preference persistence contradict the linked issue's requirements. The linked issue states the front camera should default on mobile and the selected camera should be maintained throughout the capture flow, but this implementation defaults to and re-forces the back camera on every open.
infrastructure/eid-wallet/src/routes/(app)/personal/components/AddPhotoSheet.svelte#L52-L60: change the initialfacingModedefault (and the doc comment) so front is the default on mobile, reserving"environment"as the fallback for desktop where "user"-facing has no meaning.infrastructure/eid-wallet/src/routes/(app)/personal/components/AddPhotoSheet.svelte#L212-L224: drop the unconditionalfacingMode = "environment"reset inchooseCamera()so a user's prior front/back selection carries over between opens, per the "maintained throughout the capture flow" requirement.
📍 Affects 1 file
infrastructure/eid-wallet/src/routes/(app)/personal/components/AddPhotoSheet.svelte#L52-L60(this comment)infrastructure/eid-wallet/src/routes/(app)/personal/components/AddPhotoSheet.svelte#L212-L224
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@infrastructure/eid-wallet/src/routes/`(app)/personal/components/AddPhotoSheet.svelte
around lines 52 - 60, Update AddPhotoSheet.svelte so facingMode defaults to the
front camera on mobile, using the environment camera only as the desktop
fallback, and revise its comment accordingly; at chooseCamera, remove the
unconditional environment reset so the user’s selected camera persists across
opens. Apply these changes at the facingMode declaration (lines 52-60) and
chooseCamera (lines 212-224).
| const BACK_LABEL = /back|rear|environment/i; | ||
| const FRONT_LABEL = /front|face|user|selfie/i; | ||
|
|
||
| // The main rear sensor, resolved once and cached — flipping back to it then | ||
| // costs a single getUserMedia instead of re-probing every camera. | ||
| let backDeviceId: string | undefined; | ||
|
|
||
| // Open a working stream for the requested facing. | ||
| // - Front: a plain facingMode constraint is reliable and fast. | ||
| // - Back: the default rear device is often a secondary sensor (ultrawide / | ||
| // depth) that streams a black frame, so we locate the *main* rear sensor | ||
| // by its torch capability (the technique the KYC scanner uses) and cache | ||
| // it. The probe stream for the winning camera is reused, so the main | ||
| // camera is opened only once. | ||
| async function openCameraStream( | ||
| facing: "user" | "environment", | ||
| ): Promise<MediaStream> { | ||
| if (facing === "user") { | ||
| return navigator.mediaDevices.getUserMedia({ | ||
| video: { facingMode: "user" }, | ||
| }); | ||
| } | ||
| if (backDeviceId) { | ||
| return navigator.mediaDevices.getUserMedia({ | ||
| video: { deviceId: { exact: backDeviceId } }, | ||
| }); | ||
| } | ||
|
|
||
| let cams = (await navigator.mediaDevices.enumerateDevices()).filter( | ||
| (d) => d.kind === "videoinput", | ||
| ); | ||
| // Labels stay empty until the session has been granted a getUserMedia; | ||
| // prime with a throwaway stream so we can tell rear cameras from front. | ||
| if (cams.length && cams.every((d) => !d.label)) { | ||
| try { | ||
| const primer = await navigator.mediaDevices.getUserMedia({ | ||
| video: true, | ||
| }); | ||
| for (const t of primer.getTracks()) t.stop(); | ||
| cams = (await navigator.mediaDevices.enumerateDevices()).filter( | ||
| (d) => d.kind === "videoinput", | ||
| ); | ||
| } catch { | ||
| // Ignore — fall through to whatever labels we have. | ||
| } | ||
| } | ||
|
|
||
| // Candidate rear cameras: those labelled back, or (labels hidden) | ||
| // everything that isn't explicitly a front camera. | ||
| const labelledBack = cams.filter((d) => BACK_LABEL.test(d.label)); | ||
| const candidates = labelledBack.length | ||
| ? labelledBack | ||
| : cams.filter((d) => !FRONT_LABEL.test(d.label)); | ||
|
|
||
| // The main rear sensor is the one exposing a torch. Probe each candidate | ||
| // and reuse the winner's stream rather than reopening it. | ||
| for (const cam of candidates) { | ||
| try { | ||
| const probe = await navigator.mediaDevices.getUserMedia({ | ||
| video: { deviceId: { exact: cam.deviceId } }, | ||
| }); | ||
| const caps = probe.getVideoTracks()[0]?.getCapabilities?.(); | ||
| if (caps && "torch" in caps) { | ||
| backDeviceId = cam.deviceId; | ||
| return probe; | ||
| } | ||
| for (const t of probe.getTracks()) t.stop(); | ||
| } catch { | ||
| // Skip cameras we can't open. | ||
| } | ||
| } | ||
|
|
||
| // No torch found (desktop, or a webview that hides capabilities): fall | ||
| // back to the last rear candidate — conventionally the main camera — or a | ||
| // plain facingMode constraint if we couldn't enumerate anything. | ||
| const fallback = candidates.at(-1) ?? cams.at(-1); | ||
| backDeviceId = fallback?.deviceId; | ||
| return navigator.mediaDevices.getUserMedia({ | ||
| video: fallback | ||
| ? { deviceId: { exact: fallback.deviceId } } | ||
| : { facingMode: "environment" }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file context =="
sed -n '80,195p' "infrastructure/eid-wallet/src/routes/(app)/personal/components/AddPhotoSheet.svelte" 2>/dev/null || true
echo
echo "== PR/objective references to platforms =="
rg -n -i "iOS|Apple|iPhone|iPad|Android|desktop|rear|torch|getCapabilities|facingMode" . \
-g '!node_modules' -g '!dist' -g '!build' -g '!coverage' | head -200
echo
echo "== nearby usages of openCameraStream or AddPhotoSheet =="
rg -n "openCameraStream|AddPhotoSheet|enable.*camera|camera" "infrastructure/eid-wallet/src/routes/(app)/personal" 2>/dev/null || trueRepository: MetaState-Prototype-Project/prototype
Length of output: 26058
🌐 Web query:
Apple Developer Forums Safari WKWebView getCapabilities torch capability unsupported iOS
💡 Result:
Support for the 'torch' capability in WKWebView on iOS has been an evolving feature in WebKit [1][2]. While standard Web Media Capture APIs often report the 'torch' capability as unsupported or missing when accessed via getCapabilities() in older iOS versions or standard WKWebView implementations, recent updates have introduced support within the WebKit engine [1]. Key points regarding the current status: 1. Official WebKit Support: Support for the 'torch' capability was added to WebKit (the engine powering WKWebView) to expose it in MediaTrackCapabilities and MediaTrackSettings [1]. This allows web applications to potentially detect and control the torch if the underlying hardware and implementation permit [1]. 2. Limitations and Variability: Even with engine-level support, developers often find that the 'torch' capability is not consistently available or functional across all iOS devices, browser versions, or specific WKWebView configurations [3][4]. In many practical scenarios, especially for barcode scanning or camera-intensive web apps, getCapabilities() may still return an object that lacks the torch property or reports it as unsupported [3][5]. 3. Native Alternatives: Due to the historical inconsistency of the web-based Torch API on iOS, many developers use native integration to control the flashlight [6]. For hybrid apps (using frameworks like Capacitor, Cordova, or custom WKWebView wrappers), it is common practice to implement a native bridge that accesses the camera's torch functionality directly through Swift or Objective-C, bypassing the limitations of the web view's MediaStreamTrack API [5][6][2]. 4. Recent Updates: Apple continues to refine WKWebView and Safari's media capabilities [7]. For instance, recent release notes (e.g., Safari 18.4) have included fixes related to getUserMedia video track constraints and the reporting of torch settings [7]. If you are encountering issues, ensure your environment is updated, though native workarounds remain the most reliable method for production-grade torch control on iOS [5][6]. If you are developing a web application, it is recommended to feature-detect the 'torch' capability dynamically using track.getCapabilities().torch and provide a fallback or inform the user if it is unavailable [8][9].
Citations:
- 1: [MediaStream] Add support for torch WebKit/WebKit#18249
- 2: Add flashlight control cordova-rtc/cordova-plugin-iosrtc#731
- 3: toggleTorch not working properly on mobile web versions juliansteenbakker/mobile_scanner#955
- 4: Torch in IOS zxing-js/browser#12
- 5: fix(barcode-scanning):
isTorchAvailablealways returning false on iOS capawesome-team/capacitor-mlkit#320 - 6: https://cocoapods.org/pods/WebViewSDK
- 7: https://developer.apple.com/documentation/safari-release-notes/safari-18_4-release-notes
- 8: https://blog.michal.pawlik.dev/posts/til/flashlight-using-typescript/
- 9: https://stackoverflow.com/questions/78769178/is-there-an-html5-api-to-control-camera-focus-and-zoom-on-iphone
🌐 Web query:
Tauri 2 iOS WKWebView navigator.mediaDevices getUserMedia Torch enabled MediaTrackCapabilities torch
💡 Result:
In Tauri 2 iOS applications, you cannot reliably control the device torch directly through the browser-based navigator.mediaDevices API within the WKWebView [1]. While modern versions of WebKit have added experimental support for MediaTrackCapabilities including 'torch' [2][3], relying on this within a WKWebView container is often inconsistent or unsupported depending on the iOS version and specific platform implementation [1][4]. To achieve reliable torch control in a Tauri 2 iOS app, the recommended approach is to use a native plugin that interfaces directly with the iOS AVFoundation framework [5][6]. Key points for implementing torch functionality: 1. Native Plugin Requirement: Use a dedicated Tauri plugin such as tauri-plugin-torch [5][6]. These plugins bridge the Rust backend to the native iOS Swift code, providing a stable API to toggle the flashlight independently of the webview's media constraints [7][6]. 2. Limitations of Web APIs: Even if navigator.mediaDevices is available in your WKWebView, the torch constraint is frequently ignored or restricted by the iOS system for security and privacy reasons, making it unreliable for production applications [1][4]. 3. Permissions: Ensure your Info.plist includes the necessary privacy keys (e.g., NSCameraUsageDescription) to allow the app to access the camera hardware, which is required for torch control [8][1]. 4. Implementation: With a native plugin, you can expose methods to your frontend (e.g., toggle(), check()) that execute native commands, bypassing the limitations of the webview's media stream environment [5][6]. For camera preview functionality (if needed alongside the torch), you may also consider community plugins like tauri-plugin-nativecamera, which provide native-backed interfaces for camera operations on mobile platforms [9].
Citations:
- 1: https://bugs.webkit.org/show_bug.cgi?format=multiple&id=188360
- 2: [MediaStream] Add support for torch WebKit/WebKit#18249
- 3: https://webkit.org/blog/16574/webkit-features-in-safari-18-4/
- 4: https://stackoverflow.com/questions/73441115/navigator-mediadevices-is-undefined-in-wkwebview
- 5: https://github.com/sosweetham/tauri-plugin-torch
- 6: https://crates.io/crates/tauri-plugin-torch
- 7: https://takazudomodular.com/pj/zudo-tauri/docs/mobile/
- 8: navigator.mediaDevices is undefined tauri-apps/tauri#5370
- 9: https://github.com/ZAAFHachemrachid/tauri-plugin-nativecamera
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Tauri deps =="
sed -n '1,90p' infrastructure/eid-wallet/src-tauri/Cargo.toml
jq '.build, .bundle, .plugins, .identifier, .productName' infrastructure/eid-wallet/src-tauri/tauri.conf.json 2>/dev/null | head -220
echo
echo "== mobile capability =="
cat -n infrastructure/eid-wallet/src-tauri/capabilities/mobile.json
echo
echo "== lockfile relevant Tauri versions (concise) =="
rg -n "'(`@tauri-apps/`|Tauri|tauri)" pnpm-lock.yaml | head -160Repository: MetaState-Prototype-Project/prototype
Length of output: 5545
🌐 Web query:
WebKit Safari MediaTrackCapabilities torch supported iOS version
💡 Result:
Support for the 'torch' constraint in MediaTrackCapabilities was added to WebKit in September 2023 [1]. While specific official Apple documentation detailing the exact release version is sparse, developer reports and technical tracking indicate that the 'torch' constraint became functional in Safari on iOS starting with iOS 17.4 [2]. Before this implementation, the 'torch' constraint was ignored on iOS Safari [3]. Even following its introduction, some users have reported ongoing issues with the consistency of the 'torch' setting, such as instances where getSettings may return stale or unexpected values in later versions, including iOS 18 [4]. To check if it is supported on a specific device or browser instance, developers should inspect the MediaStreamTrack.getCapabilities object to see if 'torch' is present in the returned list of supported constraints [5].
Citations:
- 1: [MediaStream] Add support for torch WebKit/WebKit#18249
- 2: https://stackoverflow.com/questions/62717417/is-it-possible-to-create-a-web-app-that-can-access-the-torch-from-safari-using-j
- 3: https://bugs.webkit.org/show_bug.cgi?id=243075
- 4: https://github.com/pixelverse-llc/safari-ios18-gum-torch-setting
- 5: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities
Avoid relying on torch detection for iOS rear-camera selection.
getCapabilities() returns torch support inconsistently on iOS/WKWebView, so this path can fall back to the last rear candidate on devices where the heuristic should identify the main rear sensor. Test the fallback on iOS, or use an iOS-specific strategy such as a native camera/torch plugin or disabling the torch-based multi_probe path for that platform.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@infrastructure/eid-wallet/src/routes/`(app)/personal/components/AddPhotoSheet.svelte
around lines 102 - 184, Update openCameraStream so iOS/WKWebView rear-camera
selection does not depend on getCapabilities().torch or the multi-camera probing
heuristic. Detect the iOS environment and use an iOS-safe strategy, such as the
established native camera/torch integration, or bypass torch probing and select
the appropriate rear camera deterministically; preserve the existing torch-based
selection for supported non-iOS platforms.
Description of change
Added camera rotation in add photo sheet and added back camera caching so that it doesn't take huge time to switch to multiple cameras and find the actual camera.
Issue Number
Closes #1093
Type of change
How the change has been tested
Manually.
Change checklist
Summary by CodeRabbit
New Features
Bug Fixes