Skip to content

Conversation

@sosweetham
Copy link
Member

@sosweetham sosweetham commented Jan 21, 2026

Description of change

adds heartbeat to keep auth sse connection alive and refreshes qr code every 60 seconds on pictique and blabsy

Issue Number

Closes #236

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Partial Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Login QR codes and offerings now auto-refresh automatically every 60 seconds, eliminating the need for manual page refresh.
  • Improvements

    • Enhanced connection stability during authentication flows with keepalive heartbeats for real-time event streams.
    • Updated user messaging to clarify automatic refresh behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@sosweetham sosweetham requested a review from coodos as a code owner January 21, 2026 05:28
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

This PR implements SSE heartbeat mechanisms across two API controllers to prevent connection timeouts, adds automatic QR code refresh every 60 seconds in two frontend login components, and updates the eID wallet identifier to reflect organizational branding.

Changes

Cohort / File(s) Summary
Configuration
infrastructure/eid-wallet/src-tauri/tauri.conf.json
Updated app identifier from com.kodski.eid-wallet to foundation.metastate.eid-wallet
Backend SSE Heartbeat
platforms/blabsy-w3ds-auth-api/src/controllers/AuthController.ts, platforms/pictique-api/src/controllers/AuthController.ts
Added 30-second SSE heartbeat interval to keep connections alive; clears interval on client disconnect or error
Frontend Auto-Refresh (React)
platforms/blabsy/src/components/login/login-main.tsx
Introduced useCallback and useRef hooks for SSE and timer management; refactored offer-fetch flow with auto-refresh scheduling every 60 seconds; added cleanup on unmount; updated UX text to indicate automatic refresh
Frontend Auto-Refresh (Svelte)
platforms/pictique/src/routes/(auth)/auth/+page.svelte
Extracted SSE handling into watchEventStream() and QR-fetch logic into fetchOfferAndSetupSSE() functions; added 60-second auto-refresh timer; added onDestroy cleanup for SSE and timer; updated messaging to reflect automatic refresh behavior

Sequence Diagram

sequenceDiagram
    participant Client as Client (Browser)
    participant API as Auth API
    participant SSE as SSE Connection

    Client->>API: POST /login (fetch offer URI)
    API-->>Client: offer_uri, session_id
    Client->>SSE: new EventSource(session_id)
    activate SSE
    SSE-->>API: connection established
    activate API
    
    loop Every 30 seconds
        API->>SSE: ": heartbeat\n\n"
        SSE-->>Client: heartbeat received
    end
    
    Note over Client: 60-second auto-refresh timer started
    
    par Waiting for auth
        API->>SSE: authentication result
        SSE-->>Client: onmessage event
        Client->>Client: handle auth/error
    and Auto-refresh timeout
        Client->>Client: 60s elapsed
        Client->>API: POST /login (re-fetch offer)
    end
    
    Client->>SSE: close()
    deactivate SSE
    deactivate API
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Fix/final UI fixes #301: Updates platforms/blabsy/src/components/login/login-main.tsx with app-store links and UI text changes related to the same login component.
  • Fix/author details #229: Modifies platforms/pictique/src/routes/(auth)/auth/+page.svelte to store authenticated user ID on SSE messages within the same login flow.
  • feat: health check #437: Updates infrastructure/eid-wallet/src-tauri/tauri.conf.json and vault health-check logic alongside the identifier configuration in this PR.

Suggested reviewers

  • coodos
  • xPathin

Poem

🐰 A heartbeat whispers every half-minute more,
Keeping the SSE connection at the door,
QR codes refresh when sixty seconds elapse,
No more manual taps—just automatic maps!
The wallet's rebranded, the login's alive, 🥕✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The Tauri configuration identifier change appears unrelated to the auth refresh objective; other changes (Tauri config) may be out of scope for issue #236 which focuses on QR refresh. Clarify whether the Tauri identifier change in tauri.conf.json is intentional and related to the PR objectives, or if it should be removed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: auth refresh' is related to the main changes (adding heartbeat and auto-refresh to auth SSE), but is somewhat vague and doesn't clearly convey the specific mechanisms added (heartbeat mechanism and 60-second QR refresh).
Description check ✅ Passed The PR description provides a clear summary of changes, specifies the issue being fixed (#236), marks it as a Fix, and completes all checklist items including testing status.
Linked Issues check ✅ Passed The PR successfully implements automatic QR code refresh every 60 seconds on both Pictique and Blabsy platforms, and adds a heartbeat mechanism to keep SSE connections alive, addressing the core requirement of issue #236.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
platforms/blabsy/src/components/login/login-main.tsx (2)

28-34: Add error handling for JSON parsing.

JSON.parse(e.data) can throw if the server sends malformed data. Wrap in try/catch to prevent unhandled exceptions that could break the message handler.

🛠️ Suggested fix
 eventSource.onmessage = async (e): Promise<void> => {
-    const data = JSON.parse(e.data as string) as {
-        token?: string;
-        error?: boolean;
-        message?: string;
-        type?: string;
-    };
+    let data: {
+        token?: string;
+        error?: boolean;
+        message?: string;
+        type?: string;
+    };
+    try {
+        data = JSON.parse(e.data as string);
+    } catch (parseError) {
+        console.error('Failed to parse SSE message:', parseError);
+        return;
+    }

     if (data.error && data.type === 'version_mismatch') {

107-140: Verify useEffect doesn't re-run unnecessarily.

The useEffect depends on getOfferData (memoized), which depends on watchEventStream (memoized), which depends on signInWithCustomToken from useAuth(). However, signInWithCustomToken in the auth context is not memoized with useCallback and is created fresh on every AuthContextProvider render. This causes watchEventStream to be recreated, which in turn causes getOfferData to be recreated, triggering the useEffect to run on every AuthContextProvider render cycle.

While the cleanup logic prevents resource leaks, this causes unnecessary network requests for the QR code and creates multiple SSE connections. Wrap signInWithCustomToken with useCallback in auth-context.tsx (line 136).

🤖 Fix all issues with AI agents
In `@platforms/blabsy/src/components/login/login-main.tsx`:
- Around line 68-77: Wrap the axios.get call in a try/catch around the block
that calls setQr and assigns eventSourceRef.current so network errors don't
leave the old/closed EventSource in place; on error, log/report the error and
ensure eventSourceRef.current remains closed or is cleaned up. After receiving
data.uri, extract the session value from new
URL(data.uri).searchParams.get('session') and explicitly check for null/empty
before calling watchEventStream; only call watchEventStream(session) and assign
eventSourceRef.current when session is present, otherwise handle the missing
session (error log or user-visible fallback) instead of using the unsafe `as
string` assertion.

In `@platforms/pictique/src/routes/`(auth)/auth/+page.svelte:
- Around line 125-128: Wrap the apiClient.get('/api/auth/offer') call in a
try/catch and only assign qrData and call watchEventStream after a successful
response; on error set a local error state or fallback (so the component doesn't
remain in an inconsistent state). After setting qrData, parse the URL and
validate that new URL(qrData).searchParams.get('session') is non-null and
non-empty before passing it to watchEventStream (do not use an unconditional "as
string" cast); if the session param is missing, handle it by setting an error
state or aborting the eventSource creation instead of calling watchEventStream
with a null value. Ensure references to apiClient.get, qrData, and
watchEventStream/eventSource are updated accordingly.
- Around line 83-93: Wrap the JSON.parse call inside the
newEventSource.onmessage handler in a try/catch to guard against malformed
server data: inside the onmessage callback (newEventSource.onmessage) attempt to
parse e.data with JSON.parse in a try block, and in the catch set/append a
user-facing errorMessage (or log the error) and call newEventSource.close() to
stop the stream and prevent an unhandled exception; ensure subsequent usage of
the parsed variable only occurs if parsing succeeded so the existing
version_mismatch check (data.error && data.type === 'version_mismatch') remains
valid.
🧹 Nitpick comments (1)
platforms/pictique-api/src/controllers/AuthController.ts (1)

20-58: Consider extracting SSE handling to a shared utility.

The sseStream method is nearly identical to the one in blabsy-w3ds-auth-api/src/controllers/AuthController.ts. For maintainability, consider extracting the SSE setup, heartbeat, and cleanup logic into a shared utility that both controllers can use.

Comment on lines 68 to 77
const { data } = await axios.get<{ uri: string }>(
new URL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current = watchEventStream(
new URL(data.uri).searchParams.get('session') as string
);
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

Add error handling and validate session parameter.

Two concerns consistent with the Svelte implementation:

  1. The axios.get call isn't wrapped in try/catch - if it fails, setQr won't be called and eventSourceRef.current remains with the old (closed) value.
  2. The as string assertion on line 76 is unsafe - searchParams.get('session') can return null.
🛠️ Suggested fix
 const getOfferData = useCallback(async (): Promise<void> => {
     // 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',
-            process.env.NEXT_PUBLIC_BASE_URL
-        ).toString()
-    );
-    setQr(data.uri);
-    eventSourceRef.current = watchEventStream(
-        new URL(data.uri).searchParams.get('session') as string
-    );
+    try {
+        const { data } = await axios.get<{ uri: string }>(
+            new URL(
+                '/api/auth/offer',
+                process.env.NEXT_PUBLIC_BASE_URL
+            ).toString()
+        );
+        setQr(data.uri);
+        
+        const session = new URL(data.uri).searchParams.get('session');
+        if (!session) {
+            console.error('Session parameter missing from offer URI');
+            return;
+        }
+        eventSourceRef.current = watchEventStream(session);
+    } catch (error) {
+        console.error('Failed to fetch offer:', error);
+        return;
+    }

     // Set up auto-refresh after 60 seconds
📝 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 { data } = await axios.get<{ uri: string }>(
new URL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current = watchEventStream(
new URL(data.uri).searchParams.get('session') as string
);
const getOfferData = useCallback(async (): Promise<void> => {
// Clean up existing SSE connection
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
// Clean up existing refresh timer
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
try {
const { data } = await axios.get<{ uri: string }>(
new URL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
const session = new URL(data.uri).searchParams.get('session');
if (!session) {
console.error('Session parameter missing from offer URI');
return;
}
eventSourceRef.current = watchEventStream(session);
} catch (error) {
console.error('Failed to fetch offer:', error);
return;
}
// Set up auto-refresh after 60 seconds
🤖 Prompt for AI Agents
In `@platforms/blabsy/src/components/login/login-main.tsx` around lines 68 - 77,
Wrap the axios.get call in a try/catch around the block that calls setQr and
assigns eventSourceRef.current so network errors don't leave the old/closed
EventSource in place; on error, log/report the error and ensure
eventSourceRef.current remains closed or is cleaned up. After receiving
data.uri, extract the session value from new
URL(data.uri).searchParams.get('session') and explicitly check for null/empty
before calling watchEventStream; only call watchEventStream(session) and assign
eventSourceRef.current when session is present, otherwise handle the missing
session (error log or user-visible fallback) instead of using the unsafe `as
string` assertion.

Comment on lines +83 to +93
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;
}
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

Add error handling for JSON parsing.

JSON.parse(e.data) can throw if the server sends malformed data. Wrap in try/catch to prevent unhandled exceptions.

🛠️ Suggested fix
 newEventSource.onmessage = (e) => {
-    const data = JSON.parse(e.data as string);
+    let data;
+    try {
+        data = JSON.parse(e.data as string);
+    } catch (parseError) {
+        console.error('Failed to parse SSE message:', parseError);
+        return;
+    }

     // Check for error messages (version mismatch)
📝 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
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;
}
newEventSource.onmessage = (e) => {
let data;
try {
data = JSON.parse(e.data as string);
} catch (parseError) {
console.error('Failed to parse SSE message:', parseError);
return;
}
// 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;
}
🤖 Prompt for AI Agents
In `@platforms/pictique/src/routes/`(auth)/auth/+page.svelte around lines 83 - 93,
Wrap the JSON.parse call inside the newEventSource.onmessage handler in a
try/catch to guard against malformed server data: inside the onmessage callback
(newEventSource.onmessage) attempt to parse e.data with JSON.parse in a try
block, and in the catch set/append a user-facing errorMessage (or log the error)
and call newEventSource.close() to stop the stream and prevent an unhandled
exception; ensure subsequent usage of the parsed variable only occurs if parsing
succeeded so the existing version_mismatch check (data.error && data.type ===
'version_mismatch') remains valid.

Comment on lines +125 to +128
const { data } = await apiClient.get('/api/auth/offer');
qrData = data.uri;
eventSource = watchEventStream(new URL(qrData).searchParams.get('session') as string);
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

Add error handling and validate session parameter.

Two concerns:

  1. The apiClient.get call isn't wrapped in try/catch - if it fails, the component will be left in an inconsistent state with no QR code.
  2. The as string assertion on line 128 is unsafe - searchParams.get('session') can return null if the session parameter is missing from the URI.
🛠️ Suggested fix
 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);
+    try {
+        const { data } = await apiClient.get('/api/auth/offer');
+        qrData = data.uri;
+
+        const session = new URL(qrData).searchParams.get('session');
+        if (!session) {
+            console.error('Session parameter missing from offer URI');
+            return;
+        }
+        eventSource = watchEventStream(session);
+    } catch (error) {
+        console.error('Failed to fetch offer:', error);
+        return;
+    }

     // Set up auto-refresh after 60 seconds
🤖 Prompt for AI Agents
In `@platforms/pictique/src/routes/`(auth)/auth/+page.svelte around lines 125 -
128, Wrap the apiClient.get('/api/auth/offer') call in a try/catch and only
assign qrData and call watchEventStream after a successful response; on error
set a local error state or fallback (so the component doesn't remain in an
inconsistent state). After setting qrData, parse the URL and validate that new
URL(qrData).searchParams.get('session') is non-null and non-empty before passing
it to watchEventStream (do not use an unconditional "as string" cast); if the
session param is missing, handle it by setting an error state or aborting the
eventSource creation instead of calling watchEventStream with a null value.
Ensure references to apiClient.get, qrData, and watchEventStream/eventSource are
updated accordingly.

@coodos coodos merged commit 55ea233 into main Jan 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] After 60 seconds refresh QR on blabsy & Pictique

3 participants