Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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";
Expand All @@ -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 = (
Expand All @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts">
import { ButtonAction, Drawer } from "$lib/ui";

interface CameraPermissionDialogProps {
isOpen: boolean;
onOpenSettings: () => void;
onGoBack?: () => void;
onOpenChange?: (value: boolean) => void;
title?: string;
description?: string;
dismissible?: boolean;
}

let {
isOpen = $bindable(false),
onOpenSettings,
onGoBack,
onOpenChange,
title = "Camera Access Required",
description = "To continue, please grant camera permission in your device settings.",
dismissible = false,
}: CameraPermissionDialogProps = $props();

function handleSwipe(value: boolean | undefined) {
// Only allow swipe to close when dismissible is true and onOpenChange is provided
if (!dismissible || !onOpenChange) {
return;
}
if (value) {
isOpen = false;
onOpenChange(false);
}
}
</script>

<Drawer
isPaneOpen={isOpen}
handleSwipe={dismissible && onOpenChange ? handleSwipe : undefined}
dismissible={dismissible}
>
<div class="flex flex-col items-center text-center pb-4">
<!-- Camera icon with slash -->
<svg
class="mx-auto mb-6"
width="80"
height="80"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="text-gray-700"
/>
<path
d="M3 3l18 18"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
class="text-gray-700"
/>
</svg>

<h4 class="font-semibold text-xl mb-3 text-gray-900">
{title}
</h4>

<p class="text-gray-600 text-sm mb-6 max-w-xs">
{description}
</p>

<div class="flex flex-col gap-3 w-full">
<ButtonAction
variant="solid"
callback={onOpenSettings}
class="w-full"
>
Open Settings
</ButtonAction>

{#if onGoBack}
<ButtonAction
variant="soft"
callback={onGoBack}
class="w-full"
>
Go Back
</ButtonAction>
{:else}
<!-- Spacer to maintain consistent bottom spacing -->
<div class="h-10"></div>
{/if}
</div>
</div>
</Drawer>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as CameraPermissionDialog } from "./CameraPermissionDialog.svelte";
1 change: 1 addition & 0 deletions infrastructure/eid-wallet/src/lib/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
120 changes: 120 additions & 0 deletions infrastructure/eid-wallet/src/lib/utils/cameraPermission.ts
Original file line number Diff line number Diff line change
@@ -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<CameraPermissionState>;
checkAndRequestPermission: () => Promise<boolean>;
retryPermission: () => Promise<boolean>;
openSettings: () => Promise<void>;
}

/**
* 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<CameraPermissionState>({
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<boolean> {
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 = permissions === "denied";

permissionState.set({
status: permissions,
isDenied,
isGranted,
isChecking: false,
});

if (!isGranted) {
console.warn("Camera permission not granted:", permissions);
}

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<boolean> {
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<void> {
await openAppSettings();
}

return {
permissionState,
checkAndRequestPermission,
retryPermission,
openSettings,
};
}
1 change: 1 addition & 0 deletions infrastructure/eid-wallet/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./mergeClasses";
export * from "./clickOutside";
export * from "./capitalize";
export * from "./swipeGesture";
export * from "./cameraPermission";
16 changes: 16 additions & 0 deletions infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -40,6 +41,7 @@ const {
authError,
signingError,
authLoading,
cameraPermissionDenied,
} = stores;

const {
Expand All @@ -56,6 +58,8 @@ const {
handleBlindVoteSelection,
handleSignVote,
initialize,
retryPermission,
handleOpenSettings,
} = actions;

const pathProps: SVGAttributes<SVGPathElement> = {
Expand Down Expand Up @@ -141,6 +145,10 @@ function handleRevealDrawerCancel() {
function handleRevealDrawerOpenChange(value: boolean) {
setRevealRequestOpen(value);
}

function handlePermissionGoBack() {
goto("/main");
}
</script>

<AppNav title="Scan QR Code" titleClasses="text-white" iconColor="white" />
Expand Down Expand Up @@ -170,6 +178,14 @@ function handleRevealDrawerOpenChange(value: boolean) {
</h4>
</div>

<CameraPermissionDialog
isOpen={$cameraPermissionDenied}
onOpenSettings={handleOpenSettings}
onGoBack={handlePermissionGoBack}
title="Camera Access Required"
description="To scan QR codes, please grant camera permission in your device settings."
/>

<AuthDrawer
isOpen={$codeScannedDrawerOpen}
platform={$platform}
Expand Down
Loading