{feedFieldErrors.form}
+ {manualRetryStrategy && ( +Subscribe to this URL in your RSS reader.
+ {result.retry && ( ++ {`Retried automatically with ${result.retry.to} after ${result.retry.from} could not finish the page.`} +
+ )}Preview
+Latest items from this feed
+Loading preview…
+Preview
diff --git a/frontend/src/hooks/useAccessToken.ts b/frontend/src/hooks/useAccessToken.ts index 01f022cc..eff39b28 100644 --- a/frontend/src/hooks/useAccessToken.ts +++ b/frontend/src/hooks/useAccessToken.ts @@ -31,9 +31,23 @@ const resolveStorage = (): Storage => { if (typeof window === 'undefined') return memoryStorage; try { - return window.sessionStorage ?? memoryStorage; + return window.localStorage ?? window.sessionStorage ?? memoryStorage; } catch { - return memoryStorage; + try { + return window.sessionStorage ?? memoryStorage; + } catch { + return memoryStorage; + } + } +}; + +const clearLegacySessionToken = () => { + if (typeof window === 'undefined') return; + + try { + window.sessionStorage?.removeItem(ACCESS_TOKEN_KEY); + } catch { + // Ignore restricted sessionStorage access (privacy mode, sandboxed contexts). } }; @@ -49,9 +63,23 @@ export function useAccessToken() { try { const token = storage.getItem(ACCESS_TOKEN_KEY)?.trim() ?? ''; + let legacyToken = ''; + if (!token && typeof window !== 'undefined') { + try { + legacyToken = window.sessionStorage?.getItem(ACCESS_TOKEN_KEY)?.trim() ?? ''; + } catch { + // Treat restricted sessionStorage access as no legacy token. + legacyToken = ''; + } + } + + if (!token && legacyToken) { + storage.setItem(ACCESS_TOKEN_KEY, legacyToken); + clearLegacySessionToken(); + } setState({ - token: token || null, + token: token || legacyToken || null, isLoading: false, error: null, }); @@ -70,6 +98,7 @@ export function useAccessToken() { const storage = resolveStorage(); storage.setItem(ACCESS_TOKEN_KEY, normalized); + clearLegacySessionToken(); setState({ token: normalized, @@ -81,6 +110,7 @@ export function useAccessToken() { const clearToken = () => { const storage = resolveStorage(); storage.removeItem(ACCESS_TOKEN_KEY); + clearLegacySessionToken(); setState({ token: null, diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index a3e1ddfc..197f50ac 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -38,9 +38,13 @@ const resolveStorage = (): Storage => { } try { - return window.sessionStorage ?? memoryStorage; + return window.localStorage ?? window.sessionStorage ?? memoryStorage; } catch (error) { - return memoryStorage; + try { + return window.sessionStorage ?? memoryStorage; + } catch { + return memoryStorage; + } } }; diff --git a/frontend/src/hooks/useFeedConversion.ts b/frontend/src/hooks/useFeedConversion.ts index aeab5a3e..3ff52d31 100644 --- a/frontend/src/hooks/useFeedConversion.ts +++ b/frontend/src/hooks/useFeedConversion.ts @@ -1,7 +1,8 @@ -import { useState } from 'preact/hooks'; +import { useRef, useState } from 'preact/hooks'; import { createFeed } from '../api/generated'; import { apiClient } from '../api/client'; import type { CreatedFeedResult, FeedPreviewItem, FeedRecord } from '../api/contracts'; +import { normalizeUserUrl } from '../utils/url'; interface JsonFeedItem { title?: string; @@ -22,7 +23,13 @@ interface ConversionState { error: string | null; } +interface ConversionError extends Error { + manualRetryStrategy?: string; + autoRetryAttempted?: boolean; +} + export function useFeedConversion() { + const requestIdRef = useRef(0); const [state, setState] = useState