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
86 changes: 51 additions & 35 deletions platforms/eVoting/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardHeader } from "@/components/ui/card";
import { useRouter } from "next/navigation";
import QRCode from "qrcode.react";
import { useAuth } from "@/lib/auth-context";
import { setAuthToken, setAuthId } from "@/lib/authUtils";
import { isMobileDevice, getDeepLinkUrl } from "@/lib/utils/mobile-detection";

export default function LoginPage() {
const router = useRouter();
const { login } = useAuth();
const [qrData, setQrData] = useState<string | null>(null);
const [sessionId, setSessionId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isMobile, setIsMobile] = useState(false);

useEffect(() => {
setIsMobile(isMobileDevice());
}, []);
Comment on lines +17 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding window resize listener for responsive behavior.

Unlike the Pictique implementation, this component doesn't listen for window resize events. Users who rotate their device or resize their browser window won't see the UI update accordingly.

Add a resize event listener to update mobile state dynamically:

 useEffect(() => {
   setIsMobile(isMobileDevice());
+  
+  const handleResize = () => {
+    setIsMobile(isMobileDevice());
+  };
+  
+  window.addEventListener('resize', handleResize);
+  return () => window.removeEventListener('resize', handleResize);
 }, []);
🤖 Prompt for AI Agents
In platforms/eVoting/src/app/(auth)/login/page.tsx around lines 17 to 19, the
useEffect only sets isMobile once and doesn't respond to window resizes; add a
window resize listener inside the same useEffect that recalculates and calls
setIsMobile(isMobileDevice()) on resize, remove the listener in the effect
cleanup to avoid leaks, and optionally debounce the handler to reduce frequent
state updates.


useEffect(() => {
const fetchQRCode = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_EVOTING_BASE_URL}/api/auth/offer`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_EVOTING_BASE_URL}/api/auth/offer`,
{ method: "GET", headers: { "Content-Type": "application/json" } }
);

if (!response.ok) {
throw new Error("Failed to fetch QR code");
Expand All @@ -33,7 +34,7 @@ export default function LoginPage() {
setQrData(data.offer);
setSessionId(data.sessionId);
setIsLoading(false);
} catch (err) {
} catch {
setError("Failed to load QR code");
setIsLoading(false);
}
Expand All @@ -54,59 +55,69 @@ export default function LoginPage() {
if (data.token && data.user) {
setAuthToken(data.token);
setAuthId(data.user.id);
// Reload to trigger auth initialization
window.location.href = '/';
window.location.href = "/";
}
};

eventSource.onerror = () => {
eventSource.close();
};

return () => {
eventSource.close();
};
return () => eventSource.close();
}, [sessionId, login]);

return (
<div className="modal-container flex flex-col items-center justify-center gap-4 safe-area-top safe-area-bottom min-h-screen px-4 pb-safe">
<div className="flex flex-col items-center justify-center gap-4 min-h-screen px-4 pb-safe">
{/* Logo + Tagline */}
<div className="flex flex-col items-center text-center gap-4">
<div className="flex items-center gap-2 text-2xl font-bold">
<img src="/Logo.png" alt="eVoting Logo" className="h-12" />
eVoting
</div>
<p className="text-2xl">Secure voting in the W3DS</p>
<p className="text-lg sm:text-2xl">Secure voting in the W3DS</p>
</div>
<Card className="flex flex-col items-center gap-1 w-full max-w-md p-4 pt-2 mx-4">
<CardHeader className="text-foreground text-2xl font-black">

{/* Main Card */}
<Card className="flex flex-col items-center gap-4 w-full max-w-md p-4 pt-2 mx-4">
<CardHeader className="text-foreground text-xl sm:text-2xl font-black text-center">
Welcome to eVoting
</CardHeader>
<div className="flex flex-col gap-4 text-muted-foreground items-center">
<div className="flex justify-center items-center text-xl space-x-1 whitespace-nowrap">
<span>Scan the QR using your</span>
<span className="font-bold underline">eID App</span>
<span>to login</span>

<div className="flex flex-col gap-4 text-muted-foreground items-center text-center">
{/* Dynamic heading text */}
<div className="text-lg sm:text-xl space-x-1">
{isMobile ? (
<>
<span>Click the button below using your</span>
<span className="font-bold underline">eID App</span>
<span>to login</span>
</>
) : (
<>
<span>Scan the QR using your</span>
<span className="font-bold underline">eID App</span>
<span>to login</span>
</>
)}
</div>
{error && (
<div className="w-full text-red-500 text-center">
{error}
</div>
)}

{error && <div className="w-full text-red-500">{error}</div>}

{isLoading ? (
<div className="w-48 h-48 bg-gray-100 rounded-lg flex items-center justify-center">
<div className="text-gray-500">Loading QR Code...</div>
</div>
) : qrData ? (
<>
{isMobileDevice() ? (
{isMobile ? (
<div className="flex flex-col gap-4 items-center">
<a
href={getDeepLinkUrl(qrData)}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify getDeepLinkUrl implementation.

The getDeepLinkUrl function currently just returns the input without transformation. This may not provide the correct deep link format for mobile wallets.


🌐 Web query:

What is the standard deep link URL format for opening authentication URIs in mobile wallet applications?

💡 Result:

Short answer: there isn’t one universal URL format — mobile wallets use either (A) an app-specific custom URI scheme or (B) an HTTPS universal link that the wallet claims — and the common pattern is:

  • ://wc?uri=<URL‑ENCODED_AUTH_URI>
    or
  • https:///wc?uri=<URL‑ENCODED_AUTH_URI>

Where the embedded auth/connection URI (e.g. a WalletConnect "wc:..." URI or an OAuth redirect) must be percent-encoded. Wallets (and dapps) commonly use that pattern for WalletConnect-style session/auth requests; iOS prefers claimed HTTPS (universal) links for better UX/security. (alpha-docs.walletconnect.com, reddit.com)

Examples

Best practices

If you tell me which wallet/protocol you’re targeting (MetaMask, WalletConnect v1/v2, Phantom, Rainbow, OAuth, Solana, Ethereum EIP‑681, etc.), I can give an exact example URL formatted for that wallet and note any encoding/redirect parameters required.

Citations:


Implement proper deep-link formatting in getDeepLinkUrl

The helper currently returns the raw qrData URI, but mobile wallets expect a percent-encoded URI wrapped in either a custom scheme or a claimed HTTPS link. Please update getDeepLinkUrl to:

  • Percent-encode the embedded auth URI:
    const encoded = encodeURIComponent(qrData)
  • Prefix with the wallet’s scheme or universal link:
    • Custom scheme (e.g. MetaMask/WalletConnect):
    metamask://wc?uri=${encoded}
    • HTTPS universal link (recommended on iOS/Android):
    https://example.wallet/wc?uri=${encoded}
  • Match the exact path and query param names from your target wallet’s docs (MetaMask, WalletConnect v2, Phantom, etc.)

Action items:

  • In platforms/eVoting/src/app/(auth)/login/page.tsx (or wherever getDeepLinkUrl is defined), replace the stub with logic that builds and returns a properly formatted deep link.
  • If you need to support multiple wallets, consider adding a wallet parameter or separate helpers for each format.
  • Add or update unit tests to verify the output URL for your chosen wallet protocols.
🤖 Prompt for AI Agents
In platforms/eVoting/src/app/(auth)/login/page.tsx around line 115, replace the
current stub that returns raw qrData with a helper that percent-encodes the
embedded auth URI (const encoded = encodeURIComponent(qrData)) and then prefixes
it with the chosen wallet deep-link format (e.g. for MetaMask/WalletConnect use
metamask://wc?uri=${encoded} or use an HTTPS universal link like
https://example.wallet/wc?uri=${encoded}); ensure the path and query parameter
names exactly match the target wallet docs, add an optional wallet parameter or
separate helpers if you must support multiple wallets, and add/update unit tests
to assert the expected deep-link outputs for each supported wallet format.

className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-center"
>
Login with eID Wallet
</a>
<div className="text-xs text-gray-500 text-center max-w-xs">
<div className="text-xs text-gray-500 max-w-xs">
Click the button to open your eID wallet app
</div>
</div>
Expand All @@ -126,21 +137,26 @@ export default function LoginPage() {
<div className="text-gray-500">QR Code not available</div>
</div>
)}
<span className="flex flex-col gap-2 items-center">

{/* Expiry Note */}
<div>
<p className="font-bold text-md">
The code is only valid for 60 seconds
</p>
<p>Please refresh the page if it expires</p>
</span>
<div className="bg-muted-foreground/20 p-4 rounded-md text-center">
You are entering eVoting - a voting platform built on
the Web 3.0 Data Space (WDS) architecture. This system
</div>

{/* Info Box */}
<div className="bg-muted-foreground/20 p-4 rounded-md">
You are entering eVoting — a voting platform built on
the Web 3.0 Data Space (W3DS) architecture. This system
is designed around the principle of data-platform
separation, where all your personal content is stored in
your own sovereign eVault, not on centralised servers.
</div>
</div>
</Card>

<img src="/W3DS.svg" alt="w3ds Logo" className="max-h-8" />
</div>
);
Expand Down
95 changes: 61 additions & 34 deletions platforms/pictique/src/routes/(auth)/auth/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@
import { Qr } from '$lib/ui';
import { apiClient, setAuthId, setAuthToken } from '$lib/utils';
import { onMount } from 'svelte';
import { onDestroy } from 'svelte';
import { qrcode } from 'svelte-qrcode-action';
let qrData: string;
let isMobile = false;
function checkMobile() {
isMobile = window.innerWidth <= 640; // Tailwind's `sm` breakpoint
}
onMount(async () => {
checkMobile();
window.addEventListener('resize', checkMobile);
const { data } = await apiClient.get('/api/auth/offer');
qrData = data.uri;
Expand All @@ -32,60 +41,78 @@
}
watchEventStream(new URL(qrData).searchParams.get('session') as string);
onDestroy(() => {
window.removeEventListener('resize', checkMobile);
});
Comment on lines +45 to +47
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

Incorrect placement of onDestroy call.

The onDestroy call is nested inside onMount, which is incorrect. Svelte lifecycle hooks should be called at the top level of the component.

Move the onDestroy call outside of onMount:

 watchEventStream(new URL(qrData).searchParams.get('session') as string);
-
- onDestroy(() => {
-   window.removeEventListener('resize', checkMobile);
- });
 });
+
+onDestroy(() => {
+  window.removeEventListener('resize', checkMobile);
+});
</script>
🤖 Prompt for AI Agents
In platforms/pictique/src/routes/(auth)/auth/+page.svelte around lines 45 to 47,
the onDestroy call is incorrectly nested inside onMount; move the onDestroy(()
=> window.removeEventListener('resize', checkMobile')) call out of the onMount
block so Svelte lifecycle hooks are invoked at the component top level, keeping
the addEventListener in onMount and the corresponding removeEventListener in a
sibling onDestroy to ensure proper cleanup.

});
</script>

<div
class="align-center flex h-full w-full flex-col content-center items-center
justify-center"
>
<div class="mb-5 flex flex-col items-center gap-2">
<div class="flex h-full w-full flex-col items-center justify-center p-4">
<div class="mb-5 flex flex-col items-center gap-2 text-center">
<img src="/images/Logo.svg" alt="logo" class="w-30" />
<p>Connect Socially in the Metastate</p>
</div>

<div
class="h-max-[600px] w-max-[400px] mb-5 flex flex-col items-center gap-5 rounded-xl bg-[#F476481A] p-5"
class="mb-5 flex w-full max-w-[400px] flex-col items-center gap-5 rounded-xl bg-[#F476481A] p-5"
>
<h2>Scan the QR code using your <b><u>eID App</u></b> to login</h2>
{#if qrData}
<article
class="overflow-hidden rounded-2xl"
use:qrcode={{
data: qrData,
width: 250,
height: 250,
margin: 12,
type: 'canvas',
dotsOptions: {
type: 'rounded',
color: '#fff'
},
backgroundOptions: {
gradient: {
type: 'linear',
rotation: 50,
colorStops: [
{ offset: 0, color: '#4D44EF' },
{ offset: 0.65, color: '#F35B5B' },
{ offset: 1, color: '#F7A428' }
]
{#if isMobile}
<h2 class="text-center">
Click the button below to login using your <b><u>eID App</u></b>
</h2>
<a
href={qrData}
class="w-full rounded-lg bg-[#4D44EF] px-4 py-3 text-center font-semibold text-white transition hover:opacity-90"
>
Login with eID Wallet
</a>
{:else}
<h2 class="text-center">
Scan the QR code using your <b><u>eID App</u></b> to login
</h2>
<article
class="overflow-hidden rounded-2xl"
use:qrcode={{
data: qrData,
width: 250,
height: 250,
margin: 12,
type: 'canvas',
dotsOptions: {
type: 'rounded',
color: '#fff'
},
backgroundOptions: {
gradient: {
type: 'linear',
rotation: 50,
colorStops: [
{ offset: 0, color: '#4D44EF' },
{ offset: 0.65, color: '#F35B5B' },
{ offset: 1, color: '#F7A428' }
]
}
}
}
}}
></article>
<a href={qrData}>{qrData}</a>
}}
></article>
{/if}
{/if}
<p>

<p class="text-center">
<span class="mb-1 block font-bold text-gray-600">The code is valid for 60 seconds</span>
<span class="block font-light text-gray-600">Please refresh the page if it expires</span
>
</p>
<p class="w-[350px] bg-white/60 p-4 leading-4 text-black/60">

<p class="w-full rounded-md bg-white/60 p-4 text-sm leading-4 text-black/60">
You are entering Pictique - a social network built on the Web 3.0 Data Space (W3DS)
architecture. This system is designed around the principle of data-platform separation,
where all your personal content is stored in your own sovereign eVault, not on
centralised servers.
</p>
</div>

<W3dslogo />
</div>
Loading