Merge pull request #131 from donetick/bugfixes-07-08-2026

Bugfixes 07 08 2026
This commit is contained in:
Mohamad Tarbin
2026-07-08 14:13:30 -04:00
committed by GitHub
7 changed files with 89 additions and 37 deletions

View File

@@ -2,37 +2,20 @@ name: Build validation
on:
push:
branches: [main]
pull_request:
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
- 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

View File

@@ -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.

View File

@@ -61,6 +61,11 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>CFBundleURLTypes</key>

View File

@@ -1,7 +1,7 @@
{
"name": "donetick",
"private": true,
"version": "1.2.4",
"version": "1.2.6",
"type": "module",
"engines": {
"node": ">=20.0.0",

View File

@@ -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
}

View File

@@ -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,
])

View File

@@ -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({