From 97448aae5fc771206d327aee7f96954696fb008e Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Mon, 19 Jan 2026 11:28:51 +0530 Subject: [PATCH 1/6] feat: add re request perm logic --- .../src/routes/(app)/scan-qr/+page.svelte | 84 ++++++++++++++----- .../src/routes/(app)/scan-qr/scanLogic.ts | 21 +++++ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte index b39ff02e..b9f919bb 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte @@ -40,6 +40,7 @@ const { authError, signingError, authLoading, + cameraPermissionDenied, } = stores; const { @@ -56,6 +57,7 @@ const { handleBlindVoteSelection, handleSignVote, initialize, + retryPermission, } = actions; const pathProps: SVGAttributes = { @@ -148,26 +150,68 @@ function handleRevealDrawerOpenChange(value: boolean) {
- - - - - - - -

- Point the camera at the code -

+ {#if $cameraPermissionDenied} +
+ + + + + +

+ Camera Access Required +

+ +

+ To scan QR codes, please grant camera permission. You can enable it in your device settings or tap the button below to try again. +

+ + +
+ {:else} + + + + + + + +

+ Point the camera at the code +

+ {/if}
; authLoading: Writable; isFromScan: Writable; + cameraPermissionDenied: Writable; } interface ScanActions { @@ -106,6 +107,7 @@ interface ScanActions { redirectUri: string | null, ) => Promise; initialize: () => Promise<() => void>; + retryPermission: () => Promise; } interface ScanLogic { @@ -145,10 +147,14 @@ export function createScanLogic({ const signingError = writable(null); const authLoading = writable(false); const isFromScan = writable(false); + const cameraPermissionDenied = writable(false); let permissionsNullable: PermissionState | null = null; async function startScan() { + // Reset permission denied state when attempting to scan + cameraPermissionDenied.set(false); + let permissions: PermissionState | null = null; try { permissions = await checkPermissions(); @@ -162,6 +168,13 @@ export function createScanLogic({ permissionsNullable = permissions; + // If permission is still denied after requesting, inform the user + if (permissions !== "granted") { + console.warn("Camera permission denied or unavailable"); + cameraPermissionDenied.set(true); + return; + } + if (permissions === "granted") { const formats = [Format.QRCode]; const windowed = true; @@ -232,6 +245,12 @@ export function createScanLogic({ scanning.set(false); } + async function retryPermission() { + // Attempt to request permissions again + cameraPermissionDenied.set(false); + await startScan(); + } + async function handleAuth() { const vault = await globalState.vaultController.vault; if (!vault || !get(redirect)) return; @@ -1466,6 +1485,7 @@ export function createScanLogic({ signingError, authLoading, isFromScan, + cameraPermissionDenied, }, actions: { startScan, @@ -1487,6 +1507,7 @@ export function createScanLogic({ handleDeepLinkData, handleBlindVotingRequest, initialize, + retryPermission, }, }; } From 065d2b96d7ca895e5e47e5e459291ce063a95711 Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Mon, 19 Jan 2026 11:47:30 +0530 Subject: [PATCH 2/6] fix: app settings page --- .../src-tauri/capabilities/mobile.json | 1 + .../eid-wallet.xcodeproj/project.pbxproj | 8 ++--- .../eid-wallet/src-tauri/tauri.conf.json | 2 +- .../src/routes/(app)/scan-qr/+page.svelte | 31 ++++++++++++++----- .../src/routes/(app)/scan-qr/scanLogic.ts | 24 +++++++++++++- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json b/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json index 6e6c8369..6af140f4 100644 --- a/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json +++ b/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json @@ -11,6 +11,7 @@ "store:default", "biometric:default", "barcode-scanner:default", + "barcode-scanner:allow-open-app-settings", "deep-link:default", "crypto-hw:default", "notification:default", diff --git a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj index 3a4c3da0..55e11ecd 100644 --- a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj +++ b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj @@ -389,7 +389,7 @@ CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = 0.3.0.0; - DEVELOPMENT_TEAM = M49C8XS835; + DEVELOPMENT_TEAM = 7F2T2WK6DR; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; FRAMEWORK_SEARCH_PATHS = ( @@ -416,7 +416,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); MARKETING_VERSION = 0.3.0; - PRODUCT_BUNDLE_IDENTIFIER = foundation.metastate.eid-wallet; + PRODUCT_BUNDLE_IDENTIFIER = com.kodski.eid-wallet; PRODUCT_NAME = "eID for W3DS"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -437,7 +437,7 @@ CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = 0.3.0.0; - DEVELOPMENT_TEAM = M49C8XS835; + DEVELOPMENT_TEAM = 7F2T2WK6DR; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; FRAMEWORK_SEARCH_PATHS = ( @@ -464,7 +464,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); MARKETING_VERSION = 0.3.0; - PRODUCT_BUNDLE_IDENTIFIER = foundation.metastate.eid-wallet; + PRODUCT_BUNDLE_IDENTIFIER = com.kodski.eid-wallet; PRODUCT_NAME = "eID for W3DS"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/infrastructure/eid-wallet/src-tauri/tauri.conf.json b/infrastructure/eid-wallet/src-tauri/tauri.conf.json index 8431a83e..49757042 100644 --- a/infrastructure/eid-wallet/src-tauri/tauri.conf.json +++ b/infrastructure/eid-wallet/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "eID for W3DS", "version": "0.5.0", - "identifier": "foundation.metastate.eid-wallet", + "identifier": "com.kodski.eid-wallet", "build": { "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte index b9f919bb..f101dc1a 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte @@ -2,6 +2,7 @@ import { goto } from "$app/navigation"; import AppNav from "$lib/fragments/AppNav/AppNav.svelte"; import type { GlobalState } from "$lib/global"; +import { ButtonAction } from "$lib/ui"; import { getContext, onDestroy, onMount } from "svelte"; import type { SVGAttributes } from "svelte/elements"; import { get } from "svelte/store"; @@ -58,6 +59,7 @@ const { handleSignVote, initialize, retryPermission, + handleOpenSettings, } = actions; const pathProps: SVGAttributes = { @@ -145,13 +147,17 @@ function handleRevealDrawerOpenChange(value: boolean) { } +{#if $cameraPermissionDenied} +
+{/if} +
{#if $cameraPermissionDenied} -
+

- To scan QR codes, please grant camera permission. You can enable it in your device settings or tap the button below to try again. + To scan QR codes, please grant camera permission in your device settings.

- +
+ + Open Settings + + + goto("/main")} + > + Go Back + +
{:else} Promise; initialize: () => Promise<() => void>; retryPermission: () => Promise; + handleOpenSettings: () => Promise; } interface ScanLogic { @@ -246,11 +248,30 @@ export function createScanLogic({ } async function retryPermission() { - // Attempt to request permissions again + // Check current permission state + let permissions: PermissionState | null = null; + try { + permissions = await checkPermissions(); + } catch { + permissions = null; + } + + // If permission is denied (not just prompt), open app settings + // because the OS won't show the permission dialog again + if (permissions === "denied") { + await openAppSettings(); + return; + } + + // Otherwise, attempt to request permissions again cameraPermissionDenied.set(false); await startScan(); } + async function handleOpenSettings() { + await openAppSettings(); + } + async function handleAuth() { const vault = await globalState.vaultController.vault; if (!vault || !get(redirect)) return; @@ -1508,6 +1529,7 @@ export function createScanLogic({ handleBlindVotingRequest, initialize, retryPermission, + handleOpenSettings, }, }; } From 4c102b96d2ad1fd76375d100ae7d372609a236d9 Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Mon, 19 Jan 2026 16:03:19 +0530 Subject: [PATCH 3/6] fix: segregate common logic and add to onboarding --- .../CameraPermissionDialog.svelte | 93 ++++++++++++++ .../lib/ui/CameraPermissionDialog/index.ts | 1 + infrastructure/eid-wallet/src/lib/ui/index.ts | 1 + .../src/lib/utils/cameraPermission.ts | 120 ++++++++++++++++++ .../eid-wallet/src/lib/utils/index.ts | 1 + .../src/routes/(app)/scan-qr/+page.svelte | 109 +++++----------- .../(auth)/verify/steps/passport.svelte | 48 ++++--- .../routes/(auth)/verify/steps/selfie.svelte | 31 ++++- 8 files changed, 310 insertions(+), 94 deletions(-) create mode 100644 infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte create mode 100644 infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/index.ts create mode 100644 infrastructure/eid-wallet/src/lib/utils/cameraPermission.ts diff --git a/infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte b/infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte new file mode 100644 index 00000000..094c1299 --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte @@ -0,0 +1,93 @@ + + + +
+ + + + + + +

+ {title} +

+ +

+ {description} +

+ +
+ + Open Settings + + + {#if onGoBack} + + Go Back + + {:else} + +
+ {/if} +
+
+ diff --git a/infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/index.ts b/infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/index.ts new file mode 100644 index 00000000..a695847a --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/index.ts @@ -0,0 +1 @@ +export { default as CameraPermissionDialog } from "./CameraPermissionDialog.svelte"; diff --git a/infrastructure/eid-wallet/src/lib/ui/index.ts b/infrastructure/eid-wallet/src/lib/ui/index.ts index 09034124..8619822e 100644 --- a/infrastructure/eid-wallet/src/lib/ui/index.ts +++ b/infrastructure/eid-wallet/src/lib/ui/index.ts @@ -3,3 +3,4 @@ export { default as InputPin } from "./InputPin/InputPin.svelte"; export { default as ButtonAction } from "./Button/ButtonAction.svelte"; export { default as Selector } from "./Selector/Selector.svelte"; export { default as Toast } from "./Toast/Toast.svelte"; +export { default as CameraPermissionDialog } from "./CameraPermissionDialog/CameraPermissionDialog.svelte"; diff --git a/infrastructure/eid-wallet/src/lib/utils/cameraPermission.ts b/infrastructure/eid-wallet/src/lib/utils/cameraPermission.ts new file mode 100644 index 00000000..a4a3fe9e --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/utils/cameraPermission.ts @@ -0,0 +1,120 @@ +import { + type PermissionState, + checkPermissions, + openAppSettings, + requestPermissions, +} from "@tauri-apps/plugin-barcode-scanner"; +import { writable, type Writable } from "svelte/store"; + +export interface CameraPermissionState { + status: PermissionState | null; + isDenied: boolean; + isGranted: boolean; + isChecking: boolean; +} + +export interface CameraPermissionResult { + permissionState: Writable; + checkAndRequestPermission: () => Promise; + retryPermission: () => Promise; + openSettings: () => Promise; +} + +/** + * Creates a camera permission manager that handles checking, requesting, + * and managing camera permissions using Tauri's barcode-scanner plugin. + * + * This can be used in both the scan page and onboarding flows where camera + * access is required. + */ +export function createCameraPermissionManager(): CameraPermissionResult { + const permissionState = writable({ + status: null, + isDenied: false, + isGranted: false, + isChecking: false, + }); + + /** + * Check current permission status and request if needed. + * Returns true if permission is granted, false otherwise. + */ + async function checkAndRequestPermission(): Promise { + permissionState.update((state) => ({ + ...state, + isChecking: true, + isDenied: false, + })); + + let permissions: PermissionState | null = null; + + try { + permissions = await checkPermissions(); + } catch { + permissions = null; + } + + // If permission is prompt or denied, request it + if (permissions === "prompt" || permissions === "denied") { + try { + permissions = await requestPermissions(); + } catch { + permissions = null; + } + } + + const isGranted = permissions === "granted"; + const isDenied = !isGranted; + + permissionState.set({ + status: permissions, + isDenied, + isGranted, + isChecking: false, + }); + + if (isDenied) { + console.warn("Camera permission denied or unavailable"); + } + + return isGranted; + } + + /** + * Retry permission request. If permission was previously denied (not just prompt), + * this will open app settings since the OS won't show the dialog again. + */ + async function retryPermission(): Promise { + let permissions: PermissionState | null = null; + + try { + permissions = await checkPermissions(); + } catch { + permissions = null; + } + + // If permission is denied (not just prompt), open app settings + // because the OS won't show the permission dialog again + if (permissions === "denied") { + await openAppSettings(); + return false; + } + + // Otherwise, attempt to request permissions again + return checkAndRequestPermission(); + } + + /** + * Open the app's settings page in system settings. + */ + async function openSettings(): Promise { + await openAppSettings(); + } + + return { + permissionState, + checkAndRequestPermission, + retryPermission, + openSettings, + }; +} diff --git a/infrastructure/eid-wallet/src/lib/utils/index.ts b/infrastructure/eid-wallet/src/lib/utils/index.ts index 287c0a24..229c49be 100644 --- a/infrastructure/eid-wallet/src/lib/utils/index.ts +++ b/infrastructure/eid-wallet/src/lib/utils/index.ts @@ -2,3 +2,4 @@ export * from "./mergeClasses"; export * from "./clickOutside"; export * from "./capitalize"; export * from "./swipeGesture"; +export * from "./cameraPermission"; diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte index f101dc1a..47e180af 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte @@ -2,7 +2,7 @@ import { goto } from "$app/navigation"; import AppNav from "$lib/fragments/AppNav/AppNav.svelte"; import type { GlobalState } from "$lib/global"; -import { ButtonAction } from "$lib/ui"; +import { ButtonAction, CameraPermissionDialog } from "$lib/ui"; import { getContext, onDestroy, onMount } from "svelte"; import type { SVGAttributes } from "svelte/elements"; import { get } from "svelte/store"; @@ -145,90 +145,47 @@ function handleRevealDrawerCancel() { function handleRevealDrawerOpenChange(value: boolean) { setRevealRequestOpen(value); } - -{#if $cameraPermissionDenied} -
-{/if} +function handlePermissionGoBack() { + goto("/main"); +} +
- {#if $cameraPermissionDenied} -
- - - - - -

- Camera Access Required -

- -

- To scan QR codes, please grant camera permission in your device settings. -

- -
- - Open Settings - - - goto("/main")} - > - Go Back - -
-
- {:else} - - - - - - - -

- Point the camera at the code -

- {/if} + + + + + + + +

+ Point the camera at the code +

+ + import { PUBLIC_PROVISIONER_URL } from "$env/static/public"; -import { ButtonAction } from "$lib/ui"; +import { ButtonAction, CameraPermissionDialog } from "$lib/ui"; +import { createCameraPermissionManager } from "$lib/utils"; import axios from "axios"; import { onMount } from "svelte"; -import { writable } from "svelte/store"; +import { get, writable } from "svelte/store"; import { DocBack, DocFront, @@ -24,20 +25,10 @@ let image2Captured = writable(false); let loading = false; let stream: MediaStream; -async function ensureCameraPermission() { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - video: true, - }); - // Stop immediately after granting - for (const track of stream.getTracks()) { - track.stop(); - } - } catch (err) { - console.error("Camera permission denied:", err); - throw err; - } -} +// Camera permission management +const cameraPermission = createCameraPermissionManager(); +const { permissionState, checkAndRequestPermission, openSettings } = cameraPermission; +let showPermissionDialog = $state(false); async function hasTorch(track: MediaStreamTrack) { try { @@ -92,18 +83,34 @@ async function getMainCameraStream() { throw err; } } + async function requestCameraPermission() { + // First check native permissions via Tauri + const hasPermission = await checkAndRequestPermission(); + + if (!hasPermission) { + permissionGranted.set(false); + showPermissionDialog = true; + return; + } + + // Now get the camera stream try { - await ensureCameraPermission(); stream = await getMainCameraStream(); video.srcObject = stream; video.play(); permissionGranted.set(true); + showPermissionDialog = false; } catch (err) { permissionGranted.set(false); + showPermissionDialog = true; console.error("Camera permission denied", err); } } + +function handleOpenSettings() { + openSettings(); +} async function captureImage() { if (image === 1) { const context1 = canvas1.getContext("2d"); @@ -437,3 +444,10 @@ onMount(() => {
+ + diff --git a/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/selfie.svelte b/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/selfie.svelte index 62868c39..2df6dc29 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/selfie.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/selfie.svelte @@ -1,6 +1,7 @@