From 186f5aa83a1d37e04e4b954a4cd6ad050fd925b1 Mon Sep 17 00:00:00 2001 From: cielarchazure Date: Sat, 29 Nov 2025 02:40:27 -0700 Subject: [PATCH] feat: Added support for Gecko (firefox) --- bun.lockb | Bin 264512 -> 265384 bytes .../manifest.json => manifest.base.json} | 18 +--- packages/chrome/manifest.chrome.json | 6 ++ packages/chrome/manifest.firefox.json | 12 +++ packages/chrome/package.json | 16 ++-- packages/chrome/scripts/copy-frontend.js | 8 +- packages/chrome/vite.config.ts | 80 +++++++++++------- packages/frontend/package.json | 10 ++- packages/frontend/src/App.tsx | 13 +-- .../frontend/src/hooks/useChromeStorage.ts | 54 +++++------- packages/frontend/src/lib/utils.ts | 14 +-- 11 files changed, 130 insertions(+), 101 deletions(-) rename packages/chrome/{public/manifest.json => manifest.base.json} (57%) create mode 100644 packages/chrome/manifest.chrome.json create mode 100644 packages/chrome/manifest.firefox.json diff --git a/bun.lockb b/bun.lockb index 1d129f43b7c493e0a045886e538fd922bfe57261..5e451d1dbd55f00b7b083ea8bf29269876adfbba 100755 GIT binary patch delta 11773 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 delta 11175 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≧sti8r`+AyvY{lk&##J~*Apg*LHOvyoj*uskt#_N7KS_chA4bIeeJi!@16uT{yjsvHp>9)$hKD z_~kHjb3=q>-_H5))rkR_Z@=^+BCLc>c9R30EIV($dY<>?0?#YHxXVWW5P!U3JF{JG zxQTvcY^XbmvTV)S`23r`w@pcb=0GC_PAFqPqq2WpRPbL#b+Iw0werSF;bm-tyF8!Y z57yp89xrV@RuS(ZceIkesAQ>La-ej-lI44$mxA#!g=crp)!;6ZXMFLSO&4+K5R~R0 zC`2L$AFDYF(+@p=Y*(COFD3*b19M<|75fS2N(EI+?~Sr5tJq*~_-Cq^TR+*6rFqNa zv4B?RjnSqV=K>Ul@2i}&Zv6*8s(cV4iMT+2BMMtx&D#6Ojy#YJ^+9X**RV}sQcVpj z@{#9D;kC@Zj~wV6g*QHYTIQ^^k55fqBW~Av3WH0SP|Fhf;EdU|EDMoXaVzd4`$=2t z*sVUYTZma!&S@|}iRi3!v$^zEbL%-h%z1}OA z*v~N6YreIwAOhd^IY!&oAAF;`SV`g>kS@$fUcYAKk;b7&kO3@7FWuzUVs3CaTbr?;QIe zvgHaQXb2%9@-&-B5teOO9sGlaw>*f72A~*x+gdhN4Qa=&`*yilXKufd zdgrZ17T+INXxWZ6bJXfNKaR~SeR1x#Mz$FdA(jnX-)xE6vG?Zq2j&QK8Rk7+7!req z4WD}N@b-A*(1oGi+D7&j&XvwLvWNZg>~&-#0^|Ww;d!<)K+ci;o7j^8JOUiqT%$Zr zDrsa#jq+uwppm^1D9?AcY~kv@&?=-h>rWq)8%Tpb47<{_Zs~uC^&EiKTQ+@pU+MgD z&VMd?W{$+Q{e%63vFdV}Ee(>lNg0<}`(T_g>I(A;#w>PcGlFG5-Aw$azXOXsDmh3qudsaN zIG115HlzLbAKUV)?5ALPGK&w9gKSZ%`KNDbFe?j@&tzNni=LC`?RTx6=A2Nd&C|b~ z3tL+=CHS*+_*hBHvHR@9xBvX$Lfsqiu^^R`ye#F~t6A2sPQ9q@wtRb0O~kPK`9T@* z@sg9XE9g@8v#&Cr@8J0zqwd`NqkPqr8lIo{B+GH}{tw!Dqw(ejC0SQUVe-aLSn=ob zYPMys>{L" - ], + "js": ["bridge.js"], + "matches": [""], "run_at": "document_start" } ], - "permissions": [ - "storage", - "activeTab", - "scripting" - ] + "permissions": ["storage", "activeTab", "scripting"] } diff --git a/packages/chrome/manifest.chrome.json b/packages/chrome/manifest.chrome.json new file mode 100644 index 0000000..bb5f2fe --- /dev/null +++ b/packages/chrome/manifest.chrome.json @@ -0,0 +1,6 @@ +{ + "background": { + "service_worker": "background.js", + "type": "module" + } +} diff --git a/packages/chrome/manifest.firefox.json b/packages/chrome/manifest.firefox.json new file mode 100644 index 0000000..c10a250 --- /dev/null +++ b/packages/chrome/manifest.firefox.json @@ -0,0 +1,12 @@ +{ + "background": { + "scripts": ["background.js"], + "type": "module" + }, + "browser_specific_settings": { + "gecko": { + "id": "openbanker@voidranjer", + "strict_min_version": "109.0" + } + } +} diff --git a/packages/chrome/package.json b/packages/chrome/package.json index 1090b7a..75ca9da 100644 --- a/packages/chrome/package.json +++ b/packages/chrome/package.json @@ -7,14 +7,20 @@ ], "type": "module", "scripts": { - "dev": "tsc --watch & vite build --watch", - "build": "tsc && vite build && node scripts/copy-frontend.js" + "dev": "cross-env TARGET=chrome tsc --watch & vite build --watch", + "build": "tsc && npm run build:chrome && npm run build:firefox", + "build:chrome": "cross-env TARGET=chrome vite build && node scripts/copy-frontend.js chrome", + "build:firefox": "cross-env TARGET=firefox vite build && node scripts/copy-frontend.js firefox" }, "devDependencies": { + "@openbanker/core": "workspace:*", + "@openbanker/frontend": "workspace:*", "@types/chrome": "^0.1.24", + "@types/webextension-polyfill": "^0.12.4", "typescript": "~5.9.3", - "vite": "^7.1.7", - "@openbanker/frontend": "workspace:*", - "@openbanker/core": "workspace:*" + "vite": "^7.1.7" + }, + "dependencies": { + "webextension-polyfill": "^0.12.0" } } diff --git a/packages/chrome/scripts/copy-frontend.js b/packages/chrome/scripts/copy-frontend.js index 6bd6813..35e4c92 100644 --- a/packages/chrome/scripts/copy-frontend.js +++ b/packages/chrome/scripts/copy-frontend.js @@ -1,8 +1,14 @@ import { cpSync } from 'node:fs'; import { resolve } from 'node:path'; +/** + * Get the build target from the command line argument (e.g., "node copy-frontend.js firefox") + * If no argument is provided, default to 'chrome'. + */ +const target = process.argv[2] || 'chrome'; + const source = resolve('../frontend/dist'); -const destination = resolve('./dist'); +const destination = resolve(`./dist/${target}`); try { cpSync(source, destination, { recursive: true }); diff --git a/packages/chrome/vite.config.ts b/packages/chrome/vite.config.ts index b3b957b..fbaf818 100644 --- a/packages/chrome/vite.config.ts +++ b/packages/chrome/vite.config.ts @@ -1,42 +1,60 @@ import { defineConfig } from "vite"; import { resolve } from "path"; +import fs from "fs"; -// https://vite.dev/config/ -export default defineConfig({ - /* import aliases */ - resolve: { - alias: { - "@": resolve(__dirname, "src"), - }, - }, +// Merges the base manifest with the platform-specific manifest. +function mergeManifests(target) { + // Read the shared configuration (icons, content scripts, permissions) + const base = JSON.parse(fs.readFileSync("./manifest.base.json", "utf-8")); + + // Read platform-specific overrides (background scripts vs service workers, IDs) + const specific = JSON.parse(fs.readFileSync(`./manifest.${target}.json`, "utf-8")); + + // Merge them + return { ...base, ...specific }; +} - /* Use relative paths for Chrome extension */ - // Remove if unecessary - // base: "./", +export default defineConfig(({ mode }) => { + // Default to 'chrome' if the target env var is not set + const target = process.env.TARGET || "chrome"; - build: { - outDir: "dist", - rollupOptions: { - input: { - background: resolve(__dirname, "src/background.ts"), - bridge: resolve(__dirname, "src/bridge.ts"), + return { + resolve: { + alias: { + "@": resolve(__dirname, "src"), }, - output: { - entryFileNames: (chunkInfo) => { - // Keep chrome extension files in chrome/ folder - if (["background", "bridge"].includes(chunkInfo.name as string)) { - return "[name].js"; - } - // Main app files go in assets/ - return "assets/[name]-[hash].js"; + }, + build: { + // Separate output directories to avoid overwriting builds + // e.g., dist/chrome or dist/firefox + outDir: `dist/${target}`, + emptyOutDir: true, + rollupOptions: { + input: { + background: resolve(__dirname, "src/background.ts"), + bridge: resolve(__dirname, "src/bridge.ts"), }, - assetFileNames: () => { - // Keep assets organized - return "assets/[name]-[hash][extname]"; + output: { + entryFileNames: "[name].js", + assetFileNames: "assets/[name]-[hash][extname]", }, }, }, - // Ensure relative paths work correctly in the extension - // assetsDir: "assets", - }, + plugins: [ + { + name: "generate-manifest", + /** + * Custom plugin to generate the manifest.json file at build time. + * This ensures the correct manifest version is bundled for the specific browser. + */ + closeBundle() { + const manifest = mergeManifests(target); + fs.writeFileSync( + `dist/${target}/manifest.json`, + JSON.stringify(manifest, null, 2) + ); + }, + }, + ], + }; }); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 7eb8540..48073ec 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -9,6 +9,8 @@ "@blueprintjs/core": "^6.3.4", "@blueprintjs/table": "^6.0.8", "@google/genai": "^1.29.0", + "@openbanker/core": "workspace:*", + "@openbanker/plugins": "workspace:*", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-separator": "^1.1.8", @@ -20,6 +22,7 @@ "@types/node": "^24.7.2", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", + "@types/webextension-polyfill": "^0.12.4", "@vitejs/plugin-react": "^5.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -36,13 +39,14 @@ "tailwindcss": "^4.1.14", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "vite": "^7.1.7", - "@openbanker/plugins": "workspace:*", - "@openbanker/core": "workspace:*" + "vite": "^7.1.7" }, "scripts": { "dev": "cross-env PORT=8001 vite", "build": "tsc -b && vite build", "preview": "vite preview" + }, + "dependencies": { + "webextension-polyfill": "^0.12.0" } } diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index b965ca0..f86dde5 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -6,9 +6,11 @@ import config from "@/config"; import TransactionsTable from "@/components/TransactionsTable"; import ActionButtons from "@/components/ActionButtons"; import EmptyState from "@/components/EmptyState"; -import { emptyTransactionStore } from "@openbanker/core/types"; +// Import Transaction type to use it for casting +import { emptyTransactionStore, type Transaction } from "@openbanker/core/types"; import { getChromeContext } from "@/lib/utils"; import useChromeStorage from "@/hooks/useChromeStorage"; +import browser from "webextension-polyfill"; export default function App() { const [transactionStore, setTransactionStore] = useChromeStorage("transactionStore", emptyTransactionStore()) @@ -21,25 +23,24 @@ export default function App() { setTransactionStore(emptyTransactionStore()) async function detectTransactions() { - let [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + let [tab] = await browser.tabs.query({ active: true, currentWindow: true }); if (!tab.url || !tab.id) return; const plugin = config.plugins.find(p => p.urlPattern.test(tab.url ?? "FALLBACK")); if (plugin !== undefined) { - const res = await chrome.scripting.executeScript({ + const res = await browser.scripting.executeScript({ target: { tabId: tab.id }, func: plugin.scrapeFunc, }); if (res && res[0] && res[0].result) { - setTransactionStore({ pluginName: plugin.name, transactions: res[0].result }); + const transactions = res[0].result as Transaction[]; + setTransactionStore({ pluginName: plugin.name, transactions }); } - } } detectTransactions(); - }, []); return ( diff --git a/packages/frontend/src/hooks/useChromeStorage.ts b/packages/frontend/src/hooks/useChromeStorage.ts index ad586d4..08049bc 100644 --- a/packages/frontend/src/hooks/useChromeStorage.ts +++ b/packages/frontend/src/hooks/useChromeStorage.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from "react"; import { type AppStorage } from "@openbanker/core/types"; import { getChromeContext } from "@/lib/utils"; +import browser from "webextension-polyfill"; export const CHROME_STORAGE_STRATEGY: StorageArea = "local"; @@ -26,7 +27,7 @@ function useChromeStorage( if (getChromeContext() !== "extension") return; const handleStorageChange = ( - changes: { [key: string]: chrome.storage.StorageChange }, + changes: { [key: string]: browser.Storage.StorageChange }, areaName: string ) => { if (areaName === storageArea && key in changes) { @@ -34,35 +35,25 @@ function useChromeStorage( } }; - chrome.storage.onChanged.addListener(handleStorageChange); + browser.storage.onChanged.addListener(handleStorageChange); - chrome.storage[storageArea].get([key], (result) => { - if (chrome.runtime.lastError) { - console.error( - `Error getting ${String(key)} from chrome.storage.${storageArea}:`, - chrome.runtime.lastError - ); - return; - } - - if (result[key] !== undefined) { - setStoredValue(result[key] as AppStorage[K]); - } else { - chrome.storage[storageArea].set({ [key]: initialValue }, () => { - if (chrome.runtime.lastError) { - console.error( - `Error setting initial ${String( - key - )} in chrome.storage.${storageArea}:`, - chrome.runtime.lastError - ); - } - }); - } - }); + browser.storage[storageArea].get([key]) + .then((result) => { + if (result[key] !== undefined) { + setStoredValue(result[key] as AppStorage[K]); + } else { + // If key doesn't exist, set the initial value + browser.storage[storageArea].set({ [key]: initialValue }).catch((err) => { + console.error(`Error setting initial value for ${String(key)}:`, err); + }); + } + }) + .catch((error) => { + console.error(`Error getting ${String(key)} from storage:`, error); + }); return () => { - chrome.storage.onChanged.removeListener(handleStorageChange); + browser.storage.onChanged.removeListener(handleStorageChange); }; }, [key, initialValue, storageArea]); @@ -73,13 +64,8 @@ function useChromeStorage( const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); - chrome.storage[storageArea].set({ [key]: valueToStore }, () => { - if (chrome.runtime.lastError) { - console.error( - `Error setting ${String(key)} in chrome.storage.${storageArea}:`, - chrome.runtime.lastError - ); - } + browser.storage[storageArea].set({ [key]: valueToStore }).catch((error) => { + console.error(`Error setting ${String(key)} in storage:`, error); }); }, [key, storageArea, storedValue] diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts index 5463bab..b9d878d 100644 --- a/packages/frontend/src/lib/utils.ts +++ b/packages/frontend/src/lib/utils.ts @@ -1,17 +1,19 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" +// [FIX] Tell TypeScript that 'browser' is a valid global variable +declare const browser: any; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } export function getChromeContext(): 'extension' | 'web_page' { - if(import.meta.env.APP_MODE === "SOLO") return 'web_page'; + if (import.meta.env.APP_MODE === "SOLO") return 'web_page'; - if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) { - return 'extension'; - } else { - return 'web_page'; - } + const isExtension = + (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) || + (typeof browser !== 'undefined' && browser.runtime && browser.runtime.id); + + return isExtension ? 'extension' : 'web_page'; }