From 181156de102d691f9c5632d07fb2059a54ff41c5 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 8 Jul 2026 12:46:42 -0400 Subject: [PATCH 1/5] fix: prevent infinite reload loop in OAuth deep link handling and clean up comments in LoginView --- src/CapacitorListener.js | 15 +++++++++++++-- src/views/Authorization/LoginView.jsx | 1 - 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/CapacitorListener.js b/src/CapacitorListener.js index c8fdf21..8150569 100644 --- a/src/CapacitorListener.js +++ b/src/CapacitorListener.js @@ -48,6 +48,16 @@ const handleOAuthDeepLink = async url => { const state = urlObj.searchParams.get('state') if (code && state) { + // getLaunchUrl() persists across every WebView reload caused by + // window.location.href. If we're already on the OAuth handler page with + // the same code, skip re-navigating to avoid an infinite reload loop. + const currentCode = new URLSearchParams(window.location.search).get( + 'code', + ) + if (window.location.pathname === '/auth/oauth2' && currentCode === code) { + return + } + // Store the OAuth params for the app to pick up await Preferences.set({ key: 'oauth_callback', @@ -258,7 +268,7 @@ const registerCapacitorListeners = () => { console.log('[NFC] appUrlOpen:', event.url) handleUrlOpen(event.url) }) - + mobileApp.addListener('appStateChange', ({ isActive }) => { focusManager.setFocused(isActive) }) @@ -277,5 +287,6 @@ const registerCapacitorListeners = () => { export { registerCapacitorListeners, - pushNotificationListenerRegistration as registerPushNotifications, + pushNotificationListenerRegistration as registerPushNotifications } + diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx index 108ff14..48a3a92 100644 --- a/src/views/Authorization/LoginView.jsx +++ b/src/views/Authorization/LoginView.jsx @@ -310,7 +310,6 @@ const LoginView = () => { const state = generateRandomState() if (Capacitor.isNativePlatform()) { - // For mobile devices, use a custom URL scheme for the redirect const redirectUri = 'donetick://auth/oauth2' const params = new URLSearchParams({ From f8d772149b8e3cbdbfc773692b5c88afff56219a Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 8 Jul 2026 13:56:10 -0400 Subject: [PATCH 2/5] fix: enhance SSE connection handling for native platforms and implement ticket fetching --- src/hooks/useSSE.js | 76 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/src/hooks/useSSE.js b/src/hooks/useSSE.js index 8a63f20..1653b63 100644 --- a/src/hooks/useSSE.js +++ b/src/hooks/useSSE.js @@ -1,3 +1,4 @@ +import { Capacitor } from '@capacitor/core' import { useQueryClient } from '@tanstack/react-query' import { EventSourcePolyfill } from 'event-source-polyfill' import { useCallback, useEffect, useRef, useState } from 'react' @@ -60,6 +61,24 @@ export const useSSE = () => { return { url: sseUrl, token } }, [token, isAuthenticated]) // Fixed: Added missing dependencies + // Exchange the JWT (sent via Authorization header by apiClient) for a + // short-lived, single-use SSE ticket. Used by the native EventSource path, + // which cannot send custom headers. + const fetchSSETicket = useCallback(async () => { + try { + const response = await apiClient.get('/realtime/sse/ticket') + if (!response || !response.ok) { + console.error('SSE: Ticket request failed', response?.status) + return null + } + const data = await response.json() + return data.ticket || null + } catch (err) { + console.error('SSE: Ticket request error', err) + return null + } + }, []) + const handleSSEMessage = useCallback( event => { try { @@ -289,7 +308,7 @@ export const useSSE = () => { }, []) // Create connect function that can be called from anywhere - const connect = useCallback(() => { + const connect = useCallback(async () => { // Clear the scheduled flag when actually connecting isReconnectScheduledRef.current = false @@ -348,16 +367,49 @@ export const useSSE = () => { setConnectionState(SSE_STATES.CONNECTING) isManuallyClosedRef.current = false - eventSourceRef.current = new EventSourcePolyfill(sseConfig.url, { - headers: { - Authorization: `Bearer ${localStorage.getItem('token')}`, - 'Cache-Control': 'no-cache', - Accept: 'text/event-stream', - }, - withCredentials: true, - heartbeatTimeout: 120000, - silentTimeoutRetry: true, - }) + if (Capacitor.isNativePlatform()) { + // Capacitor's native HTTP bridge does not support streaming responses, + // which breaks EventSourcePolyfill (fetch/XHR based). Use the native + // EventSource instead, which uses the WKWebView HTTP stack directly. + // Native EventSource cannot send custom headers, so we first exchange + // our JWT (sent in the Authorization header) for a short-lived, + // single-use ticket and pass that ticket as a query parameter. This + // keeps the long-lived token out of URLs and proxy access logs. + const ticket = await fetchSSETicket() + if (!ticket) { + console.error('SSE: Failed to obtain connection ticket') + setError('Connection error occurred') + setConnectionState(SSE_STATES.CLOSED) + scheduleReconnect( + RECONNECT_INTERVALS[ + Math.min( + reconnectAttemptsRef.current, + RECONNECT_INTERVALS.length - 1, + ) + ], + 'ticket-fetch-failed', + ) + return + } + + const nativeUrl = new URL(sseConfig.url) + nativeUrl.searchParams.set('ticket', ticket) + + eventSourceRef.current = new EventSource(nativeUrl.toString(), { + withCredentials: true, + }) + } else { + eventSourceRef.current = new EventSourcePolyfill(sseConfig.url, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + 'Cache-Control': 'no-cache', + Accept: 'text/event-stream', + }, + withCredentials: true, + heartbeatTimeout: 120000, + silentTimeoutRetry: true, + }) + } eventSourceRef.current.onopen = () => { console.log('SSE connection opened') @@ -573,8 +625,10 @@ export const useSSE = () => { } }, [ getSSEUrl, + fetchSSETicket, handleSSEMessage, stopHeartbeatMonitor, + scheduleReconnect, isCircuitBreakerOpen, showError, ]) From 648f76bd22cb976635417d6967b3a59c6c048d05 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 8 Jul 2026 14:06:03 -0400 Subject: [PATCH 3/5] fix: update version codes and add local networking support in iOS Info.plist --- android/app/build.gradle | 4 ++-- ios/App/App/Info.plist | 5 +++++ package.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 729845e..478dbdd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.donetick.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 26 - versionName "1.2.4" + versionCode 27 + versionName "1.2.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 5a58712..276929d 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -61,6 +61,11 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAppTransportSecurity + + NSAllowsLocalNetworking + + UIViewControllerBasedStatusBarAppearance CFBundleURLTypes diff --git a/package.json b/package.json index b73685f..2ae3abc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "donetick", "private": true, - "version": "1.2.4", + "version": "1.2.6", "type": "module", "engines": { "node": ">=20.0.0", From 1ca91d684f789edacd47d1bcb03361f88c64edd5 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 8 Jul 2026 14:08:41 -0400 Subject: [PATCH 4/5] fix: remove redundant lint job from build workflow and ensure main branch is specified for push events --- .github/workflows/build.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a49b178..5925ff7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,7 @@ name: Build validation on: push: + branches: [main] pull_request: jobs: @@ -22,17 +23,3 @@ jobs: - name: Install dependencies run: npm i - run: npm run build - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 22 - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - name: Install dependencies - run: npm i - # Lint currently reporting lots of preexisting issues - # - run: npm run lint From 7889c32be069d9da79617bc922effe0523a40d8b Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 8 Jul 2026 14:13:18 -0400 Subject: [PATCH 5/5] fix: simplify Node.js setup in build workflow by removing version matrix --- .github/workflows/build.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5925ff7..5048ee6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,16 +9,12 @@ jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20.x, 22.x] - steps: - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js 20 uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: 20.x cache: 'npm' - name: Install dependencies run: npm i