diff --git a/src/app/pages/ConnectDevicePage/index.tsx b/extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx similarity index 91% rename from src/app/pages/ConnectDevicePage/index.tsx rename to extension/src/ExtensionRequestLedgerPermissionPopup/ExtensionRequestLedgerPermissionPopup.tsx index 6f03ab355d..534df6a4c4 100644 --- a/src/app/pages/ConnectDevicePage/index.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,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' +import logotype from '../../../public/Icon Blue 192.png' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' type ConnectionStatusIconPros = { @@ -44,9 +41,9 @@ function ConnectionStatusIcon({ success = true, label, withMargin = false }: Con ) } -export function ConnectDevicePage() { +// TODO rename again to ExtLedgerAccessPopup to match openLedgerAccessPopup +export function ExtensionRequestLedgerPermissionPopup() { const { t } = useTranslation() - const dispatch = useDispatch() const [connection, setConnection] = useState('disconnected') const handleConnect = async () => { setConnection('connecting') @@ -54,7 +51,6 @@ export function ConnectDevicePage() { const device = await requestDevice() if (device) { setConnection('connected') - dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) } } catch { setConnection('error') 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 60% rename from src/app/pages/ConnectDevicePage/__tests__/index.test.tsx rename to extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx index 6c6513c310..b6b6b85cd7 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx +++ b/extension/src/ExtensionRequestLedgerPermissionPopup/__tests__/index.test.tsx @@ -2,21 +2,13 @@ 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' +import { ExtensionRequestLedgerPermissionPopup } from '../ExtensionRequestLedgerPermissionPopup' jest.mock('app/lib/ledger') -const mockDispatch = jest.fn() -jest.mock('react-redux', () => ({ - useSelector: jest.fn(), - useDispatch: () => mockDispatch, -})) - -describe('', () => { +describe('', () => { it('should render component', () => { - const { container } = render() + const { container } = render() expect(container).toMatchSnapshot() }) @@ -24,28 +16,23 @@ describe('', () => { it('should render success state', async () => { jest.mocked(requestDevice).mockResolvedValue({} as USBDevice) - render() + 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() + 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/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/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..beb74df034 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -2,22 +2,36 @@ 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' +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 = new Store() -const router = createHashRouter(routes) - -store.ready().then(() => { +// 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( @@ -29,6 +43,4 @@ store.ready().then(() => { , ) -}) - -console.log('popup') +} diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index 1f54fd8432..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 { ConnectDevicePage } from 'app/pages/ConnectDevicePage' 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/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/package.json b/package.json index f55babe605..8a5bcb4a31 100644 --- a/package.json +++ b/package.json @@ -96,8 +96,7 @@ "tweetnacl": "1.0.3", "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 +122,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/public/manifest.json b/public/manifest.json index 7b79d32e0f..e238ac485e 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,13 @@ "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": [] } + "externally_connectable": { "ids": [] }, + "web_accessible_resources": [{ + "matches": [], + "resources": ["../extension/src/ExtensionRequestLedgerPermissionPopup/index.html"] + }] } 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 @@ - 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, })} 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 ? (