From 36a543e2c00b96f5eb6816ffadb6f4e708fb844a Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:21:22 +0100 Subject: [PATCH 01/27] Update webextension-polyfill --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index f55babe605..941cdc9232 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "typed-redux-saga": "1.5.0", "valid-url": "1.0.9", "webext-redux": "2.1.9", - "webextension-polyfill": "0.10.0" + "webextension-polyfill": "0.12.0" }, "devDependencies": { "@capacitor/cli": "6.0.0", @@ -123,7 +123,7 @@ "@types/testing-library__jest-dom": "5.14.9", "@types/valid-url": "1.0.7", "@types/w3c-web-usb": "1.0.10", - "@types/webextension-polyfill": "0.10.7", + "@types/webextension-polyfill": "0.12.1", "@typescript-eslint/eslint-plugin": "6.9.1", "@typescript-eslint/parser": "6.9.1", "babel-plugin-istanbul": "6.1.1", diff --git a/yarn.lock b/yarn.lock index 0daa7cb1d9..731713b565 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3194,10 +3194,10 @@ resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== -"@types/webextension-polyfill@0.10.7": - version "0.10.7" - resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.7.tgz#de059250599733a60ed26c8a0c81e21e11183b90" - integrity sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw== +"@types/webextension-polyfill@0.12.1": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.12.1.tgz#8dae244fe094cbb541005362e8e22f16671f6054" + integrity sha512-xPTFWwQ8BxPevPF2IKsf4hpZNss4LxaOLZXypQH4E63BDLmcwX/RMGdI4tB4VO4Nb6xDBH3F/p4gz4wvof1o9w== "@types/yargs-parser@*": version "20.2.0" @@ -9954,10 +9954,10 @@ webext-redux@2.1.9: lodash.assignin "^4.2.0" lodash.clonedeep "^4.5.0" -webextension-polyfill@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" - integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== +webextension-polyfill@0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69" + integrity sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q== webidl-conversions@^7.0.0: version "7.0.0" From 1a0a61f12fe9907f85f0f745e3bb25e90bec6d8c Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:30:38 +0100 Subject: [PATCH 02/27] Remove bg page and migrate to manifest v3 (each popup/tab has own state) --- extension/src/background.html | 6 ------ extension/src/background.ts | 6 ------ extension/src/popup/popup.tsx | 30 +++++++++++++----------------- internals/getSecurityHeaders.js | 7 ++++--- public/manifest.json | 13 +++++-------- public/oasis-xu-frame.html | 1 - 6 files changed, 22 insertions(+), 41 deletions(-) delete mode 100644 extension/src/background.html delete mode 100644 extension/src/background.ts delete mode 100644 public/oasis-xu-frame.html diff --git a/extension/src/background.html b/extension/src/background.html deleted file mode 100644 index 7838cde936..0000000000 --- a/extension/src/background.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/extension/src/background.ts b/extension/src/background.ts deleted file mode 100644 index 2a14ad0e58..0000000000 --- a/extension/src/background.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { wrapStore } from 'webext-redux' -import { configureAppStore } from 'store/configureStore' - -const store = configureAppStore() - -wrapStore(store) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 4909fa7619..e5e6a5f55b 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -2,7 +2,7 @@ import React from 'react' import { createRoot } from 'react-dom/client' import { Provider } from 'react-redux' import { HelmetProvider } from 'react-helmet-async' -import { Store } from 'webext-redux' +import { configureAppStore } from 'store/configureStore' import { createHashRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from 'styles/theme/ThemeProvider' @@ -14,21 +14,17 @@ import { routes } from './routes' const container = document.getElementById('root') as HTMLElement const root = createRoot(container!) -const store = new Store() +const store = configureAppStore() const router = createHashRouter(routes) -store.ready().then(() => { - root.render( - - - - - - - - - , - ) -}) - -console.log('popup') +root.render( + + + + + + + + + , +) diff --git a/internals/getSecurityHeaders.js b/internals/getSecurityHeaders.js index 500a7f5cf2..16c8d4adff 100644 --- a/internals/getSecurityHeaders.js +++ b/internals/getSecurityHeaders.js @@ -34,9 +34,10 @@ const getCsp = ({ isExtension, isDev }) => default-src 'none'; script-src 'self' - ${isDev ? reactErrorOverlay : ''} - ${isDev ? hmrScripts : ''} - 'report-sample'; + ${!isExtension && isDev ? reactErrorOverlay : '' /* Manifest v3 doesn't allow anything */} + ${!isExtension && isDev ? hmrScripts : ''} + ${!isExtension ? 'report-sample' : ''} + ; style-src 'self' 'unsafe-inline' diff --git a/public/manifest.json b/public/manifest.json index 7b79d32e0f..dfa81c90c4 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,7 +3,7 @@ "name": "__MSG_appName__", "short_name": "__MSG_appName__", "description": "__MSG_appDescription__", - "manifest_version": 2, + "manifest_version": 3, "version": "2.0.0", "default_locale": "en", "icons": { @@ -15,7 +15,7 @@ "128": "./Icon Blue 512.png", "512": "./Icon Blue 512.png" }, - "browser_action": { + "action": { "default_icon": { "16": "./Icon Blue 512.png", "19": "./Icon Blue 512.png", @@ -28,12 +28,9 @@ "default_title": "ROSE Wallet", "default_popup": "../extension/src/popup.html" }, - "permissions": ["storage", "notifications", "activeTab"], - "content_security_policy": "{{{ EXTENSION_CSP }}}", - "background": { - "page": "../extension/src/background.html", - "persistent": true + "permissions": ["storage", "notifications"], + "content_security_policy": { + "extension_pages": "{{{ EXTENSION_CSP }}}" }, - "web_accessible_resources": ["./oasis-xu-frame.html"], "externally_connectable": { "ids": [] } } diff --git a/public/oasis-xu-frame.html b/public/oasis-xu-frame.html deleted file mode 100644 index 0e76edd65b..0000000000 --- a/public/oasis-xu-frame.html +++ /dev/null @@ -1 +0,0 @@ - From 736928b8e2e45781969768fac5255f1decc7a3c9 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:33:29 +0100 Subject: [PATCH 03/27] iteration 1: Open ext as a single tab --- extension/src/popup/popup.tsx | 56 +++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index e5e6a5f55b..1fd0191425 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -6,25 +6,49 @@ import { configureAppStore } from 'store/configureStore' import { createHashRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from 'styles/theme/ThemeProvider' +import browser from 'webextension-polyfill' import 'locales/i18n' import 'sanitize.css/sanitize.css' import 'styles/main.css' import { routes } from './routes' -const container = document.getElementById('root') as HTMLElement -const root = createRoot(container!) -const store = configureAppStore() -const router = createHashRouter(routes) - -root.render( - - - - - - - - - , -) +// Open as a single tab: +// - single: so no need to sync between tabs +// - tab: so it doesn't constantly close like popup, and lose unlocked state +;(async () => { + const singleTabId = (await browser.runtime.getContexts({ contextTypes: ['TAB'] }))[0]?.tabId + const isTabOrPopup = (await browser.tabs.getCurrent()) ? 'tab' : 'popup' + if (isTabOrPopup === 'popup') { + if (singleTabId) { + console.log('Focus existing tab and close popup.') + await browser.tabs.update(singleTabId, { active: true }) + window.close() + } else { + console.log('Open new tab and close popup.') + await browser.tabs.create({ url: window.location.href }) + window.close() + } + } else if (browser.extension.getViews({ type: 'tab' }).length > 1) { + console.log('This is a second tab. Close and focus first one.') + await browser.tabs.update(singleTabId, { active: true }) + window.close() + } else { + console.log('This is the single tab.') + const container = document.getElementById('root') as HTMLElement + const root = createRoot(container!) + const store = configureAppStore() + const router = createHashRouter(routes) + root.render( + + + + + + + + + , + ) + } +})() From b826ddb90a0f5fc1f2a1b47e20a21eee9eb7237d Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:33:44 +0100 Subject: [PATCH 04/27] Badge show open tab --- extension/src/popup/popup.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 1fd0191425..7a68cd8163 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -35,6 +35,11 @@ import { routes } from './routes' window.close() } else { console.log('This is the single tab.') + await browser.action.setBadgeText({ text: '🗔' }) + window.addEventListener('beforeunload', () => { + browser.action.setBadgeText({ text: null }) + }) + const container = document.getElementById('root') as HTMLElement const root = createRoot(container!) const store = configureAppStore() From 69263fc9e97fc309770eb14ce8e2b6827bf477a6 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:34:00 +0100 Subject: [PATCH 05/27] Ext force profile --- src/app/components/Persist/ChoosePasswordFields.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/components/Persist/ChoosePasswordFields.tsx b/src/app/components/Persist/ChoosePasswordFields.tsx index 0d7eaae848..c754f5b7b7 100644 --- a/src/app/components/Persist/ChoosePasswordFields.tsx +++ b/src/app/components/Persist/ChoosePasswordFields.tsx @@ -9,6 +9,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { ChoosePasswordInputFields } from './ChoosePasswordInputFields' +import { runtimeIs } from 'config' export function ChoosePasswordFields() { const { t } = useTranslation() @@ -17,6 +18,8 @@ export function ChoosePasswordFields() { const hasUnpersistedAccounts = unlockedStatus === 'openUnpersisted' const [startPersisting, setStartPersisting] = useState(!hasUnpersistedAccounts) + const isExtension = runtimeIs === 'extension' + const isChoiceDisabled = isPersistenceUnsupported || unlockedStatus === 'unlockedProfile' || @@ -41,6 +44,12 @@ export function ChoosePasswordFields() { disabled: true, checked: unlockedStatus === 'unlockedProfile', } + : isExtension + ? { + disabled: true, + // Force creating a profile in Manifest v3 extension because we can't keep state in memory + checked: true, + } : { checked: startPersisting, })} From 9685f6a405465661126e80e756e3ae66b43abf95 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 22:52:31 +0100 Subject: [PATCH 06/27] iteration 2: Open ext as a single popup --- extension/src/popup/popup.tsx | 38 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 7a68cd8163..8a5fb3bf15 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -13,28 +13,46 @@ import 'sanitize.css/sanitize.css' import 'styles/main.css' import { routes } from './routes' -// Open as a single tab: +// Open as a single persistent popup: // - single: so no need to sync between tabs -// - tab: so it doesn't constantly close like popup, and lose unlocked state +// - persistent popup: so it doesn't constantly close like popup, and lose unlocked state ;(async () => { - const singleTabId = (await browser.runtime.getContexts({ contextTypes: ['TAB'] }))[0]?.tabId + // Unexpected: persistent popup is classified as 'TAB' in contexts API + const singlePersistentPopupId = (await browser.runtime.getContexts({ contextTypes: ['TAB'] }))[0]?.windowId const isTabOrPopup = (await browser.tabs.getCurrent()) ? 'tab' : 'popup' if (isTabOrPopup === 'popup') { - if (singleTabId) { - console.log('Focus existing tab and close popup.') - await browser.tabs.update(singleTabId, { active: true }) + if (singlePersistentPopupId) { + console.log('Focus existing persistent popup.') + await browser.windows.update(singlePersistentPopupId, { focused: true }) window.close() } else { - console.log('Open new tab and close popup.') - await browser.tabs.create({ url: window.location.href }) + console.log('Open new persistent popup.') + // Add slight delay so `window.screenLeft` returns popup's position, not extension's icon position + await new Promise(r => setTimeout(r, 100)) + const position = { + left: window.screenLeft, + top: window.screenTop + 30, + width: 400, + height: 600, + } + const newPopup = await browser.windows.create({ + url: window.location.href, + type: 'popup', + left: Math.max(Math.min(position.left, window.screen.width - position.width), 0), + top: Math.max(Math.min(position.top, window.screen.height - position.height), 0), + width: position.width, + height: position.height, + focused: true, + }) + await browser.windows.update(newPopup.id!, { focused: true }) // Focus again. Helps in rare cases like when screensharing. window.close() } } else if (browser.extension.getViews({ type: 'tab' }).length > 1) { console.log('This is a second tab. Close and focus first one.') - await browser.tabs.update(singleTabId, { active: true }) + await browser.windows.update(singlePersistentPopupId, { focused: true }) window.close() } else { - console.log('This is the single tab.') + console.log('This is the single persistent popup.') await browser.action.setBadgeText({ text: '🗔' }) window.addEventListener('beforeunload', () => { browser.action.setBadgeText({ text: null }) From e5dc53c9bfdd947de6530978de346362e5adf0d8 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sat, 2 Nov 2024 03:17:46 +0100 Subject: [PATCH 07/27] iteration 3: Retain encryption key between popup reopenings --- src/app/state/persist/encryption.ts | 8 ++++ src/app/state/persist/saga.ts | 57 ++++++++++++++++++++++++++++- src/app/state/persist/selectors.ts | 5 +++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/app/state/persist/encryption.ts b/src/app/state/persist/encryption.ts index e2b7e683c9..a7953bdc92 100644 --- a/src/app/state/persist/encryption.ts +++ b/src/app/state/persist/encryption.ts @@ -47,6 +47,14 @@ export async function decryptWithPassword( ): Promise { const encryptedObj = fromBase64andParse(encryptedString) const derivedKeyWithSalt = await deriveKeyFromPassword(password, encryptedObj.salt) + return await decryptWithKey(derivedKeyWithSalt, encryptedString) +} + +export async function decryptWithKey( + derivedKeyWithSalt: KeyWithSalt, + encryptedString: EncryptedString, +): Promise { + const encryptedObj = fromBase64andParse(encryptedString) const dataBytes = nacl.secretbox.open(encryptedObj.secretbox, encryptedObj.nonce, derivedKeyWithSalt.key) if (!dataBytes) throw new PasswordWrongError() diff --git a/src/app/state/persist/saga.ts b/src/app/state/persist/saga.ts index 7920dc7774..79bed15f34 100644 --- a/src/app/state/persist/saga.ts +++ b/src/app/state/persist/saga.ts @@ -4,18 +4,26 @@ import { isActionSynced } from 'redux-state-sync' import { persistActions, STORAGE_FIELD } from './index' import { base64andStringify, + decryptWithKey, decryptWithPassword, deriveKeyFromPassword, encryptWithKey, fromBase64andParse, } from './encryption' import { RootState } from 'types' -import { EncryptedString, KeyWithSalt, PersistedRootState, SetUnlockedRootStatePayload } from './types' +import { + EncryptedString, + KeyWithSalt, + PersistState, + PersistedRootState, + SetUnlockedRootStatePayload, +} from './types' import { PasswordWrongError } from 'types/errors' import { walletActions } from 'app/state/wallet' import { selectUnlockedStatus } from 'app/state/selectUnlockedStatus' import { runtimeIs } from 'config' import { backupAndDeleteV0ExtProfile, readStorageV0 } from '../../../utils/walletExtensionV0' +import { selectStringifiedEncryptionKey } from './selectors' function* watchPersistAsync() { yield* fork(function* () { @@ -211,8 +219,55 @@ function* encryptAndPersistState(action: AnyAction) { window.localStorage.setItem(STORAGE_FIELD, encryptedState) } +function* retainEncryptionKeyBetweenPopupReopenings() { + if (runtimeIs !== 'extension') return + yield* fork(function* () { + const channelQueue = yield* actionChannel('*') + let previousStringifiedEncryptionKey: PersistState['stringifiedEncryptionKey'] = undefined + while (true) { + yield* take(channelQueue) + const stringifiedEncryptionKey = yield* select(selectStringifiedEncryptionKey) + if (stringifiedEncryptionKey !== previousStringifiedEncryptionKey) { + previousStringifiedEncryptionKey = stringifiedEncryptionKey + yield* call(writeSharedExtMemory, stringifiedEncryptionKey) + } + } + }) + + yield* fork(function* () { + const encryptedState = window.localStorage.getItem( + STORAGE_FIELD, + ) as EncryptedString | null + if (!encryptedState) return // Ignore + try { + const stringifiedEncryptionKey = yield* call(readSharedExtMemory) + if (!stringifiedEncryptionKey) return // Ignore + if (stringifiedEncryptionKey === 'skipped') return // Ignore + const keyWithSalt: KeyWithSalt = fromBase64andParse(stringifiedEncryptionKey) + const persistedRootState = yield* call(decryptWithKey, keyWithSalt, encryptedState) + yield* put(persistActions.setUnlockedRootState({ persistedRootState, stringifiedEncryptionKey })) + } catch (error) { + // Ignore + } + }) +} + +async function writeSharedExtMemory(stringifiedEncryptionKey: PersistState['stringifiedEncryptionKey']) { + if (runtimeIs !== 'extension') return + const browser = await import('webextension-polyfill') + await browser.storage.session.set({ stringifiedEncryptionKey }) +} + +async function readSharedExtMemory() { + if (runtimeIs !== 'extension') return + const browser = await import('webextension-polyfill') + const storage = await browser.storage.session.get('stringifiedEncryptionKey') + return storage.stringifiedEncryptionKey as PersistState['stringifiedEncryptionKey'] +} + export function* persistSaga() { yield* watchPersistAsync() + yield* retainEncryptionKeyBetweenPopupReopenings() const storageV0 = yield* call(readStorageV0) yield* put(persistActions.setHasV0StorageToMigrate(!!storageV0?.chromeStorageLocal.keyringData)) } diff --git a/src/app/state/persist/selectors.ts b/src/app/state/persist/selectors.ts index c112c070f7..dcd6f3b435 100644 --- a/src/app/state/persist/selectors.ts +++ b/src/app/state/persist/selectors.ts @@ -20,3 +20,8 @@ export const selectIsPersistenceUnsupported = createSelector( [selectSlice], state => state.isPersistenceUnsupported, ) + +export const selectStringifiedEncryptionKey = createSelector( + [selectSlice], + state => state.stringifiedEncryptionKey, +) From e81a4b0288709ca6373ded233452a333c95ae108 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sat, 2 Nov 2024 03:19:58 +0100 Subject: [PATCH 08/27] iteration 4: revert back to non-persistent popup --- extension/src/popup/popup.tsx | 82 +++++++++-------------------------- 1 file changed, 21 insertions(+), 61 deletions(-) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 8a5fb3bf15..b2c755ba2b 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -13,65 +13,25 @@ import 'sanitize.css/sanitize.css' import 'styles/main.css' import { routes } from './routes' -// Open as a single persistent popup: +// Open as a single popup: // - single: so no need to sync between tabs -// - persistent popup: so it doesn't constantly close like popup, and lose unlocked state -;(async () => { - // Unexpected: persistent popup is classified as 'TAB' in contexts API - const singlePersistentPopupId = (await browser.runtime.getContexts({ contextTypes: ['TAB'] }))[0]?.windowId - const isTabOrPopup = (await browser.tabs.getCurrent()) ? 'tab' : 'popup' - if (isTabOrPopup === 'popup') { - if (singlePersistentPopupId) { - console.log('Focus existing persistent popup.') - await browser.windows.update(singlePersistentPopupId, { focused: true }) - window.close() - } else { - console.log('Open new persistent popup.') - // Add slight delay so `window.screenLeft` returns popup's position, not extension's icon position - await new Promise(r => setTimeout(r, 100)) - const position = { - left: window.screenLeft, - top: window.screenTop + 30, - width: 400, - height: 600, - } - const newPopup = await browser.windows.create({ - url: window.location.href, - type: 'popup', - left: Math.max(Math.min(position.left, window.screen.width - position.width), 0), - top: Math.max(Math.min(position.top, window.screen.height - position.height), 0), - width: position.width, - height: position.height, - focused: true, - }) - await browser.windows.update(newPopup.id!, { focused: true }) // Focus again. Helps in rare cases like when screensharing. - window.close() - } - } else if (browser.extension.getViews({ type: 'tab' }).length > 1) { - console.log('This is a second tab. Close and focus first one.') - await browser.windows.update(singlePersistentPopupId, { focused: true }) - window.close() - } else { - console.log('This is the single persistent popup.') - await browser.action.setBadgeText({ text: '🗔' }) - window.addEventListener('beforeunload', () => { - browser.action.setBadgeText({ text: null }) - }) - - const container = document.getElementById('root') as HTMLElement - const root = createRoot(container!) - const store = configureAppStore() - const router = createHashRouter(routes) - root.render( - - - - - - - - - , - ) - } -})() +if (browser.extension.getViews({ type: 'tab' }).length > 0) { + console.log('This is a tab. Close.') + window.close() +} else { + const container = document.getElementById('root') as HTMLElement + const root = createRoot(container!) + const store = configureAppStore() + const router = createHashRouter(routes) + root.render( + + + + + + + + + , + ) +} From 6d03d50dabc2543df3711981831bcf3380a8c6d1 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sat, 2 Nov 2024 03:27:29 +0100 Subject: [PATCH 09/27] TODO: check if ledger popup works in all iterations --- src/app/pages/OpenWalletPage/webextension.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/pages/OpenWalletPage/webextension.tsx b/src/app/pages/OpenWalletPage/webextension.tsx index 225d6ba0e2..43f1156768 100644 --- a/src/app/pages/OpenWalletPage/webextension.tsx +++ b/src/app/pages/OpenWalletPage/webextension.tsx @@ -12,6 +12,7 @@ export function FromLedgerWebExtension() { webExtensionUSBLedgerAccess={() => { navigate('/open-wallet/ledger/usb') openLedgerAccessPopup(href) + // check if ledger popup works }} /> ) From ffa9ad8869ea0c70b453a4a9ae5437f311dc72e4 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Tue, 5 Nov 2024 04:03:41 +0100 Subject: [PATCH 10/27] Disconnect redux from ConnectDevicePage --- .../__tests__/index.test.tsx | 34 +------------------ src/app/pages/ConnectDevicePage/index.tsx | 5 --- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx b/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx index 6c6513c310..2d51576f8b 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx +++ b/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx @@ -1,10 +1,6 @@ import React from 'react' -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { requestDevice } from 'app/lib/ledger' -import { importAccountsActions } from 'app/state/importaccounts' +import { render } from '@testing-library/react' import { ConnectDevicePage } from '..' -import { WalletType } from '../../../state/wallet/types' jest.mock('app/lib/ledger') @@ -20,32 +16,4 @@ describe('', () => { expect(container).toMatchSnapshot() }) - - it('should render success state', async () => { - jest.mocked(requestDevice).mockResolvedValue({} as USBDevice) - - render() - - await userEvent.click(screen.getByRole('button')) - - expect(await screen.findByText('ledger.extension.succeed')).toBeInTheDocument() - expect(screen.getByLabelText('Status is okay')).toBeInTheDocument() - expect(screen.queryByRole('button')).not.toBeInTheDocument() - expect(mockDispatch).toHaveBeenCalledWith({ - payload: WalletType.UsbLedger, - type: importAccountsActions.enumerateAccountsFromLedger.type, - }) - }) - - it('should render error state', async () => { - jest.mocked(requestDevice).mockRejectedValue(new Error('error')) - - render() - - userEvent.click(screen.getByRole('button')) - - expect(await screen.findByText('ledger.extension.failed')).toBeInTheDocument() - expect(screen.getByLabelText('Status is critical')).toBeInTheDocument() - expect(mockDispatch).not.toHaveBeenCalled() - }) }) diff --git a/src/app/pages/ConnectDevicePage/index.tsx b/src/app/pages/ConnectDevicePage/index.tsx index 6f03ab355d..4840736789 100644 --- a/src/app/pages/ConnectDevicePage/index.tsx +++ b/src/app/pages/ConnectDevicePage/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' import { Box } from 'grommet/es6/components/Box' import { Button } from 'grommet/es6/components/Button' import { Spinner } from 'grommet/es6/components/Spinner' @@ -11,10 +10,8 @@ import { Header } from 'app/components/Header' import { ErrorFormatter } from 'app/components/ErrorFormatter' import { AlertBox } from 'app/components/AlertBox' import { WalletErrors } from 'types/errors' -import { importAccountsActions } from 'app/state/importaccounts' import { requestDevice } from 'app/lib/ledger' import logotype from '../../../../public/Icon Blue 192.png' -import { WalletType } from '../../state/wallet/types' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' type ConnectionStatusIconPros = { @@ -46,7 +43,6 @@ function ConnectionStatusIcon({ success = true, label, withMargin = false }: Con export function ConnectDevicePage() { const { t } = useTranslation() - const dispatch = useDispatch() const [connection, setConnection] = useState('disconnected') const handleConnect = async () => { setConnection('connecting') @@ -54,7 +50,6 @@ export function ConnectDevicePage() { const device = await requestDevice() if (device) { setConnection('connected') - dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) } } catch { setConnection('error') From 17061b087bf46201f25c8241b497e675a45f6f09 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Tue, 5 Nov 2024 04:04:08 +0100 Subject: [PATCH 11/27] fixup! Remove bg page and migrate to manifest v3 (each popup/tab has own state) --- src/utils/webextension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index dd260904e3..efb257aa4c 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -8,7 +8,7 @@ type Props = { } const getPopupUrl = (path: string) => - browser.runtime.getURL(`${browser.runtime.getManifest()?.browser_action?.default_popup}${path}`) + browser.runtime.getURL(`${browser.runtime.getManifest()?.action?.default_popup}${path}`) const openPopup = ({ path, height, width, type }: Props) => { const existingPopupWindow = browser.extension From 1fb1a159e8caf4f20259d6472b62e95cc595e3b2 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Tue, 5 Nov 2024 04:15:23 +0100 Subject: [PATCH 12/27] Rename ConnectDevicePage to ExtensionRequestLedgerPermissionPopup --- extension/src/popup/routes.tsx | 6 +++--- .../__tests__/__snapshots__/index.test.tsx.snap | 2 +- .../__tests__/index.test.tsx | 6 +++--- .../index.tsx | 2 +- src/app/pages/OpenWalletPage/webextension.tsx | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename src/app/pages/{ConnectDevicePage => ExtensionRequestLedgerPermissionPopup}/__tests__/__snapshots__/index.test.tsx.snap (98%) rename src/app/pages/{ConnectDevicePage => ExtensionRequestLedgerPermissionPopup}/__tests__/index.test.tsx (62%) rename src/app/pages/{ConnectDevicePage => ExtensionRequestLedgerPermissionPopup}/index.tsx (98%) diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index 1f54fd8432..ff091c5509 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -1,7 +1,7 @@ import React from 'react' import { RouteObject } from 'react-router-dom' import { App } from 'app' -import { ConnectDevicePage } from 'app/pages/ConnectDevicePage' +import { ExtensionRequestLedgerPermissionPopup } from 'app/pages/ExtensionRequestLedgerPermissionPopup' import { FromLedgerWebExtension } from 'app/pages/OpenWalletPage/webextension' import { commonRoutes } from '../../../src/commonRoutes' import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage' @@ -23,7 +23,7 @@ export const routes: RouteObject[] = [ ], }, { - path: 'open-wallet/connect-device', - element: , + path: 'extension-request-ledger-permission-popup', + element: , }, ] diff --git a/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap b/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap similarity index 98% rename from src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap rename to src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap index f48aea18e0..a0f727306e 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render component 1`] = ` +exports[` should render component 1`] = ` .c0 { display: -webkit-box; display: -webkit-flex; diff --git a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx b/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx similarity index 62% rename from src/app/pages/ConnectDevicePage/__tests__/index.test.tsx rename to src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx index 2d51576f8b..e5f008c250 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx +++ b/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { render } from '@testing-library/react' -import { ConnectDevicePage } from '..' +import { ExtensionRequestLedgerPermissionPopup } from '..' jest.mock('app/lib/ledger') @@ -10,9 +10,9 @@ jest.mock('react-redux', () => ({ useDispatch: () => mockDispatch, })) -describe('', () => { +describe('', () => { it('should render component', () => { - const { container } = render() + const { container } = render() expect(container).toMatchSnapshot() }) diff --git a/src/app/pages/ConnectDevicePage/index.tsx b/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx similarity index 98% rename from src/app/pages/ConnectDevicePage/index.tsx rename to src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx index 4840736789..8c171a9633 100644 --- a/src/app/pages/ConnectDevicePage/index.tsx +++ b/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx @@ -41,7 +41,7 @@ function ConnectionStatusIcon({ success = true, label, withMargin = false }: Con ) } -export function ConnectDevicePage() { +export function ExtensionRequestLedgerPermissionPopup() { const { t } = useTranslation() const [connection, setConnection] = useState('disconnected') const handleConnect = async () => { diff --git a/src/app/pages/OpenWalletPage/webextension.tsx b/src/app/pages/OpenWalletPage/webextension.tsx index 43f1156768..fa9778111f 100644 --- a/src/app/pages/OpenWalletPage/webextension.tsx +++ b/src/app/pages/OpenWalletPage/webextension.tsx @@ -4,7 +4,7 @@ import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' export function FromLedgerWebExtension() { - const href = useHref('/open-wallet/connect-device') + const href = useHref('/extension-request-ledger-permission-popup') const navigate = useNavigate() return ( From 3d5876b380fc299bc59d213fe3627f5e314269d7 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Tue, 5 Nov 2024 06:06:48 +0100 Subject: [PATCH 13/27] rename component --- extension/src/popup/routes.tsx | 2 +- .../{index.tsx => ExtensionRequestLedgerPermissionPopup.tsx} | 0 .../__tests__/index.test.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/app/pages/ExtensionRequestLedgerPermissionPopup/{index.tsx => ExtensionRequestLedgerPermissionPopup.tsx} (100%) diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index ff091c5509..7dbc24ac60 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -1,7 +1,7 @@ import React from 'react' import { RouteObject } from 'react-router-dom' import { App } from 'app' -import { ExtensionRequestLedgerPermissionPopup } from 'app/pages/ExtensionRequestLedgerPermissionPopup' +import { ExtensionRequestLedgerPermissionPopup } from 'app/pages/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup' import { FromLedgerWebExtension } from 'app/pages/OpenWalletPage/webextension' import { commonRoutes } from '../../../src/commonRoutes' import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage' diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx b/src/app/pages/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx similarity index 100% rename from src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx rename to src/app/pages/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx b/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx index e5f008c250..3cec20e28f 100644 --- a/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx +++ b/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { render } from '@testing-library/react' -import { ExtensionRequestLedgerPermissionPopup } from '..' +import { ExtensionRequestLedgerPermissionPopup } from '../ExtensionRequestLedgerPermissionPopup' jest.mock('app/lib/ledger') From 7bfe2e2f22c3b59a4f85343809f6328e3b8e10f7 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Tue, 5 Nov 2024 06:41:07 +0100 Subject: [PATCH 14/27] ExtensionRequestLedgerPermissionPopup as separate entry --- extension/src/popup/routes.tsx | 5 ---- public/manifest.json | 6 ++++- .../index.html | 16 +++++++++++ .../index.tsx | 27 +++++++++++++++++++ src/app/pages/OpenWalletPage/webextension.tsx | 6 +---- src/index.tsx | 4 +-- src/utils/webextension.ts | 8 +++--- 7 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html create mode 100644 src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index 7dbc24ac60..afb086a8f4 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -1,7 +1,6 @@ import React from 'react' import { RouteObject } from 'react-router-dom' import { App } from 'app' -import { ExtensionRequestLedgerPermissionPopup } from 'app/pages/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup' import { FromLedgerWebExtension } from 'app/pages/OpenWalletPage/webextension' import { commonRoutes } from '../../../src/commonRoutes' import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage' @@ -22,8 +21,4 @@ export const routes: RouteObject[] = [ }, ], }, - { - path: 'extension-request-ledger-permission-popup', - element: , - }, ] diff --git a/public/manifest.json b/public/manifest.json index dfa81c90c4..982f4a2f22 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -32,5 +32,9 @@ "content_security_policy": { "extension_pages": "{{{ EXTENSION_CSP }}}" }, - "externally_connectable": { "ids": [] } + "externally_connectable": { "ids": [] }, + "web_accessible_resources": [{ + "matches": [], + "resources": ["./src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html"] + }] } diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html b/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html new file mode 100644 index 0000000000..a584a4749f --- /dev/null +++ b/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html @@ -0,0 +1,16 @@ + + + + + + + + + ROSE Wallet + + + +
+ + + diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx b/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx new file mode 100644 index 0000000000..18855676f4 --- /dev/null +++ b/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx @@ -0,0 +1,27 @@ +import 'react-app-polyfill/stable' + +import * as React from 'react' +import { createRoot } from 'react-dom/client' + +// Use consistent styling +import 'sanitize.css/sanitize.css' + +import { ThemeProvider } from 'styles/theme/ThemeProvider' + +// Initialize languages +import 'locales/i18n' + +// Fonts +import 'styles/main.css' +import { ExtensionRequestLedgerPermissionPopup } from './ExtensionRequestLedgerPermissionPopup' + +const container = document.getElementById('root') as HTMLElement +const root = createRoot(container!) + +root.render( + + + + + , +) diff --git a/src/app/pages/OpenWalletPage/webextension.tsx b/src/app/pages/OpenWalletPage/webextension.tsx index fa9778111f..a4622437f6 100644 --- a/src/app/pages/OpenWalletPage/webextension.tsx +++ b/src/app/pages/OpenWalletPage/webextension.tsx @@ -1,18 +1,14 @@ import React from 'react' -import { useHref, useNavigate } from 'react-router-dom' import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' export function FromLedgerWebExtension() { - const href = useHref('/extension-request-ledger-permission-popup') - const navigate = useNavigate() + const href = new URL('../ExtensionRequestLedgerPermissionPopup/index.html', import.meta.url).href return ( { - navigate('/open-wallet/ledger/usb') openLedgerAccessPopup(href) - // check if ledger popup works }} /> ) diff --git a/src/index.tsx b/src/index.tsx index eef5f41714..b2100856cb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,10 +22,10 @@ import { configureAppStore } from 'store/configureStore' import { ThemeProvider } from 'styles/theme/ThemeProvider' // Initialize languages -import './locales/i18n' +import 'locales/i18n' // Fonts -import './styles/main.css' +import 'styles/main.css' import { routes } from './routes' const store = configureAppStore() diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index efb257aa4c..b8d6b0c317 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -7,19 +7,17 @@ type Props = { type?: browser.Windows.CreateType } -const getPopupUrl = (path: string) => +export const getPopupUrl = (path: string) => browser.runtime.getURL(`${browser.runtime.getManifest()?.action?.default_popup}${path}`) const openPopup = ({ path, height, width, type }: Props) => { - const existingPopupWindow = browser.extension - .getViews() - .find(window => window.location.href === getPopupUrl(path)) + const existingPopupWindow = browser.extension.getViews().find(window => window.location.href === path) if (existingPopupWindow) { existingPopupWindow.close() } browser.windows.create({ - url: getPopupUrl(path), + url: path, type: type ?? 'popup', width: width, height: height, From 6fcd145fd4fcbe4358c2592050da7b679e4ad3f0 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 00:53:18 +0100 Subject: [PATCH 15/27] move --- .../ExtensionRequestLedgerPermissionPopup.tsx | 2 +- .../__tests__/__snapshots__/index.test.tsx.snap | 0 .../__tests__/index.test.tsx | 0 .../src}/ExtensionRequestLedgerPermissionPopup/index.html | 0 .../src}/ExtensionRequestLedgerPermissionPopup/index.tsx | 0 public/manifest.json | 2 +- src/app/pages/OpenWalletPage/webextension.tsx | 8 +++++++- 7 files changed, 9 insertions(+), 3 deletions(-) rename {src/app/pages => extension/src}/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx (98%) rename {src/app/pages => extension/src}/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap (100%) rename {src/app/pages => extension/src}/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx (100%) rename {src/app/pages => extension/src}/ExtensionRequestLedgerPermissionPopup/index.html (100%) rename {src/app/pages => extension/src}/ExtensionRequestLedgerPermissionPopup/index.tsx (100%) diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx similarity index 98% rename from src/app/pages/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx rename to extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx index 8c171a9633..38e1bd29c3 100644 --- a/src/app/pages/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx @@ -11,7 +11,7 @@ import { ErrorFormatter } from 'app/components/ErrorFormatter' import { AlertBox } from 'app/components/AlertBox' import { WalletErrors } from 'types/errors' import { requestDevice } from 'app/lib/ledger' -import logotype from '../../../../public/Icon Blue 192.png' +import logotype from '../../../public/Icon Blue 192.png' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' type ConnectionStatusIconPros = { diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap similarity index 100% rename from src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap rename to extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx similarity index 100% rename from src/app/pages/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx rename to extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html b/extension/src/ExtensionRequestLedgerPermissionPopup/index.html similarity index 100% rename from src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html rename to extension/src/ExtensionRequestLedgerPermissionPopup/index.html diff --git a/src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx similarity index 100% rename from src/app/pages/ExtensionRequestLedgerPermissionPopup/index.tsx rename to extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx diff --git a/public/manifest.json b/public/manifest.json index 982f4a2f22..e238ac485e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -35,6 +35,6 @@ "externally_connectable": { "ids": [] }, "web_accessible_resources": [{ "matches": [], - "resources": ["./src/app/pages/ExtensionRequestLedgerPermissionPopup/index.html"] + "resources": ["../extension/src/ExtensionRequestLedgerPermissionPopup/index.html"] }] } diff --git a/src/app/pages/OpenWalletPage/webextension.tsx b/src/app/pages/OpenWalletPage/webextension.tsx index a4622437f6..3d16605bb3 100644 --- a/src/app/pages/OpenWalletPage/webextension.tsx +++ b/src/app/pages/OpenWalletPage/webextension.tsx @@ -1,9 +1,15 @@ import React from 'react' import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { ExtensionRequestLedgerPermissionPopup } from '../../../../extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup' export function FromLedgerWebExtension() { - const href = new URL('../ExtensionRequestLedgerPermissionPopup/index.html', import.meta.url).href + /** See {@link ExtensionRequestLedgerPermissionPopup} */ + const href = new URL( + '../../../../extension/src/ExtensionRequestLedgerPermissionPopup/index.html', + import.meta.url, + ).href return ( Date: Wed, 6 Nov 2024 01:09:18 +0100 Subject: [PATCH 16/27] Disconnect redux from theme ExtensionRequestLedgerPermissionPopup (also fixes compile) --- .../ExtensionRequestLedgerPermissionPopup/index.tsx | 8 +++++--- src/styles/theme/ThemeProvider.tsx | 13 ++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx index 18855676f4..6a1aaee387 100644 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx @@ -6,7 +6,7 @@ import { createRoot } from 'react-dom/client' // Use consistent styling import 'sanitize.css/sanitize.css' -import { ThemeProvider } from 'styles/theme/ThemeProvider' +import { ThemeProviderWithoutRedux } from 'styles/theme/ThemeProvider' // Initialize languages import 'locales/i18n' @@ -19,9 +19,11 @@ const container = document.getElementById('root') as HTMLElement const root = createRoot(container!) root.render( - + // Avoid redux: it's not necessary and has it has a little potential to cause + // conflicts in stored state because it runs in parallel with wallet popup. + - , + , ) diff --git a/src/styles/theme/ThemeProvider.tsx b/src/styles/theme/ThemeProvider.tsx index 03c2cfa979..556488d2bb 100644 --- a/src/styles/theme/ThemeProvider.tsx +++ b/src/styles/theme/ThemeProvider.tsx @@ -9,6 +9,7 @@ import { selectTheme } from './slice/selectors' import { dataTableTheme } from './dataTableTheme' import { css } from 'styled-components' import { getTargetTheme } from './utils' +import { getInitialState as getInitialThemeState } from './slice' /** * React-data-table by default sets its own background and text colors @@ -382,7 +383,7 @@ const grommetCustomTheme: ThemeType = { }, }, } -export const ThemeProvider = (props: { children: React.ReactChild }) => { +export const ThemeProvider = (props: { children: React.ReactNode }) => { const theme = deepMerge(grommet, grommetCustomTheme) const mode = useSelector(selectTheme) @@ -392,3 +393,13 @@ export const ThemeProvider = (props: { children: React.ReactChild }) => { ) } +export const ThemeProviderWithoutRedux = (props: { children: React.ReactNode }) => { + const theme = deepMerge(grommet, grommetCustomTheme) + const mode = getInitialThemeState().selected + + return ( + + {React.Children.only(props.children)} + + ) +} From 7671cb9ed75c015fb296ddd8f04d7f754c6906b1 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 02:41:48 +0100 Subject: [PATCH 17/27] focus ExtensionRequestLedgerPermissionPopup if it exists when user clicks wallet again --- extension/src/popup/popup.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index b2c755ba2b..3e7d492018 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -16,8 +16,18 @@ import { routes } from './routes' // Open as a single popup: // - single: so no need to sync between tabs if (browser.extension.getViews({ type: 'tab' }).length > 0) { - console.log('This is a tab. Close.') - window.close() + // Either this is a tab, or something else is. + // If this is a tab, just close. + // If something else is a tab, it must be ExtensionRequestLedgerPermissionPopup. Focus that and close self. + ;(async () => { + // Unexpected: persistent popup is classified as 'TAB' in contexts API + const tabsAndPersistentPopups = await browser.runtime.getContexts({ contextTypes: ['TAB'] }) + for (const c of tabsAndPersistentPopups) { + await browser.windows.update(c.windowId, { focused: true }) + await browser.tabs.update(c.tabId, { active: true }) + } + window.close() + })() } else { const container = document.getElementById('root') as HTMLElement const root = createRoot(container!) From a8f474e0e8c49835adaa801634120597aa228531 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 03:31:48 +0100 Subject: [PATCH 18/27] Revert to 1a0a61f12fe9907f85f0f745e3bb25e90bec6d8c --- .../__tests__/index.test.tsx | 19 ------- .../index.html | 16 ------ .../index.tsx | 29 ---------- extension/src/popup/popup.tsx | 49 ++++++---------- extension/src/popup/routes.tsx | 5 ++ public/manifest.json | 6 +- .../Persist/ChoosePasswordFields.tsx | 9 --- .../__snapshots__/index.test.tsx.snap | 2 +- .../__tests__/index.test.tsx | 51 +++++++++++++++++ .../app/pages/ConnectDevicePage/index.tsx | 9 ++- src/app/pages/OpenWalletPage/webextension.tsx | 11 ++-- src/app/state/persist/encryption.ts | 8 --- src/app/state/persist/saga.ts | 57 +------------------ src/app/state/persist/selectors.ts | 5 -- src/index.tsx | 4 +- src/styles/theme/ThemeProvider.tsx | 13 +---- src/utils/webextension.ts | 10 ++-- 17 files changed, 95 insertions(+), 208 deletions(-) delete mode 100644 extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx delete mode 100644 extension/src/ExtensionRequestLedgerPermissionPopup/index.html delete mode 100644 extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx rename {extension/src/ExtensionRequestLedgerPermissionPopup => src/app/pages/ConnectDevicePage}/__tests__/__snapshots__/index.test.tsx.snap (98%) create mode 100644 src/app/pages/ConnectDevicePage/__tests__/index.test.tsx rename extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx => src/app/pages/ConnectDevicePage/index.tsx (91%) diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx deleted file mode 100644 index 3cec20e28f..0000000000 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { render } from '@testing-library/react' -import { ExtensionRequestLedgerPermissionPopup } from '../ExtensionRequestLedgerPermissionPopup' - -jest.mock('app/lib/ledger') - -const mockDispatch = jest.fn() -jest.mock('react-redux', () => ({ - useSelector: jest.fn(), - useDispatch: () => mockDispatch, -})) - -describe('', () => { - it('should render component', () => { - const { container } = render() - - expect(container).toMatchSnapshot() - }) -}) diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/index.html b/extension/src/ExtensionRequestLedgerPermissionPopup/index.html deleted file mode 100644 index a584a4749f..0000000000 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - ROSE Wallet - - - -
- - - diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx deleted file mode 100644 index 6a1aaee387..0000000000 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import 'react-app-polyfill/stable' - -import * as React from 'react' -import { createRoot } from 'react-dom/client' - -// Use consistent styling -import 'sanitize.css/sanitize.css' - -import { ThemeProviderWithoutRedux } from 'styles/theme/ThemeProvider' - -// Initialize languages -import 'locales/i18n' - -// Fonts -import 'styles/main.css' -import { ExtensionRequestLedgerPermissionPopup } from './ExtensionRequestLedgerPermissionPopup' - -const container = document.getElementById('root') as HTMLElement -const root = createRoot(container!) - -root.render( - // Avoid redux: it's not necessary and has it has a little potential to cause - // conflicts in stored state because it runs in parallel with wallet popup. - - - - - , -) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 3e7d492018..e5e6a5f55b 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -6,42 +6,25 @@ import { configureAppStore } from 'store/configureStore' import { createHashRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from 'styles/theme/ThemeProvider' -import browser from 'webextension-polyfill' import 'locales/i18n' import 'sanitize.css/sanitize.css' import 'styles/main.css' import { routes } from './routes' -// Open as a single popup: -// - single: so no need to sync between tabs -if (browser.extension.getViews({ type: 'tab' }).length > 0) { - // Either this is a tab, or something else is. - // If this is a tab, just close. - // If something else is a tab, it must be ExtensionRequestLedgerPermissionPopup. Focus that and close self. - ;(async () => { - // Unexpected: persistent popup is classified as 'TAB' in contexts API - const tabsAndPersistentPopups = await browser.runtime.getContexts({ contextTypes: ['TAB'] }) - for (const c of tabsAndPersistentPopups) { - await browser.windows.update(c.windowId, { focused: true }) - await browser.tabs.update(c.tabId, { active: true }) - } - window.close() - })() -} else { - const container = document.getElementById('root') as HTMLElement - const root = createRoot(container!) - const store = configureAppStore() - const router = createHashRouter(routes) - root.render( - - - - - - - - - , - ) -} +const container = document.getElementById('root') as HTMLElement +const root = createRoot(container!) +const store = configureAppStore() +const router = createHashRouter(routes) + +root.render( + + + + + + + + + , +) diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index afb086a8f4..1f54fd8432 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -1,6 +1,7 @@ import React from 'react' import { RouteObject } from 'react-router-dom' import { App } from 'app' +import { ConnectDevicePage } from 'app/pages/ConnectDevicePage' import { FromLedgerWebExtension } from 'app/pages/OpenWalletPage/webextension' import { commonRoutes } from '../../../src/commonRoutes' import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage' @@ -21,4 +22,8 @@ export const routes: RouteObject[] = [ }, ], }, + { + path: 'open-wallet/connect-device', + element: , + }, ] diff --git a/public/manifest.json b/public/manifest.json index e238ac485e..dfa81c90c4 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -32,9 +32,5 @@ "content_security_policy": { "extension_pages": "{{{ EXTENSION_CSP }}}" }, - "externally_connectable": { "ids": [] }, - "web_accessible_resources": [{ - "matches": [], - "resources": ["../extension/src/ExtensionRequestLedgerPermissionPopup/index.html"] - }] + "externally_connectable": { "ids": [] } } diff --git a/src/app/components/Persist/ChoosePasswordFields.tsx b/src/app/components/Persist/ChoosePasswordFields.tsx index c754f5b7b7..0d7eaae848 100644 --- a/src/app/components/Persist/ChoosePasswordFields.tsx +++ b/src/app/components/Persist/ChoosePasswordFields.tsx @@ -9,7 +9,6 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { ChoosePasswordInputFields } from './ChoosePasswordInputFields' -import { runtimeIs } from 'config' export function ChoosePasswordFields() { const { t } = useTranslation() @@ -18,8 +17,6 @@ export function ChoosePasswordFields() { const hasUnpersistedAccounts = unlockedStatus === 'openUnpersisted' const [startPersisting, setStartPersisting] = useState(!hasUnpersistedAccounts) - const isExtension = runtimeIs === 'extension' - const isChoiceDisabled = isPersistenceUnsupported || unlockedStatus === 'unlockedProfile' || @@ -44,12 +41,6 @@ export function ChoosePasswordFields() { disabled: true, checked: unlockedStatus === 'unlockedProfile', } - : isExtension - ? { - disabled: true, - // Force creating a profile in Manifest v3 extension because we can't keep state in memory - checked: true, - } : { checked: startPersisting, })} diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap b/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap similarity index 98% rename from extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap rename to src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap index a0f727306e..f48aea18e0 100644 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render component 1`] = ` +exports[` should render component 1`] = ` .c0 { display: -webkit-box; display: -webkit-flex; diff --git a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx b/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx new file mode 100644 index 0000000000..6c6513c310 --- /dev/null +++ b/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { requestDevice } from 'app/lib/ledger' +import { importAccountsActions } from 'app/state/importaccounts' +import { ConnectDevicePage } from '..' +import { WalletType } from '../../../state/wallet/types' + +jest.mock('app/lib/ledger') + +const mockDispatch = jest.fn() +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), + useDispatch: () => mockDispatch, +})) + +describe('', () => { + it('should render component', () => { + const { container } = render() + + expect(container).toMatchSnapshot() + }) + + it('should render success state', async () => { + jest.mocked(requestDevice).mockResolvedValue({} as USBDevice) + + render() + + await userEvent.click(screen.getByRole('button')) + + expect(await screen.findByText('ledger.extension.succeed')).toBeInTheDocument() + expect(screen.getByLabelText('Status is okay')).toBeInTheDocument() + expect(screen.queryByRole('button')).not.toBeInTheDocument() + expect(mockDispatch).toHaveBeenCalledWith({ + payload: WalletType.UsbLedger, + type: importAccountsActions.enumerateAccountsFromLedger.type, + }) + }) + + it('should render error state', async () => { + jest.mocked(requestDevice).mockRejectedValue(new Error('error')) + + render() + + userEvent.click(screen.getByRole('button')) + + expect(await screen.findByText('ledger.extension.failed')).toBeInTheDocument() + expect(screen.getByLabelText('Status is critical')).toBeInTheDocument() + expect(mockDispatch).not.toHaveBeenCalled() + }) +}) diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx b/src/app/pages/ConnectDevicePage/index.tsx similarity index 91% rename from extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx rename to src/app/pages/ConnectDevicePage/index.tsx index 38e1bd29c3..6f03ab355d 100644 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx +++ b/src/app/pages/ConnectDevicePage/index.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' import { Box } from 'grommet/es6/components/Box' import { Button } from 'grommet/es6/components/Button' import { Spinner } from 'grommet/es6/components/Spinner' @@ -10,8 +11,10 @@ import { Header } from 'app/components/Header' import { ErrorFormatter } from 'app/components/ErrorFormatter' import { AlertBox } from 'app/components/AlertBox' import { WalletErrors } from 'types/errors' +import { importAccountsActions } from 'app/state/importaccounts' import { requestDevice } from 'app/lib/ledger' -import logotype from '../../../public/Icon Blue 192.png' +import logotype from '../../../../public/Icon Blue 192.png' +import { WalletType } from '../../state/wallet/types' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' type ConnectionStatusIconPros = { @@ -41,8 +44,9 @@ function ConnectionStatusIcon({ success = true, label, withMargin = false }: Con ) } -export function ExtensionRequestLedgerPermissionPopup() { +export function ConnectDevicePage() { const { t } = useTranslation() + const dispatch = useDispatch() const [connection, setConnection] = useState('disconnected') const handleConnect = async () => { setConnection('connecting') @@ -50,6 +54,7 @@ export function ExtensionRequestLedgerPermissionPopup() { const device = await requestDevice() if (device) { setConnection('connected') + dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) } } catch { setConnection('error') diff --git a/src/app/pages/OpenWalletPage/webextension.tsx b/src/app/pages/OpenWalletPage/webextension.tsx index 3d16605bb3..225d6ba0e2 100644 --- a/src/app/pages/OpenWalletPage/webextension.tsx +++ b/src/app/pages/OpenWalletPage/webextension.tsx @@ -1,19 +1,16 @@ import React from 'react' +import { useHref, useNavigate } from 'react-router-dom' import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { ExtensionRequestLedgerPermissionPopup } from '../../../../extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup' export function FromLedgerWebExtension() { - /** See {@link ExtensionRequestLedgerPermissionPopup} */ - const href = new URL( - '../../../../extension/src/ExtensionRequestLedgerPermissionPopup/index.html', - import.meta.url, - ).href + const href = useHref('/open-wallet/connect-device') + const navigate = useNavigate() return ( { + navigate('/open-wallet/ledger/usb') openLedgerAccessPopup(href) }} /> diff --git a/src/app/state/persist/encryption.ts b/src/app/state/persist/encryption.ts index a7953bdc92..e2b7e683c9 100644 --- a/src/app/state/persist/encryption.ts +++ b/src/app/state/persist/encryption.ts @@ -47,14 +47,6 @@ export async function decryptWithPassword( ): Promise { const encryptedObj = fromBase64andParse(encryptedString) const derivedKeyWithSalt = await deriveKeyFromPassword(password, encryptedObj.salt) - return await decryptWithKey(derivedKeyWithSalt, encryptedString) -} - -export async function decryptWithKey( - derivedKeyWithSalt: KeyWithSalt, - encryptedString: EncryptedString, -): Promise { - const encryptedObj = fromBase64andParse(encryptedString) const dataBytes = nacl.secretbox.open(encryptedObj.secretbox, encryptedObj.nonce, derivedKeyWithSalt.key) if (!dataBytes) throw new PasswordWrongError() diff --git a/src/app/state/persist/saga.ts b/src/app/state/persist/saga.ts index 79bed15f34..7920dc7774 100644 --- a/src/app/state/persist/saga.ts +++ b/src/app/state/persist/saga.ts @@ -4,26 +4,18 @@ import { isActionSynced } from 'redux-state-sync' import { persistActions, STORAGE_FIELD } from './index' import { base64andStringify, - decryptWithKey, decryptWithPassword, deriveKeyFromPassword, encryptWithKey, fromBase64andParse, } from './encryption' import { RootState } from 'types' -import { - EncryptedString, - KeyWithSalt, - PersistState, - PersistedRootState, - SetUnlockedRootStatePayload, -} from './types' +import { EncryptedString, KeyWithSalt, PersistedRootState, SetUnlockedRootStatePayload } from './types' import { PasswordWrongError } from 'types/errors' import { walletActions } from 'app/state/wallet' import { selectUnlockedStatus } from 'app/state/selectUnlockedStatus' import { runtimeIs } from 'config' import { backupAndDeleteV0ExtProfile, readStorageV0 } from '../../../utils/walletExtensionV0' -import { selectStringifiedEncryptionKey } from './selectors' function* watchPersistAsync() { yield* fork(function* () { @@ -219,55 +211,8 @@ function* encryptAndPersistState(action: AnyAction) { window.localStorage.setItem(STORAGE_FIELD, encryptedState) } -function* retainEncryptionKeyBetweenPopupReopenings() { - if (runtimeIs !== 'extension') return - yield* fork(function* () { - const channelQueue = yield* actionChannel('*') - let previousStringifiedEncryptionKey: PersistState['stringifiedEncryptionKey'] = undefined - while (true) { - yield* take(channelQueue) - const stringifiedEncryptionKey = yield* select(selectStringifiedEncryptionKey) - if (stringifiedEncryptionKey !== previousStringifiedEncryptionKey) { - previousStringifiedEncryptionKey = stringifiedEncryptionKey - yield* call(writeSharedExtMemory, stringifiedEncryptionKey) - } - } - }) - - yield* fork(function* () { - const encryptedState = window.localStorage.getItem( - STORAGE_FIELD, - ) as EncryptedString | null - if (!encryptedState) return // Ignore - try { - const stringifiedEncryptionKey = yield* call(readSharedExtMemory) - if (!stringifiedEncryptionKey) return // Ignore - if (stringifiedEncryptionKey === 'skipped') return // Ignore - const keyWithSalt: KeyWithSalt = fromBase64andParse(stringifiedEncryptionKey) - const persistedRootState = yield* call(decryptWithKey, keyWithSalt, encryptedState) - yield* put(persistActions.setUnlockedRootState({ persistedRootState, stringifiedEncryptionKey })) - } catch (error) { - // Ignore - } - }) -} - -async function writeSharedExtMemory(stringifiedEncryptionKey: PersistState['stringifiedEncryptionKey']) { - if (runtimeIs !== 'extension') return - const browser = await import('webextension-polyfill') - await browser.storage.session.set({ stringifiedEncryptionKey }) -} - -async function readSharedExtMemory() { - if (runtimeIs !== 'extension') return - const browser = await import('webextension-polyfill') - const storage = await browser.storage.session.get('stringifiedEncryptionKey') - return storage.stringifiedEncryptionKey as PersistState['stringifiedEncryptionKey'] -} - export function* persistSaga() { yield* watchPersistAsync() - yield* retainEncryptionKeyBetweenPopupReopenings() const storageV0 = yield* call(readStorageV0) yield* put(persistActions.setHasV0StorageToMigrate(!!storageV0?.chromeStorageLocal.keyringData)) } diff --git a/src/app/state/persist/selectors.ts b/src/app/state/persist/selectors.ts index dcd6f3b435..c112c070f7 100644 --- a/src/app/state/persist/selectors.ts +++ b/src/app/state/persist/selectors.ts @@ -20,8 +20,3 @@ export const selectIsPersistenceUnsupported = createSelector( [selectSlice], state => state.isPersistenceUnsupported, ) - -export const selectStringifiedEncryptionKey = createSelector( - [selectSlice], - state => state.stringifiedEncryptionKey, -) diff --git a/src/index.tsx b/src/index.tsx index b2100856cb..eef5f41714 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,10 +22,10 @@ import { configureAppStore } from 'store/configureStore' import { ThemeProvider } from 'styles/theme/ThemeProvider' // Initialize languages -import 'locales/i18n' +import './locales/i18n' // Fonts -import 'styles/main.css' +import './styles/main.css' import { routes } from './routes' const store = configureAppStore() diff --git a/src/styles/theme/ThemeProvider.tsx b/src/styles/theme/ThemeProvider.tsx index 556488d2bb..03c2cfa979 100644 --- a/src/styles/theme/ThemeProvider.tsx +++ b/src/styles/theme/ThemeProvider.tsx @@ -9,7 +9,6 @@ import { selectTheme } from './slice/selectors' import { dataTableTheme } from './dataTableTheme' import { css } from 'styled-components' import { getTargetTheme } from './utils' -import { getInitialState as getInitialThemeState } from './slice' /** * React-data-table by default sets its own background and text colors @@ -383,7 +382,7 @@ const grommetCustomTheme: ThemeType = { }, }, } -export const ThemeProvider = (props: { children: React.ReactNode }) => { +export const ThemeProvider = (props: { children: React.ReactChild }) => { const theme = deepMerge(grommet, grommetCustomTheme) const mode = useSelector(selectTheme) @@ -393,13 +392,3 @@ export const ThemeProvider = (props: { children: React.ReactNode }) => { ) } -export const ThemeProviderWithoutRedux = (props: { children: React.ReactNode }) => { - const theme = deepMerge(grommet, grommetCustomTheme) - const mode = getInitialThemeState().selected - - return ( - - {React.Children.only(props.children)} - - ) -} diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index b8d6b0c317..dd260904e3 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -7,17 +7,19 @@ type Props = { type?: browser.Windows.CreateType } -export const getPopupUrl = (path: string) => - browser.runtime.getURL(`${browser.runtime.getManifest()?.action?.default_popup}${path}`) +const getPopupUrl = (path: string) => + browser.runtime.getURL(`${browser.runtime.getManifest()?.browser_action?.default_popup}${path}`) const openPopup = ({ path, height, width, type }: Props) => { - const existingPopupWindow = browser.extension.getViews().find(window => window.location.href === path) + const existingPopupWindow = browser.extension + .getViews() + .find(window => window.location.href === getPopupUrl(path)) if (existingPopupWindow) { existingPopupWindow.close() } browser.windows.create({ - url: path, + url: getPopupUrl(path), type: type ?? 'popup', width: width, height: height, From 4a61f461ed487d1a87f234a547c1093e7dc92400 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 03:14:10 +0100 Subject: [PATCH 19/27] Retain encryption key between popup reopenings --- src/app/state/persist/encryption.ts | 8 ++++ src/app/state/persist/saga.ts | 57 ++++++++++++++++++++++++++++- src/app/state/persist/selectors.ts | 5 +++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/app/state/persist/encryption.ts b/src/app/state/persist/encryption.ts index e2b7e683c9..a7953bdc92 100644 --- a/src/app/state/persist/encryption.ts +++ b/src/app/state/persist/encryption.ts @@ -47,6 +47,14 @@ export async function decryptWithPassword( ): Promise { const encryptedObj = fromBase64andParse(encryptedString) const derivedKeyWithSalt = await deriveKeyFromPassword(password, encryptedObj.salt) + return await decryptWithKey(derivedKeyWithSalt, encryptedString) +} + +export async function decryptWithKey( + derivedKeyWithSalt: KeyWithSalt, + encryptedString: EncryptedString, +): Promise { + const encryptedObj = fromBase64andParse(encryptedString) const dataBytes = nacl.secretbox.open(encryptedObj.secretbox, encryptedObj.nonce, derivedKeyWithSalt.key) if (!dataBytes) throw new PasswordWrongError() diff --git a/src/app/state/persist/saga.ts b/src/app/state/persist/saga.ts index 7920dc7774..79bed15f34 100644 --- a/src/app/state/persist/saga.ts +++ b/src/app/state/persist/saga.ts @@ -4,18 +4,26 @@ import { isActionSynced } from 'redux-state-sync' import { persistActions, STORAGE_FIELD } from './index' import { base64andStringify, + decryptWithKey, decryptWithPassword, deriveKeyFromPassword, encryptWithKey, fromBase64andParse, } from './encryption' import { RootState } from 'types' -import { EncryptedString, KeyWithSalt, PersistedRootState, SetUnlockedRootStatePayload } from './types' +import { + EncryptedString, + KeyWithSalt, + PersistState, + PersistedRootState, + SetUnlockedRootStatePayload, +} from './types' import { PasswordWrongError } from 'types/errors' import { walletActions } from 'app/state/wallet' import { selectUnlockedStatus } from 'app/state/selectUnlockedStatus' import { runtimeIs } from 'config' import { backupAndDeleteV0ExtProfile, readStorageV0 } from '../../../utils/walletExtensionV0' +import { selectStringifiedEncryptionKey } from './selectors' function* watchPersistAsync() { yield* fork(function* () { @@ -211,8 +219,55 @@ function* encryptAndPersistState(action: AnyAction) { window.localStorage.setItem(STORAGE_FIELD, encryptedState) } +function* retainEncryptionKeyBetweenPopupReopenings() { + if (runtimeIs !== 'extension') return + yield* fork(function* () { + const channelQueue = yield* actionChannel('*') + let previousStringifiedEncryptionKey: PersistState['stringifiedEncryptionKey'] = undefined + while (true) { + yield* take(channelQueue) + const stringifiedEncryptionKey = yield* select(selectStringifiedEncryptionKey) + if (stringifiedEncryptionKey !== previousStringifiedEncryptionKey) { + previousStringifiedEncryptionKey = stringifiedEncryptionKey + yield* call(writeSharedExtMemory, stringifiedEncryptionKey) + } + } + }) + + yield* fork(function* () { + const encryptedState = window.localStorage.getItem( + STORAGE_FIELD, + ) as EncryptedString | null + if (!encryptedState) return // Ignore + try { + const stringifiedEncryptionKey = yield* call(readSharedExtMemory) + if (!stringifiedEncryptionKey) return // Ignore + if (stringifiedEncryptionKey === 'skipped') return // Ignore + const keyWithSalt: KeyWithSalt = fromBase64andParse(stringifiedEncryptionKey) + const persistedRootState = yield* call(decryptWithKey, keyWithSalt, encryptedState) + yield* put(persistActions.setUnlockedRootState({ persistedRootState, stringifiedEncryptionKey })) + } catch (error) { + // Ignore + } + }) +} + +async function writeSharedExtMemory(stringifiedEncryptionKey: PersistState['stringifiedEncryptionKey']) { + if (runtimeIs !== 'extension') return + const browser = await import('webextension-polyfill') + await browser.storage.session.set({ stringifiedEncryptionKey }) +} + +async function readSharedExtMemory() { + if (runtimeIs !== 'extension') return + const browser = await import('webextension-polyfill') + const storage = await browser.storage.session.get('stringifiedEncryptionKey') + return storage.stringifiedEncryptionKey as PersistState['stringifiedEncryptionKey'] +} + export function* persistSaga() { yield* watchPersistAsync() + yield* retainEncryptionKeyBetweenPopupReopenings() const storageV0 = yield* call(readStorageV0) yield* put(persistActions.setHasV0StorageToMigrate(!!storageV0?.chromeStorageLocal.keyringData)) } diff --git a/src/app/state/persist/selectors.ts b/src/app/state/persist/selectors.ts index c112c070f7..dcd6f3b435 100644 --- a/src/app/state/persist/selectors.ts +++ b/src/app/state/persist/selectors.ts @@ -20,3 +20,8 @@ export const selectIsPersistenceUnsupported = createSelector( [selectSlice], state => state.isPersistenceUnsupported, ) + +export const selectStringifiedEncryptionKey = createSelector( + [selectSlice], + state => state.stringifiedEncryptionKey, +) From 3a6223d8676cde3338c68a58fbef29948fcfe9b6 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:34:00 +0100 Subject: [PATCH 20/27] Force extension users to create a profile --- src/app/components/Persist/ChoosePasswordFields.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/components/Persist/ChoosePasswordFields.tsx b/src/app/components/Persist/ChoosePasswordFields.tsx index 0d7eaae848..c754f5b7b7 100644 --- a/src/app/components/Persist/ChoosePasswordFields.tsx +++ b/src/app/components/Persist/ChoosePasswordFields.tsx @@ -9,6 +9,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { ChoosePasswordInputFields } from './ChoosePasswordInputFields' +import { runtimeIs } from 'config' export function ChoosePasswordFields() { const { t } = useTranslation() @@ -17,6 +18,8 @@ export function ChoosePasswordFields() { const hasUnpersistedAccounts = unlockedStatus === 'openUnpersisted' const [startPersisting, setStartPersisting] = useState(!hasUnpersistedAccounts) + const isExtension = runtimeIs === 'extension' + const isChoiceDisabled = isPersistenceUnsupported || unlockedStatus === 'unlockedProfile' || @@ -41,6 +44,12 @@ export function ChoosePasswordFields() { disabled: true, checked: unlockedStatus === 'unlockedProfile', } + : isExtension + ? { + disabled: true, + // Force creating a profile in Manifest v3 extension because we can't keep state in memory + checked: true, + } : { checked: startPersisting, })} From c88b2fcbbe7e4f0c7fb638794e29ca62aaf79fa0 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 03:11:29 +0100 Subject: [PATCH 21/27] Rename ConnectDevicePage to ExtensionRequestLedgerPermissionPopup --- .../ExtensionRequestLedgerPermissionPopup.tsx | 4 ++-- .../__tests__/__snapshots__/index.test.tsx.snap | 2 +- .../__tests__/index.test.tsx | 12 ++++++------ extension/src/popup/routes.tsx | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename src/app/pages/ConnectDevicePage/index.tsx => extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx (97%) rename {src/app/pages/ConnectDevicePage => extension/src/ExtensionRequestLedgerPermissionPopup}/__tests__/__snapshots__/index.test.tsx.snap (98%) rename {src/app/pages/ConnectDevicePage => extension/src/ExtensionRequestLedgerPermissionPopup}/__tests__/index.test.tsx (77%) diff --git a/src/app/pages/ConnectDevicePage/index.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx similarity index 97% rename from src/app/pages/ConnectDevicePage/index.tsx rename to extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx index 6f03ab355d..3ada5f470c 100644 --- a/src/app/pages/ConnectDevicePage/index.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx @@ -13,8 +13,8 @@ import { AlertBox } from 'app/components/AlertBox' import { WalletErrors } from 'types/errors' import { importAccountsActions } from 'app/state/importaccounts' import { requestDevice } from 'app/lib/ledger' -import logotype from '../../../../public/Icon Blue 192.png' import { WalletType } from '../../state/wallet/types' +import logotype from '../../../public/Icon Blue 192.png' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' type ConnectionStatusIconPros = { @@ -44,7 +44,7 @@ function ConnectionStatusIcon({ success = true, label, withMargin = false }: Con ) } -export function ConnectDevicePage() { +export function ExtensionRequestLedgerPermissionPopup() { const { t } = useTranslation() const dispatch = useDispatch() const [connection, setConnection] = useState('disconnected') diff --git a/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap similarity index 98% rename from src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap rename to extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap index f48aea18e0..a0f727306e 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render component 1`] = ` +exports[` should render component 1`] = ` .c0 { display: -webkit-box; display: -webkit-flex; diff --git a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx similarity index 77% rename from src/app/pages/ConnectDevicePage/__tests__/index.test.tsx rename to extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx index 6c6513c310..3563c02ad9 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx @@ -3,8 +3,8 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { requestDevice } from 'app/lib/ledger' import { importAccountsActions } from 'app/state/importaccounts' -import { ConnectDevicePage } from '..' -import { WalletType } from '../../../state/wallet/types' +import { ExtensionRequestLedgerPermissionPopup } from '../ExtensionRequestLedgerPermissionPopup' +import { WalletType } from '../../../../src/app/state/wallet/types' jest.mock('app/lib/ledger') @@ -14,9 +14,9 @@ jest.mock('react-redux', () => ({ useDispatch: () => mockDispatch, })) -describe('', () => { +describe('', () => { it('should render component', () => { - const { container } = render() + const { container } = render() expect(container).toMatchSnapshot() }) @@ -24,7 +24,7 @@ describe('', () => { it('should render success state', async () => { jest.mocked(requestDevice).mockResolvedValue({} as USBDevice) - render() + render() await userEvent.click(screen.getByRole('button')) @@ -40,7 +40,7 @@ describe('', () => { it('should render error state', async () => { jest.mocked(requestDevice).mockRejectedValue(new Error('error')) - render() + render() userEvent.click(screen.getByRole('button')) diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index 1f54fd8432..f986dd2a7b 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -1,7 +1,7 @@ import React from 'react' import { RouteObject } from 'react-router-dom' import { App } from 'app' -import { ConnectDevicePage } from 'app/pages/ConnectDevicePage' +import { ExtensionRequestLedgerPermissionPopup } from '../ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup' import { FromLedgerWebExtension } from 'app/pages/OpenWalletPage/webextension' import { commonRoutes } from '../../../src/commonRoutes' import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage' @@ -24,6 +24,6 @@ export const routes: RouteObject[] = [ }, { path: 'open-wallet/connect-device', - element: , + element: , }, ] From f180d5180d0a9e2d22b854b37c40252920fa66d2 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 03:12:40 +0100 Subject: [PATCH 22/27] Refactor ExtensionRequestLedgerPermission as html entry without redux --- .../ExtensionRequestLedgerPermissionPopup.tsx | 5 -- .../__tests__/index.test.tsx | 13 ----- .../index.html | 16 +++++++ .../index.tsx | 29 +++++++++++ extension/src/popup/popup.tsx | 48 ++++++++++++------- extension/src/popup/routes.tsx | 5 -- public/manifest.json | 6 ++- src/app/pages/OpenWalletPage/webextension.tsx | 11 +++-- src/index.tsx | 4 +- src/styles/theme/ThemeProvider.tsx | 13 ++++- src/utils/webextension.ts | 10 ++-- 11 files changed, 107 insertions(+), 53 deletions(-) create mode 100644 extension/src/ExtensionRequestLedgerPermissionPopup/index.html create mode 100644 extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx index 3ada5f470c..38e1bd29c3 100644 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' import { Box } from 'grommet/es6/components/Box' import { Button } from 'grommet/es6/components/Button' import { Spinner } from 'grommet/es6/components/Spinner' @@ -11,9 +10,7 @@ import { Header } from 'app/components/Header' import { ErrorFormatter } from 'app/components/ErrorFormatter' import { AlertBox } from 'app/components/AlertBox' import { WalletErrors } from 'types/errors' -import { importAccountsActions } from 'app/state/importaccounts' import { requestDevice } from 'app/lib/ledger' -import { WalletType } from '../../state/wallet/types' import logotype from '../../../public/Icon Blue 192.png' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' @@ -46,7 +43,6 @@ function ConnectionStatusIcon({ success = true, label, withMargin = false }: Con export function ExtensionRequestLedgerPermissionPopup() { const { t } = useTranslation() - const dispatch = useDispatch() const [connection, setConnection] = useState('disconnected') const handleConnect = async () => { setConnection('connecting') @@ -54,7 +50,6 @@ export function ExtensionRequestLedgerPermissionPopup() { const device = await requestDevice() if (device) { setConnection('connected') - dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) } } catch { setConnection('error') diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx index 3563c02ad9..b6b6b85cd7 100644 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx @@ -2,18 +2,10 @@ import React from 'react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { requestDevice } from 'app/lib/ledger' -import { importAccountsActions } from 'app/state/importaccounts' import { ExtensionRequestLedgerPermissionPopup } from '../ExtensionRequestLedgerPermissionPopup' -import { WalletType } from '../../../../src/app/state/wallet/types' jest.mock('app/lib/ledger') -const mockDispatch = jest.fn() -jest.mock('react-redux', () => ({ - useSelector: jest.fn(), - useDispatch: () => mockDispatch, -})) - describe('', () => { it('should render component', () => { const { container } = render() @@ -31,10 +23,6 @@ describe('', () => { expect(await screen.findByText('ledger.extension.succeed')).toBeInTheDocument() expect(screen.getByLabelText('Status is okay')).toBeInTheDocument() expect(screen.queryByRole('button')).not.toBeInTheDocument() - expect(mockDispatch).toHaveBeenCalledWith({ - payload: WalletType.UsbLedger, - type: importAccountsActions.enumerateAccountsFromLedger.type, - }) }) it('should render error state', async () => { @@ -46,6 +34,5 @@ describe('', () => { expect(await screen.findByText('ledger.extension.failed')).toBeInTheDocument() expect(screen.getByLabelText('Status is critical')).toBeInTheDocument() - expect(mockDispatch).not.toHaveBeenCalled() }) }) diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/index.html b/extension/src/ExtensionRequestLedgerPermissionPopup/index.html new file mode 100644 index 0000000000..a584a4749f --- /dev/null +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/index.html @@ -0,0 +1,16 @@ + + + + + + + + + ROSE Wallet + + + +
+ + + diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx new file mode 100644 index 0000000000..6a1aaee387 --- /dev/null +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/index.tsx @@ -0,0 +1,29 @@ +import 'react-app-polyfill/stable' + +import * as React from 'react' +import { createRoot } from 'react-dom/client' + +// Use consistent styling +import 'sanitize.css/sanitize.css' + +import { ThemeProviderWithoutRedux } from 'styles/theme/ThemeProvider' + +// Initialize languages +import 'locales/i18n' + +// Fonts +import 'styles/main.css' +import { ExtensionRequestLedgerPermissionPopup } from './ExtensionRequestLedgerPermissionPopup' + +const container = document.getElementById('root') as HTMLElement +const root = createRoot(container!) + +root.render( + // Avoid redux: it's not necessary and has it has a little potential to cause + // conflicts in stored state because it runs in parallel with wallet popup. + + + + + , +) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index e5e6a5f55b..beb74df034 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -6,25 +6,41 @@ import { configureAppStore } from 'store/configureStore' import { createHashRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from 'styles/theme/ThemeProvider' +import browser from 'webextension-polyfill' import 'locales/i18n' import 'sanitize.css/sanitize.css' import 'styles/main.css' import { routes } from './routes' -const container = document.getElementById('root') as HTMLElement -const root = createRoot(container!) -const store = configureAppStore() -const router = createHashRouter(routes) - -root.render( - - - - - - - - - , -) +// Only allow one popup/tab with redux, so no need to sync state. +if (browser.extension.getViews({ type: 'tab' }).length > 0) { + // Either this is a tab, or something else is. + // If this is a tab, just close. + // If something else is a tab, it must be ExtensionRequestLedgerPermissionPopup. Focus that and close self. + ;(async () => { + // Unexpected: persistent popup is classified as 'TAB' in contexts API + const tabsAndPersistentPopups = await browser.runtime.getContexts({ contextTypes: ['TAB'] }) + for (const c of tabsAndPersistentPopups) { + await browser.windows.update(c.windowId, { focused: true }) + await browser.tabs.update(c.tabId, { active: true }) + } + window.close() + })() +} else { + const container = document.getElementById('root') as HTMLElement + const root = createRoot(container!) + const store = configureAppStore() + const router = createHashRouter(routes) + root.render( + + + + + + + + + , + ) +} diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index f986dd2a7b..afb086a8f4 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -1,7 +1,6 @@ import React from 'react' import { RouteObject } from 'react-router-dom' import { App } from 'app' -import { ExtensionRequestLedgerPermissionPopup } from '../ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup' import { FromLedgerWebExtension } from 'app/pages/OpenWalletPage/webextension' import { commonRoutes } from '../../../src/commonRoutes' import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage' @@ -22,8 +21,4 @@ export const routes: RouteObject[] = [ }, ], }, - { - path: 'open-wallet/connect-device', - element: , - }, ] diff --git a/public/manifest.json b/public/manifest.json index dfa81c90c4..e238ac485e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -32,5 +32,9 @@ "content_security_policy": { "extension_pages": "{{{ EXTENSION_CSP }}}" }, - "externally_connectable": { "ids": [] } + "externally_connectable": { "ids": [] }, + "web_accessible_resources": [{ + "matches": [], + "resources": ["../extension/src/ExtensionRequestLedgerPermissionPopup/index.html"] + }] } diff --git a/src/app/pages/OpenWalletPage/webextension.tsx b/src/app/pages/OpenWalletPage/webextension.tsx index 225d6ba0e2..3d16605bb3 100644 --- a/src/app/pages/OpenWalletPage/webextension.tsx +++ b/src/app/pages/OpenWalletPage/webextension.tsx @@ -1,16 +1,19 @@ import React from 'react' -import { useHref, useNavigate } from 'react-router-dom' import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { ExtensionRequestLedgerPermissionPopup } from '../../../../extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup' export function FromLedgerWebExtension() { - const href = useHref('/open-wallet/connect-device') - const navigate = useNavigate() + /** See {@link ExtensionRequestLedgerPermissionPopup} */ + const href = new URL( + '../../../../extension/src/ExtensionRequestLedgerPermissionPopup/index.html', + import.meta.url, + ).href return ( { - navigate('/open-wallet/ledger/usb') openLedgerAccessPopup(href) }} /> diff --git a/src/index.tsx b/src/index.tsx index eef5f41714..b2100856cb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,10 +22,10 @@ import { configureAppStore } from 'store/configureStore' import { ThemeProvider } from 'styles/theme/ThemeProvider' // Initialize languages -import './locales/i18n' +import 'locales/i18n' // Fonts -import './styles/main.css' +import 'styles/main.css' import { routes } from './routes' const store = configureAppStore() diff --git a/src/styles/theme/ThemeProvider.tsx b/src/styles/theme/ThemeProvider.tsx index 03c2cfa979..556488d2bb 100644 --- a/src/styles/theme/ThemeProvider.tsx +++ b/src/styles/theme/ThemeProvider.tsx @@ -9,6 +9,7 @@ import { selectTheme } from './slice/selectors' import { dataTableTheme } from './dataTableTheme' import { css } from 'styled-components' import { getTargetTheme } from './utils' +import { getInitialState as getInitialThemeState } from './slice' /** * React-data-table by default sets its own background and text colors @@ -382,7 +383,7 @@ const grommetCustomTheme: ThemeType = { }, }, } -export const ThemeProvider = (props: { children: React.ReactChild }) => { +export const ThemeProvider = (props: { children: React.ReactNode }) => { const theme = deepMerge(grommet, grommetCustomTheme) const mode = useSelector(selectTheme) @@ -392,3 +393,13 @@ export const ThemeProvider = (props: { children: React.ReactChild }) => { ) } +export const ThemeProviderWithoutRedux = (props: { children: React.ReactNode }) => { + const theme = deepMerge(grommet, grommetCustomTheme) + const mode = getInitialThemeState().selected + + return ( + + {React.Children.only(props.children)} + + ) +} diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index dd260904e3..b8d6b0c317 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -7,19 +7,17 @@ type Props = { type?: browser.Windows.CreateType } -const getPopupUrl = (path: string) => - browser.runtime.getURL(`${browser.runtime.getManifest()?.browser_action?.default_popup}${path}`) +export const getPopupUrl = (path: string) => + browser.runtime.getURL(`${browser.runtime.getManifest()?.action?.default_popup}${path}`) const openPopup = ({ path, height, width, type }: Props) => { - const existingPopupWindow = browser.extension - .getViews() - .find(window => window.location.href === getPopupUrl(path)) + const existingPopupWindow = browser.extension.getViews().find(window => window.location.href === path) if (existingPopupWindow) { existingPopupWindow.close() } browser.windows.create({ - url: getPopupUrl(path), + url: path, type: type ?? 'popup', width: width, height: height, From dffce4d6c282115f815b2b31e47d8e425409b2de Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 03:33:08 +0100 Subject: [PATCH 23/27] Remove unused webext-redux --- package.json | 1 - yarn.lock | 18 ------------------ 2 files changed, 19 deletions(-) diff --git a/package.json b/package.json index 941cdc9232..8a5bcb4a31 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,6 @@ "tweetnacl": "1.0.3", "typed-redux-saga": "1.5.0", "valid-url": "1.0.9", - "webext-redux": "2.1.9", "webextension-polyfill": "0.12.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 731713b565..ade6539e58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7369,16 +7369,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.assignin@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" - integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -9946,14 +9936,6 @@ weak-lru-cache@^1.2.2: resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw== -webext-redux@2.1.9: - version "2.1.9" - resolved "https://registry.yarnpkg.com/webext-redux/-/webext-redux-2.1.9.tgz#f7fd01ea4b93191d07bcdd0db5966955766ef634" - integrity sha512-z7frkQ/avFgnMxUH6Q955hU8SDsHT9Zlq9az3OpY891RXw9nKODOTnUhNo9ZAlDFUPHhX2A+z6j74BCaYwEsfQ== - dependencies: - lodash.assignin "^4.2.0" - lodash.clonedeep "^4.5.0" - webextension-polyfill@0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69" From 874365bfc36d061a18523cbb58dbb0983b396dae Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 04:52:24 +0100 Subject: [PATCH 24/27] TODO rename again --- .../ExtensionRequestLedgerPermissionPopup.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx index 38e1bd29c3..534df6a4c4 100644 --- a/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx @@ -41,6 +41,7 @@ function ConnectionStatusIcon({ success = true, label, withMargin = false }: Con ) } +// TODO rename again to ExtLedgerAccessPopup to match openLedgerAccessPopup export function ExtensionRequestLedgerPermissionPopup() { const { t } = useTranslation() const [connection, setConnection] = useState('disconnected') From 55d8b8b2dfd74f6efa77e1d251f7e1b0ba06fb48 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 04:32:51 +0100 Subject: [PATCH 25/27] Focus ledger access popup twice --- src/utils/webextension.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index b8d6b0c317..e771479e1c 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -10,18 +10,20 @@ type Props = { export const getPopupUrl = (path: string) => browser.runtime.getURL(`${browser.runtime.getManifest()?.action?.default_popup}${path}`) -const openPopup = ({ path, height, width, type }: Props) => { +const openPopup = async ({ path, height, width, type }: Props) => { const existingPopupWindow = browser.extension.getViews().find(window => window.location.href === path) if (existingPopupWindow) { existingPopupWindow.close() } - browser.windows.create({ + const popup = await browser.windows.create({ url: path, type: type ?? 'popup', width: width, height: height, + focused: true, }) + await browser.windows.update(popup.id!, { focused: true }) // Focus again. Helps in rare cases like when screensharing. } export const openLedgerAccessPopup = (path: string) => { From 64f8789f98943f01dae227507d6c078e92420fa5 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 06:55:12 +0100 Subject: [PATCH 26/27] revert f180d5180d0a9e2d22b854b37c40252920fa66d2 partially --- src/app/pages/OpenWalletPage/webextension.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/pages/OpenWalletPage/webextension.tsx b/src/app/pages/OpenWalletPage/webextension.tsx index 3d16605bb3..73bd5b6eee 100644 --- a/src/app/pages/OpenWalletPage/webextension.tsx +++ b/src/app/pages/OpenWalletPage/webextension.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useNavigate } from 'react-router-dom' import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -10,10 +11,12 @@ export function FromLedgerWebExtension() { '../../../../extension/src/ExtensionRequestLedgerPermissionPopup/index.html', import.meta.url, ).href + const navigate = useNavigate() return ( { + navigate('/open-wallet/ledger/usb') openLedgerAccessPopup(href) }} /> From aaccd7ad7895eb9eff4023080a7d6d0462fbe668 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 06:41:12 +0100 Subject: [PATCH 27/27] Refactor how extension requests ledger access --- .../Features/FromLedger/index.tsx | 23 ++++++++++++++++++- src/app/pages/OpenWalletPage/webextension.tsx | 3 --- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx b/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx index 3faa0a1a03..1a8bfc74b5 100644 --- a/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx +++ b/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx @@ -7,6 +7,7 @@ import { Text } from 'grommet/es6/components/Text' import { canAccessBle, canAccessNavigatorUsb } from '../../../../lib/ledger' import { useTranslation } from 'react-i18next' import { Capacitor } from '@capacitor/core' +import TransportWebUSB from '@ledgerhq/hw-transport-webusb' type SelectOpenMethodProps = { webExtensionUSBLedgerAccess?: () => void @@ -15,6 +16,7 @@ type SelectOpenMethodProps = { export function FromLedger({ webExtensionUSBLedgerAccess }: SelectOpenMethodProps) { const { t } = useTranslation() const [supportsUsbLedger, setSupportsUsbLedger] = React.useState(true) + const [hasUsbLedgerAccess, setHasUsbLedgerAccess] = React.useState(undefined) const [supportsBleLedger, setSupportsBleLedger] = React.useState(true) useEffect(() => { @@ -31,6 +33,25 @@ export function FromLedger({ webExtensionUSBLedgerAccess }: SelectOpenMethodProp getLedgerSupport() }, []) + useEffect(() => { + if (openLedgerAccessPopup) { + // In default ext popup this gets auto-accepted / auto-rejected. In a tab or persistent popup it would + // prompt user to select a ledger device. TransportWebUSB.create seems to match requestDevice called in + // openLedgerAccessPopup. + // If TransportWebUSB.create() is rejected then call openLedgerAccessPopup and requestDevice. When user + // confirms the prompt tell them to come back here. TransportWebUSB.create() will resolve. + TransportWebUSB.create() + .then(() => setHasUsbLedgerAccess(true)) + .catch(() => setHasUsbLedgerAccess(false)) + } else { + // Assume true in web app. enumerateAccountsFromLedger will call TransportWebUSB.create in next steps + // and will prompt user to select a ledger device. + setHasUsbLedgerAccess(true) + } + }, [openLedgerAccessPopup]) + + const shouldOpenUsbLedgerAccessPopup = openLedgerAccessPopup && !hasUsbLedgerAccess + return (
- {webExtensionUSBLedgerAccess ? ( + {shouldOpenUsbLedgerAccessPopup ? (