You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
10 KiB
339 lines
10 KiB
// @flow
|
|
/* eslint-disable react/jsx-no-literals */ // FIXME
|
|
|
|
import React, { PureComponent, Fragment } from 'react'
|
|
import styled from 'styled-components'
|
|
import { translate } from 'react-i18next'
|
|
import { connect } from 'react-redux'
|
|
import { compose } from 'redux'
|
|
import type { Device, T } from 'types/common'
|
|
import type { LedgerScriptParams } from 'helpers/common'
|
|
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
|
|
import { developerModeSelector } from 'reducers/settings'
|
|
|
|
import listApps from 'commands/listApps'
|
|
import listAppVersions from 'commands/listAppVersions'
|
|
|
|
import installApp from 'commands/installApp'
|
|
import uninstallApp from 'commands/uninstallApp'
|
|
|
|
import Box from 'components/base/Box'
|
|
import Space from 'components/base/Space'
|
|
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
|
|
import Tooltip from 'components/base/Tooltip'
|
|
import Text from 'components/base/Text'
|
|
import Progress from 'components/base/Progress'
|
|
import Spinner from 'components/base/Spinner'
|
|
import Button from 'components/base/Button'
|
|
import TranslatedError from 'components/TranslatedError'
|
|
import TrackPage from 'analytics/TrackPage'
|
|
|
|
import IconInfoCircle from 'icons/InfoCircle'
|
|
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
|
|
import Update from 'icons/Update'
|
|
import Trash from 'icons/Trash'
|
|
import CheckCircle from 'icons/CheckCircle'
|
|
|
|
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
|
|
import AppSearchBar from './AppSearchBar'
|
|
|
|
const mapStateToProps = state => ({
|
|
isDevMode: developerModeSelector(state),
|
|
})
|
|
|
|
const List = styled(Box).attrs({
|
|
horizontal: true,
|
|
m: -3,
|
|
})`
|
|
flex-wrap: wrap;
|
|
`
|
|
|
|
const ICONS_FALLBACK = {
|
|
bitcoin_testnet: 'bitcoin',
|
|
}
|
|
|
|
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
|
|
type Mode = 'home' | 'installing' | 'uninstalling'
|
|
|
|
type Props = {
|
|
device: Device,
|
|
deviceInfo: DeviceInfo,
|
|
t: T,
|
|
isDevMode: boolean,
|
|
}
|
|
|
|
type State = {
|
|
status: Status,
|
|
error: ?Error,
|
|
filteredAppVersionsList: LedgerScriptParams[],
|
|
appsLoaded: boolean,
|
|
app: string,
|
|
mode: Mode,
|
|
}
|
|
|
|
const LoadingApp = () => (
|
|
<FakeManagerAppContainer noShadow align="center" justify="center" style={{ height: 90 }}>
|
|
<Spinner size={16} color="rgba(0, 0, 0, 0.3)" />
|
|
</FakeManagerAppContainer>
|
|
)
|
|
|
|
const loadingApp = <LoadingApp />
|
|
|
|
class AppsList extends PureComponent<Props, State> {
|
|
state = {
|
|
status: 'loading',
|
|
error: null,
|
|
filteredAppVersionsList: [],
|
|
appsLoaded: false,
|
|
app: '',
|
|
mode: 'home',
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.fetchAppList()
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._unmounted = true
|
|
}
|
|
|
|
_unmounted = false
|
|
|
|
filterAppVersions = (applicationsList, compatibleAppVersionsList) => {
|
|
if (!this.props.isDevMode) {
|
|
return compatibleAppVersionsList.filter(
|
|
version => applicationsList.find(e => e.id === version.app).category !== 2,
|
|
)
|
|
}
|
|
return compatibleAppVersionsList
|
|
}
|
|
|
|
async fetchAppList() {
|
|
try {
|
|
const { deviceInfo } = this.props
|
|
const applicationsList = await listApps.send({}).toPromise()
|
|
const compatibleAppVersionsList = await listAppVersions.send(deviceInfo).toPromise()
|
|
const filteredAppVersionsList = this.filterAppVersions(
|
|
applicationsList,
|
|
compatibleAppVersionsList,
|
|
)
|
|
|
|
if (!this._unmounted) {
|
|
this.setState({
|
|
status: 'idle',
|
|
filteredAppVersionsList,
|
|
appsLoaded: true,
|
|
})
|
|
}
|
|
} catch (err) {
|
|
this.setState({ status: 'error', error: err })
|
|
}
|
|
}
|
|
|
|
handleInstallApp = (app: LedgerScriptParams) => async () => {
|
|
this.setState({ status: 'busy', app: app.name, mode: 'installing' })
|
|
try {
|
|
const {
|
|
device: { path: devicePath },
|
|
deviceInfo,
|
|
} = this.props
|
|
const data = { app, devicePath, targetId: deviceInfo.targetId }
|
|
await installApp.send(data).toPromise()
|
|
this.setState({ status: 'success' })
|
|
} catch (err) {
|
|
this.setState({ status: 'error', error: err, mode: 'home' })
|
|
}
|
|
}
|
|
|
|
handleUninstallApp = (app: LedgerScriptParams) => async () => {
|
|
this.setState({ status: 'busy', app: app.name, mode: 'uninstalling' })
|
|
try {
|
|
const {
|
|
device: { path: devicePath },
|
|
deviceInfo,
|
|
} = this.props
|
|
const data = { app, devicePath, targetId: deviceInfo.targetId }
|
|
await uninstallApp.send(data).toPromise()
|
|
this.setState({ status: 'success' })
|
|
} catch (err) {
|
|
this.setState({ status: 'error', error: err, app: '', mode: 'home' })
|
|
}
|
|
}
|
|
|
|
handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' })
|
|
|
|
renderModal = () => {
|
|
const { t } = this.props
|
|
const { app, status, error, mode } = this.state
|
|
return (
|
|
<Modal
|
|
isOpened={status !== 'idle' && status !== 'loading'}
|
|
render={() => (
|
|
<ModalBody align="center" justify="center" style={{ height: 300 }}>
|
|
{status === 'busy' || status === 'idle' ? (
|
|
<Fragment>
|
|
<ModalTitle>
|
|
{mode === 'installing' ? (
|
|
<Box color="grey">
|
|
<Update size={30} />
|
|
</Box>
|
|
) : (
|
|
<Box color="grey">
|
|
<Trash size={30} />
|
|
</Box>
|
|
)}
|
|
</ModalTitle>
|
|
<ModalContent>
|
|
<Text ff="Museo Sans|Regular" fontSize={6} color="dark">
|
|
{t(`app:manager.apps.${mode}`, { app })}
|
|
</Text>
|
|
<Box mt={6}>
|
|
<Progress style={{ width: '100%' }} infinite />
|
|
</Box>
|
|
</ModalContent>
|
|
</Fragment>
|
|
) : status === 'error' ? (
|
|
<Fragment>
|
|
<TrackPage
|
|
category="Manager"
|
|
name="Error Modal"
|
|
error={error && error.name}
|
|
app={app}
|
|
/>
|
|
<ModalContent grow align="center" justify="center" mt={3}>
|
|
<Box color="alertRed">
|
|
<ExclamationCircleThin size={44} />
|
|
</Box>
|
|
<Box
|
|
color="black"
|
|
mt={4}
|
|
fontSize={6}
|
|
ff="Museo Sans|Regular"
|
|
textAlign="center"
|
|
style={{ maxWidth: 350 }}
|
|
>
|
|
<TranslatedError error={error} field="title" />
|
|
</Box>
|
|
<Box
|
|
color="graphite"
|
|
mt={4}
|
|
fontSize={6}
|
|
ff="Open Sans"
|
|
textAlign="center"
|
|
style={{ maxWidth: 350 }}
|
|
>
|
|
<TranslatedError error={error} field="description" />
|
|
</Box>
|
|
</ModalContent>
|
|
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
|
|
<Button primary padded onClick={this.handleCloseModal}>
|
|
{t('app:common.close')}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Fragment>
|
|
) : status === 'success' ? (
|
|
<Fragment>
|
|
<ModalContent grow align="center" justify="center" mt={3}>
|
|
<Box color="positiveGreen">
|
|
<CheckCircle size={44} />
|
|
</Box>
|
|
<Box
|
|
color="black"
|
|
mt={4}
|
|
fontSize={6}
|
|
ff="Museo Sans|Regular"
|
|
textAlign="center"
|
|
style={{ maxWidth: 350 }}
|
|
>
|
|
{t(
|
|
`app:manager.apps.${
|
|
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
|
|
}`,
|
|
{ app },
|
|
)}
|
|
</Box>
|
|
</ModalContent>
|
|
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
|
|
<Button primary padded onClick={this.handleCloseModal}>
|
|
{t('app:common.close')}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Fragment>
|
|
) : null}
|
|
</ModalBody>
|
|
)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
renderList() {
|
|
const { filteredAppVersionsList, appsLoaded } = this.state
|
|
return (
|
|
<Box>
|
|
<AppSearchBar list={filteredAppVersionsList}>
|
|
{items => (
|
|
<List>
|
|
{items.map(c => (
|
|
<ManagerApp
|
|
key={`${c.name}_${c.version}`}
|
|
name={c.name}
|
|
version={`Version ${c.version}`}
|
|
icon={ICONS_FALLBACK[c.icon] || c.icon}
|
|
onInstall={this.handleInstallApp(c)}
|
|
onUninstall={this.handleUninstallApp(c)}
|
|
/>
|
|
))}
|
|
</List>
|
|
)}
|
|
</AppSearchBar>
|
|
{this.renderModal()}
|
|
{!appsLoaded && (
|
|
<Fragment>
|
|
<Space of={30} />
|
|
<List>
|
|
{loadingApp}
|
|
{loadingApp}
|
|
{loadingApp}
|
|
{loadingApp}
|
|
{loadingApp}
|
|
{loadingApp}
|
|
{loadingApp}
|
|
{loadingApp}
|
|
{loadingApp}
|
|
</List>
|
|
</Fragment>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
const { t } = this.props
|
|
return (
|
|
<Box flow={6}>
|
|
<Box>
|
|
<Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center">
|
|
<span style={{ lineHeight: 1 }}>{t('app:manager.apps.all')}</span>
|
|
<Tooltip
|
|
render={() => (
|
|
<Box ff="Open Sans|SemiBold" fontSize={2}>
|
|
{t('app:manager.apps.help')}
|
|
</Box>
|
|
)}
|
|
>
|
|
<Box color="grey">
|
|
<IconInfoCircle size={12} />
|
|
</Box>
|
|
</Tooltip>
|
|
</Box>
|
|
{this.renderList()}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default compose(
|
|
translate(),
|
|
connect(mapStateToProps),
|
|
)(AppsList)
|
|
|