Skip to content

Commit

Permalink
Merge pull request #1841 from lubej/ionic-bluetooth-ui
Browse files Browse the repository at this point in the history
Ionic Bluetooth UI
  • Loading branch information
lubej committed May 8, 2024
2 parents 016dfa1 + 93426e7 commit 1c3546c
Show file tree
Hide file tree
Showing 25 changed files with 518 additions and 254 deletions.
1 change: 1 addition & 0 deletions .changelog/1841.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ionic Bluetooth UI
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
Expand Down
9 changes: 7 additions & 2 deletions extension/src/popup/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ 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 } from 'app/pages/OpenWalletPage/webextension'
import { commonRoutes } from '../../../src/commonRoutes'
import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage'

export const routes: RouteObject[] = [
{
Expand All @@ -13,7 +14,11 @@ export const routes: RouteObject[] = [
...commonRoutes,
{
path: 'open-wallet',
element: <OpenWalletPageWebExtension />,
element: <SelectOpenMethod />,
},
{
path: 'open-wallet/ledger',
element: <FromLedgerWebExtension />,
},
],
},
Expand Down
1 change: 1 addition & 0 deletions playwright/tests/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ test.describe('The extension popup should load', () => {
test('ask for USB permissions in ledger popup', async ({ page, context, extensionPopupURL }) => {
await page.goto(`${extensionPopupURL}/open-wallet`)
const popupPromise = context.waitForEvent('page')
await page.getByRole('button', { name: /Ledger/i }).click()
await page.getByRole('button', { name: /Grant access to your Ledger/i }).click()
const popup = await popupPromise
await popup.waitForLoadState()
Expand Down
2 changes: 1 addition & 1 deletion playwright/tests/ledger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/ErrorFormatter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function ErrorFormatter(props: Props) {
[WalletErrors.NoOpenWallet]: t('errors.noOpenWallet', 'No wallet opened'),
[WalletErrors.USBTransportNotSupported]: t(
'errors.usbTransportNotSupported',
'Your browser does not support WebUSB (e.g. Firefox). Try using Chrome.',
'Current platform does not support WebUSB capability. Try on different platform or browser(preferably Chrome).',
),
[WalletErrors.USBTransportError]: t('errors.usbTransportError', 'USB Transport error: {{message}}.', {
message,
Expand Down Expand Up @@ -100,7 +100,7 @@ export function ErrorFormatter(props: Props) {
),
[WalletErrors.BluetoothTransportNotSupported]: t(
'errors.bluetoothTransportNotSupported',
'Your device does not support Bluetooth.',
'Bluetooth may be turned off or your current platform does not support Bluetooth capability.',
),
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/components/ImportAccountsStepFormatter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const ImportAccountsStepFormatter = memo((props: Props) => {

const stepMap: { [code in Step]: string } = {
[Step.Idle]: t('ledger.steps.idle', 'Idle'),
[Step.AccessingLedger]: t('ledger.steps.openingUsb', 'Opening Ledger through USB'),
[Step.AccessingLedger]: t('ledger.steps.accessingLedger', 'Connecting with Ledger device'),
[Step.LoadingAccounts]: t('ledger.steps.loadingAccounts', 'Loading account details'),
[Step.LoadingBalances]: t('ledger.steps.loadingBalances', 'Loading balance details'),
[Step.LoadingBleDevices]: t('ledger.steps.loadingBluetoothDevices', 'Loading bluetooth devices'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ exports[`<ConnectDevicePage /> should render component 1`] = `
>
<ol>
<li>
ledger.instructionSteps.connectLedger
ledger.instructionSteps.connectUsbLedger
</li>
<li>
ledger.instructionSteps.closeLedgerLive
Expand Down
5 changes: 4 additions & 1 deletion src/app/pages/ConnectDevicePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ export function ConnectDevicePage() {
<Box gap="medium">
<ol>
<li>
{t('ledger.instructionSteps.connectLedger', 'Connect your Ledger device to the computer')}
{t(
'ledger.instructionSteps.connectUsbLedger',
'Connect your USB Ledger device to the computer',
)}
</li>
<li>{t('ledger.instructionSteps.closeLedgerLive', 'Close Ledger Live app on the computer')}</li>
<li>{t('ledger.instructionSteps.openOasisApp', 'Open the Oasis App on your Ledger device')}</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react'
import { FromBleLedger } from '..'

jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn(),
}))

describe('<FromBleLedger />', () => {
it('should render component', () => {
render(<FromBleLedger />)

expect(screen.queryByText('ledger.instructionSteps.connectBluetoothLedger')).toBeInTheDocument()
expect(screen.queryByText('ledger.instructionSteps.deviceIsPaired')).toBeInTheDocument()
expect(screen.queryByText('ledger.instructionSteps.closeLedgerLive')).toBeInTheDocument()
expect(screen.queryByText('ledger.instructionSteps.openOasisApp')).toBeInTheDocument()

expect(screen.getByRole('button', { name: 'openWallet.importAccounts.selectDevice' })).toBeInTheDocument()
})
})
78 changes: 78 additions & 0 deletions src/app/pages/OpenWalletPage/Features/FromBleLedger/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 { 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 (
<Box
background="background-front"
margin="small"
pad="medium"
round="5px"
border={{ color: 'background-front-border', size: '1px' }}
>
<Header>{t('openWallet.ledger.header', 'Open from Ledger device')}</Header>

<Heading level="3" margin="0">
{t('ledger.instructionSteps.header', 'Steps:')}
</Heading>
<ol>
<li>
{t(
'ledger.instructionSteps.connectBluetoothLedger',
'Connect your Ledger to this device via Bluetooth',
)}
</li>
<li>
{t('ledger.instructionSteps.deviceIsPaired', 'Make sure your Ledger is paired with this device')}
</li>
<li>{t('ledger.instructionSteps.closeLedgerLive', 'Close Ledger Live app on the device')}</li>
<li>{t('ledger.instructionSteps.openOasisApp', 'Open the Oasis app on your Ledger')}</li>
</ol>
<Box direction="row" margin={{ top: 'medium' }}>
<Button
type="submit"
label={t('openWallet.importAccounts.selectDevice', 'Select device')}
onClick={() => {
dispatch(importAccountsActions.enumerateDevicesFromBleLedger())
}}
primary
/>
</Box>
{showBleLedgerDevicesModal && (
<ListBleLedgerDevicesModal
abort={() => {
dispatch(importAccountsActions.clear())
}}
next={() => {
dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.BleLedger))
}}
/>
)}
{showAccountsSelectionModal && (
<ImportAccountsSelectionModal
abort={() => {
dispatch(importAccountsActions.clear())
}}
type={WalletType.BleLedger}
/>
)}
</Box>
)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import * as React from 'react'
import { render } from '@testing-library/react'
import { render, screen, waitFor } 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('../../../../../lib/ledger', () => ({
...jest.requireActual('../../../../../lib/ledger'),
// Throws BLE not supported
canAccessBle: jest.fn().mockResolvedValue(false),
}))

const renderComponent = () =>
render(
<MemoryRouter>
<FromLedger />
</MemoryRouter>,
)

describe('<FromLedger />', () => {
it('should render component', () => {
const { container } = render(<FromLedger />)
it('should render component in disabled state', async () => {
renderComponent()

await waitFor(() => {
expect(screen.queryByText('openWallet.importAccounts.usbLedger')).toBeInTheDocument()
expect(screen.queryByText('openWallet.importAccounts.bluetoothLedger')).toBeInTheDocument()

expect(screen.getByText('errors.usbTransportNotSupported')).toBeInTheDocument()
expect(screen.getByText('errors.bluetoothTransportNotSupported')).toBeInTheDocument()

const usbLedgerBtn = screen.getByRole('button', { name: 'openWallet.importAccounts.usbLedger' })
const bluetoothLedgerBtn = screen.getByRole('button', {
name: 'openWallet.importAccounts.bluetoothLedger',
})

expect(container).toMatchSnapshot()
expect(usbLedgerBtn).toHaveProperty('disabled', true)
expect(bluetoothLedgerBtn).toHaveProperty('disabled', true)
})
})
})
122 changes: 82 additions & 40 deletions src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,98 @@
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'
import { Capacitor } from '@capacitor/core'

type SelectOpenMethodProps = {
webExtensionUSBLedgerAccess?: () => void
}

export function FromLedger() {
export function FromLedger({ webExtensionUSBLedgerAccess }: SelectOpenMethodProps) {
const { t } = useTranslation()
const dispatch = useDispatch()
const showAccountsSelectionModal = useSelector(selectShowAccountsSelectionModal)
const [supportsUsbLedger, setSupportsUsbLedger] = React.useState<boolean | undefined>(true)
const [supportsBleLedger, setSupportsBleLedger] = React.useState<boolean | undefined>(true)

useEffect(() => {
async function getLedgerSupport() {
const usbLedgerSupported = await canAccessNavigatorUsb()

const isNativePlatform = Capacitor.isNativePlatform()
const bleLedgerSupported = isNativePlatform && (await canAccessBle())

setSupportsUsbLedger(usbLedgerSupported)
setSupportsBleLedger(bleLedgerSupported)
}

getLedgerSupport()
}, [])

return (
<Box
round="5px"
border={{ color: 'background-front-border', size: '1px' }}
background="background-front"
margin="small"
pad="medium"
round="5px"
border={{ color: 'background-front-border', size: '1px' }}
>
<Header>{t('openWallet.ledger.header', 'Open from Ledger device')}</Header>

<Heading level="3" margin="0">
{t('ledger.instructionSteps.header', 'Steps:')}
</Heading>
<ol>
<li>{t('ledger.instructionSteps.connectLedger', 'Connect your Ledger device to the computer')}</li>
<li>{t('ledger.instructionSteps.closeLedgerLive', 'Close Ledger Live app on the computer')}</li>
<li>{t('ledger.instructionSteps.openOasisApp', 'Open the Oasis App on your Ledger device')}</li>
</ol>
<Box direction="row" margin={{ top: 'medium' }}>
<Button
type="submit"
label={t('openWallet.importAccounts.selectWallets', 'Select accounts to open')}
onClick={() => {
dispatch(importAccountsActions.enumerateAccountsFromLedger(WalletType.UsbLedger))
}}
primary
/>
<Header>
{t('openWallet.importAccounts.connectDeviceHeader', 'How do you want to connect your Ledger device?')}
</Header>

<Box direction="row-responsive" justify="start" margin={{ top: 'medium' }} gap="medium">
<div>
<div>
{webExtensionUSBLedgerAccess ? (
<Button
disabled={!supportsUsbLedger}
style={{ width: 'fit-content' }}
onClick={webExtensionUSBLedgerAccess}
label={t('ledger.extension.grantAccess', 'Grant access to your USB Ledger')}
primary
/>
) : (
<span>
<ButtonLink
disabled={!supportsUsbLedger}
to="usb"
label={t('openWallet.importAccounts.usbLedger', 'USB Ledger')}
primary
/>
</span>
)}
</div>
{!supportsUsbLedger && (
<Text size="small" textAlign="center">
{t(
'errors.usbTransportNotSupported',
'Current platform does not support WebUSB capability. Try on different platform or browser(preferably Chrome).',
)}
</Text>
)}
</div>
<div>
<div>
<ButtonLink
disabled={!supportsBleLedger}
to="ble"
label={t('openWallet.importAccounts.bluetoothLedger', 'Bluetooth Ledger')}
primary
/>
</div>
{!supportsBleLedger && (
<Text size="small" textAlign="center">
{t(
'errors.bluetoothTransportNotSupported',
'Bluetooth may be turned off or your current platform does not support Bluetooth capability.',
)}
</Text>
)}
</div>
</Box>
{showAccountsSelectionModal && (
<ImportAccountsSelectionModal
abort={() => {
dispatch(importAccountsActions.clear())
}}
type={WalletType.UsbLedger}
/>
)}
</Box>
)
}
Loading

0 comments on commit 1c3546c

Please sign in to comment.