Browse Source

Merge pull request #871 from LedgerHQ/develop

Prepare for beta.8
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
23b92a53f1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 4
      .github/ISSUE_TEMPLATE/discussion.md
  3. 7
      .github/ISSUE_TEMPLATE/question.md
  4. 0
      .github/PULL_REQUEST_TEMPLATE.md
  5. 60
      src/analytics/segment.js
  6. 4
      src/bridge/LibcoreBridge.js
  7. 2
      src/commands/index.js
  8. 2
      src/commands/libcoreSignAndBroadcast.js
  9. 14
      src/commands/shouldFlashMcu.js
  10. 11
      src/commands/testCrash.js
  11. 15
      src/components/App.js
  12. 2
      src/components/CalculateBalance.js
  13. 140
      src/components/CurrentAddress/index.js
  14. 2
      src/components/DashboardPage/AccountsOrder.js
  15. 3
      src/components/DeviceConfirm/index.js
  16. 2
      src/components/DeviceInteraction/DeviceInteractionStep.js
  17. 30
      src/components/DeviceInteraction/components.js
  18. 0
      src/components/EnsureDevice.js
  19. 8
      src/components/EnsureDeviceApp.js
  20. 6
      src/components/ExchangePage/ExchangeCard.js
  21. 6
      src/components/ExchangePage/index.js
  22. 2
      src/components/FeesField/BitcoinKind.js
  23. 2
      src/components/FeesField/EthereumKind.js
  24. 31
      src/components/FeesField/GenericContainer.js
  25. 28
      src/components/GenuineCheck.js
  26. 4
      src/components/GradientBox.js
  27. 1
      src/components/IsUnlocked.js
  28. 47
      src/components/KeyboardContent.js
  29. 10
      src/components/MainSideBar/index.js
  30. 6
      src/components/ManagerPage/AppsList.js
  31. 36
      src/components/ManagerPage/Dashboard.js
  32. 106
      src/components/ManagerPage/FirmwareUpdate.js
  33. 69
      src/components/ManagerPage/HookDeviceChange.js
  34. 5
      src/components/ManagerPage/ManagerGenuineCheck.js
  35. 43
      src/components/ManagerPage/index.js
  36. 7
      src/components/Onboarding/index.js
  37. 98
      src/components/Onboarding/steps/Analytics.js
  38. 15
      src/components/Onboarding/steps/Finish.js
  39. 7
      src/components/Onboarding/steps/GenuineCheck/index.js
  40. 9
      src/components/Onboarding/steps/NoDevice.js
  41. 2
      src/components/OperationsList/index.js
  42. 57
      src/components/PerfIndicator.js
  43. 24
      src/components/PillsDaysCount.js
  44. 6
      src/components/RecipientAddress/index.js
  45. 6
      src/components/RenderError.js
  46. 37
      src/components/SelectExchange.js
  47. 4
      src/components/SettingsPage/AboutRowItem.js
  48. 1
      src/components/SettingsPage/CleanButton.js
  49. 24
      src/components/SettingsPage/CounterValueExchangeSelect.js
  50. 3
      src/components/SettingsPage/CounterValueSelect.js
  51. 6
      src/components/SettingsPage/LanguageSelect.js
  52. 8
      src/components/SettingsPage/MarketIndicatorRadio.js
  53. 2
      src/components/SettingsPage/RegionSelect.js
  54. 6
      src/components/SettingsPage/ReleaseNotesButton.js
  55. 1
      src/components/SettingsPage/ResetButton.js
  56. 6
      src/components/SettingsPage/SettingsSection.js
  57. 6
      src/components/SettingsPage/index.js
  58. 48
      src/components/SettingsPage/sections/About.js
  59. 2
      src/components/SettingsPage/sections/Currencies.js
  60. 2
      src/components/SettingsPage/sections/CurrencyRows.js
  61. 72
      src/components/SettingsPage/sections/Help.js
  62. 2
      src/components/StickyBackToTop.js
  63. 2
      src/components/TopBar/ActivityIndicator.js
  64. 1
      src/components/base/AccountsList/index.js
  65. 37
      src/components/base/Button/index.js
  66. 29
      src/components/base/LabelWithExternalIcon.js
  67. 156
      src/components/base/Markdown/index.js
  68. 4
      src/components/base/Modal/ConfirmModal.js
  69. 1
      src/components/base/Modal/stories.js
  70. 2
      src/components/base/Progress/index.js
  71. 10
      src/components/base/SideBar/SideBarList.js
  72. 3
      src/components/base/Switch/index.js
  73. 62
      src/components/layout/Default.js
  74. 47
      src/components/layout/Print.js
  75. 12
      src/components/modals/AccountSettingRenderBody.js
  76. 1
      src/components/modals/AddAccounts/index.js
  77. 24
      src/components/modals/AddAccounts/steps/03-step-import.js
  78. 13
      src/components/modals/Debug.js
  79. 36
      src/components/modals/OperationDetails.js
  80. 53
      src/components/modals/Receive/index.js
  81. 2
      src/components/modals/Receive/steps/01-step-account.js
  82. 3
      src/components/modals/Receive/steps/02-step-connect-device.js
  83. 63
      src/components/modals/Receive/steps/03-step-confirm-address.js
  84. 64
      src/components/modals/Receive/steps/04-step-receive-funds.js
  85. 126
      src/components/modals/ReleaseNotes.js
  86. 18
      src/components/modals/Send/fields/RecipientField.js
  87. 2
      src/components/modals/Send/steps/01-step-amount.js
  88. 2
      src/components/modals/Send/steps/02-step-connect-device.js
  89. 2
      src/components/modals/Send/steps/03-step-verification.js
  90. 5
      src/components/modals/Send/steps/04-step-confirmation.js
  91. 99
      src/components/modals/ShareAnalytics.js
  92. 2
      src/components/modals/StepConnectDevice.js
  93. 67
      src/components/modals/TechnicalData.js
  94. 11
      src/components/modals/UpdateFirmware/Disclaimer.js
  95. 30
      src/components/modals/UpdateFirmware/Installing.js
  96. 64
      src/components/modals/UpdateFirmware/index.js
  97. 101
      src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js
  98. 60
      src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js
  99. 38
      src/components/modals/UpdateFirmware/steps/03-step-confirmation.js
  100. 4
      src/config/constants.js

23
.github/ISSUE_TEMPLATE/bug_report.md

@ -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 -->

4
.github/ISSUE_TEMPLATE/discussion.md

@ -0,0 +1,4 @@
---
name: 🗣 Start a Discussion
about: Discuss to propose changes or suggest feature requests.
---

7
.github/ISSUE_TEMPLATE/question.md

@ -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
PULL_REQUEST_TEMPLATE.md → .github/PULL_REQUEST_TEMPLATE.md

60
src/analytics/segment.js

@ -3,8 +3,11 @@
import uuid from 'uuid/v4'
import logger from 'logger'
import invariant from 'invariant'
import { langAndRegionSelector } from 'reducers/settings'
import { getSystemLocale } from 'helpers/systemLocale'
import { langAndRegionSelector, shareAnalyticsSelector } from 'reducers/settings'
import { getCurrentDevice } from 'reducers/devices'
import type { State } from 'reducers'
import { load } from './inject-in-window'
invariant(typeof window !== 'undefined', 'analytics/segment must be called on renderer thread')
@ -16,12 +19,19 @@ if (!process.env.STORYBOOK_ENV) {
const sessionId = uuid()
const getContext = store => {
const state = store.getState()
const getContext = _store => ({
ip: '0.0.0.0',
})
const extraProperties = store => {
const state: State = store.getState()
const { language, region } = langAndRegionSelector(state)
const systemLocale = getSystemLocale()
const device = getCurrentDevice(state)
const deviceInfo = device && {
productId: device.productId,
}
return {
ip: '0.0.0.0',
appVersion: __APP_VERSION__,
language,
region,
@ -29,6 +39,7 @@ const getContext = store => {
systemLanguage: systemLocale.language,
systemRegion: systemLocale.region,
sessionId,
...deviceInfo,
}
}
@ -45,13 +56,9 @@ export const start = (store: *) => {
return
}
load()
analytics.identify(
id,
{},
{
context: getContext(store),
},
)
analytics.identify(id, extraProperties(store), {
context: getContext(store),
})
}
export const stop = () => {
@ -67,7 +74,7 @@ export const stop = () => {
export const track = (event: string, properties: ?Object) => {
logger.analyticsTrack(event, properties)
if (!storeInstance) {
if (!storeInstance || !shareAnalyticsSelector(storeInstance.getState())) {
return
}
const { analytics } = window
@ -75,14 +82,21 @@ export const track = (event: string, properties: ?Object) => {
logger.error('analytics is not available')
return
}
analytics.track(event, properties, {
context: getContext(storeInstance),
})
analytics.track(
event,
{
...extraProperties(storeInstance),
...properties,
},
{
context: getContext(storeInstance),
},
)
}
export const page = (category: string, name: ?string, properties: ?Object) => {
logger.analyticsPage(category, name, properties)
if (!storeInstance) {
if (!storeInstance || !shareAnalyticsSelector(storeInstance.getState())) {
return
}
const { analytics } = window
@ -90,7 +104,15 @@ export const page = (category: string, name: ?string, properties: ?Object) => {
logger.error('analytics is not available')
return
}
analytics.page(category, name, properties, {
context: getContext(storeInstance),
})
analytics.page(
category,
name,
{
...extraProperties(storeInstance),
...properties,
},
{
context: getContext(storeInstance),
},
)
}

4
src/bridge/LibcoreBridge.js

@ -105,6 +105,7 @@ const LibcoreBridge: WalletBridge<Transaction> = {
const accountOps = account.operations
const syncedOps = syncedAccount.operations
const patch: $Shape<Account> = {
id: syncedAccount.id,
freshAddress: syncedAccount.freshAddress,
freshAddressPath: syncedAccount.freshAddressPath,
balance: syncedAccount.balance,
@ -116,7 +117,8 @@ const LibcoreBridge: WalletBridge<Transaction> = {
accountOps.length !== syncedOps.length || // size change, we do a full refresh for now...
(accountOps.length > 0 &&
syncedOps.length > 0 &&
accountOps[0].id !== syncedOps[0].id) // if same size, only check if the last item has changed.
(accountOps[0].accountId !== syncedOps[0].accountId ||
accountOps[0].id !== syncedOps[0].id)) // if same size, only check if the last item has changed.
if (hasChanged) {
patch.operations = syncedAccount.operations

2
src/commands/index.js

@ -26,6 +26,7 @@ import listAppVersions from 'commands/listAppVersions'
import listCategories from 'commands/listCategories'
import listenDevices from 'commands/listenDevices'
import ping from 'commands/ping'
import shouldFlashMcu from 'commands/shouldFlashMcu'
import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu'
import testCrash from 'commands/testCrash'
@ -56,6 +57,7 @@ const all: Array<Command<any, any>> = [
listCategories,
listenDevices,
ping,
shouldFlashMcu,
signTransaction,
testApdu,
testCrash,

2
src/commands/libcoreSignAndBroadcast.js

@ -210,7 +210,7 @@ export async function doSignAndBroadcast({
// NB we don't check isCancelled() because the broadcast is not cancellable now!
onOperationBroadcasted({
id: txHash,
id: `${account.xpub}-${txHash}-OUT`,
hash: txHash,
type: 'OUT',
value: transaction.amount,

14
src/commands/shouldFlashMcu.js

@ -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

11
src/commands/testCrash.js

@ -2,16 +2,15 @@
// This is a test example for dev testing purpose.
import { Observable } from 'rxjs'
import { createCommand, Command } from 'helpers/ipc'
type Input = void
type Result = void
const cmd: Command<Input, Result> = createCommand('testCrash', () =>
Observable.create(() => {
process.exit(1)
}),
)
const cmd: Command<Input, Result> = createCommand('testCrash', () => {
// $FlowFixMe
crashTest() // eslint-disable-line
throw new Error()
})
export default cmd

15
src/components/App.js

@ -12,10 +12,8 @@ import theme from 'styles/theme'
import i18n from 'renderer/i18n/electron'
import OnboardingOrElse from 'components/OnboardingOrElse'
import ThrowBlock from 'components/ThrowBlock'
import Default from 'components/layout/Default'
import Print from 'components/layout/Print'
import CounterValues from 'helpers/countervalues'
import { BridgeSyncProvider } from 'bridge/BridgeSyncContext'
@ -34,14 +32,11 @@ const App = ({
<I18nextProvider i18n={i18n} initialLanguage={language}>
<ThemeProvider theme={theme}>
<ThrowBlock>
<OnboardingOrElse>
<ConnectedRouter history={history}>
<Switch>
<Route path="/print" component={Print} />
<Route component={Default} />
</Switch>
</ConnectedRouter>
</OnboardingOrElse>
<ConnectedRouter history={history}>
<Switch>
<Route component={Default} />
</Switch>
</ConnectedRouter>
</ThrowBlock>
</ThemeProvider>
</I18nextProvider>

2
src/components/CalculateBalance.js

@ -79,7 +79,7 @@ const mapStateToProps = (state: State, props: OwnProps) => {
balanceHistory,
balanceStart: balanceHistory[0].value,
balanceEnd,
hash: `${balanceHistory.length}_${balanceEnd}`,
hash: `${balanceHistory.length}_${balanceEnd}_${isAvailable.toString()}`,
}
}

140
src/components/CurrentAddress/index.js

@ -18,18 +18,17 @@ import QRCode from 'components/base/QRCode'
import IconRecheck from 'icons/Recover'
import IconCopy from 'icons/Copy'
import IconInfoCircle from 'icons/InfoCircle'
import IconShield from 'icons/Shield'
const Container = styled(Box).attrs({
borderRadius: 1,
alignItems: 'center',
bg: p =>
p.withQRCode ? (p.notValid ? rgba(p.theme.colors.alertRed, 0.02) : 'lightGrey') : 'transparent',
py: 4,
px: 5,
bg: p => (p.isAddressVerified === false ? rgba(p.theme.colors.alertRed, 0.02) : 'lightGrey'),
p: 6,
pb: 4,
})`
border: ${p => (p.notValid ? `1px dashed ${rgba(p.theme.colors.alertRed, 0.5)}` : 'none')};
border: ${p =>
p.isAddressVerified === false ? `1px dashed ${rgba(p.theme.colors.alertRed, 0.5)}` : 'none'};
`
const Address = styled(Box).attrs({
@ -46,6 +45,8 @@ const Address = styled(Box).attrs({
border: ${p => `1px dashed ${p.theme.colors.fog}`};
cursor: text;
user-select: text;
text-align: center;
min-width: 320px;
`
const CopyFeedback = styled(Box).attrs({
@ -66,7 +67,6 @@ const Label = styled(Box).attrs({
strong {
color: ${p => p.theme.colors.dark};
font-weight: 600;
border-bottom: 1px dashed ${p => p.theme.colors.dark};
}
`
@ -127,31 +127,17 @@ const FooterButton = ({
type Props = {
account: Account,
address: string,
amount?: number,
addressVerified?: boolean,
isAddressVerified?: boolean,
onCopy: () => void,
onPrint: () => void,
onShare: () => void,
onVerify: () => void,
t: T,
withBadge: boolean,
withFooter: boolean,
withQRCode: boolean,
withVerify: boolean,
}
class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
static defaultProps = {
addressVerified: null,
amount: null,
onCopy: noop,
onPrint: noop,
onShare: noop,
onVerify: noop,
withBadge: false,
withFooter: false,
withQRCode: false,
withVerify: false,
}
state = {
@ -164,38 +150,28 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
const {
account: { name: accountName, currency },
address,
addressVerified,
amount,
onCopy,
onPrint,
onShare,
onVerify,
isAddressVerified,
t,
withBadge,
withFooter,
withQRCode,
withVerify,
...props
} = this.props
const { copyFeedback } = this.state
const currencyName = currency.name
const notValid = addressVerified === false
const { copyFeedback } = this.state
return (
<Container withQRCode={withQRCode} notValid={notValid} {...props}>
{withQRCode && (
<Box mb={4}>
<QRCode
size={120}
data={encodeURIScheme({
address,
amount,
currency,
})}
/>
</Box>
)}
<Container isAddressVerified={isAddressVerified} {...props}>
<Box mb={4}>
<QRCode
size={120}
data={encodeURIScheme({
address,
currency,
})}
/>
</Box>
<Label>
<Box>
{accountName ? (
@ -207,48 +183,56 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
t('app:currentAddress.title')
)}
</Box>
<IconInfoCircle size={12} />
</Label>
<Address withQRCode={withQRCode} notValid={notValid}>
<Address>
{copyFeedback && <CopyFeedback>{t('app:common.addressCopied')}</CopyFeedback>}
{address}
</Address>
{withBadge && (
<Box horizontal flow={2} mt={2} alignItems="center">
<Box color={notValid ? 'alertRed' : 'wallet'}>
<IconShield height={32} width={28} />
</Box>
<Box shrink fontSize={12} color={notValid ? 'alertRed' : 'dark'} ff="Open Sans">
{t('app:currentAddress.message')}
</Box>
<Box horizontal flow={2} mt={2} alignItems="center" style={{ maxWidth: 320 }}>
<Box color={isAddressVerified === false ? 'alertRed' : 'wallet'}>
<IconShield height={32} width={28} />
</Box>
)}
{withFooter && (
<Footer>
<Box
shrink
fontSize={12}
color={isAddressVerified === false ? 'alertRed' : 'dark'}
ff="Open Sans"
>
{isAddressVerified === null
? t('app:currentAddress.messageIfUnverified', { currencyName })
: isAddressVerified
? t('app:currentAddress.messageIfAccepted', { currencyName })
: t('app:currentAddress.messageIfSkipped', { currencyName })}
</Box>
</Box>
<Footer>
{isAddressVerified !== null ? (
<FooterButton
icon={<IconRecheck size={16} />}
label={notValid ? t('app:common.verify') : t('app:common.reverify')}
label={
isAddressVerified === false ? t('app:common.verify') : t('app:common.reverify')
}
onClick={onVerify}
/>
<CopyToClipboard
data={address}
render={copy => (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copy')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
</Footer>
)}
) : null}
<CopyToClipboard
data={address}
render={copy => (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
</Footer>
</Container>
)
}

2
src/components/DashboardPage/AccountsOrder.js

@ -21,6 +21,7 @@ import { reorderAccounts } from 'actions/accounts'
import { accountsSelector } from 'reducers/accounts'
import { saveSettings } from 'actions/settings'
import Track from 'analytics/Track'
import BoldToggle from 'components/base/BoldToggle'
import Box from 'components/base/Box'
import DropDown, { DropDownItem } from 'components/base/DropDown'
@ -202,6 +203,7 @@ class AccountsOrder extends Component<Props> {
onStateChange={this.onStateChange}
value={sortItems.find(item => item.key === orderAccounts)}
>
<Track onUpdate event="ChangeSort" orderAccounts={orderAccounts} />
<Text ff="Open Sans|SemiBold" fontSize={4}>
{t('app:common.sortBy')}
</Text>

3
src/components/DeviceConfirm/index.js

@ -75,6 +75,7 @@ const PushButton = styled(Box)`
type Props = {
error?: boolean,
withoutPushDisplay?: boolean,
}
const SVG = (
@ -165,7 +166,7 @@ const SVG = (
const DeviceConfirm = (props: Props) => (
<Wrapper {...props}>
{!props.error ? <PushButton /> : null}
{!props.error && !props.withoutPushDisplay ? <PushButton /> : null}
<Check error={props.error} />
{SVG}
</Wrapper>

2
src/components/DeviceInteraction/DeviceInteractionStep.js

@ -180,7 +180,7 @@ class DeviceInteractionStep extends PureComponent<
<IconContainer isTransparent={!isActive && !isSuccess}>{step.icon}</IconContainer>
<Box py={4} justify="center" grow shrink>
{title && (
<Box color={isActive || isSuccess ? 'dark' : ''} ff="Open Sans|SemiBold">
<Box color={isActive || isSuccess ? 'dark' : ''} ff="Open Sans|Regular">
{title}
</Box>
)}

30
src/components/DeviceInteraction/components.js

@ -4,7 +4,6 @@ import React from 'react'
import styled from 'styled-components'
import { radii } from 'styles/theme'
import { rgba } from 'styles/helpers'
import TranslatedError from 'components/TranslatedError'
import Box from 'components/base/Box'
@ -13,12 +12,12 @@ import Spinner from 'components/base/Spinner'
import IconCheck from 'icons/Check'
import IconCross from 'icons/Cross'
import IconExclamationCircle from 'icons/ExclamationCircle'
import IconSmoothBorders from 'icons/SmoothBorders'
export const DeviceInteractionStepContainer = styled(Box).attrs({
horizontal: true,
ff: 'Open Sans',
fontSize: 3,
bg: 'white',
color: 'graphite',
})`
position: relative;
@ -27,21 +26,28 @@ export const DeviceInteractionStepContainer = styled(Box).attrs({
min-height: 80px;
border: 1px solid ${p => p.theme.colors.fog};
border-color: ${p =>
p.isError ? p.theme.colors.alertRed : p.isActive || p.isSuccess ? p.theme.colors.wallet : ''};
p.isError ? p.theme.colors.alertRed : p.isActive && !p.isFinished ? p.theme.colors.wallet : ''};
border-top-color: ${p => (p.isFirst || p.isActive ? '' : 'transparent')};
border-bottom-color: ${p => (p.isPrecedentActive ? 'transparent' : '')};
border-bottom-left-radius: ${p => (p.isLast ? `${radii[1]}px` : 0)};
border-bottom-right-radius: ${p => (p.isLast ? `${radii[1]}px` : 0)};
border-top-left-radius: ${p => (p.isFirst ? `${radii[1]}px` : 0)};
border-top-right-radius: ${p => (p.isFirst ? `${radii[1]}px` : 0)};
box-shadow: ${p =>
p.isActive && !p.isSuccess
? `
${rgba(p.isError ? p.theme.colors.alertRed : p.theme.colors.wallet, 0.2)} 0 0 3px 2px
`
: 'none'};
`
const AbsCenter = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
`
const smoothBorders = <IconSmoothBorders size={28} />
export const IconContainer = ({
children,
isTransparent,
@ -50,15 +56,15 @@ export const IconContainer = ({
isTransparent: boolean,
}) => (
<Box
align="center"
justify="center"
relative
color="dark"
style={{
width: 70,
opacity: isTransparent ? 0.5 : 1,
}}
>
{children}
<AbsCenter>{smoothBorders}</AbsCenter>
<AbsCenter>{children}</AbsCenter>
</Box>
)

0
src/components/EnsureDevice.js

8
src/components/EnsureDeviceApp.js

@ -27,8 +27,8 @@ import { getCurrentDevice } from 'reducers/devices'
export const WrongAppOpened = createCustomErrorClass('WrongAppOpened')
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
const usbIcon = <IconUsb size={30} />
const Bold = props => <Text ff="Open Sans|Bold" {...props} />
const usbIcon = <IconUsb size={16} />
const Bold = props => <Text ff="Open Sans|SemiBold" {...props} />
const mapStateToProps = state => ({
device: getCurrentDevice(state),
@ -79,7 +79,7 @@ class EnsureDeviceApp extends Component<{
return (
<Trans i18nKey="deviceConnect:step2.open" parent="div">
{'Open the '}
<strong>{cur.name}</strong>
<Bold>{cur.name}</Bold>
{' app on your device'}
</Trans>
)
@ -107,7 +107,7 @@ class EnsureDeviceApp extends Component<{
{
id: 'address',
title: this.renderOpenAppTitle,
icon: Icon ? <Icon size={30} /> : null,
icon: Icon ? <Icon size={16} /> : null,
run: this.openAppInteractionHandler,
},
]}

6
src/components/ExchangePage/ExchangeCard.js

@ -1,8 +1,7 @@
// @flow
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { track } from 'analytics/segment'
import { openURL } from 'helpers/linking'
import type { T } from 'types/common'
@ -19,8 +18,7 @@ type CardType = {
export default class ExchangeCard extends PureComponent<{ t: T, card: CardType }> {
onClick = () => {
const { card } = this.props
shell.openExternal(card.url)
track('VisitExchange', { id: card.id, url: card.url })
openURL(card.url, 'VisitExchange', { id: card.id })
}
render() {
const {

6
src/components/ExchangePage/index.js

@ -44,13 +44,13 @@ class ExchangePage extends PureComponent<Props> {
return (
<Box pb={6}>
<TrackPage category="Exchange" />
<Box ff="Museo Sans|Regular" color="dark" fontSize={7} mb={3}>
<Box ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:exchange.title')}
</Box>
<Box ff="Museo Sans|Light" color="grey" fontSize={5} mb={5}>
<Box ff="Museo Sans|Light" fontSize={5} mb={5}>
{t('app:exchange.desc')}
</Box>
<Box flow={5}>{cards.map(card => <ExchangeCard key={card.key} t={t} card={card} />)}</Box>
<Box flow={3}>{cards.map(card => <ExchangeCard key={card.key} t={t} card={card} />)}</Box>
</Box>
)
}

2
src/components/FeesField/BitcoinKind.js

@ -125,7 +125,7 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, State>
const satoshi = units[units.length - 1]
return (
<GenericContainer error={error} help={t('app:send.steps.amount.unitPerByte')}>
<GenericContainer error={error}>
<Select width={156} options={items} value={selectedItem} onChange={this.onSelectChange} />
<InputCurrency
ref={this.input}

2
src/components/FeesField/EthereumKind.js

@ -32,7 +32,7 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, *> {
const { account, gasPrice, error, onChange } = this.props
const { units } = account.currency
return (
<GenericContainer error={error} help="Gas">
<GenericContainer error={error}>
<InputCurrency
defaultUnit={units.length > 1 ? units[1] : units[0]}
units={units}

31
src/components/FeesField/GenericContainer.js

@ -3,20 +3,23 @@
import React from 'react'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import LabelInfoTooltip from 'components/base/LabelInfoTooltip'
import LabelWithExternalIcon from 'components/base/LabelWithExternalIcon'
import { translate } from 'react-i18next'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import { track } from 'analytics/segment'
export default translate()(
({ help, children, t }: { help: string, children: React$Node, t: * }) => (
<Box flow={1}>
<Label>
<span>{t('app:send.steps.amount.fees')}</span>
{help ? <LabelInfoTooltip ml={1} text={help} /> : null}
</Label>
<Box horizontal flow={5}>
{children}
</Box>
export default translate()(({ children, t }: { children: React$Node, t: * }) => (
<Box flow={1}>
<LabelWithExternalIcon
onClick={() => {
openURL(urls.feesMoreInfo)
track('Send Flow Fees Help Requested')
}}
label={t('app:send.steps.amount.fees')}
/>
<Box horizontal flow={5}>
{children}
</Box>
),
)
</Box>
))

28
src/components/GenuineCheck.js

@ -24,27 +24,28 @@ import Text from 'components/base/Text'
import IconUsb from 'icons/Usb'
import IconHome from 'icons/Home'
import IconGenuineCheck from 'icons/GenuineCheck'
import IconCheck from 'icons/Check'
const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine')
const DeviceGenuineSocketEarlyClose = createCustomErrorClass('DeviceGenuineSocketEarlyClose')
type Props = {
t: T,
onSuccess: void => void,
onFail?: Error => void,
onUnavailable?: Error => void,
onSuccess: (*) => void,
device: ?Device,
}
const usbIcon = <IconUsb size={26} />
const homeIcon = <IconHome size={24} />
const genuineCheckIcon = <IconGenuineCheck size={24} />
const usbIcon = <IconUsb size={16} />
const homeIcon = <IconHome size={16} />
const genuineCheckIcon = <IconCheck size={16} />
const mapStateToProps = state => ({
device: getCurrentDevice(state),
})
const Bold = props => <Text ff="Open Sans|Bold" {...props} />
const Bold = props => <Text ff="Open Sans|SemiBold" {...props} />
// to speed up genuine check, cache result by device id
const genuineDevices = new WeakSet()
@ -72,15 +73,30 @@ class GenuineCheck extends PureComponent<Props> {
device: Device,
deviceInfo: DeviceInfo,
}) => {
if (deviceInfo.isOSU || deviceInfo.isBootloader) {
logger.log('device is in update mode. skipping genuine')
return true
}
if (genuineDevices.has(device)) {
logger.log("genuine was already checked. don't check again")
await delay(GENUINE_CACHE_DELAY)
return true
}
const beforeDate = Date.now()
const res = await getIsGenuine
.send({ devicePath: device.path, deviceInfo })
.pipe(timeout(GENUINE_TIMEOUT))
.toPromise()
logger.log(`genuine check resulted ${res} after ${(Date.now() - beforeDate) / 1000}s`, {
deviceInfo,
})
if (!res) {
throw new DeviceGenuineSocketEarlyClose()
}
const isGenuine = res === '0000'
if (!isGenuine) {
throw new DeviceNotGenuineError()

4
src/components/GradientBox.js

@ -5,10 +5,10 @@ export default styled.div`
width: 100%;
height: 60px;
position: absolute;
bottom: 68px;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(rgba(255, 255, 255, 0), #ffffff 70%);
background: linear-gradient(rgba(255, 255, 255, 0), #ffffff);
z-index: 2;
pointer-events: none;
`

1
src/components/IsUnlocked.js

@ -186,6 +186,7 @@ class IsUnlocked extends Component<Props, State> {
</Box>
</form>
<ConfirmModal
analyticsName="HardReset"
isDanger
isLoading={isHardResetting}
isOpened={isHardResetModalOpened}

47
src/components/KeyboardContent.js

@ -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

10
src/components/MainSideBar/index.js

@ -15,6 +15,7 @@ import type { UpdateStatus } from 'reducers/update'
import { MODAL_RECEIVE, MODAL_SEND, MODAL_ADD_ACCOUNTS } from 'config/constants'
import { i } from 'helpers/staticPath'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
@ -65,6 +66,13 @@ class MainSideBar extends PureComponent<Props> {
push(to)
}
ADD_ACCOUNT_EMPTY_STATE = (
<Box relative>
<img style={{ position: 'absolute', top: 0, right: 5 }} alt="" src={i('arrow-add.svg')} />
{this.props.t('app:emptyState.sidebar.text')}
</Box>
)
handleClickDashboard = () => this.push('/')
handleOpenSendModal = () => this.props.openModal(MODAL_SEND)
handleOpenReceiveModal = () => this.props.openModal(MODAL_RECEIVE)
@ -130,7 +138,7 @@ class MainSideBar extends PureComponent<Props> {
<SideBarList
title={t('app:sidebar.accounts', { count: accounts.length })}
titleRight={addAccountButton}
emptyText={t('app:emptyState.sidebar.text')}
emptyState={this.ADD_ACCOUNT_EMPTY_STATE}
>
{accounts.map(account => (
<AccountListItem

6
src/components/ManagerPage/AppsList.js

@ -34,6 +34,7 @@ import Update from 'icons/Update'
import Trash from 'icons/Trash'
import CheckCircle from 'icons/CheckCircle'
import { FreezeDeviceChangeEvents } from './HookDeviceChange'
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
import AppSearchBar from './AppSearchBar'
@ -170,6 +171,7 @@ class AppsList extends PureComponent<Props, State> {
isOpened={status !== 'idle' && status !== 'loading'}
render={() => (
<ModalBody align="center" justify="center" style={{ height: 300 }}>
<FreezeDeviceChangeEvents />
{status === 'busy' || status === 'idle' ? (
<Fragment>
<ModalTitle>
@ -205,7 +207,7 @@ class AppsList extends PureComponent<Props, State> {
<ExclamationCircleThin size={44} />
</Box>
<Box
color="black"
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
@ -238,7 +240,7 @@ class AppsList extends PureComponent<Props, State> {
<CheckCircle size={44} />
</Box>
<Box
color="black"
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"

36
src/components/ManagerPage/Dashboard.js

@ -1,13 +1,16 @@
// @flow
import React from 'react'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import type { T, Device } from 'types/common'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import IconExternalLink from 'icons/ExternalLink'
import TrackPage from 'analytics/TrackPage'
import FakeLink from 'components/base/FakeLink'
import AppsList from './AppsList'
import FirmwareUpdate from './FirmwareUpdate'
@ -16,26 +19,47 @@ type Props = {
t: T,
device: Device,
deviceInfo: DeviceInfo,
handleHelpRequest: () => void,
}
const Dashboard = ({ device, deviceInfo, t }: Props) => (
const Dashboard = ({ device, deviceInfo, t, handleHelpRequest }: Props) => (
<Box flow={4} pb={8}>
<TrackPage category="Manager" name="Dashboard" />
<Box>
<Text ff="Museo Sans|Regular" fontSize={7} color="black">
<Text ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:manager.title')}
</Text>
<Text ff="Museo Sans|Light" fontSize={5}>
{t('app:manager.subtitle')}
</Text>
<Box horizontal>
<Text ff="Museo Sans|Light" fontSize={5}>
{t('app:manager.subtitle')}
</Text>
<ContainerToHover>
<FakeLink mr={1} underline color="grey" fontSize={4} onClick={handleHelpRequest}>
{t('app:common.needHelp')}
</FakeLink>
<IconExternalLink size={14} />
</ContainerToHover>
</Box>
</Box>
<Box mt={5}>
<FirmwareUpdate deviceInfo={deviceInfo} device={device} />
</Box>
<Box mt={5}>
<AppsList device={device} deviceInfo={deviceInfo} />
{deviceInfo.isOSU || deviceInfo.isBootloader ? null : (
<AppsList device={device} deviceInfo={deviceInfo} />
)}
</Box>
</Box>
)
export default translate()(Dashboard)
const ContainerToHover = styled(Box).attrs({
align: 'center',
ml: 'auto',
horizontal: true,
})`
${FakeLink}:hover, &:hover {
color: ${p => p.theme.colors.wallet};
}
`

106
src/components/ManagerPage/FirmwareUpdate.js

@ -6,13 +6,14 @@ import { translate } from 'react-i18next'
import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty'
import invariant from 'invariant'
import logger from 'logger'
import type { Device, T } from 'types/common'
import type { LedgerScriptParams } from 'helpers/common'
import type { StepId } from 'components/modals/UpdateFirmware'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import shouldFlashMcu from 'commands/shouldFlashMcu'
import installOsuFirmware from 'commands/installOsuFirmware'
import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu'
@ -25,6 +26,7 @@ import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text'
import NanoS from 'icons/device/NanoS'
import Blue from 'icons/device/Blue'
import CheckFull from 'icons/CheckFull'
import UpdateFirmwareButton from './UpdateFirmwareButton'
@ -37,26 +39,36 @@ export type ModalStatus = 'closed' | 'disclaimer' | 'install' | 'error' | 'succe
type Props = {
t: T,
deviceInfo: DeviceInfo,
device: Device,
}
type State = {
latestFirmware: ?LedgerScriptParams & ?{ shouldUpdateMcu: boolean },
latestFirmware: ?LedgerScriptParams & ?{ shouldFlashMcu: boolean },
modal: ModalStatus,
stepId: ?StepId,
shouldFlash: boolean,
ready: boolean,
}
const intializeState = ({ deviceInfo }): State => ({
latestFirmware: null,
modal: 'closed',
stepId: deviceInfo.isBootloader ? 'updateMCU' : 'idCheck',
shouldFlash: false,
ready: false,
})
class FirmwareUpdate extends PureComponent<Props, State> {
state = {
latestFirmware: null,
modal: 'closed',
}
state = intializeState(this.props)
componentDidMount() {
this.fetchLatestFirmware()
}
componentDidUpdate() {
if (isEmpty(this.state.latestFirmware)) {
const { deviceInfo } = this.props
if (!deviceInfo.isOSU && !deviceInfo.isBootloader) {
this.fetchLatestFirmware()
} else if (deviceInfo.isOSU) {
this.shouldFlashMcu()
} else if (deviceInfo.isBootloader) {
this.handleInstallModal('updateMCU', true)
}
}
@ -74,61 +86,61 @@ class FirmwareUpdate extends PureComponent<Props, State> {
!isEqual(this.state.latestFirmware, latestFirmware) &&
!this._unmounting
) {
this.setState({ latestFirmware })
this.setState({ latestFirmware, ready: true })
}
}
installOsuFirmware = async (device: Device) => {
try {
const { latestFirmware } = this.state
const { deviceInfo } = this.props
invariant(latestFirmware, 'did not find a new firmware or firmware is not set')
this.setState({ modal: 'install' })
const { success } = await installOsuFirmware
.send({ devicePath: device.path, firmware: latestFirmware, targetId: deviceInfo.targetId })
.toPromise()
return success
} catch (err) {
logger.log(err)
throw err
shouldFlashMcu = async () => {
const { deviceInfo } = this.props
const shouldFlash = await shouldFlashMcu.send(deviceInfo).toPromise()
if (!this._unmounting) {
this.setState({ shouldFlash, modal: 'install', stepId: 'idCheck', ready: true })
}
}
installFinalFirmware = async (device: Device) => {
try {
const { success } = await installFinalFirmware.send({ devicePath: device.path }).toPromise()
return success
} catch (err) {
logger.log(err)
throw err
}
}
installOsuFirmware = async (device: Device) => {
const { latestFirmware } = this.state
const { deviceInfo } = this.props
invariant(latestFirmware, 'did not find a new firmware or firmware is not set')
this.setState({ modal: 'install' })
const result = await installOsuFirmware
.send({ devicePath: device.path, firmware: latestFirmware, targetId: deviceInfo.targetId })
.toPromise()
flashMCU = async (device: Device) => {
await installMcu.send({ devicePath: device.path }).toPromise()
return result
}
installFinalFirmware = (device: Device) =>
installFinalFirmware.send({ devicePath: device.path }).toPromise()
flashMCU = async (device: Device) => installMcu.send({ devicePath: device.path }).toPromise()
handleCloseModal = () => this.setState({ modal: 'closed' })
handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' })
handleInstallModal = () => this.setState({ modal: 'install' })
handleInstallModal = (stepId: StepId = 'idCheck', shouldFlash?: boolean) =>
this.setState({ modal: 'install', stepId, shouldFlash, ready: true })
handleDisclaimerNext = () => this.setState({ modal: 'install' })
render() {
const { deviceInfo, t } = this.props
const { latestFirmware, modal } = this.state
const { deviceInfo, t, device } = this.props
const { latestFirmware, modal, stepId, shouldFlash, ready } = this.state
return (
<Card p={4}>
<Box horizontal align="center" flow={2}>
<Box color="dark">
<NanoS size={30} />
{device.product === 'Blue' ? <Blue size={30} /> : <NanoS size={30} />}
</Box>
<Box>
<Box horizontal align="center">
<Text ff="Open Sans|SemiBold" fontSize={4} color="dark">
Ledger Nano S
{device.product === 'Blue'
? t('app:manager.firmware.titleBlue')
: t('app:manager.firmware.titleNano')}
</Text>
<Box color="wallet" style={{ marginLeft: 10 }}>
<Box color="wallet" ml={2}>
<Tooltip render={() => t('app:manager.yourDeviceIsGenuine')}>
<CheckFull size={13} color="wallet" />
</Tooltip>
@ -142,24 +154,26 @@ class FirmwareUpdate extends PureComponent<Props, State> {
</Box>
<UpdateFirmwareButton firmware={latestFirmware} onClick={this.handleDisclaimerModal} />
</Box>
{latestFirmware && (
{ready ? (
<Fragment>
<DisclaimerModal
firmware={latestFirmware}
status={modal}
goToNextStep={this.handleInstallModal}
goToNextStep={this.handleDisclaimerNext}
onClose={this.handleCloseModal}
/>
<UpdateModal
status={modal}
stepId={stepId}
onClose={this.handleCloseModal}
firmware={latestFirmware}
shouldFlashMcu={shouldFlash}
installOsuFirmware={this.installOsuFirmware}
installFinalFirmware={this.installFinalFirmware}
flashMCU={this.flashMCU}
/>
</Fragment>
)}
) : null}
</Card>
)
}

69
src/components/ManagerPage/HookDeviceChange.js

@ -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)

5
src/components/ManagerPage/ManagerGenuineCheck.js

@ -22,16 +22,15 @@ class ManagerGenuineCheck extends PureComponent<Props> {
render() {
const { t, onSuccess } = this.props
return (
<Box align="center">
<Box align="center" py={7}>
<TrackPage category="Manager" name="Genuine Check" />
<Space of={60} />
<Box align="center" style={{ maxWidth: 460 }}>
<img
src={i('logos/connectDevice.png')}
alt="connect your device"
style={{ marginBottom: 30, maxWidth: 362, width: '100%' }}
/>
<Text ff="Museo Sans|Regular" fontSize={7} color="black" style={{ marginBottom: 10 }}>
<Text ff="Museo Sans|Regular" fontSize={7} color="dark" style={{ marginBottom: 10 }}>
{t('app:manager.device.title')}
</Text>
<Text ff="Museo Sans|Light" fontSize={5} color="grey" align="center">

43
src/components/ManagerPage/index.js

@ -1,7 +1,9 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import type { Device } from 'types/common'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
@ -9,6 +11,7 @@ import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import Dashboard from './Dashboard'
import ManagerGenuineCheck from './ManagerGenuineCheck'
import HookDeviceChange from './HookDeviceChange'
type Props = {}
@ -18,18 +21,32 @@ type State = {
deviceInfo: ?DeviceInfo,
}
const INITIAL_STATE = {
isGenuine: null,
device: null,
deviceInfo: null,
}
class ManagerPage extends PureComponent<Props, State> {
state = {
isGenuine: null,
device: null,
deviceInfo: null,
}
state = INITIAL_STATE
// prettier-ignore
handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { // eslint-disable-line react/no-unused-prop-types
this.setState({ isGenuine: true, device, deviceInfo })
}
handleHelpRequest = () => {
openURL(urls.managerHelpRequest)
}
onDeviceChanges = () => {
this.setState(INITIAL_STATE)
}
onDeviceDisconnected = () => {
this.setState(INITIAL_STATE)
}
render() {
const { isGenuine, device, deviceInfo } = this.state
@ -40,7 +57,19 @@ class ManagerPage extends PureComponent<Props, State> {
invariant(device, 'Inexistant device considered genuine')
invariant(deviceInfo, 'Inexistant device infos for genuine device')
return <Dashboard device={device} deviceInfo={deviceInfo} />
return (
<Fragment>
<HookDeviceChange
onDeviceChanges={this.onDeviceChanges}
onDeviceDisconnected={this.onDeviceDisconnected}
/>
<Dashboard
device={device}
deviceInfo={deviceInfo}
handleHelpRequest={this.handleHelpRequest}
/>
</Fragment>
)
}
}

7
src/components/Onboarding/index.js

@ -25,9 +25,7 @@ import { getCurrentDevice } from 'reducers/devices'
import { unlock } from 'reducers/application'
import ExportLogsBtn from 'components/ExportLogsBtn'
import Box from 'components/base/Box'
import TriggerAppReady from '../TriggerAppReady'
import Start from './steps/Start'
import InitStep from './steps/Init'
@ -96,6 +94,7 @@ export type StepProps = {
savePassword: Function,
getDeviceInfo: Function,
updateGenuineCheck: Function,
openModal: Function,
isLedgerNano: Function,
flowType: Function,
}
@ -145,6 +144,7 @@ class Onboarding extends PureComponent<Props> {
onboarding,
settings,
updateGenuineCheck,
openModal,
isLedgerNano,
flowType,
prevStep,
@ -158,9 +158,6 @@ class Onboarding extends PureComponent<Props> {
return (
<Container>
<TriggerAppReady />
<ExportLogsBtn hookToShortcut />
{step.options.showBreadcrumb && <OnboardingBreadcrumb />}
<StepContainer>
<StepComponent {...stepProps} />

98
src/components/Onboarding/steps/Analytics.js

@ -6,14 +6,19 @@ import { connect } from 'react-redux'
import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import Switch from 'components/base/Switch'
import FakeLink from 'components/base/FakeLink'
import TrackPage from 'analytics/TrackPage'
import Track from 'analytics/Track'
import { openModal } from 'reducers/modals'
import { MODAL_SHARE_ANALYTICS, MODAL_TECHNICAL_DATA } from 'config/constants'
import ShareAnalytics from '../../modals/ShareAnalytics'
import TechnicalData from '../../modals/TechnicalData'
import { Title, Description, FixedTopContainer, StepContainerInner } from '../helperComponents'
import OnboardingFooter from '../OnboardingFooter'
import type { StepProps } from '..'
const mapDispatchToProps = { saveSettings }
const mapDispatchToProps = { saveSettings, openModal }
type State = {
analyticsToggle: boolean,
@ -21,7 +26,7 @@ type State = {
}
const INITIAL_STATE = {
analyticsToggle: true,
analyticsToggle: false,
sentryLogsToggle: true,
}
@ -46,7 +51,12 @@ class Analytics extends PureComponent<StepProps, State> {
savePassword(undefined)
prevStep()
}
handleShareAnalyticsModal = () => {
this.props.openModal(MODAL_SHARE_ANALYTICS)
}
handleTechnicalDataModal = () => {
this.props.openModal(MODAL_TECHNICAL_DATA)
}
render() {
const { nextStep, t, onboarding } = this.props
const { analyticsToggle, sentryLogsToggle } = this.state
@ -65,24 +75,47 @@ class Analytics extends PureComponent<StepProps, State> {
<Box mt={5}>
<Container>
<Box>
<AnalyticsTitle>{t('onboarding:analytics.sentryLogs.title')}</AnalyticsTitle>
<AnalyticsText>{t('onboarding:analytics.sentryLogs.desc')}</AnalyticsText>
<Box horizontal mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.technicalData.title')}</AnalyticsTitle>
<LearnMoreWrapper>
<FakeLink
underline
fontSize={3}
color="smoke"
ml={2}
onClick={this.handleTechnicalDataModal}
>
{t('app:common.learnMore')}
</FakeLink>
</LearnMoreWrapper>
</Box>
<TechnicalData />
<AnalyticsText>{t('onboarding:analytics.technicalData.desc')}</AnalyticsText>
<MandatoryText>
{t('onboarding:analytics.technicalData.mandatoryText')}
</MandatoryText>
</Box>
<Box justifyContent="center">
<Track
onUpdate
event={
sentryLogsToggle
? 'Sentry Logs Enabled Onboarding'
: 'Sentry Logs Disabled Onboarding'
}
/>
<Switch isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} />
<Switch disabled isChecked />
</Box>
</Container>
<Container>
<Box>
<AnalyticsTitle>{t('onboarding:analytics.shareAnalytics.title')}</AnalyticsTitle>
<Box horizontal mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.shareAnalytics.title')}</AnalyticsTitle>
<LearnMoreWrapper>
<FakeLink
style={{ textDecoration: 'underline' }}
fontSize={3}
color="smoke"
ml={2}
onClick={this.handleShareAnalyticsModal}
>
{t('app:common.learnMore')}
</FakeLink>
</LearnMoreWrapper>
<ShareAnalytics />
</Box>
<AnalyticsText>{t('onboarding:analytics.shareAnalytics.desc')}</AnalyticsText>
</Box>
<Box justifyContent="center">
@ -97,6 +130,25 @@ class Analytics extends PureComponent<StepProps, State> {
<Switch isChecked={analyticsToggle} onChange={this.handleAnalyticsToggle} />
</Box>
</Container>
<Container>
<Box>
<Box mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.sentryLogs.title')}</AnalyticsTitle>
</Box>
<AnalyticsText>{t('onboarding:analytics.sentryLogs.desc')}</AnalyticsText>
</Box>
<Box justifyContent="center">
<Track
onUpdate
event={
sentryLogsToggle
? 'Sentry Logs Enabled Onboarding'
: 'Sentry Logs Disabled Onboarding'
}
/>
<Switch isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} />
</Box>
</Container>
</Box>
</StepContainerInner>
<OnboardingFooter
@ -117,6 +169,13 @@ export default connect(
mapDispatchToProps,
)(Analytics)
const MandatoryText = styled(Box).attrs({
ff: 'Open Sans|Regular',
fontSize: 2,
textAlign: 'left',
color: 'grey',
mt: 1,
})``
export const AnalyticsText = styled(Box).attrs({
ff: 'Open Sans|Regular',
fontSize: 3,
@ -129,9 +188,7 @@ export const AnalyticsTitle = styled(Box).attrs({
ff: 'Open Sans|SemiBold',
fontSize: 4,
textAlign: 'left',
})`
margin-bottom: 5px;
`
})``
const Container = styled(Box).attrs({
horizontal: true,
p: 3,
@ -139,3 +196,8 @@ const Container = styled(Box).attrs({
width: 550px;
justify-content: space-between;
`
const LearnMoreWrapper = styled(Box).attrs({})`
${FakeLink}:hover {
color: ${p => p.theme.colors.wallet};
}
`

15
src/components/Onboarding/steps/Finish.js

@ -1,9 +1,10 @@
// @flow
import React, { Component } from 'react'
import { shell } from 'electron'
import { openURL } from 'helpers/linking'
import styled from 'styled-components'
import { i } from 'helpers/staticPath'
import { urls } from 'config/support'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
@ -32,21 +33,21 @@ const socialMedia = [
// FIXME it should just be vdom in place
{
key: 'twitter',
url: 'https://twitter.com/LedgerHQ',
url: urls.twitter,
icon: <IconSocialTwitter size={24} />,
onClick: url => shell.openExternal(url),
onClick: url => openURL(url),
},
{
key: 'github',
url: 'https://github.com/LedgerHQ/ledger-live-desktop',
url: urls.github,
icon: <IconSocialGithub size={24} />,
onClick: url => shell.openExternal(url),
onClick: url => openURL(url),
},
{
key: 'reddit',
url: 'https://www.reddit.com/r/ledgerwallet/',
url: urls.reddit,
icon: <IconSocialReddit size={24} />,
onClick: url => shell.openExternal(url),
onClick: url => openURL(url),
},
]

7
src/components/Onboarding/steps/GenuineCheck/index.js

@ -1,10 +1,11 @@
// @flow
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { openURL } from 'helpers/linking'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { colors } from 'styles/theme'
import { urls } from 'config/support'
import { updateGenuineCheck } from 'reducers/onboarding'
@ -143,9 +144,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
}
contactSupport = () => {
const contactSupportUrl =
'https://support.ledgerwallet.com/hc/en-us/requests/new?ticket_form_id=248165'
shell.openExternal(contactSupportUrl)
openURL(urls.genuineCheckContactSupport)
}
handlePrevStep = () => {

9
src/components/Onboarding/steps/NoDevice.js

@ -1,12 +1,13 @@
// @flow
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { openURL } from 'helpers/linking'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import TrackPage from 'analytics/TrackPage'
import { urls } from 'config/support'
import IconCart from 'icons/Cart'
import IconTruck from 'icons/Truck'
import IconInfoCircle from 'icons/InfoCircle'
@ -26,7 +27,7 @@ class NoDevice extends PureComponent<StepProps, *> {
icon: <IconCart size={20} />,
title: t('onboarding:noDevice.buyNew.title'),
onClick: () => {
shell.openExternal('https://www.ledgerwallet.com/')
openURL(urls.noDeviceBuyNew)
},
},
{
@ -34,7 +35,7 @@ class NoDevice extends PureComponent<StepProps, *> {
icon: <IconTruck size={20} />,
title: t('onboarding:noDevice.trackOrder.title'),
onClick: () => {
shell.openExternal('http://order.ledgerwallet.com/')
openURL(urls.noDeviceTrackOrder)
},
},
{
@ -42,7 +43,7 @@ class NoDevice extends PureComponent<StepProps, *> {
icon: <IconInfoCircle size={20} />,
title: t('onboarding:noDevice.learnMore.title'),
onClick: () => {
shell.openExternal('https://www.ledgerwallet.com/')
openURL(urls.noDeviceLearnMore)
},
},
]

2
src/components/OperationsList/index.js

@ -25,6 +25,7 @@ import IconAngleDown from 'icons/AngleDown'
import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text'
import Track from 'analytics/Track'
import { track } from 'analytics/segment'
import SectionTitle from './SectionTitle'
import OperationC from './Operation'
@ -81,6 +82,7 @@ export class OperationsList extends PureComponent<Props, State> {
// TODO: convert of async/await if fetching with the api
fetchMoreOperations = () => {
track('FetchMoreOperations')
this.setState({ nbToShow: this.state.nbToShow + 20 })
}

57
src/components/PerfIndicator.js

@ -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

24
src/components/PillsDaysCount.js

@ -1,11 +1,12 @@
// @flow
import React, { PureComponent } from 'react'
import React, { Fragment, PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Pills from 'components/base/Pills'
import { timeRangeDaysByKey } from 'reducers/settings'
import type { TimeRange } from 'reducers/settings'
import Track from 'analytics/Track'
type Props = {
selected: string,
@ -17,15 +18,18 @@ class PillsDaysCount extends PureComponent<Props> {
render() {
const { selected, onChange, t } = this.props
return (
<Pills
items={Object.keys(timeRangeDaysByKey).map((key: TimeRange) => ({
key,
value: timeRangeDaysByKey[key],
label: t(`app:time.${key}`),
}))}
activeKey={selected}
onChange={onChange}
/>
<Fragment>
<Track onUpdate event="PillsDaysChange" selected={selected} />
<Pills
items={Object.keys(timeRangeDaysByKey).map((key: TimeRange) => ({
key,
value: timeRangeDaysByKey[key],
label: t(`app:time.${key}`),
}))}
activeKey={selected}
onChange={onChange}
/>
</Fragment>
)
}
}

6
src/components/RecipientAddress/index.js

@ -11,6 +11,7 @@ import { radii } from 'styles/theme'
import QRCodeCameraPickerCanvas from 'components/QRCodeCameraPickerCanvas'
import Box from 'components/base/Box'
import Input from 'components/base/Input'
import { track } from 'analytics/segment'
import IconQrCode from 'icons/QrCode'
@ -64,10 +65,13 @@ class RecipientAddress extends PureComponent<Props, State> {
qrReaderOpened: false,
}
handleClickQrCode = () =>
handleClickQrCode = () => {
const { qrReaderOpened } = this.state
this.setState(prev => ({
qrReaderOpened: !prev.qrReaderOpened,
}))
!qrReaderOpened ? track('Send Flow QR Code Opened') : track('Send Flow QR Code Closed')
}
handleOnPick = (code: string) => {
const { address, ...rest } = decodeURIScheme(code)

6
src/components/RenderError.js

@ -2,7 +2,8 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { shell, remote } from 'electron'
import { openURL } from 'helpers/linking'
import { remote } from 'electron'
import qs from 'querystring'
import { translate } from 'react-i18next'
@ -60,7 +61,7 @@ ${error.stack}
\`\`\`
`,
})
shell.openExternal(`https://github.com/LedgerHQ/ledger-live-desktop/issues/new?${q}`)
openURL(`https://github.com/LedgerHQ/ledger-live-desktop/issues/new?${q}`)
}
handleRestart = () => {
@ -112,6 +113,7 @@ ${error.stack}
</Button>
</Box>
<ConfirmModal
analyticsName="HardReset"
isDanger
isLoading={isHardResetting}
isOpened={isHardResetModalOpened}

37
src/components/SelectExchange.js

@ -1,11 +1,12 @@
// @flow
import React, { Component } from 'react'
import React, { Fragment, Component } from 'react'
import { translate } from 'react-i18next'
import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import type { Exchange } from '@ledgerhq/live-common/lib/countervalues/types'
import logger from 'logger'
import Track from 'analytics/Track'
import Select from 'components/base/Select'
import Text from 'components/base/Text'
import CounterValues from 'helpers/countervalues'
@ -91,27 +92,37 @@ class SelectExchange extends Component<
}
render() {
const { onChange, exchangeId, style, t, ...props } = this.props
const { onChange, exchangeId, style, t, from, to, ...props } = this.props
const { exchanges, error } = this.state
const options = exchanges ? exchanges.map(e => ({ value: e.id, label: e.name, ...e })) : []
const value = options.find(e => e.id === exchangeId)
return error ? (
<Text ff="Open Sans|SemiBold" color="dark" fontSize={4}>
{t('app:common.error.load')}
</Text>
) : (
<Select
value={options.find(e => e.id === exchangeId)}
options={options}
onChange={onChange}
isLoading={options.length === 0}
placeholder={t('app:common.selectExchange')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectExchangeNoOption', { exchangeName: inputValue })
}
{...props}
/>
<Fragment>
<Track
onUpdate
event="SelectExchange"
exchangeName={value && value.id}
fromCurrency={from.ticker}
toCurrency={to.ticker}
/>
<Select
value={value}
options={options}
onChange={onChange}
isLoading={options.length === 0}
placeholder={t('app:common.selectExchange')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectExchangeNoOption', { exchangeName: inputValue })
}
{...props}
/>
</Fragment>
)
}
}

4
src/components/SettingsPage/AboutRowItem.js

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { openURL } from 'helpers/linking'
import IconExternalLink from 'icons/ExternalLink'
import { Tabbable } from 'components/base/Box'
import { SettingsSectionRow } from './SettingsSection'
@ -10,7 +10,7 @@ export default class AboutRowItem extends PureComponent<{
title: string,
desc: string,
}> {
onClick = () => shell.openExternal(this.props.url)
onClick = () => openURL(this.props.url)
render() {
const { title, desc } = this.props

1
src/components/SettingsPage/CleanButton.js

@ -50,6 +50,7 @@ class CleanButton extends PureComponent<Props, State> {
</Button>
<ConfirmModal
analyticsName="CleanCache"
isDanger
isOpened={opened}
onClose={this.close}

24
src/components/SettingsPage/CounterValueExchangeSelect.js

@ -11,6 +11,7 @@ import {
import { setCounterValueExchange } from 'actions/settings'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import SelectExchange from 'components/SelectExchange'
import Track from 'analytics/Track'
type Props = {
counterValueCurrency: Currency,
@ -25,20 +26,19 @@ class CounterValueExchangeSelect extends PureComponent<Props> {
render() {
const { counterValueCurrency, counterValueExchange } = this.props
return (
return counterValueCurrency ? (
<Fragment>
{counterValueCurrency ? (
<SelectExchange
small
from={intermediaryCurrency}
to={counterValueCurrency}
exchangeId={counterValueExchange}
onChange={this.handleChangeExchange}
minWidth={200}
/>
) : null}
<Track onUpdate event="CounterValueExchangeSelect" exchangeId={counterValueExchange} />
<SelectExchange
small
from={intermediaryCurrency}
to={counterValueCurrency}
exchangeId={counterValueExchange}
onChange={this.handleChangeExchange}
minWidth={200}
/>
</Fragment>
)
) : null
}
}

3
src/components/SettingsPage/CounterValueSelect.js

@ -8,6 +8,7 @@ import type { Currency } from '@ledgerhq/live-common/lib/types'
import { setCounterValue } from 'actions/settings'
import { counterValueCurrencySelector } from 'reducers/settings'
import Select from 'components/base/Select'
import Track from 'analytics/Track'
const fiats = listFiatCurrencies()
.map(f => f.units[0])
@ -35,7 +36,7 @@ class CounterValueSelect extends PureComponent<Props> {
return (
<Fragment>
{/* TODO Track */}
<Track onUpdate event="CounterValueSelect" counterValue={cvOption && cvOption.value} />
<Select
small
minWidth={250}

6
src/components/SettingsPage/LanguageSelect.js

@ -8,6 +8,7 @@ import { connect } from 'react-redux'
import { setLanguage } from 'actions/settings'
import { langAndRegionSelector } from 'reducers/settings'
import languageKeys from 'config/languages'
import Track from 'analytics/Track'
import Select from 'components/base/Select'
type Props = {
@ -37,6 +38,11 @@ class LanguageSelect extends PureComponent<Props> {
: this.languages.find(l => l.value === language)
return (
<Fragment>
<Track
onUpdate
event="LanguageSelect"
currentRegion={currentLanguage && currentLanguage.value}
/>
<Select
small
minWidth={250}

8
src/components/SettingsPage/MarketIndicatorRadio.js

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent } from 'react'
import React, { Fragment, PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { connect } from 'react-redux'
@ -8,6 +8,7 @@ import { createStructuredSelector } from 'reselect'
import { setMarketIndicator } from 'actions/settings'
import { marketIndicatorSelector } from 'reducers/settings'
import RadioGroup from 'components/base/RadioGroup'
import Track from 'analytics/Track'
type Props = {
t: T,
@ -35,7 +36,10 @@ class MarketIndicatorRadio extends PureComponent<Props> {
render() {
const { marketIndicator } = this.props
return (
<RadioGroup items={this.indicators} activeKey={marketIndicator} onChange={this.onChange} />
<Fragment>
<Track onUpdate event="MarketIndicatorRadio" marketIndicator={marketIndicator} />
<RadioGroup items={this.indicators} activeKey={marketIndicator} onChange={this.onChange} />
</Fragment>
)
}
}

2
src/components/SettingsPage/RegionSelect.js

@ -8,6 +8,7 @@ import { langAndRegionSelector, counterValueCurrencySelector } from 'reducers/se
import type { Currency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import Track from 'analytics/Track'
import regionsByKey from 'helpers/regions.json'
import Select from 'components/base/Select'
@ -39,6 +40,7 @@ class RegionSelect extends PureComponent<Props> {
return (
<Fragment>
<Track onUpdate event="RegionSelectChange" currentRegion={currentRegion.region} />
<Select
small
minWidth={250}

6
src/components/SettingsPage/ReleaseNotesButton.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { shell } from 'electron'
import { openURL } from 'helpers/linking'
import { connect } from 'react-redux'
import { openModal } from 'reducers/modals'
import { MODAL_RELEASES_NOTES } from 'config/constants'
@ -19,7 +19,7 @@ const mapDispatchToProps = {
}
class ReleaseNotesButton extends PureComponent<Props> {
handleOpenLink = (url: string) => shell.openExternal(url)
handleOpenLink = (url: string) => openURL(url)
render() {
const { t, openModal } = this.props
@ -32,7 +32,7 @@ class ReleaseNotesButton extends PureComponent<Props> {
openModal(MODAL_RELEASES_NOTES, version)
}}
>
{t('app:settings.about.releaseNotesBtn')}
{t('app:settings.help.releaseNotesBtn')}
</Button>
)
}

1
src/components/SettingsPage/ResetButton.js

@ -49,6 +49,7 @@ class ResetButton extends PureComponent<Props, State> {
</Button>
<ConfirmModal
analyticsName="HardReset"
isDanger
isLoading={pending}
isOpened={opened}

6
src/components/SettingsPage/SettingsSection.js

@ -24,8 +24,8 @@ const RoundIconContainer = styled(Box).attrs({
bg: p => rgba(p.theme.colors.wallet, 0.2),
color: 'wallet',
})`
height: 30px;
width: 30px;
height: 34px;
width: 34px;
border-radius: 50%;
`
@ -106,7 +106,7 @@ export function SettingsSectionRow({
<Box ff="Open Sans|SemiBold" color="dark" fontSize={4}>
{title}
</Box>
<Box ff="Open Sans" fontSize={3} color="grey" mt={1} mr={1}>
<Box ff="Open Sans" fontSize={3} color="grey" mt={1} mr={1} style={{ maxWidth: 520 }}>
{desc}
</Box>
</Box>

6
src/components/SettingsPage/index.js

@ -13,6 +13,7 @@ import Pills from 'components/base/Pills'
import Box from 'components/base/Box'
import SectionDisplay from './sections/Display'
import SectionCurrencies from './sections/Currencies'
import SectionHelp from './sections/Help'
import SectionAbout from './sections/About'
import SectionTools from './sections/Tools'
@ -52,6 +53,11 @@ class SettingsPage extends PureComponent<Props, State> {
label: props.t('app:settings.tabs.about'),
value: SectionAbout,
},
{
key: 'help',
label: props.t('app:settings.tabs.help'),
value: SectionHelp,
},
]
if (EXPERIMENTAL_TOOLS_SETTINGS) {

48
src/components/SettingsPage/sections/About.js

@ -2,14 +2,13 @@
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 IconLoader from 'icons/Loader'
import ExportLogsBtn from 'components/ExportLogsBtn'
import CleanButton from '../CleanButton'
import ResetButton from '../ResetButton'
import ReleaseNotesButton from '../ReleaseNotesButton'
import AboutRowItem from '../AboutRowItem'
@ -24,7 +23,7 @@ type Props = {
t: T,
}
class SectionAbout extends PureComponent<Props> {
class SectionHelp extends PureComponent<Props> {
render() {
const { t } = this.props
const version = __APP_VERSION__
@ -34,45 +33,20 @@ class SectionAbout extends PureComponent<Props> {
<TrackPage category="Settings" name="About" />
<Header
icon={<IconHelp size={16} />}
icon={<IconLoader size={16} />}
title={t('app:settings.tabs.about')}
desc={t('app:settings.about.desc')}
/>
<Body>
<Row title={t('app:settings.about.version')} desc={`Ledger Live ${version}`}>
<Row title={t('app:settings.help.version')} desc={`Ledger Live ${version}`}>
<ReleaseNotesButton />
</Row>
<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.about.faq')}
desc={t('app:settings.about.faqDesc')}
url="https://support.ledgerwallet.com/hc/en-us"
/>
<AboutRowItem
title={t('app:settings.about.terms')}
desc={t('app:settings.about.termsDesc')}
url="https://www.ledgerwallet.com/terms"
title={t('app:settings.help.terms')}
desc={t('app:settings.help.termsDesc')}
url={urls.terms}
/>
</Body>
</Section>
@ -80,4 +54,4 @@ class SectionAbout extends PureComponent<Props> {
}
}
export default translate()(SectionAbout)
export default translate()(SectionHelp)

2
src/components/SettingsPage/sections/Currencies.js

@ -44,7 +44,7 @@ class TabCurrencies extends PureComponent<Props, State> {
const { t, currencies } = this.props
return (
<Section key={currency.id}>
<TrackPage category="Settings" name="Currencies" />
<TrackPage category="Settings" name="Currencies" currencyId={currency.id} />
<Header
icon={<IconCurrencies size={16} />}
title={t('app:settings.tabs.currencies')}

2
src/components/SettingsPage/sections/CurrencyRows.js

@ -12,6 +12,7 @@ import type { SettingsState, CurrencySettings } from 'reducers/settings'
import { currencySettingsDefaults } from 'helpers/SettingsDefaults'
import StepperNumber from 'components/base/StepperNumber'
import ExchangeSelect from 'components/SelectExchange'
import Track from 'analytics/Track'
import { SettingsSectionRow as Row } from '../SettingsSection'
@ -87,6 +88,7 @@ class CurrencyRows extends PureComponent<Props> {
title={t('app:settings.currencies.confirmationsNb')}
desc={t('app:settings.currencies.confirmationsNbDesc')}
>
<Track onUpdate event="ConfirmationsNb" confirmationsNb={confirmationsNb} />
<StepperNumber
min={defaults.confirmationsNb.min}
max={defaults.confirmationsNb.max}

72
src/components/SettingsPage/sections/Help.js

@ -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)

2
src/components/StickyBackToTop.js

@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'
import styled from 'styled-components'
import smoothscroll from 'smoothscroll-polyfill'
import { track } from 'analytics/segment'
import Box from 'components/base/Box'
import AngleUp from 'icons/AngleUp'
import { GrowScrollContext } from './base/GrowScroll'
@ -79,6 +80,7 @@ class StickyBackToTop extends PureComponent<Props, State> {
if (scrollContainer) {
// $FlowFixMe seems to be missing in flow
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' })
track('ScrollBackToTop')
}
}

2
src/components/TopBar/ActivityIndicator.js

@ -9,6 +9,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import type { AsyncState } from 'reducers/bridgeSync'
import { track } from 'analytics/segment'
import { globalSyncStateSelector } from 'reducers/bridgeSync'
import { isUpToDateSelector } from 'reducers/accounts'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
@ -47,6 +48,7 @@ class ActivityIndicatorInner extends PureComponent<Props, { lastClickTime: numbe
this.props.cvPoll()
this.props.setSyncBehavior({ type: 'SYNC_ALL_ACCOUNTS', priority: 5 })
this.setState({ lastClickTime: Date.now() })
track('SyncRefreshClick')
}
render() {

1
src/components/base/AccountsList/index.js

@ -86,6 +86,7 @@ class AccountsList extends Component<
{withToggleAll && (
<FakeLink
ml="auto"
ff="Museo Sans|Regular"
onClick={isAllSelected ? this.onUnselectAll : this.onSelectAll}
fontSize={3}
style={{ lineHeight: '10px' }}

37
src/components/base/Button/index.js

@ -36,7 +36,7 @@ const buttonStyles: { [_: string]: Style } = {
? `
0 0 0 1px ${darken(p.theme.colors.wallet, 0.3)} inset,
0 0 0 1px ${rgba(p.theme.colors.wallet, 0.5)},
0 0 0 4px ${rgba(p.theme.colors.wallet, 0.3)};`
0 0 0 3px ${rgba(p.theme.colors.wallet, 0.3)};`
: ''
}
`,
@ -56,7 +56,7 @@ const buttonStyles: { [_: string]: Style } = {
? `
0 0 0 1px ${darken(p.theme.colors.alertRed, 0.3)} inset,
0 0 0 1px ${rgba(p.theme.colors.alertRed, 0.5)},
0 0 0 4px ${rgba(p.theme.colors.alertRed, 0.3)};
0 0 0 3px ${rgba(p.theme.colors.alertRed, 0.3)};
`
: ''
}
@ -69,15 +69,30 @@ const buttonStyles: { [_: string]: Style } = {
`,
},
outline: {
default: p => `
background: transparent;
border: 1px solid ${
p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet
};
color: ${
p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet
};
`,
default: p => {
const c = p.outlineColor
? p.theme.colors[p.outlineColor] || p.outlineColor
: p.theme.colors.wallet
return `
background: transparent;
border: 1px solid ${c};
color: ${c};
box-shadow: ${
p.isFocused
? `
0 0 0 3px ${rgba(c, 0.3)};`
: ''
}
`
},
hover: p => {
const c = p.outlineColor
? p.theme.colors[p.outlineColor] || p.outlineColor
: p.theme.colors.wallet
return `
background: ${rgba(c, 0.1)};
`
},
active: p => `
color: ${darken(
p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet,

29
src/components/base/LabelWithExternalIcon.js

@ -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;
}
`

156
src/components/base/Markdown/index.js

@ -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>
)
}
}

4
src/components/base/Modal/ConfirmModal.js

@ -5,6 +5,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
@ -23,6 +24,7 @@ type Props = {
onConfirm: Function,
t: T,
isLoading?: boolean,
analyticsName: string,
}
class ConfirmModal extends PureComponent<Props> {
@ -40,6 +42,7 @@ class ConfirmModal extends PureComponent<Props> {
isLoading,
renderIcon,
t,
analyticsName,
...props
} = this.props
@ -52,6 +55,7 @@ class ConfirmModal extends PureComponent<Props> {
{...props}
render={({ onClose }) => (
<ModalBody onClose={isLoading ? undefined : onClose}>
<TrackPage category="Modal" name={analyticsName} />
<ModalTitle>{title}</ModalTitle>
<ModalContent>
{subTitle && (

1
src/components/base/Modal/stories.js

@ -36,6 +36,7 @@ stories.add('Modal', () => (
stories.add('ConfirmModal', () => (
<ConfirmModal
categoryName=""
isOpened
isDanger={boolean('isDanger', false)}
title={text('title', 'Hard reset')}

2
src/components/base/Progress/index.js

@ -71,7 +71,7 @@ type State = {}
class Progress extends Component<Props, State> {
static defaultProps = {
infinite: false,
timing: 3000,
timing: 2500,
color: 'wallet',
}

10
src/components/base/SideBar/SideBarList.js

@ -11,13 +11,13 @@ type Props = {
children: any,
title?: Node | string,
scroll?: boolean,
titleRight?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯
emptyText?: string,
titleRight?: any,
emptyState?: any,
}
class SideBarList extends Component<Props> {
render() {
const { children, title, scroll, titleRight, emptyText, ...props } = this.props
const { children, title, scroll, titleRight, emptyState, ...props } = this.props
const ListWrapper = scroll ? GrowScroll : Box
return (
<Fragment>
@ -34,9 +34,9 @@ class SideBarList extends Component<Props> {
<ListWrapper flow={2} px={3} fontSize={3} {...props}>
{children}
</ListWrapper>
) : emptyText ? (
) : emptyState ? (
<Box px={4} ff="Open Sans|Regular" fontSize={3} color="grey">
{emptyText}
{emptyState}
</Box>
) : null}
</Fragment>

3
src/components/base/Switch/index.js

@ -15,8 +15,9 @@ const Base = styled(Tabbable).attrs({
width: 50px;
height: 26px;
border-radius: 13px;
opacity: ${p => (p.disabled ? 0.3 : 1)};
transition: 250ms linear background-color;
cursor: pointer;
cursor: ${p => (p.disabled ? 'cursor' : 'pointer')};
&:focus {
outline: none;
}

62
src/components/layout/Default.js

@ -1,5 +1,6 @@
// @flow
import { remote } from 'electron'
import React, { Fragment, Component } from 'react'
import { compose } from 'redux'
import styled from 'styled-components'
@ -18,11 +19,13 @@ import DashboardPage from 'components/DashboardPage'
import ManagerPage from 'components/ManagerPage'
import ExchangePage from 'components/ExchangePage'
import SettingsPage from 'components/SettingsPage'
import KeyboardContent from 'components/KeyboardContent'
import PerfIndicator from 'components/PerfIndicator'
import LibcoreBusyIndicator from 'components/LibcoreBusyIndicator'
import DeviceBusyIndicator from 'components/DeviceBusyIndicator'
import TriggerAppReady from 'components/TriggerAppReady'
import ExportLogsBtn from 'components/ExportLogsBtn'
import OnboardingOrElse from 'components/OnboardingOrElse'
import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked'
import SideBar from 'components/MainSideBar'
@ -68,6 +71,8 @@ class Default extends Component<Props> {
kbShortcut = event => {
if (event.ctrlKey && event.key === 'l') {
this.props.i18n.reloadResources()
} else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
remote.getCurrentWindow().webContents.reload()
}
}
@ -80,33 +85,38 @@ class Default extends Component<Props> {
{process.platform === 'darwin' && <AppRegionDrag />}
<ExportLogsBtn hookToShortcut />
<IsUnlocked>
{Object.entries(modals).map(([name, ModalComponent]: [string, any]) => (
<ModalComponent key={name} />
))}
<SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} />
<div id="sticky-back-to-top-root" />
<Box grow horizontal bg="white">
<SideBar />
<Box shrink grow bg="lightGrey" color="grey" overflow="hidden" relative>
<TopBar />
<Main innerRef={n => (this._scrollContainer = n)} tabIndex={-1}>
<Route path="/" exact component={DashboardPage} />
<Route path="/settings" component={SettingsPage} />
<Route path="/manager" component={ManagerPage} />
<Route path="/exchange" component={ExchangePage} />
<Route path="/account/:id" component={AccountPage} />
</Main>
<OnboardingOrElse>
<IsUnlocked>
{Object.entries(modals).map(([name, ModalComponent]: [string, any]) => (
<ModalComponent key={name} />
))}
<SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} />
<div id="sticky-back-to-top-root" />
<Box grow horizontal bg="white">
<SideBar />
<Box shrink grow bg="lightGrey" color="grey" overflow="hidden" relative>
<TopBar />
<Main innerRef={n => (this._scrollContainer = n)} tabIndex={-1}>
<Route path="/" exact component={DashboardPage} />
<Route path="/settings" component={SettingsPage} />
<Route path="/manager" component={ManagerPage} />
<Route path="/exchange" component={ExchangePage} />
<Route path="/account/:id" component={AccountPage} />
</Main>
</Box>
</Box>
</Box>
<LibcoreBusyIndicator />
<DeviceBusyIndicator />
</IsUnlocked>
<LibcoreBusyIndicator />
<DeviceBusyIndicator />
<KeyboardContent sequence="BJBJBJ">
<PerfIndicator />
</KeyboardContent>
</IsUnlocked>
</OnboardingOrElse>
</Fragment>
)
}

47
src/components/layout/Print.js

@ -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

12
src/components/modals/AccountSettingRenderBody.js

@ -17,6 +17,7 @@ import { setDataModal } from 'reducers/modals'
import { getBridgeForCurrency } from 'bridge'
import TrackPage from 'analytics/TrackPage'
import Spoiler from 'components/base/Spoiler'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import Box from 'components/base/Box'
@ -197,6 +198,7 @@ class HelperComp extends PureComponent<Props, State> {
return (
<ModalBody onClose={onClose}>
<form onSubmit={this.handleSubmit(account, onClose)}>
<TrackPage category="Modal" name="AccountSettings" />
<ModalTitle>{t('app:account.settings.title')}</ModalTitle>
<ModalContent mb={3}>
<Container>
@ -275,15 +277,21 @@ class HelperComp extends PureComponent<Props, State> {
</Spoiler>
</ModalContent>
<ModalFooter horizontal>
<Button danger type="button" onClick={this.handleOpenRemoveAccountModal}>
<Button
event="OpenAccountDelete"
danger
type="button"
onClick={this.handleOpenRemoveAccountModal}
>
{t('app:common.delete')}
</Button>
<Button ml="auto" type="submit" primary>
<Button event="DoneEditingAccount" ml="auto" type="submit" primary>
{t('app:common.apply')}
</Button>
</ModalFooter>
</form>
<ConfirmModal
analyticsName="RemoveAccount"
isDanger
isOpened={isRemoveAccountModalOpen}
onClose={this.handleCloseRemoveAccountModal}

1
src/components/modals/AddAccounts/index.js

@ -190,6 +190,7 @@ class AddAccounts extends PureComponent<Props, State> {
handleResetScanState = () => {
this.setState({
isAppOpened: false,
scanStatus: 'idle',
err: null,
scannedAccounts: [],

24
src/components/modals/AddAccounts/steps/03-step-import.js

@ -15,9 +15,9 @@ import CurrencyBadge from 'components/base/CurrencyBadge'
import Button from 'components/base/Button'
import AccountsList from 'components/base/AccountsList'
import IconExclamationCircleThin from 'icons/ExclamationCircleThin'
import TranslatedError from '../../../TranslatedError'
import Spinner from '../../../base/Spinner'
import Text from '../../../base/Text'
import TranslatedError from 'components/TranslatedError'
import Spinner from 'components/base/Spinner'
import Text from 'components/base/Text'
import type { StepProps } from '../index'
@ -70,10 +70,21 @@ class StepImport extends PureComponent<StepProps> {
const { t } = this.props
let { name } = account
const isLegacy = name.indexOf('legacy') !== -1
const isUnsplit = name.indexOf('unsplit') !== -1
if (name === 'New Account') {
name = t('app:addAccounts.newAccount')
} else if (name.indexOf('legacy') !== -1) {
name = t('app:addAccounts.legacyAccount', { accountName: name.replace(' (legacy)', '') })
} else if (isLegacy) {
if (isUnsplit) {
name = t('app:addAccounts.legacyUnsplitAccount', {
accountName: name.replace(' (legacy)', '').replace(' (unsplit)', ''),
})
} else {
name = t('app:addAccounts.legacyAccount', { accountName: name.replace(' (legacy)', '') })
}
} else if (isUnsplit) {
name = t('app:addAccounts.unsplitAccount', { accountName: name.replace(' (unsplit)', '') })
}
return {
@ -281,6 +292,9 @@ class StepImport extends PureComponent<StepProps> {
{scanStatus === 'scanning' ? (
<LoadingRow>
<Spinner color="grey" size={16} />
<Box ml={2} ff="Open Sans|Regular" color="grey" fontSize={4}>
{t('app:common.sync.syncing')}
</Box>
</LoadingRow>
) : null}
</Box>

13
src/components/modals/Debug.js

@ -31,12 +31,16 @@ class Debug extends Component<*, *> {
)
}
onCrash = () => {
onInternalCrash = () => {
testCrash.send().subscribe({
error: this.error,
})
}
onCrashHere = () => {
throw new Error('CrashTest')
}
onClickStressDevice = (device: *) => async () => {
try {
const currency = getCryptoCurrencyById('bitcoin')
@ -142,8 +146,11 @@ class Debug extends Component<*, *> {
</Box>
)}
<Box horizontal style={{ padding: 10 }}>
<Button onClick={this.onCrash} danger>
crash process
<Button mr={2} onClick={this.onInternalCrash} danger>
crash internal
</Button>
<Button onClick={this.onCrashHere} danger>
crash here
</Button>
</Box>
<Box horizontal style={{ padding: 10 }}>

36
src/components/modals/OperationDetails.js

@ -2,13 +2,15 @@
import React, { Fragment, Component } from 'react'
import { connect } from 'react-redux'
import { shell } from 'electron'
import { openURL } from 'helpers/linking'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import moment from 'moment'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation'
import { getAccountOperationExplorer } from '@ledgerhq/live-common/lib/explorers'
import uniq from 'lodash/uniq'
import TrackPage from 'analytics/TrackPage'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import type { T, CurrencySettings } from 'types/common'
@ -44,7 +46,7 @@ const OpDetailsTitle = styled(Box).attrs({
letter-spacing: 2px;
`
const GradientHover = styled(Box).attrs({
export const GradientHover = styled(Box).attrs({
align: 'center',
color: 'wallet',
})`
@ -125,11 +127,13 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
const isConfirmed = confirmations >= currencySettings.confirmationsNb
const url = getAccountOperationExplorer(account, operation)
const uniqueSenders = uniq(senders)
return (
<ModalBody onClose={onClose}>
<TrackPage category="Modal" name="OperationDetails" />
<ModalTitle>{t('app:operationDetails.title')}</ModalTitle>
<ModalContent style={{ height: 500 }} mx={-5} pb={0}>
<ModalContent relative style={{ height: 500 }} px={0} pb={0}>
<GrowScroll px={5} pt={1} pb={8}>
<Box flow={3}>
<Box alignItems="center" mt={1}>
@ -215,12 +219,12 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
<B />
<Box>
<OpDetailsTitle>{t('app:operationDetails.from')}</OpDetailsTitle>
<Recipients recipients={senders} t={t} />
<DataList lines={uniqueSenders} t={t} />
</Box>
<B />
<Box>
<OpDetailsTitle>{t('app:operationDetails.to')}</OpDetailsTitle>
<Recipients recipients={recipients} t={t} />
<DataList lines={recipients} t={t} />
</Box>
</Box>
</GrowScroll>
@ -229,7 +233,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
{url && (
<ModalFooter horizontal justify="flex-end" flow={2}>
<Button primary onClick={() => shell.openExternal(url)}>
<Button primary onClick={() => openURL(url)}>
{t('app:operationDetails.viewOperation')}
</Button>
</ModalFooter>
@ -269,7 +273,7 @@ const More = styled(Text).attrs({
outline: none;
`
export class Recipients extends Component<{ recipients: Array<*>, t: T }, *> {
export class DataList extends Component<{ lines: string[], t: T }, *> {
state = {
showMore: false,
}
@ -277,18 +281,18 @@ export class Recipients extends Component<{ recipients: Array<*>, t: T }, *> {
this.setState(({ showMore }) => ({ showMore: !showMore }))
}
render() {
const { recipients, t } = this.props
const { lines, t } = this.props
const { showMore } = this.state
// Hardcoded for now
const numToShow = 2
const shouldShowMore = recipients.length > 3
const shouldShowMore = lines.length > 3
return (
<Box>
{(shouldShowMore ? recipients.slice(0, numToShow) : recipients).map(recipient => (
<OpDetailsData key={recipient}>
{recipient}
{(shouldShowMore ? lines.slice(0, numToShow) : lines).map(line => (
<OpDetailsData key={line}>
{line}
<GradientHover>
<CopyWithFeedback text={recipient} />
<CopyWithFeedback text={line} />
</GradientHover>
</OpDetailsData>
))}
@ -297,14 +301,12 @@ export class Recipients extends Component<{ recipients: Array<*>, t: T }, *> {
<Box onClick={this.onClick} py={1}>
<More fontSize={4} color="wallet" ff="Open Sans|SemiBold" mt={1}>
<IconChevronRight size={12} style={{ marginRight: 5 }} />
{t('app:operationDetails.showMore', { recipients: recipients.length - numToShow })}
{t('app:operationDetails.showMore', { recipients: lines.length - numToShow })}
</More>
</Box>
)}
{showMore &&
recipients
.slice(numToShow)
.map(recipient => <OpDetailsData key={recipient}>{recipient}</OpDetailsData>)}
lines.slice(numToShow).map(line => <OpDetailsData key={line}>{line}</OpDetailsData>)}
{shouldShowMore &&
showMore && (
<Box onClick={this.onClick} py={1}>

53
src/components/modals/Receive/index.js

@ -12,6 +12,8 @@ import Track from 'analytics/Track'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { MODAL_RECEIVE } from 'config/constants'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import type { T, Device } from 'types/common'
import type { StepProps as DefaultStepProps } from 'components/base/Stepper'
@ -40,7 +42,6 @@ type State = {
isAppOpened: boolean,
isAddressVerified: ?boolean,
disabledSteps: number[],
errorSteps: number[],
verifyAddressError: ?Error,
}
@ -57,6 +58,7 @@ export type StepProps = DefaultStepProps & {
onChangeAccount: (?Account) => void,
onChangeAppOpened: boolean => void,
onChangeAddressVerified: (?boolean, ?Error) => void,
contactUs: () => void,
}
const createSteps = ({ t }: { t: T }) => [
@ -76,15 +78,16 @@ const createSteps = ({ t }: { t: T }) => [
{
id: 'confirm',
label: t('app:receive.steps.confirmAddress.title'),
component: StepConfirmAddress,
footer: StepConfirmAddressFooter,
component: StepConfirmAddress,
onBack: ({ transitionTo }: StepProps) => transitionTo('device'),
shouldRenderFooter: ({ isAddressVerified }: StepProps) => isAddressVerified === false,
shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null,
},
{
id: 'receive',
label: t('app:receive.steps.receiveFunds.title'),
component: StepReceiveFunds,
shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null,
},
]
@ -103,7 +106,6 @@ const INITIAL_STATE = {
isAppOpened: false,
isAddressVerified: null,
disabledSteps: [],
errorSteps: [],
verifyAddressError: null,
}
@ -124,35 +126,40 @@ class ReceiveModal extends PureComponent<Props, State> {
}
}
handleRetry = () => this.setState({ isAddressVerified: null, isAppOpened: false, errorSteps: [] })
handleRetry = () =>
this.setState({
verifyAddressError: null,
isAddressVerified: null,
isAppOpened: false,
})
handleContactUs = () => {
openURL(urls.receiveFlowContactSupport)
}
handleReset = () => this.setState({ ...INITIAL_STATE })
handleCloseModal = () => this.props.closeModal(MODAL_RECEIVE)
handleStepChange = step => this.setState({ stepId: step.id })
handleChangeAccount = (account: ?Account) => this.setState({ account })
handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened })
handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => {
if (isAddressVerified) {
this.setState({ isAddressVerified, verifyAddressError: err })
} else if (isAddressVerified === null) {
this.setState({ isAddressVerified: null, errorSteps: [], verifyAddressError: err })
} else {
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1) {
this.setState({
isAddressVerified,
verifyAddressError: err,
errorSteps: [confirmStepIndex],
})
}
}
this.setState({ isAddressVerified, verifyAddressError: err })
}
handleResetSkip = () => this.setState({ disabledSteps: [] })
handleSkipConfirm = () => {
const connectStepIndex = this.STEPS.findIndex(step => step.id === 'device')
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1 && connectStepIndex > -1) {
this.setState({ disabledSteps: [connectStepIndex, confirmStepIndex] })
this.setState({
isAddressVerified: false,
verifyAddressError: null,
disabledSteps: [connectStepIndex, confirmStepIndex],
})
}
}
@ -164,7 +171,6 @@ class ReceiveModal extends PureComponent<Props, State> {
isAppOpened,
isAddressVerified,
disabledSteps,
errorSteps,
verifyAddressError,
} = this.state
@ -181,8 +187,13 @@ class ReceiveModal extends PureComponent<Props, State> {
onChangeAccount: this.handleChangeAccount,
onChangeAppOpened: this.handleChangeAppOpened,
onChangeAddressVerified: this.handleChangeAddressVerified,
contactUs: this.handleContactUs,
}
const errorSteps = verifyAddressError
? [verifyAddressError.name === 'UserRefusedAddress' ? 2 : 3]
: []
const isModalLocked = stepId === 'confirm' && isAddressVerified === null
return (

2
src/components/modals/Receive/steps/01-step-account.js

@ -13,7 +13,7 @@ import type { StepProps } from '../index'
export default function StepAccount({ t, account, onChangeAccount }: StepProps) {
return (
<Box flow={1}>
<TrackPage category="Receive" name="Step1" />
<TrackPage category="Receive Flow" name="Step 1" />
<Label>{t('app:receive.steps.chooseAccount.label')}</Label>
<SelectAccount autoFocus onChange={onChangeAccount} value={account} />
</Box>

3
src/components/modals/Receive/steps/02-step-connect-device.js

@ -5,6 +5,7 @@ import React from 'react'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import EnsureDeviceApp from 'components/EnsureDeviceApp'
import TrackPage from 'analytics/TrackPage'
import type { StepProps } from '../index'
@ -26,7 +27,9 @@ export function StepConnectDeviceFooter({
}: StepProps) {
return (
<Box horizontal flow={2}>
<TrackPage category="Receive Flow" name="Step 2" />
<Button
event="Receive Flow Without Device Clicked"
onClick={() => {
onSkipConfirm()
transitionTo('receive')

63
src/components/modals/Receive/steps/03-step-confirm-address.js

@ -4,59 +4,24 @@ import invariant from 'invariant'
import styled from 'styled-components'
import React, { Fragment, PureComponent } from 'react'
import getAddress from 'commands/getAddress'
import { isSegwitAccount } from 'helpers/bip32'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import DeviceConfirm from 'components/DeviceConfirm'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import type { StepProps } from '../index'
import TranslatedError from '../../../TranslatedError'
export default class StepConfirmAddress extends PureComponent<StepProps> {
componentDidMount() {
this.confirmAddress()
}
confirmAddress = async () => {
const { account, device, onChangeAddressVerified, transitionTo } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
try {
const params = {
currencyId: account.currency.id,
devicePath: device.path,
path: account.freshAddressPath,
segwit: isSegwitAccount(account),
verify: true,
}
const { address } = await getAddress.send(params).toPromise()
if (address !== account.freshAddress) {
throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, {
accountName: account.name,
})
}
onChangeAddressVerified(true)
transitionTo('receive')
} catch (err) {
onChangeAddressVerified(false, err)
}
}
render() {
const { t, device, account, isAddressVerified, verifyAddressError } = this.props
const { t, device, account, isAddressVerified, verifyAddressError, transitionTo } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
return (
<Container>
<TrackPage category="Receive" name="Step3" />
<TrackPage category="Receive Flow" name="Step 3" />
{isAddressVerified === false ? (
<Fragment>
<TrackPage category="Receive Flow" name="Step 3 Address Not Verified Error" />
<Title>
<TranslatedError error={verifyAddressError} />
</Title>
@ -68,9 +33,13 @@ export default class StepConfirmAddress extends PureComponent<StepProps> {
) : (
<Fragment>
<Title>{t('app:receive.steps.confirmAddress.action')}</Title>
<Text>{t('app:receive.steps.confirmAddress.text')}</Text>
<CurrentAddressForAccount account={account} />
<DeviceConfirm mb={2} mt={-1} error={isAddressVerified === false} />
<Text>
{t('app:receive.steps.confirmAddress.text', { currencyName: account.currency.name })}
</Text>
<Button mt={4} mb={2} primary onClick={() => transitionTo('receive')}>
{t('app:buttons.displayAddressOnDevice')}
</Button>
<DeviceConfirm withoutPushDisplay error={isAddressVerified === false} />
</Fragment>
)}
</Container>
@ -78,14 +47,17 @@ export default class StepConfirmAddress extends PureComponent<StepProps> {
}
}
export function StepConfirmAddressFooter({ t, transitionTo, onRetry }: StepProps) {
export function StepConfirmAddressFooter({ t, transitionTo, onRetry, contactUs }: StepProps) {
// This will be displayed only if user rejected address
return (
<Fragment>
<Button>{t('app:receive.steps.confirmAddress.support')}</Button>
<Button onClick={contactUs} event="Receive Flow Step 3 Contact Us Clicked">
{t('app:receive.steps.confirmAddress.support')}
</Button>
<Button
ml={2}
primary
event="Receive Flow Step 3 Retry Clicked"
onClick={() => {
onRetry()
transitionTo('device')
@ -101,11 +73,12 @@ const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
color: 'dark',
px: 7,
px: 5,
mb: 2,
})``
const Title = styled(Box).attrs({
ff: 'Museo Sans|Regular',
ff: 'Open Sans|SemiBold',
fontSize: 6,
mb: 1,
})``

64
src/components/modals/Receive/steps/04-step-receive-funds.js

@ -4,24 +4,50 @@ import invariant from 'invariant'
import React, { PureComponent } from 'react'
import TrackPage from 'analytics/TrackPage'
import getAddress from 'commands/getAddress'
import { isSegwitAccount } from 'helpers/bip32'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import RequestAmount from 'components/RequestAmount'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import type { StepProps } from '../index'
import type { StepProps } from '..'
type State = {
amount: number,
}
export default class StepReceiveFunds extends PureComponent<StepProps> {
componentDidMount() {
if (this.props.isAddressVerified === null) {
this.confirmAddress()
}
}
confirmAddress = async () => {
const { account, device, onChangeAddressVerified, transitionTo } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
try {
const params = {
currencyId: account.currency.id,
devicePath: device.path,
path: account.freshAddressPath,
segwit: isSegwitAccount(account),
verify: true,
}
const { address } = await getAddress.send(params).toPromise()
export default class StepReceiveFunds extends PureComponent<StepProps, State> {
state = {
amount: 0,
if (address !== account.freshAddress) {
throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, {
accountName: account.name,
})
}
onChangeAddressVerified(true)
transitionTo('receive')
} catch (err) {
onChangeAddressVerified(false, err)
this.props.transitionTo('confirm')
}
}
handleChangeAmount = (amount: number) => this.setState({ amount })
handleGoPrev = () => {
// FIXME this is not a good practice at all. it triggers tons of setState. these are even concurrent setState potentially in future React :o
this.props.onChangeAddressVerified(null)
this.props.onChangeAppOpened(false)
this.props.onResetSkip()
@ -29,30 +55,18 @@ export default class StepReceiveFunds extends PureComponent<StepProps, State> {
}
render() {
const { t, account, isAddressVerified } = this.props
const { amount } = this.state
const { account, isAddressVerified } = this.props
invariant(account, 'No account given')
return (
<Box flow={5}>
<TrackPage category="Receive" name="Step4" />
<Box flow={1}>
<Label>{t('app:receive.steps.receiveFunds.label')}</Label>
<RequestAmount
account={account}
onChange={this.handleChangeAmount}
value={amount}
withMax={false}
/>
</Box>
<TrackPage category="Receive Flow" name="Step 4" />
<CurrentAddressForAccount
account={account}
addressVerified={isAddressVerified === true}
amount={amount}
isAddressVerified={isAddressVerified}
onVerify={this.handleGoPrev}
withBadge
withFooter
withQRCode
withVerify={isAddressVerified !== true}
/>
</Box>
)

126
src/components/modals/ReleaseNotes.js

@ -1,7 +1,6 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
import network from 'api/network'
@ -14,6 +13,8 @@ import GrowScroll from 'components/base/GrowScroll'
import Text from 'components/base/Text'
import Spinner from 'components/base/Spinner'
import GradientBox from 'components/GradientBox'
import TrackPage from 'analytics/TrackPage'
import Markdow, { Notes } from 'components/base/Markdown'
import type { T } from 'types/common'
@ -25,124 +26,6 @@ type State = {
markdown: ?string,
}
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;
}
`
const Title = styled(Text).attrs({
ff: 'Museo Sans',
fontSize: 5,
@ -213,15 +96,16 @@ class ReleaseNotes extends PureComponent<Props, State> {
content = (
<Notes>
<Title>{t('app:releaseNotes.version', { versionNb: version })}</Title>
<ReactMarkdown>{markdown}</ReactMarkdown>
<Markdow>{markdown}</Markdow>
</Notes>
)
}
return (
<ModalBody onClose={onClose}>
<TrackPage category="Modal" name="ReleaseNotes" />
<ModalTitle>{t('app:releaseNotes.title')}</ModalTitle>
<ModalContent style={{ height: 500 }} mx={-5} pb={0}>
<ModalContent relative style={{ height: 500 }} px={0} pb={0}>
<GrowScroll px={5} pb={8}>
{content}
</GrowScroll>

18
src/components/modals/Send/fields/RecipientField.js

@ -3,10 +3,12 @@ import React, { Component } from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import type { WalletBridge } from 'bridge/types'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import LabelInfoTooltip from 'components/base/LabelInfoTooltip'
import LabelWithExternalIcon from 'components/base/LabelWithExternalIcon'
import RecipientAddress from 'components/RecipientAddress'
import { track } from 'analytics/segment'
type Props<Transaction> = {
t: T,
@ -60,16 +62,20 @@ class RecipientField<Transaction> extends Component<Props<Transaction>, { isVali
return true
}
handleRecipientAddressHelp = () => {
openURL(urls.recipientAddressInfo)
track('Send Flow Recipient Address Help Requested')
}
render() {
const { bridge, account, transaction, t, autoFocus } = this.props
const { isValid } = this.state
const value = bridge.getTransactionRecipient(account, transaction)
return (
<Box flow={1}>
<Label>
<span>{t('app:send.steps.amount.recipientAddress')}</span>
<LabelInfoTooltip ml={1} text={t('app:send.steps.amount.recipientAddress')} />
</Label>
<LabelWithExternalIcon
onClick={this.handleRecipientAddressHelp}
label={t('app:send.steps.amount.recipientAddress')}
/>
<RecipientAddress
autoFocus={autoFocus}
withQrCode

2
src/components/modals/Send/steps/01-step-amount.js

@ -10,6 +10,7 @@ import FormattedVal from 'components/base/FormattedVal'
import Text from 'components/base/Text'
import CounterValue from 'components/CounterValue'
import Spinner from 'components/base/Spinner'
import TrackPage from 'analytics/TrackPage'
import RecipientField from '../fields/RecipientField'
import AmountField from '../fields/AmountField'
@ -34,6 +35,7 @@ export default ({
return (
<Box flow={4}>
<TrackPage category="Send Flow" name="Step 1" />
<Box flow={1}>
<Label>{t('app:send.steps.amount.selectAccountDebit')}</Label>
<SelectAccount autoFocus={!openedFromAccount} onChange={onChangeAccount} value={account} />

2
src/components/modals/Send/steps/02-step-connect-device.js

@ -11,7 +11,7 @@ import type { StepProps } from '../index'
export default function StepConnectDevice({ account, onChangeAppOpened }: StepProps<*>) {
return (
<Fragment>
<TrackPage category="Send" name="Step2" />
<TrackPage category="Send Flow" name="Step 2" />
<EnsureDeviceApp
account={account}
waitBeforeSuccess={200}

2
src/components/modals/Send/steps/03-step-verification.js

@ -33,7 +33,7 @@ export default class StepVerification extends PureComponent<StepProps<*>> {
const { t } = this.props
return (
<Container>
<TrackPage category="Send" name="Step3" />
<TrackPage category="Send Flow" name="Step 3" />
<WarnBox>{multiline(t('app:send.steps.verification.warning'))}</WarnBox>
<Info>{t('app:send.steps.verification.body')}</Info>
<DeviceConfirm />

5
src/components/modals/Send/steps/04-step-confirmation.js

@ -60,7 +60,7 @@ export default function StepConfirmation({ t, optimisticOperation, error }: Step
const translatedErrDesc = error ? <TranslatedError error={error} field="description" /> || '' : ''
return (
<Container>
<TrackPage category="Send" name="Step4" />
<TrackPage category="Send Flow" name="Step 4" />
<span style={{ color: iconColor }}>
<Icon size={43} />
</span>
@ -86,12 +86,11 @@ export function StepConfirmationFooter({
optimisticOperation && account && getAccountOperationExplorer(account, optimisticOperation)
return (
<Fragment>
<Button onClick={closeModal}>{t('app:common.close')}</Button>
{optimisticOperation ? (
// TODO: actually go to operations details
url ? (
<Button
ml={2}
event="Send Flow Step 4 View OpD Clicked"
onClick={() => {
closeModal()
if (account && optimisticOperation) {

99
src/components/modals/ShareAnalytics.js

@ -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',
})``

2
src/components/modals/StepConnectDevice.js

@ -20,7 +20,7 @@ const StepConnectDevice = ({ account, currency, onChangeDevice, onStatusChange }
<EnsureDeviceApp
account={account}
currency={currency}
waitBeforeSuccess={500}
waitBeforeSuccess={200}
onSuccess={({ device }) => {
// TODO: remove those non-nense callbacks
if (onChangeDevice) {

67
src/components/modals/TechnicalData.js

@ -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)

11
src/components/modals/UpdateFirmware/Disclaimer.js

@ -3,7 +3,6 @@
import React, { PureComponent, Fragment } from 'react'
import { translate, Trans } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import type { T } from 'types/common'
@ -12,7 +11,8 @@ import Text from 'components/base/Text'
import Button from 'components/base/Button'
import GrowScroll from 'components/base/GrowScroll'
import GradientBox from 'components/GradientBox'
import { Notes } from 'components/modals/ReleaseNotes'
import Markdown, { Notes } from 'components/base/Markdown'
import TrackPage from 'analytics/TrackPage'
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
@ -42,6 +42,7 @@ class DisclaimerModal extends PureComponent<Props, State> {
onClose={onClose}
render={({ onClose }) => (
<ModalBody onClose={onClose} grow align="center" justify="center" mt={3}>
<TrackPage category="Manager" name="DisclaimerModal" />
<Fragment>
<ModalTitle>{t('app:manager.firmware.update')}</ModalTitle>
<ModalContent>
@ -58,10 +59,10 @@ class DisclaimerModal extends PureComponent<Props, State> {
{t('app:manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<ModalContent style={{ height: 250, width: '100%' }}>
<GrowScroll>
<ModalContent relative pb={0} style={{ height: 250, width: '100%' }}>
<GrowScroll pb={5}>
<Notes>
<ReactMarkdown>{firmware.notes}</ReactMarkdown>
<Markdown>{firmware.notes}</Markdown>
</Notes>
</GrowScroll>
<GradientBox />

30
src/components/modals/UpdateFirmware/Installing.js

@ -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)

64
src/components/modals/UpdateFirmware/index.js

@ -12,23 +12,12 @@ import type { StepProps as DefaultStepProps, Step } from 'components/base/Steppe
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import type { LedgerScriptParams } from 'helpers/common'
import { FreezeDeviceChangeEvents } from '../../ManagerPage/HookDeviceChange'
import StepFullFirmwareInstall from './steps/01-step-install-full-firmware'
import StepFlashMcu from './steps/02-step-flash-mcu'
import StepConfirmation, { StepConfirmFooter } from './steps/03-step-confirmation'
export type Firmware = LedgerScriptParams & { shouldUpdateMcu: boolean }
export type StepProps = DefaultStepProps & {
firmware: Firmware,
onCloseModal: () => void,
installOsuFirmware: (device: Device) => void,
installFinalFirmware: (device: Device) => void,
flashMCU: (device: Device) => void,
}
type StepId = 'idCheck' | 'updateMCU' | 'finish'
const createSteps = ({ t, firmware }: { t: T, firmware: Firmware }): Array<*> => {
const createSteps = ({ t, shouldFlashMcu }: { t: T, shouldFlashMcu: boolean }): Array<*> => {
const updateStep = {
id: 'idCheck',
label: t('app:manager.modal.steps.idCheck'),
@ -58,7 +47,7 @@ const createSteps = ({ t, firmware }: { t: T, firmware: Firmware }): Array<*> =>
const steps = [updateStep]
if (firmware.shouldUpdateMcu) {
if (shouldFlashMcu) {
steps.push(mcuStep)
}
@ -67,11 +56,27 @@ const createSteps = ({ t, firmware }: { t: T, firmware: Firmware }): Array<*> =>
return steps
}
export type Firmware = LedgerScriptParams & { shouldFlashMcu: boolean }
export type StepProps = DefaultStepProps & {
firmware: Firmware,
onCloseModal: () => void,
installOsuFirmware: (device: Device) => void,
installFinalFirmware: (device: Device) => void,
flashMCU: (device: Device) => void,
shouldFlashMcu: boolean,
error: ?Error,
setError: Error => void,
}
export type StepId = 'idCheck' | 'updateMCU' | 'finish'
type Props = {
t: T,
status: ModalStatus,
onClose: () => void,
firmware: Firmware,
shouldFlashMcu: boolean,
installOsuFirmware: (device: Device) => void,
installFinalFirmware: (device: Device) => void,
flashMCU: (device: Device) => void,
@ -80,44 +85,59 @@ type Props = {
type State = {
stepId: StepId | string,
error: ?Error,
nonce: number,
}
class UpdateModal extends PureComponent<Props, State> {
static defaultProps = {
stepId: 'idCheck',
}
state = {
stepId: this.props.stepId,
error: null,
nonce: 0,
}
STEPS = createSteps({ t: this.props.t, firmware: this.props.firmware })
STEPS = createSteps({
t: this.props.t,
shouldFlashMcu: this.props.firmware
? this.props.firmware.shouldFlashMcu
: this.props.shouldFlashMcu,
})
setError = (e: Error) => this.setState({ error: e })
handleReset = () => this.setState({ stepId: 'idCheck', error: null, nonce: this.state.nonce++ })
handleStepChange = (step: Step) => this.setState({ stepId: step.id })
render(): React$Node {
const { status, t, firmware, onClose, ...props } = this.props
const { stepId } = this.state
const { stepId, error, nonce } = this.state
const additionalProps = {
firmware,
error,
onCloseModal: onClose,
setError: this.setError,
...props,
}
return (
<Modal
onClose={onClose}
onHide={this.handleReset}
isOpened={status === 'install'}
refocusWhenChange={stepId}
preventBackdropClick={false}
preventBackdropClick={stepId !== 'finish' && !error}
render={() => (
<Stepper
key={nonce}
onStepChange={this.handleStepChange}
title={t('app:manager.firmware.update')}
initialStepId="idCheck"
initialStepId={stepId}
steps={this.STEPS}
{...additionalProps}
>
<FreezeDeviceChangeEvents />
<SyncSkipUnderPriority priority={100} />
</Stepper>
)}

101
src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js

@ -9,16 +9,18 @@ import { DEVICE_INFOS_TIMEOUT } from 'config/constants'
import getDeviceInfo from 'commands/getDeviceInfo'
import { getCurrentDevice } from 'reducers/devices'
import { createCancelablePolling } from 'helpers/promise'
import { createCancelablePolling, delay } from 'helpers/promise'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import Progress from 'components/base/Progress'
import DeviceConfirm from 'components/DeviceConfirm'
import type { Device } from 'types/common'
import type { StepProps } from '../'
import Installing from '../Installing'
const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
@ -46,13 +48,7 @@ const Address = styled(Box).attrs({
cursor: text;
user-select: text;
width: 325px;
`
const Ellipsis = styled.span`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
text-align: center;
`
type Props = StepProps & {
@ -94,55 +90,75 @@ class StepFullFirmwareInstall extends PureComponent<Props, State> {
}
install = async () => {
const { installOsuFirmware, installFinalFirmware } = this.props
const {
installOsuFirmware,
installFinalFirmware,
firmware,
shouldFlashMcu,
transitionTo,
setError,
} = this.props
const { device, deviceInfo } = await this.ensureDevice()
if (deviceInfo.isOSU) {
this.setState({ installing: true })
const finalSuccess = await installFinalFirmware(device)
if (finalSuccess) this.transitionTo()
if (deviceInfo.isBootloader) {
transitionTo('updateMCU')
}
const success = await installOsuFirmware(device)
if (success) {
this.setState({ installing: true })
if (this._unsubConnect) this._unsubConnect()
const { device: cleanDevice } = await this.ensureDevice()
const finalSuccess = await installFinalFirmware(cleanDevice)
if (finalSuccess) {
this.transitionTo()
try {
if (deviceInfo.isOSU) {
this.setState({ installing: true })
await installFinalFirmware(device)
transitionTo('finish')
} else {
await installOsuFirmware(device)
this.setState({ installing: true })
if (this._unsubConnect) this._unsubConnect()
if ((firmware && firmware.shouldFlashMcu) || shouldFlashMcu) {
delay(1000)
transitionTo('updateMCU')
} else {
const { device: freshDevice } = await this.ensureDevice()
await installFinalFirmware(freshDevice)
transitionTo('finish')
}
}
} catch (error) {
setError(error)
transitionTo('finish')
}
}
transitionTo = () => {
const { firmware, transitionTo } = this.props
if (firmware.shouldUpdateMcu) {
transitionTo('updateMCU')
} else {
transitionTo('finish')
formatHashName = (hash: string): string[] => {
if (!hash) {
return []
}
const length = hash.length
const half = Math.ceil(length / 2)
const start = hash.slice(0, half)
const end = hash.slice(half)
return [start, end]
}
renderBody = () => {
const { installing } = this.state
const { firmware, t } = this.props
if (installing) {
return (
<Box mx={7}>
<Progress infinite style={{ width: '100%' }} />
</Box>
)
}
const { t, firmware } = this.props
return (
return installing ? (
<Installing />
) : (
<Fragment>
<Text ff="Open Sans|Regular" align="center" color="smoke">
{t('app:manager.modal.confirmIdentifierText')}
</Text>
<Box mx={7} mt={5}>
<Text ff="Open Sans|SemiBold" align="center" color="smoke">
{t('app:manager.modal.identifier')}
</Text>
<Address>
<Ellipsis>{firmware && firmware.hash}</Ellipsis>
{firmware &&
firmware.hash &&
this.formatHashName(firmware.hash.toUpperCase()).join('\n')}
</Address>
</Box>
<Box mt={5}>
@ -155,13 +171,12 @@ class StepFullFirmwareInstall extends PureComponent<Props, State> {
_unsubConnect: *
render() {
const { installing } = this.state
const { t } = this.props
return (
<Container>
<Title>{t('app:manager.modal.confirmIdentifier')}</Title>
<Text ff="Open Sans|Regular" align="center" color="smoke">
{t('app:manager.modal.confirmIdentifierText')}
</Text>
<Title>{installing ? '' : t('app:manager.modal.confirmIdentifier')}</Title>
<TrackPage category="Manager" name="InstallFirmware" />
{this.renderBody()}
</Container>
)

60
src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js

@ -11,14 +11,16 @@ import { getCurrentDevice } from 'reducers/devices'
import { createCancelablePolling } from 'helpers/promise'
import getDeviceInfo from 'commands/getDeviceInfo'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import Progress from 'components/base/Progress'
import type { Device } from 'types/common'
import type { StepProps } from '../'
import Installing from '../Installing'
const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
@ -68,7 +70,7 @@ class StepFlashMcu extends PureComponent<Props, State> {
if (this._unsubDeviceInfo) this._unsubDeviceInfo()
}
waitForDeviceInBootloader = () => {
getDeviceInfo = () => {
const { unsubscribe, promise } = createCancelablePolling(async () => {
const { device } = this.props
if (!device) {
@ -78,16 +80,14 @@ class StepFlashMcu extends PureComponent<Props, State> {
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise()
if (!deviceInfo.isBootloader) {
throw new Error('Device is not in bootloader')
}
return { device, deviceInfo }
})
this._unsubConnect = unsubscribe
this._unsubDeviceInfo = unsubscribe
return promise
}
getDeviceInfo = () => {
waitForDeviceInBootloader = () => {
const { unsubscribe, promise } = createCancelablePolling(async () => {
const { device } = this.props
if (!device) {
@ -97,9 +97,13 @@ class StepFlashMcu extends PureComponent<Props, State> {
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise()
if (!deviceInfo.isBootloader) {
throw new Error('Device is not in bootloader')
}
return { device, deviceInfo }
})
this._unsubDeviceInfo = unsubscribe
this._unsubConnect = unsubscribe
return promise
}
@ -113,29 +117,35 @@ class StepFlashMcu extends PureComponent<Props, State> {
}
install = async () => {
const { transitionTo } = this.props
this.flash()
const deviceInfo = await this.getDeviceInfo()
if (deviceInfo.isBootloader) {
this.flash()
} else {
const { transitionTo, installFinalFirmware, setError } = this.props
const { deviceInfo, device } = await this.getDeviceInfo()
try {
if (deviceInfo.isBootloader) {
await this.flash()
this.install()
} else if (deviceInfo.isOSU) {
await installFinalFirmware(device)
transitionTo('finish')
}
} catch (error) {
setError(error)
transitionTo('finish')
}
}
firstFlash = async () => {
await this.flash()
this.install()
}
renderBody = () => {
const { installing } = this.state
const { t } = this.props
if (installing) {
return (
<Box mx={7}>
<Progress infinite style={{ width: '100%' }} />
</Box>
)
}
return (
return installing ? (
<Installing />
) : (
<Fragment>
<Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke">
@ -169,9 +179,11 @@ class StepFlashMcu extends PureComponent<Props, State> {
render() {
const { t } = this.props
const { installing } = this.state
return (
<Container>
<Title>{t('app:manager.modal.mcuTitle')}</Title>
<Title>{installing ? '' : t('app:manager.modal.mcuTitle')}</Title>
<TrackPage category="Manager" name="FlashMCU" />
{this.renderBody()}
</Container>
)

38
src/components/modals/UpdateFirmware/steps/03-step-confirmation.js

@ -3,10 +3,13 @@
import React from 'react'
import styled from 'styled-components'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError'
import CheckCircle from 'icons/CheckCircle'
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import type { StepProps } from '../'
@ -24,14 +27,45 @@ const Title = styled(Box).attrs({
font-weight: 500;
`
function StepConfirmation({ t }: StepProps) {
function StepConfirmation({ t, error }: StepProps) {
if (error) {
return (
<Container>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
</Box>
<Box
color="dark"
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>
</Container>
)
}
return (
<Container>
<TrackPage category="Manager" name="FirmwareConfirmation" />
<Box mx={7} color="positiveGreen" my={4}>
<CheckCircle size={44} />
</Box>
<Title>{t('app:manager.modal.successTitle')}</Title>
<Box mt={2} mb={8}>
<Box mt={2} mb={5}>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite">
{t('app:manager.modal.successText')}
</Text>

4
src/config/constants.js

@ -88,7 +88,7 @@ export const EXPERIMENTAL_MARKET_INDICATOR_SETTINGS = boolFromEnv(
// Other constants
export const MAX_ACCOUNT_NAME_SIZE = 30
export const MAX_ACCOUNT_NAME_SIZE = 50
export const MODAL_ADD_ACCOUNTS = 'MODAL_ADD_ACCOUNTS'
export const MODAL_OPERATION_DETAILS = 'MODAL_OPERATION_DETAILS'
@ -96,6 +96,8 @@ export const MODAL_RECEIVE = 'MODAL_RECEIVE'
export const MODAL_SEND = 'MODAL_SEND'
export const MODAL_SETTINGS_ACCOUNT = 'MODAL_SETTINGS_ACCOUNT'
export const MODAL_RELEASES_NOTES = 'MODAL_RELEASES_NOTES'
export const MODAL_SHARE_ANALYTICS = 'MODAL_SHARE_ANALYTICS'
export const MODAL_TECHNICAL_DATA = 'MODAL_TECHNICAL_DATA'
export const MODAL_DISCLAIMER = 'MODAL_DISCLAIMER'
export const MODAL_DISCLAIMER_DELAY = 1 * 1000

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save