diff --git a/bun.lockb b/bun.lockb index 1d129f4..5e451d1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/chrome/public/manifest.json b/packages/chrome/manifest.base.json similarity index 57% rename from packages/chrome/public/manifest.json rename to packages/chrome/manifest.base.json index 4519466..8e66169 100644 --- a/packages/chrome/public/manifest.json +++ b/packages/chrome/manifest.base.json @@ -8,24 +8,12 @@ "default_title": "Click to open panel", "default_popup": "index.html" }, - "background": { - "service_worker": "background.js", - "type": "module" - }, "content_scripts": [ { - "js": [ - "bridge.js" - ], - "matches": [ - "" - ], + "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'; }