From 2a35fde032c637a910c6d51429a2cd535ebfe8f9 Mon Sep 17 00:00:00 2001 From: utopiaeh Date: Thu, 27 Aug 2026 20:07:18 +0300 Subject: [PATCH] feat: add store review link and error log, harden persisted settings --- components/ErrorBoundary.tsx | 42 +++++++++++++++++ components/HelpPanel.tsx | 14 ++++++ components/OriginMappingsEditor.tsx | 2 + components/options/ErrorLogSection.tsx | 38 +++++++++++++++ components/options/ProtectedSitesSection.tsx | 1 + entrypoints/background/index.ts | 16 +++++-- entrypoints/options/App.tsx | 19 ++++++-- entrypoints/options/main.tsx | 8 +++- entrypoints/popup/main.tsx | 8 +++- hooks/useBrowserName.ts | 3 -- package.json | 2 +- public/icon/128-linked.png | Bin 14800 -> 14706 bytes public/icon/16-linked.png | Bin 946 -> 945 bytes public/icon/32-linked.png | Bin 2507 -> 2491 bytes public/icon/48-linked.png | Bin 4671 -> 4634 bytes public/icon/96-linked.png | Bin 10542 -> 10499 bytes store/clearLog.ts | 11 +++-- store/errorLog.ts | 42 +++++++++++++++++ store/settings.test.ts | 31 +++++++++++- store/settings.ts | 31 +++++------- store/storageError.ts | 13 +++++ utils/browser-info.ts | 9 ---- utils/clear-status.ts | 2 - utils/clearing.browser.test.ts | 2 - utils/clearing.firefox.test.ts | 1 - utils/clearing.ts | 47 +------------------ utils/data-types.ts | 3 -- utils/error-log.ts | 31 ++++++++++++ utils/i18n.ts | 8 ++++ utils/settings-defaults.ts | 23 +++++++++ utils/settings-transfer.test.ts | 19 +++++++- utils/settings-transfer.ts | 19 ++++---- utils/storage-adapter.test.ts | 4 -- utils/storage-adapter.ts | 25 ++++------ 34 files changed, 342 insertions(+), 132 deletions(-) create mode 100644 components/ErrorBoundary.tsx create mode 100644 components/options/ErrorLogSection.tsx create mode 100644 store/errorLog.ts create mode 100644 store/storageError.ts create mode 100644 utils/error-log.ts create mode 100644 utils/settings-defaults.ts diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx new file mode 100644 index 0000000..8f88e98 --- /dev/null +++ b/components/ErrorBoundary.tsx @@ -0,0 +1,42 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; +import { useSettingsStore } from '../store/settings'; + +interface Props { + children: ReactNode; +} + +interface State { + error: Error | null; +} + +export class ErrorBoundary extends Component { + override state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + override componentDidCatch(error: Error, info: ErrorInfo): void { + console.error('Bleep UI crashed:', error, info.componentStack); + } + + override render() { + if (!this.state.error) return this.props.children; + return ( +
+

Something went wrong.

+

{this.state.error.message}

+ +
+ ); + } +} diff --git a/components/HelpPanel.tsx b/components/HelpPanel.tsx index b1319a4..8c489ed 100644 --- a/components/HelpPanel.tsx +++ b/components/HelpPanel.tsx @@ -1,7 +1,10 @@ import { useTranslation } from '../hooks/useTranslation'; +import { isGeckoBased } from '../utils/browser-info'; import type { TranslationKey } from '../utils/i18n'; const GITHUB_ISSUES_URL = 'https://github.com/utopiaeh/bleep/issues'; +const CHROME_REVIEW_URL = 'https://chromewebstore.google.com/detail/dnihnhdbmgjfkdlkahlbbfhginfglomj/reviews'; +const FIREFOX_REVIEW_URL = 'https://addons.mozilla.org/en-US/firefox/addon/bleep-cache-cleaner/reviews/'; const ITEMS: Array<{ title: TranslationKey; text: TranslationKey }> = [ { title: 'helpPopupTitle', text: 'helpPopupText' }, @@ -37,6 +40,17 @@ export function HelpPanel() { {t('helpFeedbackLink')}

+

+ {t('helpReviewText')}{' '} + + {t('helpReviewLink')} + +

diff --git a/components/OriginMappingsEditor.tsx b/components/OriginMappingsEditor.tsx index 9872762..f1ede2a 100644 --- a/components/OriginMappingsEditor.tsx +++ b/components/OriginMappingsEditor.tsx @@ -89,6 +89,7 @@ export function OriginMappingsEditor({ value, onChange }: OriginMappingsEditorPr value={row.source} onChange={(e) => updateRow(i, { source: e.target.value })} placeholder={t('linkedOriginSourcePlaceholder')} + maxLength={253} className="w-full min-w-0 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500" /> {sourceInvalid(row) && ( @@ -107,6 +108,7 @@ export function OriginMappingsEditor({ value, onChange }: OriginMappingsEditorPr if (el) autoResize(el); }} placeholder={t('linkedOriginTargetPlaceholder')} + maxLength={2000} rows={1} className="w-full min-w-0 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500 resize-y overflow-y-auto" /> diff --git a/components/options/ErrorLogSection.tsx b/components/options/ErrorLogSection.tsx new file mode 100644 index 0000000..3763a52 --- /dev/null +++ b/components/options/ErrorLogSection.tsx @@ -0,0 +1,38 @@ +import { useErrorLogStore } from '../../store/errorLog'; + +export function ErrorLogSection() { + const entries = useErrorLogStore((s) => s.entries); + const clearErrors = useErrorLogStore((s) => s.clearErrors); + + return ( +
+
+

Error log

+ {entries.length > 0 && ( + + )} +
+ {entries.length === 0 ? ( +

Nothing logged.

+ ) : ( +
    + {entries.map((entry) => ( +
  • +
    + {entry.message} + + {new Date(entry.timestamp).toLocaleString()} + +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/components/options/ProtectedSitesSection.tsx b/components/options/ProtectedSitesSection.tsx index ea50eb7..3e2e816 100644 --- a/components/options/ProtectedSitesSection.tsx +++ b/components/options/ProtectedSitesSection.tsx @@ -15,6 +15,7 @@ export function ProtectedSitesSection({ value, onChange }: ProtectedSitesSection value={value} onChange={(e) => onChange(e.target.value)} placeholder={t('protectedSitesPlaceholder')} + maxLength={5000} rows={2} className="w-full rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500 resize-y" /> diff --git a/entrypoints/background/index.ts b/entrypoints/background/index.ts index 9cb851c..b959606 100644 --- a/entrypoints/background/index.ts +++ b/entrypoints/background/index.ts @@ -1,8 +1,11 @@ import { browser } from 'wxt/browser'; import { defineBackground } from 'wxt/utils/define-background'; import { linkedOriginsFor, tabOrigin } from '../../utils/clearing'; +import { installGlobalErrorLogging } from '../../utils/error-log'; import { readPersistedSettingsRaw } from '../../utils/storage-adapter'; +installGlobalErrorLogging(); + const SETTINGS_KEY = 'cache-cleaner-settings'; const DEFAULT_ICON = { @@ -81,10 +84,13 @@ export default defineBackground(() => { if (areaName !== 'sync' || !Object.keys(changes).some((key) => key.startsWith(SETTINGS_KEY))) return; invalidateMappingSettingsCache(); - browser.tabs.query({ active: true }).then((tabs) => { - for (const tab of tabs) { - if (tab.id != null) updateIcon(tab.id, tab.url); - } - }); + browser.tabs + .query({ active: true }) + .then((tabs) => { + for (const tab of tabs) { + if (tab.id != null) updateIcon(tab.id, tab.url); + } + }) + .catch(() => {}); }); }); diff --git a/entrypoints/options/App.tsx b/entrypoints/options/App.tsx index d6e7b3a..3326c70 100644 --- a/entrypoints/options/App.tsx +++ b/entrypoints/options/App.tsx @@ -5,6 +5,7 @@ import { HelpPanel } from '../../components/HelpPanel'; import { OriginMappingsEditor } from '../../components/OriginMappingsEditor'; import { BackupSection } from '../../components/options/BackupSection'; import { ClearLogSection } from '../../components/options/ClearLogSection'; +import { ErrorLogSection } from '../../components/options/ErrorLogSection'; import { GlobalSection } from '../../components/options/GlobalSection'; import { LanguageSection } from '../../components/options/LanguageSection'; import { ProtectedSitesSection } from '../../components/options/ProtectedSitesSection'; @@ -19,6 +20,7 @@ import { useTheme } from '../../hooks/useTheme'; import { useTranslation } from '../../hooks/useTranslation'; import { recordClear } from '../../store/clearLog'; import { useSettingsStore } from '../../store/settings'; +import { useStorageErrorStore } from '../../store/storageError'; import { isGeckoBased } from '../../utils/browser-info'; import { runClear } from '../../utils/clear-status'; import { @@ -138,8 +140,6 @@ export default function App() { }, [visitedSites, visitedSiteFilter]); async function handleGlobalClear() { - // Only Chrome can exclude specific origins from an unscoped clear at all, and only - // for sites we actually know the real origin of (i.e. ones in your history). const excludeOrigins = allVisitedSites .filter((site) => isProtectedSite(protectedSites, site.hostname)) .map((site) => site.origin); @@ -244,8 +244,6 @@ export default function App() { async function clearVisitedSite(site: VisitedSite): Promise { const ids = siteScopedIds(selectedTypesSite); - // Prefer an already-open tab over clearLinkedOrigin's throwaway background tab — - // picks up the tab's real cookieStoreId (Firefox container) too. const openTab = (await browser.tabs.query({})).find((tab) => tabHostname(tab) === site.hostname); const cookieStoreId = openTab ? tabCookieStoreId(openTab) : undefined; @@ -355,6 +353,9 @@ export default function App() { setTimeout(() => setHistoryBulkStatus('idle'), 1500); } + const storageError = useStorageErrorStore((s) => s.message); + const clearStorageError = useStorageErrorStore((s) => s.clear); + return (
@@ -363,6 +364,15 @@ export default function App() {

{t('settingsTitle')}

+ {storageError && ( +
+ {storageError} + +
+ )} + {activeTab === 'general' && ( @@ -373,6 +383,7 @@ export default function App() {
+ )} diff --git a/entrypoints/options/main.tsx b/entrypoints/options/main.tsx index ecc1b1e..a8641d4 100644 --- a/entrypoints/options/main.tsx +++ b/entrypoints/options/main.tsx @@ -1,10 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { ErrorBoundary } from '../../components/ErrorBoundary.tsx'; +import { installGlobalErrorLogging } from '../../utils/error-log.ts'; import App from './App.tsx'; import '../popup/style.css'; +installGlobalErrorLogging(); + ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , ); diff --git a/entrypoints/popup/main.tsx b/entrypoints/popup/main.tsx index 4cc9737..5a4725f 100644 --- a/entrypoints/popup/main.tsx +++ b/entrypoints/popup/main.tsx @@ -1,10 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { ErrorBoundary } from '../../components/ErrorBoundary.tsx'; +import { installGlobalErrorLogging } from '../../utils/error-log.ts'; import App from './App.tsx'; import './style.css'; +installGlobalErrorLogging(); + ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , ); diff --git a/hooks/useBrowserName.ts b/hooks/useBrowserName.ts index 8d04990..b7c4d65 100644 --- a/hooks/useBrowserName.ts +++ b/hooks/useBrowserName.ts @@ -1,9 +1,6 @@ import { useEffect, useState } from 'react'; import { getBrowserName, isGeckoBased } from '../utils/browser-info'; -/** The real self-reported browser name (e.g. "Firefox", "Zen", "LibreWolf") on - * Gecko-based browsers, or null on Chromium-family browsers (which don't implement - * getBrowserInfo) — use this instead of hardcoding "Firefox" in Gecko-only copy. */ export function useBrowserName(): string | null { const [name, setName] = useState(null); diff --git a/package.json b/package.json index 0b0ebca..d4ea14e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "Bleep - Clear cache, storage, and browsing data — globally or per-site.", "author": "utopiaeh01@gmail.com", "private": true, - "version": "0.4.0", + "version": "0.4.2", "type": "module", "scripts": { "dev": "wxt", diff --git a/public/icon/128-linked.png b/public/icon/128-linked.png index 3b15469948cde2015b18b3531b59c219dcaa58ad..e5d675390f6cc986f185c4b22d76e71b5ddb74a8 100644 GIT binary patch literal 14706 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 A`2YX_ literal 14800 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 LNkvXXu0mjfGfw6r diff --git a/public/icon/48-linked.png b/public/icon/48-linked.png index 665c7ffdfb2a4487a29f539a59f3fb9a622f6661..0116a70dcf2e156fd8da6f393a1ff521ea6e05ee 100644 GIT binary patch literal 4634 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 QmjD0&07*qoM6N<$f>px;U;qFB literal 4671 zcmV-F62R?=P)kIfvKdEhfRzUkuzuMAJ z6{=N>q6qTPim0dvl~Sb&0Zj~V63B~WH~Yvw?qjCExx0%P6A)#7zk$7T&z(8v%sKya z&P;$)+b98`jLG)4pfe|V?+t!0PZIQRdOEX)bT%^04o4QY^vfN zR90+f{Yo9Cd^4yd#Z(&|41{f8r%g8@1S5o)x)2i-;GLH~H1NRlAv2cX)TT3jYY49hxHC<4!OrqFN-JJgU0I> z6?xH`nnua|QSFPgDeyyKQJ?@XzPvFdW>$6ZA|GRJ2ZIoRNN=yCV~f_tMIr@r;mq85 zx-R~8Dosc>IiG-cSbl+QR zsz`RZDZt@2lfQg_A)RzK`;4AK#^qe}_S+Y$lF>PL-#P!~rOO&+pEKvq`Ety9 z_Z4kcd3=Kq(h{bu+H8u>B#6SgXI?gw15O8}OW%vi$Fslh4rG}TS*KdpSUaN>hGAsCrQ^rXr5iWSH+CHC$RF0|a=X>o4#9O`h!Xw;VHUfe|iO zac@a0zuC0uVJQtsbjGaR(+puT_-&eD0-S$$7-eb3DTa=j>Z{qh=`3bfrnz&xKTuS) zP-TjZFsc&5oRU}wGa*c_>!zXUn%0)=jC`P{jFsaqzM%S(_g4njtZr~!aYfON$5-hC z|Nqq>_vbMx8SGYT6zEI>qXsdodMN*-1<0C=Ir;Xvu6JK~@dt`Rer~tBMtJh_y=RU- zmzED3j=rS>Q0Om&&5;HwwGD^%??vs7?bz|3&1N(lj3iUZPhC#?!uH0RH-r#R z*EV9t`Ey!~Kps;dXMe%m)UI85eDDf7;{q z4WInOn=xtHbkmbpD02wPg&?LOEZgKAkE;$zCbxhK zOAxz*@x5QxnNR)pUiFKAZ`k_m3r{V*<)-T|1u&iiE>i$1fkdoY01F5q&t(*HLeR3J zX1^hq6t%bPU)iVsfN`@QuF?lrlq>O0UFsWD!ov4xoO2KeINhq)^LdT=Zf(E$Wml=wc~lyeXYg9Ay8(_!bFhq0Gj zC77z-D^=w=I()`dKp#U1nPcieG@kpYKqcvw$9 zz(9y;Q+ri7^9$a)XzI;bF)^?;S1FXx$iOrR|O zIEp{-Hkl)dqv@nvkAN@=ArU7d2vrrFd(JrAc=N4r7MzYl zVIm{AjM{P<;xFne3VRnjq8-7D?jL)@5?SgUlldM`09o$6hCQFWKm#vK9;e@C2G$+btvJCI?j*xXc$Myh_6CE(l@0mvBiLgN#gsF|1y* z7CU$B#6yq#27$g~(G-!Q3g`wWP@KGE=m^?;aMu}<7l1^7X z1*a7QRE1#*Xk5VWq~O6_sCwd$s6W&KuO|o0row;DS;!sSAJN_QII_7Krja(cEY>qc zf&bhzO>B2PS~piiPid&B+lNOVo{!%zSO~YP455e=PU>1kVNo#%Q*wbwE+J?V;_J-! zWI~xOQhkIH#qIS&&X`IWP!$CZGm3R@{sTvX9dNsxmL>BfJW@ z3xJMecxh04TDx)f3EE=V3MUcLeaa>6(u z#WZ2+f>%dtSrx_f;wyvh5@eW5Qb?z6PwI-Gb`F`VBerokJ50# z5s!BQ!mvO>1~mf^nxV^Ogh{xb&1}2wLRSIOkRgdpJt71*-OzjrJsh)kQZi9CWC#p3&kAmm z1>FSVQCYZi6NVP)PWwIiKt@C}CBTdkB9SUWLCk+H0UR-wNRapP_+t}aG1e@K4 z+Q(l&;$RcJLkA#Svlsi9y(?`FiamXfB~7j7v14iW;lQi!K-P^3O@lwzi%C~ri3rKJ z)^OMu3|&LGtx*QGB*5Eyij&>JC?HlKr%!Rr)Dq22`)c|cdJ@!;V@Vc_n(*Zf#;n=1 z@uw&Li00NHSS*gvAC`dG6_}aIT?1#J z9EalRXv~QMUDwdK?+f{VV{TDNXSDIlG;dvCv>P8-VeR46iTA~{cx?0G-P?_LXOx

AE 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 FV1m{%zCr*1 literal 10542 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 void; } -// Device-specific activity, not a preference — kept in local storage rather than the -// synced settings store so it doesn't consume the sync quota or show up on other -// machines as "stuff that happened somewhere else". export const useClearLogStore = create()( persist( (set, get) => ({ @@ -35,6 +32,14 @@ export const useClearLogStore = create()( { name: 'cache-cleaner-clear-log', storage: createJSONStorage(() => browserLocalStorage), + version: 1, + migrate: (persisted) => persisted as ClearLogState, + merge: (persisted, current) => ({ + ...current, + entries: Array.isArray((persisted as Partial | null)?.entries) + ? (persisted as ClearLogState).entries + : current.entries, + }), }, ), ); diff --git a/store/errorLog.ts b/store/errorLog.ts new file mode 100644 index 0000000..366f03a --- /dev/null +++ b/store/errorLog.ts @@ -0,0 +1,42 @@ +import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; +import { browserLocalStorage } from '../utils/storage-adapter'; + +export interface ErrorLogEntry { + id: string; + timestamp: number; + message: string; +} + +const MAX_ERROR_LOG_ENTRIES = 30; + +interface ErrorLogState { + entries: ErrorLogEntry[]; + logError: (message: string) => void; + clearErrors: () => void; +} + +export const useErrorLogStore = create()( + persist( + (set, get) => ({ + entries: [], + logError: (message) => { + const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + set({ entries: [{ id, timestamp: Date.now(), message }, ...get().entries].slice(0, MAX_ERROR_LOG_ENTRIES) }); + }, + clearErrors: () => set({ entries: [] }), + }), + { + name: 'cache-cleaner-error-log', + storage: createJSONStorage(() => browserLocalStorage), + version: 1, + migrate: (persisted) => persisted as ErrorLogState, + merge: (persisted, current) => ({ + ...current, + entries: Array.isArray((persisted as Partial | null)?.entries) + ? (persisted as ErrorLogState).entries + : current.entries, + }), + }, + ), +); diff --git a/store/settings.test.ts b/store/settings.test.ts index 7ed9b45..1f329bd 100644 --- a/store/settings.test.ts +++ b/store/settings.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { DEFAULTS, useSettingsStore } from './settings'; +import { DEFAULTS, mergeSettings, useSettingsStore } from './settings'; beforeEach(() => { useSettingsStore.setState(DEFAULTS); @@ -57,6 +57,35 @@ describe('importSettings', () => { }); }); +describe('mergeSettings (persist rehydration guard)', () => { + it('applies a valid persisted blob over the current state', () => { + const current = useSettingsStore.getState(); + const merged = mergeSettings({ theme: 'dark', linkedOrigins: 'a.com => b.com' }, current); + expect(merged.theme).toBe('dark'); + expect(merged.linkedOrigins).toBe('a.com => b.com'); + }); + + it('falls back to defaults for a corrupted field instead of propagating it (e.g. a version update that changed a field type)', () => { + const current = useSettingsStore.getState(); + const merged = mergeSettings({ theme: 42, selectedTypesGlobal: 'not-an-array' }, current); + expect(merged.theme).toBe(DEFAULTS.theme); + expect(merged.selectedTypesGlobal).toEqual(DEFAULTS.selectedTypesGlobal); + }); + + it('handles a completely empty/missing persisted value (first install) without throwing', () => { + const current = useSettingsStore.getState(); + expect(() => mergeSettings(undefined, current)).not.toThrow(); + expect(mergeSettings(undefined, current)).toMatchObject(DEFAULTS); + }); + + it('never drops the action functions from current state', () => { + const current = useSettingsStore.getState(); + const merged = mergeSettings({ theme: 'dark' }, current); + expect(typeof merged.setTheme).toBe('function'); + expect(typeof merged.resetSettings).toBe('function'); + }); +}); + describe('resetSettings', () => { it('restores every field to DEFAULTS', () => { const s = useSettingsStore.getState(); diff --git a/store/settings.ts b/store/settings.ts index b4107c9..c073e65 100644 --- a/store/settings.ts +++ b/store/settings.ts @@ -2,28 +2,12 @@ import { create } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; import type { DataTypeId } from '../utils/data-types'; import type { Language } from '../utils/i18n'; +import { DEFAULTS, type Theme } from '../utils/settings-defaults'; +import { sanitizeSettings } from '../utils/settings-transfer'; import { browserSyncStorage } from '../utils/storage-adapter'; -export type Theme = 'system' | 'light' | 'dark'; - -const DEFAULT_TYPES: DataTypeId[] = [ - 'cacheStorage', - 'cache', - 'indexedDB', - 'localStorage', - 'sessionStorage', -]; - -export const DEFAULTS = { - selectedTypesGlobal: [...DEFAULT_TYPES], - selectedTypesSite: [...DEFAULT_TYPES], - autoReloadAfterClear: true, - linkedOrigins: '', - useOriginMappings: true, - protectedSites: '', - theme: 'system' as Theme, - language: 'auto' as Language, -}; +export type { Theme }; +export { DEFAULTS }; interface SettingsState { selectedTypesGlobal: DataTypeId[]; @@ -56,6 +40,10 @@ function toggleIn( set({ [field]: current.includes(id) ? current.filter((t) => t !== id) : [...current, id] }); } +export function mergeSettings(persisted: unknown, current: SettingsState): SettingsState { + return { ...current, ...sanitizeSettings(persisted) }; +} + export const useSettingsStore = create()( persist( (set, get) => ({ @@ -74,6 +62,9 @@ export const useSettingsStore = create()( { name: 'cache-cleaner-settings', storage: createJSONStorage(() => browserSyncStorage), + version: 1, + migrate: (persisted) => persisted as SettingsState, + merge: mergeSettings, }, ), ); diff --git a/store/storageError.ts b/store/storageError.ts new file mode 100644 index 0000000..5b161f4 --- /dev/null +++ b/store/storageError.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; + +interface StorageErrorState { + message: string | null; + setError: (message: string) => void; + clear: () => void; +} + +export const useStorageErrorStore = create((set) => ({ + message: null, + setError: (message) => set({ message }), + clear: () => set({ message: null }), +})); diff --git a/utils/browser-info.ts b/utils/browser-info.ts index 1f91004..a04307c 100644 --- a/utils/browser-info.ts +++ b/utils/browser-info.ts @@ -4,15 +4,6 @@ export function isGeckoBased(): boolean { return typeof (browser.runtime as { getBrowserInfo?: unknown }).getBrowserInfo === 'function'; } -/** The actual browser's self-reported name (e.g. "Firefox", "Zen", "LibreWolf") for - * Gecko-based forks — only call after isGeckoBased() confirms getBrowserInfo exists. - * Chromium-family browsers don't implement this API at all. - * - * Some forks pass the Gecko app identity through unchanged, so `name` alone isn't - * reliable — Zen self-reports `name: "Firefox"` but adds its own `zen: {version}` - * field to the same object (verified live: Zen 1.21.15b on Firefox 154 base), which - * we use to override the name. Other forks (LibreWolf, Waterfox, etc.) aren't known - * to add a similar marker, so they fall through to whatever they self-report. */ export async function getBrowserName(): Promise { const getBrowserInfo = ( browser.runtime as { getBrowserInfo?: () => Promise<{ name: string; zen?: unknown }> } diff --git a/utils/clear-status.ts b/utils/clear-status.ts index 2010b1c..43d16e4 100644 --- a/utils/clear-status.ts +++ b/utils/clear-status.ts @@ -12,8 +12,6 @@ export function clearAllLabel( return t('clearAllCount', { count }); } -/** clearing → run fn → done/failed → back to idle after a short flash, used by every - * all-or-nothing clear action (Global clear in both popup and options). */ export async function runClear(setStatus: (status: ClearStatus) => void, fn: () => Promise): Promise { setStatus('clearing'); try { diff --git a/utils/clearing.browser.test.ts b/utils/clearing.browser.test.ts index 557945d..64175ac 100644 --- a/utils/clearing.browser.test.ts +++ b/utils/clearing.browser.test.ts @@ -9,8 +9,6 @@ import { requestOriginPermission, } from './clearing'; -// fake-browser (wxt/testing) doesn't implement browsingData/cookies/permissions — -// stub them directly on the shared fake `browser` object per test. function stubBrowserApis() { Object.assign(browser, { browsingData: { diff --git a/utils/clearing.firefox.test.ts b/utils/clearing.firefox.test.ts index 3c2cf4d..d046d6c 100644 --- a/utils/clearing.firefox.test.ts +++ b/utils/clearing.firefox.test.ts @@ -139,7 +139,6 @@ describe('clearLinkedOrigin (Firefox path)', () => { it('removes the tabs.onUpdated listener even when the tab never reaches "complete" (no leak on timeout)', async () => { vi.useFakeTimers(); - // Override the default mock: never fire the "complete" event for this tab. (browser.tabs.create as ReturnType).mockImplementation(async (props: { url: string }) => ({ id: nextTabId++, url: props.url, diff --git a/utils/clearing.ts b/utils/clearing.ts index 66a8357..55bbb90 100644 --- a/utils/clearing.ts +++ b/utils/clearing.ts @@ -15,9 +15,6 @@ export async function getSupportedKeys(): Promise> { return new Set(Object.keys(settings.dataToRemove)); } -/** Chrome (since Chrome 74) supports excludeOrigins on an unscoped clear — Firefox - * has no equivalent (Bugzilla 1632796, still open), so exclusion there is a no-op: - * Global stays all-or-nothing on Firefox regardless of a protected-sites list. */ export async function clearGlobal(ids: DataTypeId[], excludeOrigins: string[] = []): Promise { const supported = await getSupportedKeys(); const dataToRemove = toRemovalOptions(ids, supported); @@ -34,7 +31,6 @@ export function siteScopedIds(ids: DataTypeId[]): DataTypeId[] { return ids.filter((id) => scoped.has(id)); } -/** Chrome/Chromium: browsingData's `origins` param scopes removal to one site. On Firefox it does not work. */ export async function clearSiteViaBrowsingData(origin: string, ids: DataTypeId[]): Promise { const supported = await getSupportedKeys(); const dataToRemove = toRemovalOptions(siteScopedIds(ids), supported); @@ -59,9 +55,6 @@ function withTimeout(promise: Promise, ms: number): Promise { } async function clearInMainWorld(ids: string[]): Promise<{ failed: Record }> { - // Duplicated from the module-level withTimeout: this function is serialized and - // injected into the page by scripting.executeScript, so it can't close over - // anything outside itself. function withTimeout(promise: Promise, ms: number): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms); @@ -78,9 +71,6 @@ async function clearInMainWorld(ids: string[]): Promise<{ failed: Record Promise | void> = { async cacheStorage() { if (!('caches' in self)) return; @@ -105,10 +95,6 @@ async function clearInMainWorld(ids: string[]): Promise<{ failed: Record> = { indexedDB: 'indexedDB', localStorage: 'localStorage', @@ -128,8 +114,6 @@ async function clearHostnameScopedNative(hostname: string, ids: DataTypeId[]): P ); } -/** allFrames: an auth widget commonly lives in an iframe on a different origin than - * the top page — its storage is otherwise unreachable here. */ async function runContentScriptClear(tabId: number, scriptIds: DataTypeId[]): Promise { const injections = await browser.scripting.executeScript({ target: { tabId, allFrames: true }, @@ -171,14 +155,6 @@ export async function requestOriginPermission(): Promise { return browser.permissions.request({ origins: ['*://*/*'] }); } -/** document.cookie can't see HttpOnly cookies; the privileged cookies API can. - * Firefox's Total Cookie Protection also partitions third-party cookies (e.g. an - * SSO/silent-renew iframe from another domain, partitioned under whatever OTHER - * top-level site embeds it) — a plain getAll() only sees the unpartitioned jar and - * would silently miss a still-alive session cookie set that way. Passing an empty - * partitionKey (per MDN) matches every partition regardless of its topLevelSite in - * one call — matching on `partitionKey: { topLevelSite: origin }` would only catch - * the origin partitioning itself, not the actual leak case this exists for. */ async function clearCookiesForOrigin(origin: string, storeId?: string): Promise { const options = { url: origin, storeId, partitionKey: {} } as Parameters[0]; const cookies = await browser.cookies @@ -221,18 +197,12 @@ function extractHostname(input: string): string { const DOMAIN_RE = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i; -/** Loose check for "does this look like a domain" (rejects e.g. "asdasd") — used - * only for inline validation hints, not for matching. */ export function isValidHost(input: string): boolean { const host = extractHostname(input); if (!host) return false; return host === 'localhost' || DOMAIN_RE.test(host); } -/** One mapping per line: `source => target1, target2`. Lines with no `=>` (or an - * empty source/target) are ignored. `source` is kept as typed (matching extracts - * its hostname on demand); targets are normalized to full origins up front since - * they're passed straight to the clearing calls. */ export interface OriginMapping { source: string; targets: string[]; @@ -251,11 +221,6 @@ export function parseOriginMappings(raw: string): OriginMapping[] { .filter((m) => m.source && m.targets.length > 0); } -/** Matches regardless of scheme or path, and treats the mapping's source as covering - * its own subdomains too — e.g. a source of "domain.com" also matches - * "sso.domain.com". A site with no matching mapping gets none. A mapping with an - * invalid-looking source or any invalid-looking target is skipped entirely — the - * editor shows these as errors instead of silently acting on them. */ export function linkedOriginsFor(raw: string, activeOrigin: string): string[] { const activeHost = extractHostname(activeOrigin); return parseOriginMappings(raw) @@ -274,11 +239,6 @@ function parseHostList(raw: string): string[] { .filter(Boolean); } -/** Same subdomain-covering semantics as linkedOriginsFor's source matching — a - * protected entry of "domain.com" also protects "sso.domain.com". Doesn't apply to - * the Global scope: browsingData.remove has no way to exclude specific origins from - * an unscoped clear, so protection only guards the per-site clear surfaces (tabs, - * visited sites, and mapped linked-origin targets). */ export function isProtectedSite(raw: string, hostname: string): boolean { const host = hostname.toLowerCase(); return parseHostList(raw).some((protectedHost) => host === protectedHost || host.endsWith(`.${protectedHost}`)); @@ -353,7 +313,6 @@ export async function clearLinkedOrigin( } } -/** Firefox-only field, absent from the shared cross-browser Tab type. */ export function tabCookieStoreId(tab: object): string | undefined { return (tab as { cookieStoreId?: string }).cookieStoreId; } @@ -387,14 +346,10 @@ export function dedupeSitesByHostname(urls: Array): VisitedS if (!url) continue; try { const parsed = new URL(url); - // Only http(s) sites are real per-site clear targets. chrome://, about:, and - // file:// all parse without throwing, but either have no hostname at all (blank - // row) or a bogus one (e.g. "chrome://extensions/" parses to hostname - // "extensions", which isn't a site anyone meant to clear). if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue; if (!seen.has(parsed.hostname)) seen.set(parsed.hostname, parsed.origin); } catch { - // skip + continue; } } return Array.from(seen, ([hostname, origin]) => ({ hostname, origin })); diff --git a/utils/data-types.ts b/utils/data-types.ts index 0715286..d17fd49 100644 --- a/utils/data-types.ts +++ b/utils/data-types.ts @@ -32,9 +32,6 @@ export const DATA_TYPES: DataTypeDef[] = [ { id: 'formData', browsingDataKey: 'formData', quick: false, siteScoped: false }, ]; -/** Types actually usable for a per-site clear on the current browser — excludes - * HTTP Cache on Firefox, which has no per-site clearing API at all (see - * clearInMainWorld in utils/clearing.ts). */ export function siteScopedDataTypes(): DataTypeDef[] { return DATA_TYPES.filter((type) => type.siteScoped && !(type.id === 'cache' && isGeckoBased())); } diff --git a/utils/error-log.ts b/utils/error-log.ts new file mode 100644 index 0000000..34333dc --- /dev/null +++ b/utils/error-log.ts @@ -0,0 +1,31 @@ +import { useErrorLogStore } from '../store/errorLog'; + +let installed = false; + +function messageFrom(value: unknown): string { + if (value instanceof Error) return value.message; + try { + return String(value); + } catch { + return 'Unknown error'; + } +} + +export function installGlobalErrorLogging(): void { + if (installed) return; + installed = true; + + const originalConsoleError = console.error.bind(console); + console.error = (...args: unknown[]) => { + originalConsoleError(...args); + useErrorLogStore.getState().logError(args.map(messageFrom).join(' ')); + }; + + globalThis.addEventListener?.('error', (event) => { + useErrorLogStore.getState().logError(messageFrom(event.error ?? event.message)); + }); + + globalThis.addEventListener?.('unhandledrejection', (event) => { + useErrorLogStore.getState().logError(messageFrom(event.reason)); + }); +} diff --git a/utils/i18n.ts b/utils/i18n.ts index 46c1e4d..58dd38b 100644 --- a/utils/i18n.ts +++ b/utils/i18n.ts @@ -134,6 +134,8 @@ const en = { helpFeedbackTitle: 'Feedback', helpFeedbackText: 'Found a bug or have a feature request?', helpFeedbackLink: 'Open an issue on GitHub', + helpReviewText: 'Like Bleep? A review helps others find it.', + helpReviewLink: 'Leave a review', }; const ru: typeof en = { @@ -258,6 +260,8 @@ const ru: typeof en = { helpFeedbackTitle: 'Обратная связь', helpFeedbackText: 'Нашли баг или есть предложение?', helpFeedbackLink: 'Открыть issue на GitHub', + helpReviewText: 'Нравится Bleep? Отзыв поможет другим найти его.', + helpReviewLink: 'Оставить отзыв', }; const ro: typeof en = { @@ -382,6 +386,8 @@ const ro: typeof en = { helpFeedbackTitle: 'Feedback', helpFeedbackText: 'Ai găsit un bug sau ai o sugestie?', helpFeedbackLink: 'Deschide un issue pe GitHub', + helpReviewText: 'Îți place Bleep? O recenzie ajută alți utilizatori să-l găsească.', + helpReviewLink: 'Lasă o recenzie', }; const uk: typeof en = { @@ -506,6 +512,8 @@ const uk: typeof en = { helpFeedbackTitle: 'Зворотний зв’язок', helpFeedbackText: 'Знайшли помилку чи маєте пропозицію?', helpFeedbackLink: 'Відкрити issue на GitHub', + helpReviewText: 'Подобається Bleep? Відгук допоможе іншим його знайти.', + helpReviewLink: 'Залишити відгук', }; export type TranslationKey = keyof typeof en; diff --git a/utils/settings-defaults.ts b/utils/settings-defaults.ts new file mode 100644 index 0000000..f79c310 --- /dev/null +++ b/utils/settings-defaults.ts @@ -0,0 +1,23 @@ +import type { DataTypeId } from './data-types'; +import type { Language } from './i18n'; + +export type Theme = 'system' | 'light' | 'dark'; + +const DEFAULT_TYPES: DataTypeId[] = [ + 'cacheStorage', + 'cache', + 'indexedDB', + 'localStorage', + 'sessionStorage', +]; + +export const DEFAULTS = { + selectedTypesGlobal: [...DEFAULT_TYPES], + selectedTypesSite: [...DEFAULT_TYPES], + autoReloadAfterClear: true, + linkedOrigins: '', + useOriginMappings: true, + protectedSites: '', + theme: 'system' as Theme, + language: 'auto' as Language, +}; diff --git a/utils/settings-transfer.test.ts b/utils/settings-transfer.test.ts index c5259ca..973dc25 100644 --- a/utils/settings-transfer.test.ts +++ b/utils/settings-transfer.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULTS } from '../store/settings'; -import { exportSettingsJson, parseImportedSettings } from './settings-transfer'; +import { DEFAULTS } from './settings-defaults'; +import { exportSettingsJson, parseImportedSettings, sanitizeSettings } from './settings-transfer'; describe('exportSettingsJson', () => { it('serializes only the known settings fields', () => { @@ -51,3 +51,18 @@ describe('parseImportedSettings', () => { expect(() => parseImportedSettings('not json')).toThrow(); }); }); + +describe('sanitizeSettings', () => { + it('passes through a valid object unchanged (field-wise)', () => { + expect(sanitizeSettings({ ...DEFAULTS, theme: 'dark' })).toEqual({ ...DEFAULTS, theme: 'dark' }); + }); + + it('treats null/undefined as an empty object instead of throwing — the shape a persist store sees on first load', () => { + expect(sanitizeSettings(null)).toEqual({}); + expect(sanitizeSettings(undefined)).toEqual({}); + }); + + it('drops a corrupted field instead of propagating it (e.g. a partial/garbled storage write)', () => { + expect(sanitizeSettings({ ...DEFAULTS, theme: 42 })).not.toHaveProperty('theme'); + }); +}); diff --git a/utils/settings-transfer.ts b/utils/settings-transfer.ts index f294321..d7b4db2 100644 --- a/utils/settings-transfer.ts +++ b/utils/settings-transfer.ts @@ -1,4 +1,4 @@ -import { DEFAULTS, type Theme } from '../store/settings'; +import { DEFAULTS, type Theme } from './settings-defaults'; import type { Language } from './i18n'; export type ExportableSettings = typeof DEFAULTS; @@ -24,17 +24,18 @@ function isValidField(key: keyof ExportableSettings, value: unknown): boolean { return typeof value === typeof defaultValue; } -/** Only known keys with the right shape survive — an untrusted/edited-by-hand file - * can't inject unexpected fields or crash the UI with e.g. an unrecognized theme or - * language string (translate() would throw on a language not in its dictionary). */ -export function parseImportedSettings(json: string): Partial { - const parsed = JSON.parse(json) as Record; +export function sanitizeSettings(value: unknown): Partial { + const parsed = (value ?? {}) as Record; const result: Partial = {}; for (const key of Object.keys(DEFAULTS) as (keyof ExportableSettings)[]) { - const value = parsed[key]; - if (value !== undefined && isValidField(key, value)) { - (result as Record)[key] = value; + const v = parsed[key]; + if (v !== undefined && isValidField(key, v)) { + (result as Record)[key] = v; } } return result; } + +export function parseImportedSettings(json: string): Partial { + return sanitizeSettings(JSON.parse(json)); +} diff --git a/utils/storage-adapter.test.ts b/utils/storage-adapter.test.ts index b4b900d..6dc1114 100644 --- a/utils/storage-adapter.test.ts +++ b/utils/storage-adapter.test.ts @@ -2,8 +2,6 @@ import { browser } from 'wxt/browser'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { browserSyncStorage } from './storage-adapter'; -// fake-browser doesn't implement storage.sync at all — back it with a plain in-memory -// object so get/set/remove behave like the real StorageArea API. function stubSyncStorage() { const data: Record = {}; Object.assign(browser, { @@ -42,7 +40,6 @@ describe('browserSyncStorage', () => { const big = 'x'.repeat(20000); await browserSyncStorage.setItem('settings', big); expect(await browserSyncStorage.getItem('settings')).toBe(big); - // sanity: confirm it's actually split, not stored as one oversized item expect(browser.storage.sync.set).toHaveBeenCalled(); const lastCallArgs = (browser.storage.sync.set as ReturnType).mock.calls.at(-1)![0]; expect(Object.keys(lastCallArgs).length).toBeGreaterThan(1); @@ -76,7 +73,6 @@ describe('browserSyncStorage', () => { .mockResolvedValue({ settings: '{"legacy":true}' }); expect(await browserSyncStorage.getItem('settings')).toBe('{"legacy":true}'); - // migrated forward — a second read shouldn't need storage.local anymore expect(await browserSyncStorage.getItem('settings')).toBe('{"legacy":true}'); }); }); diff --git a/utils/storage-adapter.ts b/utils/storage-adapter.ts index 47da5e6..3c60476 100644 --- a/utils/storage-adapter.ts +++ b/utils/storage-adapter.ts @@ -1,12 +1,8 @@ import { browser } from 'wxt/browser'; import type { Browser } from 'wxt/browser'; import type { StateStorage } from 'zustand/middleware'; +import { useStorageErrorStore } from '../store/storageError'; -// storage.sync caps each item at ~8KB (Chrome: 8192 bytes/item, 100KB total) — a -// mapping-heavy settings blob stored under one key can exceed that. Split into -// fixed-size string chunks under a `${name}__N` key each, with `${name}__len` -// tracking how many chunks exist, so the actual quota that matters is the ~100KB -// total, not the ~8KB per-item cap. const CHUNK_SIZE = 6000; async function readChunked(area: Browser.storage.StorageArea, name: string): Promise { @@ -32,8 +28,6 @@ async function writeChunked(area: Browser.storage.StorageArea, name: string, val }); await area.set(payload); - // A shrinking value (fewer chunks than before) must drop the now-unused trailing - // keys, or they sit there forever silently eating into the ~100KB total quota. if (oldLen > chunks.length) { const staleKeys = Array.from({ length: oldLen - chunks.length }, (_, i) => `${name}__${chunks.length + i}`); await area.remove(staleKeys); @@ -60,10 +54,6 @@ export const browserLocalStorage: StateStorage = { }, }; -/** Settings synced across the user's browsers via storage.sync. Falls back to - * reading (and migrating forward) a pre-sync plain-JSON value that older versions - * of Bleep stored directly under `name` in storage.local — a one-time, read-triggered - * migration with no separate "have I migrated" flag needed. */ export const browserSyncStorage: StateStorage = { getItem: async (name) => { const fromSync = await readChunked(browser.storage.sync, name); @@ -77,16 +67,21 @@ export const browserSyncStorage: StateStorage = { return legacyValue ?? null; }, setItem: async (name, value) => { - await writeChunked(browser.storage.sync, name, value); + try { + await writeChunked(browser.storage.sync, name, value); + } catch (e) { + console.error('Bleep: failed to save settings', e); + useStorageErrorStore + .getState() + .setError('Could not save — the change may be too large or sync storage is full.'); + throw e; + } }, removeItem: async (name) => { await removeChunked(browser.storage.sync, name); }, }; -/** Reads the persisted settings JSON directly, bypassing zustand — used by the - * background script, which can't rely on the store's React-oriented hydration - * lifecycle inside a short-lived MV3 service worker. */ export async function readPersistedSettingsRaw(name: string): Promise { const raw = await browserSyncStorage.getItem(name); if (!raw) return null;