From b6d42c28b57d81180a51d49803db6e59ae1bc608 Mon Sep 17 00:00:00 2001 From: Tarik Gul <47201679+TarikGul@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:32:42 -0700 Subject: [PATCH] Transition from manifest v2 to v3 for chrome (#1367) * Update manifest.json * replace `extension.getUrl` to `runtime.getUrl` * Update localStorage to async chrome.storage.local * Change action to browserAction * fix bug * revert async changes * revert test change * Fix localStorage breaking change * .extension -> .runtime * fix undefined behaviour * fixed tests * lint * Up some callbacks to async * Change where extractMetadata is called * fix in mock * Update background field to take in type * Change background.js to service_worker.js * Update id and request origin for service worker * updated chrome mock * 13 tests passing * Fix async issue * switch naming back to original * linting * Fix linting in base * remove timeOut * added permission to alarms in manifest * fixed alarms uncaught errors * Cleanup addr for tests --------- Co-authored-by: bee344 --- .../src/background/handlers/Extension.spec.ts | 19 ++--- .../src/background/handlers/Extension.ts | 24 ++++--- .../src/background/handlers/State.ts | 70 +++++++++++-------- .../src/background/handlers/Tabs.ts | 2 +- .../src/background/handlers/index.ts | 2 + .../extension-base/src/stores/Accounts.ts | 4 +- packages/extension-base/src/stores/Base.ts | 48 ++++++++----- packages/extension-mocks/src/chrome.ts | 32 ++++++++- packages/extension-mocks/src/chromeWrapper.ts | 24 +++++++ .../src/Popup/Signing/Request/SignArea.tsx | 55 ++++++++++----- packages/extension-ui/src/Popup/index.tsx | 2 + .../src/components/Toast/ToastProvider.tsx | 29 ++++++-- packages/extension/manifest.json | 25 +++++-- packages/extension/src/background.ts | 2 +- packages/extension/src/content.ts | 2 +- 15 files changed, 236 insertions(+), 104 deletions(-) create mode 100644 packages/extension-mocks/src/chromeWrapper.ts diff --git a/packages/extension-base/src/background/handlers/Extension.spec.ts b/packages/extension-base/src/background/handlers/Extension.spec.ts index 42664c77..64676d42 100644 --- a/packages/extension-base/src/background/handlers/Extension.spec.ts +++ b/packages/extension-base/src/background/handlers/Extension.spec.ts @@ -7,7 +7,7 @@ import '@polkadot/extension-mocks/chrome'; -import type { AuthUrls, ResponseSigning } from '@polkadot/extension-base/background/types'; +import type { ResponseSigning } from '@polkadot/extension-base/background/types'; import type { MetadataDef } from '@polkadot/extension-inject/types'; import type { KeyringPair } from '@polkadot/keyring/types'; import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types'; @@ -29,24 +29,15 @@ describe('Extension', () => { let tabs: Tabs; const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy'; const password = 'passw0rd'; - const address = '5FbSap4BsWfjyRhCchoVdZHkDnmDm3NEgLZ25mesq4aw2WvX'; async function createExtension (): Promise { try { await cryptoWaitReady(); keyring.loadAll({ store: new AccountsStore() }); - const authUrls: AuthUrls = {}; - authUrls['localhost:3000'] = { - authorizedAccounts: [address], - count: 0, - id: '11', - origin: 'example.com', - url: 'http://localhost:3000' - }; - localStorage.setItem('authUrls', JSON.stringify(authUrls)); state = new State(); + await state.init(); tabs = new Tabs(state); return new Extension(state); @@ -313,7 +304,7 @@ describe('Extension', () => { userExtensions }; - state.saveMetadata(meta); + await state.saveMetadata(meta); const payload: SignerPayloadJSON = { address, @@ -388,7 +379,7 @@ describe('Extension', () => { userExtensions }; - state.saveMetadata(meta); + await state.saveMetadata(meta); const registry = new TypeRegistry(); @@ -444,7 +435,7 @@ describe('Extension', () => { userExtensions }; - state.saveMetadata(meta); + await state.saveMetadata(meta); const payload = { address, diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index c497782a..6af8984c 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -116,7 +116,7 @@ export default class Extension { }; } - private accountsForget ({ address }: RequestAccountForget): boolean { + private async accountsForget ({ address }: RequestAccountForget): Promise { const authorizedAccountsDiff: AuthorizedAccountsDiff = []; // cycle through authUrls and prepare the array of diff @@ -128,12 +128,12 @@ export default class Extension { } }); - this.#state.updateAuthorizedAccounts(authorizedAccountsDiff); + await this.#state.updateAuthorizedAccounts(authorizedAccountsDiff); // cycle through default account selection for auth and remove any occurrence of the account const newDefaultAuthAccounts = this.#state.defaultAuthAccountSelection.filter((defaultSelectionAddress) => defaultSelectionAddress !== address); - this.#state.updateDefaultAuthAccounts(newDefaultAuthAccounts); + await this.#state.updateDefaultAuthAccounts(newDefaultAuthAccounts); keyring.forgetAccount(address); @@ -212,8 +212,8 @@ export default class Extension { return true; } - private authorizeUpdate ({ authorizedAccounts, url }: RequestUpdateAuthorizedAccounts): void { - return this.#state.updateAuthorizedAccounts([[url, authorizedAccounts]]); + private async authorizeUpdate ({ authorizedAccounts, url }: RequestUpdateAuthorizedAccounts): Promise { + return await this.#state.updateAuthorizedAccounts([[url, authorizedAccounts]]); } private getAuthList (): ResponseAuthorizeList { @@ -235,14 +235,14 @@ export default class Extension { return true; } - private metadataApprove ({ id }: RequestMetadataApprove): boolean { + private async metadataApprove ({ id }: RequestMetadataApprove): Promise { const queued = this.#state.getMetaRequest(id); assert(queued, 'Unable to find request'); const { request, resolve } = queued; - this.#state.saveMetadata(request); + await this.#state.saveMetadata(request); resolve(true); @@ -467,7 +467,7 @@ export default class Extension { } private windowOpen (path: AllowedPath): boolean { - const url = `${chrome.extension.getURL('index.html')}#${path}`; + const url = `${chrome.runtime.getURL('index.html')}#${path}`; if (!ALLOWED_PATH.includes(path)) { console.error('Not allowed to open the url:', url); @@ -518,8 +518,10 @@ export default class Extension { return true; } - private removeAuthorization (url: string): ResponseAuthorizeList { - return { list: this.#state.removeAuthorization(url) }; + private async removeAuthorization (url: string): Promise { + const remAuth = await this.#state.removeAuthorization(url); + + return { list: remAuth }; } private deleteAuthRequest (requestId: string): void { @@ -593,7 +595,7 @@ export default class Extension { return this.accountsValidate(request as RequestAccountValidate); case 'pri(metadata.approve)': - return this.metadataApprove(request as RequestMetadataApprove); + return await this.metadataApprove(request as RequestMetadataApprove); case 'pri(metadata.get)': return this.metadataGet(request as string); diff --git a/packages/extension-base/src/background/handlers/State.ts b/packages/extension-base/src/background/handlers/State.ts index e288cc4c..e2a1d846 100644 --- a/packages/extension-base/src/background/handlers/State.ts +++ b/packages/extension-base/src/background/handlers/State.ts @@ -59,7 +59,7 @@ interface SignRequest extends Resolver { url: string; } -const NOTIFICATION_URL = chrome.extension.getURL('notification.html'); +const NOTIFICATION_URL = chrome.runtime.getURL('notification.html'); const POPUP_WINDOW_OPTS: chrome.windows.CreateData = { focused: true, @@ -86,8 +86,8 @@ export enum NotificationOptions { const AUTH_URLS_KEY = 'authUrls'; const DEFAULT_AUTH_ACCOUNTS = 'defaultAuthAccounts'; -function extractMetadata (store: MetadataStore): void { - store.allMap((map): void => { +async function extractMetadata (store: MetadataStore): Promise { + await store.allMap(async (map): Promise => { const knownEntries = Object.entries(knownGenesis); const defs: Record = {}; const removals: string[] = []; @@ -117,13 +117,16 @@ function extractMetadata (store: MetadataStore): void { } }); - removals.forEach((key) => store.remove(key)); + for (const key of removals) { + await store.remove(key); + } + Object.values(defs).forEach(({ def }) => addMetadata(def)); }); } export default class State { - readonly #authUrls: AuthUrls = {}; + #authUrls: AuthUrls = {}; readonly #authRequests: Record = {}; @@ -155,17 +158,20 @@ export default class State { constructor (providers: Providers = {}) { this.#providers = providers; + } - extractMetadata(this.#metaStore); - + public async init () { + await extractMetadata(this.#metaStore); // retrieve previously set authorizations - const authString = localStorage.getItem(AUTH_URLS_KEY) || '{}'; + const storageAuthUrls: Record = await chrome.storage.local.get(AUTH_URLS_KEY); + const authString = storageAuthUrls?.[AUTH_URLS_KEY] || '{}'; const previousAuth = JSON.parse(authString) as AuthUrls; this.#authUrls = previousAuth; // retrieve previously set default auth accounts - const defaultAuthString = localStorage.getItem(DEFAULT_AUTH_ACCOUNTS) || '[]'; + const storageDefaultAuthAccounts: Record = await chrome.storage.local.get(DEFAULT_AUTH_ACCOUNTS); + const defaultAuthString: string = storageDefaultAuthAccounts?.[DEFAULT_AUTH_ACCOUNTS] || '[]'; const previousDefaultAuth = JSON.parse(defaultAuthString) as string[]; this.defaultAuthAccountSelection = previousDefaultAuth; @@ -209,6 +215,10 @@ export default class State { return this.#authUrls; } + private set authUrls (urls: AuthUrls) { + this.#authUrls = urls; + } + private popupClose (): void { this.#windows.forEach((id: number) => withErrorLog(() => chrome.windows.remove(id)) @@ -230,7 +240,7 @@ export default class State { } private authComplete = (id: string, resolve: (resValue: AuthResponse) => void, reject: (error: Error) => void): Resolver => { - const complete = (authorizedAccounts: string[] = []) => { + const complete = async (authorizedAccounts: string[] = []) => { const { idStr, request: { origin }, url } = this.#authRequests[id]; this.#authUrls[this.stripUrl(url)] = { @@ -241,19 +251,21 @@ export default class State { url }; - this.saveCurrentAuthList(); - this.updateDefaultAuthAccounts(authorizedAccounts); + await this.saveCurrentAuthList(); + await this.updateDefaultAuthAccounts(authorizedAccounts); delete this.#authRequests[id]; this.updateIconAuth(true); }; return { - reject: (error: Error): void => { - complete(); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + reject: async (error: Error): Promise => { + await complete(); reject(error); }, - resolve: ({ authorizedAccounts, result }: AuthResponse): void => { - complete(authorizedAccounts); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + resolve: async ({ authorizedAccounts, result }: AuthResponse): Promise => { + await complete(authorizedAccounts); resolve({ authorizedAccounts, result }); } }; @@ -289,17 +301,17 @@ export default class State { this.updateIconAuth(true); } - private saveCurrentAuthList () { - localStorage.setItem(AUTH_URLS_KEY, JSON.stringify(this.#authUrls)); + private async saveCurrentAuthList () { + await chrome.storage.local.set({ [AUTH_URLS_KEY]: JSON.stringify(this.#authUrls) }); } - private saveDefaultAuthAccounts () { - localStorage.setItem(DEFAULT_AUTH_ACCOUNTS, JSON.stringify(this.defaultAuthAccountSelection)); + private async saveDefaultAuthAccounts () { + await chrome.storage.local.set({ [DEFAULT_AUTH_ACCOUNTS]: JSON.stringify(this.defaultAuthAccountSelection) }); } - public updateDefaultAuthAccounts (newList: string[]) { + public async updateDefaultAuthAccounts (newList: string[]) { this.defaultAuthAccountSelection = newList; - this.saveDefaultAuthAccounts(); + await this.saveDefaultAuthAccounts(); } private metaComplete = (id: string, resolve: (result: boolean) => void, reject: (error: Error) => void): Resolver => { @@ -358,20 +370,20 @@ export default class State { : (signCount ? `${signCount}` : '') ); - withErrorLog(() => chrome.browserAction.setBadgeText({ text })); + withErrorLog(() => chrome.action.setBadgeText({ text })); if (shouldClose && text === '') { this.popupClose(); } } - public removeAuthorization (url: string): AuthUrls { + public async removeAuthorization (url: string): Promise { const entry = this.#authUrls[url]; assert(entry, `The source ${url} is not known`); delete this.#authUrls[url]; - this.saveCurrentAuthList(); + await this.saveCurrentAuthList(); return this.#authUrls; } @@ -391,12 +403,12 @@ export default class State { this.updateIcon(shouldClose); } - public updateAuthorizedAccounts (authorizedAccountDiff: AuthorizedAccountsDiff): void { + public async updateAuthorizedAccounts (authorizedAccountDiff: AuthorizedAccountsDiff): Promise { authorizedAccountDiff.forEach(([url, authorizedAccountDiff]) => { this.#authUrls[url].authorizedAccounts = authorizedAccountDiff; }); - this.saveCurrentAuthList(); + await this.saveCurrentAuthList(); } public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise { @@ -539,8 +551,8 @@ export default class State { return provider.unsubscribe(request.type, request.method, request.subscriptionId); } - public saveMetadata (meta: MetadataDef): void { - this.#metaStore.set(meta.genesisHash, meta); + public async saveMetadata (meta: MetadataDef): Promise { + await this.#metaStore.set(meta.genesisHash, meta); addMetadata(meta); } diff --git a/packages/extension-base/src/background/handlers/Tabs.ts b/packages/extension-base/src/background/handlers/Tabs.ts index a8783507..58fd5e2a 100644 --- a/packages/extension-base/src/background/handlers/Tabs.ts +++ b/packages/extension-base/src/background/handlers/Tabs.ts @@ -188,7 +188,7 @@ export default class Tabs { private redirectPhishingLanding (phishingWebsite: string): void { const nonFragment = phishingWebsite.split('#')[0]; const encodedWebsite = encodeURIComponent(nonFragment); - const url = `${chrome.extension.getURL('index.html')}#${PHISHING_PAGE_REDIRECT}/${encodedWebsite}`; + const url = `${chrome.runtime.getURL('index.html')}#${PHISHING_PAGE_REDIRECT}/${encodedWebsite}`; chrome.tabs.query({ url: nonFragment }, (tabs) => { tabs diff --git a/packages/extension-base/src/background/handlers/index.ts b/packages/extension-base/src/background/handlers/index.ts index b26b5d34..759cfc7f 100644 --- a/packages/extension-base/src/background/handlers/index.ts +++ b/packages/extension-base/src/background/handlers/index.ts @@ -15,6 +15,8 @@ import Tabs from './Tabs.js'; export { withErrorLog } from './helpers.js'; const state = new State(); + +await state.init(); const extension = new Extension(state); const tabs = new Tabs(state); diff --git a/packages/extension-base/src/stores/Accounts.ts b/packages/extension-base/src/stores/Accounts.ts index f54f6068..75cbc480 100644 --- a/packages/extension-base/src/stores/Accounts.ts +++ b/packages/extension-base/src/stores/Accounts.ts @@ -15,7 +15,7 @@ export default class AccountsStore extends BaseStore implements Key ); } - public override set (key: string, value: KeyringJson, update?: () => void): void { + public override async set (key: string, value: KeyringJson, update?: () => void): Promise { // shortcut, don't save testing accounts in extension storage if (key.startsWith('account:') && value.meta && value.meta.isTesting) { update && update(); @@ -23,6 +23,6 @@ export default class AccountsStore extends BaseStore implements Key return; } - super.set(key, value, update); + await super.set(key, value, update); } } diff --git a/packages/extension-base/src/stores/Base.ts b/packages/extension-base/src/stores/Base.ts index de6194e5..5af63c52 100644 --- a/packages/extension-base/src/stores/Base.ts +++ b/packages/extension-base/src/stores/Base.ts @@ -20,16 +20,19 @@ export default abstract class BaseStore { this.#prefix = prefix ? `${prefix}:` : ''; } - public all (update: (key: string, value: T) => void): void { - this.allMap((map): void => { - Object.entries(map).forEach(([key, value]): void => { - update(key, value); - }); + public async all (update: (key: string, value: T) => void): Promise { + await this.allMap(async (map): Promise => { + const entries = Object.entries(map); + + for (const [key, value] of entries) { + // eslint-disable-next-line @typescript-eslint/await-thenable + await update(key, value); + } }); } - public allMap (update: (value: Record) => void): void { - chrome.storage.local.get(null, (result: StoreValue): void => { + public async allMap (update: (value: Record) => Promise): Promise { + await chrome.storage.local.get(null).then(async (result: StoreValue) => { lastError('all'); const entries = Object.entries(result); @@ -43,37 +46,48 @@ export default abstract class BaseStore { } } - update(map); + await update(map); + }).catch(({ message }: Error) => { + console.error(`BaseStore error within allMap: ${message}`); }); } - public get (key: string, update: (value: T) => void): void { + public async get (key: string, update: (value: T) => void): Promise { const prefixedKey = `${this.#prefix}${key}`; - chrome.storage.local.get([prefixedKey], (result: StoreValue): void => { + await chrome.storage.local.get([prefixedKey]).then(async (result: StoreValue) => { lastError('get'); - update(result[prefixedKey] as T); + // eslint-disable-next-line @typescript-eslint/await-thenable + await update(result[prefixedKey] as T); + }).catch(({ message }: Error) => { + console.error(`BaseStore error within get: ${message}`); }); } - public remove (key: string, update?: () => void): void { + public async remove (key: string, update?: () => void): Promise { const prefixedKey = `${this.#prefix}${key}`; - chrome.storage.local.remove(prefixedKey, (): void => { + await chrome.storage.local.remove(prefixedKey).then(async () => { lastError('remove'); - update && update(); + // eslint-disable-next-line @typescript-eslint/await-thenable + update && await update(); + }).catch(({ message }: Error) => { + console.error(`BaseStore error within remove: ${message}`); }); } - public set (key: string, value: T, update?: () => void): void { + public async set (key: string, value: T, update?: () => void): Promise { const prefixedKey = `${this.#prefix}${key}`; - chrome.storage.local.set({ [prefixedKey]: value }, (): void => { + await chrome.storage.local.set({ [prefixedKey]: value }).then(async () => { lastError('set'); - update && update(); + // eslint-disable-next-line @typescript-eslint/await-thenable + update && await update(); + }).catch(({ message }: Error) => { + console.error(`BaseStore error within set: ${message}`); }); } } diff --git a/packages/extension-mocks/src/chrome.ts b/packages/extension-mocks/src/chrome.ts index 47e63f43..82ae2351 100644 --- a/packages/extension-mocks/src/chrome.ts +++ b/packages/extension-mocks/src/chrome.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /* eslint-disable @typescript-eslint/no-explicit-any */ -import chrome from 'sinon-chrome'; +import chrome from './chromeWrapper'; class MessagingFake { private listeners: ((...params: unknown[]) => unknown)[] = []; @@ -28,6 +28,36 @@ const messagingFake = new MessagingFake(); chrome.runtime.connect.returns(messagingFake); +chrome.storage.local.get.returns( + new Promise>((resolve, reject) => { + try { + const result = { + authUrls: JSON.stringify({ + 'localhost:3000': { + authorizedAccounts: ['5FbSap4BsWfjyRhCchoVdZHkDnmDm3NEgLZ25mesq4aw2WvX'], + count: 0, + id: '11', + origin: 'example.com', + url: 'http://localhost:3000' + } + }) + }; + + resolve(result); + } catch (error) { + reject(error); + } + })); + +chrome.storage.local.set.returns( + new Promise((resolve, reject) => { + try { + resolve(); + } catch (error) { + reject(error); + } + })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access (window as any).chrome = (globalThis as any).chrome = chrome; diff --git a/packages/extension-mocks/src/chromeWrapper.ts b/packages/extension-mocks/src/chromeWrapper.ts new file mode 100644 index 00000000..cd1d550c --- /dev/null +++ b/packages/extension-mocks/src/chromeWrapper.ts @@ -0,0 +1,24 @@ +// Copyright 2019-2024 @polkadot/extension-mocks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import sinonChrome from 'sinon-chrome'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace ChromeWrapper { + export interface IAction { + setBadgeText: (content: object) => Promise; + } + export const action: IAction = { + setBadgeText: (_: object) => { + return new Promise((resolve, _reject) => { + resolve(); + }); + } + }; +} +const extendedSinonChrome = { + ...sinonChrome, + action: ChromeWrapper.action +}; + +export default extendedSinonChrome; diff --git a/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx b/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx index 3a92ea95..d1d5814d 100644 --- a/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx +++ b/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx @@ -1,6 +1,8 @@ // Copyright 2019-2024 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +/* global chrome */ + import React, { useCallback, useContext, useEffect, useState } from 'react'; import { PASSWORD_EXPIRY_MIN } from '@polkadot/extension-base/defaults'; @@ -30,25 +32,46 @@ function SignArea ({ buttonText, className, error, isExternal, isFirst, setError const { t } = useTranslation(); useEffect(() => { - setIsLocked(null); - let timeout: ReturnType; + const lockSigner = async () => { + setIsLocked(null); + + try { + const { isLocked, remainingTime } = await isSignLocked(signId); - !isExternal && isSignLocked(signId) - .then(({ isLocked, remainingTime }) => { setIsLocked(isLocked); - timeout = setTimeout(() => { - setIsLocked(true); - }, remainingTime); - // if the account was unlocked check the remember me - // automatically to prolong the unlock period - !isLocked && setSavePass(true); - }) - .catch((error: Error) => console.error(error)); + await chrome.alarms.create('SIGNER_TIMEOUT', { delayInMinutes: remainingTime / 60000 }); - return () => { - !!timeout && clearTimeout(timeout); + if (!isLocked) { + setSavePass(true); + } + } catch (error) { + console.error('Error locking signer:', error); + } }; + + const resetAlarm = async () => { + return new Promise((resolve, reject) => { + chrome.alarms.clear('SIGNER_TIMEOUT', (cleared) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else { + resolve(cleared); + } + }); + }); + }; + + const executeLockSigner = async () => { + if (!isExternal) { + await lockSigner(); + await resetAlarm(); + } + }; + + executeLockSigner().then(async () => { + return await executeLockSigner(); + }).catch((error) => console.error('Error clearing the alarm: ', error)); }, [isExternal, signId]); const _onSign = useCallback( @@ -80,7 +103,7 @@ function SignArea ({ buttonText, className, error, isExternal, isFirst, setError const RememberPasswordCheckbox = () => ( ` +export default styled(SignArea) ` flex-direction: column; padding: 6px 24px; diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index a489029a..74a02739 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -79,6 +79,8 @@ export default function Popup (): React.ReactElement { const [settingsCtx, setSettingsCtx] = useState(startSettings); const history = useHistory(); + console.log('WINDOW; ', window); + const _onAction = useCallback( (to?: string): void => { setWelcomeDone(window.localStorage.getItem('welcome_read') === 'ok'); diff --git a/packages/extension-ui/src/components/Toast/ToastProvider.tsx b/packages/extension-ui/src/components/Toast/ToastProvider.tsx index d9b41ae5..a3ebea04 100644 --- a/packages/extension-ui/src/components/Toast/ToastProvider.tsx +++ b/packages/extension-ui/src/components/Toast/ToastProvider.tsx @@ -1,7 +1,9 @@ // Copyright 2019-2024 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useCallback, useState } from 'react'; +/* global chrome */ + +import React, { useCallback, useEffect, useState } from 'react'; import { ToastContext } from '../index.js'; import Toast from './Toast.js'; @@ -11,18 +13,37 @@ interface ToastProviderProps { } const TOAST_TIMEOUT = 1500; +const ALARM_NAME = 'toastAlarm'; const ToastProvider = ({ children }: ToastProviderProps): React.ReactElement => { const [content, setContent] = useState(''); const [visible, setVisible] = useState(false); - const show = useCallback((message: string): () => void => { - const timerId = setTimeout(() => setVisible(false), TOAST_TIMEOUT); + useEffect(() => { + const createAlarm = async () => { + try { + await chrome.alarms.create(ALARM_NAME, { delayInMinutes: TOAST_TIMEOUT / 60000 }); + chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === ALARM_NAME) { + setVisible(false); + } + }); + } catch (error) { + console.error('Error creating the alarm:', error); + throw error; + } + }; + createAlarm().then(async () => { + return await chrome.alarms.clear(ALARM_NAME); + }).catch((error) => console.error('Error clearing the alarm: ', error)); + }, []); + + const show = useCallback((message: string): (() => void) => { setContent(message); setVisible(true); - return (): void => clearTimeout(timerId); + return (): void => setVisible(false); }, []); return ( diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 202476bb..439f57f1 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -4,13 +4,13 @@ "homepage_url": "https://github.com/polkadot-js/extension", "name": "polkadot{.js} extension", "short_name": "polkadot{.js}", - "manifest_version": 2, - "permissions": ["storage", "tabs"], + "manifest_version": 3, + "permissions": ["storage", "tabs", "alarms"], "background": { - "scripts": ["background.js"], - "persistent": true + "service_worker": "background.js", + "type": "module" }, - "browser_action": { + "action": { "default_title": "polkadot{.js}", "default_popup": "index.html" }, @@ -26,6 +26,17 @@ "64": "images/icon-64.png", "128": "images/icon-128.png" }, - "web_accessible_resources": ["page.js"], - "content_security_policy": "script-src 'self' blob: 'unsafe-eval' 'wasm-eval'; object-src 'self'" + "web_accessible_resources": [ + { + "resources": [ + "page.js" + ], + "matches": [ + "*://*/*" + ] + } + ], + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + } } diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 26b691c8..962afcda 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -17,7 +17,7 @@ import { assert } from '@polkadot/util'; import { cryptoWaitReady } from '@polkadot/util-crypto'; // setup the notification (same a FF default background, white text) -withErrorLog(() => chrome.browserAction.setBadgeBackgroundColor({ color: '#d90000' })); +withErrorLog(() => chrome.action.setBadgeBackgroundColor({ color: '#d90000' })); // listen to all messages and handle appropriately chrome.runtime.onConnect.addListener((port): void => { diff --git a/packages/extension/src/content.ts b/packages/extension/src/content.ts index 2663f4e8..9cfb10f2 100644 --- a/packages/extension/src/content.ts +++ b/packages/extension/src/content.ts @@ -27,7 +27,7 @@ window.addEventListener('message', ({ data, source }: Message): void => { // inject our data injector const script = document.createElement('script'); -script.src = chrome.extension.getURL('page.js'); +script.src = chrome.runtime.getURL('page.js'); script.onload = (): void => { // remove the injecting tag when loaded