Merge pull request #131 from donetick/bugfixes-07-08-2026
Bugfixes 07 08 2026
This commit is contained in:
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "donetick",
|
||||
"private": true,
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.6",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user