fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: auth refresh - #697

Merged
coodos merged 1 commit into
mainfrom
fix/auth-refresh
Jan 21, 2026
Merged

fix: auth refresh#697
coodos merged 1 commit into
mainfrom
fix/auth-refresh

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 21, 2026

Copy link
Copy Markdown
Member

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.

@coderabbitai

coderabbitaiBot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor
📝 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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❓ InconclusiveThe 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedThe 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✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
watchEventStream(
eventSourceRef.current=watchEventStream(
newURL(data.uri).searchParams.get('session')asstring
);
constgetOfferData=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 }=awaitaxios.get<{uri: string}>(
newURL(
'/api/auth/offer',
process.env.NEXT_PUBLIC_BASE_URL
).toString()
);
setQr(data.uri);
constsession=newURL(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
Copy Markdown
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
Copy Markdown
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 mainJan 21, 2026
4 checks passed
Sign up for freeto 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

2 participants

@sosweetham@coodos