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