committed by
GitHub
148 changed files with 2355 additions and 1002 deletions
@ -0,0 +1,23 @@ |
|||
--- |
|||
name: 🐛 Bug Report |
|||
about: Report a bug in Ledger Live Desktop or a regression. |
|||
--- |
|||
|
|||
#### Ledger Live Version and Operating System |
|||
|
|||
<!-- Precise the app version (Settings > About or bottom-left corner on a crash screen) --> |
|||
|
|||
- Ledger Live **version_here** |
|||
- Platform: **windows OR mac OR linux** |
|||
|
|||
#### Expected behavior |
|||
|
|||
<!-- what is the feature and what should normally happen --> |
|||
|
|||
#### Actual behavior |
|||
|
|||
<!-- what actually happened that you consider a bug --> |
|||
|
|||
#### Steps to reproduce the behavior |
|||
|
|||
<!-- explain steps in detail so we can easily reproduce on our side --> |
@ -0,0 +1,4 @@ |
|||
--- |
|||
name: 🗣 Start a Discussion |
|||
about: Discuss to propose changes or suggest feature requests. |
|||
--- |
@ -0,0 +1,7 @@ |
|||
--- |
|||
name: 💬 Question |
|||
about: If you need help using the app, please contact the support at https://support.ledgerwallet.com/ |
|||
|
|||
--- |
|||
|
|||
<!-- Please prefer using the support for app usage questions, Github issues are only for technical / developer usage --> |
@ -0,0 +1,14 @@ |
|||
// @flow
|
|||
|
|||
import { createCommand, Command } from 'helpers/ipc' |
|||
import { fromPromise } from 'rxjs/observable/fromPromise' |
|||
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' |
|||
import shouldFlashMcu from 'helpers/devices/shouldFlashMcu' |
|||
|
|||
type Result = boolean |
|||
|
|||
const cmd: Command<DeviceInfo, Result> = createCommand('shouldFlashMcu', data => |
|||
fromPromise(shouldFlashMcu(data)), |
|||
) |
|||
|
|||
export default cmd |
@ -0,0 +1,47 @@ |
|||
// @flow
|
|||
// Toggle something after a sequence of keyboard
|
|||
|
|||
import { Component } from 'react' |
|||
|
|||
class KeyboardContent extends Component< |
|||
{ |
|||
sequence: string, |
|||
children?: *, |
|||
}, |
|||
{ enabled: boolean }, |
|||
> { |
|||
state = { |
|||
enabled: false, |
|||
} |
|||
|
|||
componentDidMount() { |
|||
window.addEventListener('keypress', this.onKeyPress) |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
window.removeEventListener('keypress', this.onKeyPress) |
|||
} |
|||
|
|||
seqIndex = -1 |
|||
onKeyPress = (e: *) => { |
|||
const { sequence } = this.props |
|||
const next = sequence[this.seqIndex + 1] |
|||
if (next && next === e.key) { |
|||
this.seqIndex++ |
|||
} else { |
|||
this.seqIndex = -1 |
|||
} |
|||
if (this.seqIndex === sequence.length - 1) { |
|||
this.seqIndex = -1 |
|||
this.setState(({ enabled }) => ({ enabled: !enabled })) |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { children } = this.props |
|||
const { enabled } = this.state |
|||
return enabled ? children : null |
|||
} |
|||
} |
|||
|
|||
export default KeyboardContent |
@ -0,0 +1,69 @@ |
|||
// @flow
|
|||
|
|||
import { PureComponent } from 'react' |
|||
import { createStructuredSelector } from 'reselect' |
|||
import { connect } from 'react-redux' |
|||
import { getCurrentDevice } from 'reducers/devices' |
|||
import type { Device } from 'types/common' |
|||
|
|||
const hookDeviceChangeInstances = [] |
|||
let frozen = 0 |
|||
export class FreezeDeviceChangeEvents extends PureComponent<{}> { |
|||
componentDidMount() { |
|||
frozen++ |
|||
} |
|||
componentWillUnmount() { |
|||
frozen-- |
|||
if (!frozen) { |
|||
hookDeviceChangeInstances.forEach(i => i.onUnfreeze()) |
|||
} |
|||
} |
|||
render() { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
/* eslint-disable react/no-multi-comp */ |
|||
class HookDeviceChange extends PureComponent<{ |
|||
device: ?Device, |
|||
onDeviceDisconnected: () => void, |
|||
onDeviceChanges: Device => void, |
|||
}> { |
|||
componentDidMount() { |
|||
const { device, onDeviceDisconnected } = this.props |
|||
if (!device && !frozen) { |
|||
onDeviceDisconnected() |
|||
} |
|||
hookDeviceChangeInstances.push(this) |
|||
} |
|||
componentDidUpdate(prevProps) { |
|||
const { device, onDeviceDisconnected, onDeviceChanges } = this.props |
|||
if (!device) { |
|||
if (frozen) { |
|||
this.onUnfreeze = onDeviceDisconnected |
|||
} else { |
|||
onDeviceDisconnected() |
|||
} |
|||
} else if (device !== prevProps.device) { |
|||
if (frozen) { |
|||
this.onUnfreeze = () => onDeviceChanges(device) |
|||
} else { |
|||
onDeviceChanges(device) |
|||
} |
|||
} |
|||
} |
|||
componentWillUnmount() { |
|||
const i = hookDeviceChangeInstances.indexOf(this) |
|||
if (i !== -1) hookDeviceChangeInstances.splice(i, 1) |
|||
} |
|||
onUnfreeze = () => {} |
|||
render() { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
export default connect( |
|||
createStructuredSelector({ |
|||
device: getCurrentDevice, |
|||
}), |
|||
)(HookDeviceChange) |
@ -0,0 +1,57 @@ |
|||
// @flow
|
|||
import React, { PureComponent } from 'react' |
|||
import styled from 'styled-components' |
|||
import ping from 'commands/ping' |
|||
|
|||
const Indicator = styled.div` |
|||
opacity: 0.8; |
|||
border-radius: 3px; |
|||
background-color: white; |
|||
position: fixed; |
|||
font-size: 10px; |
|||
padding: 3px 6px; |
|||
bottom: 0; |
|||
left: 0; |
|||
z-index: 999; |
|||
pointer-events: none; |
|||
` |
|||
|
|||
class PerfIndicator extends PureComponent<{}, { opsPerSecond: number }> { |
|||
state = { |
|||
opsPerSecond: 0, |
|||
} |
|||
componentDidMount() { |
|||
let count = 0 |
|||
const loop = () => { |
|||
++count |
|||
if (this.finished) return |
|||
this.sub = ping.send().subscribe({ |
|||
complete: loop, |
|||
}) |
|||
} |
|||
loop() |
|||
setInterval(() => { |
|||
this.setState({ opsPerSecond: count }) |
|||
count = 0 |
|||
}, 1000) |
|||
} |
|||
componentWillUnmount() { |
|||
if (this.sub) { |
|||
this.sub.unsubscribe() |
|||
this.finished = true |
|||
} |
|||
} |
|||
sub: * |
|||
interval: * |
|||
finished = false |
|||
render() { |
|||
return ( |
|||
<Indicator> |
|||
{this.state.opsPerSecond} |
|||
{' ops/s'} |
|||
</Indicator> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default PerfIndicator |
@ -0,0 +1,72 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import { translate } from 'react-i18next' |
|||
import type { T } from 'types/common' |
|||
import TrackPage from 'analytics/TrackPage' |
|||
import IconHelp from 'icons/Help' |
|||
import resolveLogsDirectory from 'helpers/resolveLogsDirectory' |
|||
import { urls } from 'config/support' |
|||
|
|||
import ExportLogsBtn from 'components/ExportLogsBtn' |
|||
import CleanButton from '../CleanButton' |
|||
import ResetButton from '../ResetButton' |
|||
import AboutRowItem from '../AboutRowItem' |
|||
|
|||
import { |
|||
SettingsSection as Section, |
|||
SettingsSectionHeader as Header, |
|||
SettingsSectionBody as Body, |
|||
SettingsSectionRow as Row, |
|||
} from '../SettingsSection' |
|||
|
|||
type Props = { |
|||
t: T, |
|||
} |
|||
|
|||
class SectionHelp extends PureComponent<Props> { |
|||
render() { |
|||
const { t } = this.props |
|||
|
|||
return ( |
|||
<Section> |
|||
<TrackPage category="Settings" name="Help" /> |
|||
|
|||
<Header |
|||
icon={<IconHelp size={16} />} |
|||
title={t('app:settings.tabs.help')} |
|||
desc={t('app:settings.help.desc')} |
|||
/> |
|||
|
|||
<Body> |
|||
<Row |
|||
title={t('app:settings.profile.softResetTitle')} |
|||
desc={t('app:settings.profile.softResetDesc')} |
|||
> |
|||
<CleanButton /> |
|||
</Row> |
|||
<Row |
|||
title={t('app:settings.profile.hardResetTitle')} |
|||
desc={t('app:settings.profile.hardResetDesc')} |
|||
> |
|||
<ResetButton /> |
|||
</Row> |
|||
<Row |
|||
title={t('app:settings.exportLogs.title')} |
|||
desc={t('app:settings.exportLogs.desc', { logsDirectory: resolveLogsDirectory() })} |
|||
> |
|||
<ExportLogsBtn /> |
|||
</Row> |
|||
|
|||
<AboutRowItem |
|||
title={t('app:settings.help.faq')} |
|||
desc={t('app:settings.help.faqDesc')} |
|||
url={urls.faq} |
|||
/> |
|||
</Body> |
|||
</Section> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default translate()(SectionHelp) |
@ -0,0 +1,29 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import Label from 'components/base/Label' |
|||
import Box from 'components/base/Box' |
|||
import IconExternalLink from 'icons/ExternalLink' |
|||
|
|||
// can add more dynamic options if needed
|
|||
export function LabelWithExternalIcon({ onClick, label }: { onClick: ?() => void, label: string }) { |
|||
return ( |
|||
<LabelWrapper onClick={onClick}> |
|||
<span>{label}</span> |
|||
<Box ml={1}> |
|||
<IconExternalLink size={12} /> |
|||
</Box> |
|||
</LabelWrapper> |
|||
) |
|||
} |
|||
|
|||
export default LabelWithExternalIcon |
|||
|
|||
const LabelWrapper = styled(Label).attrs({})` |
|||
&:hover { |
|||
color: ${p => p.theme.colors.wallet}; |
|||
cursor: pointer; |
|||
} |
|||
` |
@ -0,0 +1,156 @@ |
|||
// @flow
|
|||
import React, { PureComponent } from 'react' |
|||
import styled from 'styled-components' |
|||
import ReactMarkdown from 'react-markdown' |
|||
import { shell } from 'electron' |
|||
|
|||
import Box from 'components/base/Box' |
|||
|
|||
export const Notes = styled(Box).attrs({ |
|||
ff: 'Open Sans', |
|||
fontSize: 4, |
|||
color: 'smoke', |
|||
flow: 4, |
|||
})` |
|||
ul, |
|||
ol { |
|||
padding-left: 20px; |
|||
} |
|||
|
|||
p { |
|||
margin: 1em 0; |
|||
} |
|||
|
|||
code, |
|||
pre { |
|||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; |
|||
} |
|||
|
|||
code { |
|||
padding: 0.2em 0.4em; |
|||
font-size: 0.9em; |
|||
background-color: ${p => p.theme.colors.lightGrey}; |
|||
border-radius: 3px; |
|||
} |
|||
|
|||
pre { |
|||
word-wrap: normal; |
|||
|
|||
code { |
|||
word-break: normal; |
|||
white-space: pre; |
|||
background: transparent; |
|||
} |
|||
} |
|||
|
|||
h1, |
|||
h2, |
|||
h3, |
|||
h4, |
|||
h5, |
|||
h6 { |
|||
color: ${p => p.theme.colors.dark}; |
|||
font-weight: bold; |
|||
margin-top: 24px; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
h1 { |
|||
padding-bottom: 0.3em; |
|||
font-size: 1.33em; |
|||
} |
|||
|
|||
h2 { |
|||
padding-bottom: 0.3em; |
|||
font-size: 1.25em; |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 1em; |
|||
} |
|||
|
|||
h4 { |
|||
font-size: 0.875em; |
|||
} |
|||
|
|||
h5, |
|||
h6 { |
|||
font-size: 0.85em; |
|||
color: #6a737d; |
|||
} |
|||
|
|||
img { |
|||
max-width: 100%; |
|||
} |
|||
|
|||
hr { |
|||
height: 1px; |
|||
border: none; |
|||
background-color: ${p => p.theme.colors.fog}; |
|||
} |
|||
|
|||
blockquote { |
|||
padding: 0 1em; |
|||
border-left: 0.25em solid #dfe2e5; |
|||
} |
|||
|
|||
table { |
|||
width: 100%; |
|||
overflow: auto; |
|||
border-collapse: collapse; |
|||
|
|||
th { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
th, |
|||
td { |
|||
padding: 6px 13px; |
|||
border: 1px solid #dfe2e5; |
|||
} |
|||
|
|||
tr { |
|||
background-color: #fff; |
|||
border-top: 1px solid #c6cbd1; |
|||
} |
|||
|
|||
tr:nth-child(2n) { |
|||
background-color: #f6f8fa; |
|||
} |
|||
} |
|||
|
|||
input[type='Switch'] { |
|||
margin-right: 0.5em; |
|||
} |
|||
` |
|||
|
|||
type Props = { |
|||
children: React$Node, |
|||
} |
|||
|
|||
export default class Markdown extends PureComponent<Props> { |
|||
componentDidMount() { |
|||
if (this.parent) { |
|||
const links: NodeList<HTMLElement> = this.parent.querySelectorAll('a') |
|||
links.forEach(link => { |
|||
link.addEventListener('click', (e: MouseEvent) => { |
|||
e.preventDefault() |
|||
// $FlowFixMe
|
|||
const href = e.target && e.target.href |
|||
shell.openExternal(href) |
|||
}) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
parent: ?HTMLDivElement |
|||
|
|||
render() { |
|||
const { children } = this.props |
|||
return ( |
|||
<div ref={c => (this.parent = c)}> |
|||
<ReactMarkdown>{children}</ReactMarkdown> |
|||
</div> |
|||
) |
|||
} |
|||
} |
@ -1,47 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import { remote } from 'electron' |
|||
import qs from 'qs' |
|||
|
|||
import CurrentAddress from 'components/CurrentAddress' |
|||
|
|||
class Print extends PureComponent<any> { |
|||
componentDidMount() { |
|||
window.requestAnimationFrame(() => |
|||
setTimeout(() => { |
|||
if (!this._node) { |
|||
return |
|||
} |
|||
|
|||
const { height, width } = this._node.getBoundingClientRect() |
|||
const currentWindow = remote.getCurrentWindow() |
|||
|
|||
currentWindow.setContentSize(width, height) |
|||
currentWindow.emit('print-ready') |
|||
}, 300), |
|||
) |
|||
} |
|||
|
|||
_node = null |
|||
|
|||
render() { |
|||
const data = qs.parse(this.props.location.search, { ignoreQueryPrefix: true }) |
|||
|
|||
if (!data) { |
|||
return null |
|||
} |
|||
const { address, amount, accountName } = data |
|||
return ( |
|||
<CurrentAddress |
|||
accountName={accountName} |
|||
address={address} |
|||
amount={amount} |
|||
innerRef={n => (this._node = n)} |
|||
withQRCode |
|||
/> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default Print |
@ -0,0 +1,99 @@ |
|||
// @flow
|
|||
import React, { PureComponent } from 'react' |
|||
import { translate } from 'react-i18next' |
|||
import styled from 'styled-components' |
|||
|
|||
import { MODAL_SHARE_ANALYTICS } from 'config/constants' |
|||
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' |
|||
|
|||
import Button from 'components/base/Button' |
|||
import Box from 'components/base/Box' |
|||
|
|||
import type { T } from 'types/common' |
|||
|
|||
type Props = { |
|||
t: T, |
|||
} |
|||
class ShareAnalytics extends PureComponent<Props, *> { |
|||
render() { |
|||
const { t } = this.props |
|||
const items = [ |
|||
{ |
|||
key: 'item1', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item1'), |
|||
}, |
|||
{ |
|||
key: 'item2', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item2'), |
|||
}, |
|||
{ |
|||
key: 'item3', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item3'), |
|||
}, |
|||
{ |
|||
key: 'item4', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item4'), |
|||
}, |
|||
{ |
|||
key: 'item5', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item5'), |
|||
}, |
|||
{ |
|||
key: 'item6', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item6'), |
|||
}, |
|||
{ |
|||
key: 'item7', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item7'), |
|||
}, |
|||
{ |
|||
key: 'item8', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item8'), |
|||
}, |
|||
{ |
|||
key: 'item9', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item9'), |
|||
}, |
|||
{ |
|||
key: 'item10', |
|||
desc: t('onboarding:analytics.shareAnalytics.mandatoryContextual.item10'), |
|||
}, |
|||
] |
|||
return ( |
|||
<Modal |
|||
name={MODAL_SHARE_ANALYTICS} |
|||
render={({ onClose }) => ( |
|||
<ModalBody onClose={onClose}> |
|||
<ModalTitle>{t('onboarding:analytics.shareAnalytics.title')}</ModalTitle> |
|||
<InlineDesc>{t('onboarding:analytics.shareAnalytics.desc')}</InlineDesc> |
|||
<ModalContent mx={5}> |
|||
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> |
|||
</ModalContent> |
|||
<ModalFooter horizontal justifyContent="flex-end"> |
|||
<Button onClick={onClose} primary> |
|||
{t('app:common.close')} |
|||
</Button> |
|||
</ModalFooter> |
|||
</ModalBody> |
|||
)} |
|||
/> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default translate()(ShareAnalytics) |
|||
|
|||
export const Ul = styled.ul.attrs({ |
|||
ff: 'Open Sans|Regular', |
|||
})` |
|||
margin-top: 15px; |
|||
font-size: 13px; |
|||
color: ${p => p.theme.colors.graphite}; |
|||
line-height: 1.69; |
|||
` |
|||
export const InlineDesc = styled(Box).attrs({ |
|||
ff: 'Open Sans|SemiBold', |
|||
fontSize: 4, |
|||
color: 'dark', |
|||
mx: '45px', |
|||
})`` |
@ -0,0 +1,67 @@ |
|||
// @flow
|
|||
import React, { PureComponent } from 'react' |
|||
import { translate } from 'react-i18next' |
|||
|
|||
import { MODAL_TECHNICAL_DATA } from 'config/constants' |
|||
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' |
|||
import Button from 'components/base/Button' |
|||
|
|||
import type { T } from 'types/common' |
|||
import { Ul, InlineDesc } from './ShareAnalytics' |
|||
|
|||
type Props = { |
|||
t: T, |
|||
} |
|||
|
|||
class TechnicalData extends PureComponent<Props, *> { |
|||
render() { |
|||
const { t } = this.props |
|||
|
|||
const items = [ |
|||
{ |
|||
key: 'item1', |
|||
desc: t('onboarding:analytics.technicalData.mandatoryContextual.item1'), |
|||
}, |
|||
{ |
|||
key: 'item2', |
|||
desc: t('onboarding:analytics.technicalData.mandatoryContextual.item2'), |
|||
}, |
|||
{ |
|||
key: 'item3', |
|||
desc: t('onboarding:analytics.technicalData.mandatoryContextual.item3'), |
|||
}, |
|||
{ |
|||
key: 'item4', |
|||
desc: t('onboarding:analytics.technicalData.mandatoryContextual.item4'), |
|||
}, |
|||
{ |
|||
key: 'item5', |
|||
desc: t('onboarding:analytics.technicalData.mandatoryContextual.item5'), |
|||
}, |
|||
] |
|||
|
|||
return ( |
|||
<Modal |
|||
name={MODAL_TECHNICAL_DATA} |
|||
render={({ onClose }) => ( |
|||
<ModalBody onClose={onClose}> |
|||
<ModalTitle> |
|||
{t('onboarding:analytics.technicalData.mandatoryContextual.title')} |
|||
</ModalTitle> |
|||
<InlineDesc>{t('onboarding:analytics.technicalData.desc')}</InlineDesc> |
|||
<ModalContent mx={5}> |
|||
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> |
|||
</ModalContent> |
|||
<ModalFooter horizontal justifyContent="flex-end"> |
|||
<Button onClick={onClose} primary> |
|||
{t('app:common.close')} |
|||
</Button> |
|||
</ModalFooter> |
|||
</ModalBody> |
|||
)} |
|||
/> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default translate()(TechnicalData) |
@ -0,0 +1,30 @@ |
|||
// @flow
|
|||
import React, { Fragment } from 'react' |
|||
import { translate } from 'react-i18next' |
|||
|
|||
import Box from 'components/base/Box' |
|||
import Text from 'components/base/Text' |
|||
import Spinner from 'components/base/Spinner' |
|||
|
|||
import type { T } from 'types/common' |
|||
|
|||
type Props = { |
|||
t: T, |
|||
} |
|||
|
|||
function Installing({ t }: Props) { |
|||
return ( |
|||
<Fragment> |
|||
<Box mx={7} align="center"> |
|||
<Spinner color="fog" size={44} /> |
|||
</Box> |
|||
<Box mx={7} mt={4} mb={7}> |
|||
<Text ff="Museo Sans|Regular" align="center" color="dark" fontSize={6}> |
|||
{t('app:manager.modal.installing')} |
|||
</Text> |
|||
</Box> |
|||
</Fragment> |
|||
) |
|||
} |
|||
|
|||
export default translate()(Installing) |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue