Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/contexts/ConnectionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
useCallback,
useEffect,
} from "react"
import { getLocalStorageItem } from "../utils/safeLocalStorage"

type ConnectionStatus = "connecting" | "connected" | "disconnected"

Expand Down Expand Up @@ -105,7 +106,7 @@ export function ConnectionProvider({
const token =
typeof window !== "undefined"
? new URLSearchParams(window.location.search).get("token") ||
localStorage.getItem("rein_auth_token")
getLocalStorageItem("rein_auth_token")
: null
fetch("/api/debug/report-latency", {
method: "POST",
Expand Down
4 changes: 2 additions & 2 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@tanstack/react-router"
import { useEffect } from "react"
import { APP_CONFIG, THEMES } from "../config"
import { getLocalStorageItem } from "../utils/safeLocalStorage"
import "../styles.css"
import {
ConnectionProvider,
Expand Down Expand Up @@ -46,8 +47,7 @@ function RootComponent() {

function ThemeInit() {
useEffect(() => {
if (typeof localStorage === "undefined") return
const saved = localStorage.getItem(APP_CONFIG.THEME_STORAGE_KEY)
const saved = getLocalStorageItem(APP_CONFIG.THEME_STORAGE_KEY)
const theme =
saved === THEMES.LIGHT || saved === THEMES.DARK ? saved : THEMES.DEFAULT
document.documentElement.setAttribute("data-theme", theme)
Expand Down
54 changes: 24 additions & 30 deletions src/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { APP_CONFIG, THEMES } from "../config"
import serverConfig from "../server-config.json"
import pkg from "../../package.json"
import { t } from "../utils/i18n"
import {
getLocalStorageItem,
setLocalStorageItem,
} from "../utils/safeLocalStorage"
export const Route = createFileRoute("/settings")({
component: SettingsPage,
})
Expand Down Expand Up @@ -46,43 +50,34 @@ function SettingsPage() {
useEffect(() => {
setFrontendPort(window.location.port)
}, [])
// Client Side Settings (LocalStorage)
// Client Side Settings (LocalStorage) — safe when storage is blocked
const [initialSensitivity, initialInvert] = (() => {
try {
const savedSensitivity = localStorage.getItem("rein_sensitivity")
const parsed = savedSensitivity
? Number.parseFloat(savedSensitivity)
: Number.NaN
return [
Number.isFinite(parsed) ? parsed : 1.0,
localStorage.getItem("rein_invert") === "true",
] as const
} catch {
return [1.0, false] as const
}
const savedSensitivity = getLocalStorageItem("rein_sensitivity")
const parsed = savedSensitivity
? Number.parseFloat(savedSensitivity)
: Number.NaN
return [
Number.isFinite(parsed) ? parsed : 1.0,
getLocalStorageItem("rein_invert") === "true",
] as const
})()

const sensitivity = useRef(initialSensitivity)
const invertScroll = useRef(initialInvert)

const [theme, setTheme] = useState(() => {
if (typeof window === "undefined") return THEMES.DEFAULT
try {
const saved = localStorage.getItem(APP_CONFIG.THEME_STORAGE_KEY)
return saved === THEMES.LIGHT || saved === THEMES.DARK
? saved
: THEMES.DEFAULT
} catch {
return THEMES.DEFAULT
}
const saved = getLocalStorageItem(APP_CONFIG.THEME_STORAGE_KEY)
return saved === THEMES.LIGHT || saved === THEMES.DARK
? saved
: THEMES.DEFAULT
})

const [qrData, setQrData] = useState("")
const setConfig = (sensitivity_val: number, invertedScroll_val: boolean) => {
sensitivity.current = sensitivity_val
invertScroll.current = invertedScroll_val
localStorage.setItem("rein_sensitivity", String(sensitivity_val))
localStorage.setItem("rein_invert", JSON.stringify(invertedScroll_val))
setLocalStorageItem("rein_sensitivity", String(sensitivity_val))
setLocalStorageItem("rein_invert", JSON.stringify(invertedScroll_val))
const timer = setTimeout(() => {
fetch("/api/config", {
method: "POST",
Expand All @@ -107,10 +102,9 @@ function SettingsPage() {
}

// Load initial state (IP is not stored in localStorage; only sensitivity, invert, theme are client settings)
const [authToken, setAuthToken] = useState(() => {
if (typeof window === "undefined") return ""
return localStorage.getItem("rein_auth_token") || ""
})
const [authToken, setAuthToken] = useState(
() => getLocalStorageItem("rein_auth_token") || "",
)

// Derive URLs once at the top
const protocol =
Expand Down Expand Up @@ -142,7 +136,7 @@ function SettingsPage() {
.then((data) => {
if (isMounted && data.token) {
setAuthToken(data.token)
localStorage.setItem("rein_auth_token", data.token)
setLocalStorageItem("rein_auth_token", data.token)
}
})
.catch((e) => console.error("Token fetch error:", e))
Expand All @@ -155,7 +149,7 @@ function SettingsPage() {
// Effect: Theme
useEffect(() => {
if (typeof window === "undefined") return
localStorage.setItem(APP_CONFIG.THEME_STORAGE_KEY, theme)
setLocalStorageItem(APP_CONFIG.THEME_STORAGE_KEY, theme)
document.documentElement.setAttribute("data-theme", theme)
}, [theme])

Expand Down
12 changes: 6 additions & 6 deletions src/routes/trackpad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { useTrackpadGesture } from "../hooks/useTrackpadGesture"
import { ScreenMirror } from "../components/Trackpad/ScreenMirror"
import { ErrorComponent } from "../components/Trackpad/ErrorComponent"
import { useWebRtcStream } from "../hooks/useWebRtcStream"
import {
getLocalStorageItem,
setLocalStorageItem,
} from "../utils/safeLocalStorage"

export const Route = createFileRoute("/trackpad")({
component: TrackpadPage,
Expand All @@ -22,16 +26,12 @@ function TrackpadPage() {

// Scan standard URL parameter fields to locate token strings passed from settings QR codes
const urlToken = searchParams.get("token")
const token =
urlToken ||
(typeof window !== "undefined"
? localStorage.getItem("rein_auth_token")
: null)
const token = urlToken || getLocalStorageItem("rein_auth_token")

// Save token internally if extracted directly from the URL scan pass
useEffect(() => {
if (urlToken) {
localStorage.setItem("rein_auth_token", urlToken)
setLocalStorageItem("rein_auth_token", urlToken)
}
}, [urlToken])
const [scrollMode, setScrollMode] = useState(false)
Expand Down
50 changes: 50 additions & 0 deletions src/utils/safeLocalStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it } from "vitest"
import { getLocalStorageItem, setLocalStorageItem } from "./safeLocalStorage"

describe("safeLocalStorage", () => {
const store = new Map<string, string>()
const mockStorage = {
getItem: (k: string) => store.get(k) ?? null,
setItem: (k: string, v: string) => {
store.set(k, v)
},
}

beforeEach(() => {
store.clear()
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: mockStorage,
})
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("reads and writes when storage works", () => {
expect(setLocalStorageItem("k", "v")).toBe(true)
expect(getLocalStorageItem("k")).toBe("v")
})

it("returns null/false when getItem/setItem throw", () => {
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: {
getItem: () => {
throw new Error("blocked")
},
setItem: () => {
throw new Error("blocked")
},
},
})
expect(getLocalStorageItem("k")).toBeNull()
expect(setLocalStorageItem("k", "v")).toBe(false)
})

it("returns null/false when localStorage is unavailable", () => {
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: undefined,
})
expect(getLocalStorageItem("k")).toBeNull()
expect(setLocalStorageItem("k", "v")).toBe(false)
})
})
24 changes: 24 additions & 0 deletions src/utils/safeLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Safe localStorage accessors for restricted contexts (private browsing,
* embedded webviews, blocked storage). Never throw to the UI.
*/

export function getLocalStorageItem(key: string): string | null {
try {
if (typeof localStorage === "undefined") return null
return localStorage.getItem(key)
} catch {
return null
}
}

/** @returns false when storage is unavailable or the write failed */
export function setLocalStorageItem(key: string, value: string): boolean {
try {
if (typeof localStorage === "undefined") return false
localStorage.setItem(key, value)
return true
} catch {
return false
}
}
Loading