Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ionic Bluetooth UI #1841

Merged
merged 17 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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">
lubej marked this conversation as resolved.
Show resolved Hide resolved

<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')
lubej marked this conversation as resolved.
Show resolved Hide resolved
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
Loading