Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<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;
}

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

function handleSwipe(value: boolean | undefined) {
if (value) {
isOpen = false;
onOpenChange?.(false);
}
}
</script>

<Drawer
isPaneOpen={isOpen}
handleSwipe={handleSwipe}
dismissible={false}
>
Comment on lines +22 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "handleSwipe|dismissible" -C3 infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 1076


🏁 Script executed:

rg -n "CupertinoPane|events:" -A5 infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 167


🏁 Script executed:

rg "new CupertinoPane" -A20 infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte | head -30

Repository: MetaState-Prototype-Project/prototype

Length of output: 648


🌐 Web query:

CupertinoPane library bottomClose dismissible behavior swipe events

💡 Result:

Short summary — how bottomClose / dismissible and swipe events work in CupertinoPane:

  • bottomClose: when true, dragging the pane down to the bottom breakpoint will close/dismiss it. [1]
  • fastSwipeClose / fastSwipeSensivity: if enabled, a quick (fast) drag downward will close the pane; sensitivity controls how “fast” must be. [1]
  • lowerThanBottom / upperThanTop / bottomOffset: control whether pane can be dragged past the bottom/top limits and the offset used for bottom-close behavior. [1]
  • preventDismiss(preventFlag): programmatically prevent dismissal (useful together with onWillDismiss to cancel dismiss). [2]
  • Drag/touch options that affect swipe behavior: touchAngle, dragBy, dragByCursor, freeMode, showDraggable, simulateTouch, passiveListeners. These affect which gestures trigger drag and how they’re interpreted. [1]

Events you can listen to for swipe/close flows:

  • onDragStart, onDrag, onDragEnd — get updates during user drags (use these to track swipe progress). [2]
  • onWillDismiss, onDidDismiss — fired before/after a dismiss. Use onWillDismiss together with preventDismiss to cancel. [2]
  • onWillPresent, onDidPresent, onBackdropTap, onTransitionStart, onTransitionEnd — additional lifecycle hooks. [2]

Quick usage patterns:

  • Enable bottom-close: new CupertinoPane('.pane', { bottomClose: true, fastSwipeClose: true, fastSwipeSensivity: 3 });
  • Listen for dismiss/cancel: pane.on('onWillDismiss', (ev) => { /* call pane.preventDismiss(true) to cancel */ });
  • Track swipe progress: pane.on('onDrag', (pos) => { /* pos = current pane position */ });

Sources: CupertinoPane docs (settings and options) and Events / Public Methods docs. [1][2]

References
[1] Cupertino Pane settings (bottomClose, fastSwipeClose, touchAngle, etc.).
[2] Cupertino Pane Events & Public Methods (onDragStart/onDrag/onDragEnd, onWillDismiss/onDidDismiss, preventDismiss).


🏁 Script executed:

rg -n "handleDrawerSwipe|addEventListener|on:" infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 206


🏁 Script executed:

sed -n '30,45p' infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 448


🏁 Script executed:

rg -n "use:swipe" infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 97


🏁 Script executed:

sed -n '88,98p' infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 255


🏁 Script executed:

cat -n infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte | head -50

Repository: MetaState-Prototype-Project/prototype

Length of output: 1896


Remove handleSwipe or guard it based on dismissible and onOpenChange availability.

The use:swipe directive in the Drawer component is unconditional—swipe events fire even when dismissible={false}. In CameraPermissionDialog, this means a user can swipe to set isOpen = false locally while the parent remains unaware (if onOpenChange is undefined), causing state desynchronization and bypassing the permission gate.

Suggested fix
<Drawer
    isPaneOpen={isOpen}
-    handleSwipe={handleSwipe}
+    handleSwipe={onOpenChange ? handleSwipe : undefined}
    dismissible={false}
>

Alternatively, only wire handleSwipe when the dialog is actually dismissible:

<Drawer
    isPaneOpen={isOpen}
-    handleSwipe={handleSwipe}
-    dismissible={false}
+    handleSwipe={onOpenChange ? handleSwipe : undefined}
+    dismissible={Boolean(onOpenChange)}
>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function handleSwipe(value: boolean | undefined) {
if (value) {
isOpen = false;
onOpenChange?.(false);
}
}
</script>
<Drawer
isPaneOpen={isOpen}
handleSwipe={handleSwipe}
dismissible={false}
>
function handleSwipe(value: boolean | undefined) {
if (value) {
isOpen = false;
onOpenChange?.(false);
}
}
</script>
<Drawer
isPaneOpen={isOpen}
handleSwipe={onOpenChange ? handleSwipe : undefined}
dismissible={false}
>
🤖 Prompt for AI Agents
In
`@infrastructure/eid-wallet/src/lib/ui/CameraPermissionDialog/CameraPermissionDialog.svelte`
around lines 22 - 34, The handleSwipe function is currently always passed to
<Drawer> allowing local isOpen toggles even when dismissible is false or
onOpenChange is absent; either remove handleSwipe entirely or guard wiring so
swipe only changes state when dismissible is true and onOpenChange exists.
Update the component to conditionally pass handleSwipe (or make handleSwipe
no-op) based on the dismissible prop and presence of onOpenChange, ensuring you
reference the handleSwipe function, the isOpen variable, the onOpenChange
callback, and the Drawer component when making the change.

<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 = !isGranted;

permissionState.set({
status: permissions,
isDenied,
isGranted,
isChecking: false,
});
Comment on lines +66 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n infrastructure/eid-wallet/src/lib/utils/cameraPermission.ts

Repository: MetaState-Prototype-Project/prototype

Length of output: 4288


🏁 Script executed:

rg "isDenied" infrastructure/eid-wallet/src --type ts --type tsx -B 2 -A 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 167


🏁 Script executed:

rg "isDenied" infrastructure/eid-wallet/src --max-count 20

Repository: MetaState-Prototype-Project/prototype

Length of output: 600


🏁 Script executed:

rg "permissionState" infrastructure/eid-wallet/src --max-count 30

Repository: MetaState-Prototype-Project/prototype

Length of output: 914


🏁 Script executed:

cat -n infrastructure/eid-wallet/src/routes/\(auth\)/verify/steps/selfie.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 5982


🏁 Script executed:

cat -n infrastructure/eid-wallet/src/routes/\(auth\)/verify/steps/passport.svelte

Repository: MetaState-Prototype-Project/prototype

Length of output: 20862


Fix isDenied to be semantically accurate.

The computed isDenied flag conflates "denied" with other non-granted states ("prompt", null/error). While this doesn't currently cause routing issues (since retryPermission() explicitly checks permissions === "denied"), the flag's inclusion in the state type is misleading. Update it to accurately reflect only the denied state.

Suggested fix
-const isGranted = permissions === "granted";
-const isDenied = !isGranted;
+const isGranted = permissions === "granted";
+const isDenied = permissions === "denied";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isGranted = permissions === "granted";
const isDenied = !isGranted;
permissionState.set({
status: permissions,
isDenied,
isGranted,
isChecking: false,
});
const isGranted = permissions === "granted";
const isDenied = permissions === "denied";
permissionState.set({
status: permissions,
isDenied,
isGranted,
isChecking: false,
});
🤖 Prompt for AI Agents
In `@infrastructure/eid-wallet/src/lib/utils/cameraPermission.ts` around lines 66
- 74, The computed isDenied boolean is currently set to !isGranted which
incorrectly treats non-granted states like "prompt" as denied; change the
calculation to isDenied = permissions === "denied" and update the
permissionState.set call to use that value (keeping status, isGranted, isDenied,
isChecking fields) so the stored state accurately represents the denied state;
also update any type/signature for the permission state if necessary and
run/adjust any code or tests that reference permissionState.isDenied (e.g.,
retryPermission) to ensure behavior remains correct.


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