diff --git a/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json b/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json
index 6e6c8369b..6af140f45 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 3a4c3da07..55e11ecde 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/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte b/infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte
new file mode 100644
index 000000000..094c1299c
--- /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 000000000..a695847a4
--- /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 090341243..8619822ef 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 000000000..2ce71d6e0
--- /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 { type Writable, 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 287c0a243..229c49be0 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 b39ff02e5..47e180afc 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, CameraPermissionDialog } from "$lib/ui";
import { getContext, onDestroy, onMount } from "svelte";
import type { SVGAttributes } from "svelte/elements";
import { get } from "svelte/store";
@@ -40,6 +41,7 @@ const {
authError,
signingError,
authLoading,
+ cameraPermissionDenied,
} = stores;
const {
@@ -56,6 +58,8 @@ const {
handleBlindVoteSelection,
handleSignVote,
initialize,
+ retryPermission,
+ handleOpenSettings,
} = actions;
const pathProps: SVGAttributes = {
@@ -141,6 +145,10 @@ function handleRevealDrawerCancel() {
function handleRevealDrawerOpenChange(value: boolean) {
setRevealRequestOpen(value);
}
+
+function handlePermissionGoBack() {
+ goto("/main");
+}
@@ -170,6 +178,14 @@ function handleRevealDrawerOpenChange(value: boolean) {
+
+
;
authLoading: Writable;
isFromScan: Writable;
+ cameraPermissionDenied: Writable;
}
interface ScanActions {
@@ -106,6 +108,8 @@ interface ScanActions {
redirectUri: string | null,
) => Promise;
initialize: () => Promise<() => void>;
+ retryPermission: () => Promise;
+ handleOpenSettings: () => Promise;
}
interface ScanLogic {
@@ -145,10 +149,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 +170,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 +247,31 @@ export function createScanLogic({
scanning.set(false);
}
+ async function retryPermission() {
+ // 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;
@@ -1466,6 +1506,7 @@ export function createScanLogic({
signingError,
authLoading,
isFromScan,
+ cameraPermissionDenied,
},
actions: {
startScan,
@@ -1487,6 +1528,8 @@ export function createScanLogic({
handleDeepLinkData,
handleBlindVotingRequest,
initialize,
+ retryPermission,
+ handleOpenSettings,
},
};
}
diff --git a/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte b/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte
index dd549a210..aaa88f1a9 100644
--- a/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte
+++ b/infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte
@@ -1,9 +1,10 @@