diff --git a/.github/actions/bump-manifest-version.cjs b/.github/actions/bump-manifest-version.cjs index 89ec7cd8..c7f001c2 100644 --- a/.github/actions/bump-manifest-version.cjs +++ b/.github/actions/bump-manifest-version.cjs @@ -1,28 +1,28 @@ // @ts-check /* eslint-disable @typescript-eslint/no-require-imports, no-console */ -const fs = require('node:fs/promises') +const fs = require('node:fs/promises'); /** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ module.exports = async ({ core }) => { - const manifestPath = './src/manifest.json' - const manifestFile = await fs.readFile(manifestPath, 'utf8') - const manifest = JSON.parse(manifestFile) + const manifestPath = './src/manifest.json'; + const manifestFile = await fs.readFile(manifestPath, 'utf8'); + const manifest = JSON.parse(manifestFile); /**@type {string} */ - const existingVersion = manifest.version + const existingVersion = manifest.version; - const bumpType = /** @type {BumpType} */ (process.env.INPUT_VERSION) + const bumpType = /** @type {BumpType} */ (process.env.INPUT_VERSION); if (!bumpType) { - throw new Error('Missing bump type') + throw new Error('Missing bump type'); } - const version = bumpVersion(existingVersion, bumpType).join('.') + const version = bumpVersion(existingVersion, bumpType).join('.'); - console.log({ existingVersion, bumpType, version }) + console.log({ existingVersion, bumpType, version }); - manifest.version = version - await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)) - core.setOutput('version', version) -} + manifest.version = version; + await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); + core.setOutput('version', version); +}; /** * @typedef {'build' | 'patch' | 'minor'} BumpType @@ -31,20 +31,20 @@ module.exports = async ({ core }) => { * @return {[major: number, minor: number, patch: number, build: number]} */ function bumpVersion(existingVersion, type) { - const parts = existingVersion.split('.').map(Number) + const parts = existingVersion.split('.').map(Number); if (parts.length !== 4 || parts.some((e) => !Number.isSafeInteger(e))) { - throw new Error('Existing version does not have right format') + throw new Error('Existing version does not have right format'); } - const [major, minor, patch, build] = parts + const [major, minor, patch, build] = parts; switch (type) { case 'build': - return [major, minor, patch, build + 1] + return [major, minor, patch, build + 1]; case 'patch': - return [major, minor, patch + 1, 0] + return [major, minor, patch + 1, 0]; case 'minor': - return [major, minor + 1, 0, 0] + return [major, minor + 1, 0, 0]; default: - throw new Error('Unknown bump type: ' + type) + throw new Error('Unknown bump type: ' + type); } } diff --git a/.github/actions/constants.cjs b/.github/actions/constants.cjs index 0c499cc7..3ba55382 100644 --- a/.github/actions/constants.cjs +++ b/.github/actions/constants.cjs @@ -5,25 +5,25 @@ */ const BADGE = - 'Badge' + 'Badge'; /** @type {Browser[]} */ -const BROWSERS = ['chrome', 'firefox'] +const BROWSERS = ['chrome', 'firefox']; const COLORS = { green: '3fb950', - red: 'd73a49' -} + red: 'd73a49', +}; const TEMPLATE_VARS = { tableBody: '{{ TABLE_BODY }}', sha: '{{ SHA }}', conclusion: '{{ CONCLUSION }}', badgeColor: '{{ BADGE_COLOR }}', badgeLabel: '{{ BADGE_LABEL }}', - jobLogs: '{{ JOB_LOGS }}' -} + jobLogs: '{{ JOB_LOGS }}', +}; module.exports = { BADGE, BROWSERS, COLORS, - TEMPLATE_VARS -} + TEMPLATE_VARS, +}; diff --git a/.github/actions/delete-artifacts.cjs b/.github/actions/delete-artifacts.cjs index 7cdf1fe9..2aa9a3d4 100644 --- a/.github/actions/delete-artifacts.cjs +++ b/.github/actions/delete-artifacts.cjs @@ -1,6 +1,6 @@ // @ts-check /* eslint-disable @typescript-eslint/no-require-imports, no-console */ -const { BROWSERS } = require('./constants.cjs') +const { BROWSERS } = require('./constants.cjs'); /** * @param {Pick} AsyncFunctionArguments @@ -10,9 +10,9 @@ async function getBrowserArtifacts({ github, context }, name) { const result = await github.rest.actions.listArtifactsForRepo({ owner: context.repo.owner, repo: context.repo.repo, - name - }) - return result.data.artifacts + name, + }); + return result.data.artifacts; } /** @@ -22,40 +22,40 @@ async function getBrowserArtifacts({ github, context }, name) { async function getPRArtifacts({ github, context }, prNumber) { const data = await Promise.all( BROWSERS.map((browser) => - getBrowserArtifacts({ github, context }, `${prNumber}-${browser}`) - ) - ) + getBrowserArtifacts({ github, context }, `${prNumber}-${browser}`), + ), + ); /** @type {{id: number}[]} */ - const artifacts = [] + const artifacts = []; for (let i = 0; i < data.length; i++) { // same as `artifacts.push(...data[i])` but it's a bit faster - artifacts.push.apply(artifacts, data[i]) + artifacts.push.apply(artifacts, data[i]); } - return artifacts + return artifacts; } /** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ module.exports = async ({ github, context, core }) => { if (context.payload.action !== 'closed') { - core.setFailed('This action only works on closed PRs.') + core.setFailed('This action only works on closed PRs.'); } - const { owner, repo } = context.repo + const { owner, repo } = context.repo; /** @type {number} */ - const prNumber = context.payload.number + const prNumber = context.payload.number; - const artifacts = await getPRArtifacts({ github, context }, prNumber) + const artifacts = await getPRArtifacts({ github, context }, prNumber); await Promise.all( artifacts.map((artifact) => github.rest.actions.deleteArtifact({ owner, repo, - artifact_id: artifact.id - }) - ) - ) + artifact_id: artifact.id, + }), + ), + ); - console.log(`Deleted ${artifacts.length} artifacts for PR #${prNumber}.`) -} + console.log(`Deleted ${artifacts.length} artifacts for PR #${prNumber}.`); +}; diff --git a/.github/actions/get-built-version.cjs b/.github/actions/get-built-version.cjs index 4bc30d3f..56a24200 100644 --- a/.github/actions/get-built-version.cjs +++ b/.github/actions/get-built-version.cjs @@ -1,6 +1,6 @@ // @ts-check /* eslint-disable @typescript-eslint/no-require-imports */ -const fs = require('node:fs/promises') +const fs = require('node:fs/promises'); /** * Retrieves the manifest version from the built extension. @@ -9,7 +9,7 @@ const fs = require('node:fs/promises') module.exports = async ({ core }) => { const manifest = await fs .readFile('./dist/chrome/manifest.json', 'utf8') - .then(JSON.parse) + .then(JSON.parse); - core.setOutput('version', manifest.version) -} + core.setOutput('version', manifest.version); +}; diff --git a/.github/actions/get-workflow-artifacts.cjs b/.github/actions/get-workflow-artifacts.cjs index cd89b196..d3fb8aa2 100644 --- a/.github/actions/get-workflow-artifacts.cjs +++ b/.github/actions/get-workflow-artifacts.cjs @@ -1,7 +1,7 @@ // @ts-check /* eslint-disable @typescript-eslint/no-require-imports, no-console */ -const fs = require('node:fs/promises') -const { COLORS, TEMPLATE_VARS, BADGE } = require('./constants.cjs') +const fs = require('node:fs/promises'); +const { COLORS, TEMPLATE_VARS, BADGE } = require('./constants.cjs'); /** * @typedef {import('./constants.cjs').Browser} Browser @@ -12,14 +12,14 @@ const ARTIFACTS_DATA = { chrome: { name: 'Chrome', url: '', - size: '' + size: '', }, firefox: { name: 'Firefox', url: '', - size: '' - } -} + size: '', + }, +}; /** * @param {string} conclusion @@ -29,7 +29,7 @@ const ARTIFACTS_DATA = { function getBadge(conclusion, badgeColor, badgeLabel) { return BADGE.replace(TEMPLATE_VARS.conclusion, conclusion) .replace(TEMPLATE_VARS.badgeColor, badgeColor) - .replace(TEMPLATE_VARS.badgeLabel, badgeLabel) + .replace(TEMPLATE_VARS.badgeLabel, badgeLabel); } /** @@ -37,73 +37,73 @@ function getBadge(conclusion, badgeColor, badgeLabel) { * @param {number} decimals */ function formatBytes(bytes, decimals = 2) { - if (!Number(bytes)) return '0B' - const k = 1024 - const dm = decimals < 0 ? 0 : decimals - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}` + if (!Number(bytes)) return '0B'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`; } /** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ module.exports = async ({ github, context, core }) => { - const { owner, repo } = context.repo - const baseUrl = context.payload.repository?.html_url - const suiteId = context.payload.workflow_run.check_suite_id - const runId = context.payload.workflow_run.id - const conclusion = context.payload.workflow_run.conclusion - const sha = context.payload.workflow_run.pull_requests[0].head.sha - const prNumber = context.payload.workflow_run.pull_requests[0].number - const jobLogsUrl = `${baseUrl}/actions/runs/${context.payload.workflow_run.id}` + const { owner, repo } = context.repo; + const baseUrl = context.payload.repository?.html_url; + const suiteId = context.payload.workflow_run.check_suite_id; + const runId = context.payload.workflow_run.id; + const conclusion = context.payload.workflow_run.conclusion; + const sha = context.payload.workflow_run.pull_requests[0].head.sha; + const prNumber = context.payload.workflow_run.pull_requests[0].number; + const jobLogsUrl = `${baseUrl}/actions/runs/${context.payload.workflow_run.id}`; const template = await fs.readFile( './.github/actions/templates/build-status.md', - 'utf8' - ) + 'utf8', + ); /** @type {string[]} */ - const tableRows = [] + const tableRows = []; - core.setOutput('conclusion', conclusion) + core.setOutput('conclusion', conclusion); if (conclusion === 'cancelled') { - return + return; } const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, - run_id: runId - }) + run_id: runId, + }); artifacts.data.artifacts.forEach((artifact) => { - const key = /** @type {Browser} */ (artifact.name.split('-')[1]) + const key = /** @type {Browser} */ (artifact.name.split('-')[1]); ARTIFACTS_DATA[key].url = - `${baseUrl}/suites/${suiteId}/artifacts/${artifact.id}` - ARTIFACTS_DATA[key].size = formatBytes(artifact.size_in_bytes) - }) + `${baseUrl}/suites/${suiteId}/artifacts/${artifact.id}`; + ARTIFACTS_DATA[key].size = formatBytes(artifact.size_in_bytes); + }); Object.keys(ARTIFACTS_DATA).forEach((k) => { - const { name, url, size } = ARTIFACTS_DATA[/** @type {Browser} */ (k)] + const { name, url, size } = ARTIFACTS_DATA[/** @type {Browser} */ (k)]; if (!url && !size) { - const badgeUrl = getBadge('failure', COLORS.red, name) + const badgeUrl = getBadge('failure', COLORS.red, name); tableRows.push( - `${badgeUrl}N/A` - ) + `${badgeUrl}N/A`, + ); } else { - const badgeUrl = getBadge('success', COLORS.green, `${name} (${size})`) + const badgeUrl = getBadge('success', COLORS.green, `${name} (${size})`); tableRows.push( - `${badgeUrl}Download` - ) + `${badgeUrl}Download`, + ); } - }) + }); - const tableBody = tableRows.join('') + const tableBody = tableRows.join(''); const commentBody = template .replace(TEMPLATE_VARS.conclusion, conclusion) .replace(TEMPLATE_VARS.sha, sha) .replace(TEMPLATE_VARS.jobLogs, `Run #${runId}`) - .replace(TEMPLATE_VARS.tableBody, tableBody) + .replace(TEMPLATE_VARS.tableBody, tableBody); - core.setOutput('comment_body', commentBody) - core.setOutput('pr_number', prNumber) -} + core.setOutput('comment_body', commentBody); + core.setOutput('pr_number', prNumber); +}; diff --git a/.github/actions/validate-stable-release.cjs b/.github/actions/validate-stable-release.cjs index d3f3ebd0..2eeba23f 100644 --- a/.github/actions/validate-stable-release.cjs +++ b/.github/actions/validate-stable-release.cjs @@ -7,34 +7,36 @@ */ module.exports = async ({ github, context }) => { if (context.ref !== 'refs/heads/main') { - throw new Error('This action only works on main branch') + throw new Error('This action only works on main branch'); } - const { owner, repo } = context.repo - const previewVersionTag = process.env.INPUT_VERSION + const { owner, repo } = context.repo; + const previewVersionTag = process.env.INPUT_VERSION; if (!previewVersionTag) { - throw new Error('Missing env.INPUT_VERSION') + throw new Error('Missing env.INPUT_VERSION'); } if (!previewVersionTag.match(/^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+-preview$/)) { - throw new Error('Input "version" must match vX.X.X.X-preview') + throw new Error('Input "version" must match vX.X.X.X-preview'); } - const versionTag = previewVersionTag.replace('-preview', '') + const versionTag = previewVersionTag.replace('-preview', ''); try { await github.rest.repos.getReleaseByTag({ owner, repo, - tag: versionTag - }) - throw new Error('Release already promoted to stable') + tag: versionTag, + }); + throw new Error('Release already promoted to stable'); } catch (error) { if (!error.status) { - throw error + throw error; } if (error.status === 404) { // do nothing } else { - throw new Error(`Failed to check: HTTP ${error.status}`, { cause: error }) + throw new Error(`Failed to check: HTTP ${error.status}`, { + cause: error, + }); } } -} +}; diff --git a/.prettierrc.js b/.prettierrc.js index 43ce286a..1c453e54 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,8 +1,8 @@ module.exports = { singleQuote: true, - trailingComma: 'none', + trailingComma: 'all', jsxSingleQuote: false, - semi: false, + semi: true, plugins: ['prettier-plugin-tailwindcss'], tailwindFunctions: [ 'classnames', @@ -13,6 +13,6 @@ module.exports = { 'twStyle', 'twMerge', 'twJoin', - 'cn' - ] -} + 'cn', + ], +}; diff --git a/esbuild/config.ts b/esbuild/config.ts index 5f99811c..ce1250e4 100644 --- a/esbuild/config.ts +++ b/esbuild/config.ts @@ -1,41 +1,41 @@ -import path from 'node:path' -import type { BuildOptions } from 'esbuild' -import type { Manifest } from 'webextension-polyfill' +import path from 'node:path'; +import type { BuildOptions } from 'esbuild'; +import type { Manifest } from 'webextension-polyfill'; -export const TARGETS = ['chrome', 'firefox'] as const -export const CHANNELS = ['nightly', 'preview', 'stable'] as const +export const TARGETS = ['chrome', 'firefox'] as const; +export const CHANNELS = ['nightly', 'preview', 'stable'] as const; -export const ROOT_DIR = path.resolve(__dirname, '..') -export const SRC_DIR = path.resolve(ROOT_DIR, 'src') -export const DEV_DIR = path.resolve(ROOT_DIR, 'dev') -export const DIST_DIR = path.resolve(ROOT_DIR, 'dist') +export const ROOT_DIR = path.resolve(__dirname, '..'); +export const SRC_DIR = path.resolve(ROOT_DIR, 'src'); +export const DEV_DIR = path.resolve(ROOT_DIR, 'dev'); +export const DIST_DIR = path.resolve(ROOT_DIR, 'dist'); -export type Target = (typeof TARGETS)[number] -export type Channel = (typeof CHANNELS)[number] +export type Target = (typeof TARGETS)[number]; +export type Channel = (typeof CHANNELS)[number]; export type BuildArgs = { - target: Target - channel: Channel - dev: boolean -} + target: Target; + channel: Channel; + dev: boolean; +}; export const options: BuildOptions = { entryPoints: [ { in: path.join(SRC_DIR, 'background', 'index.ts'), - out: path.join('background', 'background') + out: path.join('background', 'background'), }, { in: path.join(SRC_DIR, 'content', 'index.ts'), - out: path.join('content', 'content') + out: path.join('content', 'content'), }, { in: path.join(SRC_DIR, 'content', 'polyfill.ts'), - out: path.join('polyfill', 'polyfill') + out: path.join('polyfill', 'polyfill'), }, { in: path.join(SRC_DIR, 'popup', 'index.tsx'), - out: path.join('popup', 'popup') - } + out: path.join('popup', 'popup'), + }, ], bundle: true, legalComments: 'none', @@ -44,14 +44,14 @@ export const options: BuildOptions = { format: 'iife', write: true, logLevel: 'info', - treeShaking: true -} + treeShaking: true, +}; export type WebExtensionManifest = Manifest.WebExtensionManifest & { - background: Manifest.WebExtensionManifestBackgroundC3Type -} + background: Manifest.WebExtensionManifestBackgroundC3Type; +}; export const SERVE_PORTS: Record = { chrome: 7000, - firefox: 7002 -} + firefox: 7002, +}; diff --git a/esbuild/dev.ts b/esbuild/dev.ts index d5b7bf46..ce9dfb41 100644 --- a/esbuild/dev.ts +++ b/esbuild/dev.ts @@ -1,15 +1,15 @@ -import { readFile } from 'node:fs/promises' -import type { BuildOptions, Plugin as ESBuildPlugin } from 'esbuild' -import { SERVE_PORTS, type BuildArgs, type Target } from './config' -import { getPlugins } from './plugins' -import { typecheckPlugin } from '@jgoz/esbuild-plugin-typecheck' +import { readFile } from 'node:fs/promises'; +import type { BuildOptions, Plugin as ESBuildPlugin } from 'esbuild'; +import { SERVE_PORTS, type BuildArgs, type Target } from './config'; +import { getPlugins } from './plugins'; +import { typecheckPlugin } from '@jgoz/esbuild-plugin-typecheck'; export const getDevOptions = ({ outDir, target, - channel + channel, }: Omit & { - outDir: string + outDir: string; }): BuildOptions => { return { sourcemap: 'linked', @@ -17,24 +17,24 @@ export const getDevOptions = ({ minify: false, plugins: getPlugins({ outDir, dev: true, target, channel }).concat([ typecheckPlugin({ buildMode: 'readonly', watch: true }), - liveReloadPlugin({ target }) + liveReloadPlugin({ target }), ]), define: { NODE_ENV: JSON.stringify('development'), CONFIG_LOG_LEVEL: JSON.stringify('DEBUG'), CONFIG_PERMISSION_HOSTS: JSON.stringify({ - origins: ['http://*/*', 'https://*/*'] + origins: ['http://*/*', 'https://*/*'], }), CONFIG_ALLOWED_PROTOCOLS: JSON.stringify(['http:', 'https:']), CONFIG_OPEN_PAYMENTS_REDIRECT_URL: JSON.stringify( - 'https://webmonetization.org/welcome' - ) - } - } -} + 'https://webmonetization.org/welcome', + ), + }, + }; +}; function liveReloadPlugin({ target }: { target: Target }): ESBuildPlugin { - const port = SERVE_PORTS[target] + const port = SERVE_PORTS[target]; const reloadScriptBackground = ` new EventSource("http://localhost:${port}/esbuild").addEventListener( "change", @@ -49,7 +49,7 @@ function liveReloadPlugin({ target }: { target: Target }): ESBuildPlugin { await browser.runtime.reload(); } } - );` + );`; const reloadScriptPopup = ` new EventSource("http://localhost:${port}/esbuild").addEventListener( @@ -63,26 +63,26 @@ function liveReloadPlugin({ target }: { target: Target }): ESBuildPlugin { globalThis.location.reload(); } } - );` + );`; return { name: 'live-reload', setup(build) { build.onLoad({ filter: /src\/background\/index\.ts$/ }, async (args) => { - const contents = await readFile(args.path, 'utf8') + const contents = await readFile(args.path, 'utf8'); return { contents: reloadScriptBackground + '\n' + contents, - loader: 'ts' as const - } - }) + loader: 'ts' as const, + }; + }); build.onLoad({ filter: /src\/popup\/index\.tsx$/ }, async (args) => { - const contents = await readFile(args.path, 'utf8') + const contents = await readFile(args.path, 'utf8'); return { contents: contents + '\n\n\n' + reloadScriptPopup, - loader: 'tsx' as const - } - }) - } - } + loader: 'tsx' as const, + }; + }); + }, + }; } diff --git a/esbuild/plugins.ts b/esbuild/plugins.ts index 73cdfa3b..b996be81 100644 --- a/esbuild/plugins.ts +++ b/esbuild/plugins.ts @@ -1,26 +1,26 @@ -import path from 'node:path' -import fs from 'node:fs/promises' -import type { Plugin as ESBuildPlugin } from 'esbuild' -import { nodeBuiltin } from 'esbuild-node-builtin' -import esbuildStylePlugin from 'esbuild-style-plugin' -import { copy } from 'esbuild-plugin-copy' -import tailwind from 'tailwindcss' -import autoprefixer from 'autoprefixer' +import path from 'node:path'; +import fs from 'node:fs/promises'; +import type { Plugin as ESBuildPlugin } from 'esbuild'; +import { nodeBuiltin } from 'esbuild-node-builtin'; +import esbuildStylePlugin from 'esbuild-style-plugin'; +import { copy } from 'esbuild-plugin-copy'; +import tailwind from 'tailwindcss'; +import autoprefixer from 'autoprefixer'; import { SRC_DIR, ROOT_DIR, type BuildArgs, - type WebExtensionManifest -} from './config' + type WebExtensionManifest, +} from './config'; export const getPlugins = ({ outDir, target, channel, - dev + dev, }: BuildArgs & { - outDir: string + outDir: string; }): ESBuildPlugin[] => { return [ cleanPlugin([outDir]), @@ -33,38 +33,38 @@ export const getPlugins = ({ name: 'crypto-for-extension', setup(build) { build.onResolve({ filter: /^crypto$/ }, () => ({ - path: require.resolve('crypto-browserify') - })) - } + path: require.resolve('crypto-browserify'), + })); + }, }, ignorePackagePlugin([/@apidevtools[/|\\]json-schema-ref-parser/]), esbuildStylePlugin({ extract: true, postcss: { - plugins: [tailwind, autoprefixer] - } + plugins: [tailwind, autoprefixer], + }, }), copy({ resolveFrom: ROOT_DIR, assets: [ { from: path.join(SRC_DIR, 'popup', 'index.html'), - to: path.join(outDir, 'popup', 'index.html') + to: path.join(outDir, 'popup', 'index.html'), }, { from: path.join(SRC_DIR, '_locales/**/*'), - to: path.join(outDir, '_locales') + to: path.join(outDir, '_locales'), }, { from: path.join(SRC_DIR, 'assets/**/*'), - to: path.join(outDir, 'assets') - } + to: path.join(outDir, 'assets'), + }, ], - watch: dev + watch: dev, }), - processManifestPlugin({ outDir, dev, target, channel }) - ] -} + processManifestPlugin({ outDir, dev, target, channel }), + ]; +}; // Based on https://github.com/Knowre-Dev/esbuild-plugin-ignore function ignorePackagePlugin(ignores: RegExp[]): ESBuildPlugin { @@ -73,59 +73,59 @@ function ignorePackagePlugin(ignores: RegExp[]): ESBuildPlugin { setup(build) { build.onResolve({ filter: /.*/, namespace: 'ignore' }, (args) => ({ path: args.path, - namespace: 'ignore' - })) + namespace: 'ignore', + })); for (const ignorePattern of ignores) { build.onResolve({ filter: ignorePattern }, (args) => { - return { path: args.path, namespace: 'ignore' } - }) + return { path: args.path, namespace: 'ignore' }; + }); } build.onLoad({ filter: /.*/, namespace: 'ignore' }, () => ({ - contents: '' - })) - } - } + contents: '', + })); + }, + }; } function processManifestPlugin({ outDir, target, channel, - dev + dev, }: BuildArgs & { outDir: string }): ESBuildPlugin { return { name: 'process-manifest', setup(build) { build.onEnd(async () => { - const src = path.join(SRC_DIR, 'manifest.json') - const dest = path.join(outDir, 'manifest.json') + const src = path.join(SRC_DIR, 'manifest.json'); + const dest = path.join(outDir, 'manifest.json'); const json = JSON.parse( - await fs.readFile(src, 'utf8') - ) as WebExtensionManifest + await fs.readFile(src, 'utf8'), + ) as WebExtensionManifest; // Transform manifest as targets have different expectations // @ts-expect-error Only for IDE. No target accepts it - delete json['$schema'] + delete json['$schema']; if (channel === 'nightly') { // Set version to YYYY.M.D - const now = new Date() + const now = new Date(); const [year, month, day] = [ now.getFullYear(), now.getMonth() + 1, - now.getDate() - ] - json.version = `${year}.${month}.${day}` + now.getDate(), + ]; + json.version = `${year}.${month}.${day}`; if (target !== 'firefox') { - json.version_name = `Nightly ${json.version}` + json.version_name = `Nightly ${json.version}`; } } if (channel === 'preview') { - json.name = json.name + ' Preview' + json.name = json.name + ' Preview'; } else if (channel === 'nightly') { - json.name = json.name + ' Nightly' + json.name = json.name + ' Nightly'; } if (dev) { @@ -133,34 +133,34 @@ function processManifestPlugin({ json.host_permissions && !json.host_permissions.includes('http://*/*') ) { - json.host_permissions.push('http://*/*') + json.host_permissions.push('http://*/*'); } json.content_scripts?.forEach((contentScript) => { if (!contentScript.matches.includes('http://*/*')) { - contentScript.matches.push('http://*/*') + contentScript.matches.push('http://*/*'); } - }) + }); } if (target === 'firefox') { // @ts-expect-error Firefox doesn't support Service Worker in MV3 yet json.background = { - scripts: [json.background.service_worker] - } + scripts: [json.background.service_worker], + }; json.content_scripts?.forEach((contentScript) => { // TODO: Remove this when Firefox supports `world` - at least last 10 // versions - contentScript.world = undefined - }) - delete json.minimum_chrome_version + contentScript.world = undefined; + }); + delete json.minimum_chrome_version; } else { - delete json['browser_specific_settings'] + delete json['browser_specific_settings']; } - await fs.writeFile(dest, JSON.stringify(json, null, 2)) - }) - } - } + await fs.writeFile(dest, JSON.stringify(json, null, 2)); + }); + }, + }; } function cleanPlugin(dirs: string[]): ESBuildPlugin { @@ -169,9 +169,9 @@ function cleanPlugin(dirs: string[]): ESBuildPlugin { setup(build) { build.onStart(async () => { await Promise.all( - dirs.map((dir) => fs.rm(dir, { recursive: true, force: true })) - ) - }) - } - } + dirs.map((dir) => fs.rm(dir, { recursive: true, force: true })), + ); + }); + }, + }; } diff --git a/esbuild/prod.ts b/esbuild/prod.ts index e6c8a32c..8355605a 100644 --- a/esbuild/prod.ts +++ b/esbuild/prod.ts @@ -1,19 +1,24 @@ /* eslint-disable no-console */ -import fs from 'node:fs/promises' -import { createWriteStream } from 'node:fs' -import path from 'node:path' -import type { BuildOptions, Plugin as ESBuildPlugin } from 'esbuild' -import archiver from 'archiver' -import type { BuildArgs, Channel, Target, WebExtensionManifest } from './config' -import { getPlugins } from './plugins' -import { typecheckPlugin } from '@jgoz/esbuild-plugin-typecheck' +import fs from 'node:fs/promises'; +import { createWriteStream } from 'node:fs'; +import path from 'node:path'; +import type { BuildOptions, Plugin as ESBuildPlugin } from 'esbuild'; +import archiver from 'archiver'; +import type { + BuildArgs, + Channel, + Target, + WebExtensionManifest, +} from './config'; +import { getPlugins } from './plugins'; +import { typecheckPlugin } from '@jgoz/esbuild-plugin-typecheck'; export const getProdOptions = ({ outDir, target, - channel + channel, }: Omit & { - outDir: string + outDir: string; }): BuildOptions => { return { sourcemap: false, @@ -22,7 +27,7 @@ export const getProdOptions = ({ plugins: getPlugins({ outDir, dev: false, target, channel }).concat([ typecheckPlugin({ buildMode: 'readonly' }), preservePolyfillClassNamesPlugin({ outDir }), - zipPlugin({ outDir, target, channel }) + zipPlugin({ outDir, target, channel }), ]), define: { NODE_ENV: JSON.stringify('production'), @@ -30,92 +35,92 @@ export const getProdOptions = ({ CONFIG_PERMISSION_HOSTS: JSON.stringify({ origins: ['https://*/*'] }), CONFIG_ALLOWED_PROTOCOLS: JSON.stringify(['https:']), CONFIG_OPEN_PAYMENTS_REDIRECT_URL: JSON.stringify( - 'https://webmonetization.org/welcome' - ) - } - } -} + 'https://webmonetization.org/welcome', + ), + }, + }; +}; function zipPlugin({ outDir, target, - channel + channel, }: { - channel: Channel - target: Target - outDir: string + channel: Channel; + target: Target; + outDir: string; }): ESBuildPlugin { return { name: 'zip', setup(build) { build.onEnd(async () => { const manifest = JSON.parse( - await fs.readFile(path.join(outDir, 'manifest.json'), 'utf8') - ) as WebExtensionManifest + await fs.readFile(path.join(outDir, 'manifest.json'), 'utf8'), + ) as WebExtensionManifest; - let zipName = `${target}-${manifest.version}.zip` + let zipName = `${target}-${manifest.version}.zip`; if (channel !== 'stable') { - zipName = `${channel}-${zipName}` + zipName = `${channel}-${zipName}`; } - const dest = path.join(outDir, '..', zipName) - const output = createWriteStream(dest) - const archive = archiver('zip') + const dest = path.join(outDir, '..', zipName); + const output = createWriteStream(dest); + const archive = archiver('zip'); archive.on('end', function () { - const archiveSize = archive.pointer() - const fileName = path.relative(process.cwd(), dest) - console.log(` Archived ${fileName}: ${formatBytes(archiveSize)}`) - }) - archive.pipe(output) - archive.glob('**/*', { cwd: outDir, ignore: ['meta.json'] }) - await archive.finalize() - }) - } - } + const archiveSize = archive.pointer(); + const fileName = path.relative(process.cwd(), dest); + console.log(` Archived ${fileName}: ${formatBytes(archiveSize)}`); + }); + archive.pipe(output); + archive.glob('**/*', { cwd: outDir, ignore: ['meta.json'] }); + await archive.finalize(); + }); + }, + }; } /** * Unmangles the MonetizationEvent class */ function preservePolyfillClassNamesPlugin({ - outDir + outDir, }: { - outDir: string + outDir: string; }): ESBuildPlugin { return { name: 'preserve-polyfill-class-names', setup(build) { build.onEnd(async () => { - const polyfillPath = path.join(outDir, 'polyfill', 'polyfill.js') - const polyfillContent = await fs.readFile(polyfillPath, 'utf8') - const definitionRegex = /class\s+([A-Za-z_$][\w$]*)\s+extends\s+Event/ + const polyfillPath = path.join(outDir, 'polyfill', 'polyfill.js'); + const polyfillContent = await fs.readFile(polyfillPath, 'utf8'); + const definitionRegex = /class\s+([A-Za-z_$][\w$]*)\s+extends\s+Event/; - const match = polyfillContent.match(definitionRegex) + const match = polyfillContent.match(definitionRegex); if (!match) { - throw new Error('Could not find MonetizationEvent definition') + throw new Error('Could not find MonetizationEvent definition'); } - const minifiedName = match[1] + const minifiedName = match[1]; const result = polyfillContent .replace(definitionRegex, `class MonetizationEvent extends Event`) .replace( `window.MonetizationEvent=${minifiedName}`, - `window.MonetizationEvent=MonetizationEvent` + `window.MonetizationEvent=MonetizationEvent`, ) - .replaceAll(`new ${minifiedName}`, 'new MonetizationEvent') + .replaceAll(`new ${minifiedName}`, 'new MonetizationEvent'); - await fs.writeFile(polyfillPath, result) - }) - } - } + await fs.writeFile(polyfillPath, result); + }); + }, + }; } function formatBytes(bytes: number, decimals: number = 2) { - if (!Number(bytes)) return '0B' - const k = 1024 - const dm = decimals < 0 ? 0 : decimals - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}` + if (!Number(bytes)) return '0B'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`; } diff --git a/jest.config.ts b/jest.config.ts index f911435f..ef369982 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -7,25 +7,25 @@ export default { '!src/**/*.css', '!src/**/*.svg', '!src/**/*.d.ts', - '!src/**/index.ts' + '!src/**/index.ts', ], coverageDirectory: 'coverage', coverageProvider: 'v8', maxWorkers: '50%', moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'], moduleNameMapper: { - '@/(.*)': '/src/$1' + '@/(.*)': '/src/$1', }, setupFilesAfterEnv: ['./jest.setup.ts'], testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], testEnvironment: 'jsdom', testPathIgnorePatterns: [ '/node_modules/', - '/jest.config.ts' + '/jest.config.ts', ], transform: { '^.+\\.(js|jsx)$': 'babel-jest', '^.+\\.(ts|tsx)?$': 'ts-jest', - '\\.(css|less|scss|sass|svg)$': 'jest-transform-stub' - } -} + '\\.(css|less|scss|sass|svg)$': 'jest-transform-stub', + }, +}; diff --git a/jest.setup.ts b/jest.setup.ts index 60d87370..e524c769 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,5 +1,5 @@ -import '@testing-library/jest-dom' +import '@testing-library/jest-dom'; -import { chrome } from 'jest-chrome' +import { chrome } from 'jest-chrome'; -Object.assign(global, { chrome: chrome, browser: chrome }) +Object.assign(global, { chrome: chrome, browser: chrome }); diff --git a/scripts/build.ts b/scripts/build.ts index 8365cb9a..48f773f9 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,10 +1,10 @@ /* eslint-disable no-console */ // cSpell:ignore metafile,iife,outdir,servedir -import sade from 'sade' -import path from 'node:path' -import fs from 'node:fs' -import esbuild from 'esbuild' +import sade from 'sade'; +import path from 'node:path'; +import fs from 'node:fs'; +import esbuild from 'esbuild'; import { BuildArgs, CHANNELS, @@ -13,10 +13,10 @@ import { options, SERVE_PORTS, Target, - TARGETS -} from '../esbuild/config' -import { getDevOptions } from '../esbuild/dev' -import { getProdOptions } from '../esbuild/prod' + TARGETS, +} from '../esbuild/config'; +import { getDevOptions } from '../esbuild/dev'; +import { getProdOptions } from '../esbuild/prod'; sade('build [target]', true) .option('--channel', `One of: ${CHANNELS.join(', ')}`, 'nightly') @@ -25,69 +25,69 @@ sade('build [target]', true) .example('firefox --channel=stable') .describe(['`target` should be one of ' + TARGETS.join(', ')]) .action(async (target: Target, opts: BuildArgs) => { - const options = { ...opts, target } + const options = { ...opts, target }; if (!options.target && !options.dev) { - console.log(`Building all targets with channel: ${options.channel}`) - return Promise.all(TARGETS.map((t) => build({ ...options, target: t }))) + console.log(`Building all targets with channel: ${options.channel}`); + return Promise.all(TARGETS.map((t) => build({ ...options, target: t }))); } // Default to chrome in dev build if (options.dev) { - options.target ||= 'chrome' + options.target ||= 'chrome'; } if (!TARGETS.includes(options.target)) { - console.warn('Invalid --target. Must be one of ' + TARGETS.join(', ')) - process.exit(1) + console.warn('Invalid --target. Must be one of ' + TARGETS.join(', ')); + process.exit(1); } if (!CHANNELS.includes(options.channel)) { - console.warn('Invalid --channel. Must be one of ' + CHANNELS.join(', ')) - process.exit(1) + console.warn('Invalid --channel. Must be one of ' + CHANNELS.join(', ')); + process.exit(1); } console.log( - `Building target: "${options.target}" with channel: "${options.channel}"` - ) - return options.dev ? buildWatch(options) : build(options) + `Building target: "${options.target}" with channel: "${options.channel}"`, + ); + return options.dev ? buildWatch(options) : build(options); }) - .parse(process.argv) + .parse(process.argv); async function build({ target, channel }: BuildArgs) { - const OUTPUT_DIR = path.join(DIST_DIR, target) + const OUTPUT_DIR = path.join(DIST_DIR, target); const result = await esbuild.build({ ...options, ...getProdOptions({ outDir: OUTPUT_DIR, target, channel }), - outdir: OUTPUT_DIR - }) + outdir: OUTPUT_DIR, + }); if (result.metafile) { fs.writeFileSync( path.join(OUTPUT_DIR, 'meta.json'), - JSON.stringify(result.metafile) - ) + JSON.stringify(result.metafile), + ); } } async function buildWatch({ target, channel }: BuildArgs) { - const OUTPUT_DIR = path.join(DEV_DIR, target) + const OUTPUT_DIR = path.join(DEV_DIR, target); const ctx = await esbuild.context({ ...options, ...getDevOptions({ outDir: OUTPUT_DIR, target, channel }), - outdir: OUTPUT_DIR - }) + outdir: OUTPUT_DIR, + }); try { await ctx.serve({ host: 'localhost', port: SERVE_PORTS[target], - servedir: OUTPUT_DIR - }) + servedir: OUTPUT_DIR, + }); } catch (error) { - console.log(error.message) - console.log('>>> PLEASE TRY SAVING BUILD SCRIPT AGAIN') + console.log(error.message); + console.log('>>> PLEASE TRY SAVING BUILD SCRIPT AGAIN'); } - await ctx.watch() + await ctx.watch(); - process.on('beforeExit', () => ctx.dispose()) + process.on('beforeExit', () => ctx.dispose()); } diff --git a/src/background/config.ts b/src/background/config.ts index cf71db34..b0248b46 100644 --- a/src/background/config.ts +++ b/src/background/config.ts @@ -1,9 +1,9 @@ -export const DEFAULT_SCALE = 2 -export const DEFAULT_INTERVAL_MS = 3_600_000 +export const DEFAULT_SCALE = 2; +export const DEFAULT_INTERVAL_MS = 3_600_000; -export const DEFAULT_RATE_OF_PAY = '60' -export const MIN_RATE_OF_PAY = '1' -export const MAX_RATE_OF_PAY = '100' +export const DEFAULT_RATE_OF_PAY = '60'; +export const MIN_RATE_OF_PAY = '1'; +export const MAX_RATE_OF_PAY = '100'; export const EXCHANGE_RATES_URL = - 'https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json' + 'https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json'; diff --git a/src/background/container.ts b/src/background/container.ts index f88c001e..3440d2f1 100644 --- a/src/background/container.ts +++ b/src/background/container.ts @@ -1,5 +1,5 @@ -import { asClass, asValue, createContainer, InjectionMode } from 'awilix' -import browser, { type Browser } from 'webextension-polyfill' +import { asClass, asValue, createContainer, InjectionMode } from 'awilix'; +import browser, { type Browser } from 'webextension-polyfill'; import { OpenPaymentsService, StorageService, @@ -10,39 +10,39 @@ import { SendToPopup, EventsService, Heartbeat, - Deduplicator -} from './services' -import { createLogger, Logger } from '@/shared/logger' -import { LOG_LEVEL } from '@/shared/defines' -import { tFactory, type Translation } from '@/shared/helpers' + Deduplicator, +} from './services'; +import { createLogger, Logger } from '@/shared/logger'; +import { LOG_LEVEL } from '@/shared/defines'; +import { tFactory, type Translation } from '@/shared/helpers'; import { MessageManager, - type BackgroundToContentMessage -} from '@/shared/messages' + type BackgroundToContentMessage, +} from '@/shared/messages'; export interface Cradle { - logger: Logger - browser: Browser - events: EventsService - deduplicator: Deduplicator - storage: StorageService - openPaymentsService: OpenPaymentsService - monetizationService: MonetizationService - message: MessageManager - sendToPopup: SendToPopup - tabEvents: TabEvents - background: Background - t: Translation - tabState: TabState - heartbeat: Heartbeat + logger: Logger; + browser: Browser; + events: EventsService; + deduplicator: Deduplicator; + storage: StorageService; + openPaymentsService: OpenPaymentsService; + monetizationService: MonetizationService; + message: MessageManager; + sendToPopup: SendToPopup; + tabEvents: TabEvents; + background: Background; + t: Translation; + tabState: TabState; + heartbeat: Heartbeat; } export const configureContainer = () => { const container = createContainer({ - injectionMode: InjectionMode.PROXY - }) + injectionMode: InjectionMode.PROXY, + }); - const logger = createLogger(LOG_LEVEL) + const logger = createLogger(LOG_LEVEL); container.register({ logger: asValue(logger), @@ -52,22 +52,22 @@ export const configureContainer = () => { deduplicator: asClass(Deduplicator) .singleton() .inject(() => ({ - logger: logger.getLogger('deduplicator') + logger: logger.getLogger('deduplicator'), })), storage: asClass(StorageService) .singleton() .inject(() => ({ - logger: logger.getLogger('storage') + logger: logger.getLogger('storage'), })), openPaymentsService: asClass(OpenPaymentsService) .singleton() .inject(() => ({ - logger: logger.getLogger('open-payments') + logger: logger.getLogger('open-payments'), })), monetizationService: asClass(MonetizationService) .singleton() .inject(() => ({ - logger: logger.getLogger('monetization') + logger: logger.getLogger('monetization'), })), message: asClass(MessageManager).singleton(), tabEvents: asClass(TabEvents).singleton(), @@ -75,15 +75,15 @@ export const configureContainer = () => { background: asClass(Background) .singleton() .inject(() => ({ - logger: logger.getLogger('main') + logger: logger.getLogger('main'), })), tabState: asClass(TabState) .singleton() .inject(() => ({ - logger: logger.getLogger('tab-state') + logger: logger.getLogger('tab-state'), })), - heartbeat: asClass(Heartbeat).singleton() - }) + heartbeat: asClass(Heartbeat).singleton(), + }); - return container -} + return container; +}; diff --git a/src/background/globalBuffer.ts b/src/background/globalBuffer.ts index cbd0f311..2c012c98 100644 --- a/src/background/globalBuffer.ts +++ b/src/background/globalBuffer.ts @@ -1,3 +1,3 @@ -import { Buffer } from 'safe-buffer' +import { Buffer } from 'safe-buffer'; // @ts-expect-error we know -globalThis.Buffer = Buffer +globalThis.Buffer = Buffer; diff --git a/src/background/index.ts b/src/background/index.ts index b52014ee..b8230590 100755 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,5 +1,5 @@ -import './globalBuffer' -import { configureContainer } from './container' +import './globalBuffer'; +import { configureContainer } from './container'; -const container = configureContainer() -container.resolve('background').start() +const container = configureContainer(); +container.resolve('background').start(); diff --git a/src/background/services/background.ts b/src/background/services/background.ts index 5919e11c..52eb6b8a 100644 --- a/src/background/services/background.ts +++ b/src/background/services/background.ts @@ -1,29 +1,29 @@ -import type { Browser } from 'webextension-polyfill' -import type { ToBackgroundMessage } from '@/shared/messages' +import type { Browser } from 'webextension-polyfill'; +import type { ToBackgroundMessage } from '@/shared/messages'; import { failure, getNextOccurrence, getWalletInformation, - success -} from '@/shared/helpers' -import { OpenPaymentsClientError } from '@interledger/open-payments/dist/client/error' -import { getCurrentActiveTab, OPEN_PAYMENTS_ERRORS } from '@/background/utils' -import { PERMISSION_HOSTS } from '@/shared/defines' -import type { Cradle } from '@/background/container' + success, +} from '@/shared/helpers'; +import { OpenPaymentsClientError } from '@interledger/open-payments/dist/client/error'; +import { getCurrentActiveTab, OPEN_PAYMENTS_ERRORS } from '@/background/utils'; +import { PERMISSION_HOSTS } from '@/shared/defines'; +import type { Cradle } from '@/background/container'; -type AlarmCallback = Parameters[0] -const ALARM_RESET_OUT_OF_FUNDS = 'reset-out-of-funds' +type AlarmCallback = Parameters[0]; +const ALARM_RESET_OUT_OF_FUNDS = 'reset-out-of-funds'; export class Background { - private browser: Cradle['browser'] - private openPaymentsService: Cradle['openPaymentsService'] - private monetizationService: Cradle['monetizationService'] - private storage: Cradle['storage'] - private logger: Cradle['logger'] - private tabEvents: Cradle['tabEvents'] - private sendToPopup: Cradle['sendToPopup'] - private events: Cradle['events'] - private heartbeat: Cradle['heartbeat'] + private browser: Cradle['browser']; + private openPaymentsService: Cradle['openPaymentsService']; + private monetizationService: Cradle['monetizationService']; + private storage: Cradle['storage']; + private logger: Cradle['logger']; + private tabEvents: Cradle['tabEvents']; + private sendToPopup: Cradle['sendToPopup']; + private events: Cradle['events']; + private heartbeat: Cradle['heartbeat']; constructor({ browser, @@ -34,7 +34,7 @@ export class Background { tabEvents, sendToPopup, events, - heartbeat + heartbeat, }: Cradle) { Object.assign(this, { browser, @@ -45,244 +45,248 @@ export class Background { tabEvents, logger, events, - heartbeat - }) + heartbeat, + }); } async start() { - this.bindOnInstalled() - await this.onStart() - this.heartbeat.start() - this.bindMessageHandler() - this.bindPermissionsHandler() - this.bindEventsHandler() - this.bindTabHandlers() - this.bindWindowHandlers() - this.sendToPopup.start() + this.bindOnInstalled(); + await this.onStart(); + this.heartbeat.start(); + this.bindMessageHandler(); + this.bindPermissionsHandler(); + this.bindEventsHandler(); + this.bindTabHandlers(); + this.bindWindowHandlers(); + this.sendToPopup.start(); } async onStart() { - await this.storage.populate() - await this.checkPermissions() - await this.scheduleResetOutOfFundsState() + await this.storage.populate(); + await this.checkPermissions(); + await this.scheduleResetOutOfFundsState(); } async scheduleResetOutOfFundsState() { // Reset out_of_funds state, we'll detect latest state as we make a payment. - await this.storage.setState({ out_of_funds: false }) + await this.storage.setState({ out_of_funds: false }); - const { recurringGrant } = await this.storage.get(['recurringGrant']) - if (!recurringGrant) return + const { recurringGrant } = await this.storage.get(['recurringGrant']); + if (!recurringGrant) return; - const renewDate = getNextOccurrence(recurringGrant.amount.interval) + const renewDate = getNextOccurrence(recurringGrant.amount.interval); this.browser.alarms.create(ALARM_RESET_OUT_OF_FUNDS, { - when: renewDate.valueOf() - }) + when: renewDate.valueOf(), + }); const resetOutOfFundsState: AlarmCallback = (alarm) => { - if (alarm.name !== ALARM_RESET_OUT_OF_FUNDS) return - this.storage.setState({ out_of_funds: false }) - this.browser.alarms.onAlarm.removeListener(resetOutOfFundsState) - } - this.browser.alarms.onAlarm.addListener(resetOutOfFundsState) + if (alarm.name !== ALARM_RESET_OUT_OF_FUNDS) return; + this.storage.setState({ out_of_funds: false }); + this.browser.alarms.onAlarm.removeListener(resetOutOfFundsState); + }; + this.browser.alarms.onAlarm.addListener(resetOutOfFundsState); } bindWindowHandlers() { this.browser.windows.onFocusChanged.addListener(async () => { const windows = await this.browser.windows.getAll({ - windowTypes: ['normal', 'panel', 'popup'] - }) + windowTypes: ['normal', 'panel', 'popup'], + }); windows.forEach(async (w) => { const activeTab = ( await this.browser.tabs.query({ windowId: w.id, active: true }) - )[0] - if (!activeTab?.id) return + )[0]; + if (!activeTab?.id) return; if (this.sendToPopup.isPopupOpen) { - this.logger.debug('Popup is open, ignoring focus change') - return + this.logger.debug('Popup is open, ignoring focus change'); + return; } if (w.focused) { this.logger.debug( - `Trying to resume monetization for window=${w.id}, activeTab=${activeTab.id} (URL: ${activeTab.url})` - ) + `Trying to resume monetization for window=${w.id}, activeTab=${activeTab.id} (URL: ${activeTab.url})`, + ); void this.monetizationService.resumePaymentSessionsByTabId( - activeTab.id - ) + activeTab.id, + ); } else { this.logger.debug( - `Trying to pause monetization for window=${w.id}, activeTab=${activeTab.id} (URL: ${activeTab.url})` - ) - void this.monetizationService.stopPaymentSessionsByTabId(activeTab.id) + `Trying to pause monetization for window=${w.id}, activeTab=${activeTab.id} (URL: ${activeTab.url})`, + ); + void this.monetizationService.stopPaymentSessionsByTabId( + activeTab.id, + ); } - }) - }) + }); + }); } bindTabHandlers() { - this.browser.tabs.onRemoved.addListener(this.tabEvents.onRemovedTab) - this.browser.tabs.onUpdated.addListener(this.tabEvents.onUpdatedTab) - this.browser.tabs.onCreated.addListener(this.tabEvents.onCreatedTab) - this.browser.tabs.onActivated.addListener(this.tabEvents.onActivatedTab) + this.browser.tabs.onRemoved.addListener(this.tabEvents.onRemovedTab); + this.browser.tabs.onUpdated.addListener(this.tabEvents.onUpdatedTab); + this.browser.tabs.onCreated.addListener(this.tabEvents.onCreatedTab); + this.browser.tabs.onActivated.addListener(this.tabEvents.onActivatedTab); } bindMessageHandler() { this.browser.runtime.onMessage.addListener( async (message: ToBackgroundMessage, sender) => { - this.logger.debug('Received message', message) + this.logger.debug('Received message', message); try { switch (message.action) { // region Popup case 'GET_CONTEXT_DATA': - return success(await this.monetizationService.getPopupData()) + return success(await this.monetizationService.getPopupData()); case 'CONNECT_WALLET': - await this.openPaymentsService.connectWallet(message.payload) + await this.openPaymentsService.connectWallet(message.payload); if (message.payload.recurring) { - this.scheduleResetOutOfFundsState() + this.scheduleResetOutOfFundsState(); } - return + return; case 'RECONNECT_WALLET': { - await this.openPaymentsService.reconnectWallet() - await this.monetizationService.resumePaymentSessionActiveTab() - await this.updateVisualIndicatorsForCurrentTab() - return success(undefined) + await this.openPaymentsService.reconnectWallet(); + await this.monetizationService.resumePaymentSessionActiveTab(); + await this.updateVisualIndicatorsForCurrentTab(); + return success(undefined); } case 'ADD_FUNDS': - await this.openPaymentsService.addFunds(message.payload) - await this.browser.alarms.clear(ALARM_RESET_OUT_OF_FUNDS) + await this.openPaymentsService.addFunds(message.payload); + await this.browser.alarms.clear(ALARM_RESET_OUT_OF_FUNDS); if (message.payload.recurring) { - this.scheduleResetOutOfFundsState() + this.scheduleResetOutOfFundsState(); } - return + return; case 'DISCONNECT_WALLET': - await this.openPaymentsService.disconnectWallet() - await this.browser.alarms.clear(ALARM_RESET_OUT_OF_FUNDS) - await this.updateVisualIndicatorsForCurrentTab() - this.sendToPopup.send('SET_STATE', { state: {}, prevState: {} }) - return + await this.openPaymentsService.disconnectWallet(); + await this.browser.alarms.clear(ALARM_RESET_OUT_OF_FUNDS); + await this.updateVisualIndicatorsForCurrentTab(); + this.sendToPopup.send('SET_STATE', { state: {}, prevState: {} }); + return; case 'TOGGLE_WM': { - await this.monetizationService.toggleWM() - await this.updateVisualIndicatorsForCurrentTab() - return + await this.monetizationService.toggleWM(); + await this.updateVisualIndicatorsForCurrentTab(); + return; } case 'UPDATE_RATE_OF_PAY': return success( - await this.storage.updateRate(message.payload.rateOfPay) - ) + await this.storage.updateRate(message.payload.rateOfPay), + ); case 'PAY_WEBSITE': return success( - await this.monetizationService.pay(message.payload.amount) - ) + await this.monetizationService.pay(message.payload.amount), + ); // endregion // region Content case 'CHECK_WALLET_ADDRESS_URL': return success( - await getWalletInformation(message.payload.walletAddressUrl) - ) + await getWalletInformation(message.payload.walletAddressUrl), + ); case 'START_MONETIZATION': await this.monetizationService.startPaymentSession( message.payload, - sender - ) - return + sender, + ); + return; case 'STOP_MONETIZATION': await this.monetizationService.stopPaymentSession( message.payload, - sender - ) - return + sender, + ); + return; case 'RESUME_MONETIZATION': await this.monetizationService.resumePaymentSession( message.payload, - sender - ) - return + sender, + ); + return; case 'IS_WM_ENABLED': - return success(await this.storage.getWMState()) + return success(await this.storage.getWMState()); // endregion default: - return + return; } } catch (e) { if (e instanceof OpenPaymentsClientError) { - this.logger.error(message.action, e.message, e.description) - return failure(OPEN_PAYMENTS_ERRORS[e.description] ?? e.description) + this.logger.error(message.action, e.message, e.description); + return failure( + OPEN_PAYMENTS_ERRORS[e.description] ?? e.description, + ); } - this.logger.error(message.action, e.message) - return failure(e.message) + this.logger.error(message.action, e.message); + return failure(e.message); } - } - ) + }, + ); } private async updateVisualIndicatorsForCurrentTab() { - const activeTab = await getCurrentActiveTab(this.browser) + const activeTab = await getCurrentActiveTab(this.browser); if (activeTab?.id) { - void this.tabEvents.updateVisualIndicators(activeTab.id) + void this.tabEvents.updateVisualIndicators(activeTab.id); } } bindPermissionsHandler() { - this.browser.permissions.onAdded.addListener(this.checkPermissions) - this.browser.permissions.onRemoved.addListener(this.checkPermissions) + this.browser.permissions.onAdded.addListener(this.checkPermissions); + this.browser.permissions.onRemoved.addListener(this.checkPermissions); } bindEventsHandler() { this.events.on('storage.state_update', async ({ state, prevState }) => { - this.sendToPopup.send('SET_STATE', { state, prevState }) - await this.updateVisualIndicatorsForCurrentTab() - }) + this.sendToPopup.send('SET_STATE', { state, prevState }); + await this.updateVisualIndicatorsForCurrentTab(); + }); this.events.on('monetization.state_update', (tabId) => { - void this.tabEvents.updateVisualIndicators(tabId) - }) + void this.tabEvents.updateVisualIndicators(tabId); + }); this.events.on('storage.balance_update', (balance) => - this.sendToPopup.send('SET_BALANCE', balance) - ) + this.sendToPopup.send('SET_BALANCE', balance), + ); } bindOnInstalled() { this.browser.runtime.onInstalled.addListener(async (details) => { - const data = await this.storage.get() - this.logger.info(data) + const data = await this.storage.get(); + this.logger.info(data); if (details.reason === 'install') { - await this.storage.populate() - await this.openPaymentsService.generateKeys() + await this.storage.populate(); + await this.openPaymentsService.generateKeys(); } else if (details.reason === 'update') { - const migrated = await this.storage.migrate() + const migrated = await this.storage.migrate(); if (migrated) { - const prevVersion = data.version ?? 1 + const prevVersion = data.version ?? 1; this.logger.info( - `Migrated from ${prevVersion} to ${migrated.version}` - ) + `Migrated from ${prevVersion} to ${migrated.version}`, + ); } } - }) + }); } checkPermissions = async () => { try { - this.logger.debug('checking hosts permission') + this.logger.debug('checking hosts permission'); const hasPermissions = - await this.browser.permissions.contains(PERMISSION_HOSTS) - this.storage.setState({ missing_host_permissions: !hasPermissions }) + await this.browser.permissions.contains(PERMISSION_HOSTS); + this.storage.setState({ missing_host_permissions: !hasPermissions }); } catch (error) { - this.logger.error(error) + this.logger.error(error); } - } + }; } diff --git a/src/background/services/deduplicator.ts b/src/background/services/deduplicator.ts index b5856e55..589fc09d 100644 --- a/src/background/services/deduplicator.ts +++ b/src/background/services/deduplicator.ts @@ -1,77 +1,77 @@ -import type { Cradle } from '../container' +import type { Cradle } from '../container'; -type AsyncFn = (...args: any[]) => Promise +type AsyncFn = (...args: any[]) => Promise; interface CacheEntry { - promise: Promise + promise: Promise; } interface DedupeOptions { - cacheFnArgs: boolean - wait: number + cacheFnArgs: boolean; + wait: number; } export class Deduplicator { - private logger: Cradle['logger'] + private logger: Cradle['logger']; - private cache: Map = new Map() + private cache: Map = new Map(); constructor({ logger }: Cradle) { - Object.assign(this, { logger }) + Object.assign(this, { logger }); } dedupe>( fn: T, - { cacheFnArgs = false, wait = 5000 }: Partial = {} + { cacheFnArgs = false, wait = 5000 }: Partial = {}, ): T { return ((...args: Parameters): ReturnType => { - const key = this.generateCacheKey(fn, args, cacheFnArgs) - const entry = this.cache.get(key) + const key = this.generateCacheKey(fn, args, cacheFnArgs); + const entry = this.cache.get(key); if (entry) { this.logger.debug( - `Deduplicating function=${fn.name}, ${cacheFnArgs ? 'args=' + JSON.stringify(args) : 'without args'}` - ) - return entry.promise as ReturnType + `Deduplicating function=${fn.name}, ${cacheFnArgs ? 'args=' + JSON.stringify(args) : 'without args'}`, + ); + return entry.promise as ReturnType; } - const promise = fn(...args) - this.cache.set(key, { promise }) + const promise = fn(...args); + this.cache.set(key, { promise }); promise .then((res) => { - this.cache.set(key, { promise: Promise.resolve(res) }) - return res + this.cache.set(key, { promise: Promise.resolve(res) }); + return res; }) .catch((err) => { - throw err + throw err; }) - .finally(() => this.scheduleCacheClear(key, wait)) + .finally(() => this.scheduleCacheClear(key, wait)); - return promise as ReturnType - }) as unknown as T + return promise as ReturnType; + }) as unknown as T; } private generateCacheKey( fn: AsyncFn, args: any[], - cacheFnArgs: boolean + cacheFnArgs: boolean, ): string { - let key = fn.name + let key = fn.name; if (cacheFnArgs) { - key += `_${JSON.stringify(args)}` + key += `_${JSON.stringify(args)}`; } - return key + return key; } private scheduleCacheClear(key: string, wait: number): void { setTimeout(() => { - this.logger.debug(`Attempting to remove key=${key} from cache.`) - const entry = this.cache.get(key) + this.logger.debug(`Attempting to remove key=${key} from cache.`); + const entry = this.cache.get(key); if (entry) { - this.logger.debug(`Removing key=${key} from cache.`) - this.cache.delete(key) + this.logger.debug(`Removing key=${key} from cache.`); + this.cache.delete(key); } - }, wait) + }, wait); } } diff --git a/src/background/services/events.ts b/src/background/services/events.ts index bd4e12f8..9ce7ea96 100644 --- a/src/background/services/events.ts +++ b/src/background/services/events.ts @@ -1,39 +1,39 @@ -import { EventEmitter } from 'events' -import type { AmountValue, Storage, TabId } from '@/shared/types' +import { EventEmitter } from 'events'; +import type { AmountValue, Storage, TabId } from '@/shared/types'; interface BackgroundEvents { - 'open_payments.key_revoked': void - 'open_payments.out_of_funds': void - 'open_payments.invalid_receiver': { tabId: number } - 'storage.rate_of_pay_update': { rate: string } + 'open_payments.key_revoked': void; + 'open_payments.out_of_funds': void; + 'open_payments.invalid_receiver': { tabId: number }; + 'storage.rate_of_pay_update': { rate: string }; 'storage.state_update': { - state: Storage['state'] - prevState: Storage['state'] - } + state: Storage['state']; + prevState: Storage['state']; + }; 'storage.balance_update': Record< 'recurring' | 'oneTime' | 'total', AmountValue - > - 'monetization.state_update': TabId + >; + 'monetization.state_update': TabId; } export class EventsService extends EventEmitter { constructor() { - super() + super(); } on( eventName: TEvent, - listener: (param: BackgroundEvents[TEvent]) => void + listener: (param: BackgroundEvents[TEvent]) => void, ): this { - return super.on(eventName, listener) + return super.on(eventName, listener); } once( eventName: TEvent, - listener: (param: BackgroundEvents[TEvent]) => void + listener: (param: BackgroundEvents[TEvent]) => void, ): this { - return super.once(eventName, listener) + return super.once(eventName, listener); } emit( @@ -42,7 +42,7 @@ export class EventsService extends EventEmitter { ? [param?: BackgroundEvents[TEvent]] : [param: BackgroundEvents[TEvent]] ): boolean { - return super.emit(eventName, ...rest) + return super.emit(eventName, ...rest); } /** @@ -50,7 +50,7 @@ export class EventsService extends EventEmitter { * @deprecated */ addListener(): this { - throw new Error('Use `on` instead of `addListener`.') + throw new Error('Use `on` instead of `addListener`.'); } /** @@ -59,6 +59,6 @@ export class EventsService extends EventEmitter { */ removeListener(): this { // eslint-disable-next-line prefer-rest-params - return super.removeListener.apply(this, arguments) + return super.removeListener.apply(this, arguments); } } diff --git a/src/background/services/heartbeat.ts b/src/background/services/heartbeat.ts index 1e0d8e5b..80259487 100644 --- a/src/background/services/heartbeat.ts +++ b/src/background/services/heartbeat.ts @@ -1,14 +1,14 @@ -import type { Cradle } from '@/background/container' +import type { Cradle } from '@/background/container'; export class Heartbeat { - private browser: Cradle['browser'] + private browser: Cradle['browser']; constructor({ browser }: Cradle) { - Object.assign(this, { browser }) + Object.assign(this, { browser }); } start() { - const alarms = this.browser.alarms + const alarms = this.browser.alarms; // The minimum supported cross-browser period is 1 minute. So, we create 4 // alarms at a 0,15,30,45 seconds delay. So, we'll get an alarm every 15s - // and that'll help us keep the background script alive. @@ -18,30 +18,30 @@ export class Heartbeat { // first minute that our extension stays alive. setTimeout( () => alarms.create('keep-alive-alarm-0', { periodInMinutes: 1 }), - 0 - ) + 0, + ); setTimeout( () => alarms.create('keep-alive-alarm-1', { periodInMinutes: 1 }), - 15 * 1000 - ) + 15 * 1000, + ); setTimeout( () => alarms.create('keep-alive-alarm-2', { periodInMinutes: 1 }), - 30 * 1000 - ) + 30 * 1000, + ); setTimeout( () => alarms.create('keep-alive-alarm-3', { periodInMinutes: 1 }), - 45 * 1000 - ) + 45 * 1000, + ); alarms.onAlarm.addListener(() => { // doing nothing is enough to keep it alive - }) + }); } stop() { - this.browser.alarms.clear('keep-alive-alarm-0') - this.browser.alarms.clear('keep-alive-alarm-1') - this.browser.alarms.clear('keep-alive-alarm-2') - this.browser.alarms.clear('keep-alive-alarm-3') + this.browser.alarms.clear('keep-alive-alarm-0'); + this.browser.alarms.clear('keep-alive-alarm-1'); + this.browser.alarms.clear('keep-alive-alarm-2'); + this.browser.alarms.clear('keep-alive-alarm-3'); } } diff --git a/src/background/services/index.ts b/src/background/services/index.ts index dffd4092..f8cf399c 100644 --- a/src/background/services/index.ts +++ b/src/background/services/index.ts @@ -1,10 +1,10 @@ -export { OpenPaymentsService } from './openPayments' -export { StorageService } from './storage' -export { MonetizationService } from './monetization' -export { Background } from './background' -export { TabEvents } from './tabEvents' -export { TabState } from './tabState' -export { SendToPopup } from './sendToPopup' -export { EventsService } from './events' -export { Deduplicator } from './deduplicator' -export { Heartbeat } from './heartbeat' +export { OpenPaymentsService } from './openPayments'; +export { StorageService } from './storage'; +export { MonetizationService } from './monetization'; +export { Background } from './background'; +export { TabEvents } from './tabEvents'; +export { TabState } from './tabState'; +export { SendToPopup } from './sendToPopup'; +export { EventsService } from './events'; +export { Deduplicator } from './deduplicator'; +export { Heartbeat } from './heartbeat'; diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index 4b4e6537..2de577cb 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -1,26 +1,31 @@ -import type { Runtime } from 'webextension-polyfill' +import type { Runtime } from 'webextension-polyfill'; import { ResumeMonetizationPayload, StartMonetizationPayload, - StopMonetizationPayload -} from '@/shared/messages' -import { PaymentSession } from './paymentSession' -import { computeRate, getCurrentActiveTab, getSender, getTabId } from '../utils' -import { isOutOfBalanceError } from './openPayments' -import { isOkState, removeQueryParams } from '@/shared/helpers' -import { ALLOWED_PROTOCOLS } from '@/shared/defines' -import type { AmountValue, PopupStore, Storage } from '@/shared/types' -import type { Cradle } from '../container' + StopMonetizationPayload, +} from '@/shared/messages'; +import { PaymentSession } from './paymentSession'; +import { + computeRate, + getCurrentActiveTab, + getSender, + getTabId, +} from '../utils'; +import { isOutOfBalanceError } from './openPayments'; +import { isOkState, removeQueryParams } from '@/shared/helpers'; +import { ALLOWED_PROTOCOLS } from '@/shared/defines'; +import type { AmountValue, PopupStore, Storage } from '@/shared/types'; +import type { Cradle } from '../container'; export class MonetizationService { - private logger: Cradle['logger'] - private t: Cradle['t'] - private openPaymentsService: Cradle['openPaymentsService'] - private storage: Cradle['storage'] - private browser: Cradle['browser'] - private events: Cradle['events'] - private tabState: Cradle['tabState'] - private message: Cradle['message'] + private logger: Cradle['logger']; + private t: Cradle['t']; + private openPaymentsService: Cradle['openPaymentsService']; + private storage: Cradle['storage']; + private browser: Cradle['browser']; + private events: Cradle['events']; + private tabState: Cradle['tabState']; + private message: Cradle['message']; constructor({ logger, @@ -30,7 +35,7 @@ export class MonetizationService { events, openPaymentsService, tabState, - message + message, }: Cradle) { Object.assign(this, { logger, @@ -40,54 +45,54 @@ export class MonetizationService { browser, events, tabState, - message - }) + message, + }); - this.registerEventListeners() + this.registerEventListeners(); } async startPaymentSession( payload: StartMonetizationPayload[], - sender: Runtime.MessageSender + sender: Runtime.MessageSender, ) { if (!payload.length) { - throw new Error('Unexpected: payload is empty') + throw new Error('Unexpected: payload is empty'); } const { state, enabled, rateOfPay, connected, - walletAddress: connectedWallet + walletAddress: connectedWallet, } = await this.storage.get([ 'state', 'enabled', 'connected', 'rateOfPay', - 'walletAddress' - ]) + 'walletAddress', + ]); if (!rateOfPay || !connectedWallet) { this.logger.error( - `Did not find rate of pay or connect wallet information. Received rate=${rateOfPay}, wallet=${connectedWallet}. Payment session will not be initialized.` - ) - return + `Did not find rate of pay or connect wallet information. Received rate=${rateOfPay}, wallet=${connectedWallet}. Payment session will not be initialized.`, + ); + return; } - const { tabId, frameId, url } = getSender(sender) - const sessions = this.tabState.getSessions(tabId) + const { tabId, frameId, url } = getSender(sender); + const sessions = this.tabState.getSessions(tabId); - const replacedSessions = new Set() + const replacedSessions = new Set(); // Initialize new sessions payload.forEach((p) => { - const { requestId, walletAddress: receiver } = p + const { requestId, walletAddress: receiver } = p; // Q: How does this impact client side apps/routing? - const existingSession = sessions.get(requestId) + const existingSession = sessions.get(requestId); if (existingSession) { - existingSession.stop() - sessions.delete(requestId) - replacedSessions.add(requestId) + existingSession.stop(); + sessions.delete(requestId); + replacedSessions.add(requestId); } const session = new PaymentSession( @@ -101,264 +106,264 @@ export class MonetizationService { this.tabState, removeQueryParams(url!), this.logger, - this.message - ) + this.message, + ); - sessions.set(requestId, session) - }) + sessions.set(requestId, session); + }); - this.events.emit('monetization.state_update', tabId) + this.events.emit('monetization.state_update', tabId); - const sessionsArr = this.tabState.getPayableSessions(tabId) - if (!sessionsArr.length) return - const rate = computeRate(rateOfPay, sessionsArr.length) + const sessionsArr = this.tabState.getPayableSessions(tabId); + if (!sessionsArr.length) return; + const rate = computeRate(rateOfPay, sessionsArr.length); // Since we probe (through quoting) the debitAmount we have to await this call. - const isAdjusted = await this.adjustSessionsAmount(sessionsArr, rate) - if (!isAdjusted) return + const isAdjusted = await this.adjustSessionsAmount(sessionsArr, rate); + if (!isAdjusted) return; if (enabled && this.canTryPayment(connected, state)) { sessionsArr.forEach((session) => { - if (!sessions.get(session.id)) return + if (!sessions.get(session.id)) return; const source = replacedSessions.has(session.id) ? 'request-id-reused' - : 'new-link' - void session.start(source) - }) + : 'new-link'; + void session.start(source); + }); } } async stopPaymentSessionsByTabId(tabId: number) { - const sessions = this.tabState.getSessions(tabId) + const sessions = this.tabState.getSessions(tabId); if (!sessions.size) { - this.logger.debug(`No active sessions found for tab ${tabId}.`) - return + this.logger.debug(`No active sessions found for tab ${tabId}.`); + return; } for (const session of sessions.values()) { - session.stop() + session.stop(); } } async stopPaymentSession( payload: StopMonetizationPayload[], - sender: Runtime.MessageSender + sender: Runtime.MessageSender, ) { - let needsAdjustAmount = false - const tabId = getTabId(sender) - const sessions = this.tabState.getSessions(tabId) + let needsAdjustAmount = false; + const tabId = getTabId(sender); + const sessions = this.tabState.getSessions(tabId); if (!sessions.size) { - this.logger.debug(`No active sessions found for tab ${tabId}.`) - return + this.logger.debug(`No active sessions found for tab ${tabId}.`); + return; } payload.forEach((p) => { - const { requestId } = p + const { requestId } = p; - const session = sessions.get(requestId) - if (!session) return + const session = sessions.get(requestId); + if (!session) return; if (p.intent === 'remove') { - needsAdjustAmount = true - session.stop() - sessions.delete(requestId) + needsAdjustAmount = true; + session.stop(); + sessions.delete(requestId); } else if (p.intent === 'disable') { - needsAdjustAmount = true - session.disable() + needsAdjustAmount = true; + session.disable(); } else { - session.stop() + session.stop(); } - }) + }); - const { rateOfPay } = await this.storage.get(['rateOfPay']) - if (!rateOfPay) return + const { rateOfPay } = await this.storage.get(['rateOfPay']); + if (!rateOfPay) return; if (needsAdjustAmount) { - const sessionsArr = this.tabState.getPayableSessions(tabId) - this.events.emit('monetization.state_update', tabId) - if (!sessionsArr.length) return - const rate = computeRate(rateOfPay, sessionsArr.length) + const sessionsArr = this.tabState.getPayableSessions(tabId); + this.events.emit('monetization.state_update', tabId); + if (!sessionsArr.length) return; + const rate = computeRate(rateOfPay, sessionsArr.length); await this.adjustSessionsAmount(sessionsArr, rate).catch((e) => { - this.logger.error(e) - }) + this.logger.error(e); + }); } } async resumePaymentSession( payload: ResumeMonetizationPayload[], - sender: Runtime.MessageSender + sender: Runtime.MessageSender, ) { - const tabId = getTabId(sender) - const sessions = this.tabState.getSessions(tabId) + const tabId = getTabId(sender); + const sessions = this.tabState.getSessions(tabId); if (!sessions.size) { - this.logger.debug(`No active sessions found for tab ${tabId}.`) - return + this.logger.debug(`No active sessions found for tab ${tabId}.`); + return; } const { state, connected, enabled } = await this.storage.get([ 'state', 'connected', - 'enabled' - ]) - if (!enabled || !this.canTryPayment(connected, state)) return + 'enabled', + ]); + if (!enabled || !this.canTryPayment(connected, state)) return; payload.forEach((p) => { - const { requestId } = p + const { requestId } = p; - sessions.get(requestId)?.resume() - }) + sessions.get(requestId)?.resume(); + }); } async resumePaymentSessionsByTabId(tabId: number) { - const sessions = this.tabState.getSessions(tabId) + const sessions = this.tabState.getSessions(tabId); if (!sessions.size) { - this.logger.debug(`No active sessions found for tab ${tabId}.`) - return + this.logger.debug(`No active sessions found for tab ${tabId}.`); + return; } const { state, connected, enabled } = await this.storage.get([ 'state', 'connected', - 'enabled' - ]) - if (!enabled || !this.canTryPayment(connected, state)) return + 'enabled', + ]); + if (!enabled || !this.canTryPayment(connected, state)) return; for (const session of sessions.values()) { - session.resume() + session.resume(); } } async resumePaymentSessionActiveTab() { - const currentTab = await getCurrentActiveTab(this.browser) - if (!currentTab?.id) return - await this.resumePaymentSessionsByTabId(currentTab.id) + const currentTab = await getCurrentActiveTab(this.browser); + if (!currentTab?.id) return; + await this.resumePaymentSessionsByTabId(currentTab.id); } async toggleWM() { - const { enabled } = await this.storage.get(['enabled']) - await this.storage.set({ enabled: !enabled }) - await this.message.sendToActiveTab('EMIT_TOGGLE_WM', { enabled: !enabled }) + const { enabled } = await this.storage.get(['enabled']); + await this.storage.set({ enabled: !enabled }); + await this.message.sendToActiveTab('EMIT_TOGGLE_WM', { enabled: !enabled }); } async pay(amount: string) { - const tab = await getCurrentActiveTab(this.browser) + const tab = await getCurrentActiveTab(this.browser); if (!tab || !tab.id) { - throw new Error('Unexpected error: could not find active tab.') + throw new Error('Unexpected error: could not find active tab.'); } - const payableSessions = this.tabState.getPayableSessions(tab.id) + const payableSessions = this.tabState.getPayableSessions(tab.id); if (!payableSessions.length) { if (this.tabState.getEnabledSessions(tab.id).length) { - throw new Error(this.t('pay_error_invalidReceivers')) + throw new Error(this.t('pay_error_invalidReceivers')); } - throw new Error(this.t('pay_error_notMonetized')) + throw new Error(this.t('pay_error_notMonetized')); } - const splitAmount = Number(amount) / payableSessions.length + const splitAmount = Number(amount) / payableSessions.length; // TODO: handle paying across two grants (when one grant doesn't have enough funds) const results = await Promise.allSettled( - payableSessions.map((session) => session.pay(splitAmount)) - ) + payableSessions.map((session) => session.pay(splitAmount)), + ); const totalSentAmount = results .filter((e) => e.status === 'fulfilled') - .reduce((acc, curr) => acc + BigInt(curr.value?.value ?? 0), 0n) + .reduce((acc, curr) => acc + BigInt(curr.value?.value ?? 0), 0n); if (totalSentAmount === 0n) { const isNotEnoughFunds = results .filter((e) => e.status === 'rejected') - .some((e) => isOutOfBalanceError(e.reason)) + .some((e) => isOutOfBalanceError(e.reason)); if (isNotEnoughFunds) { - throw new Error(this.t('pay_error_notEnoughFunds')) + throw new Error(this.t('pay_error_notEnoughFunds')); } - throw new Error('Could not facilitate payment for current website.') + throw new Error('Could not facilitate payment for current website.'); } } private canTryPayment( connected: Storage['connected'], - state: Storage['state'] + state: Storage['state'], ): boolean { - if (!connected) return false - if (isOkState(state)) return true + if (!connected) return false; + if (isOkState(state)) return true; if (state.out_of_funds && this.openPaymentsService.isAnyGrantUsable()) { // if we're in out_of_funds state, we still try to make payments hoping we // have funds available now. If a payment succeeds, we move out from // of_out_funds state. - return true + return true; } - return false + return false; } private registerEventListeners() { - this.onRateOfPayUpdate() - this.onKeyRevoked() - this.onOutOfFunds() - this.onInvalidReceiver() + this.onRateOfPayUpdate(); + this.onKeyRevoked(); + this.onOutOfFunds(); + this.onInvalidReceiver(); } private onRateOfPayUpdate() { this.events.on('storage.rate_of_pay_update', async ({ rate }) => { - this.logger.debug("Received event='storage.rate_of_pay_update'") - const tabIds = this.tabState.getAllTabs() + this.logger.debug("Received event='storage.rate_of_pay_update'"); + const tabIds = this.tabState.getAllTabs(); // Move the current active tab to the front of the array - const currentTab = await getCurrentActiveTab(this.browser) + const currentTab = await getCurrentActiveTab(this.browser); if (currentTab?.id) { - const idx = tabIds.indexOf(currentTab.id) + const idx = tabIds.indexOf(currentTab.id); if (idx !== -1) { - const tmp = tabIds[0] - tabIds[0] = currentTab.id - tabIds[idx] = tmp + const tmp = tabIds[0]; + tabIds[0] = currentTab.id; + tabIds[idx] = tmp; } } for (const tabId of tabIds) { - const sessions = this.tabState.getPayableSessions(tabId) - if (!sessions.length) continue - const computedRate = computeRate(rate, sessions.length) + const sessions = this.tabState.getPayableSessions(tabId); + if (!sessions.length) continue; + const computedRate = computeRate(rate, sessions.length); await this.adjustSessionsAmount(sessions, computedRate).catch((e) => { - this.logger.error(e) - }) + this.logger.error(e); + }); } - }) + }); } private onKeyRevoked() { this.events.once('open_payments.key_revoked', async () => { - this.logger.warn(`Key revoked. Stopping all payment sessions.`) - this.stopAllSessions() - await this.storage.setState({ key_revoked: true }) - this.onKeyRevoked() // setup listener again once all is done - }) + this.logger.warn(`Key revoked. Stopping all payment sessions.`); + this.stopAllSessions(); + await this.storage.setState({ key_revoked: true }); + this.onKeyRevoked(); // setup listener again once all is done + }); } private onOutOfFunds() { this.events.once('open_payments.out_of_funds', async () => { - this.logger.warn(`Out of funds. Stopping all payment sessions.`) - this.stopAllSessions() - await this.storage.setState({ out_of_funds: true }) - this.onOutOfFunds() // setup listener again once all is done - }) + this.logger.warn(`Out of funds. Stopping all payment sessions.`); + this.stopAllSessions(); + await this.storage.setState({ out_of_funds: true }); + this.onOutOfFunds(); // setup listener again once all is done + }); } private onInvalidReceiver() { this.events.on('open_payments.invalid_receiver', async ({ tabId }) => { if (this.tabState.tabHasAllSessionsInvalid(tabId)) { - this.logger.debug(`Tab ${tabId} has all sessions invalid`) - this.events.emit('monetization.state_update', tabId) + this.logger.debug(`Tab ${tabId} has all sessions invalid`); + this.events.emit('monetization.state_update', tabId); } - }) + }); } private stopAllSessions() { for (const session of this.tabState.getAllSessions()) { - session.stop() + session.stop(); } - this.logger.debug(`All payment sessions stopped.`) + this.logger.debug(`All payment sessions stopped.`); } async getPopupData(): Promise { @@ -372,29 +377,29 @@ export class MonetizationService { 'walletAddress', 'oneTimeGrant', 'recurringGrant', - 'publicKey' - ]) - const balance = await this.storage.getBalance() - const tab = await getCurrentActiveTab(this.browser) + 'publicKey', + ]); + const balance = await this.storage.getBalance(); + const tab = await getCurrentActiveTab(this.browser); - const { oneTimeGrant, recurringGrant, ...dataFromStorage } = storedData + const { oneTimeGrant, recurringGrant, ...dataFromStorage } = storedData; - let url + let url; if (tab && tab.url) { try { - const tabUrl = new URL(tab.url) + const tabUrl = new URL(tab.url); if (ALLOWED_PROTOCOLS.includes(tabUrl.protocol)) { // Do not include search params - url = `${tabUrl.origin}${tabUrl.pathname}` + url = `${tabUrl.origin}${tabUrl.pathname}`; } } catch { // noop } } - const isSiteMonetized = this.tabState.isTabMonetized(tab.id!) + const isSiteMonetized = this.tabState.isTabMonetized(tab.id!); const hasAllSessionsInvalid = this.tabState.tabHasAllSessionsInvalid( - tab.id! - ) + tab.id!, + ); return { ...dataFromStorage, @@ -402,26 +407,26 @@ export class MonetizationService { url, grants: { oneTime: oneTimeGrant?.amount, - recurring: recurringGrant?.amount + recurring: recurringGrant?.amount, }, isSiteMonetized, - hasAllSessionsInvalid - } + hasAllSessionsInvalid, + }; } private async adjustSessionsAmount( sessions: PaymentSession[], - rate: AmountValue + rate: AmountValue, ): Promise { try { - await Promise.all(sessions.map((session) => session.adjustAmount(rate))) - return true + await Promise.all(sessions.map((session) => session.adjustAmount(rate))); + return true; } catch (err) { if (err.name === 'AbortError') { - this.logger.debug('adjustAmount aborted due to new call') - return false + this.logger.debug('adjustAmount aborted due to new call'); + return false; } else { - throw err + throw err; } } } diff --git a/src/background/services/openPayments.ts b/src/background/services/openPayments.ts index ba362455..9fc24ea8 100644 --- a/src/background/services/openPayments.ts +++ b/src/background/services/openPayments.ts @@ -3,145 +3,145 @@ import type { AccessToken, AmountValue, GrantDetails, - WalletAmount -} from 'shared/types' + WalletAmount, +} from 'shared/types'; import { type AuthenticatedClient, createAuthenticatedClient, - OpenPaymentsClientError -} from '@interledger/open-payments/dist/client' + OpenPaymentsClientError, +} from '@interledger/open-payments/dist/client'; import { isFinalizedGrant, isPendingGrant, type IncomingPayment, type OutgoingPaymentWithSpentAmounts as OutgoingPayment, - type WalletAddress -} from '@interledger/open-payments/dist/types' -import * as ed from '@noble/ed25519' -import { type Request } from 'http-message-signatures' -import { signMessage } from 'http-message-signatures/lib/httpbis' -import { createContentDigestHeader } from 'httpbis-digest-headers' -import type { Tabs } from 'webextension-polyfill' -import { getExchangeRates, getRateOfPay, toAmount } from '../utils' -import { exportJWK, generateEd25519KeyPair } from '@/shared/crypto' -import { bytesToHex } from '@noble/hashes/utils' -import { getWalletInformation } from '@/shared/helpers' -import { AddFundsPayload, ConnectWalletPayload } from '@/shared/messages' + type WalletAddress, +} from '@interledger/open-payments/dist/types'; +import * as ed from '@noble/ed25519'; +import { type Request } from 'http-message-signatures'; +import { signMessage } from 'http-message-signatures/lib/httpbis'; +import { createContentDigestHeader } from 'httpbis-digest-headers'; +import type { Tabs } from 'webextension-polyfill'; +import { getExchangeRates, getRateOfPay, toAmount } from '../utils'; +import { exportJWK, generateEd25519KeyPair } from '@/shared/crypto'; +import { bytesToHex } from '@noble/hashes/utils'; +import { getWalletInformation } from '@/shared/helpers'; +import { AddFundsPayload, ConnectWalletPayload } from '@/shared/messages'; import { DEFAULT_RATE_OF_PAY, MAX_RATE_OF_PAY, - MIN_RATE_OF_PAY -} from '../config' -import { OPEN_PAYMENTS_REDIRECT_URL } from '@/shared/defines' -import type { Cradle } from '../container' + MIN_RATE_OF_PAY, +} from '../config'; +import { OPEN_PAYMENTS_REDIRECT_URL } from '@/shared/defines'; +import type { Cradle } from '../container'; interface KeyInformation { - privateKey: string - keyId: string + privateKey: string; + keyId: string; } interface InteractionParams { - interactRef: string - hash: string - tabId: NonNullable + interactRef: string; + hash: string; + tabId: NonNullable; } export interface SignatureHeaders { - Signature: string - 'Signature-Input': string + Signature: string; + 'Signature-Input': string; } interface ContentHeaders { - 'Content-Digest': string - 'Content-Length': string - 'Content-Type': string + 'Content-Digest': string; + 'Content-Length': string; + 'Content-Type': string; } -type Headers = SignatureHeaders & Partial +type Headers = SignatureHeaders & Partial; interface RequestLike extends Request { - body?: string + body?: string; } interface SignOptions { - request: RequestLike - privateKey: Uint8Array - keyId: string + request: RequestLike; + privateKey: Uint8Array; + keyId: string; } interface VerifyInteractionHashParams { - clientNonce: string - interactRef: string - interactNonce: string - hash: string - authServer: string + clientNonce: string; + interactRef: string; + interactNonce: string; + hash: string; + authServer: string; } interface CreateOutgoingPaymentGrantParams { - clientNonce: string - walletAddress: WalletAddress - amount: WalletAmount + clientNonce: string; + walletAddress: WalletAddress; + amount: WalletAmount; } interface CreateOutgoingPaymentParams { - walletAddress: WalletAddress - incomingPaymentId: IncomingPayment['id'] - amount: string + walletAddress: WalletAddress; + incomingPaymentId: IncomingPayment['id']; + amount: string; } -type TabUpdateCallback = Parameters[0] +type TabUpdateCallback = Parameters[0]; const enum ErrorCode { CONTINUATION_FAILED = 'continuation_failed', - HASH_FAILED = 'hash_failed' + HASH_FAILED = 'hash_failed', } const enum GrantResult { SUCCESS = 'grant_success', - ERROR = 'grant_error' + ERROR = 'grant_error', } const enum InteractionIntent { CONNECT = 'connect', - FUNDS = 'funds' + FUNDS = 'funds', } export class OpenPaymentsService { - private browser: Cradle['browser'] - private storage: Cradle['storage'] - private deduplicator: Cradle['deduplicator'] - private logger: Cradle['logger'] - private t: Cradle['t'] + private browser: Cradle['browser']; + private storage: Cradle['storage']; + private deduplicator: Cradle['deduplicator']; + private logger: Cradle['logger']; + private t: Cradle['t']; - client?: AuthenticatedClient + client?: AuthenticatedClient; - public switchGrant: OpenPaymentsService['_switchGrant'] + public switchGrant: OpenPaymentsService['_switchGrant']; - private token: AccessToken - private grantDetails: GrantDetails | null + private token: AccessToken; + private grantDetails: GrantDetails | null; /** Whether a grant has enough balance to make payments */ - private isGrantUsable = { recurring: false, oneTime: false } + private isGrantUsable = { recurring: false, oneTime: false }; constructor({ browser, storage, deduplicator, logger, t }: Cradle) { - Object.assign(this, { browser, storage, deduplicator, logger, t }) + Object.assign(this, { browser, storage, deduplicator, logger, t }); - void this.initialize() - this.switchGrant = this.deduplicator.dedupe(this._switchGrant.bind(this)) + void this.initialize(); + this.switchGrant = this.deduplicator.dedupe(this._switchGrant.bind(this)); } public isAnyGrantUsable() { - return this.isGrantUsable.recurring || this.isGrantUsable.oneTime + return this.isGrantUsable.recurring || this.isGrantUsable.oneTime; } private get grant() { - return this.grantDetails + return this.grantDetails; } private set grant(grantDetails) { - this.logger.debug(`🤝🏻 Using grant: ${grantDetails?.type || null}`) - this.grantDetails = grantDetails + this.logger.debug(`🤝🏻 Using grant: ${grantDetails?.type || null}`); + this.grantDetails = grantDetails; this.token = grantDetails ? grantDetails.accessToken - : { value: '', manageUrl: '' } + : { value: '', manageUrl: '' }; } private async initialize() { @@ -150,43 +150,43 @@ export class OpenPaymentsService { 'connected', 'walletAddress', 'oneTimeGrant', - 'recurringGrant' - ]) + 'recurringGrant', + ]); - this.isGrantUsable.recurring = !!recurringGrant - this.isGrantUsable.oneTime = !!oneTimeGrant + this.isGrantUsable.recurring = !!recurringGrant; + this.isGrantUsable.oneTime = !!oneTimeGrant; if ( connected === true && walletAddress && (recurringGrant || oneTimeGrant) ) { - this.grant = recurringGrant || oneTimeGrant! // prefer recurring - await this.initClient(walletAddress.id) + this.grant = recurringGrant || oneTimeGrant!; // prefer recurring + await this.initClient(walletAddress.id); } } private async getPrivateKeyInformation(): Promise { - const data = await this.browser.storage.local.get(['privateKey', 'keyId']) + const data = await this.browser.storage.local.get(['privateKey', 'keyId']); if (data.privateKey && data.keyId) { - return data as unknown as KeyInformation + return data as unknown as KeyInformation; } throw new Error( - 'Could not create OpenPayments client. Missing `privateKey` and `keyId`.' - ) + 'Could not create OpenPayments client. Missing `privateKey` and `keyId`.', + ); } private createContentHeaders(body: string): ContentHeaders { return { 'Content-Digest': createContentDigestHeader( JSON.stringify(JSON.parse(body)), - ['sha-512'] + ['sha-512'], ), 'Content-Length': new TextEncoder().encode(body).length.toString(), - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }; } private createSigner(key: Uint8Array, keyId: string) { @@ -194,70 +194,70 @@ export class OpenPaymentsService { id: keyId, alg: 'ed25519', async sign(data: Uint8Array) { - return Buffer.from(await ed.signAsync(data, key.slice(16))) - } - } + return Buffer.from(await ed.signAsync(data, key.slice(16))); + }, + }; } private async createSignatureHeaders({ request, privateKey, - keyId + keyId, }: SignOptions): Promise { - const components = ['@method', '@target-uri'] + const components = ['@method', '@target-uri']; if (request.headers['Authorization'] || request.headers['authorization']) { - components.push('authorization') + components.push('authorization'); } if (request.body) { - components.push('content-digest', 'content-length', 'content-type') + components.push('content-digest', 'content-length', 'content-type'); } - const signingKey = this.createSigner(privateKey, keyId) + const signingKey = this.createSigner(privateKey, keyId); const { headers } = await signMessage( { name: 'sig1', params: ['keyid', 'created'], fields: components, - key: signingKey + key: signingKey, }, { url: request.url, method: request.method, - headers: request.headers - } - ) + headers: request.headers, + }, + ); return { Signature: headers['Signature'] as string, - 'Signature-Input': headers['Signature-Input'] as string - } + 'Signature-Input': headers['Signature-Input'] as string, + }; } private async createHeaders({ request, privateKey, - keyId + keyId, }: SignOptions): Promise { if (request.body) { - const contentHeaders = this.createContentHeaders(request.body) - request.headers = { ...request.headers, ...contentHeaders } + const contentHeaders = this.createContentHeaders(request.body); + request.headers = { ...request.headers, ...contentHeaders }; } const signatureHeaders = await this.createSignatureHeaders({ request, privateKey, - keyId - }) + keyId, + }); return { ...request.headers, - ...signatureHeaders - } + ...signatureHeaders, + }; } async initClient(walletAddressUrl: string) { - const { privateKey, keyId } = await this.getPrivateKeyInformation() + const { privateKey, keyId } = await this.getPrivateKeyInformation(); this.client = await createAuthenticatedClient({ validateResponses: false, @@ -265,187 +265,191 @@ export class OpenPaymentsService { walletAddressUrl, authenticatedRequestInterceptor: async (request) => { if (!request.method || !request.url) { - throw new Error('Cannot intercept request: url or method missing') + throw new Error('Cannot intercept request: url or method missing'); } - const initialRequest = request.clone() + const initialRequest = request.clone(); const headers = await this.createHeaders({ request: { method: request.method, url: request.url, headers: JSON.parse( - JSON.stringify(Object.fromEntries(request.headers)) + JSON.stringify(Object.fromEntries(request.headers)), ), body: request.body ? JSON.stringify(await request.json()) - : undefined + : undefined, }, privateKey: ed.etc.hexToBytes(privateKey), - keyId - }) + keyId, + }); if (request.body) { initialRequest.headers.set( 'Content-Type', - headers['Content-Type'] as string - ) + headers['Content-Type'] as string, + ); initialRequest.headers.set( 'Content-Digest', - headers['Content-Digest'] as string - ) + headers['Content-Digest'] as string, + ); } - initialRequest.headers.set('Signature', headers['Signature']) + initialRequest.headers.set('Signature', headers['Signature']); initialRequest.headers.set( 'Signature-Input', - headers['Signature-Input'] - ) + headers['Signature-Input'], + ); - return initialRequest - } - }) + return initialRequest; + }, + }); } async connectWallet({ walletAddressUrl, amount, - recurring + recurring, }: ConnectWalletPayload) { - const walletAddress = await getWalletInformation(walletAddressUrl) - const exchangeRates = await getExchangeRates() + const walletAddress = await getWalletInformation(walletAddressUrl); + const exchangeRates = await getExchangeRates(); - let rateOfPay = DEFAULT_RATE_OF_PAY - let minRateOfPay = MIN_RATE_OF_PAY - let maxRateOfPay = MAX_RATE_OF_PAY + let rateOfPay = DEFAULT_RATE_OF_PAY; + let minRateOfPay = MIN_RATE_OF_PAY; + let maxRateOfPay = MAX_RATE_OF_PAY; if (!exchangeRates.rates[walletAddress.assetCode]) { - throw new Error(`Exchange rate for ${walletAddress.assetCode} not found.`) + throw new Error( + `Exchange rate for ${walletAddress.assetCode} not found.`, + ); } - const exchangeRate = exchangeRates.rates[walletAddress.assetCode] + const exchangeRate = exchangeRates.rates[walletAddress.assetCode]; rateOfPay = getRateOfPay({ rate: DEFAULT_RATE_OF_PAY, exchangeRate, - assetScale: walletAddress.assetScale - }) + assetScale: walletAddress.assetScale, + }); minRateOfPay = getRateOfPay({ rate: MIN_RATE_OF_PAY, exchangeRate, - assetScale: walletAddress.assetScale - }) + assetScale: walletAddress.assetScale, + }); maxRateOfPay = getRateOfPay({ rate: MAX_RATE_OF_PAY, exchangeRate, - assetScale: walletAddress.assetScale - }) + assetScale: walletAddress.assetScale, + }); - await this.initClient(walletAddress.id) + await this.initClient(walletAddress.id); await this.completeGrant( amount, walletAddress, recurring, - InteractionIntent.CONNECT - ) + InteractionIntent.CONNECT, + ); await this.storage.set({ walletAddress, rateOfPay, minRateOfPay, maxRateOfPay, - connected: true - }) + connected: true, + }); } async addFunds({ amount, recurring }: AddFundsPayload) { const { walletAddress, ...grants } = await this.storage.get([ 'walletAddress', 'oneTimeGrant', - 'recurringGrant' - ]) + 'recurringGrant', + ]); await this.completeGrant( amount, walletAddress!, recurring, - InteractionIntent.FUNDS - ) + InteractionIntent.FUNDS, + ); // cancel existing grants of same type, if any if (grants.oneTimeGrant && !recurring) { - await this.cancelGrant(grants.oneTimeGrant.continue) + await this.cancelGrant(grants.oneTimeGrant.continue); } else if (grants.recurringGrant && recurring) { - await this.cancelGrant(grants.recurringGrant.continue) + await this.cancelGrant(grants.recurringGrant.continue); } - await this.storage.setState({ out_of_funds: false }) + await this.storage.setState({ out_of_funds: false }); } private async completeGrant( amount: string, walletAddress: WalletAddress, recurring: boolean, - intent: InteractionIntent + intent: InteractionIntent, ): Promise { const transformedAmount = toAmount({ value: amount, recurring, - assetScale: walletAddress.assetScale - }) + assetScale: walletAddress.assetScale, + }); - const clientNonce = crypto.randomUUID() + const clientNonce = crypto.randomUUID(); const grant = await this.createOutgoingPaymentGrant({ clientNonce, walletAddress, - amount: transformedAmount + amount: transformedAmount, }).catch((err) => { if (isInvalidClientError(err)) { - const msg = this.t('connectWallet_error_invalidClient') - throw new Error(msg, { cause: err }) + const msg = this.t('connectWallet_error_invalidClient'); + throw new Error(msg, { cause: err }); } - throw err - }) + throw err; + }); const { interactRef, hash, tabId } = await this.getInteractionInfo( - grant.interact.redirect - ) + grant.interact.redirect, + ); await this.verifyInteractionHash({ clientNonce, interactNonce: grant.interact.finish, interactRef, hash, - authServer: walletAddress.authServer + authServer: walletAddress.authServer, }).catch(async (e) => { await this.redirectToWelcomeScreen( tabId, GrantResult.ERROR, intent, - ErrorCode.HASH_FAILED - ) - throw e - }) + ErrorCode.HASH_FAILED, + ); + throw e; + }); const continuation = await this.client!.grant.continue( { url: grant.continue.uri, - accessToken: grant.continue.access_token.value + accessToken: grant.continue.access_token.value, }, { - interact_ref: interactRef - } + interact_ref: interactRef, + }, ).catch(async (e) => { await this.redirectToWelcomeScreen( tabId, GrantResult.ERROR, intent, - ErrorCode.CONTINUATION_FAILED - ) - throw e - }) + ErrorCode.CONTINUATION_FAILED, + ); + throw e; + }); if (!isFinalizedGrant(continuation)) { - throw new Error('Expected finalized grant. Received non-finalized grant.') + throw new Error( + 'Expected finalized grant. Received non-finalized grant.', + ); } const grantDetails: GrantDetails = { @@ -453,63 +457,63 @@ export class OpenPaymentsService { amount: transformedAmount as Required, accessToken: { value: continuation.access_token.value, - manageUrl: continuation.access_token.manage + manageUrl: continuation.access_token.manage, }, continue: { accessToken: continuation.continue.access_token.value, - url: continuation.continue.uri - } - } + url: continuation.continue.uri, + }, + }; if (grantDetails.type === 'recurring') { await this.storage.set({ recurringGrant: grantDetails, - recurringGrantSpentAmount: '0' - }) - this.isGrantUsable.recurring = true + recurringGrantSpentAmount: '0', + }); + this.isGrantUsable.recurring = true; } else { await this.storage.set({ oneTimeGrant: grantDetails, - oneTimeGrantSpentAmount: '0' - }) - this.isGrantUsable.oneTime = true + oneTimeGrantSpentAmount: '0', + }); + this.isGrantUsable.oneTime = true; } - this.grant = grantDetails - await this.redirectToWelcomeScreen(tabId, GrantResult.SUCCESS, intent) - return grantDetails + this.grant = grantDetails; + await this.redirectToWelcomeScreen(tabId, GrantResult.SUCCESS, intent); + return grantDetails; } private async redirectToWelcomeScreen( tabId: NonNullable, result: GrantResult, intent: InteractionIntent, - errorCode?: ErrorCode + errorCode?: ErrorCode, ) { - const url = new URL(OPEN_PAYMENTS_REDIRECT_URL) - url.searchParams.set('result', result) - url.searchParams.set('intent', intent) - if (errorCode) url.searchParams.set('errorCode', errorCode) + const url = new URL(OPEN_PAYMENTS_REDIRECT_URL); + url.searchParams.set('result', result); + url.searchParams.set('intent', intent); + if (errorCode) url.searchParams.set('errorCode', errorCode); await this.browser.tabs.update(tabId, { - url: url.toString() - }) + url: url.toString(), + }); } private async createOutgoingPaymentGrant({ amount, walletAddress, - clientNonce + clientNonce, }: CreateOutgoingPaymentGrantParams) { const grant = await this.client!.grant.request( { - url: walletAddress.authServer + url: walletAddress.authServer, }, { access_token: { access: [ { type: 'quote', - actions: ['create'] + actions: ['create'], }, { type: 'outgoing-payment', @@ -519,29 +523,31 @@ export class OpenPaymentsService { debitAmount: { value: amount.value, assetScale: walletAddress.assetScale, - assetCode: walletAddress.assetCode + assetCode: walletAddress.assetCode, }, - interval: amount.interval - } - } - ] + interval: amount.interval, + }, + }, + ], }, interact: { start: ['redirect'], finish: { method: 'redirect', uri: OPEN_PAYMENTS_REDIRECT_URL, - nonce: clientNonce - } - } - } - ) + nonce: clientNonce, + }, + }, + }, + ); if (!isPendingGrant(grant)) { - throw new Error('Expected interactive grant. Received non-pending grant.') + throw new Error( + 'Expected interactive grant. Received non-pending grant.', + ); } - return grant + return grant; } private async verifyInteractionHash({ @@ -549,110 +555,110 @@ export class OpenPaymentsService { interactRef, interactNonce, hash, - authServer + authServer, }: VerifyInteractionHashParams): Promise { - const grantEndpoint = new URL(authServer).origin + '/' + const grantEndpoint = new URL(authServer).origin + '/'; const data = new TextEncoder().encode( - `${clientNonce}\n${interactNonce}\n${interactRef}\n${grantEndpoint}` - ) + `${clientNonce}\n${interactNonce}\n${interactRef}\n${grantEndpoint}`, + ); - const digest = await crypto.subtle.digest('SHA-256', data) + const digest = await crypto.subtle.digest('SHA-256', data); const calculatedHash = btoa( - String.fromCharCode.apply(null, new Uint8Array(digest)) - ) - if (calculatedHash !== hash) throw new Error('Invalid interaction hash') + String.fromCharCode.apply(null, new Uint8Array(digest)), + ); + if (calculatedHash !== hash) throw new Error('Invalid interaction hash'); } private async getInteractionInfo(url: string): Promise { return await new Promise((res) => { this.browser.tabs.create({ url }).then((tab) => { - if (!tab.id) return + if (!tab.id) return; const getInteractionInfo: TabUpdateCallback = async ( tabId, - changeInfo + changeInfo, ) => { - if (tabId !== tab.id) return + if (tabId !== tab.id) return; try { - const tabUrl = new URL(changeInfo.url || '') - const interactRef = tabUrl.searchParams.get('interact_ref') - const hash = tabUrl.searchParams.get('hash') - const result = tabUrl.searchParams.get('result') + const tabUrl = new URL(changeInfo.url || ''); + const interactRef = tabUrl.searchParams.get('interact_ref'); + const hash = tabUrl.searchParams.get('hash'); + const result = tabUrl.searchParams.get('result'); if ( (interactRef && hash) || result === 'grant_rejected' || result === 'grant_invalid' ) { - this.browser.tabs.onUpdated.removeListener(getInteractionInfo) + this.browser.tabs.onUpdated.removeListener(getInteractionInfo); } if (interactRef && hash) { - res({ interactRef, hash, tabId }) + res({ interactRef, hash, tabId }); } } catch { /* do nothing */ } - } - this.browser.tabs.onUpdated.addListener(getInteractionInfo) - }) - }) + }; + this.browser.tabs.onUpdated.addListener(getInteractionInfo); + }); + }); } async disconnectWallet() { const { recurringGrant, oneTimeGrant } = await this.storage.get([ 'recurringGrant', - 'oneTimeGrant' - ]) + 'oneTimeGrant', + ]); if (!recurringGrant && !oneTimeGrant) { - return + return; } if (recurringGrant) { - await this.cancelGrant(recurringGrant.continue) - this.isGrantUsable.recurring = false + await this.cancelGrant(recurringGrant.continue); + this.isGrantUsable.recurring = false; } if (oneTimeGrant) { - await this.cancelGrant(oneTimeGrant.continue) - this.isGrantUsable.oneTime = false + await this.cancelGrant(oneTimeGrant.continue); + this.isGrantUsable.oneTime = false; } - await this.storage.clear() - this.grant = null + await this.storage.clear(); + this.grant = null; } private async cancelGrant(grantContinuation: GrantDetails['continue']) { try { - await this.client!.grant.cancel(grantContinuation) + await this.client!.grant.cancel(grantContinuation); } catch (error) { if (isInvalidClientError(error)) { // key already removed from wallet - return + return; } - throw error + throw error; } } async generateKeys() { - if (await this.storage.keyPairExists()) return + if (await this.storage.keyPairExists()) return; - const { privateKey, publicKey } = await generateEd25519KeyPair() - const keyId = crypto.randomUUID() - const jwk = exportJWK(publicKey, keyId) + const { privateKey, publicKey } = await generateEd25519KeyPair(); + const keyId = crypto.randomUUID(); + const jwk = exportJWK(publicKey, keyId); await this.storage.set({ privateKey: bytesToHex(privateKey), publicKey: btoa(JSON.stringify(jwk)), - keyId - }) + keyId, + }); } async createOutgoingPayment({ walletAddress, amount, - incomingPaymentId + incomingPaymentId, }: CreateOutgoingPaymentParams): Promise { const outgoingPayment = (await this.client!.outgoingPayment.create( { accessToken: this.token.value, - url: walletAddress.resourceServer + url: walletAddress.resourceServer, }, { incomingPayment: incomingPaymentId, @@ -660,34 +666,34 @@ export class OpenPaymentsService { debitAmount: { value: amount, assetCode: walletAddress.assetCode, - assetScale: walletAddress.assetScale + assetScale: walletAddress.assetScale, }, metadata: { - source: 'Web Monetization' - } - } - )) as OutgoingPayment + source: 'Web Monetization', + }, + }, + )) as OutgoingPayment; if (outgoingPayment.grantSpentDebitAmount) { this.storage.updateSpentAmount( this.grant!.type, - outgoingPayment.grantSpentDebitAmount.value - ) + outgoingPayment.grantSpentDebitAmount.value, + ); } - await this.storage.setState({ out_of_funds: false }) + await this.storage.setState({ out_of_funds: false }); - return outgoingPayment + return outgoingPayment; } async probeDebitAmount( amount: AmountValue, incomingPayment: IncomingPayment['id'], - sender: WalletAddress + sender: WalletAddress, ): Promise { await this.client!.quote.create( { url: sender.resourceServer, - accessToken: this.token.value + accessToken: this.token.value, }, { method: 'ilp', @@ -696,23 +702,23 @@ export class OpenPaymentsService { debitAmount: { value: amount, assetCode: sender.assetCode, - assetScale: sender.assetScale - } - } - ) + assetScale: sender.assetScale, + }, + }, + ); } async reconnectWallet() { try { - await this.rotateToken() + await this.rotateToken(); } catch (error) { if (isInvalidClientError(error)) { - const msg = this.t('connectWallet_error_invalidClient') - throw new Error(msg, { cause: error }) + const msg = this.t('connectWallet_error_invalidClient'); + throw new Error(msg, { cause: error }); } - throw error + throw error; } - await this.storage.setState({ key_revoked: false }) + await this.storage.setState({ key_revoked: false }); } /** @@ -722,101 +728,101 @@ export class OpenPaymentsService { */ private async _switchGrant(): Promise { if (!this.isAnyGrantUsable()) { - return null + return null; } - this.logger.debug('Switching from grant', this.grant?.type) + this.logger.debug('Switching from grant', this.grant?.type); const { oneTimeGrant, recurringGrant } = await this.storage.get([ 'oneTimeGrant', - 'recurringGrant' - ]) + 'recurringGrant', + ]); if (this.grant?.type === 'recurring') { - this.isGrantUsable.recurring = false + this.isGrantUsable.recurring = false; if (oneTimeGrant) { - this.grant = oneTimeGrant - return 'one-time' + this.grant = oneTimeGrant; + return 'one-time'; } } else if (this.grant?.type === 'one-time') { - this.isGrantUsable.oneTime = false + this.isGrantUsable.oneTime = false; if (recurringGrant) { - this.grant = recurringGrant - return 'recurring' + this.grant = recurringGrant; + return 'recurring'; } } - return null + return null; } async rotateToken() { if (!this.grant) { - throw new Error('No grant to rotate token for') + throw new Error('No grant to rotate token for'); } - const rotate = this.deduplicator.dedupe(this.client!.token.rotate) + const rotate = this.deduplicator.dedupe(this.client!.token.rotate); const newToken = await rotate({ url: this.token.manageUrl, - accessToken: this.token.value - }) + accessToken: this.token.value, + }); const accessToken: AccessToken = { value: newToken.access_token.value, - manageUrl: newToken.access_token.manage - } + manageUrl: newToken.access_token.manage, + }; if (this.grant.type === 'recurring') { - this.storage.set({ recurringGrant: { ...this.grant, accessToken } }) + this.storage.set({ recurringGrant: { ...this.grant, accessToken } }); } else { - this.storage.set({ oneTimeGrant: { ...this.grant, accessToken } }) + this.storage.set({ oneTimeGrant: { ...this.grant, accessToken } }); } - this.grant = { ...this.grant, accessToken } + this.grant = { ...this.grant, accessToken }; } } const isOpenPaymentsClientError = (error: any) => - error instanceof OpenPaymentsClientError + error instanceof OpenPaymentsClientError; export const isKeyRevokedError = (error: any) => { - if (!isOpenPaymentsClientError(error)) return false - return isInvalidClientError(error) || isSignatureValidationError(error) -} + if (!isOpenPaymentsClientError(error)) return false; + return isInvalidClientError(error) || isSignatureValidationError(error); +}; // AUTH SERVER error export const isInvalidClientError = (error: any) => { - if (!isOpenPaymentsClientError(error)) return false - return error.status === 400 && error.code === 'invalid_client' -} + if (!isOpenPaymentsClientError(error)) return false; + return error.status === 400 && error.code === 'invalid_client'; +}; // RESOURCE SERVER error. Create outgoing payment and create quote can fail // with: `Signature validation error: could not find key in list of client keys` export const isSignatureValidationError = (error: any) => { - if (!isOpenPaymentsClientError(error)) return false + if (!isOpenPaymentsClientError(error)) return false; return ( error.status === 401 && error.description?.includes('Signature validation error') - ) -} + ); +}; export const isTokenExpiredError = (error: any) => { - if (!isOpenPaymentsClientError(error)) return false - return isTokenInvalidError(error) || isTokenInactiveError(error) -} + if (!isOpenPaymentsClientError(error)) return false; + return isTokenInvalidError(error) || isTokenInactiveError(error); +}; export const isTokenInvalidError = (error: OpenPaymentsClientError) => { - return error.status === 401 && error.description === 'Invalid Token' -} + return error.status === 401 && error.description === 'Invalid Token'; +}; export const isTokenInactiveError = (error: OpenPaymentsClientError) => { - return error.status === 403 && error.description === 'Inactive Token' -} + return error.status === 403 && error.description === 'Inactive Token'; +}; // happens during quoting only export const isNonPositiveAmountError = (error: any) => { - if (!isOpenPaymentsClientError(error)) return false + if (!isOpenPaymentsClientError(error)) return false; return ( error.status === 400 && error.description?.toLowerCase()?.includes('non-positive receive amount') - ) -} + ); +}; export const isOutOfBalanceError = (error: any) => { - if (!isOpenPaymentsClientError(error)) return false - return error.status === 403 && error.description === 'unauthorized' -} + if (!isOpenPaymentsClientError(error)) return false; + return error.status === 403 && error.description === 'unauthorized'; +}; export const isInvalidReceiverError = (error: any) => { - if (!isOpenPaymentsClientError(error)) return false - return error.status === 400 && error.description === 'invalid receiver' -} + if (!isOpenPaymentsClientError(error)) return false; + return error.status === 400 && error.description === 'invalid receiver'; +}; diff --git a/src/background/services/paymentSession.ts b/src/background/services/paymentSession.ts index dbc26f22..a9207185 100644 --- a/src/background/services/paymentSession.ts +++ b/src/background/services/paymentSession.ts @@ -2,51 +2,51 @@ import { isPendingGrant, type IncomingPayment, type OutgoingPayment, - type WalletAddress -} from '@interledger/open-payments/dist/types' -import { bigIntMax, convert } from '@/shared/helpers' -import { transformBalance } from '@/popup/lib/utils' + type WalletAddress, +} from '@interledger/open-payments/dist/types'; +import { bigIntMax, convert } from '@/shared/helpers'; +import { transformBalance } from '@/popup/lib/utils'; import { isInvalidReceiverError, isKeyRevokedError, isNonPositiveAmountError, isOutOfBalanceError, - isTokenExpiredError -} from './openPayments' -import { getNextSendableAmount } from '@/background/utils' -import type { EventsService, OpenPaymentsService, TabState } from '.' + isTokenExpiredError, +} from './openPayments'; +import { getNextSendableAmount } from '@/background/utils'; +import type { EventsService, OpenPaymentsService, TabState } from '.'; import type { BackgroundToContentMessage, MessageManager, MonetizationEventDetails, - MonetizationEventPayload -} from '@/shared/messages' -import type { AmountValue } from '@/shared/types' -import type { Logger } from '@/shared/logger' + MonetizationEventPayload, +} from '@/shared/messages'; +import type { AmountValue } from '@/shared/types'; +import type { Logger } from '@/shared/logger'; -const HOUR_MS = 3600 * 1000 -const MIN_SEND_AMOUNT = 1n // 1 unit -const MAX_INVALID_RECEIVER_ATTEMPTS = 2 +const HOUR_MS = 3600 * 1000; +const MIN_SEND_AMOUNT = 1n; // 1 unit +const MAX_INVALID_RECEIVER_ATTEMPTS = 2; -type PaymentSessionSource = 'tab-change' | 'request-id-reused' | 'new-link' -type IncomingPaymentSource = 'one-time' | 'continuous' +type PaymentSessionSource = 'tab-change' | 'request-id-reused' | 'new-link'; +type IncomingPaymentSource = 'one-time' | 'continuous'; export class PaymentSession { - private rate: string - private active: boolean = false + private rate: string; + private active: boolean = false; /** Invalid receiver (providers not peered or other reasons) */ - private isInvalid: boolean = false - private countInvalidReceiver: number = 0 - private isDisabled: boolean = false - private incomingPaymentUrl: string - private incomingPaymentExpiresAt: number - private amount: string - private intervalInMs: number - private probingId: number - private shouldRetryImmediately: boolean = false - - private interval: ReturnType | null = null - private timeout: ReturnType | null = null + private isInvalid: boolean = false; + private countInvalidReceiver: number = 0; + private isDisabled: boolean = false; + private incomingPaymentUrl: string; + private incomingPaymentExpiresAt: number; + private amount: string; + private intervalInMs: number; + private probingId: number; + private shouldRetryImmediately: boolean = false; + + private interval: ReturnType | null = null; + private timeout: ReturnType | null = null; constructor( private receiver: WalletAddress, @@ -59,31 +59,31 @@ export class PaymentSession { private tabState: TabState, private url: string, private logger: Logger, - private message: MessageManager + private message: MessageManager, ) {} async adjustAmount(rate: AmountValue): Promise { - this.probingId = Date.now() - const localProbingId = this.probingId - this.rate = rate + this.probingId = Date.now(); + const localProbingId = this.probingId; + this.rate = rate; // The amount that needs to be sent every second. // In senders asset scale already. - let amountToSend = BigInt(this.rate) / 3600n - const senderAssetScale = this.sender.assetScale - const receiverAssetScale = this.receiver.assetScale - const isCrossCurrency = this.sender.assetCode !== this.receiver.assetCode + let amountToSend = BigInt(this.rate) / 3600n; + const senderAssetScale = this.sender.assetScale; + const receiverAssetScale = this.receiver.assetScale; + const isCrossCurrency = this.sender.assetCode !== this.receiver.assetCode; if (!isCrossCurrency) { if (amountToSend <= MIN_SEND_AMOUNT) { // We need to add another unit when using a debit amount, since // @interledger/pay subtracts one unit. if (senderAssetScale <= receiverAssetScale) { - amountToSend = MIN_SEND_AMOUNT + 1n + amountToSend = MIN_SEND_AMOUNT + 1n; } else if (senderAssetScale > receiverAssetScale) { // If the sender scale is greater than the receiver scale, the unit // issue will not be present. - amountToSend = MIN_SEND_AMOUNT + amountToSend = MIN_SEND_AMOUNT; } } @@ -94,76 +94,76 @@ export class PaymentSession { const amountInReceiversScale = convert( amountToSend, senderAssetScale, - receiverAssetScale - ) + receiverAssetScale, + ); if (amountInReceiversScale === 0n) { amountToSend = convert( MIN_SEND_AMOUNT, receiverAssetScale, - senderAssetScale - ) + senderAssetScale, + ); } } } // This all will eventually get replaced by OpenPayments response update // that includes a min rate that we can directly use. - await this.setIncomingPaymentUrl() + await this.setIncomingPaymentUrl(); const amountIter = getNextSendableAmount( senderAssetScale, receiverAssetScale, - bigIntMax(amountToSend, MIN_SEND_AMOUNT) - ) + bigIntMax(amountToSend, MIN_SEND_AMOUNT), + ); - amountToSend = BigInt(amountIter.next().value) + amountToSend = BigInt(amountIter.next().value); while (true) { if (this.probingId !== localProbingId) { // In future we can throw `new AbortError()` - throw new DOMException('Aborting existing probing', 'AbortError') + throw new DOMException('Aborting existing probing', 'AbortError'); } try { await this.openPaymentsService.probeDebitAmount( amountToSend.toString(), this.incomingPaymentUrl, - this.sender - ) - this.setAmount(amountToSend) - break + this.sender, + ); + this.setAmount(amountToSend); + break; } catch (e) { if (isTokenExpiredError(e)) { - await this.openPaymentsService.rotateToken() + await this.openPaymentsService.rotateToken(); } else if (isNonPositiveAmountError(e)) { - amountToSend = BigInt(amountIter.next().value) - continue + amountToSend = BigInt(amountIter.next().value); + continue; } else if (isInvalidReceiverError(e)) { - this.markInvalid() + this.markInvalid(); this.events.emit('open_payments.invalid_receiver', { - tabId: this.tabId - }) - break + tabId: this.tabId, + }); + break; } else { - throw e + throw e; } } } } get id() { - return this.requestId + return this.requestId; } get disabled() { - return this.isDisabled + return this.isDisabled; } get invalid() { - return this.isInvalid + return this.isInvalid; } disable() { - this.isDisabled = true - this.stop() + this.isDisabled = true; + this.stop(); } /** @@ -172,66 +172,66 @@ export class PaymentSession { * @deprecated */ enable() { - throw new Error('Method not implemented.') + throw new Error('Method not implemented.'); } private markInvalid() { - this.isInvalid = true - this.stop() + this.isInvalid = true; + this.stop(); } stop() { - this.active = false - this.clearTimers() + this.active = false; + this.clearTimers(); } resume() { - this.start('tab-change') + this.start('tab-change'); } private clearTimers() { if (this.interval) { - this.debug(`Clearing interval=${this.timeout}`) - clearInterval(this.interval) - this.interval = null + this.debug(`Clearing interval=${this.timeout}`); + clearInterval(this.interval); + this.interval = null; } if (this.timeout) { - this.debug(`Clearing timeout=${this.timeout}`) - clearTimeout(this.timeout) - this.timeout = null + this.debug(`Clearing timeout=${this.timeout}`); + clearTimeout(this.timeout); + this.timeout = null; } } private debug(message: string) { this.logger.debug( `[PAYMENT SESSION] requestId=${this.requestId}; receiver=${this.receiver.id}\n\n`, - ` ${message}` - ) + ` ${message}`, + ); } async start(source: PaymentSessionSource) { this.debug( - `Attempting to start; source=${source} active=${this.active} disabled=${this.isDisabled} isInvalid=${this.isInvalid}` - ) - if (this.active || this.isDisabled || this.isInvalid) return - this.debug(`Session started; source=${source}`) - this.active = true + `Attempting to start; source=${source} active=${this.active} disabled=${this.isDisabled} isInvalid=${this.isInvalid}`, + ); + if (this.active || this.isDisabled || this.isInvalid) return; + this.debug(`Session started; source=${source}`); + this.active = true; - await this.setIncomingPaymentUrl() + await this.setIncomingPaymentUrl(); const { waitTime, monetizationEvent } = this.tabState.getOverpayingDetails( this.tabId, this.url, - this.receiver.id - ) + this.receiver.id, + ); - this.debug(`Overpaying: waitTime=${waitTime}`) + this.debug(`Overpaying: waitTime=${waitTime}`); if (monetizationEvent && source !== 'tab-change') { this.sendMonetizationEvent({ requestId: this.requestId, - details: monetizationEvent - }) + details: monetizationEvent, + }); } // Uncomment this after we perform the Rafiki test and remove the leftover @@ -253,29 +253,29 @@ export class PaymentSession { // Leftover const continuePayment = () => { - if (!this.canContinuePayment) return + if (!this.canContinuePayment) return; // alternatively (leftover) after we perform the Rafiki test, we can just // skip the `.then()` here and call setTimeout recursively immediately void this.payContinuous().then(() => { this.timeout = setTimeout( () => { - continuePayment() + continuePayment(); }, - this.shouldRetryImmediately ? 0 : this.intervalInMs - ) - }) - } + this.shouldRetryImmediately ? 0 : this.intervalInMs, + ); + }); + }; if (this.canContinuePayment) { this.timeout = setTimeout(async () => { - await this.payContinuous() + await this.payContinuous(); this.timeout = setTimeout( () => { - continuePayment() + continuePayment(); }, - this.shouldRetryImmediately ? 0 : this.intervalInMs - ) - }, waitTime) + this.shouldRetryImmediately ? 0 : this.intervalInMs, + ); + }, waitTime); } } @@ -284,40 +284,40 @@ export class PaymentSession { this.tabId, this.frameId, 'MONETIZATION_EVENT', - payload - ) + payload, + ); } private get canContinuePayment() { - return this.active && !this.isDisabled && !this.isInvalid + return this.active && !this.isDisabled && !this.isInvalid; } private async setIncomingPaymentUrl(reset?: boolean) { - if (this.incomingPaymentUrl && !reset) return + if (this.incomingPaymentUrl && !reset) return; try { - const incomingPayment = await this.createIncomingPayment('continuous') - this.incomingPaymentUrl = incomingPayment.id + const incomingPayment = await this.createIncomingPayment('continuous'); + this.incomingPaymentUrl = incomingPayment.id; } catch (error) { if (isKeyRevokedError(error)) { - this.events.emit('open_payments.key_revoked') - return + this.events.emit('open_payments.key_revoked'); + return; } - throw error + throw error; } } private async createIncomingPayment( - source: IncomingPaymentSource + source: IncomingPaymentSource, ): Promise { const expiresAt = new Date( - Date.now() + 1000 * (source === 'continuous' ? 60 * 10 : 30) - ).toISOString() + Date.now() + 1000 * (source === 'continuous' ? 60 * 10 : 30), + ).toISOString(); const incomingPaymentGrant = await this.openPaymentsService.client!.grant.request( { - url: this.receiver.authServer + url: this.receiver.authServer, }, { access_token: { @@ -325,82 +325,84 @@ export class PaymentSession { { type: 'incoming-payment', actions: ['create'], - identifier: this.receiver.id - } - ] - } - } - ) + identifier: this.receiver.id, + }, + ], + }, + }, + ); if (isPendingGrant(incomingPaymentGrant)) { - throw new Error('Expected non-interactive grant. Received pending grant.') + throw new Error( + 'Expected non-interactive grant. Received pending grant.', + ); } const incomingPayment = await this.openPaymentsService.client!.incomingPayment.create( { url: this.receiver.resourceServer, - accessToken: incomingPaymentGrant.access_token.value + accessToken: incomingPaymentGrant.access_token.value, }, { walletAddress: this.receiver.id, expiresAt, metadata: { - source: 'Web Monetization' - } - } - ) + source: 'Web Monetization', + }, + }, + ); if (incomingPayment.expiresAt) { this.incomingPaymentExpiresAt = new Date( - incomingPayment.expiresAt - ).valueOf() + incomingPayment.expiresAt, + ).valueOf(); } // Revoke grant to avoid leaving users with unused, dangling grants. await this.openPaymentsService.client!.grant.cancel({ url: incomingPaymentGrant.continue.uri, - accessToken: incomingPaymentGrant.continue.access_token.value - }) + accessToken: incomingPaymentGrant.continue.access_token.value, + }); - return incomingPayment + return incomingPayment; } async pay(amount: number) { if (this.isDisabled) { - throw new Error('Attempted to send a payment to a disabled session.') + throw new Error('Attempted to send a payment to a disabled session.'); } const incomingPayment = await this.createIncomingPayment('one-time').catch( (error) => { if (isKeyRevokedError(error)) { - this.events.emit('open_payments.key_revoked') - return + this.events.emit('open_payments.key_revoked'); + return; } - throw error - } - ) - if (!incomingPayment) return + throw error; + }, + ); + if (!incomingPayment) return; - let outgoingPayment: OutgoingPayment | undefined + let outgoingPayment: OutgoingPayment | undefined; try { outgoingPayment = await this.openPaymentsService.createOutgoingPayment({ walletAddress: this.sender, incomingPaymentId: incomingPayment.id, - amount: (amount * 10 ** this.sender.assetScale).toFixed(0) - }) + amount: (amount * 10 ** this.sender.assetScale).toFixed(0), + }); } catch (e) { if (isKeyRevokedError(e)) { - this.events.emit('open_payments.key_revoked') + this.events.emit('open_payments.key_revoked'); } else if (isTokenExpiredError(e)) { - await this.openPaymentsService.rotateToken() + await this.openPaymentsService.rotateToken(); } else { - throw e + throw e; } } finally { if (outgoingPayment) { - const { receiveAmount, receiver: incomingPayment } = outgoingPayment + const { receiveAmount, receiver: incomingPayment } = outgoingPayment; this.sendMonetizationEvent({ requestId: this.requestId, @@ -409,22 +411,22 @@ export class PaymentSession { currency: receiveAmount.assetCode, value: transformBalance( receiveAmount.value, - receiveAmount.assetScale - ) + receiveAmount.assetScale, + ), }, incomingPayment, - paymentPointer: this.receiver.id - } - }) + paymentPointer: this.receiver.id, + }, + }); } } - return outgoingPayment?.debitAmount + return outgoingPayment?.debitAmount; } private setAmount(amount: bigint): void { - this.amount = amount.toString() - this.intervalInMs = Number((amount * BigInt(HOUR_MS)) / BigInt(this.rate)) + this.amount = amount.toString(); + this.intervalInMs = Number((amount * BigInt(HOUR_MS)) / BigInt(this.rate)); } private async payContinuous() { @@ -433,65 +435,68 @@ export class PaymentSession { await this.openPaymentsService.createOutgoingPayment({ walletAddress: this.sender, incomingPaymentId: this.incomingPaymentUrl, - amount: this.amount - }) - const { receiveAmount, receiver: incomingPayment } = outgoingPayment + amount: this.amount, + }); + const { receiveAmount, receiver: incomingPayment } = outgoingPayment; const monetizationEventDetails: MonetizationEventDetails = { amountSent: { currency: receiveAmount.assetCode, - value: transformBalance(receiveAmount.value, receiveAmount.assetScale) + value: transformBalance( + receiveAmount.value, + receiveAmount.assetScale, + ), }, incomingPayment, - paymentPointer: this.receiver.id - } + paymentPointer: this.receiver.id, + }; this.sendMonetizationEvent({ requestId: this.requestId, - details: monetizationEventDetails - }) + details: monetizationEventDetails, + }); // TO DO: find a better source of truth for deciding if overpaying is applicable if (this.intervalInMs > 1000) { this.tabState.saveOverpaying(this.tabId, this.url, { walletAddressId: this.receiver.id, monetizationEvent: monetizationEventDetails, - intervalInMs: this.intervalInMs - }) + intervalInMs: this.intervalInMs, + }); } - this.shouldRetryImmediately = false + this.shouldRetryImmediately = false; } catch (e) { if (isKeyRevokedError(e)) { - this.events.emit('open_payments.key_revoked') + this.events.emit('open_payments.key_revoked'); } else if (isTokenExpiredError(e)) { - await this.openPaymentsService.rotateToken() - this.shouldRetryImmediately = true + await this.openPaymentsService.rotateToken(); + this.shouldRetryImmediately = true; } else if (isOutOfBalanceError(e)) { - const switched = await this.openPaymentsService.switchGrant() + const switched = await this.openPaymentsService.switchGrant(); if (switched === null) { - this.events.emit('open_payments.out_of_funds') + this.events.emit('open_payments.out_of_funds'); } else { - this.shouldRetryImmediately = true + this.shouldRetryImmediately = true; } } else if (isInvalidReceiverError(e)) { if (Date.now() >= this.incomingPaymentExpiresAt) { - await this.setIncomingPaymentUrl(true) - this.shouldRetryImmediately = true + await this.setIncomingPaymentUrl(true); + this.shouldRetryImmediately = true; } else { - ++this.countInvalidReceiver + ++this.countInvalidReceiver; if ( this.countInvalidReceiver >= MAX_INVALID_RECEIVER_ATTEMPTS && !this.isInvalid ) { - this.markInvalid() + this.markInvalid(); this.events.emit('open_payments.invalid_receiver', { - tabId: this.tabId - }) + tabId: this.tabId, + }); } else { - this.shouldRetryImmediately = true + this.shouldRetryImmediately = true; } } } else { - throw e + throw e; } } } diff --git a/src/background/services/sendToPopup.ts b/src/background/services/sendToPopup.ts index aef24db5..22a702d9 100644 --- a/src/background/services/sendToPopup.ts +++ b/src/background/services/sendToPopup.ts @@ -1,53 +1,53 @@ -import type { Runtime } from 'webextension-polyfill' +import type { Runtime } from 'webextension-polyfill'; import { BACKGROUND_TO_POPUP_CONNECTION_NAME as CONNECTION_NAME, type BackgroundToPopupMessage, - type BackgroundToPopupMessagesMap -} from '@/shared/messages' -import type { Cradle } from '@/background/container' + type BackgroundToPopupMessagesMap, +} from '@/shared/messages'; +import type { Cradle } from '@/background/container'; export class SendToPopup { - private browser: Cradle['browser'] + private browser: Cradle['browser']; - private isConnected = false - private port: Runtime.Port - private queue = new Map() + private isConnected = false; + private port: Runtime.Port; + private queue = new Map(); constructor({ browser }: Cradle) { - Object.assign(this, { browser }) + Object.assign(this, { browser }); } start() { this.browser.runtime.onConnect.addListener((port) => { - if (port.name !== CONNECTION_NAME) return + if (port.name !== CONNECTION_NAME) return; if (port.error) { - return + return; } - this.port = port - this.isConnected = true + this.port = port; + this.isConnected = true; for (const [type, data] of this.queue) { - this.send(type, data) - this.queue.delete(type) + this.send(type, data); + this.queue.delete(type); } port.onDisconnect.addListener(() => { - this.isConnected = false - }) - }) + this.isConnected = false; + }); + }); } get isPopupOpen() { - return this.isConnected + return this.isConnected; } async send( type: T, - data: BackgroundToPopupMessagesMap[T] + data: BackgroundToPopupMessagesMap[T], ) { if (!this.isConnected) { - this.queue.set(type, data) - return + this.queue.set(type, data); + return; } - const message = { type, data } as BackgroundToPopupMessage - this.port.postMessage(message) + const message = { type, data } as BackgroundToPopupMessage; + this.port.postMessage(message); } } diff --git a/src/background/services/storage.ts b/src/background/services/storage.ts index 7d9ace01..754ada38 100644 --- a/src/background/services/storage.ts +++ b/src/background/services/storage.ts @@ -4,11 +4,11 @@ import type { GrantDetails, Storage, StorageKey, - WalletAmount -} from '@/shared/types' -import { bigIntMax, objectEquals, ThrottleBatch } from '@/shared/helpers' -import { computeBalance } from '../utils' -import type { Cradle } from '../container' + WalletAmount, +} from '@/shared/types'; +import { bigIntMax, objectEquals, ThrottleBatch } from '@/shared/helpers'; +import { computeBalance } from '../utils'; +import type { Cradle } from '../container'; const defaultStorage = { /** @@ -30,59 +30,59 @@ const defaultStorage = { oneTimeGrantSpentAmount: '0', rateOfPay: null, minRateOfPay: null, - maxRateOfPay: null -} satisfies Omit + maxRateOfPay: null, +} satisfies Omit; export class StorageService { - private browser: Cradle['browser'] - private events: Cradle['events'] + private browser: Cradle['browser']; + private events: Cradle['events']; - private setSpentAmountRecurring: ThrottleBatch<[amount: string]> - private setSpentAmountOneTime: ThrottleBatch<[amount: string]> + private setSpentAmountRecurring: ThrottleBatch<[amount: string]>; + private setSpentAmountOneTime: ThrottleBatch<[amount: string]>; // used as an optimization/cache - private currentState: Storage['state'] | null = null + private currentState: Storage['state'] | null = null; constructor({ browser, events }: Cradle) { - Object.assign(this, { browser, events }) + Object.assign(this, { browser, events }); this.setSpentAmountRecurring = new ThrottleBatch( (amount) => this.setSpentAmount('recurring', amount), (args) => [args.reduce((max, [v]) => bigIntMax(max, v), '0')], - 1000 - ) + 1000, + ); this.setSpentAmountOneTime = new ThrottleBatch( (amount) => this.setSpentAmount('one-time', amount), (args) => [args.reduce((max, [v]) => bigIntMax(max, v), '0')], - 1000 - ) + 1000, + ); } async get( - keys?: TKey[] + keys?: TKey[], ): Promise<{ [Key in TKey[][number]]: Storage[Key] }> { - const data = await this.browser.storage.local.get(keys) - return data as { [Key in TKey[][number]]: Storage[Key] } + const data = await this.browser.storage.local.get(keys); + return data as { [Key in TKey[][number]]: Storage[Key] }; } async set(data: { - [K in TKey]: Storage[TKey] + [K in TKey]: Storage[TKey]; }): Promise { - await this.browser.storage.local.set(data) + await this.browser.storage.local.set(data); } async clear(): Promise { - await this.set(defaultStorage) - this.currentState = { ...defaultStorage.state } + await this.set(defaultStorage); + this.currentState = { ...defaultStorage.state }; } /** * Needs to run before any other storage `set` call. */ async populate(): Promise { - const data = await this.get(Object.keys(defaultStorage) as StorageKey[]) + const data = await this.get(Object.keys(defaultStorage) as StorageKey[]); if (Object.keys(data).length === 0) { - await this.set(defaultStorage) + await this.set(defaultStorage); } } @@ -90,36 +90,36 @@ export class StorageService { * Migrate storage to given target version. */ async migrate(targetVersion: Storage['version'] = defaultStorage.version) { - const storage = this.browser.storage.local + const storage = this.browser.storage.local; - let { version = 1 } = await this.get(['version']) + let { version = 1 } = await this.get(['version']); if (version === targetVersion) { - return null + return null; } - let data = await storage.get() + let data = await storage.get(); while (version < targetVersion) { - ++version - const migrate = MIGRATIONS[version] + ++version; + const migrate = MIGRATIONS[version]; if (!migrate) { - throw new Error(`No migration available to reach version "${version}"`) + throw new Error(`No migration available to reach version "${version}"`); } - const [newData, deleteKeys = []] = migrate(data) - data = { ...newData, version } - await storage.set(data) - await storage.remove(deleteKeys) + const [newData, deleteKeys = []] = migrate(data); + data = { ...newData, version }; + await storage.set(data); + await storage.remove(deleteKeys); } - return data as unknown as Storage + return data as unknown as Storage; } async getWMState(): Promise { - const { enabled } = await this.get(['enabled']) + const { enabled } = await this.get(['enabled']); - return enabled + return enabled; } async keyPairExists(): Promise { - const keys = await this.get(['privateKey', 'publicKey', 'keyId']) + const keys = await this.get(['privateKey', 'publicKey', 'keyId']); if ( keys.privateKey && typeof keys.privateKey === 'string' && @@ -128,48 +128,48 @@ export class StorageService { keys.keyId && typeof keys.keyId === 'string' ) { - return true + return true; } - return false + return false; } async setState(state: Storage['state']): Promise { - const prevState = this.currentState ?? (await this.get(['state'])).state + const prevState = this.currentState ?? (await this.get(['state'])).state; - const newState: Storage['state'] = { ...this.currentState } + const newState: Storage['state'] = { ...this.currentState }; for (const key of Object.keys(state) as ExtensionState[]) { - newState[key] = state[key] + newState[key] = state[key]; } - this.currentState = newState + this.currentState = newState; if (prevState && objectEquals(prevState, newState)) { - return false + return false; } - await this.set({ state: newState }) + await this.set({ state: newState }); this.events.emit('storage.state_update', { state: newState, - prevState: prevState - }) - return true + prevState: prevState, + }); + return true; } updateSpentAmount(grant: GrantDetails['type'], amount: string) { if (grant === 'recurring') { - this.setSpentAmountRecurring.enqueue(amount) + this.setSpentAmountRecurring.enqueue(amount); } else if (grant === 'one-time') { - this.setSpentAmountOneTime.enqueue(amount) + this.setSpentAmountOneTime.enqueue(amount); } } private async setSpentAmount(grant: GrantDetails['type'], amount: string) { if (grant === 'recurring') { - await this.set({ recurringGrantSpentAmount: amount }) + await this.set({ recurringGrantSpentAmount: amount }); } else if (grant === 'one-time') { - await this.set({ oneTimeGrantSpentAmount: amount }) + await this.set({ oneTimeGrantSpentAmount: amount }); } - const balance = await this.getBalance() - this.events.emit('storage.balance_update', balance) + const balance = await this.getBalance(); + this.events.emit('storage.balance_update', balance); } async getBalance(): Promise< @@ -179,27 +179,27 @@ export class StorageService { 'recurringGrant', 'recurringGrantSpentAmount', 'oneTimeGrant', - 'oneTimeGrantSpentAmount' - ]) + 'oneTimeGrantSpentAmount', + ]); const balanceRecurring = computeBalance( data.recurringGrant, - data.recurringGrantSpentAmount - ) + data.recurringGrantSpentAmount, + ); const balanceOneTime = computeBalance( data.oneTimeGrant, - data.oneTimeGrantSpentAmount - ) - const balance = balanceRecurring + balanceOneTime + data.oneTimeGrantSpentAmount, + ); + const balance = balanceRecurring + balanceOneTime; return { total: balance.toString(), recurring: balanceRecurring.toString(), - oneTime: balanceOneTime.toString() - } + oneTime: balanceOneTime.toString(), + }; } async updateRate(rate: string): Promise { - await this.set({ rateOfPay: rate }) - this.events.emit('storage.rate_of_pay_update', { rate }) + await this.set({ rateOfPay: rate }); + this.events.emit('storage.rate_of_pay_update', { rate }); } } @@ -207,8 +207,8 @@ export class StorageService { * @param existingData Existing data from previous version. */ type Migration = ( - existingData: Record -) => [data: Record, deleteKeys?: string[]] + existingData: Record, +) => [data: Record, deleteKeys?: string[]]; // There was never a migration to reach 1. // @@ -216,16 +216,16 @@ type Migration = ( // require user to reinstall and setup extension from scratch. const MIGRATIONS: Record = { 2: (data) => { - const deleteKeys = ['amount', 'token', 'grant', 'hasHostPermissions'] + const deleteKeys = ['amount', 'token', 'grant', 'hasHostPermissions']; - data.recurringGrant = null - data.recurringGrantSpentAmount = '0' - data.oneTimeGrant = null - data.oneTimeGrantSpentAmount = '0' - data.state = null + data.recurringGrant = null; + data.recurringGrantSpentAmount = '0'; + data.oneTimeGrant = null; + data.oneTimeGrantSpentAmount = '0'; + data.state = null; if (data.amount?.value && data.token && data.grant) { - const type = data.amount.interval ? 'recurring' : 'one-time' + const type = data.amount.interval ? 'recurring' : 'one-time'; const grantDetails: GrantDetails = { type, @@ -233,37 +233,37 @@ const MIGRATIONS: Record = { value: data.amount.value as string, ...(type === 'recurring' ? { interval: data.amount.interval as string } - : {}) + : {}), } as Required, accessToken: { value: data.token.value as string, - manageUrl: data.token.manage as string + manageUrl: data.token.manage as string, }, continue: { url: data.grant.continueUri as string, - accessToken: data.grant.accessToken as string - } - } + accessToken: data.grant.accessToken as string, + }, + }; if (type === 'recurring') { - data.recurringGrant = grantDetails + data.recurringGrant = grantDetails; } else { - data.oneTimeGrant = grantDetails + data.oneTimeGrant = grantDetails; } } if (data.hasHostPermissions === false) { - data.state = 'missing_host_permissions' + data.state = 'missing_host_permissions'; } - return [data, deleteKeys] + return [data, deleteKeys]; }, 3: (data) => { const newState = data.state && typeof data.state === 'string' ? { [data.state as ExtensionState]: true } - : {} - data.state = newState satisfies Storage['state'] - return [data] - } -} + : {}; + data.state = newState satisfies Storage['state']; + return [data]; + }, +}; diff --git a/src/background/services/tabEvents.ts b/src/background/services/tabEvents.ts index 059a988c..2bf5930a 100644 --- a/src/background/services/tabEvents.ts +++ b/src/background/services/tabEvents.ts @@ -1,62 +1,62 @@ -import browser from 'webextension-polyfill' -import type { Browser } from 'webextension-polyfill' -import { isOkState, removeQueryParams } from '@/shared/helpers' -import type { Storage, TabId } from '@/shared/types' -import type { Cradle } from '@/background/container' +import browser from 'webextension-polyfill'; +import type { Browser } from 'webextension-polyfill'; +import { isOkState, removeQueryParams } from '@/shared/helpers'; +import type { Storage, TabId } from '@/shared/types'; +import type { Cradle } from '@/background/container'; -const runtime = browser.runtime +const runtime = browser.runtime; const ICONS = { default: { 32: runtime.getURL('assets/icons/32x32/default.png'), 48: runtime.getURL('assets/icons/48x48/default.png'), - 128: runtime.getURL('assets/icons/128x128/default.png') + 128: runtime.getURL('assets/icons/128x128/default.png'), }, default_gray: { 32: runtime.getURL('assets/icons/32x32/default-gray.png'), 48: runtime.getURL('assets/icons/48x48/default-gray.png'), - 128: runtime.getURL('assets/icons/128x128/default-gray.png') + 128: runtime.getURL('assets/icons/128x128/default-gray.png'), }, enabled_hasLinks: { 32: runtime.getURL('assets/icons/32x32/enabled-has-links.png'), 48: runtime.getURL('assets/icons/48x48/enabled-has-links.png'), - 128: runtime.getURL('assets/icons/128x128/enabled-has-links.png') + 128: runtime.getURL('assets/icons/128x128/enabled-has-links.png'), }, enabled_noLinks: { 32: runtime.getURL('assets/icons/32x32/enabled-no-links.png'), 48: runtime.getURL('assets/icons/48x48/enabled-no-links.png'), - 128: runtime.getURL('assets/icons/128x128/enabled-no-links.png') + 128: runtime.getURL('assets/icons/128x128/enabled-no-links.png'), }, enabled_warn: { 32: runtime.getURL('assets/icons/32x32/enabled-warn.png'), 48: runtime.getURL('assets/icons/48x48/enabled-warn.png'), - 128: runtime.getURL('assets/icons/128x128/enabled-warn.png') + 128: runtime.getURL('assets/icons/128x128/enabled-warn.png'), }, disabled_hasLinks: { 32: runtime.getURL('assets/icons/32x32/disabled-has-links.png'), 48: runtime.getURL('assets/icons/48x48/disabled-has-links.png'), - 128: runtime.getURL('assets/icons/128x128/disabled-has-links.png') + 128: runtime.getURL('assets/icons/128x128/disabled-has-links.png'), }, disabled_noLinks: { 32: runtime.getURL('assets/icons/32x32/disabled-no-links.png'), 48: runtime.getURL('assets/icons/48x48/disabled-no-links.png'), - 128: runtime.getURL('assets/icons/128x128/disabled-no-links.png') + 128: runtime.getURL('assets/icons/128x128/disabled-no-links.png'), }, disabled_warn: { 32: runtime.getURL('assets/icons/32x32/disabled-warn.png'), 48: runtime.getURL('assets/icons/48x48/disabled-warn.png'), - 128: runtime.getURL('assets/icons/128x128/disabled-warn.png') - } -} + 128: runtime.getURL('assets/icons/128x128/disabled-warn.png'), + }, +}; type CallbackTab> = - Parameters[0] + Parameters[0]; export class TabEvents { - private storage: Cradle['storage'] - private tabState: Cradle['tabState'] - private sendToPopup: Cradle['sendToPopup'] - private t: Cradle['t'] - private browser: Cradle['browser'] + private storage: Cradle['storage']; + private tabState: Cradle['tabState']; + private sendToPopup: Cradle['sendToPopup']; + private t: Cradle['t']; + private browser: Cradle['browser']; constructor({ storage, tabState, sendToPopup, t, browser }: Cradle) { Object.assign(this, { @@ -64,8 +64,8 @@ export class TabEvents { tabState, sendToPopup, t, - browser - }) + browser, + }); } onUpdatedTab: CallbackTab<'onUpdated'> = (tabId, changeInfo, tab) => { @@ -74,30 +74,30 @@ export class TabEvents { * if loading and url -> we need to check if state keys include this url. */ if (changeInfo.status === 'loading') { - const url = tab.url ? removeQueryParams(tab.url) : '' - const clearOverpaying = this.tabState.shouldClearOverpaying(tabId, url) + const url = tab.url ? removeQueryParams(tab.url) : ''; + const clearOverpaying = this.tabState.shouldClearOverpaying(tabId, url); - this.tabState.clearSessionsByTabId(tabId) + this.tabState.clearSessionsByTabId(tabId); if (clearOverpaying) { - this.tabState.clearOverpayingByTabId(tabId) + this.tabState.clearOverpayingByTabId(tabId); } - void this.updateVisualIndicators(tabId) + void this.updateVisualIndicators(tabId); } - } + }; onRemovedTab: CallbackTab<'onRemoved'> = (tabId, _removeInfo) => { - this.tabState.clearSessionsByTabId(tabId) - this.tabState.clearOverpayingByTabId(tabId) - } + this.tabState.clearSessionsByTabId(tabId); + this.tabState.clearOverpayingByTabId(tabId); + }; onActivatedTab: CallbackTab<'onActivated'> = async (info) => { - await this.updateVisualIndicators(info.tabId) - } + await this.updateVisualIndicators(info.tabId); + }; onCreatedTab: CallbackTab<'onCreated'> = async (tab) => { - if (!tab.id) return - await this.updateVisualIndicators(tab.id) - } + if (!tab.id) return; + await this.updateVisualIndicators(tab.id); + }; updateVisualIndicators = async ( tabId: TabId, @@ -106,77 +106,77 @@ export class TabEvents { : false, hasTabAllSessionsInvalid: boolean = tabId ? this.tabState.tabHasAllSessionsInvalid(tabId) - : false + : false, ) => { const { enabled, connected, state } = await this.storage.get([ 'enabled', 'connected', - 'state' - ]) + 'state', + ]); const { path, title, isMonetized } = this.getIconAndTooltip({ enabled, connected, state, isTabMonetized, - hasTabAllSessionsInvalid - }) + hasTabAllSessionsInvalid, + }); - this.sendToPopup.send('SET_IS_MONETIZED', isMonetized) - this.sendToPopup.send('SET_ALL_SESSIONS_INVALID', hasTabAllSessionsInvalid) - await this.setIconAndTooltip(path, title, tabId) - } + this.sendToPopup.send('SET_IS_MONETIZED', isMonetized); + this.sendToPopup.send('SET_ALL_SESSIONS_INVALID', hasTabAllSessionsInvalid); + await this.setIconAndTooltip(path, title, tabId); + }; // TODO: memoize this call private setIconAndTooltip = async ( path: (typeof ICONS)[keyof typeof ICONS], title: string, - tabId?: TabId + tabId?: TabId, ) => { - await this.browser.action.setIcon({ path, tabId }) - await this.browser.action.setTitle({ title, tabId }) - } + await this.browser.action.setIcon({ path, tabId }); + await this.browser.action.setTitle({ title, tabId }); + }; private getIconAndTooltip({ enabled, connected, state, isTabMonetized, - hasTabAllSessionsInvalid + hasTabAllSessionsInvalid, }: { - enabled: Storage['enabled'] - connected: Storage['connected'] - state: Storage['state'] - isTabMonetized: boolean - hasTabAllSessionsInvalid: boolean + enabled: Storage['enabled']; + connected: Storage['connected']; + state: Storage['state']; + isTabMonetized: boolean; + hasTabAllSessionsInvalid: boolean; }) { - let title = this.t('appName') - let iconData = ICONS.default + let title = this.t('appName'); + let iconData = ICONS.default; if (!connected) { // use defaults } else if (!isOkState(state) || hasTabAllSessionsInvalid) { - iconData = enabled ? ICONS.enabled_warn : ICONS.disabled_warn - const tabStateText = this.t('icon_state_actionRequired') - title = `${title} - ${tabStateText}` + iconData = enabled ? ICONS.enabled_warn : ICONS.disabled_warn; + const tabStateText = this.t('icon_state_actionRequired'); + title = `${title} - ${tabStateText}`; } else { if (enabled) { iconData = isTabMonetized ? ICONS.enabled_hasLinks - : ICONS.enabled_noLinks + : ICONS.enabled_noLinks; } else { iconData = isTabMonetized ? ICONS.disabled_hasLinks - : ICONS.disabled_noLinks + : ICONS.disabled_noLinks; } const tabStateText = isTabMonetized ? this.t('icon_state_monetizationActive') - : this.t('icon_state_monetizationInactive') - title = `${title} - ${tabStateText}` + : this.t('icon_state_monetizationInactive'); + title = `${title} - ${tabStateText}`; } return { path: iconData, isMonetized: isTabMonetized, - title - } + title, + }; } } diff --git a/src/background/services/tabState.ts b/src/background/services/tabState.ts index ef40ef5f..29dd4059 100644 --- a/src/background/services/tabState.ts +++ b/src/background/services/tabState.ts @@ -1,140 +1,140 @@ -import type { MonetizationEventDetails } from '@/shared/messages' -import type { TabId } from '@/shared/types' -import type { PaymentSession } from './paymentSession' -import type { Cradle } from '@/background/container' +import type { MonetizationEventDetails } from '@/shared/messages'; +import type { TabId } from '@/shared/types'; +import type { PaymentSession } from './paymentSession'; +import type { Cradle } from '@/background/container'; type State = { - monetizationEvent: MonetizationEventDetails - lastPaymentTimestamp: number - expiresAtTimestamp: number -} + monetizationEvent: MonetizationEventDetails; + lastPaymentTimestamp: number; + expiresAtTimestamp: number; +}; interface SaveOverpayingDetails { - walletAddressId: string - monetizationEvent: MonetizationEventDetails - intervalInMs: number + walletAddressId: string; + monetizationEvent: MonetizationEventDetails; + intervalInMs: number; } -type SessionId = string +type SessionId = string; export class TabState { - private logger: Cradle['logger'] + private logger: Cradle['logger']; - private state = new Map>() - private sessions = new Map>() + private state = new Map>(); + private sessions = new Map>(); constructor({ logger }: Cradle) { Object.assign(this, { - logger - }) + logger, + }); } private getOverpayingStateKey(url: string, walletAddressId: string): string { - return `${url}:${walletAddressId}` + return `${url}:${walletAddressId}`; } shouldClearOverpaying(tabId: TabId, url: string): boolean { - const tabState = this.state.get(tabId) - if (!tabState?.size || !url) return false - return ![...tabState.keys()].some((key) => key.startsWith(`${url}:`)) + const tabState = this.state.get(tabId); + if (!tabState?.size || !url) return false; + return ![...tabState.keys()].some((key) => key.startsWith(`${url}:`)); } getOverpayingDetails( tabId: TabId, url: string, - walletAddressId: string + walletAddressId: string, ): { waitTime: number; monetizationEvent?: MonetizationEventDetails } { - const key = this.getOverpayingStateKey(url, walletAddressId) - const state = this.state.get(tabId)?.get(key) - const now = Date.now() + const key = this.getOverpayingStateKey(url, walletAddressId); + const state = this.state.get(tabId)?.get(key); + const now = Date.now(); if (state && state.expiresAtTimestamp > now) { return { waitTime: state.expiresAtTimestamp - now, - monetizationEvent: state.monetizationEvent - } + monetizationEvent: state.monetizationEvent, + }; } return { - waitTime: 0 - } + waitTime: 0, + }; } saveOverpaying( tabId: TabId, url: string, - details: SaveOverpayingDetails + details: SaveOverpayingDetails, ): void { - const { intervalInMs, walletAddressId, monetizationEvent } = details - if (!intervalInMs) return + const { intervalInMs, walletAddressId, monetizationEvent } = details; + if (!intervalInMs) return; - const now = Date.now() - const expiresAtTimestamp = now + intervalInMs + const now = Date.now(); + const expiresAtTimestamp = now + intervalInMs; - const key = this.getOverpayingStateKey(url, walletAddressId) - const state = this.state.get(tabId)?.get(key) + const key = this.getOverpayingStateKey(url, walletAddressId); + const state = this.state.get(tabId)?.get(key); if (!state) { - const tabState = this.state.get(tabId) || new Map() + const tabState = this.state.get(tabId) || new Map(); tabState.set(key, { monetizationEvent, expiresAtTimestamp: expiresAtTimestamp, - lastPaymentTimestamp: now - }) - this.state.set(tabId, tabState) + lastPaymentTimestamp: now, + }); + this.state.set(tabId, tabState); } else { - state.expiresAtTimestamp = expiresAtTimestamp - state.lastPaymentTimestamp = now + state.expiresAtTimestamp = expiresAtTimestamp; + state.lastPaymentTimestamp = now; } } getSessions(tabId: TabId) { - let sessions = this.sessions.get(tabId) + let sessions = this.sessions.get(tabId); if (!sessions) { - sessions = new Map() - this.sessions.set(tabId, sessions) + sessions = new Map(); + this.sessions.set(tabId, sessions); } - return sessions + return sessions; } getEnabledSessions(tabId: TabId) { - return [...this.getSessions(tabId).values()].filter((s) => !s.disabled) + return [...this.getSessions(tabId).values()].filter((s) => !s.disabled); } getPayableSessions(tabId: TabId) { - return this.getEnabledSessions(tabId).filter((s) => !s.invalid) + return this.getEnabledSessions(tabId).filter((s) => !s.invalid); } isTabMonetized(tabId: TabId) { - return this.getEnabledSessions(tabId).length > 0 + return this.getEnabledSessions(tabId).length > 0; } tabHasAllSessionsInvalid(tabId: TabId) { - const sessions = this.getEnabledSessions(tabId) - return sessions.length > 0 && sessions.every((s) => s.invalid) + const sessions = this.getEnabledSessions(tabId); + return sessions.length > 0 && sessions.every((s) => s.invalid); } getAllSessions() { - return [...this.sessions.values()].flatMap((s) => [...s.values()]) + return [...this.sessions.values()].flatMap((s) => [...s.values()]); } getAllTabs(): TabId[] { - return [...this.sessions.keys()] + return [...this.sessions.keys()]; } clearOverpayingByTabId(tabId: TabId) { - this.state.delete(tabId) - this.logger.debug(`Cleared overpaying state for tab ${tabId}.`) + this.state.delete(tabId); + this.logger.debug(`Cleared overpaying state for tab ${tabId}.`); } clearSessionsByTabId(tabId: TabId) { - const sessions = this.getSessions(tabId) - if (!sessions.size) return + const sessions = this.getSessions(tabId); + if (!sessions.size) return; for (const session of sessions.values()) { - session.stop() + session.stop(); } - this.logger.debug(`Cleared ${sessions.size} sessions for tab ${tabId}.`) - this.sessions.delete(tabId) + this.logger.debug(`Cleared ${sessions.size} sessions for tab ${tabId}.`); + this.sessions.delete(tabId); } } diff --git a/src/background/utils.test.ts b/src/background/utils.test.ts index ffaba9c7..3ddd6ca1 100644 --- a/src/background/utils.test.ts +++ b/src/background/utils.test.ts @@ -1,19 +1,19 @@ -import { getNextSendableAmount } from './utils' +import { getNextSendableAmount } from './utils'; // same as BuiltinIterator.take(n) function take(iter: IterableIterator, n: number) { - const result: T[] = [] + const result: T[] = []; for (let i = 0; i < n; i++) { - const item = iter.next() - if (item.done) break - result.push(item.value) + const item = iter.next(); + if (item.done) break; + result.push(item.value); } - return result + return result; } describe('getNextSendableAmount', () => { it('from assetScale 8 to 9', () => { - const min = 990_00_000n / 3600n // 0.99XPR per hour == 0.000275 XRP per second (27500 at scale 8) + const min = 990_00_000n / 3600n; // 0.99XPR per hour == 0.000275 XRP per second (27500 at scale 8) expect(take(getNextSendableAmount(8, 9, min), 8)).toEqual([ '27500', '27501', @@ -22,12 +22,12 @@ describe('getNextSendableAmount', () => { '27508', '27515', '27527', - '27547' - ]) - }) + '27547', + ]); + }); it('from assetScale 8 to 2', () => { - const min = 990_00_000n / 3600n + const min = 990_00_000n / 3600n; expect(take(getNextSendableAmount(8, 2, min), 8)).toEqual([ '27500', '1027500', @@ -36,9 +36,9 @@ describe('getNextSendableAmount', () => { '8027500', '15027500', '27027500', - '47027500' - ]) - }) + '47027500', + ]); + }); it('from assetScale 3 to 2', () => { expect(take(getNextSendableAmount(3, 2), 8)).toEqual([ @@ -49,9 +49,9 @@ describe('getNextSendableAmount', () => { '150', '270', '470', - '800' - ]) - }) + '800', + ]); + }); it('from assetScale 2 to 3', () => { expect(take(getNextSendableAmount(2, 3), 8)).toEqual([ @@ -62,9 +62,9 @@ describe('getNextSendableAmount', () => { '15', '27', '47', - '80' - ]) - }) + '80', + ]); + }); it('from assetScale 2 to 2', () => { expect(take(getNextSendableAmount(2, 2), 8)).toEqual([ @@ -75,7 +75,7 @@ describe('getNextSendableAmount', () => { '15', '27', '47', - '80' - ]) - }) -}) + '80', + ]); + }); +}); diff --git a/src/background/utils.ts b/src/background/utils.ts index 1a378abf..34b82a25 100644 --- a/src/background/utils.ts +++ b/src/background/utils.ts @@ -1,107 +1,107 @@ -import type { AmountValue, GrantDetails, WalletAmount } from '@/shared/types' -import type { Browser, Runtime, Tabs } from 'webextension-polyfill' -import { DEFAULT_SCALE, EXCHANGE_RATES_URL } from './config' -import { notNullOrUndef } from '@/shared/helpers' +import type { AmountValue, GrantDetails, WalletAmount } from '@/shared/types'; +import type { Browser, Runtime, Tabs } from 'webextension-polyfill'; +import { DEFAULT_SCALE, EXCHANGE_RATES_URL } from './config'; +import { notNullOrUndef } from '@/shared/helpers'; export const getCurrentActiveTab = async (browser: Browser) => { - const window = await browser.windows.getLastFocused() + const window = await browser.windows.getLastFocused(); const activeTabs = await browser.tabs.query({ active: true, - windowId: window.id - }) - return activeTabs[0] -} + windowId: window.id, + }); + return activeTabs[0]; +}; interface ToAmountParams { - value: string - recurring: boolean - assetScale: number + value: string; + recurring: boolean; + assetScale: number; } export const toAmount = ({ value, recurring, - assetScale + assetScale, }: ToAmountParams): WalletAmount => { - const interval = `R/${new Date().toISOString()}/P1M` + const interval = `R/${new Date().toISOString()}/P1M`; return { value: Math.floor(parseFloat(value) * 10 ** assetScale).toString(), - ...(recurring ? { interval } : {}) - } -} + ...(recurring ? { interval } : {}), + }; +}; export const OPEN_PAYMENTS_ERRORS: Record = { 'invalid client': - 'Please make sure that you uploaded the public key for your desired wallet address.' -} + 'Please make sure that you uploaded the public key for your desired wallet address.', +}; export interface GetRateOfPayParams { - rate: string - exchangeRate: number - assetScale: number + rate: string; + exchangeRate: number; + assetScale: number; } export const getRateOfPay = ({ rate, exchangeRate, - assetScale + assetScale, }: GetRateOfPayParams) => { - const scaleDiff = assetScale - DEFAULT_SCALE + const scaleDiff = assetScale - DEFAULT_SCALE; if (exchangeRate < 0.8 || exchangeRate > 1.5) { - const scaledExchangeRate = (1 / exchangeRate) * 10 ** scaleDiff - return BigInt(Math.round(Number(rate) * scaledExchangeRate)).toString() + const scaledExchangeRate = (1 / exchangeRate) * 10 ** scaleDiff; + return BigInt(Math.round(Number(rate) * scaledExchangeRate)).toString(); } - return (Number(rate) * 10 ** scaleDiff).toString() -} + return (Number(rate) * 10 ** scaleDiff).toString(); +}; interface ExchangeRates { - base: string - rates: Record + base: string; + rates: Record; } export const getExchangeRates = async (): Promise => { - const response = await fetch(EXCHANGE_RATES_URL) + const response = await fetch(EXCHANGE_RATES_URL); if (!response.ok) { throw new Error( - `Could not fetch exchange rates. [Status code: ${response.status}]` - ) + `Could not fetch exchange rates. [Status code: ${response.status}]`, + ); } - const rates = await response.json() + const rates = await response.json(); if (!rates.base || !rates.rates) { - throw new Error('Invalid rates format') + throw new Error('Invalid rates format'); } - return rates -} + return rates; +}; export const getTabId = (sender: Runtime.MessageSender): number => { - return notNullOrUndef(notNullOrUndef(sender.tab, 'sender.tab').id, 'tab.id') -} + return notNullOrUndef(notNullOrUndef(sender.tab, 'sender.tab').id, 'tab.id'); +}; export const getTab = (sender: Runtime.MessageSender): Tabs.Tab => { - return notNullOrUndef(notNullOrUndef(sender.tab, 'sender.tab'), 'tab') -} + return notNullOrUndef(notNullOrUndef(sender.tab, 'sender.tab'), 'tab'); +}; export const getSender = (sender: Runtime.MessageSender) => { - const tabId = getTabId(sender) - const frameId = notNullOrUndef(sender.frameId, 'sender.frameId') + const tabId = getTabId(sender); + const frameId = notNullOrUndef(sender.frameId, 'sender.frameId'); - return { tabId, frameId, url: sender.url } -} + return { tabId, frameId, url: sender.url }; +}; export const computeRate = (rate: string, sessionsCount: number): AmountValue => - (BigInt(rate) / BigInt(sessionsCount)).toString() + (BigInt(rate) / BigInt(sessionsCount)).toString(); export function computeBalance( grant?: GrantDetails | null, - grantSpentAmount?: AmountValue | null + grantSpentAmount?: AmountValue | null, ) { - if (!grant?.amount) return 0n - const total = BigInt(grant.amount.value) - return grantSpentAmount ? total - BigInt(grantSpentAmount) : total + if (!grant?.amount) return 0n; + const total = BigInt(grant.amount.value); + return grantSpentAmount ? total - BigInt(grantSpentAmount) : total; } // USD Scale 9 (connected wallet) @@ -110,24 +110,24 @@ export function computeBalance( export function* getNextSendableAmount( senderAssetScale: number, receiverAssetScale: number, - amount: bigint = 0n + amount: bigint = 0n, ): Generator { - const EXPONENTIAL_INCREASE = 0.5 + const EXPONENTIAL_INCREASE = 0.5; const scaleDiff = senderAssetScale < receiverAssetScale ? 0 - : senderAssetScale - receiverAssetScale - const base = 1n * 10n ** BigInt(scaleDiff) + : senderAssetScale - receiverAssetScale; + const base = 1n * 10n ** BigInt(scaleDiff); if (amount) { - yield amount.toString() + yield amount.toString(); } - let exp = 0 + let exp = 0; while (true) { - amount += base * BigInt(Math.floor(Math.exp(exp))) - yield amount.toString() - exp += EXPONENTIAL_INCREASE + amount += base * BigInt(Math.floor(Math.exp(exp))); + yield amount.toString(); + exp += EXPONENTIAL_INCREASE; } } diff --git a/src/content/container.ts b/src/content/container.ts index 069971f0..35969553 100644 --- a/src/content/container.ts +++ b/src/content/container.ts @@ -1,32 +1,32 @@ -import { asClass, asValue, createContainer, InjectionMode } from 'awilix' -import browser, { type Browser } from 'webextension-polyfill' -import { createLogger, Logger } from '@/shared/logger' -import { ContentScript } from './services/contentScript' -import { MonetizationTagManager } from './services/monetizationTagManager' -import { LOG_LEVEL } from '@/shared/defines' -import { FrameManager } from './services/frameManager' +import { asClass, asValue, createContainer, InjectionMode } from 'awilix'; +import browser, { type Browser } from 'webextension-polyfill'; +import { createLogger, Logger } from '@/shared/logger'; +import { ContentScript } from './services/contentScript'; +import { MonetizationTagManager } from './services/monetizationTagManager'; +import { LOG_LEVEL } from '@/shared/defines'; +import { FrameManager } from './services/frameManager'; import { type ContentToBackgroundMessage, - MessageManager -} from '@/shared/messages' + MessageManager, +} from '@/shared/messages'; export interface Cradle { - logger: Logger - browser: Browser - document: Document - window: Window - message: MessageManager - monetizationTagManager: MonetizationTagManager - frameManager: FrameManager - contentScript: ContentScript + logger: Logger; + browser: Browser; + document: Document; + window: Window; + message: MessageManager; + monetizationTagManager: MonetizationTagManager; + frameManager: FrameManager; + contentScript: ContentScript; } export const configureContainer = () => { const container = createContainer({ - injectionMode: InjectionMode.PROXY - }) + injectionMode: InjectionMode.PROXY, + }); - const logger = createLogger(LOG_LEVEL) + const logger = createLogger(LOG_LEVEL); container.register({ logger: asValue(logger), @@ -37,19 +37,19 @@ export const configureContainer = () => { frameManager: asClass(FrameManager) .singleton() .inject(() => ({ - logger: logger.getLogger('content-script:frameManager') + logger: logger.getLogger('content-script:frameManager'), })), monetizationTagManager: asClass(MonetizationTagManager) .singleton() .inject(() => ({ - logger: logger.getLogger('content-script:tagManager') + logger: logger.getLogger('content-script:tagManager'), })), contentScript: asClass(ContentScript) .singleton() .inject(() => ({ - logger: logger.getLogger('content-script:main') - })) - }) + logger: logger.getLogger('content-script:main'), + })), + }); - return container -} + return container; +}; diff --git a/src/content/debug.ts b/src/content/debug.ts index 9a50b4bf..5daca846 100644 --- a/src/content/debug.ts +++ b/src/content/debug.ts @@ -11,38 +11,38 @@ const listenForLinkChange = (mutationsList: MutationRecord[]) => { link.getAttribute('href')?.match(/^http/) && link.getAttribute('rel')?.match(/monetization/) ) { - acc.push(link) + acc.push(link); } - return acc + return acc; }, - [] - ) + [], + ); if (monetizationLinks.length) { - console.log(monetizationLinks) + console.log(monetizationLinks); } } if (mutationsList[0].type === 'attributes') { - const target = mutationsList[0].target as HTMLElement + const target = mutationsList[0].target as HTMLElement; if ( target.tagName?.toLowerCase() === 'link' && target.getAttribute('href')?.match(/^http/) && target.getAttribute('rel')?.match(/monetization/) ) { - console.log('LINK ATTR CHANGED', target) + console.log('LINK ATTR CHANGED', target); } } -} +}; export const loadObserver = () => { - const observer = new MutationObserver(listenForLinkChange) + const observer = new MutationObserver(listenForLinkChange); const observeOptions = { attributes: true, childList: true, - subtree: true - } + subtree: true, + }; - observer.observe(document, observeOptions) -} + observer.observe(document, observeOptions); +}; diff --git a/src/content/index.ts b/src/content/index.ts index ef66372c..60234077 100755 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -1,4 +1,4 @@ -import { configureContainer } from './container' +import { configureContainer } from './container'; -const container = configureContainer() -container.resolve('contentScript').start() +const container = configureContainer(); +container.resolve('contentScript').start(); diff --git a/src/content/messages.ts b/src/content/messages.ts index 1d745383..4eb30d26 100644 --- a/src/content/messages.ts +++ b/src/content/messages.ts @@ -5,5 +5,5 @@ export enum ContentToContentAction { IS_MONETIZATION_ALLOWED_ON_STOP = 'IS_MONETIZATION_ALLOWED_ON_STOP', START_MONETIZATION = 'START_MONETIZATION', STOP_MONETIZATION = 'STOP_MONETIZATION', - RESUME_MONETIZATION = 'RESUME_MONETIZATION' + RESUME_MONETIZATION = 'RESUME_MONETIZATION', } diff --git a/src/content/polyfill.ts b/src/content/polyfill.ts index 68ece957..062dae0d 100644 --- a/src/content/polyfill.ts +++ b/src/content/polyfill.ts @@ -1,125 +1,125 @@ -import type { MonetizationEventPayload } from '@/shared/messages' -;(function () { - const handlers = new WeakMap() +import type { MonetizationEventPayload } from '@/shared/messages'; +(function () { + const handlers = new WeakMap(); const attributes: PropertyDescriptor & ThisType = { enumerable: true, configurable: false, get() { - return handlers.get(this) || null + return handlers.get(this) || null; }, set(val) { - const listener = handlers.get(this) + const listener = handlers.get(this); if (listener && listener === val) { // nothing to do here ? - return + return; } const removeAnyExisting = () => { if (listener) { - this.removeEventListener('monetization', listener) + this.removeEventListener('monetization', listener); } - } + }; if (val == null /* OR undefined*/) { - handlers.delete(this) - removeAnyExisting() + handlers.delete(this); + removeAnyExisting(); } else if (typeof val === 'function') { - removeAnyExisting() - this.addEventListener('monetization', val) - handlers.set(this, val) + removeAnyExisting(); + this.addEventListener('monetization', val); + handlers.set(this, val); } else { - throw new Error('val must be a function, got ' + typeof val) + throw new Error('val must be a function, got ' + typeof val); } - } - } + }, + }; - const supportsOriginal = DOMTokenList.prototype.supports - const supportsMonetization = Symbol.for('link-supports-monetization') + const supportsOriginal = DOMTokenList.prototype.supports; + const supportsMonetization = Symbol.for('link-supports-monetization'); DOMTokenList.prototype.supports = function (token) { // @ts-expect-error: polyfilled if (this[supportsMonetization] && token === 'monetization') { - return true + return true; } else { - return supportsOriginal.call(this, token) + return supportsOriginal.call(this, token); } - } + }; const relList = Object.getOwnPropertyDescriptor( HTMLLinkElement.prototype, - 'relList' - )! - const relListGetOriginal = relList.get! + 'relList', + )!; + const relListGetOriginal = relList.get!; relList.get = function () { - const val = relListGetOriginal.call(this) - val[supportsMonetization] = true - return val - } + const val = relListGetOriginal.call(this); + val[supportsMonetization] = true; + return val; + }; - Object.defineProperty(HTMLLinkElement.prototype, 'relList', relList) - Object.defineProperty(HTMLElement.prototype, 'onmonetization', attributes) - Object.defineProperty(Window.prototype, 'onmonetization', attributes) - Object.defineProperty(Document.prototype, 'onmonetization', attributes) + Object.defineProperty(HTMLLinkElement.prototype, 'relList', relList); + Object.defineProperty(HTMLElement.prototype, 'onmonetization', attributes); + Object.defineProperty(Window.prototype, 'onmonetization', attributes); + Object.defineProperty(Document.prototype, 'onmonetization', attributes); - let eventDetailDeprecationEmitted = false + let eventDetailDeprecationEmitted = false; class MonetizationEvent extends Event { - public readonly amountSent: PaymentCurrencyAmount - public readonly incomingPayment: string - public readonly paymentPointer: string + public readonly amountSent: PaymentCurrencyAmount; + public readonly incomingPayment: string; + public readonly paymentPointer: string; constructor( type: 'monetization', - eventInitDict: MonetizationEventPayload['details'] + eventInitDict: MonetizationEventPayload['details'], ) { - super(type, { bubbles: true }) - const { amountSent, incomingPayment, paymentPointer } = eventInitDict - this.amountSent = amountSent - this.incomingPayment = incomingPayment - this.paymentPointer = paymentPointer + super(type, { bubbles: true }); + const { amountSent, incomingPayment, paymentPointer } = eventInitDict; + this.amountSent = amountSent; + this.incomingPayment = incomingPayment; + this.paymentPointer = paymentPointer; } get [Symbol.toStringTag]() { - return 'MonetizationEvent' + return 'MonetizationEvent'; } get detail() { if (!eventDetailDeprecationEmitted) { - const msg = `MonetizationEvent.detail is deprecated. Access attributes directly instead.` + const msg = `MonetizationEvent.detail is deprecated. Access attributes directly instead.`; // eslint-disable-next-line no-console - console.warn(msg) - eventDetailDeprecationEmitted = true + console.warn(msg); + eventDetailDeprecationEmitted = true; } - const { amountSent, incomingPayment, paymentPointer } = this - return { amountSent, incomingPayment, paymentPointer } + const { amountSent, incomingPayment, paymentPointer } = this; + return { amountSent, incomingPayment, paymentPointer }; } } // @ts-expect-error: we're defining this now - window.MonetizationEvent = MonetizationEvent + window.MonetizationEvent = MonetizationEvent; window.addEventListener( '__wm_ext_monetization', (event: CustomEvent) => { - if (!(event.target instanceof HTMLLinkElement)) return - if (!event.target.isConnected) return + if (!(event.target instanceof HTMLLinkElement)) return; + if (!event.target.isConnected) return; - const monetizationTag = event.target + const monetizationTag = event.target; monetizationTag.dispatchEvent( - new MonetizationEvent('monetization', event.detail) - ) + new MonetizationEvent('monetization', event.detail), + ); }, - { capture: true } - ) + { capture: true }, + ); window.addEventListener( '__wm_ext_onmonetization_attr_change', (event: CustomEvent<{ attribute?: string }>) => { - if (!event.target) return + if (!event.target) return; - const { attribute } = event.detail + const { attribute } = event.detail; // @ts-expect-error: we're defining this now event.target.onmonetization = attribute ? new Function(attribute).bind(event.target) - : null + : null; }, - { capture: true } - ) -})() + { capture: true }, + ); +})(); diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index de615fb2..84a36d94 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -1,46 +1,46 @@ -import type { ToContentMessage } from '@/shared/messages' -import { failure } from '@/shared/helpers' -import type { Cradle } from '@/content/container' +import type { ToContentMessage } from '@/shared/messages'; +import { failure } from '@/shared/helpers'; +import type { Cradle } from '@/content/container'; export class ContentScript { - private browser: Cradle['browser'] - private window: Cradle['window'] - private logger: Cradle['logger'] - private monetizationTagManager: Cradle['monetizationTagManager'] - private frameManager: Cradle['frameManager'] + private browser: Cradle['browser']; + private window: Cradle['window']; + private logger: Cradle['logger']; + private monetizationTagManager: Cradle['monetizationTagManager']; + private frameManager: Cradle['frameManager']; - private isFirstLevelFrame: boolean - private isTopFrame: boolean + private isFirstLevelFrame: boolean; + private isTopFrame: boolean; constructor({ browser, window, logger, monetizationTagManager, - frameManager + frameManager, }: Cradle) { Object.assign(this, { browser, window, logger, monetizationTagManager, - frameManager - }) + frameManager, + }); - this.isTopFrame = window === window.top - this.isFirstLevelFrame = window.parent === window.top + this.isTopFrame = window === window.top; + this.isFirstLevelFrame = window.parent === window.top; - this.bindMessageHandler() + this.bindMessageHandler(); } async start() { - await this.injectPolyfill() + await this.injectPolyfill(); if (this.isFirstLevelFrame) { - this.logger.info('Content script started') + this.logger.info('Content script started'); - if (this.isTopFrame) this.frameManager.start() + if (this.isTopFrame) this.frameManager.start(); - this.monetizationTagManager.start() + this.monetizationTagManager.start(); } } @@ -51,36 +51,36 @@ export class ContentScript { switch (message.action) { case 'MONETIZATION_EVENT': this.monetizationTagManager.dispatchMonetizationEvent( - message.payload - ) - return + message.payload, + ); + return; case 'EMIT_TOGGLE_WM': - this.monetizationTagManager.toggleWM(message.payload) + this.monetizationTagManager.toggleWM(message.payload); - return + return; default: - return + return; } } catch (e) { - this.logger.error(message.action, e.message) - return failure(e.message) + this.logger.error(message.action, e.message); + return failure(e.message); } - } - ) + }, + ); } // TODO: When Firefox has good support for `world: MAIN`, inject this directly // via manifest.json https://bugzilla.mozilla.org/show_bug.cgi?id=1736575 async injectPolyfill() { - const document = this.window.document - const script = document.createElement('script') - script.src = this.browser.runtime.getURL('polyfill/polyfill.js') + const document = this.window.document; + const script = document.createElement('script'); + script.src = this.browser.runtime.getURL('polyfill/polyfill.js'); await new Promise((resolve) => { - script.addEventListener('load', () => resolve(), { once: true }) - document.documentElement.appendChild(script) - }) - script.remove() + script.addEventListener('load', () => resolve(), { once: true }); + document.documentElement.appendChild(script); + }); + script.remove(); } } diff --git a/src/content/services/frameManager.ts b/src/content/services/frameManager.ts index d9870370..61b0552e 100644 --- a/src/content/services/frameManager.ts +++ b/src/content/services/frameManager.ts @@ -1,92 +1,92 @@ -import { ContentToContentAction } from '../messages' +import { ContentToContentAction } from '../messages'; import type { ResumeMonetizationPayload, StartMonetizationPayload, - StopMonetizationPayload -} from '@/shared/messages' -import type { Cradle } from '@/content/container' + StopMonetizationPayload, +} from '@/shared/messages'; +import type { Cradle } from '@/content/container'; export class FrameManager { - private window: Cradle['window'] - private document: Cradle['document'] - private logger: Cradle['logger'] - private message: Cradle['message'] + private window: Cradle['window']; + private document: Cradle['document']; + private logger: Cradle['logger']; + private message: Cradle['message']; - private documentObserver: MutationObserver - private frameAllowAttrObserver: MutationObserver + private documentObserver: MutationObserver; + private frameAllowAttrObserver: MutationObserver; private frames = new Map< HTMLIFrameElement, { frameId: string | null; requestIds: string[] } - >() + >(); constructor({ window, document, logger, message }: Cradle) { Object.assign(this, { window, document, logger, - message - }) + message, + }); this.documentObserver = new MutationObserver((records) => - this.onWholeDocumentObserved(records) - ) + this.onWholeDocumentObserved(records), + ); this.frameAllowAttrObserver = new MutationObserver((records) => - this.onFrameAllowAttrChange(records) - ) + this.onFrameAllowAttrChange(records), + ); } private findIframe(sourceWindow: Window): HTMLIFrameElement | null { - const iframes = this.frames.keys() - let frame + const iframes = this.frames.keys(); + let frame; do { - frame = iframes.next() - if (frame.done) return null - if (frame.value.contentWindow === sourceWindow) return frame.value - } while (!frame.done) + frame = iframes.next(); + if (frame.done) return null; + if (frame.value.contentWindow === sourceWindow) return frame.value; + } while (!frame.done); - return null + return null; } private observeDocumentForFrames() { this.documentObserver.observe(this.document, { subtree: true, - childList: true - }) + childList: true, + }); } private observeFrameAllowAttrs(frame: HTMLIFrameElement) { this.frameAllowAttrObserver.observe(frame, { childList: false, attributeOldValue: true, - attributeFilter: ['allow'] - }) + attributeFilter: ['allow'], + }); } async onFrameAllowAttrChange(records: MutationRecord[]) { - const handledTags = new Set() + const handledTags = new Set(); // Check for a non specified link with the type now specified and // just treat it as a newly seen, monetization tag for (const record of records) { - const target = record.target as HTMLIFrameElement + const target = record.target as HTMLIFrameElement; if (handledTags.has(target)) { - continue + continue; } - const hasTarget = this.frames.has(target) + const hasTarget = this.frames.has(target); const typeSpecified = - target instanceof HTMLIFrameElement && target.allow === 'monetization' + target instanceof HTMLIFrameElement && target.allow === 'monetization'; if (!hasTarget && typeSpecified) { - await this.onAddedFrame(target) - handledTags.add(target) + await this.onAddedFrame(target); + handledTags.add(target); } else if (hasTarget && !typeSpecified) { - this.onRemovedFrame(target) - handledTags.add(target) + this.onRemovedFrame(target); + handledTags.add(target); } else if (!hasTarget && !typeSpecified) { // ignore these changes - handledTags.add(target) + handledTags.add(target); } } } @@ -94,37 +94,37 @@ export class FrameManager { private async onAddedFrame(frame: HTMLIFrameElement) { this.frames.set(frame, { frameId: null, - requestIds: [] - }) + requestIds: [], + }); } private async onRemovedFrame(frame: HTMLIFrameElement) { - this.logger.info('onRemovedFrame', frame) + this.logger.info('onRemovedFrame', frame); - const frameDetails = this.frames.get(frame) + const frameDetails = this.frames.get(frame); const stopMonetizationTags: StopMonetizationPayload[] = frameDetails?.requestIds.map((requestId) => ({ requestId, - intent: 'remove' - })) || [] + intent: 'remove', + })) || []; if (stopMonetizationTags.length) { - this.message.send('STOP_MONETIZATION', stopMonetizationTags) + this.message.send('STOP_MONETIZATION', stopMonetizationTags); } - this.frames.delete(frame) + this.frames.delete(frame); } private onWholeDocumentObserved(records: MutationRecord[]) { for (const record of records) { if (record.type === 'childList') { - record.removedNodes.forEach((node) => this.check('removed', node)) + record.removedNodes.forEach((node) => this.check('removed', node)); } } for (const record of records) { if (record.type === 'childList') { - record.addedNodes.forEach((node) => this.check('added', node)) + record.addedNodes.forEach((node) => this.check('added', node)); } } } @@ -132,127 +132,127 @@ export class FrameManager { async check(op: string, node: Node) { if (node instanceof HTMLIFrameElement) { if (op === 'added') { - this.observeFrameAllowAttrs(node) - await this.onAddedFrame(node) + this.observeFrameAllowAttrs(node); + await this.onAddedFrame(node); } else if (op === 'removed' && this.frames.has(node)) { - this.onRemovedFrame(node) + this.onRemovedFrame(node); } } } start(): void { - this.bindMessageHandler() + this.bindMessageHandler(); if ( document.readyState === 'interactive' || document.readyState === 'complete' ) - this.run() + this.run(); document.addEventListener( 'readystatechange', () => { if (document.readyState === 'interactive') { - this.run() + this.run(); } }, - { once: true } - ) + { once: true }, + ); } private run() { const frames: NodeListOf = - this.document.querySelectorAll('iframe') + this.document.querySelectorAll('iframe'); frames.forEach(async (frame) => { try { - this.observeFrameAllowAttrs(frame) - await this.onAddedFrame(frame) + this.observeFrameAllowAttrs(frame); + await this.onAddedFrame(frame); } catch (e) { - this.logger.error(e) + this.logger.error(e); } - }) + }); - this.observeDocumentForFrames() + this.observeDocumentForFrames(); } private bindMessageHandler() { this.window.addEventListener( 'message', (event: any) => { - const { message, payload, id } = event.data + const { message, payload, id } = event.data; if ( ![ ContentToContentAction.INITIALIZE_IFRAME, ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, - ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME + ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, ].includes(message) ) { - return + return; } - const frame = this.findIframe(event.source) + const frame = this.findIframe(event.source); if (!frame) { - event.stopPropagation() - return + event.stopPropagation(); + return; } - if (event.origin === this.window.location.href) return + if (event.origin === this.window.location.href) return; switch (message) { case ContentToContentAction.INITIALIZE_IFRAME: - event.stopPropagation() + event.stopPropagation(); this.frames.set(frame, { frameId: id, - requestIds: [] - }) - return + requestIds: [], + }); + return; case ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START: - event.stopPropagation() + event.stopPropagation(); if (frame.allow === 'monetization') { this.frames.set(frame, { frameId: id, requestIds: payload.map( - (p: StartMonetizationPayload) => p.requestId - ) - }) + (p: StartMonetizationPayload) => p.requestId, + ), + }); event.source.postMessage( { message: ContentToContentAction.START_MONETIZATION, id, - payload + payload, }, - '*' - ) + '*', + ); } - return + return; case ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME: - event.stopPropagation() + event.stopPropagation(); if (frame.allow === 'monetization') { this.frames.set(frame, { frameId: id, requestIds: payload.map( - (p: ResumeMonetizationPayload) => p.requestId - ) - }) + (p: ResumeMonetizationPayload) => p.requestId, + ), + }); event.source.postMessage( { message: ContentToContentAction.RESUME_MONETIZATION, id, - payload + payload, }, - '*' - ) + '*', + ); } - return + return; default: - return + return; } }, - { capture: true } - ) + { capture: true }, + ); } } diff --git a/src/content/services/monetizationTagManager.ts b/src/content/services/monetizationTagManager.ts index 7dd1cc6e..af50d29d 100644 --- a/src/content/services/monetizationTagManager.ts +++ b/src/content/services/monetizationTagManager.ts @@ -1,193 +1,193 @@ -import { EventEmitter } from 'events' -import { mozClone } from '../utils' -import type { MonetizationTagDetails } from '../types' -import type { WalletAddress } from '@interledger/open-payments/dist/types' -import { checkWalletAddressUrlFormat } from '../utils' +import { EventEmitter } from 'events'; +import { mozClone } from '../utils'; +import type { MonetizationTagDetails } from '../types'; +import type { WalletAddress } from '@interledger/open-payments/dist/types'; +import { checkWalletAddressUrlFormat } from '../utils'; import type { EmitToggleWMPayload, MonetizationEventPayload, ResumeMonetizationPayload, StartMonetizationPayload, - StopMonetizationPayload -} from '@/shared/messages' -import { ContentToContentAction } from '../messages' -import type { Cradle } from '@/content/container' + StopMonetizationPayload, +} from '@/shared/messages'; +import { ContentToContentAction } from '../messages'; +import type { Cradle } from '@/content/container'; -export type MonetizationTag = HTMLLinkElement +export type MonetizationTag = HTMLLinkElement; interface FireOnMonetizationChangeIfHaveAttributeParams { - node: HTMLElement - changeDetected?: boolean + node: HTMLElement; + changeDetected?: boolean; } export class MonetizationTagManager extends EventEmitter { - private window: Cradle['window'] - private document: Cradle['document'] - private logger: Cradle['logger'] - private message: Cradle['message'] - - private isTopFrame: boolean - private isFirstLevelFrame: boolean - private documentObserver: MutationObserver - private monetizationTagAttrObserver: MutationObserver - private id: string - private monetizationTags = new Map() + private window: Cradle['window']; + private document: Cradle['document']; + private logger: Cradle['logger']; + private message: Cradle['message']; + + private isTopFrame: boolean; + private isFirstLevelFrame: boolean; + private documentObserver: MutationObserver; + private monetizationTagAttrObserver: MutationObserver; + private id: string; + private monetizationTags = new Map(); constructor({ window, document, logger, message }: Cradle) { - super() + super(); Object.assign(this, { window, document, logger, - message - }) + message, + }); this.documentObserver = new MutationObserver((records) => - this.onWholeDocumentObserved(records) - ) + this.onWholeDocumentObserved(records), + ); this.monetizationTagAttrObserver = new MutationObserver((records) => - this.onMonetizationTagAttrsChange(records) - ) + this.onMonetizationTagAttrsChange(records), + ); document.addEventListener('visibilitychange', async () => { if (document.visibilityState === 'visible') { - await this.resumeAllMonetization() + await this.resumeAllMonetization(); } else { - this.stopAllMonetization() + this.stopAllMonetization(); } - }) + }); - this.isTopFrame = window === window.top - this.isFirstLevelFrame = window.parent === window.top - this.id = crypto.randomUUID() + this.isTopFrame = window === window.top; + this.isFirstLevelFrame = window.parent === window.top; + this.id = crypto.randomUUID(); if (!this.isTopFrame && this.isFirstLevelFrame) { - this.bindMessageHandler() + this.bindMessageHandler(); } } private dispatchLoadEvent(tag: MonetizationTag) { - tag.dispatchEvent(new Event('load')) + tag.dispatchEvent(new Event('load')); } private dispatchErrorEvent(tag: MonetizationTag) { - tag.dispatchEvent(new Event('error')) + tag.dispatchEvent(new Event('error')); } dispatchMonetizationEvent({ requestId, details }: MonetizationEventPayload) { this.monetizationTags.forEach((tagDetails, tag) => { - if (tagDetails.requestId !== requestId) return + if (tagDetails.requestId !== requestId) return; tag.dispatchEvent( new CustomEvent('__wm_ext_monetization', { detail: mozClone(details, this.document), - bubbles: true - }) - ) - }) - return + bubbles: true, + }), + ); + }); + return; } private async resumeAllMonetization() { - const response = await this.message.send('IS_WM_ENABLED') + const response = await this.message.send('IS_WM_ENABLED'); if (response.success && response.payload) { - const resumeMonetizationTags: ResumeMonetizationPayload[] = [] + const resumeMonetizationTags: ResumeMonetizationPayload[] = []; this.monetizationTags.forEach((value) => { if (value.requestId && value.walletAddress) { - resumeMonetizationTags.push({ requestId: value.requestId }) + resumeMonetizationTags.push({ requestId: value.requestId }); } - }) + }); - this.sendResumeMonetization(resumeMonetizationTags) + this.sendResumeMonetization(resumeMonetizationTags); } } private stopAllMonetization(intent?: StopMonetizationPayload['intent']) { - const stopMonetizationTags: StopMonetizationPayload[] = [] + const stopMonetizationTags: StopMonetizationPayload[] = []; this.monetizationTags.forEach((value) => { if (value.requestId && value.walletAddress) { - stopMonetizationTags.push({ requestId: value.requestId, intent }) + stopMonetizationTags.push({ requestId: value.requestId, intent }); } - }) + }); - this.sendStopMonetization(stopMonetizationTags) + this.sendStopMonetization(stopMonetizationTags); } private async onWholeDocumentObserved(records: MutationRecord[]) { const startMonetizationTagsPromises: Promise[] = - [] - const stopMonetizationTags: StopMonetizationPayload[] = [] + []; + const stopMonetizationTags: StopMonetizationPayload[] = []; for (const record of records) { if (record.type === 'childList') { record.removedNodes.forEach(async (node) => { - const stopMonetizationTag = this.checkRemoved(node) + const stopMonetizationTag = this.checkRemoved(node); if (stopMonetizationTag) - stopMonetizationTags.push(stopMonetizationTag) - }) + stopMonetizationTags.push(stopMonetizationTag); + }); } } - await this.sendStopMonetization(stopMonetizationTags) + await this.sendStopMonetization(stopMonetizationTags); if (this.isTopFrame) { for (const record of records) { if (record.type === 'childList') { record.addedNodes.forEach(async (node) => { - const startMonetizationTag = this.checkAdded(node) - startMonetizationTagsPromises.push(startMonetizationTag) - }) + const startMonetizationTag = this.checkAdded(node); + startMonetizationTagsPromises.push(startMonetizationTag); + }); } } Promise.allSettled(startMonetizationTagsPromises).then((result) => { - const startMonetizationTags: StartMonetizationPayload[] = [] + const startMonetizationTags: StartMonetizationPayload[] = []; result.forEach((res) => { if (res.status === 'fulfilled' && res.value) { - startMonetizationTags.push(res.value) + startMonetizationTags.push(res.value); } - }) + }); - this.sendStartMonetization(startMonetizationTags) - }) + this.sendStartMonetization(startMonetizationTags); + }); } - this.onOnMonetizationChangeObserved(records) + this.onOnMonetizationChangeObserved(records); } async onMonetizationTagAttrsChange(records: MutationRecord[]) { - const handledTags = new Set() - const startMonetizationTags: StartMonetizationPayload[] = [] - const stopMonetizationTags: StopMonetizationPayload[] = [] + const handledTags = new Set(); + const startMonetizationTags: StartMonetizationPayload[] = []; + const stopMonetizationTags: StopMonetizationPayload[] = []; // Check for a non specified link with the type now specified and // just treat it as a newly seen, monetization tag for (const record of records) { - const target = record.target as MonetizationTag + const target = record.target as MonetizationTag; if (handledTags.has(target)) { - continue + continue; } - const hasTarget = this.monetizationTags.has(target) + const hasTarget = this.monetizationTags.has(target); const typeSpecified = - target instanceof HTMLLinkElement && target.rel === 'monetization' + target instanceof HTMLLinkElement && target.rel === 'monetization'; // this will also handle the case of a @disabled tag that // is not tracked, becoming enabled if (!hasTarget && typeSpecified) { - const startMonetizationTag = await this.onAddedTag(target) + const startMonetizationTag = await this.onAddedTag(target); if (startMonetizationTag) - startMonetizationTags.push(startMonetizationTag) + startMonetizationTags.push(startMonetizationTag); - handledTags.add(target) + handledTags.add(target); } else if (hasTarget && !typeSpecified) { - const stopMonetizationTag = this.onRemovedTag(target) - stopMonetizationTags.push(stopMonetizationTag) + const stopMonetizationTag = this.onRemovedTag(target); + stopMonetizationTags.push(stopMonetizationTag); - handledTags.add(target) + handledTags.add(target); } else if (!hasTarget && !typeSpecified) { // ignore these changes - handledTags.add(target) + handledTags.add(target); } else if (hasTarget && typeSpecified) { if ( record.type === 'attributes' && @@ -195,27 +195,27 @@ export class MonetizationTagManager extends EventEmitter { target instanceof HTMLLinkElement && target.getAttribute('disabled') !== record.oldValue ) { - const wasDisabled = record.oldValue !== null - const isDisabled = target.hasAttribute('disabled') + const wasDisabled = record.oldValue !== null; + const isDisabled = target.hasAttribute('disabled'); if (wasDisabled != isDisabled) { try { const { requestId, walletAddress } = this.getTagDetails( target, - 'onChangeDisabled' - ) + 'onChangeDisabled', + ); if (isDisabled) { - stopMonetizationTags.push({ requestId, intent: 'disable' }) + stopMonetizationTags.push({ requestId, intent: 'disable' }); } else if (walletAddress) { - startMonetizationTags.push({ requestId, walletAddress }) + startMonetizationTags.push({ requestId, walletAddress }); } } catch { - const startMonetizationPayload = await this.onAddedTag(target) + const startMonetizationPayload = await this.onAddedTag(target); if (startMonetizationPayload) { - startMonetizationTags.push(startMonetizationPayload) + startMonetizationTags.push(startMonetizationPayload); } } - handledTags.add(target) + handledTags.add(target); } } else if ( record.type === 'attributes' && @@ -224,75 +224,75 @@ export class MonetizationTagManager extends EventEmitter { target.href !== record.oldValue ) { const { startMonetizationTag, stopMonetizationTag } = - await this.onChangedWalletAddressUrl(target) + await this.onChangedWalletAddressUrl(target); if (startMonetizationTag) - startMonetizationTags.push(startMonetizationTag) + startMonetizationTags.push(startMonetizationTag); if (stopMonetizationTag) - stopMonetizationTags.push(stopMonetizationTag) + stopMonetizationTags.push(stopMonetizationTag); - handledTags.add(target) + handledTags.add(target); } } } - await this.sendStopMonetization(stopMonetizationTags) - this.sendStartMonetization(startMonetizationTags) + await this.sendStopMonetization(stopMonetizationTags); + this.sendStartMonetization(startMonetizationTags); } private async checkAdded(node: Node) { if (node instanceof HTMLElement) { - this.fireOnMonetizationAttrChangedEvent({ node }) + this.fireOnMonetizationAttrChangedEvent({ node }); } if (node instanceof HTMLLinkElement) { - this.observeMonetizationTagAttrs(node) - return await this.onAddedTag(node) + this.observeMonetizationTagAttrs(node); + return await this.onAddedTag(node); } - return null + return null; } private checkRemoved(node: Node) { return node instanceof HTMLLinkElement && this.monetizationTags.has(node) ? this.onRemovedTag(node) - : null + : null; } private observeMonetizationTagAttrs(tag: MonetizationTag) { this.monetizationTagAttrObserver.observe(tag, { childList: false, attributeOldValue: true, - attributeFilter: ['href', 'disabled', 'rel', 'crossorigin', 'type'] - }) + attributeFilter: ['href', 'disabled', 'rel', 'crossorigin', 'type'], + }); } private getTagDetails(tag: MonetizationTag, caller = '') { - const tagDetails = this.monetizationTags.get(tag) + const tagDetails = this.monetizationTags.get(tag); if (!tagDetails) { throw new Error( - `${caller}: tag not tracked: ${tag.outerHTML.slice(0, 200)}` - ) + `${caller}: tag not tracked: ${tag.outerHTML.slice(0, 200)}`, + ); } - return tagDetails + return tagDetails; } // If wallet address changed, remove old tag and add new one async onChangedWalletAddressUrl( tag: MonetizationTag, wasDisabled = false, - isDisabled = false + isDisabled = false, ) { - let stopMonetizationTag = null + let stopMonetizationTag = null; if (!wasDisabled && !isDisabled) { - stopMonetizationTag = this.onRemovedTag(tag) + stopMonetizationTag = this.onRemovedTag(tag); } - const startMonetizationTag = await this.onAddedTag(tag) + const startMonetizationTag = await this.onAddedTag(tag); - return { startMonetizationTag, stopMonetizationTag } + return { startMonetizationTag, stopMonetizationTag }; } private onOnMonetizationChangeObserved(records: MutationRecord[]) { @@ -304,26 +304,26 @@ export class MonetizationTagManager extends EventEmitter { ) { this.fireOnMonetizationAttrChangedEvent({ node: record.target, - changeDetected: true - }) + changeDetected: true, + }); } } } private fireOnMonetizationAttrChangedEvent({ node, - changeDetected = false + changeDetected = false, }: FireOnMonetizationChangeIfHaveAttributeParams) { - const attribute = node.getAttribute('onmonetization') + const attribute = node.getAttribute('onmonetization'); - if (!attribute && !changeDetected) return + if (!attribute && !changeDetected) return; const customEvent = new CustomEvent('__wm_ext_onmonetization_attr_change', { bubbles: true, - detail: mozClone({ attribute }, this.document) - }) + detail: mozClone({ attribute }, this.document), + }); - node.dispatchEvent(customEvent) + node.dispatchEvent(customEvent); } private isDocumentReady() { @@ -331,34 +331,34 @@ export class MonetizationTagManager extends EventEmitter { (document.readyState === 'interactive' || document.readyState === 'complete') && document.visibilityState === 'visible' - ) + ); } start(): void { if (this.isDocumentReady()) { - this.run() - return + this.run(); + return; } document.addEventListener( 'readystatechange', () => { if (this.isDocumentReady()) { - this.run() + this.run(); } else { document.addEventListener( 'visibilitychange', () => { if (this.isDocumentReady()) { - this.run() + this.run(); } }, - { once: true } - ) + { once: true }, + ); } }, - { once: true } - ) + { once: true }, + ); } private run() { @@ -366,201 +366,201 @@ export class MonetizationTagManager extends EventEmitter { this.window.parent.postMessage( { message: ContentToContentAction.INITIALIZE_IFRAME, - id: this.id + id: this.id, }, - '*' - ) + '*', + ); } - let monetizationTags: NodeListOf | MonetizationTag[] + let monetizationTags: NodeListOf | MonetizationTag[]; if (this.isTopFrame) { monetizationTags = this.document.querySelectorAll( - 'link[rel="monetization"]' - ) + 'link[rel="monetization"]', + ); } else { const monetizationTag: MonetizationTag | null = - this.document.querySelector('head link[rel="monetization"]') - monetizationTags = monetizationTag ? [monetizationTag] : [] + this.document.querySelector('head link[rel="monetization"]'); + monetizationTags = monetizationTag ? [monetizationTag] : []; } const startMonetizationTagsPromises: Promise[] = - [] + []; monetizationTags.forEach(async (tag) => { try { - this.observeMonetizationTagAttrs(tag) - const startMonetizationTag = this.onAddedTag(tag) - startMonetizationTagsPromises.push(startMonetizationTag) + this.observeMonetizationTagAttrs(tag); + const startMonetizationTag = this.onAddedTag(tag); + startMonetizationTagsPromises.push(startMonetizationTag); } catch (e) { - this.logger.error(e) + this.logger.error(e); } - }) + }); Promise.allSettled(startMonetizationTagsPromises).then((result) => { - const startMonetizationTags: StartMonetizationPayload[] = [] + const startMonetizationTags: StartMonetizationPayload[] = []; result.forEach((res) => { if (res.status === 'fulfilled' && res.value) { - startMonetizationTags.push(res.value) + startMonetizationTags.push(res.value); } - }) + }); - this.sendStartMonetization(startMonetizationTags) - }) + this.sendStartMonetization(startMonetizationTags); + }); const onMonetizations: NodeListOf = - this.document.querySelectorAll('[onmonetization]') + this.document.querySelectorAll('[onmonetization]'); onMonetizations.forEach((node) => { - this.fireOnMonetizationAttrChangedEvent({ node }) - }) + this.fireOnMonetizationAttrChangedEvent({ node }); + }); this.documentObserver.observe(this.document, { subtree: true, childList: true, - attributeFilter: ['onmonetization'] - }) + attributeFilter: ['onmonetization'], + }); } stop() { - this.documentObserver.disconnect() - this.monetizationTagAttrObserver.disconnect() - this.monetizationTags.clear() + this.documentObserver.disconnect(); + this.monetizationTagAttrObserver.disconnect(); + this.monetizationTags.clear(); } // Remove tag from list & stop monetization private onRemovedTag(tag: MonetizationTag): StopMonetizationPayload { - const { requestId } = this.getTagDetails(tag, 'onRemovedTag') - this.monetizationTags.delete(tag) + const { requestId } = this.getTagDetails(tag, 'onRemovedTag'); + this.monetizationTags.delete(tag); - return { requestId, intent: 'remove' } + return { requestId, intent: 'remove' }; } // Add tag to list & start monetization private async onAddedTag( tag: MonetizationTag, - crtRequestId?: string + crtRequestId?: string, ): Promise { - const walletAddress = await this.checkTag(tag) - if (!walletAddress) return null + const walletAddress = await this.checkTag(tag); + if (!walletAddress) return null; - const requestId = crtRequestId ?? crypto.randomUUID() + const requestId = crtRequestId ?? crypto.randomUUID(); const details: MonetizationTagDetails = { walletAddress, - requestId - } + requestId, + }; - this.monetizationTags.set(tag, details) - return { walletAddress, requestId } + this.monetizationTags.set(tag, details); + return { walletAddress, requestId }; } private sendStartMonetization(tags: StartMonetizationPayload[]) { - if (!tags.length) return + if (!tags.length) return; if (this.isTopFrame) { if (tags.length) { - void this.message.send('START_MONETIZATION', tags) + void this.message.send('START_MONETIZATION', tags); } } else if (this.isFirstLevelFrame) { this.window.parent.postMessage( { message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, id: this.id, - payload: tags + payload: tags, }, - '*' - ) + '*', + ); } } private async sendStopMonetization(tags: StopMonetizationPayload[]) { - if (!tags.length) return - await this.message.send('STOP_MONETIZATION', tags) + if (!tags.length) return; + await this.message.send('STOP_MONETIZATION', tags); } private sendResumeMonetization(tags: ResumeMonetizationPayload[]) { if (this.isTopFrame) { if (tags.length) { - void this.message.send('RESUME_MONETIZATION', tags) + void this.message.send('RESUME_MONETIZATION', tags); } } else if (this.isFirstLevelFrame) { this.window.parent.postMessage( { message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, id: this.id, - payload: tags + payload: tags, }, - '*' - ) + '*', + ); } } // Check tag to be enabled and for valid wallet address private async checkTag(tag: MonetizationTag): Promise { if (!(tag instanceof HTMLLinkElement && tag.rel === 'monetization')) - return null + return null; - if (tag.hasAttribute('disabled')) return null + if (tag.hasAttribute('disabled')) return null; - const walletAddressInfo = await this.validateWalletAddress(tag) + const walletAddressInfo = await this.validateWalletAddress(tag); - return walletAddressInfo + return walletAddressInfo; } private async validateWalletAddress( - tag: MonetizationTag + tag: MonetizationTag, ): Promise { - const walletAddressUrl = tag.href.trim() + const walletAddressUrl = tag.href.trim(); try { - checkWalletAddressUrlFormat(walletAddressUrl) + checkWalletAddressUrlFormat(walletAddressUrl); const response = await this.message.send('CHECK_WALLET_ADDRESS_URL', { - walletAddressUrl - }) + walletAddressUrl, + }); if (response.success === false) { throw new Error( - `Could not retrieve wallet address information for ${JSON.stringify(walletAddressUrl)}.` - ) + `Could not retrieve wallet address information for ${JSON.stringify(walletAddressUrl)}.`, + ); } - this.dispatchLoadEvent(tag) - return response.payload + this.dispatchLoadEvent(tag); + return response.payload; } catch (e) { - this.logger.error(e) - this.dispatchErrorEvent(tag) - return null + this.logger.error(e); + this.dispatchErrorEvent(tag); + return null; } } private bindMessageHandler() { this.window.addEventListener('message', (event) => { - const { message, id, payload } = event.data + const { message, id, payload } = event.data; - if (event.origin === window.location.href || id !== this.id) return + if (event.origin === window.location.href || id !== this.id) return; switch (message) { case ContentToContentAction.START_MONETIZATION: if (payload.length) { - void this.message.send('START_MONETIZATION', payload) + void this.message.send('START_MONETIZATION', payload); } - return + return; case ContentToContentAction.RESUME_MONETIZATION: if (payload.length) { - void this.message.send('RESUME_MONETIZATION', payload) + void this.message.send('RESUME_MONETIZATION', payload); } - return + return; default: - return + return; } - }) + }); } async toggleWM({ enabled }: EmitToggleWMPayload) { if (enabled) { - await this.resumeAllMonetization() + await this.resumeAllMonetization(); } else { // TODO: https://github.com/interledger/web-monetization-extension/issues/452 - this.stopAllMonetization() + this.stopAllMonetization(); } } } diff --git a/src/content/types.ts b/src/content/types.ts index ec773fc7..9daa52fe 100644 --- a/src/content/types.ts +++ b/src/content/types.ts @@ -1,9 +1,9 @@ -import { WalletAddress } from '@interledger/open-payments/dist/types' +import { WalletAddress } from '@interledger/open-payments/dist/types'; -export type MonetizationTag = HTMLLinkElement & { href?: string } -export type MonetizationTagList = NodeListOf +export type MonetizationTag = HTMLLinkElement & { href?: string }; +export type MonetizationTagList = NodeListOf; export type MonetizationTagDetails = { - walletAddress: WalletAddress | null - requestId: string -} + walletAddress: WalletAddress | null; + requestId: string; +}; diff --git a/src/content/utils.ts b/src/content/utils.ts index 0b59fc81..1a341747 100644 --- a/src/content/utils.ts +++ b/src/content/utils.ts @@ -1,62 +1,62 @@ export class WalletAddressFormatError extends Error {} export function checkWalletAddressUrlFormat(walletAddressUrl: string): void { - let url: URL + let url: URL; try { - url = new URL(walletAddressUrl) + url = new URL(walletAddressUrl); if (url.protocol !== 'https:') { throw new WalletAddressFormatError( `Wallet address URL must be specified as a fully resolved https:// url, ` + - `got ${JSON.stringify(walletAddressUrl)} ` - ) + `got ${JSON.stringify(walletAddressUrl)} `, + ); } } catch (e) { if (e instanceof WalletAddressFormatError) { - throw e + throw e; } else { throw new WalletAddressFormatError( - `Invalid wallet address URL: ${JSON.stringify(walletAddressUrl)}` - ) + `Invalid wallet address URL: ${JSON.stringify(walletAddressUrl)}`, + ); } } - const { hash, search, port, username, password } = url + const { hash, search, port, username, password } = url; if (hash || search || port || username || password) { throw new WalletAddressFormatError( - `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}` - ) + `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}`, + ); } } -type DefaultView = WindowProxy & typeof globalThis -type CloneInto = (obj: unknown, _window: DefaultView | null) => typeof obj -declare const cloneInto: CloneInto | undefined +type DefaultView = WindowProxy & typeof globalThis; +type CloneInto = (obj: unknown, _window: DefaultView | null) => typeof obj; +declare const cloneInto: CloneInto | undefined; -let cloneIntoRef: CloneInto | undefined +let cloneIntoRef: CloneInto | undefined; try { - cloneIntoRef = cloneInto + cloneIntoRef = cloneInto; } catch { - cloneIntoRef = undefined + cloneIntoRef = undefined; } export function mozClone(obj: T, document: Document) { - return cloneIntoRef ? cloneIntoRef(obj, document.defaultView) : obj + return cloneIntoRef ? cloneIntoRef(obj, document.defaultView) : obj; } export class CustomError extends Error { constructor(message?: string) { // 'Error' breaks prototype chain here - super(message) + super(message); // restore prototype chain - const actualProto = new.target.prototype + const actualProto = new.target.prototype; if (Object.setPrototypeOf) { - Object.setPrototypeOf(this, actualProto) + Object.setPrototypeOf(this, actualProto); } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(this as any).__proto__ = actualProto + (this as any).__proto__ = actualProto; } } } diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index 8ff68902..42ab49ab 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -1,19 +1,19 @@ -import { MainLayout } from '@/popup/components/layout/MainLayout' +import { MainLayout } from '@/popup/components/layout/MainLayout'; import { BrowserContextProvider, MessageContextProvider, PopupContextProvider, - TranslationContextProvider -} from './lib/context' -import { LazyMotion, domAnimation } from 'framer-motion' -import React from 'react' -import browser from 'webextension-polyfill' -import { ProtectedRoute } from '@/popup/components/ProtectedRoute' + TranslationContextProvider, +} from './lib/context'; +import { LazyMotion, domAnimation } from 'framer-motion'; +import React from 'react'; +import browser from 'webextension-polyfill'; +import { ProtectedRoute } from '@/popup/components/ProtectedRoute'; import { RouteObject, RouterProvider, - createMemoryRouter -} from 'react-router-dom' + createMemoryRouter, +} from 'react-router-dom'; export const ROUTES_PATH = { HOME: '/', @@ -21,8 +21,8 @@ export const ROUTES_PATH = { MISSING_HOST_PERMISSION: '/missing-host-permission', OUT_OF_FUNDS: '/out-of-funds', OUT_OF_FUNDS_ADD_FUNDS: '/out-of-funds/s/add-funds', - ERROR_KEY_REVOKED: '/error/key-revoked' -} as const + ERROR_KEY_REVOKED: '/error/key-revoked', +} as const; export const routes = [ { @@ -33,39 +33,39 @@ export const routes = [ children: [ { path: ROUTES_PATH.HOME, - lazy: () => import('./pages/Home') - } - ] + lazy: () => import('./pages/Home'), + }, + ], }, { children: [ { path: ROUTES_PATH.MISSING_HOST_PERMISSION, - lazy: () => import('./pages/MissingHostPermission') + lazy: () => import('./pages/MissingHostPermission'), }, { path: ROUTES_PATH.ERROR_KEY_REVOKED, - lazy: () => import('./pages/ErrorKeyRevoked') + lazy: () => import('./pages/ErrorKeyRevoked'), }, { path: ROUTES_PATH.OUT_OF_FUNDS, - lazy: () => import('./pages/OutOfFunds') + lazy: () => import('./pages/OutOfFunds'), }, { path: ROUTES_PATH.OUT_OF_FUNDS_ADD_FUNDS, - lazy: () => import('./pages/OutOfFunds_AddFunds') + lazy: () => import('./pages/OutOfFunds_AddFunds'), }, { path: ROUTES_PATH.SETTINGS, - lazy: () => import('./pages/Settings') - } - ] - } - ] - } -] satisfies RouteObject[] + lazy: () => import('./pages/Settings'), + }, + ], + }, + ], + }, +] satisfies RouteObject[]; -const router = createMemoryRouter(routes) +const router = createMemoryRouter(routes); export const Popup = () => { return ( @@ -80,5 +80,5 @@ export const Popup = () => { - ) -} + ); +}; diff --git a/src/popup/components/AllSessionsInvalid.tsx b/src/popup/components/AllSessionsInvalid.tsx index 6b172c1d..0cff2966 100644 --- a/src/popup/components/AllSessionsInvalid.tsx +++ b/src/popup/components/AllSessionsInvalid.tsx @@ -1,9 +1,9 @@ -import React from 'react' -import { WarningSign } from '@/popup/components/Icons' -import { useTranslation } from '@/popup/lib/context' +import React from 'react'; +import { WarningSign } from '@/popup/components/Icons'; +import { useTranslation } from '@/popup/lib/context'; export const AllSessionsInvalid = () => { - const t = useTranslation() + const t = useTranslation(); return (
@@ -11,5 +11,5 @@ export const AllSessionsInvalid = () => {

{t('allInvalidLinks_state_text')}

- ) -} + ); +}; diff --git a/src/popup/components/ConnectWalletForm.tsx b/src/popup/components/ConnectWalletForm.tsx index 74c0825d..ed62e402 100644 --- a/src/popup/components/ConnectWalletForm.tsx +++ b/src/popup/components/ConnectWalletForm.tsx @@ -1,38 +1,38 @@ -import React, { useCallback, useEffect } from 'react' -import { Button } from '@/popup/components/ui/Button' -import { Input } from '@/popup/components/ui/Input' -import { Label } from '@/popup/components/ui/Label' -import { Switch } from '@/popup/components/ui/Switch' -import { Code } from '@/popup/components/ui/Code' -import { debounceSync, getWalletInformation } from '@/shared/helpers' +import React, { useCallback, useEffect } from 'react'; +import { Button } from '@/popup/components/ui/Button'; +import { Input } from '@/popup/components/ui/Input'; +import { Label } from '@/popup/components/ui/Label'; +import { Switch } from '@/popup/components/ui/Switch'; +import { Code } from '@/popup/components/ui/Code'; +import { debounceSync, getWalletInformation } from '@/shared/helpers'; import { charIsNumber, formatNumber, getCurrencySymbol, - toWalletAddressUrl -} from '@/popup/lib/utils' -import { useForm } from 'react-hook-form' -import { useMessage } from '@/popup/lib/context' + toWalletAddressUrl, +} from '@/popup/lib/utils'; +import { useForm } from 'react-hook-form'; +import { useMessage } from '@/popup/lib/context'; interface ConnectWalletFormInputs { - walletAddressUrl: string - amount: string - recurring: boolean + walletAddressUrl: string; + amount: string; + recurring: boolean; } interface ConnectWalletFormProps { - publicKey: string + publicKey: string; } export const ConnectWalletForm = ({ publicKey }: ConnectWalletFormProps) => { - const message = useMessage() + const message = useMessage(); const { register, handleSubmit, formState: { errors, isSubmitting }, clearErrors, setError, - setValue + setValue, } = useForm({ criteriaMode: 'firstError', mode: 'onSubmit', @@ -40,83 +40,83 @@ export const ConnectWalletForm = ({ publicKey }: ConnectWalletFormProps) => { defaultValues: { recurring: localStorage?.getItem('recurring') === 'true' || false, amount: localStorage?.getItem('amountValue') || undefined, - walletAddressUrl: localStorage?.getItem('walletAddressUrl') || undefined - } - }) + walletAddressUrl: localStorage?.getItem('walletAddressUrl') || undefined, + }, + }); const [currencySymbol, setCurrencySymbol] = React.useState<{ - symbol: string - scale: number - }>({ symbol: '$', scale: 2 }) + symbol: string; + scale: number; + }>({ symbol: '$', scale: 2 }); const getWalletCurrency = useCallback( async (walletAddressUrl: string): Promise => { - clearErrors('walletAddressUrl') - if (!walletAddressUrl) return + clearErrors('walletAddressUrl'); + if (!walletAddressUrl) return; try { - const url = new URL(toWalletAddressUrl(walletAddressUrl)) - const walletAddress = await getWalletInformation(url.toString()) + const url = new URL(toWalletAddressUrl(walletAddressUrl)); + const walletAddress = await getWalletInformation(url.toString()); setCurrencySymbol({ symbol: getCurrencySymbol(walletAddress.assetCode), - scale: walletAddress.assetScale - }) + scale: walletAddress.assetScale, + }); } catch { setError('walletAddressUrl', { type: 'validate', - message: 'Invalid wallet address.' - }) + message: 'Invalid wallet address.', + }); } }, - [clearErrors, setError] - ) + [clearErrors, setError], + ); const handleOnChangeAmount = async ( - e: React.ChangeEvent + e: React.ChangeEvent, ) => { const amountValue = formatNumber( +e.currentTarget.value, - currencySymbol.scale - ) + currencySymbol.scale, + ); debounceSync(() => { - localStorage?.setItem('amountValue', amountValue) - }, 100)() - } + localStorage?.setItem('amountValue', amountValue); + }, 100)(); + }; const handleOnChangeWalletAddressUrl = async ( - e: React.ChangeEvent + e: React.ChangeEvent, ) => { - const walletAddressUrl = e.currentTarget.value + const walletAddressUrl = e.currentTarget.value; debounceSync(() => { - localStorage?.setItem('walletAddressUrl', walletAddressUrl) - }, 100)() - } + localStorage?.setItem('walletAddressUrl', walletAddressUrl); + }, 100)(); + }; const handleOnChangeRecurring = (e: React.ChangeEvent) => { - const recurring = e.currentTarget.checked + const recurring = e.currentTarget.checked; debounceSync( () => localStorage?.setItem('recurring', `${recurring}`), - 100 - )() - } + 100, + )(); + }; useEffect(() => { const walletAddressUrl = - localStorage?.getItem('walletAddressUrl') || undefined - if (!walletAddressUrl) return - getWalletCurrency(walletAddressUrl) - }, [getWalletCurrency]) + localStorage?.getItem('walletAddressUrl') || undefined; + if (!walletAddressUrl) return; + getWalletCurrency(walletAddressUrl); + }, [getWalletCurrency]); return (
{ const response = await message.send('CONNECT_WALLET', { ...data, - walletAddressUrl: toWalletAddressUrl(data.walletAddressUrl) - }) + walletAddressUrl: toWalletAddressUrl(data.walletAddressUrl), + }); if (!response.success) { setError('walletAddressUrl', { type: 'validate', - message: response.message - }) + message: response.message, + }); } })} className="space-y-4" @@ -148,9 +148,9 @@ export const ConnectWalletForm = ({ publicKey }: ConnectWalletFormProps) => { {...register('walletAddressUrl', { required: { value: true, message: 'Wallet address URL is required.' }, onBlur: (e: React.FocusEvent) => { - getWalletCurrency(e.currentTarget.value) + getWalletCurrency(e.currentTarget.value); }, - onChange: handleOnChangeWalletAddressUrl + onChange: handleOnChangeWalletAddressUrl, })} /> { e.key !== 'Delete' && e.key !== 'Tab' ) { - e.preventDefault() + e.preventDefault(); } }} errorMessage={errors.amount?.message} @@ -177,16 +177,16 @@ export const ConnectWalletForm = ({ publicKey }: ConnectWalletFormProps) => { onBlur: (e: React.FocusEvent) => { setValue( 'amount', - formatNumber(+e.currentTarget.value, currencySymbol.scale) - ) + formatNumber(+e.currentTarget.value, currencySymbol.scale), + ); }, - onChange: handleOnChangeAmount + onChange: handleOnChangeAmount, })} />
@@ -201,5 +201,5 @@ export const ConnectWalletForm = ({ publicKey }: ConnectWalletFormProps) => { Connect - ) -} + ); +}; diff --git a/src/popup/components/ErrorKeyRevoked.tsx b/src/popup/components/ErrorKeyRevoked.tsx index 3d83ab2c..954c6162 100644 --- a/src/popup/components/ErrorKeyRevoked.tsx +++ b/src/popup/components/ErrorKeyRevoked.tsx @@ -1,36 +1,36 @@ -import React from 'react' -import { useForm } from 'react-hook-form' -import { AnimatePresence, m } from 'framer-motion' -import { WarningSign } from '@/popup/components/Icons' -import { Button } from '@/popup/components/ui/Button' -import { Code } from '@/popup/components/ui/Code' -import { useTranslation } from '@/popup/lib/context' -import { useLocalStorage } from '@/popup/lib/hooks' -import type { PopupStore } from '@/shared/types' -import type { Response } from '@/shared/messages' +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { AnimatePresence, m } from 'framer-motion'; +import { WarningSign } from '@/popup/components/Icons'; +import { Button } from '@/popup/components/ui/Button'; +import { Code } from '@/popup/components/ui/Code'; +import { useTranslation } from '@/popup/lib/context'; +import { useLocalStorage } from '@/popup/lib/hooks'; +import type { PopupStore } from '@/shared/types'; +import type { Response } from '@/shared/messages'; interface Props { - info: Pick - disconnectWallet: () => Promise - reconnectWallet: () => Promise - onReconnect?: () => void - onDisconnect?: () => void + info: Pick; + disconnectWallet: () => Promise; + reconnectWallet: () => Promise; + onReconnect?: () => void; + onDisconnect?: () => void; } -type Screen = 'main' | 'reconnect' +type Screen = 'main' | 'reconnect'; export const ErrorKeyRevoked = ({ info, disconnectWallet, reconnectWallet, onReconnect, - onDisconnect + onDisconnect, }: Props) => { const [screen, setScreen, clearScreen] = useLocalStorage( 'keyRevokedScreen', 'main', - { maxAge: 2 * 60 } - ) + { maxAge: 2 * 60 }, + ); if (screen === 'main') { return ( @@ -41,7 +41,7 @@ export const ErrorKeyRevoked = ({ onDisconnect={onDisconnect} /> - ) + ); } else { return ( @@ -49,41 +49,41 @@ export const ErrorKeyRevoked = ({ info={info} reconnectWallet={reconnectWallet} onReconnect={() => { - clearScreen() - onReconnect?.() + clearScreen(); + onReconnect?.(); }} /> - ) + ); } -} +}; interface MainScreenProps { - disconnectWallet: Props['disconnectWallet'] - onDisconnect?: Props['onDisconnect'] - requestReconnect: () => void + disconnectWallet: Props['disconnectWallet']; + onDisconnect?: Props['onDisconnect']; + requestReconnect: () => void; } const MainScreen = ({ disconnectWallet, onDisconnect, - requestReconnect + requestReconnect, }: MainScreenProps) => { - const t = useTranslation() - const [errorMsg, setErrorMsg] = React.useState('') - const [loading, setIsLoading] = React.useState(false) + const t = useTranslation(); + const [errorMsg, setErrorMsg] = React.useState(''); + const [loading, setIsLoading] = React.useState(false); const requestDisconnect = async () => { - setErrorMsg('') + setErrorMsg(''); try { - setIsLoading(true) - await disconnectWallet() - onDisconnect?.() + setIsLoading(true); + await disconnectWallet(); + onDisconnect?.(); } catch (error) { - setErrorMsg(error.message) + setErrorMsg(error.message); } - setIsLoading(false) - } + setIsLoading(false); + }; return ( @@ -115,41 +115,41 @@ const MainScreen = ({ - ) -} + ); +}; interface ReconnectScreenProps { - info: Props['info'] - reconnectWallet: Props['reconnectWallet'] - onReconnect?: Props['onDisconnect'] + info: Props['info']; + reconnectWallet: Props['reconnectWallet']; + onReconnect?: Props['onDisconnect']; } const ReconnectScreen = ({ info, reconnectWallet, - onReconnect + onReconnect, }: ReconnectScreenProps) => { - const t = useTranslation() + const t = useTranslation(); const { handleSubmit, formState: { errors, isSubmitting }, clearErrors, - setError - } = useForm({ criteriaMode: 'firstError', mode: 'onSubmit' }) + setError, + } = useForm({ criteriaMode: 'firstError', mode: 'onSubmit' }); const requestReconnect = async () => { - clearErrors() + clearErrors(); try { - const res = await reconnectWallet() + const res = await reconnectWallet(); if (res.success) { - onReconnect?.() + onReconnect?.(); } else { - setError('root', { message: res.message }) + setError('root', { message: res.message }); } } catch (error) { - setError('root', { message: error.message }) + setError('root', { message: error.message }); } - } + }; return ( - ) -} + ); +}; diff --git a/src/popup/components/ErrorMessage.tsx b/src/popup/components/ErrorMessage.tsx index 060006a2..b0b6d052 100644 --- a/src/popup/components/ErrorMessage.tsx +++ b/src/popup/components/ErrorMessage.tsx @@ -1,13 +1,13 @@ -import React from 'react' -import { XIcon } from './Icons' -import { cn } from '@/shared/helpers' +import React from 'react'; +import { XIcon } from './Icons'; +import { cn } from '@/shared/helpers'; interface ErrorMessageProps extends React.HTMLAttributes { - error?: string + error?: string; } export const ErrorMessage = React.forwardRef( ({ error, className, children, ...props }, ref) => { - if (!error) return null + if (!error) return null; return (
( ref={ref} className={cn( 'break-word mb-4 flex items-center gap-2 rounded-xl border border-red-300 bg-red-500/10 px-3 py-2', - className + className, )} > @@ -24,8 +24,8 @@ export const ErrorMessage = React.forwardRef( {children}
- ) - } -) + ); + }, +); -ErrorMessage.displayName = 'ErrorMessage' +ErrorMessage.displayName = 'ErrorMessage'; diff --git a/src/popup/components/Icons.tsx b/src/popup/components/Icons.tsx index 149e5308..36a951a5 100644 --- a/src/popup/components/Icons.tsx +++ b/src/popup/components/Icons.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React from 'react'; export const Spinner = (props: React.SVGProps) => { return ( @@ -16,8 +16,8 @@ export const Spinner = (props: React.SVGProps) => { d="M2.204 6.447A6 6 0 108 2" /> - ) -} + ); +}; export const ArrowBack = (props: React.SVGProps) => { return ( @@ -46,8 +46,8 @@ export const ArrowBack = (props: React.SVGProps) => { /> - ) -} + ); +}; export const Settings = (props: React.SVGProps) => { return ( @@ -76,8 +76,8 @@ export const Settings = (props: React.SVGProps) => { /> - ) -} + ); +}; export const DollarSign = (props: React.SVGProps) => { return ( @@ -110,8 +110,8 @@ export const DollarSign = (props: React.SVGProps) => { - ) -} + ); +}; export const WarningSign = (props: React.SVGProps) => { return ( @@ -141,8 +141,8 @@ export const WarningSign = (props: React.SVGProps) => { /> - ) -} + ); +}; export const ClipboardIcon = (props: React.SVGProps) => { return ( @@ -159,8 +159,8 @@ export const ClipboardIcon = (props: React.SVGProps) => { fill="currentColor" /> - ) -} + ); +}; export const CheckIcon = (props: React.SVGProps) => { return ( @@ -179,8 +179,8 @@ export const CheckIcon = (props: React.SVGProps) => { d="m4.5 12.75 6 6 9-13.5" /> - ) -} + ); +}; export const XIcon = (props: React.SVGProps) => { return ( @@ -199,5 +199,5 @@ export const XIcon = (props: React.SVGProps) => { d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> - ) -} + ); +}; diff --git a/src/popup/components/LoadingSpinner.tsx b/src/popup/components/LoadingSpinner.tsx index 5e6e10b0..fc4c5268 100644 --- a/src/popup/components/LoadingSpinner.tsx +++ b/src/popup/components/LoadingSpinner.tsx @@ -1,22 +1,22 @@ -import { type VariantProps, cva } from 'class-variance-authority' -import React from 'react' +import { type VariantProps, cva } from 'class-variance-authority'; +import React from 'react'; -import { Spinner } from '@/popup/components/Icons' +import { Spinner } from '@/popup/components/Icons'; const loadingSpinnerStyles = cva('animate-spin text-white', { variants: { variant: { md: 'h-4 w-4', - lg: 'h-6 w-6' - } + lg: 'h-6 w-6', + }, }, defaultVariants: { - variant: 'lg' - } -}) + variant: 'lg', + }, +}); -export type LoadingIndicatorProps = VariantProps +export type LoadingIndicatorProps = VariantProps; export const LoadingSpinner = ({ variant }: LoadingIndicatorProps) => { - return -} + return ; +}; diff --git a/src/popup/components/OutOfFunds.tsx b/src/popup/components/OutOfFunds.tsx index ca7367bd..4ad9ea06 100644 --- a/src/popup/components/OutOfFunds.tsx +++ b/src/popup/components/OutOfFunds.tsx @@ -1,37 +1,37 @@ -import React from 'react' -import { useForm } from 'react-hook-form' -import type { RecurringGrant, OneTimeGrant, AmountValue } from '@/shared/types' -import type { AddFundsPayload, Response } from '@/shared/messages' -import type { WalletAddress } from '@interledger/open-payments' +import React from 'react'; +import { useForm } from 'react-hook-form'; +import type { RecurringGrant, OneTimeGrant, AmountValue } from '@/shared/types'; +import type { AddFundsPayload, Response } from '@/shared/messages'; +import type { WalletAddress } from '@interledger/open-payments'; import { charIsNumber, formatNumber, getCurrencySymbol, - transformBalance -} from '@/popup/lib/utils' -import { useTranslation } from '@/popup/lib/context' -import { getNextOccurrence } from '@/shared/helpers' -import { ErrorMessage } from '@/popup/components/ErrorMessage' -import { Button } from '@/popup/components/ui/Button' -import { Input } from '@/popup/components/ui/Input' + transformBalance, +} from '@/popup/lib/utils'; +import { useTranslation } from '@/popup/lib/context'; +import { getNextOccurrence } from '@/shared/helpers'; +import { ErrorMessage } from '@/popup/components/ErrorMessage'; +import { Button } from '@/popup/components/ui/Button'; +import { Input } from '@/popup/components/ui/Input'; interface OutOfFundsProps { - info: Pick - grantRecurring?: RecurringGrant['amount'] - grantOneTime?: OneTimeGrant['amount'] - onChooseOption: (recurring: boolean) => void + info: Pick; + grantRecurring?: RecurringGrant['amount']; + grantOneTime?: OneTimeGrant['amount']; + onChooseOption: (recurring: boolean) => void; } export const OutOfFunds = ({ info, grantOneTime, grantRecurring, - onChooseOption + onChooseOption, }: OutOfFundsProps) => { if (!grantOneTime && !grantRecurring) { - throw new Error('Provide at least one of grantOneTime and grantRecurring') + throw new Error('Provide at least one of grantOneTime and grantRecurring'); } - const t = useTranslation() + const t = useTranslation(); return (
@@ -61,39 +61,39 @@ export const OutOfFunds = ({ {t('outOfFunds_action_optionOneTime')}
- ) -} + ); +}; interface AddFundsProps { - info: Pick - recurring: boolean - defaultAmount: AmountValue - requestAddFunds: (details: AddFundsPayload) => Promise + info: Pick; + recurring: boolean; + defaultAmount: AmountValue; + requestAddFunds: (details: AddFundsPayload) => Promise; } export function AddFunds({ info, defaultAmount, recurring, - requestAddFunds + requestAddFunds, }: AddFundsProps) { - const t = useTranslation() + const t = useTranslation(); const { register, handleSubmit, formState: { errors, isSubmitting }, setError, - setValue + setValue, } = useForm({ criteriaMode: 'firstError', mode: 'onSubmit', reValidateMode: 'onBlur', defaultValues: { - amount: transformBalance(defaultAmount, info.assetScale) - } - }) + amount: transformBalance(defaultAmount, info.assetScale), + }, + }); - const currencySymbol = getCurrencySymbol(info.assetCode) + const currencySymbol = getCurrencySymbol(info.assetCode); return (
{ const response = await requestAddFunds({ amount: data.amount, - recurring: !!recurring - }) + recurring: !!recurring, + }); if (!response.success) { - setError('root', { message: response.message }) + setError('root', { message: response.message }); } })} > @@ -124,7 +124,7 @@ export function AddFunds({ description={ recurring ? t('outOfFundsAddFunds_label_amountDescriptionRecurring', [ - getNextOccurrenceDate('P1M') + getNextOccurrenceDate('P1M'), ]) : t('outOfFundsAddFunds_label_amountDescriptionOneTime') } @@ -138,7 +138,7 @@ export function AddFunds({ e.key !== 'Delete' && e.key !== 'Tab' ) { - e.preventDefault() + e.preventDefault(); } }} errorMessage={errors.amount?.message} @@ -146,8 +146,8 @@ export function AddFunds({ required: { value: true, message: 'Amount is required.' }, valueAsNumber: false, onBlur: (e: React.FocusEvent) => { - setValue('amount', formatNumber(+e.currentTarget.value, 2)) - } + setValue('amount', formatNumber(+e.currentTarget.value, 2)); + }, })} /> @@ -164,35 +164,35 @@ export function AddFunds({ : t('outOfFundsAddFunds_action_addOneTime')} - ) + ); } function RecurringAutoRenewInfo({ grantRecurring, - info + info, }: Pick) { - const t = useTranslation() + const t = useTranslation(); - if (!grantRecurring) return null + if (!grantRecurring) return null; - const currencySymbol = getCurrencySymbol(info.assetCode) - const amount = transformBalance(grantRecurring.value, info.assetScale) - const renewDate = getNextOccurrence(grantRecurring.interval, new Date()) + const currencySymbol = getCurrencySymbol(info.assetCode); + const amount = transformBalance(grantRecurring.value, info.assetScale); + const renewDate = getNextOccurrence(grantRecurring.interval, new Date()); const renewDateLocalized = renewDate.toLocaleString(undefined, { dateStyle: 'medium', - timeStyle: 'short' - }) + timeStyle: 'short', + }); return t('outOfFunds_error_textDoNothing', [ `${currencySymbol}${amount}`, - renewDateLocalized - ]) + renewDateLocalized, + ]); } function getNextOccurrenceDate(period: 'P1M', baseDate = new Date()) { const date = getNextOccurrence( `R/${baseDate.toISOString()}/${period}`, - baseDate - ) - return date.toLocaleDateString(undefined, { dateStyle: 'medium' }) + baseDate, + ); + return date.toLocaleDateString(undefined, { dateStyle: 'medium' }); } diff --git a/src/popup/components/PayWebsiteForm.tsx b/src/popup/components/PayWebsiteForm.tsx index 9807fba8..3be8cef7 100644 --- a/src/popup/components/PayWebsiteForm.tsx +++ b/src/popup/components/PayWebsiteForm.tsx @@ -1,36 +1,36 @@ -import { Button } from '@/popup/components/ui/Button' -import { Input } from '@/popup/components/ui/Input' -import { useMessage, usePopupState } from '@/popup/lib/context' +import { Button } from '@/popup/components/ui/Button'; +import { Input } from '@/popup/components/ui/Input'; +import { useMessage, usePopupState } from '@/popup/lib/context'; import { getCurrencySymbol, charIsNumber, - formatNumber -} from '@/popup/lib/utils' -import React, { useMemo } from 'react' -import { useForm } from 'react-hook-form' -import { AnimatePresence, m } from 'framer-motion' -import { Spinner } from './Icons' -import { cn } from '@/shared/helpers' -import { ErrorMessage } from './ErrorMessage' + formatNumber, +} from '@/popup/lib/utils'; +import React, { useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { AnimatePresence, m } from 'framer-motion'; +import { Spinner } from './Icons'; +import { cn } from '@/shared/helpers'; +import { ErrorMessage } from './ErrorMessage'; interface PayWebsiteFormProps { - amount: string + amount: string; } const BUTTON_STATE = { idle: 'Send now', loading: , - success: 'Payment successful' -} + success: 'Payment successful', +}; export const PayWebsiteForm = () => { - const message = useMessage() + const message = useMessage(); const { - state: { walletAddress, url } - } = usePopupState() + state: { walletAddress, url }, + } = usePopupState(); const [buttonState, setButtonState] = - React.useState('idle') - const isIdle = useMemo(() => buttonState === 'idle', [buttonState]) + React.useState('idle'); + const isIdle = useMemo(() => buttonState === 'idle', [buttonState]); const { register, @@ -38,26 +38,26 @@ export const PayWebsiteForm = () => { setValue, handleSubmit, ...form - } = useForm() + } = useForm(); const onSubmit = handleSubmit(async (data) => { - if (buttonState !== 'idle') return + if (buttonState !== 'idle') return; - setButtonState('loading') + setButtonState('loading'); - const response = await message.send('PAY_WEBSITE', { amount: data.amount }) + const response = await message.send('PAY_WEBSITE', { amount: data.amount }); if (!response.success) { - setButtonState('idle') - form.setError('root', { message: response.message }) + setButtonState('idle'); + form.setError('root', { message: response.message }); } else { - setButtonState('success') - form.reset() + setButtonState('success'); + form.reset(); setTimeout(() => { - setButtonState('idle') - }, 2000) + setButtonState('idle'); + }, 2000); } - }) + }); return (
@@ -66,7 +66,7 @@ export const PayWebsiteForm = () => { { placeholder="0.00" onKeyDown={(e) => { if (e.key === 'Enter') { - e.currentTarget.blur() - onSubmit() + e.currentTarget.blur(); + onSubmit(); } else if ( !charIsNumber(e.key) && e.key !== 'Backspace' && e.key !== 'Delete' && e.key !== 'Tab' ) { - e.preventDefault() + e.preventDefault(); } }} errorMessage={errors.amount?.message} @@ -108,9 +108,9 @@ export const PayWebsiteForm = () => { onBlur: (e: React.FocusEvent) => { setValue( 'amount', - formatNumber(+e.currentTarget.value, walletAddress.assetScale) - ) - } + formatNumber(+e.currentTarget.value, walletAddress.assetScale), + ); + }, })} /> - ) -} + ); +}; diff --git a/src/popup/components/ProtectedRoute.tsx b/src/popup/components/ProtectedRoute.tsx index bfc31b9c..049eebb0 100644 --- a/src/popup/components/ProtectedRoute.tsx +++ b/src/popup/components/ProtectedRoute.tsx @@ -1,23 +1,23 @@ -import { usePopupState } from '@/popup/lib/context' -import React from 'react' -import { Navigate, Outlet } from 'react-router-dom' -import { ROUTES_PATH } from '../Popup' +import { usePopupState } from '@/popup/lib/context'; +import React from 'react'; +import { Navigate, Outlet } from 'react-router-dom'; +import { ROUTES_PATH } from '../Popup'; export const ProtectedRoute = () => { - const { state } = usePopupState() + const { state } = usePopupState(); if (state.state.missing_host_permissions) { - return + return ; } if (state.state.key_revoked) { - return + return ; } if (state.state.out_of_funds) { - return + return ; } if (state.connected === false) { - return + return ; } - return -} + return ; +}; diff --git a/src/popup/components/SiteNotMonetized.tsx b/src/popup/components/SiteNotMonetized.tsx index 569ff00e..b06f0144 100644 --- a/src/popup/components/SiteNotMonetized.tsx +++ b/src/popup/components/SiteNotMonetized.tsx @@ -1,9 +1,9 @@ -import React from 'react' -import { WarningSign } from '@/popup/components/Icons' -import { useTranslation } from '@/popup/lib/context' +import React from 'react'; +import { WarningSign } from '@/popup/components/Icons'; +import { useTranslation } from '@/popup/lib/context'; export const SiteNotMonetized = () => { - const t = useTranslation() + const t = useTranslation(); return (
@@ -11,5 +11,5 @@ export const SiteNotMonetized = () => {

{t('siteNotMonetized_state_text')}

- ) -} + ); +}; diff --git a/src/popup/components/WalletInformation.tsx b/src/popup/components/WalletInformation.tsx index 9e1e0673..7cc85069 100644 --- a/src/popup/components/WalletInformation.tsx +++ b/src/popup/components/WalletInformation.tsx @@ -1,22 +1,22 @@ -import { Input } from '@/popup/components/ui/Input' -import { Label } from '@/popup/components/ui/Label' -import React from 'react' -import { Code } from '@/popup/components/ui/Code' -import { PopupStore } from '@/shared/types' -import { Button } from '@/popup/components/ui/Button' -import { useMessage } from '@/popup/lib/context' -import { useForm } from 'react-hook-form' +import { Input } from '@/popup/components/ui/Input'; +import { Label } from '@/popup/components/ui/Label'; +import React from 'react'; +import { Code } from '@/popup/components/ui/Code'; +import { PopupStore } from '@/shared/types'; +import { Button } from '@/popup/components/ui/Button'; +import { useMessage } from '@/popup/lib/context'; +import { useForm } from 'react-hook-form'; interface WalletInformationProps { - info: PopupStore + info: PopupStore; } export const WalletInformation = ({ info }: WalletInformationProps) => { - const message = useMessage() + const message = useMessage(); const { handleSubmit, - formState: { isSubmitting } - } = useForm() + formState: { isSubmitting }, + } = useForm(); return (
@@ -37,8 +37,8 @@ export const WalletInformation = ({ info }: WalletInformationProps) => { {/* TODO: Improve error handling */}
{ - await message.send('DISCONNECT_WALLET') - window.location.reload() + await message.send('DISCONNECT_WALLET'); + window.location.reload(); })} >
- ) -} + ); +}; diff --git a/src/popup/components/WarningMessage.tsx b/src/popup/components/WarningMessage.tsx index aac52ac1..7d2c01b4 100644 --- a/src/popup/components/WarningMessage.tsx +++ b/src/popup/components/WarningMessage.tsx @@ -1,15 +1,15 @@ -import React from 'react' -import { WarningSign } from './Icons' -import { cn } from '@/shared/helpers' +import React from 'react'; +import { WarningSign } from './Icons'; +import { cn } from '@/shared/helpers'; interface WarningMessageProps extends React.HTMLAttributes { - warning?: string + warning?: string; } export const WarningMessage = React.forwardRef< HTMLDivElement, WarningMessageProps >(({ warning, className, children, ...props }, ref) => { - if (!warning) return null + if (!warning) return null; return (
@@ -26,7 +26,7 @@ export const WarningMessage = React.forwardRef< {children}
- ) -}) + ); +}); -WarningMessage.displayName = 'WarningMessage' +WarningMessage.displayName = 'WarningMessage'; diff --git a/src/popup/components/layout/Header.tsx b/src/popup/components/layout/Header.tsx index 631f6eb7..e186a95b 100644 --- a/src/popup/components/layout/Header.tsx +++ b/src/popup/components/layout/Header.tsx @@ -1,23 +1,23 @@ -import React from 'react' -import { Link, useLocation } from 'react-router-dom' -import { ArrowBack, Settings } from '../Icons' -import { ROUTES_PATH } from '@/popup/Popup' -import { useBrowser, usePopupState } from '@/popup/lib/context' +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { ArrowBack, Settings } from '../Icons'; +import { ROUTES_PATH } from '@/popup/Popup'; +import { useBrowser, usePopupState } from '@/popup/lib/context'; const NavigationButton = () => { - const location = useLocation() + const location = useLocation(); const { - state: { connected } - } = usePopupState() + state: { connected }, + } = usePopupState(); return React.useMemo(() => { - if (!connected) return null + if (!connected) return null; if (location.pathname.includes('/s/')) { return ( - ) + ); } return location.pathname === `${ROUTES_PATH.SETTINGS}` ? ( @@ -28,13 +28,13 @@ const NavigationButton = () => { - ) - }, [location, connected]) -} + ); + }, [location, connected]); +}; export const Header = () => { - const browser = useBrowser() - const Logo = browser.runtime.getURL('assets/images/logo.svg') + const browser = useBrowser(); + const Logo = browser.runtime.getURL('assets/images/logo.svg'); return (
@@ -46,5 +46,5 @@ export const Header = () => {
- ) -} + ); +}; diff --git a/src/popup/components/layout/MainLayout.tsx b/src/popup/components/layout/MainLayout.tsx index e4e175ac..4f8174b5 100644 --- a/src/popup/components/layout/MainLayout.tsx +++ b/src/popup/components/layout/MainLayout.tsx @@ -1,11 +1,11 @@ -import React from 'react' -import { Outlet } from 'react-router-dom' +import React from 'react'; +import { Outlet } from 'react-router-dom'; -import { Header } from './Header' +import { Header } from './Header'; const Divider = () => { - return
-} + return
; +}; export const MainLayout = () => { return ( @@ -16,5 +16,5 @@ export const MainLayout = () => {
- ) -} + ); +}; diff --git a/src/popup/components/ui/Button.tsx b/src/popup/components/ui/Button.tsx index 2ed857cb..bc21e6fd 100644 --- a/src/popup/components/ui/Button.tsx +++ b/src/popup/components/ui/Button.tsx @@ -1,14 +1,14 @@ -import { type VariantProps, cva } from 'class-variance-authority' -import React, { forwardRef } from 'react' +import { type VariantProps, cva } from 'class-variance-authority'; +import React, { forwardRef } from 'react'; -import { LoadingSpinner } from '@/popup/components/LoadingSpinner' -import { cn } from '@/shared/helpers' +import { LoadingSpinner } from '@/popup/components/LoadingSpinner'; +import { cn } from '@/shared/helpers'; const buttonVariants = cva( [ 'relative inline-flex items-center justify-center whitespace-nowrap rounded-xl font-semibold', 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500', - 'disabled:pointer-events-none disabled:select-none disabled:opacity-50' + 'disabled:pointer-events-none disabled:select-none disabled:opacity-50', ], { @@ -16,32 +16,32 @@ const buttonVariants = cva( variant: { default: 'bg-button-base text-white hover:bg-button-base-hover', destructive: 'bg-error text-error hover:bg-error-hover', - ghost: '' + ghost: '', }, size: { default: 'px-6 py-4 font-medium', - icon: 'h-6 w-6' + icon: 'h-6 w-6', }, fullWidth: { - true: 'w-full' + true: 'w-full', }, loading: { - true: 'text-transparent' - } + true: 'text-transparent', + }, }, defaultVariants: { variant: 'default', - size: 'default' - } - } -) + size: 'default', + }, + }, +); export interface ButtonProps extends VariantProps, React.ButtonHTMLAttributes { - loading?: boolean + loading?: boolean; /** Optional only when children are passed */ - ['aria-label']?: string + ['aria-label']?: string; } export const Button = forwardRef( @@ -56,7 +56,7 @@ export const Button = forwardRef( children, ...props }, - ref + ref, ) { return ( - ) - } -) + ); + }, +); diff --git a/src/popup/components/ui/Code.tsx b/src/popup/components/ui/Code.tsx index 931e0b39..71998828 100644 --- a/src/popup/components/ui/Code.tsx +++ b/src/popup/components/ui/Code.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import { Button } from './Button' -import { CheckIcon, ClipboardIcon } from '../Icons' -import { cn } from '@/shared/helpers' +import React from 'react'; +import { Button } from './Button'; +import { CheckIcon, ClipboardIcon } from '../Icons'; +import { cn } from '@/shared/helpers'; interface CodeProps extends React.HTMLAttributes { - value: string + value: string; } export const Code = ({ value, className, ...props }: CodeProps) => { @@ -12,7 +12,7 @@ export const Code = ({ value, className, ...props }: CodeProps) => {
@@ -21,23 +21,23 @@ export const Code = ({ value, className, ...props }: CodeProps) => {
- ) -} + ); +}; interface CopyButtonProps extends React.HTMLAttributes { - value: string + value: string; } const CopyButton = ({ value, ...props }: CopyButtonProps) => { - const [hasCopied, setHasCopied] = React.useState(false) + const [hasCopied, setHasCopied] = React.useState(false); React.useEffect(() => { if (hasCopied === true) { setTimeout(() => { - setHasCopied(false) - }, 2000) + setHasCopied(false); + }, 2000); } - }, [hasCopied]) + }, [hasCopied]); return ( - ) -} + ); +}; diff --git a/src/popup/components/ui/Input.tsx b/src/popup/components/ui/Input.tsx index 6d9cf837..49c4a65e 100644 --- a/src/popup/components/ui/Input.tsx +++ b/src/popup/components/ui/Input.tsx @@ -1,38 +1,38 @@ -import { type VariantProps, cva } from 'class-variance-authority' -import React, { forwardRef } from 'react' -import { cn } from '@/shared/helpers' -import { Label } from '@/popup/components/ui/Label' +import { type VariantProps, cva } from 'class-variance-authority'; +import React, { forwardRef } from 'react'; +import { cn } from '@/shared/helpers'; +import { Label } from '@/popup/components/ui/Label'; const inputVariants = cva( [ 'h-14 w-full rounded-xl border border-2 px-4 text-base text-medium', 'focus:border-focus focus:outline-none', - 'placeholder-disabled' + 'placeholder-disabled', ], { variants: { variant: { - default: 'border-base' + default: 'border-base', }, disabled: { - true: 'border-transparent bg-disabled' - } + true: 'border-transparent bg-disabled', + }, }, defaultVariants: { - variant: 'default' - } - } -) + variant: 'default', + }, + }, +); export interface InputProps extends VariantProps, React.InputHTMLAttributes { - errorMessage?: string - disabled?: boolean - addOn?: React.ReactNode - label?: React.ReactNode - description?: React.ReactNode + errorMessage?: string; + disabled?: boolean; + addOn?: React.ReactNode; + label?: React.ReactNode; + description?: React.ReactNode; } export const Input = forwardRef(function Input( @@ -46,9 +46,9 @@ export const Input = forwardRef(function Input( className, ...props }, - ref + ref, ) { - const id = React.useId() + const id = React.useId(); return (
{label ? : null} @@ -67,7 +67,7 @@ export const Input = forwardRef(function Input( inputVariants({ disabled }), addOn && 'pl-10', errorMessage && 'border-error', - className + className, )} disabled={disabled ?? false} aria-disabled={disabled ?? false} @@ -80,5 +80,5 @@ export const Input = forwardRef(function Input(

{errorMessage}

)}
- ) -}) + ); +}); diff --git a/src/popup/components/ui/Label.tsx b/src/popup/components/ui/Label.tsx index 40502dc2..b17df2f3 100644 --- a/src/popup/components/ui/Label.tsx +++ b/src/popup/components/ui/Label.tsx @@ -1,25 +1,25 @@ -import { type VariantProps, cva } from 'class-variance-authority' -import React, { forwardRef } from 'react' +import { type VariantProps, cva } from 'class-variance-authority'; +import React, { forwardRef } from 'react'; -import { cn } from '@/shared/helpers' +import { cn } from '@/shared/helpers'; const labelVariants = cva( - 'flex items-center px-2 font-medium leading-6 text-medium' -) + 'flex items-center px-2 font-medium leading-6 text-medium', +); export interface LabelProps extends VariantProps, React.LabelHTMLAttributes { - children: React.ReactNode + children: React.ReactNode; } export const Label = forwardRef(function Label( { className, children, ...props }, - ref + ref, ) { return ( - ) -}) + ); +}); diff --git a/src/popup/components/ui/RadioGroup.tsx b/src/popup/components/ui/RadioGroup.tsx index 09e4bc23..c3ff2a51 100644 --- a/src/popup/components/ui/RadioGroup.tsx +++ b/src/popup/components/ui/RadioGroup.tsx @@ -1,17 +1,17 @@ -import { type VariantProps, cva } from 'class-variance-authority' -import React, { useEffect, useMemo, useState } from 'react' +import { type VariantProps, cva } from 'class-variance-authority'; +import React, { useEffect, useMemo, useState } from 'react'; -import { cn } from '@/shared/helpers' +import { cn } from '@/shared/helpers'; export interface RadioProps { - checked?: boolean - label?: string - value: string - name: string - id?: string - disabled?: boolean - onChange?: any - noSelected?: boolean + checked?: boolean; + label?: string; + value: string; + name: string; + id?: string; + disabled?: boolean; + onChange?: any; + noSelected?: boolean; } export const Radio = ({ @@ -22,14 +22,14 @@ export const Radio = ({ disabled, onChange, checked, - noSelected + noSelected, }: RadioProps): JSX.Element => { - const inputId = id || `id-${name}-${value}` - const divId = `div-${inputId}` + const inputId = id || `id-${name}-${value}`; + const divId = `div-${inputId}`; useEffect(() => { - if (checked) document.getElementById(divId)?.focus() - }, [checked, divId]) + if (checked) document.getElementById(divId)?.focus(); + }, [checked, divId]); return (
- ) -} + ); +}; const radioGroupVariants = cva(['flex gap-3'], { variants: { variant: { default: 'flex-col', - inline: 'flex-row' + inline: 'flex-row', }, fullWidth: { - true: 'w-full' - } + true: 'w-full', + }, }, defaultVariants: { - variant: 'default' - } -}) + variant: 'default', + }, +}); export interface RadioGroupProps extends VariantProps, React.InputHTMLAttributes { - disabled?: boolean - items: Omit[] - name: string - handleChange?: (value: string) => void + disabled?: boolean; + items: Omit[]; + name: string; + handleChange?: (value: string) => void; } export const RadioGroup = ({ @@ -97,27 +97,27 @@ export const RadioGroup = ({ disabled, className, handleChange, - value + value, }: RadioGroupProps) => { const checkedItem = useMemo( () => items.findIndex((item) => item.checked || item.value === value), - [items, value] - ) - const [selected, setSelected] = useState(checkedItem) + [items, value], + ); + const [selected, setSelected] = useState(checkedItem); const handleKeyDown = (event: React.KeyboardEvent) => { if (event.code === 'ArrowRight' || event.code === 'ArrowDown') { - event.preventDefault() + event.preventDefault(); - const nextIndex = (selected >= 0 ? selected + 1 : 1) % items.length - setSelected(nextIndex) + const nextIndex = (selected >= 0 ? selected + 1 : 1) % items.length; + setSelected(nextIndex); } else if (event.code === 'ArrowLeft' || event.code === 'ArrowUp') { - event.preventDefault() + event.preventDefault(); - const prevIndex = selected > 0 ? selected - 1 : items.length - 1 - setSelected(prevIndex) + const prevIndex = selected > 0 ? selected - 1 : items.length - 1; + setSelected(prevIndex); } - } + }; useEffect(() => { const handleKeyPress = (event: KeyboardEvent) => { @@ -125,15 +125,15 @@ export const RadioGroup = ({ selected === -1 && (event.code === 'Enter' || event.code === 'Space') ) { - setSelected(0) + setSelected(0); } - } + }; - document.addEventListener('keypress', handleKeyPress) + document.addEventListener('keypress', handleKeyPress); return () => { - document.removeEventListener('keypress', handleKeyPress) - } - }, [selected]) + document.removeEventListener('keypress', handleKeyPress); + }; + }, [selected]); return (
{ - setSelected(index) - if (handleChange) handleChange(item.value) + setSelected(index); + if (handleChange) handleChange(item.value); }} /> ))}
- ) -} + ); +}; diff --git a/src/popup/components/ui/Slider.tsx b/src/popup/components/ui/Slider.tsx index e680b5f8..da729c6d 100644 --- a/src/popup/components/ui/Slider.tsx +++ b/src/popup/components/ui/Slider.tsx @@ -1,16 +1,16 @@ -import React, { forwardRef } from 'react' +import React, { forwardRef } from 'react'; -import { cn } from '@/shared/helpers' +import { cn } from '@/shared/helpers'; export interface SliderProps extends React.InputHTMLAttributes { - errorMessage?: string - disabled?: boolean - icon?: React.ReactNode - min?: number - max?: number - value?: number - onChange?: (_event: React.ChangeEvent) => void + errorMessage?: string; + disabled?: boolean; + icon?: React.ReactNode; + min?: number; + max?: number; + value?: number; + onChange?: (_event: React.ChangeEvent) => void; } const sliderClasses = ` @@ -27,7 +27,7 @@ const sliderClasses = ` [&::-webkit-slider-thumb]:disabled:bg-disabled-strong w-full h-1 bg-disabled-strong rounded-lg appearance-none cursor-pointer dark:bg-disabled-strong -` +`; export const Slider = forwardRef(function Slider( { @@ -38,7 +38,7 @@ export const Slider = forwardRef(function Slider( disabled, ...props }, - ref + ref, ) { return (
@@ -61,5 +61,5 @@ export const Slider = forwardRef(function Slider(

{errorMessage}

)}
- ) -}) + ); +}); diff --git a/src/popup/components/ui/Switch.tsx b/src/popup/components/ui/Switch.tsx index b7eac3b8..24146770 100644 --- a/src/popup/components/ui/Switch.tsx +++ b/src/popup/components/ui/Switch.tsx @@ -1,7 +1,7 @@ -import { type VariantProps, cva } from 'class-variance-authority' -import React, { forwardRef } from 'react' +import { type VariantProps, cva } from 'class-variance-authority'; +import React, { forwardRef } from 'react'; -import { cn } from '@/shared/helpers' +import { cn } from '@/shared/helpers'; const switchVariants = cva( [ @@ -10,7 +10,7 @@ const switchVariants = cva( 'before:left-[4px] before:top-1/2 before:-translate-y-1/2 before:transform', 'before:transition-all before:duration-300 before:ease-in-out', 'peer-checked:bg-switch-base peer-checked:before:left-[18px]', - 'peer-focus:outline peer-focus:outline-2 peer-focus:outline-blue-500' + 'peer-focus:outline peer-focus:outline-2 peer-focus:outline-blue-500', ], { @@ -19,27 +19,27 @@ const switchVariants = cva( default: 'h-[26px] w-[42px] before:h-5 before:w-5', small: [ 'h-[22px] w-9 before:left-[3px] before:h-4 before:w-4', - 'peer-checked:before:left-4' - ] - } + 'peer-checked:before:left-4', + ], + }, }, defaultVariants: { - size: 'default' - } - } -) + size: 'default', + }, + }, +); export interface SwitchProps extends VariantProps, React.HTMLAttributes { - checked?: boolean - label?: string - onChange?: (e: React.ChangeEvent) => void + checked?: boolean; + label?: string; + onChange?: (e: React.ChangeEvent) => void; } export const Switch = forwardRef(function Switch( { size, label, className, onChange = () => {}, ...props }, - ref + ref, ) { return (