diff --git a/capacitor.config.ts b/capacitor.config.ts new file mode 100644 index 0000000000..73b775a522 --- /dev/null +++ b/capacitor.config.ts @@ -0,0 +1,12 @@ +import { CapacitorConfig } from '@capacitor/cli' + +const config: CapacitorConfig = { + appId: 'com.oasisprotocol.wallet', + appName: 'Oasis Wallet', + webDir: 'build', + server: { + androidScheme: 'https', + }, +} + +export default config diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index 8f296fe2d1..a4db770e0a 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -2,7 +2,7 @@ import React from 'react' import { RouteObject } from 'react-router-dom' import { App } from 'app' import { ConnectDevicePage } from 'app/pages/ConnectDevicePage' -import { OpenWalletPageWebExtension } from 'app/pages/OpenWalletPage/webextension' +import { FromLedgerWebExtension, OpenWalletPageWebExtension } from 'app/pages/OpenWalletPage/webextension' import { commonRoutes } from '../../../src/commonRoutes' export const routes: RouteObject[] = [ @@ -21,4 +21,8 @@ export const routes: RouteObject[] = [ path: 'open-wallet/connect-device', element: , }, + { + path: 'open-wallet/ledger', + element: , + }, ] diff --git a/package.json b/package.json index b5e19fffcf..3d4edb9d48 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "lint-docs": "markdownlint --ignore '**/node_modules/**' '**/*.md'", "extract-messages": "rm src/locales/en/translation.json && i18next-scanner --config=internals/extractMessages/i18next-scanner.config.js", "fix-grommet-icons-types": "node ./internals/scripts/fix-grommet-icons-types.js", - "print-extension-dev-csp": "node ./internals/scripts/print-extension-dev-csp.js" + "print-extension-dev-csp": "node ./internals/scripts/print-extension-dev-csp.js", + "android": "yarn build && yarn cap run android -l --external", + "ios": "yarn build && yarn cap run ios -l --external" }, "browserslist": { "production": [ @@ -53,11 +55,16 @@ "report-dir": "cypress-coverage" }, "dependencies": { + "@capacitor-community/bluetooth-le": "3.0.0", + "@capacitor/android": "5.0.4", + "@capacitor/core": "5.0.4", + "@capacitor/ios": "5.0.4", "@ethereumjs/util": "9.0.0", "@ledgerhq/hw-transport-webusb": "6.27.19", "@metamask/jazzicon": "2.0.0", "@oasisprotocol/client": "0.1.1-alpha.2", "@oasisprotocol/client-rt": "0.2.1-alpha.2", + "@oasisprotocol/ionic-ledger-hw-transport-ble": "1.0.0-beta", "@oasisprotocol/ledger": "1.0.0", "@reduxjs/toolkit": "1.9.7", "base64-arraybuffer": "1.0.2", @@ -90,6 +97,7 @@ "webextension-polyfill": "0.10.0" }, "devDependencies": { + "@capacitor/cli": "5.0.4", "@cypress/code-coverage": "3.12.4", "@parcel/config-webextension": "2.9.3", "@parcel/packager-raw-url": "2.9.3", diff --git a/playwright/tests/extension.spec.ts b/playwright/tests/extension.spec.ts index 17937e1f26..8e5812970f 100644 --- a/playwright/tests/extension.spec.ts +++ b/playwright/tests/extension.spec.ts @@ -64,6 +64,7 @@ test.describe('The extension popup should load', () => { test('ask for USB permissions in ledger popup', async ({ page, context, extensionId }) => { await page.goto(`chrome-extension://${extensionId}/${popupFile}#/open-wallet`) + await page.getByRole('button', { name: /Ledger/i }).click() const popupPromise = context.waitForEvent('page') await page.getByRole('button', { name: /Grant access to your Ledger/i }).click() const popup = await popupPromise diff --git a/playwright/tests/ledger.spec.ts b/playwright/tests/ledger.spec.ts index ed77b04b44..cc0f4d7926 100644 --- a/playwright/tests/ledger.spec.ts +++ b/playwright/tests/ledger.spec.ts @@ -6,7 +6,7 @@ test.describe('Ledger', () => { expect((await page.request.head('/')).headers()).toHaveProperty('permissions-policy') await expectNoErrorsInConsole(page) - await page.goto('/open-wallet/ledger') + await page.goto('/open-wallet/ledger/usb') await page.getByRole('button', { name: 'Select accounts to open' }).click() await expect(page.getByText('error').or(page.getByText('fail'))).toBeHidden() }) diff --git a/resources/android/icon/drawable-hdpi-icon.png b/resources/android/icon/drawable-hdpi-icon.png new file mode 100644 index 0000000000..b4767bbd7b Binary files /dev/null and b/resources/android/icon/drawable-hdpi-icon.png differ diff --git a/resources/android/icon/drawable-ldpi-icon.png b/resources/android/icon/drawable-ldpi-icon.png new file mode 100644 index 0000000000..280d1c9258 Binary files /dev/null and b/resources/android/icon/drawable-ldpi-icon.png differ diff --git a/resources/android/icon/drawable-mdpi-icon.png b/resources/android/icon/drawable-mdpi-icon.png new file mode 100644 index 0000000000..3dee7f268c Binary files /dev/null and b/resources/android/icon/drawable-mdpi-icon.png differ diff --git a/resources/android/icon/drawable-xhdpi-icon.png b/resources/android/icon/drawable-xhdpi-icon.png new file mode 100644 index 0000000000..0c08305a34 Binary files /dev/null and b/resources/android/icon/drawable-xhdpi-icon.png differ diff --git a/resources/android/icon/drawable-xxhdpi-icon.png b/resources/android/icon/drawable-xxhdpi-icon.png new file mode 100644 index 0000000000..d8b7ad3590 Binary files /dev/null and b/resources/android/icon/drawable-xxhdpi-icon.png differ diff --git a/resources/android/icon/drawable-xxxhdpi-icon.png b/resources/android/icon/drawable-xxxhdpi-icon.png new file mode 100644 index 0000000000..c2c5e91eba Binary files /dev/null and b/resources/android/icon/drawable-xxxhdpi-icon.png differ diff --git a/resources/android/icon/hdpi-foreground.png b/resources/android/icon/hdpi-foreground.png new file mode 100644 index 0000000000..b4767bbd7b Binary files /dev/null and b/resources/android/icon/hdpi-foreground.png differ diff --git a/resources/android/icon/mdpi-foreground.png b/resources/android/icon/mdpi-foreground.png new file mode 100644 index 0000000000..3dee7f268c Binary files /dev/null and b/resources/android/icon/mdpi-foreground.png differ diff --git a/resources/android/icon/xhdpi-foreground.png b/resources/android/icon/xhdpi-foreground.png new file mode 100644 index 0000000000..0c08305a34 Binary files /dev/null and b/resources/android/icon/xhdpi-foreground.png differ diff --git a/resources/android/icon/xxhdpi-foreground.png b/resources/android/icon/xxhdpi-foreground.png new file mode 100644 index 0000000000..d8b7ad3590 Binary files /dev/null and b/resources/android/icon/xxhdpi-foreground.png differ diff --git a/resources/android/icon/xxxhdpi-foreground.png b/resources/android/icon/xxxhdpi-foreground.png new file mode 100644 index 0000000000..c2c5e91eba Binary files /dev/null and b/resources/android/icon/xxxhdpi-foreground.png differ diff --git a/resources/android/splash/drawable-land-hdpi-screen.png b/resources/android/splash/drawable-land-hdpi-screen.png new file mode 100644 index 0000000000..d57089eb46 Binary files /dev/null and b/resources/android/splash/drawable-land-hdpi-screen.png differ diff --git a/resources/android/splash/drawable-land-ldpi-screen.png b/resources/android/splash/drawable-land-ldpi-screen.png new file mode 100644 index 0000000000..877e46fdd4 Binary files /dev/null and b/resources/android/splash/drawable-land-ldpi-screen.png differ diff --git a/resources/android/splash/drawable-land-mdpi-screen.png b/resources/android/splash/drawable-land-mdpi-screen.png new file mode 100644 index 0000000000..2cba7dbec3 Binary files /dev/null and b/resources/android/splash/drawable-land-mdpi-screen.png differ diff --git a/resources/android/splash/drawable-land-xhdpi-screen.png b/resources/android/splash/drawable-land-xhdpi-screen.png new file mode 100644 index 0000000000..959db248a4 Binary files /dev/null and b/resources/android/splash/drawable-land-xhdpi-screen.png differ diff --git a/resources/android/splash/drawable-land-xxhdpi-screen.png b/resources/android/splash/drawable-land-xxhdpi-screen.png new file mode 100644 index 0000000000..b4f3d90dad Binary files /dev/null and b/resources/android/splash/drawable-land-xxhdpi-screen.png differ diff --git a/resources/android/splash/drawable-land-xxxhdpi-screen.png b/resources/android/splash/drawable-land-xxxhdpi-screen.png new file mode 100644 index 0000000000..ad163f5c4f Binary files /dev/null and b/resources/android/splash/drawable-land-xxxhdpi-screen.png differ diff --git a/resources/android/splash/drawable-port-hdpi-screen.png b/resources/android/splash/drawable-port-hdpi-screen.png new file mode 100644 index 0000000000..54bbf75ce0 Binary files /dev/null and b/resources/android/splash/drawable-port-hdpi-screen.png differ diff --git a/resources/android/splash/drawable-port-ldpi-screen.png b/resources/android/splash/drawable-port-ldpi-screen.png new file mode 100644 index 0000000000..4a4bbe51b3 Binary files /dev/null and b/resources/android/splash/drawable-port-ldpi-screen.png differ diff --git a/resources/android/splash/drawable-port-mdpi-screen.png b/resources/android/splash/drawable-port-mdpi-screen.png new file mode 100644 index 0000000000..8d6917730e Binary files /dev/null and b/resources/android/splash/drawable-port-mdpi-screen.png differ diff --git a/resources/android/splash/drawable-port-xhdpi-screen.png b/resources/android/splash/drawable-port-xhdpi-screen.png new file mode 100644 index 0000000000..2d7cf4a72a Binary files /dev/null and b/resources/android/splash/drawable-port-xhdpi-screen.png differ diff --git a/resources/android/splash/drawable-port-xxhdpi-screen.png b/resources/android/splash/drawable-port-xxhdpi-screen.png new file mode 100644 index 0000000000..772749e17e Binary files /dev/null and b/resources/android/splash/drawable-port-xxhdpi-screen.png differ diff --git a/resources/android/splash/drawable-port-xxxhdpi-screen.png b/resources/android/splash/drawable-port-xxxhdpi-screen.png new file mode 100644 index 0000000000..cca01e14b2 Binary files /dev/null and b/resources/android/splash/drawable-port-xxxhdpi-screen.png differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000000..4d0330932d Binary files /dev/null and b/resources/icon.png differ diff --git a/resources/splash.png b/resources/splash.png new file mode 100644 index 0000000000..83a3065878 Binary files /dev/null and b/resources/splash.png differ diff --git a/src/app/components/ErrorFormatter/index.tsx b/src/app/components/ErrorFormatter/index.tsx index 949f1f648c..00fbfb9b25 100644 --- a/src/app/components/ErrorFormatter/index.tsx +++ b/src/app/components/ErrorFormatter/index.tsx @@ -98,6 +98,10 @@ export function ErrorFormatter(props: Props) { message, }, ), + [WalletErrors.BluetoothTransportNotSupported]: t( + 'errors.bluetoothTransportNotSupported', + 'Your device does not support Bluetooth.', + ), } const error = errorMap[props.code] diff --git a/src/app/components/ImportAccountsStepFormatter/index.tsx b/src/app/components/ImportAccountsStepFormatter/index.tsx index 904f652045..8ad62a16f4 100644 --- a/src/app/components/ImportAccountsStepFormatter/index.tsx +++ b/src/app/components/ImportAccountsStepFormatter/index.tsx @@ -12,9 +12,10 @@ export const ImportAccountsStepFormatter = memo((props: Props) => { const stepMap: { [code in Step]: string } = { [Step.Idle]: t('ledger.steps.idle', 'Idle'), - [Step.OpeningUSB]: t('ledger.steps.openingUsb', 'Opening Ledger through USB'), + [Step.AccessingLedger]: t('ledger.steps.openingUsb', 'Opening Ledger through USB'), [Step.LoadingAccounts]: t('ledger.steps.loadingAccounts', 'Loading account details'), [Step.LoadingBalances]: t('ledger.steps.loadingBalances', 'Loading balance details'), + [Step.LoadingBleDevices]: 'Loading devices', } const message = stepMap[step] diff --git a/src/app/components/Toolbar/Features/Account/DerivationFormatter.tsx b/src/app/components/Toolbar/Features/Account/DerivationFormatter.tsx index 0dd19610df..73daa21df4 100644 --- a/src/app/components/Toolbar/Features/Account/DerivationFormatter.tsx +++ b/src/app/components/Toolbar/Features/Account/DerivationFormatter.tsx @@ -11,7 +11,8 @@ export interface DerivationFormatterProps { export const DerivationFormatter = (props: DerivationFormatterProps) => { const { t } = useTranslation() const walletTypes: { [type in WalletType]: string } = { - [WalletType.Ledger]: t('toolbar.wallets.type.ledger', 'Ledger'), + [WalletType.UsbLedger]: t('toolbar.wallets.type.usbLedger', 'USB Ledger'), + [WalletType.BleLedger]: t('toolbar.wallets.type.bluetoothLedger', 'BLE Ledger'), [WalletType.Mnemonic]: t('toolbar.wallets.type.mnemonic', 'Mnemonic'), [WalletType.PrivateKey]: t('toolbar.wallets.type.privateKey', 'Private key'), } diff --git a/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx b/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx index 5a56b9d784..534dc4dc81 100644 --- a/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx +++ b/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx @@ -27,7 +27,7 @@ describe('', () => { address: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe', balance: { available: '100', debonding: '0', delegations: '0', total: '100' }, publicKey: '00', - type: WalletType.Ledger, + type: WalletType.UsbLedger, }, }, }, diff --git a/src/app/lib/__tests__/ledger.test.ts b/src/app/lib/__tests__/ledger.test.ts index 19068cd06c..98dd9e934e 100644 --- a/src/app/lib/__tests__/ledger.test.ts +++ b/src/app/lib/__tests__/ledger.test.ts @@ -1,10 +1,16 @@ -import { Ledger, LedgerSigner, requestDevice } from '../ledger' +import { canAccessBle, Ledger, LedgerSigner, requestDevice } from '../ledger' import OasisApp from '@oasisprotocol/ledger' import { WalletError, WalletErrors } from 'types/errors' import { Wallet, WalletType } from 'app/state/wallet/types' import { isSupported, requestLedgerDevice } from '@ledgerhq/hw-transport-webusb/lib-es/webusb' +import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib' jest.mock('@ledgerhq/hw-transport-webusb/lib-es/webusb') +jest.mock('@oasisprotocol/ionic-ledger-hw-transport-ble/lib', () => { + return { + isEnabled: jest.fn(), + } +}) jest.mock('@oasisprotocol/ledger', () => ({ ...(jest.createMockFromModule('@oasisprotocol/ledger') as any), @@ -31,6 +37,30 @@ describe('Ledger Library', () => { jest.resetAllMocks() }) + describe('BLE Ledger', () => { + it('should support Bluetooth', async () => { + ;(BleTransport.isEnabled as jest.Mock).mockResolvedValue(true) + Object.defineProperty(window.navigator, 'bluetooth', { + writable: true, + value: { + requestLEScan: jest.fn(), + }, + }) + + const canAccessBluetooth = await canAccessBle() + expect(canAccessBluetooth).toBe(true) + }) + + it('should not throw if platform does not support Bluetooth', async () => { + ;(BleTransport.isEnabled as jest.Mock).mockRejectedValue( + new Error('Platform does not support Bluetooth'), + ) + + const canAccessBluetooth = await canAccessBle() + expect(canAccessBluetooth).toBe(false) + }) + }) + describe('Ledger', () => { it('enumerateAccounts should pass when Oasis App is open', async () => { mockAppIsOpen('Oasis') @@ -120,14 +150,14 @@ describe('Ledger Library', () => { it('Should fail if the wallet does not have a path', () => { const openWallet = () => { - new LedgerSigner({ type: WalletType.Ledger } as Wallet) + new LedgerSigner({ type: WalletType.UsbLedger } as Wallet) } expect(openWallet).toThrow(/ not a ledger wallet/) }) it('Should fail without USB transport', async () => { const signer = new LedgerSigner({ - type: WalletType.Ledger, + type: WalletType.UsbLedger, path: [44, 474, 0, 0, 0], pathDisplay: `m/44'/474'/0'/0'/0'`, publicKey: '00', @@ -140,7 +170,7 @@ describe('Ledger Library', () => { it('Should return the public key', () => { const signer = new LedgerSigner({ - type: WalletType.Ledger, + type: WalletType.UsbLedger, path: [44, 474, 0, 0, 0], pathDisplay: `m/44'/474'/0'/0'/0'`, publicKey: 'aabbcc', @@ -160,7 +190,7 @@ describe('Ledger Library', () => { sign.mockResolvedValueOnce({ return_code: 0x6986, error_message: '' }) const signer = new LedgerSigner({ - type: WalletType.Ledger, + type: WalletType.UsbLedger, path: [44, 474, 0, 0, 0], pathDisplay: `m/44'/474'/0'/0'/0'`, publicKey: '00', @@ -187,7 +217,7 @@ describe('Ledger Library', () => { }) const signer = new LedgerSigner({ - type: WalletType.Ledger, + type: WalletType.UsbLedger, path: [44, 474, 0, 0, 0], pathDisplay: `m/44'/474'/0'/0'/0'`, publicKey: '00', diff --git a/src/app/lib/ledger.ts b/src/app/lib/ledger.ts index c6df15f955..7eaa4795dd 100644 --- a/src/app/lib/ledger.ts +++ b/src/app/lib/ledger.ts @@ -1,11 +1,13 @@ import { ContextSigner } from '@oasisprotocol/client/dist/signature' import OasisApp, { successOrThrow } from '@oasisprotocol/ledger' import { Response } from '@oasisprotocol/ledger/dist/types' -import { Wallet, WalletType } from 'app/state/wallet/types' +import { LedgerWalletType, Wallet, WalletType } from 'app/state/wallet/types' import { WalletError, WalletErrors } from 'types/errors' import { hex2uint, publicKeyToAddress } from './helpers' import type Transport from '@ledgerhq/hw-transport' import { isSupported, requestLedgerDevice } from '@ledgerhq/hw-transport-webusb/lib-es/webusb' +import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib' +import { Capacitor } from '@capacitor/core' interface LedgerAccount { publicKey: Uint8Array @@ -17,6 +19,13 @@ export async function canAccessNavigatorUsb(): Promise { return await isSupported() } +export async function canAccessBle(): Promise { + const hasBLE = await BleTransport.isEnabled().catch(() => false) + // Scan depends on requestLEScan method, which is not available on the web(feature flag) + const hasLEScan = Capacitor.isNativePlatform() || !!navigator?.bluetooth?.requestLEScan + return hasBLE && hasLEScan +} + export async function requestDevice(): Promise { if (await isSupported()) { return await requestLedgerDevice() @@ -99,13 +108,15 @@ export class LedgerSigner implements ContextSigner { protected transport?: Transport protected path: number[] protected publicKey: Uint8Array + transportType: LedgerWalletType constructor(wallet: Wallet) { - if (!wallet.path || wallet.type !== WalletType.Ledger) { + if (!wallet.path || (wallet.type !== WalletType.UsbLedger && wallet.type !== WalletType.BleLedger)) { throw new Error('Given wallet is not a ledger wallet') } this.path = wallet.path this.publicKey = hex2uint(wallet.publicKey) + this.transportType = wallet.type } public setTransport(transport: Transport) { diff --git a/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap b/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap index ab6805fe90..2f9f6a4a92 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/pages/ConnectDevicePage/__tests__/__snapshots__/index.test.tsx.snap @@ -252,7 +252,7 @@ exports[` should render component 1`] = ` >
  1. - ledger.instructionSteps.connectLedger + ledger.instructionSteps.connectUsbLedger
  2. ledger.instructionSteps.closeLedgerLive diff --git a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx b/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx index 627344b6bf..6c6513c310 100644 --- a/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx +++ b/src/app/pages/ConnectDevicePage/__tests__/index.test.tsx @@ -4,6 +4,7 @@ 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') @@ -31,7 +32,7 @@ describe('', () => { expect(screen.getByLabelText('Status is okay')).toBeInTheDocument() expect(screen.queryByRole('button')).not.toBeInTheDocument() expect(mockDispatch).toHaveBeenCalledWith({ - payload: undefined, + payload: WalletType.UsbLedger, type: importAccountsActions.enumerateAccountsFromLedger.type, }) }) diff --git a/src/app/pages/ConnectDevicePage/index.tsx b/src/app/pages/ConnectDevicePage/index.tsx index d41bdf8e2d..88b6a4bb8e 100644 --- a/src/app/pages/ConnectDevicePage/index.tsx +++ b/src/app/pages/ConnectDevicePage/index.tsx @@ -14,6 +14,7 @@ import { WalletErrors } from 'types/errors' import { importAccountsActions } from 'app/state/importaccounts' import { requestDevice } from 'app/lib/ledger' import logotype from '../../../../public/logo192.png' +import { WalletType } from '../../state/wallet/types' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' type ConnectionStatusIconPros = { @@ -53,7 +54,7 @@ export function ConnectDevicePage() { const device = await requestDevice() if (device) { setConnection('connected') - dispatch(importAccountsActions.enumerateAccountsFromLedger()) + dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) } } catch { setConnection('error') @@ -82,7 +83,10 @@ export function ConnectDevicePage() {
    1. - {t('ledger.instructionSteps.connectLedger', 'Connect your Ledger device to the computer')} + {t( + 'ledger.instructionSteps.connectUsbLedger', + 'Connect your USB Ledger device to the computer', + )}
    2. {t('ledger.instructionSteps.closeLedgerLive', 'Close Ledger Live app on the computer')}
    3. {t('ledger.instructionSteps.openOasisApp', 'Open the Oasis App on your Ledger device')}
    4. diff --git a/src/app/pages/OpenWalletPage/Features/FromBleLedger/__tests__/__snapshots__/index.test.tsx.snap b/src/app/pages/OpenWalletPage/Features/FromBleLedger/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..f0ccc0b383 --- /dev/null +++ b/src/app/pages/OpenWalletPage/Features/FromBleLedger/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,218 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render component 1`] = ` +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + margin: 12px; + background-color: #FFFFFF; + color: #444444; + border: solid 1px background-front-border; + min-width: 0; + min-height: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 24px; + border-radius: 5px; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + margin-top: 24px; + min-width: 0; + min-height: 0; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c4 { + display: inline-block; + box-sizing: border-box; + cursor: pointer; + font: inherit; + -webkit-text-decoration: none; + text-decoration: none; + margin: 0; + background: transparent; + overflow: visible; + text-transform: none; + border: 2px solid #7D4CDB; + border-radius: 18px; + color: #444444; + padding: 4px 22px; + font-size: 18px; + line-height: 24px; + background-color: #7D4CDB; + color: #f8f8f8; + border-radius: 18px; + -webkit-transition-property: color,background-color,border-color,box-shadow; + transition-property: color,background-color,border-color,box-shadow; + -webkit-transition-duration: 0.1s; + transition-duration: 0.1s; + -webkit-transition-timing-function: ease-in-out; + transition-timing-function: ease-in-out; +} + +.c4:hover { + box-shadow: 0px 0px 0px 2px #7D4CDB; +} + +.c4:focus { + outline: none; + box-shadow: 0 0 2px 2px #6FFFB0; +} + +.c4:focus > circle, +.c4:focus > ellipse, +.c4:focus > line, +.c4:focus > path, +.c4:focus > polygon, +.c4:focus > polyline, +.c4:focus > rect { + outline: none; + box-shadow: 0 0 2px 2px #6FFFB0; +} + +.c4:focus::-moz-focus-inner { + border: 0; +} + +.c4:focus:not(:focus-visible) { + outline: none; + box-shadow: none; +} + +.c4:focus:not(:focus-visible) > circle,.c4:focus:not(:focus-visible) > ellipse, +.c4:focus:not(:focus-visible) > line,.c4:focus:not(:focus-visible) > path, +.c4:focus:not(:focus-visible) > polygon,.c4:focus:not(:focus-visible) > polyline, +.c4:focus:not(:focus-visible) > rect { + outline: none; + box-shadow: none; +} + +.c4:focus:not(:focus-visible)::-moz-focus-inner { + border: 0; +} + +.c1 { + margin-top: 0px; + font-size: 34px; + line-height: 40px; + max-width: 816px; + font-weight: 600; + overflow-wrap: break-word; +} + +.c2 { + margin: 0; + font-size: 26px; + line-height: 32px; + max-width: 624px; + font-weight: 600; + overflow-wrap: break-word; +} + +@media only screen and (max-width:768px) { + .c0 { + margin: 6px; + } +} + +@media only screen and (max-width:768px) { + .c0 { + border: solid 1px background-front-border; + } +} + +@media only screen and (max-width:768px) { + .c0 { + padding: 12px; + } +} + +@media only screen and (max-width:768px) { + .c3 { + margin-top: 12px; + } +} + +@media only screen and (max-width:768px) { + .c1 { + margin-top: 0px; + } +} + +@media only screen and (max-width:768px) { + .c1 { + font-size: 26px; + line-height: 32px; + max-width: 624px; + } +} + +@media only screen and (max-width:768px) { + .c2 { + margin: 0; + } +} + +@media only screen and (max-width:768px) { + .c2 { + font-size: 18px; + line-height: 24px; + max-width: 432px; + } +} + +
      +
      +

      + openWallet.ledger.header +

      +

      + ledger.instructionSteps.header +

      +
        +
      1. + ledger.instructionSteps.connectBluetoothLedger +
      2. +
      3. + ledger.instructionSteps.deviceIsPaired +
      4. +
      5. + ledger.instructionSteps.closeLedgerLive +
      6. +
      7. + ledger.instructionSteps.openOasisApp +
      8. +
      +
      + +
      +
      +
      +`; diff --git a/src/app/pages/OpenWalletPage/Features/FromBleLedger/__tests__/index.test.tsx b/src/app/pages/OpenWalletPage/Features/FromBleLedger/__tests__/index.test.tsx new file mode 100644 index 0000000000..e3bb54af26 --- /dev/null +++ b/src/app/pages/OpenWalletPage/Features/FromBleLedger/__tests__/index.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' +import { render } from '@testing-library/react' +import { FromBleLedger } from '..' + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), + useDispatch: jest.fn(), +})) + +describe('', () => { + it('should render component', () => { + const { container } = render() + + expect(container).toMatchSnapshot() + }) +}) diff --git a/src/app/pages/OpenWalletPage/Features/FromBleLedger/index.tsx b/src/app/pages/OpenWalletPage/Features/FromBleLedger/index.tsx new file mode 100644 index 0000000000..f8f6ef0ab4 --- /dev/null +++ b/src/app/pages/OpenWalletPage/Features/FromBleLedger/index.tsx @@ -0,0 +1,79 @@ +import { importAccountsActions } from 'app/state/importaccounts' +import { Box } from 'grommet/es6/components/Box' +import { Button } from 'grommet/es6/components/Button' +import { Heading } from 'grommet/es6/components/Heading' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { + selectShowAccountsSelectionModal, + selectShowBleLedgerDevicesModal, +} from 'app/state/importaccounts/selectors' +import { Header } from 'app/components/Header' +import { ListBleLedgerDevicesModal } from '../ListBleLedgerDevicesModal' +import { ImportAccountsSelectionModal } from '../ImportAccountsSelectionModal' +import { WalletType } from '../../../../state/wallet/types' + +export function FromBleLedger() { + const { t } = useTranslation() + const dispatch = useDispatch() + const showAccountsSelectionModal = useSelector(selectShowAccountsSelectionModal) + const showBleLedgerDevicesModal = useSelector(selectShowBleLedgerDevicesModal) + + return ( + +
      {t('openWallet.ledger.header', 'Open from Ledger device')}
      + + + {t('ledger.instructionSteps.header', 'Steps:')} + +
        +
      1. + {t( + 'ledger.instructionSteps.connectBluetoothLedger', + 'Connect your Bluetooth Ledger device to the device', + )} +
      2. +
      3. + {t('ledger.instructionSteps.deviceIsPaired', 'Make sure your Ledger is paired with the device')} +
      4. +
      5. {t('ledger.instructionSteps.closeLedgerLive', 'Close Ledger Live app on the device')}
      6. +
      7. {t('ledger.instructionSteps.openOasisApp', 'Open the Oasis App on your Ledger device')}
      8. +
      + + +
      + + + errors.usbTransportNotSupported + +
      +
      +
      + + + errors.bluetoothTransportNotSupported + +
      diff --git a/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/index.test.tsx b/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/index.test.tsx index f2462faf95..53a7bc91c5 100644 --- a/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/index.test.tsx +++ b/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/index.test.tsx @@ -1,16 +1,54 @@ import * as React from 'react' -import { render } from '@testing-library/react' +import { render, act, screen } from '@testing-library/react' import { FromLedger } from '..' +import { MemoryRouter } from 'react-router-dom' jest.mock('react-redux', () => ({ useSelector: jest.fn(), useDispatch: jest.fn(), })) +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => jest.fn(), +})) + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => jest.fn(), +})) + +jest.mock('../../../../../lib/ledger', () => ({ + ...jest.requireActual('../../../../../lib/ledger'), + // Throws BLE not supported + canAccessBle: () => jest.fn().mockReturnValue(false), +})) + +const renderComponent = () => + render( + + + , + ) + describe('', () => { - it('should render component', () => { - const { container } = render() + it('should render component', async () => { + const { container } = renderComponent() + + await act(() => Promise.resolve()) expect(container).toMatchSnapshot() + + expect(screen.getByText('errors.usbTransportNotSupported')).toBeInTheDocument() + expect(screen.getByText('errors.bluetoothTransportNotSupported')).toBeInTheDocument() + }) + + it('should render component with an ledger access button', async () => { + renderComponent() + + await act(() => Promise.resolve()) + + expect(screen.queryByText('openWallet.importAccounts.usbLedger')).toBeInTheDocument() + expect(screen.queryByText('openWallet.importAccounts.bluetoothLedger')).toBeInTheDocument() }) }) diff --git a/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx b/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx index ac5d625343..fc191b5782 100644 --- a/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx +++ b/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx @@ -1,56 +1,93 @@ -import { importAccountsActions } from 'app/state/importaccounts' import { Box } from 'grommet/es6/components/Box' +import React, { useEffect } from 'react' +import { Header } from 'app/components/Header' +import { ButtonLink } from '../../../../components/ButtonLink' import { Button } from 'grommet/es6/components/Button' -import { Heading } from 'grommet/es6/components/Heading' -import React from 'react' +import { Text } from 'grommet/es6/components/Text' +import { canAccessBle, canAccessNavigatorUsb } from '../../../../lib/ledger' import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' -import { ImportAccountsSelectionModal } from 'app/pages/OpenWalletPage/Features/ImportAccountsSelectionModal' -import { selectShowAccountsSelectionModal } from 'app/state/importaccounts/selectors' -import { Header } from 'app/components/Header' -import { WalletType } from 'app/state/wallet/types' -export function FromLedger() { +type SelectOpenMethodProps = { + webExtensionLedgerAccess?: () => void + disableBluetoothLedger?: boolean +} + +export function FromLedger({ webExtensionLedgerAccess, disableBluetoothLedger }: SelectOpenMethodProps) { const { t } = useTranslation() - const dispatch = useDispatch() - const showAccountsSelectionModal = useSelector(selectShowAccountsSelectionModal) + const [supportsUsbLedger, setSupportsUsbLedger] = React.useState(true) + const [supportsBleLedger, setSupportsBleLedger] = React.useState(true) + + useEffect(() => { + async function getLedgerSupport() { + const usbLedgerSupported = await canAccessNavigatorUsb() + const bleLedgerSupported = !disableBluetoothLedger && (await canAccessBle()) + + setSupportsUsbLedger(usbLedgerSupported) + setSupportsBleLedger(bleLedgerSupported) + } + + getLedgerSupport() + }, [disableBluetoothLedger]) return ( -
      {t('openWallet.ledger.header', 'Open from Ledger device')}
      +
      + {t('openWallet.importAccounts.connectDeviceHeader', 'How do you want to connect your Ledger device?')} +
      - - {t('ledger.instructionSteps.header', 'Steps:')} - -
        -
      1. {t('ledger.instructionSteps.connectLedger', 'Connect your Ledger device to the computer')}
      2. -
      3. {t('ledger.instructionSteps.closeLedgerLive', 'Close Ledger Live app on the computer')}
      4. -
      5. {t('ledger.instructionSteps.openOasisApp', 'Open the Oasis App on your Ledger device')}
      6. -
      - - + + + +`; diff --git a/src/app/pages/OpenWalletPage/Features/FromUsbLedger/__tests__/index.test.tsx b/src/app/pages/OpenWalletPage/Features/FromUsbLedger/__tests__/index.test.tsx new file mode 100644 index 0000000000..486b7c7696 --- /dev/null +++ b/src/app/pages/OpenWalletPage/Features/FromUsbLedger/__tests__/index.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' +import { render } from '@testing-library/react' +import { FromUsbLedger } from '..' + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), + useDispatch: jest.fn(), +})) + +describe('', () => { + it('should render component', () => { + const { container } = render() + + expect(container).toMatchSnapshot() + }) +}) diff --git a/src/app/pages/OpenWalletPage/Features/FromUsbLedger/index.tsx b/src/app/pages/OpenWalletPage/Features/FromUsbLedger/index.tsx new file mode 100644 index 0000000000..d0f3899e4f --- /dev/null +++ b/src/app/pages/OpenWalletPage/Features/FromUsbLedger/index.tsx @@ -0,0 +1,58 @@ +import { importAccountsActions } from 'app/state/importaccounts' +import { Box } from 'grommet/es6/components/Box' +import { Button } from 'grommet/es6/components/Button' +import { Heading } from 'grommet/es6/components/Heading' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { ImportAccountsSelectionModal } from 'app/pages/OpenWalletPage/Features/ImportAccountsSelectionModal' +import { selectShowAccountsSelectionModal } from 'app/state/importaccounts/selectors' +import { Header } from 'app/components/Header' +import { WalletType } from 'app/state/wallet/types' + +export function FromUsbLedger() { + const { t } = useTranslation() + const dispatch = useDispatch() + const showAccountsSelectionModal = useSelector(selectShowAccountsSelectionModal) + + return ( + +
      {t('openWallet.ledger.header', 'Open from Ledger device')}
      + + + {t('ledger.instructionSteps.header', 'Steps:')} + +
        +
      1. + {t('ledger.instructionSteps.connectUsbLedger', 'Connect your USB Ledger device to the device')} +
      2. +
      3. {t('ledger.instructionSteps.closeLedgerLive', 'Close Ledger Live app on the device')}
      4. +
      5. {t('ledger.instructionSteps.openOasisApp', 'Open the Oasis App on your Ledger device')}
      6. +
      + + - - - - + - errors.usbTransportNotSupported - - + + +
      ', () => { const { container } = renderComponent() expect(container).toMatchSnapshot() - expect(screen.getByText('errors.usbTransportNotSupported')).toBeInTheDocument() - }) - - it('should render component with an access button', () => { - renderComponent(() => {}) - - expect(screen.queryByText('openWallet.method.ledger')).not.toBeInTheDocument() - expect(screen.getByText('ledger.extension.grantAccess')).toBeInTheDocument() }) it('should redirect user to ledger page', () => { @@ -51,20 +43,6 @@ describe('', () => { renderComponent(() => {}) - expect(mockNavigate).toHaveBeenCalledWith('/open-wallet/ledger') - }) - - it('should render variant with web usb support', async () => { - jest.mocked(canAccessNavigatorUsb).mockResolvedValue(true) - - const { rerender } = renderComponent() - rerender( - - - , - ) - - await waitForElementToBeRemoved(() => screen.queryByText('errors.usbTransportNotSupported')) - expect(screen.getByRole('button', { name: 'openWallet.method.ledger' })).not.toBeDisabled() + expect(mockNavigate).toHaveBeenCalledWith('/open-wallet/ledger/usb') }) }) diff --git a/src/app/pages/OpenWalletPage/index.tsx b/src/app/pages/OpenWalletPage/index.tsx index 19aaa3e887..dbc6093e29 100644 --- a/src/app/pages/OpenWalletPage/index.tsx +++ b/src/app/pages/OpenWalletPage/index.tsx @@ -5,8 +5,6 @@ */ import { Anchor } from 'grommet/es6/components/Anchor' import { Box } from 'grommet/es6/components/Box' -import { Button } from 'grommet/es6/components/Button' -import { Text } from 'grommet/es6/components/Text' import React, { useEffect } from 'react' import { useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' @@ -14,7 +12,7 @@ import { Trans, useTranslation } from 'react-i18next' import { ButtonLink } from 'app/components/ButtonLink' import { Header } from 'app/components/Header' import { selectShowAccountsSelectionModal } from 'app/state/importaccounts/selectors' -import { canAccessNavigatorUsb } from 'app/lib/ledger' +import { canAccessBle, canAccessNavigatorUsb } from 'app/lib/ledger' type SelectOpenMethodProps = { webExtensionLedgerAccess?: () => void @@ -22,22 +20,22 @@ type SelectOpenMethodProps = { export function SelectOpenMethod({ webExtensionLedgerAccess }: SelectOpenMethodProps) { const { t } = useTranslation() - const [supportsWebUsb, setSupportsWebUsb] = React.useState(false) + const [supportsLedger, setSupportsLedger] = React.useState(true) const navigate = useNavigate() const showAccountsSelectionModal = useSelector(selectShowAccountsSelectionModal) useEffect(() => { if (webExtensionLedgerAccess && showAccountsSelectionModal) { - navigate('/open-wallet/ledger') + navigate('/open-wallet/ledger/usb') } }, [navigate, showAccountsSelectionModal, webExtensionLedgerAccess]) useEffect(() => { - async function getWebUsb() { - setSupportsWebUsb(await canAccessNavigatorUsb()) + async function getLedgerSupport() { + setSupportsLedger((await canAccessNavigatorUsb()) || (await canAccessBle())) } - getWebUsb() + getLedgerSupport() }, []) return ( @@ -57,36 +55,14 @@ export function SelectOpenMethod({ webExtensionLedgerAccess }: SelectOpenMethodP -
      -
      - {webExtensionLedgerAccess ? ( -
      - {!supportsWebUsb && ( - - {t( - 'errors.usbTransportNotSupported', - 'Your browser does not support WebUSB (e.g. Firefox). Try using Chrome.', - )} - - )} -
      + + + openLedgerAccessPopup(href)} /> } + +export function FromLedgerWebExtension() { + const href = useHref('/open-wallet/connect-device') + + return openLedgerAccessPopup(href)} disableBluetoothLedger /> +} diff --git a/src/app/pages/ParaTimesPage/useParaTimesNavigation.ts b/src/app/pages/ParaTimesPage/useParaTimesNavigation.ts index 9f16330642..7559d06836 100644 --- a/src/app/pages/ParaTimesPage/useParaTimesNavigation.ts +++ b/src/app/pages/ParaTimesPage/useParaTimesNavigation.ts @@ -24,7 +24,7 @@ export const useParaTimesNavigation = (): ParaTimesNavigationHook => { const { t } = useTranslation() const dispatch = useDispatch() const walletType = useSelector(selectType) - const canAccessParaTimesRoute = backend() === BackendAPIs.OasisScan && walletType !== WalletType.Ledger + const canAccessParaTimesRoute = backend() === BackendAPIs.OasisScan && walletType !== WalletType.UsbLedger const getParaTimesRoutePath = (address: string) => `/account/${address}/paratimes` const navigateToDeposit = useCallback(() => dispatch(paraTimesActions.navigateToDeposit()), [dispatch]) const navigateToWithdraw = useCallback(() => dispatch(paraTimesActions.navigateToWithdraw()), [dispatch]) diff --git a/src/app/state/importaccounts/index.ts b/src/app/state/importaccounts/index.ts index 5bdafbfa7a..dde3e34094 100644 --- a/src/app/state/importaccounts/index.ts +++ b/src/app/state/importaccounts/index.ts @@ -2,12 +2,16 @@ import { PayloadAction } from '@reduxjs/toolkit' import { ErrorPayload } from 'types/errors' import { createSlice } from 'utils/@reduxjs/toolkit' import { ImportAccountsListAccount, ImportAccountsState, ImportAccountsStep } from './types' +import { ScanResult } from '@capacitor-community/bluetooth-le' +import { LedgerWalletType } from '../wallet/types' export const initialState: ImportAccountsState = { accounts: [], showAccountsSelectionModal: false, accountsSelectionPageNumber: 0, step: ImportAccountsStep.Idle, + bleDevices: [], + showBleLedgerDevicesModal: false, } const slice = createSlice({ @@ -19,14 +23,23 @@ const slice = createSlice({ state.error = undefined state.step = ImportAccountsStep.Idle state.showAccountsSelectionModal = false + state.bleDevices = [] + state.showBleLedgerDevicesModal = false }, - enumerateAccountsFromLedger(state) { + enumerateDevicesFromBleLedger(state) { + state.bleDevices = [] + state.step = ImportAccountsStep.Idle + state.showBleLedgerDevicesModal = true + state.bleDevices = [] + state.selectedBleDevice = undefined + }, + enumerateAccountsFromLedger(state, _action: PayloadAction) { state.accounts = [] state.accountsSelectionPageNumber = 0 state.showAccountsSelectionModal = true state.step = ImportAccountsStep.Idle }, - enumerateMoreAccountsFromLedger(state) { + enumerateMoreAccountsFromLedger(state, _action: PayloadAction) { state.step = ImportAccountsStep.Idle }, enumerateAccountsFromMnemonic(state, _action: PayloadAction) { @@ -63,6 +76,12 @@ const slice = createSlice({ state.error = action.payload state.step = ImportAccountsStep.Idle }, + setBleDevices(state, { payload }: PayloadAction) { + state.bleDevices = payload + }, + setSelectedBleDevice(state, { payload }: PayloadAction) { + state.selectedBleDevice = payload + }, }, }) diff --git a/src/app/state/importaccounts/saga.test.ts b/src/app/state/importaccounts/saga.test.ts index f3e09e1298..b67e0f21a8 100644 --- a/src/app/state/importaccounts/saga.test.ts +++ b/src/app/state/importaccounts/saga.test.ts @@ -12,6 +12,8 @@ import { OasisTransaction } from 'app/lib/transaction' import { WalletType } from 'app/state/wallet/types' import delayP from '@redux-saga/delay-p' import { getAccountBalanceWithFallback } from '../../lib/getAccountBalanceWithFallback' +import { ScanResult } from '@capacitor-community/bluetooth-le' +import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib' describe('importAccounts Sagas', () => { describe('enumerateAccountsFromLedger', () => { @@ -26,12 +28,17 @@ describe('importAccounts Sagas', () => { .withState({}) .provide([ [matchers.call.fn(TransportWebUSB.isSupported), true], - [matchers.call.fn(TransportWebUSB.create), { close: () => {} }], + [ + matchers.call.fn(TransportWebUSB.create), + { + close: () => {}, + }, + ], [matchers.call.fn(Ledger.getOasisApp), undefined], [matchers.call.fn(Ledger.deriveAccountUsingOasisApp), validAccount], [matchers.call.fn(getAccountBalanceWithFallback), {}], ]) - .dispatch(importAccountsActions.enumerateAccountsFromLedger()) + .dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) .put.actionType(importAccountsActions.accountGenerated.type) .put.actionType(importAccountsActions.accountGenerated.type) .put.actionType(importAccountsActions.accountGenerated.type) @@ -43,14 +50,53 @@ describe('importAccounts Sagas', () => { .silentRun(50) }) + it('should list ble devices', async () => { + const bleDevices: ScanResult[] = [] + for (let i = 0; i < 3; i++) { + bleDevices.push({ + device: { + deviceId: `${i}${i}:${i}${i}:${i}${i}:${i}${i}:${i}${i}:${i}${i}`, + name: `Nano X ABC${i}`, + }, + localName: `Nano X ABC${i}`, + rssi: -50, + txPower: 100, + }) + } + + return expectSaga(importAccountsSaga) + .withState({}) + .provide([ + [matchers.call.fn(BleTransport.isSupported), true], + [matchers.call.fn(BleTransport.list), bleDevices], + ]) + .dispatch(importAccountsActions.enumerateDevicesFromBleLedger) + .put.like({ action: { payload: bleDevices } }) + .silentRun(50) + }) + + it('should handle unsupported ble', async () => { + return expectSaga(importAccountsSaga) + .withState({}) + .provide([[matchers.call.fn(BleTransport.isSupported), false]]) + .dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.BleLedger)) + .put.like({ action: { payload: { code: WalletErrors.BluetoothTransportNotSupported } } }) + .silentRun(50) + }) + it('should handle unsupported browsers', async () => { return expectSaga(importAccountsSaga) .withState({}) .provide([ [matchers.call.fn(TransportWebUSB.isSupported), false], - [matchers.call.fn(TransportWebUSB.create), { close: () => {} }], + [ + matchers.call.fn(TransportWebUSB.create), + { + close: () => {}, + }, + ], ]) - .dispatch(importAccountsActions.enumerateAccountsFromLedger()) + .dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) .put.like({ action: { payload: { code: WalletErrors.USBTransportNotSupported } } }) .silentRun(50) }) @@ -62,7 +108,7 @@ describe('importAccounts Sagas', () => { [matchers.call.fn(TransportWebUSB.isSupported), true], [matchers.call.fn(TransportWebUSB.create), Promise.reject(new Error('No device selected'))], ]) - .dispatch(importAccountsActions.enumerateAccountsFromLedger()) + .dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) .put.like({ action: { payload: { code: WalletErrors.LedgerNoDeviceSelected } } }) .silentRun(50) }) @@ -74,7 +120,7 @@ describe('importAccounts Sagas', () => { [matchers.call.fn(TransportWebUSB.isSupported), true], [matchers.call.fn(TransportWebUSB.create), Promise.reject(new Error('Dummy error'))], ]) - .dispatch(importAccountsActions.enumerateAccountsFromLedger()) + .dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) .put.like({ action: { payload: { code: WalletErrors.USBTransportError, message: 'Dummy error' } } }) .silentRun(50) }) @@ -84,11 +130,16 @@ describe('importAccounts Sagas', () => { .withState({}) .provide([ [matchers.call.fn(TransportWebUSB.isSupported), true], - [matchers.call.fn(TransportWebUSB.create), { close: () => {} }], + [ + matchers.call.fn(TransportWebUSB.create), + { + close: () => {}, + }, + ], [matchers.call.fn(Ledger.getOasisApp), undefined], [matchers.call.fn(Ledger.deriveAccountUsingOasisApp), Promise.reject(new Error('Dummy error'))], ]) - .dispatch(importAccountsActions.enumerateAccountsFromLedger()) + .dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger)) .put.like({ action: { payload: { code: WalletErrors.UnknownError, message: 'Dummy error' } } }) .silentRun(50) }) diff --git a/src/app/state/importaccounts/saga.ts b/src/app/state/importaccounts/saga.ts index 76d3abd42a..3967256831 100644 --- a/src/app/state/importaccounts/saga.ts +++ b/src/app/state/importaccounts/saga.ts @@ -6,7 +6,7 @@ import { Ledger, LedgerSigner } from 'app/lib/ledger' import { OasisTransaction } from 'app/lib/transaction' import { all, call, delay, fork, put, select, takeEvery } from 'typed-redux-saga' import { ErrorPayload, WalletError, WalletErrors } from 'types/errors' -import { WalletType } from 'app/state/wallet/types' +import { LedgerWalletType, WalletType } from 'app/state/wallet/types' import { importAccountsActions } from '.' import { selectChainContext } from '../network/selectors' import { ImportAccountsListAccount, ImportAccountsStep } from './types' @@ -14,24 +14,58 @@ import type Transport from '@ledgerhq/hw-transport' import { selectImportAccountHasMissingBalances, selectImportAccounts, - selectImportAccountsOnCurrentPage, selectImportAccountsFullList, + selectImportAccountsOnCurrentPage, selectImportAccountsPageNumber, + selectSelectedBleDevice, } from './selectors' import { getAccountBalanceWithFallback } from '../../lib/getAccountBalanceWithFallback' +import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib' +import { ScanResult } from '@capacitor-community/bluetooth-le' function* setStep(step: ImportAccountsStep) { yield* put(importAccountsActions.setStep(step)) } +function* isBluetoothSupported() { + const isSupported = yield* call([BleTransport, BleTransport.isSupported]) + // https://github.com/WebBluetoothCG/web-bluetooth/blob/main/implementation-status.md#scanning-api + // !navigator.bluetooth + if (!isSupported) { + throw new WalletError(WalletErrors.BluetoothTransportNotSupported, 'Bluetooth Transport unsupported') + } +} + +function* getBluetoothDevices() { + yield* call(isBluetoothSupported) + return yield* call(BleTransport.list) +} + +function* getBluetoothTransport(device?: ScanResult) { + yield* call(isBluetoothSupported) + + if (!device) { + throw new WalletError(WalletErrors.LedgerNoDeviceSelected, 'No device selected') + } + + try { + return yield* call(BleTransport.open, device) + } catch (e: any) { + if (e.message.match(/No device selected/)) { + throw new WalletError(WalletErrors.LedgerNoDeviceSelected, e.message) + } else { + throw new WalletError(WalletErrors.USBTransportError, e.message) + } + } +} + function* getUSBTransport() { const isSupported = yield* call([TransportWebUSB, TransportWebUSB.isSupported]) if (!isSupported) { throw new WalletError(WalletErrors.USBTransportNotSupported, 'TransportWebUSB unsupported') } try { - const transport = yield* call([TransportWebUSB, TransportWebUSB.create]) - return transport + return yield* call([TransportWebUSB, TransportWebUSB.create]) } catch (e: any) { if (e.message.match(/No device selected/)) { throw new WalletError(WalletErrors.LedgerNoDeviceSelected, e.message) @@ -122,20 +156,33 @@ function* ensureAllBalancesArePresentOnCurrentPage() { yield* all(accounts.filter(a => !a.balance).map(a => call(fetchBalanceForAccount, a))) } +function* enumerateDevicesFromBleLedger() { + yield* setStep(ImportAccountsStep.LoadingBleDevices) + const devices = yield* getBluetoothDevices() + yield* put(importAccountsActions.setBleDevices(devices)) + yield* setStep(ImportAccountsStep.Idle) +} + /** * Enumerate more accounts from Ledger, enough to fill up one page. */ -function* enumerateAccountsFromLedger() { +function* enumerateAccountsFromLedger(action: PayloadAction) { const existingAccounts = yield* select(selectImportAccountsFullList) const pageNumber = yield* select(selectImportAccountsPageNumber) if (existingAccounts.length >= (pageNumber + 1) * accountsPerPage) { // Selected page was already enumerated. return } - yield* setStep(ImportAccountsStep.OpeningUSB) + yield* setStep(ImportAccountsStep.AccessingLedger) let transport: Transport | undefined try { - transport = yield* getUSBTransport() + if (action.payload === WalletType.BleLedger) { + const device = yield* select(selectSelectedBleDevice) + transport = yield* getBluetoothTransport(device) + } else { + transport = yield* getUSBTransport() + } + const existingAccounts = yield* select(selectImportAccountsFullList) const start = existingAccounts.length @@ -152,7 +199,7 @@ function* enumerateAccountsFromLedger() { address, // We select the first account by default selected: index === 0, - type: WalletType.Ledger, + type: action.payload, } as ImportAccountsListAccount yield* put(importAccountsActions.accountGenerated(wallet)) yield* fork(fetchBalanceForAccount, wallet) @@ -179,7 +226,13 @@ function* enumerateAccountsFromLedger() { } export function* sign(signer: LedgerSigner, tw: oasis.consensus.TransactionWrapper) { - const transport = yield* getUSBTransport() + let transport + if (signer.transportType === WalletType.BleLedger) { + const bleDevice = yield* select(selectSelectedBleDevice) + transport = yield* getBluetoothTransport(bleDevice) + } else { + transport = yield* getUSBTransport() + } const chainContext = yield* select(selectChainContext) signer.setTransport(transport) @@ -195,4 +248,5 @@ export function* importAccountsSaga() { yield* takeEvery(importAccountsActions.enumerateMoreAccountsFromLedger, enumerateAccountsFromLedger) yield* takeEvery(importAccountsActions.enumerateAccountsFromMnemonic, enumerateAccountsFromMnemonic) yield* takeEvery(importAccountsActions.setPage, ensureAllBalancesArePresentOnCurrentPage) + yield* takeEvery(importAccountsActions.enumerateDevicesFromBleLedger, enumerateDevicesFromBleLedger) } diff --git a/src/app/state/importaccounts/selectors.ts b/src/app/state/importaccounts/selectors.ts index 89bc91d821..5741861e54 100644 --- a/src/app/state/importaccounts/selectors.ts +++ b/src/app/state/importaccounts/selectors.ts @@ -28,3 +28,10 @@ export const selectImportAccountHasMissingBalances = createSelector( export const selectSelectedAccounts = createSelector([selectImportAccountsFullList], state => state.filter(a => a.selected), ) +export const selectBleDevices = createSelector([selectSlice], state => state.bleDevices) +export const selectSelectedBleDevice = createSelector([selectSlice], state => state.selectedBleDevice) +export const selectImportAccountsStep = createSelector([selectSlice], state => state.step) +export const selectShowBleLedgerDevicesModal = createSelector( + [selectSlice], + state => state.showBleLedgerDevicesModal, +) diff --git a/src/app/state/importaccounts/types.ts b/src/app/state/importaccounts/types.ts index a6cc2c2f02..098617a85c 100644 --- a/src/app/state/importaccounts/types.ts +++ b/src/app/state/importaccounts/types.ts @@ -1,6 +1,7 @@ import { ErrorPayload } from 'types/errors' import { WalletType } from '../wallet/types' import { BalanceDetails } from '../account/types' +import { ScanResult } from '@capacitor-community/bluetooth-le' /* --- STATE --- */ export interface ImportAccountsListAccount { @@ -16,9 +17,10 @@ export interface ImportAccountsListAccount { export enum ImportAccountsStep { Idle = 'idle', - OpeningUSB = 'opening_usb', + AccessingLedger = 'accessing_ledger', LoadingAccounts = 'loading_accounts', LoadingBalances = 'loading_balances', + LoadingBleDevices = 'loading_devices', } export interface ImportAccountsState { @@ -27,4 +29,7 @@ export interface ImportAccountsState { showAccountsSelectionModal: boolean accountsSelectionPageNumber: number step: ImportAccountsStep + bleDevices: ScanResult[] + selectedBleDevice?: ScanResult + showBleLedgerDevicesModal: boolean } diff --git a/src/app/state/transaction/saga.test.ts b/src/app/state/transaction/saga.test.ts index c2ffb21d1d..7d4de5043b 100644 --- a/src/app/state/transaction/saga.test.ts +++ b/src/app/state/transaction/saga.test.ts @@ -7,16 +7,22 @@ import { DeepPartialRootState } from 'types/RootState' import { WalletErrors } from 'types/errors' import { transactionActions as actions } from '.' import { TransactionTypes } from '../paratimes/types' -import { selectAddress, selectActiveWallet } from '../wallet/selectors' +import { selectActiveWallet, selectAddress } from '../wallet/selectors' import { Wallet, WalletType } from '../wallet/types' -import { doTransaction, submitParaTimeTransaction, getAllowanceDifference, setAllowance } from './saga' +import { doTransaction, getAllowanceDifference, setAllowance, submitParaTimeTransaction } from './saga' +import { addressToPublicKey } from '../../lib/helpers' +import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib' -const makeState = (wallet: Partial): DeepPartialRootState => { +const makeState = ( + wallet: Partial, + rootState: Partial = {}, +): DeepPartialRootState => { return { wallet: { wallets: { [wallet.address!]: wallet }, selectedWallet: wallet.address, }, + ...rootState, } } @@ -52,6 +58,7 @@ describe('Transaction Sagas', () => { [matchers.call.fn(OasisTransaction.sign), {}], [matchers.call.fn(OasisTransaction.signParaTime), {}], [matchers.call.fn(OasisTransaction.submit), {}], + [matchers.call.fn(OasisTransaction.signUsingLedger), {}], ] const providers: (EffectProviders | StaticProvider)[] = [ @@ -79,6 +86,52 @@ describe('Transaction Sagas', () => { .run() }) + it('Should send transactions from a Bluetooth Ledger Wallet', async () => { + const wallet = { + ...validKeyWallet, + type: WalletType.BleLedger, + path: [44, 474, 0, 0, 0], + publicKey: (await addressToPublicKey(matchingAddress)).toString(), + } as Partial + + const selectedBleDevice = { + device: { + deviceId: 'xx:xx:xx:xx:xx:xx', + name: 'Nano X ABCD', + }, + localName: 'Nano X ABCD', + rssi: -50, + txPower: 100, + } + + return expectSaga( + doTransaction, + actions.sendTransaction({ type: 'transfer', amount: '10000000000', to: validAddress }), + ) + .withState( + makeState(wallet, { + importAccounts: { + selectedBleDevice, + }, + }), + ) + .provide(providers) + .provide([ + ...sendProviders, + [matchers.call.fn(BleTransport.isSupported), true], + [ + matchers.call.fn(BleTransport.open), + { + close: () => {}, + }, + ], + ]) + .dispatch(actions.sendTransaction({ type: 'transfer', amount: '123000000000', to: 'testaddress' })) + .dispatch(actions.confirmTransaction()) + .put.actionType(actions.transactionSent.type) + .run() + }) + const runtime = { address: 'oasis1qrnu9yhwzap7rqh6tdcdcpz0zf86hwhycchkhvt8', id: '000000000000000000000000000000000000000000000000e199119c992377cb', diff --git a/src/app/state/transaction/saga.ts b/src/app/state/transaction/saga.ts index 3fb25d77cf..8b10ccf824 100644 --- a/src/app/state/transaction/saga.ts +++ b/src/app/state/transaction/saga.ts @@ -1,22 +1,21 @@ import { client, misc } from '@oasisprotocol/client' import { Signer } from '@oasisprotocol/client/dist/signature' import { PayloadAction } from '@reduxjs/toolkit' -import { hex2uint, isValidAddress, uint2bigintString, parseRoseStringToBaseUnitString } from 'app/lib/helpers' +import { hex2uint, isValidAddress, parseRoseStringToBaseUnitString, uint2bigintString } from 'app/lib/helpers' import { LedgerSigner } from 'app/lib/ledger' -import { OasisTransaction, signerFromPrivateKey, signerFromEthPrivateKey, TW } from 'app/lib/transaction' +import { OasisTransaction, signerFromEthPrivateKey, signerFromPrivateKey, TW } from 'app/lib/transaction' import { getEvmBech32Address, privateToEthAddress } from 'app/lib/eth-helpers' import { call, delay, put, race, select, take, takeEvery } from 'typed-redux-saga' import { ErrorPayload, ExhaustedTypeError, WalletError, WalletErrors } from 'types/errors' import { transactionActions } from '.' import { sign } from '../importaccounts/saga' import { getOasisNic } from '../network/saga' -import { selectAccountAddress } from '../account/selectors' -import { selectAccountAllowances } from '../account/selectors' +import { selectAccountAddress, selectAccountAllowances } from '../account/selectors' import { selectChainContext } from '../network/selectors' import { selectActiveWallet } from '../wallet/selectors' import { Wallet, WalletType } from '../wallet/types' import { TransactionPayload, TransactionStep } from './types' -import { Runtime, ParaTimeTransaction, TransactionTypes } from '../paratimes/types' +import { ParaTimeTransaction, Runtime, TransactionTypes } from '../paratimes/types' export function* transactionSaga() { yield* takeEvery(transactionActions.sendTransaction, doTransaction) @@ -60,7 +59,7 @@ function* getSigner() { if (wallet.type === WalletType.PrivateKey || wallet.type === WalletType.Mnemonic) { const bytes = hex2uint(privateKey!) signer = yield* call(signerFromPrivateKey, bytes) - } else if (wallet.type === WalletType.Ledger) { + } else if (wallet.type === WalletType.UsbLedger || wallet.type === WalletType.BleLedger) { signer = new LedgerSigner(wallet) } else { throw new ExhaustedTypeError('Invalid wallet type', wallet.type) @@ -191,7 +190,7 @@ export function* doTransaction(action: PayloadAction) { yield* setStep(TransactionStep.Signing) - if (activeWallet.type === WalletType.Ledger) { + if (activeWallet.type === WalletType.UsbLedger || activeWallet.type === WalletType.BleLedger) { yield* call(sign, signer as LedgerSigner, tw) } else { yield* call(OasisTransaction.sign, chainContext, signer as Signer, tw) diff --git a/src/app/state/wallet/saga.ts b/src/app/state/wallet/saga.ts index 0f38435bd2..09c95f1b41 100644 --- a/src/app/state/wallet/saga.ts +++ b/src/app/state/wallet/saga.ts @@ -51,7 +51,7 @@ export function* openWalletsFromLedger({ payload }: PayloadAction, }, + { + path: 'open-wallet/ledger/usb', + element: , + }, + { + path: 'open-wallet/ledger/ble', + element: , + }, { path: 'e2e', element: process.env.REACT_APP_E2E_TEST ? :
      , diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f197b7f8fc..309afcfd73 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -142,6 +142,7 @@ "errors": { "LedgerDerivedDifferentAccount": "This account does not belong to the currently connected Ledger.", "LedgerOasisAppIsNotOpen": "Oasis App on Ledger is closed.", + "bluetoothTransportNotSupported": "Your device does not support Bluetooth.", "cannotSendToSelf": "Cannot send to yourself", "disconnectedError": "Lost connection.", "duplicateTransaction": "Duplicate transaction", @@ -215,7 +216,9 @@ }, "instructionSteps": { "closeLedgerLive": "Close Ledger Live app on the computer", - "connectLedger": "Connect your Ledger device to the computer", + "connectBluetoothLedger": "Connect your Bluetooth Ledger device to the device", + "connectUsbLedger": "Connect your USB Ledger device to the computer", + "deviceIsPaired": "Make sure your Ledger is paired with the device", "header": "Steps:", "openOasisApp": "Open the Oasis App on your Ledger device" }, @@ -242,15 +245,21 @@ }, "header": "How do you want to open your wallet?", "importAccounts": { + "NoBLEDevicesFound": "No Bluetooth devices found", "accountCounter": "{{count}} accounts selected", "accountCounter_one": "One account selected", "accountCounter_plural": "{{count}} accounts selected", "accountCounter_zero": "No account selected", + "bluetoothLedger": "Bluetooth Ledger", + "connectDeviceHeader": "How do you want to connect your Ledger device?", "next": "Next", "openWallets": "Open", "pageNumber": "Page {{pageNum}} of {{totalPages}}", "prev": "Prev", - "selectWallets": "Select accounts to open" + "selectDevice": "Select device", + "selectLedgerDevice": "Select Ledger device", + "selectWallets": "Select accounts to open", + "usbLedger": "USB Ledger" }, "ledger": { "header": "Open from Ledger device" @@ -447,9 +456,10 @@ "wallets": { "select": "Select", "type": { - "ledger": "Ledger", + "bluetoothLedger": "BLE Ledger", "mnemonic": "Mnemonic", - "privateKey": "Private key" + "privateKey": "Private key", + "usbLedger": "USB Ledger" } } }, diff --git a/src/types/errors.ts b/src/types/errors.ts index 7deddb4a58..6c0b366836 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -20,6 +20,7 @@ export enum WalletErrors { NoOpenWallet = 'no_open_wallet', USBTransportError = 'usb_transport_error', USBTransportNotSupported = 'usb_transport_not_supported', + BluetoothTransportNotSupported = 'bluetooth_transport_not_supported', LedgerUnknownError = 'unknown_ledger_error', LedgerCannotOpenOasisApp = 'cannot_open_oasis_app', LedgerOasisAppIsNotOpen = 'oasis_app_is_not_open', diff --git a/yarn.lock b/yarn.lock index d84c13db31..0e4a9b9896 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1800,6 +1800,60 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@capacitor-community/bluetooth-le@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@capacitor-community/bluetooth-le/-/bluetooth-le-3.0.0.tgz#93d184cc28995f2606f7910ea9878c0871f79c85" + integrity sha512-fBy7EXCg9udMf0xW2lJ9uULftJWapSkEFp4WccQZbJjS5vrA146jOGp3dTMnZWPph1wTKAF0dHLCLyBoVDk1vg== + dependencies: + "@types/web-bluetooth" "^0.0.16" + +"@capacitor-community/bluetooth-le@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@capacitor-community/bluetooth-le/-/bluetooth-le-3.0.2.tgz#d32d3013ae0bfaa02e9dce7b419ea17626e46e58" + integrity sha512-3S32RlCnwEGnrWaO6/hVSpirInyoVZhXJEJygfgFvdFQz0b4WnXJwgF/0IZojuomjdfd22cfsWpO4hdGLNc7yg== + dependencies: + "@types/web-bluetooth" "^0.0.16" + +"@capacitor/android@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-5.0.4.tgz#aa6497782bae0851553682126a6051157fe932b5" + integrity sha512-WtXmjRQ0bCtFkwBID/jVEPbqnh0uR9wTSgkW7QNzogDuA0iyJJI2nxDfT9knUmg6mne/CnfTXg093zzdc13C0w== + +"@capacitor/cli@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-5.0.4.tgz#f8d31f32262e457b2a8e31f76295c15b5c6f57b8" + integrity sha512-ALzAZlU1L0lwrxtBcvYeu6tFt5i3cLglQdsbohGLqAhipMWW/QNuafIJshxMMR8lMeBAEVYizdDOxEUFHcnWOQ== + dependencies: + "@ionic/cli-framework-output" "^2.2.5" + "@ionic/utils-fs" "^3.1.6" + "@ionic/utils-subprocess" "^2.1.11" + "@ionic/utils-terminal" "^2.3.3" + commander "^9.3.0" + debug "^4.3.4" + env-paths "^2.2.0" + kleur "^4.1.4" + native-run "^1.7.2" + open "^8.4.0" + plist "^3.0.5" + prompts "^2.4.2" + rimraf "^4.4.1" + semver "^7.3.7" + tar "^6.1.11" + tslib "^2.4.0" + xml2js "^0.5.0" + +"@capacitor/core@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-5.0.4.tgz#70c5551a34657482042c351cbadac0fd9aba53a7" + integrity sha512-BFvziz9jM87pLHW2sXPNIzwrdmI5mAP0tBsBHgXoCO2+wVdpvIMYCpcst5BuTULaMz5JBFZZ6g6nqwgfs+SMCA== + dependencies: + tslib "^2.1.0" + +"@capacitor/ios@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@capacitor/ios/-/ios-5.0.4.tgz#337a5a917b75a5b5d257bb0b08b6f1d7bb396241" + integrity sha512-JVyggBTbHR1OWqxTRysgGKhZHDuM0AQwCIbylvEpza5X0zmaltbQxVC83abcwOKsgNJnrcuv+HUgV+LXg8Dk4Q== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -1998,6 +2052,90 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@ionic/cli-framework-output@^2.2.5": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@ionic/cli-framework-output/-/cli-framework-output-2.2.6.tgz#abbbe4982e82a9a4bfab3d34c372447cd48f25b3" + integrity sha512-YLPRwnk5Lw0XQ9pKWG+p2KoR5HjMBigZ6yv+/XtL3TGOnCS1+oAz56ABbAORCjTWhSJQisr8APNFiELAecY6QA== + dependencies: + "@ionic/utils-terminal" "2.3.4" + debug "^4.0.0" + tslib "^2.0.1" + +"@ionic/utils-array@2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@ionic/utils-array/-/utils-array-2.1.6.tgz#eee863be945ee1a28b9a10ff16fdea776fa18c22" + integrity sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg== + dependencies: + debug "^4.0.0" + tslib "^2.0.1" + +"@ionic/utils-fs@3.1.7", "@ionic/utils-fs@^3.1.6": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@ionic/utils-fs/-/utils-fs-3.1.7.tgz#e0d41225272c346846867e88a0b84b1a4ee9d9c9" + integrity sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA== + dependencies: + "@types/fs-extra" "^8.0.0" + debug "^4.0.0" + fs-extra "^9.0.0" + tslib "^2.0.1" + +"@ionic/utils-object@2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@ionic/utils-object/-/utils-object-2.1.6.tgz#c0259bf925b6c12663d06f6bc1703e5dcb565e6d" + integrity sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww== + dependencies: + debug "^4.0.0" + tslib "^2.0.1" + +"@ionic/utils-process@2.1.11": + version "2.1.11" + resolved "https://registry.yarnpkg.com/@ionic/utils-process/-/utils-process-2.1.11.tgz#ac06dfa2307027095ab0420a234924a9effeb6bd" + integrity sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA== + dependencies: + "@ionic/utils-object" "2.1.6" + "@ionic/utils-terminal" "2.3.4" + debug "^4.0.0" + signal-exit "^3.0.3" + tree-kill "^1.2.2" + tslib "^2.0.1" + +"@ionic/utils-stream@3.1.6": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@ionic/utils-stream/-/utils-stream-3.1.6.tgz#7c2fdcf4d9e621e8b2260e2fee2471825a4e214f" + integrity sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA== + dependencies: + debug "^4.0.0" + tslib "^2.0.1" + +"@ionic/utils-subprocess@^2.1.11": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@ionic/utils-subprocess/-/utils-subprocess-2.1.12.tgz#08ef08a13928aa97b9156b153e39fe5ff9c1e661" + integrity sha512-N05Y+dIXBHofKWJTheCMzVqmgY9wFmZcRv/LdNnfXaaA/mxLTyGxQYeig8fvQXTtDafb/siZXcrTkmQ+y6n3Yg== + dependencies: + "@ionic/utils-array" "2.1.6" + "@ionic/utils-fs" "3.1.7" + "@ionic/utils-process" "2.1.11" + "@ionic/utils-stream" "3.1.6" + "@ionic/utils-terminal" "2.3.4" + cross-spawn "^7.0.3" + debug "^4.0.0" + tslib "^2.0.1" + +"@ionic/utils-terminal@2.3.4", "@ionic/utils-terminal@^2.3.3": + version "2.3.4" + resolved "https://registry.yarnpkg.com/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz#e40c44b676265ed6a07a68407bda6e135870f879" + integrity sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA== + dependencies: + "@types/slice-ansi" "^4.0.0" + debug "^4.0.0" + signal-exit "^3.0.3" + slice-ansi "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + tslib "^2.0.1" + untildify "^4.0.0" + wrap-ansi "^7.0.0" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -2549,6 +2687,14 @@ protobufjs "~7.1.2" tweetnacl "^1.0.3" +"@oasisprotocol/ionic-ledger-hw-transport-ble@1.0.0-beta": + version "1.0.0-beta" + resolved "https://registry.yarnpkg.com/@oasisprotocol/ionic-ledger-hw-transport-ble/-/ionic-ledger-hw-transport-ble-1.0.0-beta.tgz#4f8d00fb23e3172230b481a278a14fc241d7bcd2" + integrity sha512-c/w4pGjv/0/mNQ72RCvudECPdPgSLYhX0bAOiYdLs905NBp25zjz5eREc7IkcmziT5YRP6YgOqs1wFZDE6tkSg== + dependencies: + "@capacitor-community/bluetooth-le" "^3.0.1" + "@ledgerhq/hw-transport" "^6.28.8" + "@oasisprotocol/ledger@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@oasisprotocol/ledger/-/ledger-1.0.0.tgz#65424c44d5c1fe00e698f2c4c36676b3e08670a0" @@ -3578,6 +3724,13 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^8.0.0": + version "8.1.3" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.3.tgz#4807768c0b0a5a5f4746d8fde2f7ab0137076eea" + integrity sha512-7IdV01N0u/CaVO0fuY1YmEg14HQN3+EW8mpNgg6NEfxEl/lzCa5OxlBu3iFsCAdamnYOcTQ7oEi43Xc/67Rgzw== + dependencies: + "@types/node" "*" + "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -3791,6 +3944,11 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== +"@types/slice-ansi@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/slice-ansi/-/slice-ansi-4.0.0.tgz#eb40dfbe3ac5c1de61f6bcb9ed471f54baa989d6" + integrity sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" @@ -3827,6 +3985,11 @@ resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.8.tgz#c593fef468b6e6051209c8aa89d1ead08005e23d" integrity sha512-ouEoUTyB27wFXUUyl0uKIE6VkeCczDtazWTiZGD1M4onceJnp8KnHDf7CzLbpwzek2ZFWXTC5KrNDRc9q/Jf6Q== +"@types/web-bluetooth@^0.0.16": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8" + integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ== + "@types/webextension-polyfill@0.10.4": version "0.10.4" resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.4.tgz#09feeb2d8f04ac0a28818ade8aeeb4ab9fbafebb" @@ -4013,6 +4176,11 @@ "@typescript-eslint/types" "6.8.0" eslint-visitor-keys "^3.4.1" +"@xmldom/xmldom@^0.8.8": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -4580,7 +4748,7 @@ base64-arraybuffer@1.0.2: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== -base64-js@^1.3.1: +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -4597,7 +4765,7 @@ bech32@2.0.0, bech32@^2.0.0: resolved "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz" integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== -big-integer@^1.6.16, big-integer@^1.6.44: +big-integer@1.6.x, big-integer@^1.6.16, big-integer@^1.6.44: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== @@ -4660,6 +4828,13 @@ bplist-parser@^0.2.0: dependencies: big-integer "^1.6.44" +bplist-parser@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.2.tgz#3ac79d67ec52c4c107893e0237eb787cbacbced7" + integrity sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ== + dependencies: + big-integer "1.6.x" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -4941,6 +5116,11 @@ check-more-types@^2.24.0: resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chrome-trace-event@^1.0.2, chrome-trace-event@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -5114,6 +5294,11 @@ commander@^9.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c" integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw== +commander@^9.3.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + commander@~11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" @@ -5429,7 +5614,7 @@ debug@4, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@4.3.4, debug@^4.3.4: +debug@4.3.4, debug@^4.0.0, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -5549,6 +5734,11 @@ default-require-extensions@^3.0.0: dependencies: strip-bom "^4.0.0" +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-lazy-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" @@ -5731,6 +5921,13 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.317.tgz#9a3d38a1a37f26a417d3d95dafe198ff11ed072b" integrity sha512-JhCRm9v30FMNzQSsjl4kXaygU+qHBD0Yh7mKxyjmF0V8VwYVB6qpBRX28GyAucrM9wDCpSUctT6FpMUQxbyKuA== +elementtree@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/elementtree/-/elementtree-0.1.7.tgz#9ac91be6e52fb6e6244c4e54a4ac3ed8ae8e29c0" + integrity sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg== + dependencies: + sax "1.1.4" + elliptic@^6.5.3: version "6.5.4" resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" @@ -5793,6 +5990,11 @@ entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + eol@^0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz" @@ -6694,7 +6896,7 @@ fromentries@^1.2.0: resolved "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== -fs-extra@^9.1.0: +fs-extra@^9.0.0, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -6704,6 +6906,13 @@ fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-mkdirp-stream@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz#1e82575c4023929ad35cf69269f84f1a8c973aa7" @@ -6874,6 +7083,16 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^9.2.0: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== + dependencies: + fs.realpath "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" + glob@~10.3.4: version "10.3.4" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.4.tgz#c85c9c7ab98669102b6defda76d35c5b1ef9766f" @@ -7350,6 +7569,11 @@ ini@^1.3.5: resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ini@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.1.tgz#c76ec81007875bc44d544ff7a11a55d12294102d" + integrity sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ== + ini@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" @@ -7483,7 +7707,7 @@ is-date-object@^1.0.5: dependencies: has-tostringtag "^1.0.0" -is-docker@^2.0.0: +is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== @@ -8521,6 +8745,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +kleur@^4.1.4: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + known-css-properties@^0.29.0: version "0.29.0" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f" @@ -9018,6 +9247,13 @@ minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + dependencies: + brace-expansion "^2.0.1" + minimatch@^9.0.1, minimatch@~9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" @@ -9044,11 +9280,41 @@ minimist@^1.2.7, minimist@^1.2.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": version "7.0.2" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e" integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA== +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -9109,6 +9375,23 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +native-run@^1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/native-run/-/native-run-1.7.3.tgz#892b95ee77a99c679f530719c73f58c1a46b1a8d" + integrity sha512-vEw8X3Yu8TAbP4/uCJV3nCsCrhfHgUecRRDc69ZU9EK0QXHHc7YDzmIeI7SfA08ywzPlC9YcpITcB6bgMbrtwQ== + dependencies: + "@ionic/utils-fs" "^3.1.6" + "@ionic/utils-terminal" "^2.3.3" + bplist-parser "^0.3.2" + debug "^4.3.4" + elementtree "^0.1.7" + ini "^3.0.1" + plist "^3.0.6" + split2 "^4.1.0" + through2 "^4.0.2" + tslib "^2.4.0" + yauzl "^2.10.0" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -9385,6 +9668,15 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +open@^8.4.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + open@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/open/-/open-9.1.0.tgz#684934359c90ad25742f5a26151970ff8c6c80b6" @@ -9599,7 +9891,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1: +path-scurry@^1.10.1, path-scurry@^1.6.1: version "1.10.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== @@ -9659,6 +9951,15 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +plist@^3.0.5, plist@^3.0.6: + version "3.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" + integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + postcss-resolve-nested-selector@^0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz" @@ -9814,7 +10115,7 @@ promise@^8.1.0: dependencies: asap "~2.0.6" -prompts@^2.0.1: +prompts@^2.0.1, prompts@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -10438,6 +10739,13 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" + integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== + dependencies: + glob "^9.2.0" + run-applescript@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" @@ -10515,6 +10823,16 @@ sanitize.css@13.0.0: resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173" integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== +sax@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.4.tgz#74b6d33c9ae1e001510f179a91168588f1aedaa9" + integrity sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg== + +sax@>=0.6.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + saxes@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" @@ -10551,7 +10869,7 @@ semver@^7.3.4: dependencies: lru-cache "^6.0.0" -semver@^7.3.5, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -10740,6 +11058,11 @@ spdx-license-ids@^3.0.0: resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz" integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" @@ -11149,6 +11472,18 @@ table@^6.8.1: string-width "^4.2.3" strip-ansi "^6.0.1" +tar@^6.1.11: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" + integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + teex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/teex/-/teex-1.0.1.tgz#b8fa7245ef8e8effa8078281946c85ab780a0b12" @@ -11188,7 +11523,7 @@ through2@^2.0.1: readable-stream "~2.3.6" xtend "~4.0.1" -through2@^4.0.0: +through2@^4.0.0, through2@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== @@ -11273,6 +11608,11 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + trim-newlines@^4.0.2: version "4.1.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" @@ -11312,6 +11652,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^2.1.0, tslib@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" @@ -11959,6 +12304,24 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml2js@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.5.0.tgz#d9440631fbb2ed800203fad106f2724f62c493b7" + integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"