diff --git a/infrastructure/eid-wallet/src-tauri/tauri.conf.json b/infrastructure/eid-wallet/src-tauri/tauri.conf.json
index 49757042..8431a83e 100644
--- a/infrastructure/eid-wallet/src-tauri/tauri.conf.json
+++ b/infrastructure/eid-wallet/src-tauri/tauri.conf.json
@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.5.0",
- "identifier": "com.kodski.eid-wallet",
+ "identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
diff --git a/platforms/blabsy-w3ds-auth-api/src/controllers/AuthController.ts b/platforms/blabsy-w3ds-auth-api/src/controllers/AuthController.ts
index f72a1097..1b3efa89 100644
--- a/platforms/blabsy-w3ds-auth-api/src/controllers/AuthController.ts
+++ b/platforms/blabsy-w3ds-auth-api/src/controllers/AuthController.ts
@@ -31,14 +31,25 @@ export class AuthController {
this.eventEmitter.on(id, handler);
+ // Send heartbeat every 30 seconds to keep connection alive
+ const heartbeatInterval = setInterval(() => {
+ try {
+ res.write(`: heartbeat\n\n`);
+ } catch (error) {
+ clearInterval(heartbeatInterval);
+ }
+ }, 30000);
+
// Handle client disconnect
req.on("close", () => {
+ clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});
req.on("error", (error) => {
console.error("SSE Error:", error);
+ clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});
diff --git a/platforms/blabsy/src/components/login/login-main.tsx b/platforms/blabsy/src/components/login/login-main.tsx
index 69b95e83..06e03d40 100644
--- a/platforms/blabsy/src/components/login/login-main.tsx
+++ b/platforms/blabsy/src/components/login/login-main.tsx
@@ -1,6 +1,6 @@
import QRCode from 'react-qr-code';
import axios from 'axios';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef, useCallback } from 'react';
import { useAuth } from '@lib/context/auth-context';
import { NextImage } from '@components/ui/next-image';
import Image from 'next/image';
@@ -10,8 +10,10 @@ export function LoginMain(): JSX.Element {
const { signInWithCustomToken } = useAuth();
const [qr, setQr] = useState();
const [errorMessage, setErrorMessage] = useState(null);
+ const eventSourceRef = useRef(null);
+ const refreshTimerRef = useRef(null);
- function watchEventStream(id: string): void {
+ const watchEventStream = useCallback((id: string): EventSource => {
const sseUrl = new URL(
`/api/auth/sessions/${id}`,
process.env.NEXT_PUBLIC_BASE_URL
@@ -49,9 +51,20 @@ export function LoginMain(): JSX.Element {
console.error('SSE connection error');
eventSource.close();
};
- }
- const getOfferData = async (): Promise => {
+ return eventSource;
+ }, [signInWithCustomToken]);
+
+ const getOfferData = useCallback(async (): Promise => {
+ // Clean up existing SSE connection
+ if (eventSourceRef.current) {
+ eventSourceRef.current.close();
+ }
+ // Clean up existing refresh timer
+ if (refreshTimerRef.current) {
+ clearTimeout(refreshTimerRef.current);
+ }
+
const { data } = await axios.get<{ uri: string }>(
new URL(
'/api/auth/offer',
@@ -59,10 +72,18 @@ export function LoginMain(): JSX.Element {
).toString()
);
setQr(data.uri);
- watchEventStream(
+ eventSourceRef.current = watchEventStream(
new URL(data.uri).searchParams.get('session') as string
);
- };
+
+ // Set up auto-refresh after 60 seconds
+ refreshTimerRef.current = setTimeout(() => {
+ console.log('Refreshing QR code after 60 seconds');
+ getOfferData().catch((error) =>
+ console.error('Error refreshing QR code:', error)
+ );
+ }, 60000);
+ }, [watchEventStream]);
const getAppStoreLink = () => {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
@@ -106,7 +127,17 @@ export function LoginMain(): JSX.Element {
getOfferData().catch((error) =>
console.error('Error fetching QR code data:', error)
);
- }, []);
+
+ // Cleanup on unmount
+ return () => {
+ if (eventSourceRef.current) {
+ eventSourceRef.current.close();
+ }
+ if (refreshTimerRef.current) {
+ clearTimeout(refreshTimerRef.current);
+ }
+ };
+ }, [getOfferData]);
const handleAutoLogin = async (
ename: string,
@@ -209,8 +240,7 @@ export function LoginMain(): JSX.Element {
The button is valid for 60 seconds
- Please refresh the page if it
- expires
+ It will refresh automatically
@@ -237,8 +267,7 @@ export function LoginMain(): JSX.Element {
The code is valid for 60 seconds
- Please refresh the page if it
- expires
+ It will refresh automatically
diff --git a/platforms/pictique-api/src/controllers/AuthController.ts b/platforms/pictique-api/src/controllers/AuthController.ts
index 5677d589..8ba8712a 100644
--- a/platforms/pictique-api/src/controllers/AuthController.ts
+++ b/platforms/pictique-api/src/controllers/AuthController.ts
@@ -34,14 +34,25 @@ export class AuthController {
this.eventEmitter.on(id, handler);
+ // Send heartbeat every 30 seconds to keep connection alive
+ const heartbeatInterval = setInterval(() => {
+ try {
+ res.write(`: heartbeat\n\n`);
+ } catch (error) {
+ clearInterval(heartbeatInterval);
+ }
+ }, 30000);
+
// Handle client disconnect
req.on("close", () => {
+ clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});
req.on("error", (error) => {
console.error("SSE Error:", error);
+ clearInterval(heartbeatInterval);
this.eventEmitter.off(id, handler);
res.end();
});
diff --git a/platforms/pictique/src/routes/(auth)/auth/+page.svelte b/platforms/pictique/src/routes/(auth)/auth/+page.svelte
index 0b247b8b..382c91be 100644
--- a/platforms/pictique/src/routes/(auth)/auth/+page.svelte
+++ b/platforms/pictique/src/routes/(auth)/auth/+page.svelte
@@ -16,6 +16,8 @@
let qrData = $state('');
let isMobile = $state(false);
let errorMessage = $state(null);
+ let eventSource: EventSource | null = null;
+ let refreshTimer: ReturnType | null = null;
function checkMobile() {
isMobile = window.innerWidth <= 640; // Tailwind's `sm` breakpoint
@@ -69,6 +71,71 @@
}
}
+ function watchEventStream(id: string) {
+ const sseUrl = new URL(`/api/auth/sessions/${id}`, PUBLIC_PICTIQUE_BASE_URL).toString();
+ const newEventSource = new EventSource(sseUrl);
+
+ newEventSource.onopen = () => {
+ console.log('Successfully connected.');
+ errorMessage = null;
+ };
+
+ newEventSource.onmessage = (e) => {
+ const data = JSON.parse(e.data as string);
+
+ // Check for error messages (version mismatch)
+ if (data.error && data.type === 'version_mismatch') {
+ errorMessage =
+ data.message ||
+ 'Your eID Wallet app version is outdated. Please update to continue.';
+ newEventSource.close();
+ return;
+ }
+
+ // Handle successful authentication
+ if (data.user && data.token) {
+ const { user } = data;
+ setAuthId(user.id);
+ const { token } = data;
+ setAuthToken(token);
+ goto('/home');
+ }
+ };
+
+ newEventSource.onerror = () => {
+ console.error('SSE connection error');
+ newEventSource.close();
+ };
+
+ return newEventSource;
+ }
+
+ async function fetchOfferAndSetupSSE() {
+ // Clean up existing SSE connection
+ if (eventSource) {
+ eventSource.close();
+ eventSource = null;
+ }
+ // Clean up existing refresh timer
+ if (refreshTimer) {
+ clearTimeout(refreshTimer);
+ refreshTimer = null;
+ }
+
+ const { data } = await apiClient.get('/api/auth/offer');
+ qrData = data.uri;
+
+ eventSource = watchEventStream(new URL(qrData).searchParams.get('session') as string);
+
+ // Set up auto-refresh after 60 seconds
+ refreshTimer = setTimeout(() => {
+ console.log('Refreshing QR code after 60 seconds');
+ fetchOfferAndSetupSSE().catch((error) =>
+ console.error('Error refreshing QR code:', error)
+ );
+ }, 60000);
+ }
+
onMount(async () => {
checkMobile();
window.addEventListener('resize', checkMobile);
@@ -90,51 +157,17 @@
}
// If no query params, proceed with normal flow
- const { data } = await apiClient.get('/api/auth/offer');
- qrData = data.uri;
-
- function watchEventStream(id: string) {
- const sseUrl = new URL(`/api/auth/sessions/${id}`, PUBLIC_PICTIQUE_BASE_URL).toString();
- const eventSource = new EventSource(sseUrl);
-
- eventSource.onopen = () => {
- console.log('Successfully connected.');
- errorMessage = null;
- };
-
- eventSource.onmessage = (e) => {
- const data = JSON.parse(e.data as string);
-
- // Check for error messages (version mismatch)
- if (data.error && data.type === 'version_mismatch') {
- errorMessage =
- data.message ||
- 'Your eID Wallet app version is outdated. Please update to continue.';
- eventSource.close();
- return;
- }
-
- // Handle successful authentication
- if (data.user && data.token) {
- const { user } = data;
- setAuthId(user.id);
- const { token } = data;
- setAuthToken(token);
- goto('/home');
- }
- };
+ await fetchOfferAndSetupSSE();
+ });
- eventSource.onerror = () => {
- console.error('SSE connection error');
- eventSource.close();
- };
+ onDestroy(() => {
+ window.removeEventListener('resize', checkMobile);
+ if (eventSource) {
+ eventSource.close();
+ }
+ if (refreshTimer) {
+ clearTimeout(refreshTimer);
}
-
- watchEventStream(new URL(qrData).searchParams.get('session') as string);
-
- onDestroy(() => {
- window.removeEventListener('resize', checkMobile);
- });
});
@@ -211,8 +244,7 @@
The {isMobileDevice() ? 'button' : 'code'} is valid for 60 seconds
- Please refresh the page if it expires
+ It will refresh automatically