Browse Source

Merge branch 'develop' into LL-931

develop
meriadec 6 years ago
parent
commit
4abf811706
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 15
      package.json
  2. 15
      scripts/check-no-dups.sh
  3. 5
      src/commands/autoUpdate.js
  4. 3
      src/components/CurrentAddress/index.js
  5. 13
      src/components/GenuineCheckModal.js
  6. 4
      src/components/IsUnlocked.js
  7. 93
      src/components/ManagerPage/AppsList.js
  8. 5
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js
  9. 3
      src/components/SettingsPage/CleanButton.js
  10. 29
      src/components/SettingsPage/DisablePasswordModal.js
  11. 8
      src/components/SettingsPage/PasswordAutoLockSelect.js
  12. 1
      src/components/SettingsPage/PasswordForm.js
  13. 39
      src/components/SettingsPage/PasswordModal.js
  14. 14
      src/components/SettingsPage/RepairDeviceButton.js
  15. 3
      src/components/SettingsPage/ResetButton.js
  16. 2
      src/components/SettingsPage/ResetFallbackModal.js
  17. 4
      src/components/SettingsPage/sections/Display.js
  18. 35
      src/components/SettingsPage/sections/Export.js
  19. 51
      src/components/base/Modal/ConfirmModal.js
  20. 137
      src/components/base/Modal/ModalBody.js
  21. 59
      src/components/base/Modal/ModalContent.js
  22. 18
      src/components/base/Modal/ModalFooter.js
  23. 93
      src/components/base/Modal/ModalHeader.js
  24. 75
      src/components/base/Modal/ModalTitle.js
  25. 47
      src/components/base/Modal/RepairModal.js
  26. 337
      src/components/base/Modal/index.js
  27. 71
      src/components/base/Modal/stories.js
  28. 26
      src/components/base/Stepper/index.js
  29. 82
      src/components/modals/AccountSettingRenderBody.js
  30. 2
      src/components/modals/AddAccounts/index.js
  31. 26
      src/components/modals/Debug.js
  32. 43
      src/components/modals/Disclaimer.js
  33. 31
      src/components/modals/OperationDetails.js
  34. 5
      src/components/modals/Receive/index.js
  35. 26
      src/components/modals/ReleaseNotes/ReleaseNotesBody.js
  36. 1
      src/components/modals/ReleaseNotes/index.js
  37. 1
      src/components/modals/Send/index.js
  38. 5
      src/components/modals/SettingsAccount.js
  39. 54
      src/components/modals/ShareAnalytics.js
  40. 51
      src/components/modals/TechnicalData.js
  41. 38
      src/components/modals/UpdateFirmware/Disclaimer.js
  42. 1
      src/components/modals/UpdateFirmware/index.js
  43. 1
      src/index.ejs
  44. 1
      src/main/app.js
  45. 1
      src/reducers/modals.js
  46. 8
      static/i18n/en/app.json
  47. 82
      yarn.lock

15
package.json

@ -20,7 +20,7 @@
"test-e2e": "jest test-e2e",
"test-sync": "bash test-e2e/sync/launch.sh",
"prettier": "prettier --write \"{src,webpack,.storybook,test-e2e}/**/*.{js,json}\"",
"ci": "yarn lint && yarn flow && yarn prettier && yarn test",
"ci": "yarn check --integrity && ./scripts/check-no-dups.sh && yarn lint && yarn flow && yarn prettier && yarn test",
"storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444",
"publish-storybook": "bash ./scripts/legacy/publish-storybook.sh",
"reset-files": "bash ./scripts/legacy/reset-files.sh"
@ -36,11 +36,11 @@
},
"dependencies": {
"@ledgerhq/errors": "^4.35.1",
"@ledgerhq/hw-app-btc": "^4.34.0",
"@ledgerhq/hw-app-eth": "^4.32.0",
"@ledgerhq/hw-app-xrp": "^4.32.0",
"@ledgerhq/hw-transport": "^4.32.0",
"@ledgerhq/hw-transport-node-hid": "^4.32.0",
"@ledgerhq/hw-app-btc": "^4.35.0",
"@ledgerhq/hw-app-eth": "^4.35.0",
"@ledgerhq/hw-app-xrp": "^4.35.0",
"@ledgerhq/hw-transport": "^4.35.0",
"@ledgerhq/hw-transport-node-hid": "^4.35.0",
"@ledgerhq/ledger-core": "2.0.0-rc.16",
"@ledgerhq/live-common": "4.15.0-beta.0",
"animated": "^0.2.2",
@ -185,7 +185,8 @@
"webpack": "^4.6.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-cli": "^2.0.14",
"yaml-loader": "^0.5.0"
"yaml-loader": "^0.5.0",
"yarn-deduplicate": "^1.1.1"
},
"engines": {
"node": ">=8.9.0 <=8.15.0",

15
scripts/check-no-dups.sh

@ -0,0 +1,15 @@
#!/bin/bash
yarn-deduplicate -l | grep \@ledgerhq
if [ $? -eq 0 ]; then
echo "Found duplicates in @ledgerhq/* – fix it with yarn-deduplicate"
exit 1
fi
yarn-deduplicate -l | grep \"react
if [ $? -eq 0 ]; then
echo "Found duplicates in some react packages – fix it with yarn-deduplicate"
exit 1
fi

5
src/commands/autoUpdate.js

@ -3,8 +3,9 @@
import { createCommand, Command } from 'helpers/ipc'
import { Observable } from 'rxjs'
import { UPDATE_CHECK_IGNORE, UPDATE_CHECK_FEED } from 'config/constants'
import createElectronAppUpdater from 'main/updater/createElectronAppUpdater'
// import { UPDATE_CHECK_IGNORE, UPDATE_CHECK_FEED } from 'config/constants'
import { UPDATE_CHECK_IGNORE } from 'config/constants'
// import createElectronAppUpdater from 'main/updater/createElectronAppUpdater'
import type { UpdateStatus } from 'components/Updater/UpdaterContext'
type Input = {}

3
src/components/CurrentAddress/index.js

@ -145,8 +145,11 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
componentWillUnmount() {
if (this._timeout) clearTimeout(this._timeout)
this._isUnmounted = true
}
_isUnmounted = false
renderCopy = copy => {
const { t } = this.props
return (

13
src/components/GenuineCheckModal.js

@ -5,7 +5,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal'
import Modal, { ModalBody } from 'components/base/Modal'
import GenuineCheck from 'components/GenuineCheck'
type Props = {
@ -19,12 +19,13 @@ class GenuineCheckModal extends PureComponent<Props> {
renderBody = ({ onClose }) => {
const { t, onSuccess, onFail, onUnavailable } = this.props
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('genuinecheck.modal.title')}</ModalTitle>
<ModalContent>
<ModalBody
onClose={onClose}
title={t('genuinecheck.modal.title')}
render={() => (
<GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} />
</ModalContent>
</ModalBody>
)}
/>
)
}

4
src/components/IsUnlocked.js

@ -23,8 +23,8 @@ import Box from 'components/base/Box'
import InputPassword from 'components/base/InputPassword'
import LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import IconArrowRight from 'icons/ArrowRight'
import Button from './base/Button/index'
import ConfirmModal from './base/Modal/ConfirmModal'
import Button from 'components/base/Button/index'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
type InputValue = {
password: string,

93
src/components/ManagerPage/AppsList.js

@ -1,7 +1,7 @@
// @flow
/* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent, Fragment } from 'react'
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
@ -15,7 +15,7 @@ import { developerModeSelector } from 'reducers/settings'
import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Modal from 'components/base/Modal'
import Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text'
import ProgressBar from 'components/ProgressBar'
@ -31,6 +31,7 @@ import CheckCircle from 'icons/CheckCircle'
import { FreezeDeviceChangeEvents } from './HookDeviceChange'
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
import AppSearchBar from './AppSearchBar'
import ModalBody from '../base/Modal/ModalBody'
const mapStateToProps = state => ({
isDevMode: developerModeSelector(state),
@ -183,46 +184,32 @@ class AppsList extends PureComponent<Props, State> {
handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' })
renderModal = () => {
renderBody = () => {
const { t } = this.props
const { app, status, error, mode, progress } = this.state
return (
<Modal
isOpened={status !== 'idle' && status !== 'loading'}
render={() => (
<ModalBody align="center" justify="center" style={{ height: 300 }}>
<FreezeDeviceChangeEvents />
{status === 'busy' || status === 'idle' ? (
<Fragment>
<ModalTitle>
return ['busy', 'idle'].includes(status) ? (
<Box grow align="center" justify="center">
{mode === 'installing' ? (
<Box color="grey">
<Box color="grey" grow align="center" mb={5}>
<Update size={30} />
</Box>
) : (
<Box color="grey">
<Box color="grey" grow align="center" mb={5}>
<Trash size={30} />
</Box>
)}
</ModalTitle>
<ModalContent>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`manager.apps.${mode}`, { app })}
</Text>
<Box mt={6}>
<ProgressBar width={150} progress={progress} />
</Box>
</ModalContent>
</Fragment>
</Box>
) : status === 'error' ? (
<Fragment>
<TrackPage
category="Manager"
name="Error Modal"
error={error && error.name}
app={app}
/>
<ModalContent grow align="center" justify="center" mt={5}>
<Box>
<TrackPage category="Manager" name="Error Modal" error={error && error.name} app={app} />
<Box grow align="center" justify="center" mt={5}>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
</Box>
@ -246,16 +233,10 @@ class AppsList extends PureComponent<Props, State> {
>
<TranslatedError error={error} field="description" />
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
</Box>
</Box>
) : status === 'success' ? (
<Fragment>
<ModalContent grow align="center" justify="center" mt={5}>
<Box grow align="center" justify="center" mt={5}>
<Box color="positiveGreen">
<CheckCircle size={44} />
</Box>
@ -267,24 +248,39 @@ class AppsList extends PureComponent<Props, State> {
textAlign="center"
style={{ maxWidth: 350 }}
>
{t(
`manager.apps.${
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
}`,
{ app },
)}
{t(`manager.apps.${mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'}`, {
app,
})}
</Box>
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
) : null
}
renderFooter = () => {
const { t } = this.props
return (
<Box horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
) : null}
</Box>
)
}
renderModal = () => {
const { status } = this.state
return (
<Modal isOpened={status !== 'idle' && status !== 'loading'} centered>
<ModalBody
align="center"
justify="center"
title={''}
render={this.renderBody}
renderFooter={['error', 'success'].includes(status) ? this.renderFooter : undefined}
>
<FreezeDeviceChangeEvents />
</ModalBody>
)}
/>
</Modal>
)
}
@ -325,6 +321,7 @@ class AppsList extends PureComponent<Props, State> {
render() {
const { t } = this.props
return (
<Box>
<Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center">

5
src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js

@ -31,6 +31,8 @@ export function GenuineCheckUnavailableFooter({
</Button>
<Box horizontal ml="auto">
<Button
outline
outlineColor="alertRed"
disabled={false}
event="Onboarding Skip Genuine Check"
onClick={() => nextStep()}
@ -38,9 +40,6 @@ export function GenuineCheckUnavailableFooter({
>
{t('common.skipThisStep')}
</Button>
<Button onClick={nextStep} disabled primary>
{t('common.continue')}
</Button>
</Box>
</OnboardingFooterWrapper>
)

3
src/components/SettingsPage/CleanButton.js

@ -7,7 +7,7 @@ import logger from 'logger'
import type { T } from 'types/common'
import { cleanAccountsCache } from 'actions/accounts'
import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
import { softReset } from 'helpers/reset'
import ResetFallbackModal from './ResetFallbackModal'
@ -60,6 +60,7 @@ class CleanButton extends PureComponent<Props, State> {
<ConfirmModal
analyticsName="CleanCache"
centered
isOpened={opened}
onClose={this.close}
onReject={this.close}

29
src/components/SettingsPage/DisablePasswordModal.js

@ -8,7 +8,8 @@ import Box from 'components/base/Box'
import Button from 'components/base/Button'
import InputPassword from 'components/base/InputPassword'
import Label from 'components/base/Label'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal'
import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import type { T } from 'types/common'
@ -61,17 +62,12 @@ class DisablePasswordModal extends PureComponent<Props, State> {
const { t, onClose, ...props } = this.props
const { currentPassword, incorrectPassword } = this.state
return (
<Modal
{...props}
onHide={this.handleReset}
onClose={onClose}
render={({ onClose }) => (
<Modal {...props} centered onHide={this.handleReset} onClose={onClose}>
<form onSubmit={this.disablePassword}>
<ModalBody onClose={onClose}>
<ModalTitle data-e2e="disablePassword_modalTitle">
{t('password.disablePassword.title')}
</ModalTitle>
<ModalContent>
<ModalBody
onClose={onClose}
title={t('password.disablePassword.title')}
render={() => (
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" px={4}>
{t('password.disablePassword.desc')}
<Box px={7} mt={4} flow={3}>
@ -90,8 +86,9 @@ class DisablePasswordModal extends PureComponent<Props, State> {
</Box>
</Box>
</Box>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
)}
renderFooter={() => (
<Box horizontal align="center" justify="flex-end" flow={2}>
<Button small type="button" onClick={onClose}>
{t('common.cancel')}
</Button>
@ -103,11 +100,11 @@ class DisablePasswordModal extends PureComponent<Props, State> {
>
{t('common.save')}
</Button>
</ModalFooter>
</ModalBody>
</form>
</Box>
)}
/>
</form>
</Modal>
)
}
}

8
src/components/SettingsPage/PasswordAutoLockSelect.js

@ -30,10 +30,10 @@ class PasswordAutoLockSelect extends PureComponent<Props> {
}
timeouts = [
{ value: 1, label: `1 ${this.props.t('app:time.minute')}` },
{ value: 10, label: `10 ${this.props.t('app:time.minute')}s` },
{ value: 30, label: `30 ${this.props.t('app:time.minute')}s` },
{ value: 60, label: `1 ${this.props.t('app:time.hour')}` },
{ value: 1, label: `1 ${this.props.t('time.minute')}` },
{ value: 10, label: `10 ${this.props.t('time.minute')}s` },
{ value: 30, label: `30 ${this.props.t('time.minute')}s` },
{ value: 60, label: `1 ${this.props.t('time.hour')}` },
{ value: -1, label: this.props.t(`app:common.never`) },
]

1
src/components/SettingsPage/PasswordForm.js

@ -70,6 +70,7 @@ class PasswordForm extends PureComponent<Props> {
</Label>
<InputPassword
style={{ width: 240 }}
onEnter={onSubmit}
id="confirmPassword"
onChange={onChange('confirmPassword')}
value={confirmPassword}

39
src/components/SettingsPage/PasswordModal.js

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent } from 'react'
import React, { Fragment, PureComponent } from 'react'
import type { T } from 'types/common'
@ -8,7 +8,7 @@ import db from 'helpers/db'
import { PasswordIncorrectError } from '@ledgerhq/errors'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal'
import Modal, { ModalBody } from 'components/base/Modal'
import PasswordForm from './PasswordForm'
@ -17,6 +17,7 @@ type Props = {
onClose: () => void,
onChangePassword: (?string) => void,
hasPassword: boolean,
isOpened: boolean,
}
type State = {
@ -36,6 +37,12 @@ const INITIAL_STATE = {
class PasswordModal extends PureComponent<Props, State> {
state = INITIAL_STATE
componentWillReceiveProps(nextProps: Props) {
if (!nextProps.isOpened) {
this.setState(INITIAL_STATE)
}
}
handleSave = (e: SyntheticEvent<HTMLFormElement>) => {
const { currentPassword, newPassword } = this.state
@ -73,23 +80,17 @@ class PasswordModal extends PureComponent<Props, State> {
}
render() {
const { t, hasPassword, onClose, ...props } = this.props
const { t, hasPassword, onClose, isOpened, ...props } = this.props
const { currentPassword, newPassword, incorrectPassword, confirmPassword } = this.state
return (
<Modal
<Modal isOpened={isOpened} centered>
<ModalBody
{...props}
title={hasPassword ? t('password.changePassword.title') : t('password.setPassword.title')}
onHide={this.handleReset}
onClose={onClose}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
{hasPassword ? (
<ModalTitle>{t('password.changePassword.title')}</ModalTitle>
) : (
<ModalTitle data-e2e="enablePassword_modal">
{t('password.setPassword.title')}
</ModalTitle>
)}
<ModalContent>
render={() => (
<Fragment>
<Box
ff="Museo Sans|Regular"
color="dark"
@ -116,8 +117,10 @@ class PasswordModal extends PureComponent<Props, State> {
onChange={this.handleInputChange}
t={t}
/>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
</Fragment>
)}
renderFooter={() => (
<Box horizontal align="center" justify="flex-end" flow={2}>
<Button
small
type="button"
@ -134,10 +137,10 @@ class PasswordModal extends PureComponent<Props, State> {
>
{t('common.save')}
</Button>
</ModalFooter>
</ModalBody>
</Box>
)}
/>
</Modal>
)
}
}

14
src/components/SettingsPage/RepairDeviceButton.js

@ -11,7 +11,7 @@ import logger from 'logger'
import type { T } from 'types/common'
import firmwareRepair from 'commands/firmwareRepair'
import Button from 'components/base/Button'
import { RepairModal } from 'components/base/Modal'
import RepairModal from 'components/base/Modal/RepairModal'
type Props = {
t: T,
@ -33,28 +33,38 @@ class RepairDeviceButton extends PureComponent<Props, State> {
progress: 0,
}
componentWillUnmount() {
if (this.timeout) {
clearTimeout(this.timeout)
}
}
open = () => this.setState({ opened: true, error: null })
sub: *
timeout: *
close = () => {
if (this.sub) this.sub.unsubscribe()
if (this.timeout) clearTimeout(this.timeout)
this.setState({ opened: false, isLoading: false, error: null, progress: 0 })
}
repair = (version = null) => {
if (this.state.isLoading) return
const { push } = this.props
this.setState({ isLoading: true })
this.timeout = setTimeout(() => this.setState({ isLoading: true }), 500)
this.sub = firmwareRepair.send({ version }).subscribe({
next: patch => {
this.setState(patch)
},
error: error => {
logger.critical(error)
if (this.timeout) clearTimeout(this.timeout)
this.setState({ error, isLoading: false, progress: 0 })
},
complete: () => {
if (this.timeout) clearTimeout(this.timeout)
this.setState({ opened: false, isLoading: false, progress: 0 }, () => {
push('/manager')
})

3
src/components/SettingsPage/ResetButton.js

@ -9,7 +9,7 @@ import type { T } from 'types/common'
import { hardReset } from 'helpers/reset'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
import IconTriangleWarning from 'icons/TriangleWarning'
import ResetFallbackModal from './ResetFallbackModal'
@ -58,6 +58,7 @@ class ResetButton extends PureComponent<Props, State> {
<ConfirmModal
analyticsName="HardReset"
isDanger
centered
isLoading={pending}
isOpened={opened}
onClose={this.close}

2
src/components/SettingsPage/ResetFallbackModal.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { ConfirmModal } from 'components/base/Modal'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
import { openUserDataFolderAndQuit } from 'helpers/reset'
type Props = {

4
src/components/SettingsPage/sections/Display.js

@ -91,8 +91,8 @@ class TabGeneral extends PureComponent<Props> {
</Row>
{hasPassword ? (
<Row
title={t('app:settings.profile.passwordAutoLock')}
desc={t('app:settings.profile.passwordAutoLockDesc')}
title={t('settings.profile.passwordAutoLock')}
desc={t('settings.profile.passwordAutoLockDesc')}
>
<PasswordAutoLockSelect />
</Row>

35
src/components/SettingsPage/sections/Export.js

@ -11,7 +11,8 @@ import { SettingsSection as Section, SettingsSectionHeader as Header } from '../
import { EXPERIMENTAL_WS_EXPORT } from '../../../config/constants'
import IconShare from '../../../icons/Share'
import Button from '../../base/Button'
import Modal, { ModalBody, ModalContent, ModalFooter, ModalTitle } from '../../base/Modal'
import Modal from '../../base/Modal'
import ModalBody from '../../base/Modal/ModalBody'
import Box from '../../base/Box'
import QRCodeExporter from '../../QRCodeExporter'
import { BulletRow } from '../../Onboarding/helperComponents'
@ -68,7 +69,10 @@ class SectionExport extends PureComponent<Props, State> {
<Text ff="Open Sans|SemiBold" color="dark">
{'+'}
</Text>
{'button in Accounts'}
{'button in'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Accounts'}
</Text>
</Trans>
</Box>
),
@ -92,16 +96,24 @@ class SectionExport extends PureComponent<Props, State> {
icon: <BulletRowIcon>{'3'}</BulletRowIcon>,
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="settings.export.modal.step3" />
<Trans i18nKey="settings.export.modal.step3">
{'Scan the'}
<Text ff="Open Sans|SemiBold" color="dark">
{'LiveQR Code'}
</Text>
{'until the loader hits 100%'}
</Trans>
</Box>
),
},
]
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('settings.export.modal.title')}</ModalTitle>
<ModalContent flow={2} justify="center" align="center">
<ModalBody
onClose={onClose}
title={t('settings.export.modal.title')}
render={() => (
<Box justify="center" align="center">
<Box flow={2}>
<QRCodeExporter size={330} />
</Box>
@ -113,13 +125,16 @@ class SectionExport extends PureComponent<Props, State> {
<Box style={{ width: 330 }}>
{stepsImportMobile.map(step => <BulletRow key={step.key} step={step} />)}
</Box>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
</Box>
)}
renderFooter={() => (
<Box>
<Button small onClick={onClose} primary>
{t('settings.export.modal.button')}
</Button>
</ModalFooter>
</ModalBody>
</Box>
)}
/>
)
}

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

@ -9,7 +9,8 @@ import TrackPage from 'analytics/TrackPage'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from './index'
import Modal from './index'
import ModalBody from './ModalBody'
type Props = {
isOpened: boolean,
@ -21,11 +22,13 @@ type Props = {
confirmText?: string,
cancelText?: string,
onReject: Function,
onClose?: Function,
onConfirm: Function,
t: T,
isLoading?: boolean,
analyticsName: string,
cancellable?: boolean,
centered?: boolean,
}
class ConfirmModal extends PureComponent<Props> {
@ -43,23 +46,38 @@ class ConfirmModal extends PureComponent<Props> {
onConfirm,
isLoading,
renderIcon,
onClose,
t,
analyticsName,
centered,
...props
} = this.props
const realConfirmText = confirmText || t('common.confirm')
const realCancelText = cancelText || t('common.cancel')
return (
<Modal
isOpened={isOpened}
<Modal isOpened={isOpened} centered={centered}>
<ModalBody
preventBackdropClick={isLoading}
{...props}
render={({ onClose }) => (
<ModalBody onClose={!cancellable && isLoading ? undefined : onClose}>
<TrackPage category="Modal" name={analyticsName} />
<ModalTitle>{title}</ModalTitle>
<ModalContent>
onClose={!cancellable && isLoading ? undefined : onClose}
title={title}
renderFooter={() => (
<Box horizontal align="center" justify="flex-end" flow={2}>
{!isLoading && <Button onClick={onReject}>{realCancelText}</Button>}
<Button
onClick={onConfirm}
primary={!isDanger}
danger={isDanger}
isLoading={isLoading}
disabled={isLoading}
>
{realConfirmText}
</Button>
</Box>
)}
render={() => (
<Box>
{subTitle && (
<Box ff="Museo Sans|Regular" color="dark" textAlign="center" mb={2} mt={3}>
{subTitle}
@ -73,22 +91,11 @@ class ConfirmModal extends PureComponent<Props> {
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center">
{desc}
</Box>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
{!isLoading && <Button onClick={onReject}>{realCancelText}</Button>}
<Button
onClick={onConfirm}
primary={!isDanger}
danger={isDanger}
isLoading={isLoading}
disabled={isLoading}
>
{realConfirmText}
</Button>
</ModalFooter>
</ModalBody>
</Box>
)}
/>
<TrackPage category="Modal" name={analyticsName} />
</Modal>
)
}
}

137
src/components/base/Modal/ModalBody.js

@ -1,95 +1,102 @@
// @flow
import React, { PureComponent } from 'react'
import styled, { keyframes } from 'styled-components'
import React, { PureComponent, Fragment } from 'react'
import Animated from 'animated/lib/targets/react-dom'
import { findDOMNode } from 'react-dom'
import Box from 'components/base/Box'
import IconCross from 'icons/Cross'
import ModalContent from './ModalContent'
import ModalHeader from './ModalHeader'
import ModalFooter from './ModalFooter'
export const Container = styled(Box).attrs({
px: 5,
pb: 5,
})``
import type { RenderProps } from './index'
type Props = {
deferHeight?: number,
onClose?: Function,
children: any,
title: string,
onBack?: void => void,
onClose?: void => void,
render?: (?RenderProps) => any,
renderFooter?: (?RenderProps) => any,
renderProps?: RenderProps,
noScroll?: boolean,
refocusWhenChange?: any,
}
type State = {
isHidden: boolean,
animGradient: Animated.Value,
}
class ModalBody extends PureComponent<Props, State> {
static defaultProps = {
onClose: undefined,
state = {
animGradient: new Animated.Value(0),
}
state = {
isHidden: true,
componentDidUpdate(prevProps: Props) {
const shouldFocus = prevProps.refocusWhenChange !== this.props.refocusWhenChange
if (shouldFocus) {
if (this._content) {
const node = findDOMNode(this._content) // eslint-disable-line react/no-find-dom-node
if (node) {
// $FlowFixMe
node.focus()
}
}
}
}
_content = null
componentDidMount() {
setTimeout(() => {
window.requestAnimationFrame(() => {
this.setState({ isHidden: false })
})
}, 150)
animateGradient = (isScrollable: boolean) => {
const anim = {
duration: 150,
toValue: isScrollable ? 1 : 0,
}
Animated.timing(this.state.animGradient, anim).start()
}
render() {
const { children, onClose, deferHeight, ...props } = this.props
const { isHidden } = this.state
const { onBack, onClose, title, render, renderFooter, renderProps, noScroll } = this.props
const { animGradient } = this.state
const gradientStyle = {
...GRADIENT_STYLE,
opacity: animGradient,
}
return (
<Body
style={{ height: isHidden && deferHeight ? deferHeight : undefined }}
data-e2e="modalBody"
<Fragment>
<ModalHeader onBack={onBack} onClose={onClose}>
{title}
</ModalHeader>
<ModalContent
tabIndex={0}
ref={n => (this._content = n)}
onIsScrollableChange={this.animateGradient}
noScroll={noScroll}
>
{onClose && (
<CloseContainer onClick={onClose}>
<IconCross size={16} />
</CloseContainer>
)}
{(!isHidden || !deferHeight) && <Inner {...props}>{children}</Inner>}
</Body>
{render && render(renderProps)}
</ModalContent>
<div style={GRADIENT_WRAPPER_STYLE}>
<Animated.div style={gradientStyle} />
</div>
{renderFooter && <ModalFooter>{renderFooter(renderProps)}</ModalFooter>}
</Fragment>
)
}
}
const CloseContainer = styled(Box).attrs({
p: 4,
color: 'fog',
})`
position: absolute;
top: 0;
right: 0;
z-index: 1;
&:hover {
color: ${p => p.theme.colors.grey};
const GRADIENT_STYLE = {
background: 'linear-gradient(rgba(255, 255, 255, 0), #ffffff)',
height: 40,
position: 'absolute',
bottom: 0,
left: 0,
right: 20,
}
&:active {
color: ${p => p.theme.colors.dark};
const GRADIENT_WRAPPER_STYLE = {
height: 0,
position: 'relative',
pointerEvents: 'none',
}
`
const Body = styled(Box).attrs({
bg: p => p.theme.colors.white,
relative: true,
borderRadius: 1,
})`
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
`
const appear = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`
const Inner = styled(Box)`
animation: ${appear} 80ms linear;
`
export default ModalBody

59
src/components/base/Modal/ModalContent.js

@ -0,0 +1,59 @@
// @flow
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import React, { PureComponent } from 'react'
class ModalContent extends PureComponent<{
children: any,
onIsScrollableChange: boolean => void,
noScroll?: boolean,
}> {
componentDidMount() {
window.requestAnimationFrame(() => {
if (this._isUnmounted) return
this.showHideGradient()
if (this._outer) {
const ro = new ResizeObserver(this.showHideGradient)
ro.observe(this._outer)
}
})
}
componentWillUnmount() {
this._isUnmounted = true
}
_outer = null
_isUnmounted = false
showHideGradient = () => {
if (!this._outer) return
const { onIsScrollableChange } = this.props
const isScrollable = this._outer.scrollHeight > this._outer.clientHeight
onIsScrollableChange(isScrollable)
}
render() {
const { children, noScroll } = this.props
const contentStyle = {
...CONTENT_STYLE,
overflow: noScroll ? 'visible' : 'auto',
}
return (
<div style={contentStyle} ref={n => (this._outer = n)} tabIndex={0}>
{children}
</div>
)
}
}
const CONTENT_STYLE = {
flexShrink: 1,
padding: 20,
paddingBottom: 40,
}
export default ModalContent

18
src/components/base/Modal/ModalFooter.js

@ -0,0 +1,18 @@
// @flow
import React from 'react'
import { colors } from 'styles/theme'
const MODAL_FOOTER_STYLE = {
display: 'flex',
justifyContent: 'flex-end',
borderTop: `2px solid ${colors.lightGrey}`,
padding: 20,
}
const ModalFooter = ({ children }: { children: any }) => (
<div style={MODAL_FOOTER_STYLE}>{children}</div>
)
export default ModalFooter

93
src/components/base/Modal/ModalHeader.js

@ -0,0 +1,93 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import IconAngleLeft from 'icons/AngleLeft'
import IconCross from 'icons/Cross'
const MODAL_HEADER_STYLE = {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 20,
}
const ModalTitle = styled(Box).attrs({
color: 'dark',
ff: 'Museo Sans|Regular',
fontSize: 6,
grow: true,
shrink: true,
})`
text-align: center;
line-height: 1;
`
const iconAngleLeft = <IconAngleLeft size={16} />
const iconCross = <IconCross size={16} />
const ModalHeaderAction = styled(Box).attrs({
horizontal: true,
align: 'center',
fontSize: 3,
p: 4,
color: 'grey',
})`
position: absolute;
top: 0;
left: ${p => (p.right ? 'auto' : 0)};
right: ${p => (p.right ? 0 : 'auto')};
line-height: 0;
cursor: pointer;
&:hover {
color: ${p => p.theme.colors.graphite};
}
&:active {
color: ${p => p.theme.colors.dark};
}
span {
border-bottom: 1px dashed transparent;
}
&:focus span {
border-bottom-color: inherit;
}
`
const ModalHeader = ({
children,
onBack,
onClose,
t,
}: {
children: any,
onBack: void => void,
onClose: void => void,
t: T,
}) => (
<div style={MODAL_HEADER_STYLE}>
{onBack && (
<ModalHeaderAction onClick={onBack}>
{iconAngleLeft}
<span>{t('common.back')}</span>
</ModalHeaderAction>
)}
<ModalTitle>{children}</ModalTitle>
{onClose && (
<ModalHeaderAction right color="fog" onClick={onClose}>
{iconCross}
</ModalHeaderAction>
)}
</div>
)
export default translate()(ModalHeader)

75
src/components/base/Modal/ModalTitle.js

@ -1,75 +0,0 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import IconAngleLeft from 'icons/AngleLeft'
const Container = styled(Box).attrs({
alignItems: 'center',
color: 'dark',
ff: 'Museo Sans|Regular',
fontSize: 6,
justifyContent: 'center',
p: 5,
relative: true,
})``
const Back = styled(Box).attrs({
unstyled: true,
horizontal: true,
align: 'center',
color: 'grey',
ff: 'Open Sans',
fontSize: 3,
p: 4,
})`
position: absolute;
line-height: 1;
top: 0;
left: 0;
&:hover {
color: ${p => p.theme.colors.graphite};
}
&:active {
color: ${p => p.theme.colors.dark};
}
span {
border-bottom: 1px dashed transparent;
}
&:focus span {
border-bottom-color: inherit;
}
`
function ModalTitle({
t,
onBack,
children,
...props
}: {
t: T,
onBack: any => void,
children: any,
}) {
return (
<Container {...props} data-e2e="modal_title">
{onBack && (
<Back onClick={onBack}>
<IconAngleLeft size={16} />
<span>{t('common.back')}</span>
</Back>
)}
{children}
</Container>
)
}
export default translate()(ModalTitle)

47
src/components/base/Modal/RepairModal.js

@ -17,7 +17,8 @@ import ProgressCircle from 'components/ProgressCircle'
import TranslatedError from 'components/TranslatedError'
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from './index'
import Modal from './index'
import ModalBody from './ModalBody'
const Container = styled(Box).attrs({
alignItems: 'center',
@ -39,18 +40,18 @@ const Separator = styled(Box).attrs({
`
const DisclaimerStep = ({ desc }: { desc?: string }) => (
<ModalContent>
<Box>
{desc ? (
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" mb={2}>
{desc}
</Box>
) : null}
</ModalContent>
</Box>
)
const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
progress === 0 ? (
<ModalContent>
<Box>
<Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke">
<Bullet>{'1.'}</Bullet>
@ -74,9 +75,9 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
alt={t('manager.modal.mcuFirst')}
/>
</Box>
</ModalContent>
</Box>
) : (
<ModalContent>
<Box>
<Box mx={7} align="center">
<ProgressCircle size={64} progress={progress} />
</Box>
@ -88,11 +89,11 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
{t('manager.modal.mcuPin')}
</Text>
</Box>
</ModalContent>
</Box>
)
const ErrorStep = ({ error }: { error: Error }) => (
<ModalContent>
<Box>
<Container>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
@ -118,7 +119,7 @@ const ErrorStep = ({ error }: { error: Error }) => (
<TranslatedError error={error} field="description" />
</Box>
</Container>
</ModalContent>
</Box>
)
type Props = {
@ -177,12 +178,16 @@ class RepairModal extends PureComponent<Props, *> {
return (
<Modal
isOpened={isOpened}
centered
preventBackdropClick={isLoading}
onClose={!cancellable && isLoading ? undefined : onReject}
{...props}
render={({ onClose }) => (
<ModalBody onClose={!cancellable && isLoading ? undefined : onClose}>
>
<TrackPage category="Modal" name={analyticsName} />
<ModalTitle>{title}</ModalTitle>
<ModalBody
title={title}
render={() => (
<Box>
{error ? (
<ErrorStep error={error} />
) : isLoading ? (
@ -205,10 +210,12 @@ class RepairModal extends PureComponent<Props, *> {
/>
</Box>
) : null}
{!isLoading ? (
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
{error ? <Button onClick={onReject}>{t(`common.close`)}</Button> : null}
</Box>
)}
renderFooter={() =>
!isLoading ? (
<Box horizontal align="center" justify="flex-end" flow={2}>
<Button onClick={onReject}>{t(`common.${error ? 'close' : 'cancel'}`)}</Button>
{error ? null : (
<>
<Button
@ -222,11 +229,11 @@ class RepairModal extends PureComponent<Props, *> {
</Button>
</>
)}
</ModalFooter>
) : null}
</ModalBody>
)}
</Box>
) : null
}
/>
</Modal>
)
}
}

337
src/components/base/Modal/index.js

@ -1,54 +1,28 @@
// @flow
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable react/no-multi-comp */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import React, { PureComponent, Fragment } from 'react'
import { createPortal } from 'react-dom'
import { connect } from 'react-redux'
import Mortal from 'react-mortal'
import styled from 'styled-components'
import noop from 'lodash/noop'
import { EXPERIMENTAL_CENTER_MODAL } from 'config/constants'
import { rgba } from 'styles/helpers'
import { radii } from 'styles/theme'
import Animated from 'animated/lib/targets/react-dom'
import Easing from 'animated/lib/Easing'
import { closeModal, isModalOpened, getModalData } from 'reducers/modals'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import { colors } from 'styles/theme'
export { default as ModalBody } from './ModalBody'
export { default as ConfirmModal } from './ConfirmModal'
export { default as RepairModal } from './RepairModal'
export { default as ModalTitle } from './ModalTitle'
const springConfig = {
stiffness: 320,
}
type OwnProps = {
name?: string, // eslint-disable-line
isOpened?: boolean,
onBeforeOpen?: ({ data: * }) => *, // eslint-disable-line
onClose?: () => void,
onHide?: () => void,
preventBackdropClick?: boolean,
render: Function,
refocusWhenChange?: string,
width?: string,
const animShowHide = {
duration: 200,
easing: Easing.bezier(0.3, 1.0, 0.5, 0.8),
}
type Props = OwnProps & {
isOpened?: boolean,
data?: any,
} & {
onClose?: () => void,
}
const domNode = process.env.STORYBOOK_ENV ? document.body : document.getElementById('modals')
const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: OwnProps): * => {
const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: Props): * => {
const data = getModalData(state, name || '')
const modalOpened = isOpened || (name && isModalOpened(state, name))
@ -62,7 +36,7 @@ const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: OwnProps): * =
}
}
const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: OwnProps): * => ({
const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: Props): * => ({
onClose: name
? () => {
dispatch(closeModal(name))
@ -71,178 +45,187 @@ const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: OwnProps): *
: onClose,
})
const Container = styled(Box).attrs({
color: 'grey',
sticky: true,
style: p => ({
pointerEvents: p.isVisible ? 'auto' : 'none',
}),
})`
position: fixed;
z-index: 30;
`
const Backdrop = styled(Box).attrs({
bg: p => rgba(p.theme.colors.black, 0.4),
sticky: true,
style: p => ({
opacity: p.op,
}),
})`
position: fixed;
`
const NonClickableHeadArea = styled.div`
position: fixed;
height: 48px;
width: 100%;
top: 0;
left: 0;
z-index: 1;
`
const Wrapper = styled(Box).attrs({
bg: 'transparent',
flow: 4,
style: p => ({
opacity: p.op,
transform: `scale3d(${p.scale}, ${p.scale}, ${p.scale})`,
}),
})`
outline: none;
width: ${p => (p.width ? p.width : '500px')};
z-index: 2;
`
class Pure extends Component<any> {
shouldComponentUpdate(nextProps) {
if (nextProps.isAnimated) {
return false
}
return true
export type RenderProps = {
onClose?: void => void,
data: any,
}
render() {
const { data, onClose, render } = this.props
type Props = {
isOpened?: boolean,
children?: any,
centered?: boolean,
onClose?: void => void,
onHide?: void => void,
render?: RenderProps => any,
data?: any,
preventBackdropClick?: boolean,
return render({ data, onClose })
name?: string, // eslint-disable-line
onBeforeOpen?: ({ data: * }) => *, // eslint-disable-line
}
type State = {
animShowHide: Animated.Value,
isInDOM: boolean,
}
function stopPropagation(e) {
e.stopPropagation()
class Modal extends PureComponent<Props, State> {
state = {
animShowHide: new Animated.Value(0),
isInDOM: this.props.isOpened === true,
}
const wrap = EXPERIMENTAL_CENTER_MODAL
? children => (
<Box alignItems="center" justifyContent="center" grow>
{children}
</Box>
)
: children => (
<GrowScroll alignItems="center" full pt={8}>
{children}
</GrowScroll>
)
static getDerivedStateFromProps(nextProps: Props) {
const patch = {}
if (nextProps.isOpened) {
patch.isInDOM = true
}
return patch
}
export class Modal extends Component<Props> {
static defaultProps = {
isOpened: false,
onHide: noop,
preventBackdropClick: false,
componentDidMount() {
if (this.props.isOpened) {
this.animateEnter()
}
shouldComponentUpdate(nextProps: Props) {
if (this.props.isOpened || nextProps.isOpened) {
return true
this.state.animShowHide.addListener(({ value }) => {
if (value === 0) {
const { onHide } = this.props
this.setState({ isInDOM: false })
if (onHide) {
onHide()
}
}
if (value === 1) this.setState({ isInDOM: true })
})
return false
document.addEventListener('keyup', this.handleKeyup)
}
componentDidUpdate(prevProps: Props) {
const didOpened = this.props.isOpened && !prevProps.isOpened
const didClose = !this.props.isOpened && prevProps.isOpened
const shouldFocus = didOpened || this.props.refocusWhenChange !== prevProps.refocusWhenChange
const didOpened = !prevProps.isOpened && this.props.isOpened
const didClosed = prevProps.isOpened && !this.props.isOpened
if (didOpened) {
// Store a reference to the last active element, to restore it after
// modal close
this._lastFocusedElement = document.activeElement
}
if (shouldFocus) {
this.focusWrapper()
this.animateEnter()
}
if (didClose) {
if (this._lastFocusedElement) {
this._lastFocusedElement.focus()
if (didClosed) {
this.animateLeave()
}
}
componentWillUnmount() {
document.removeEventListener('keyup', this.handleKeyup)
}
_wrapper = null
_lastFocusedElement = null
handleKeyup = (e: KeyboardEvent) => {
const { onClose, preventBackdropClick } = this.props
if (e.which === 27 && onClose && !preventBackdropClick) {
onClose()
}
}
focusWrapper = () => {
// Forced to use findDOMNode here, because innerRef is giving a proxied component
const domWrapper = findDOMNode(this._wrapper) // eslint-disable-line react/no-find-dom-node
if (domWrapper instanceof HTMLDivElement && !domWrapper.contains(this._lastFocusedElement)) {
domWrapper.focus()
handleClickOnBackdrop = () => {
const { preventBackdropClick, onClose } = this.props
if (!preventBackdropClick && onClose) {
onClose()
}
}
swallowClick = e => {
e.preventDefault()
e.stopPropagation()
}
animateEnter = () =>
Animated.timing(this.state.animShowHide, { ...animShowHide, toValue: 1 }).start()
animateLeave = () =>
Animated.timing(this.state.animShowHide, { ...animShowHide, toValue: 0 }).start()
render() {
const { preventBackdropClick, isOpened, onHide, render, data, onClose, width } = this.props
return (
<Mortal
isOpened={isOpened}
onClose={onClose}
onHide={onHide}
closeOnEsc={!preventBackdropClick}
motionStyle={(spring, isVisible) => ({
opacity: spring(isVisible ? 1 : 0, springConfig),
scale: spring(isVisible ? 1 : 0.95, springConfig),
})}
>
{(m, isVisible, isAnimated) => (
<Container isVisible={isVisible} onClick={preventBackdropClick ? undefined : onClose}>
<Backdrop op={m.opacity} />
<NonClickableHeadArea onClick={stopPropagation} />
{wrap(
<Wrapper
tabIndex={-1}
op={m.opacity}
scale={m.scale}
innerRef={n => (this._wrapper = n)}
onClick={stopPropagation}
width={width}
>
<Pure isAnimated={isAnimated} render={render} data={data} onClose={onClose} />
</Wrapper>,
)}
</Container>
)}
</Mortal>
const { animShowHide, isInDOM } = this.state
const { children, render, centered, onClose, data, isOpened } = this.props
if (!isInDOM) {
return null
}
const backdropStyle = {
...BACKDROP_STYLE,
opacity: animShowHide,
}
const containerStyle = {
...CONTAINER_STYLE,
justifyContent: centered ? 'center' : 'flex-start',
pointerEvents: isOpened ? 'auto' : 'none',
}
const scale = animShowHide.interpolate({
inputRange: [0, 1],
outputRange: [1.1, 1],
clamp: true,
})
const bodyWrapperStyle = {
...BODY_WRAPPER_STYLE,
opacity: animShowHide,
transform: [{ scale }],
}
const renderProps = {
onClose,
data,
}
const modal = (
<Fragment>
<Animated.div style={backdropStyle} />
<div style={containerStyle} onClick={this.handleClickOnBackdrop}>
<Animated.div style={bodyWrapperStyle} onClick={this.swallowClick}>
{render && render(renderProps)}
{children}
</Animated.div>
</div>
</Fragment>
)
return domNode ? createPortal(modal, domNode) : null
}
}
export const ModalFooter = styled(Box).attrs({
px: 5,
py: 3,
})`
border-top: 2px solid ${p => p.theme.colors.lightGrey};
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`
const BACKDROP_STYLE = {
pointerEvents: 'none',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.4)',
zIndex: 100,
}
export const ModalContent = styled(Box).attrs({
px: 5,
pb: 5,
selectable: true,
})``
const CONTAINER_STYLE = {
...BACKDROP_STYLE,
background: 'transparent',
padding: '60px 0 60px 0',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}
const BODY_WRAPPER_STYLE = {
background: 'white',
width: 500,
borderRadius: 3,
boxShadow: 'box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2)',
color: colors.smoke,
flexShrink: 1,
display: 'flex',
flexDirection: 'column',
}
export default connect(
mapStateToProps,

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

@ -2,50 +2,57 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { boolean, text } from '@storybook/addon-knobs'
import { action } from '@storybook/addon-actions'
import {
Modal,
ModalBody,
ModalTitle,
ModalContent,
ModalFooter,
ConfirmModal,
} from 'components/base/Modal'
import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
const stories = storiesOf('Components/base', module)
stories.add('Modal', () => (
<Modal
isOpened={boolean('isOpened', true)}
centered={boolean('centered', true)}
onClose={action('onClose')}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle>{'modal title'}</ModalTitle>
<ModalContent>{'this is the modal content'}</ModalContent>
<ModalFooter horizontal align="center">
<Box grow>{'modal footer'}</Box>
<Button primary>{'Next'}</Button>
</ModalFooter>
</ModalBody>
<ModalBody
onClose={onClose}
onBack={action('onBack')}
title={text('title', 'Send funds')}
render={() => (
<Box flow={4}>
<Box flow={2}>
<Label>{'first field'}</Label>
<Input autoFocus />
</Box>
<Box horizontal flow={4}>
<Box flow={2} flex={1}>
<Label>{'second field'}</Label>
<Input />
</Box>
<Box flow={2} flex={1}>
<Label>{'third field'}</Label>
<Input />
</Box>
</Box>
<Box horizontal flow={4}>
<Box flow={2} flex={1}>
<Label>{'second field'}</Label>
<Input />
</Box>
<Box flow={2} flex={1}>
<Label>{'third field'}</Label>
<Input />
</Box>
</Box>
</Box>
)}
renderFooter={() => 'footer'}
/>
))
stories.add('ConfirmModal', () => (
<ConfirmModal
categoryName=""
isOpened
isDanger={boolean('isDanger', false)}
title={text('title', 'Hard reset')}
subTitle={text('subTitle', 'Are you sure houston?')}
desc={text(
'desc',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus',
)}
onConfirm={action('onConfirm')}
onReject={action('onReject')}
/>
))

26
src/components/base/Stepper/index.js

@ -1,12 +1,12 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { ModalContent, ModalTitle, ModalFooter, ModalBody } from 'components/base/Modal'
import { ModalBody } from 'components/base/Modal'
import Breadcrumb from 'components/Breadcrumb'
type Props = {
@ -29,6 +29,7 @@ export type Step = {
shouldRenderFooter?: StepProps => boolean,
shouldPreventClose?: boolean | (StepProps => boolean),
onBack?: StepProps => void,
noScroll?: boolean,
}
type State = {
@ -72,6 +73,7 @@ class Stepper extends PureComponent<Props, State> {
onBack,
shouldPreventClose,
shouldRenderFooter,
noScroll,
} = step
const stepProps: StepProps = {
@ -89,9 +91,14 @@ class Stepper extends PureComponent<Props, State> {
: !!shouldPreventClose
return (
<ModalBody onClose={preventClose ? undefined : onClose}>
<ModalTitle onBack={onBack ? () => onBack(stepProps) : undefined}>{title}</ModalTitle>
<ModalContent>
<ModalBody
refocusWhenChange={stepId}
onClose={preventClose ? undefined : onClose}
onBack={onBack ? () => onBack(stepProps) : undefined}
title={title}
noScroll={noScroll}
render={() => (
<Fragment>
<Breadcrumb
mb={6}
currentStep={stepIndex}
@ -101,13 +108,10 @@ class Stepper extends PureComponent<Props, State> {
/>
<StepComponent {...stepProps} />
{children}
</ModalContent>
{renderFooter && (
<ModalFooter horizontal align="center" justify="flex-end">
<StepFooter {...stepProps} />
</ModalFooter>
</Fragment>
)}
</ModalBody>
renderFooter={renderFooter ? () => <StepFooter {...stepProps} /> : undefined}
/>
)
}
}

82
src/components/modals/AccountSettingRenderBody.js

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { compose } from 'redux'
@ -23,18 +23,14 @@ import TrackPage from 'analytics/TrackPage'
import Spoiler from 'components/base/Spoiler'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Select from 'components/base/Select'
import SyncAgo from 'components/SyncAgo'
import {
ModalBody,
ModalTitle,
ModalFooter,
ModalContent,
ConfirmModal,
} from 'components/base/Modal'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
import ModalBody from 'components/base/Modal/ModalBody'
type State = {
accountName: ?string,
@ -74,7 +70,7 @@ const defaultState = {
isRemoveAccountModalOpen: false,
}
class HelperComp extends PureComponent<Props, State> {
class AccountSettingRenderBody extends PureComponent<Props, State> {
state = {
...defaultState,
}
@ -84,7 +80,6 @@ class HelperComp extends PureComponent<Props, State> {
}
getAccount(data: Object): Account {
// FIXME this should be a selector
const { accountName } = this.state
const account = get(data, 'account', {})
@ -129,10 +124,9 @@ class HelperComp extends PureComponent<Props, State> {
})
handleSubmit = (account: Account, onClose: () => void) => (
e: SyntheticEvent<HTMLFormElement>,
e: SyntheticEvent<HTMLFormElement | HTMLInputElement>,
) => {
e.preventDefault()
const { updateAccount, setDataModal } = this.props
const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state
@ -194,10 +188,10 @@ class HelperComp extends PureComponent<Props, State> {
endpointConfigError,
} = this.state
const { t, onClose, data } = this.props
if (!data) return null
const account = this.getAccount(data)
const bridge = getBridgeForCurrency(account.currency)
const usefulData = {
xpub: account.xpub || undefined,
index: account.index,
@ -206,12 +200,15 @@ class HelperComp extends PureComponent<Props, State> {
blockHeight: account.blockHeight,
}
const onSubmit = this.handleSubmit(account, onClose)
return (
<ModalBody onClose={onClose}>
<form onSubmit={this.handleSubmit(account, onClose)}>
<ModalBody
onClose={onClose}
title={t('account.settings.title')}
render={() => (
<Fragment>
<TrackPage category="Modal" name="AccountSettings" />
<ModalTitle>{t('account.settings.title')}</ModalTitle>
<ModalContent mb={3}>
<Container>
<Box>
<OptionRowTitle>{t('account.settings.accountName.title')}</OptionRowTitle>
@ -224,6 +221,7 @@ class HelperComp extends PureComponent<Props, State> {
value={account.name}
maxLength={MAX_ACCOUNT_NAME_SIZE}
onChange={this.handleChangeName}
onEnter={onSubmit}
onFocus={e => this.handleFocus(e, 'accountName')}
error={accountNameError}
/>
@ -268,8 +266,7 @@ class HelperComp extends PureComponent<Props, State> {
) : null}
<Spoiler textTransform title={t('account.settings.advancedLogs')}>
<SyncAgo date={account.lastSyncDate} />
<textarea
readOnly
<div
style={{
userSelect: 'text',
border: '1px dashed #f9f9f9',
@ -277,28 +274,17 @@ class HelperComp extends PureComponent<Props, State> {
color: '#000',
fontFamily: 'monospace',
fontSize: '10px',
height: 200,
outline: 'none',
padding: '20px',
width: '100%',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
overflow: 'auto',
}}
value={JSON.stringify(usefulData, null, 2)}
/>
</Spoiler>
</ModalContent>
<ModalFooter horizontal>
<Button
event="OpenAccountDelete"
danger
type="button"
onClick={this.handleOpenRemoveAccountModal}
>
{t('common.delete')}
</Button>
<Button event="DoneEditingAccount" ml="auto" type="submit" primary>
{t('common.apply')}
</Button>
</ModalFooter>
</form>
{JSON.stringify(usefulData, null, 2)}
</div>
</Spoiler>
<ConfirmModal
analyticsName="RemoveAccount"
isDanger
@ -310,7 +296,25 @@ class HelperComp extends PureComponent<Props, State> {
subTitle={t('common.areYouSure')}
desc={t('settings.removeAccountModal.desc')}
/>
</ModalBody>
<Space of={20} />
</Fragment>
)}
renderFooter={() => (
<Fragment>
<Button
event="OpenAccountDelete"
danger
type="button"
onClick={this.handleOpenRemoveAccountModal}
>
{t('common.delete')}
</Button>
<Button event="DoneEditingAccount" ml="auto" onClick={onSubmit} primary>
{t('common.apply')}
</Button>
</Fragment>
)}
/>
)
}
}
@ -321,7 +325,7 @@ export default compose(
mapDispatchToProps,
),
translate(),
)(HelperComp)
)(AccountSettingRenderBody)
export function InputLeft({ currency }: { currency: Currency }) {
return (

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

@ -44,6 +44,7 @@ const createSteps = () => {
footer: StepChooseCurrencyFooter,
onBack: null,
hideFooter: false,
noScroll: true,
},
{
id: 'connectDevice',
@ -245,6 +246,7 @@ class AddAccounts extends PureComponent<Props, State> {
return (
<Modal
centered
name={MODAL_ADD_ACCOUNTS}
refocusWhenChange={stepId}
onHide={() => this.setState({ ...INITIAL_STATE })}

26
src/components/modals/Debug.js

@ -1,11 +1,12 @@
// @flow
/* eslint-disable react/jsx-no-literals */
import React, { Component } from 'react'
import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { getDerivationScheme, runDerivationScheme } from '@ledgerhq/live-common/lib/derivation'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal'
import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import { getCurrentDevice } from 'reducers/devices'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
@ -127,14 +128,12 @@ class Debug extends Component<*, *> {
const { device } = this.props
const { logs } = this.state
return (
<Modal
name="MODAL_DEBUG"
onHide={this.onHide}
render={({ onClose }: *) => (
<ModalBody onClose={onClose}>
<Modal name="MODAL_DEBUG" centered onHide={this.onHide}>
<ModalBody
title="developer internal tools"
render={() => (
<Box>
<SyncSkipUnderPriority priority={99999999} />
<ModalTitle>developer internal tools</ModalTitle>
<ModalContent>
<Box style={{ height: 60, overflow: 'auto' }}>
{device && (
<Box horizontal style={{ padding: 10 }}>
@ -191,6 +190,7 @@ class Debug extends Component<*, *> {
>
{logs.map(log => (
<Box
key={log.txt}
style={{
userSelect: 'all',
color: log.type === 'error' ? '#c22' : '#888',
@ -200,6 +200,10 @@ class Debug extends Component<*, *> {
</Box>
))}
</Box>
</Box>
)}
renderFooter={() => (
<Fragment>
<Button
style={{ position: 'absolute', right: 30, bottom: 28 }}
onClick={() => {
@ -208,10 +212,10 @@ class Debug extends Component<*, *> {
>
Clear
</Button>
</ModalContent>
</ModalBody>
</Fragment>
)}
/>
</Modal>
)
}
}

43
src/components/modals/Disclaimer.js

@ -7,42 +7,59 @@ import type { T } from 'types/common'
import { MODAL_DISCLAIMER } from 'config/constants'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal'
import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import { HandShield } from 'components/WarnBox'
import { compose } from 'redux'
import connect from 'react-redux/es/connect/connect'
import { closeModal } from '../../reducers/modals'
type Props = {
t: T,
closeModal: string => void,
}
const mapDispatchToProps = {
closeModal,
}
class DisclaimerModal extends PureComponent<Props> {
onClose = () => this.props.closeModal(MODAL_DISCLAIMER)
render() {
const { t } = this.props
return (
<Modal
name={MODAL_DISCLAIMER}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle>{t('disclaimerModal.title')}</ModalTitle>
<ModalContent flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke">
<Modal name={MODAL_DISCLAIMER} centered>
<ModalBody
onClose={this.onClose}
title={t('disclaimerModal.title')}
render={() => (
<Box flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke">
<Box align="center" mt={4} pb={4}>
<HandShield size={55} />
</Box>
<p>{t('disclaimerModal.desc_1')}</p>
<p>{t('disclaimerModal.desc_2')}</p>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button data-e2e="continue_button" onClick={onClose} primary>
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button data-e2e="continue_button" onClick={this.onClose} primary>
{t('disclaimerModal.cta')}
</Button>
</ModalFooter>
</ModalBody>
</Box>
)}
/>
</Modal>
)
}
}
export default translate()(DisclaimerModal)
export default compose(
connect(
null,
mapDispatchToProps,
),
translate(),
)(DisclaimerModal)

31
src/components/modals/OperationDetails.js

@ -18,12 +18,10 @@ import { MODAL_OPERATION_DETAILS } from 'config/constants'
import { getMarketColor } from 'styles/helpers'
import Box from 'components/base/Box'
import GradientBox from 'components/GradientBox'
import GrowScroll from 'components/base/GrowScroll'
import Button from 'components/base/Button'
import Bar from 'components/base/Bar'
import FormattedVal from 'components/base/FormattedVal'
import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal'
import Modal, { ModalBody } from 'components/base/Modal'
import Text from 'components/base/Text'
import CopyWithFeedback from 'components/base/CopyWithFeedback'
@ -130,11 +128,10 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
const uniqueSenders = uniq(senders)
return (
<ModalBody onClose={onClose}>
<TrackPage category="Modal" name="OperationDetails" />
<ModalTitle>{t('operationDetails.title')}</ModalTitle>
<ModalContent relative style={{ height: 500 }} px={0} pb={0}>
<GrowScroll px={5} pt={1} pb={8}>
<ModalBody
title={t('operationDetails.title')}
onClose={onClose}
render={() => (
<Box flow={3}>
<Box alignItems="center" mt={1}>
<ConfirmationCheck
@ -235,17 +232,16 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
</Box>
))}
</Box>
</GrowScroll>
<GradientBox />
</ModalContent>
{url && (
<ModalFooter horizontal justify="flex-end" flow={2}>
)}
renderFooter={() =>
url && (
<Button primary onClick={() => openURL(url)}>
{t('operationDetails.viewOperation')}
</Button>
</ModalFooter>
)}
)
}
>
<TrackPage category="Modal" name="OperationDetails" />
</ModalBody>
)
})
@ -255,12 +251,13 @@ type ModalRenderProps = {
account: string,
operation: string,
},
onClose: Function,
onClose?: Function,
}
const OperationDetailsWrapper = ({ t }: { t: T }) => (
<Modal
name={MODAL_OPERATION_DETAILS}
centered
render={(props: ModalRenderProps) => {
const { data, onClose } = props
return <OperationDetails t={t} {...data} onClose={onClose} />

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

@ -197,16 +197,17 @@ class ReceiveModal extends PureComponent<Props, State> {
return (
<Modal
name={MODAL_RECEIVE}
centered
refocusWhenChange={stepId}
onHide={this.handleReset}
preventBackdropClick={isModalLocked}
onBeforeOpen={this.handleBeforeOpenModal}
render={({ onClose }) => (
render={() => (
<Stepper
title={t('receive.title')}
initialStepId={stepId}
onStepChange={this.handleStepChange}
onClose={onClose}
onClose={addtionnalProps.closeModal}
steps={this.STEPS}
disabledSteps={disabledSteps}
errorSteps={errorSteps}

26
src/components/modals/ReleaseNotes/ReleaseNotesBody.js

@ -7,14 +7,13 @@ import network from 'api/network'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Text from 'components/base/Text'
import Spinner from 'components/base/Spinner'
import GradientBox from 'components/GradientBox'
import TranslatedError from 'components/TranslatedError'
import TrackPage from 'analytics/TrackPage'
import Markdown, { Notes } from 'components/base/Markdown'
import { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import type { T } from 'types/common'
@ -115,21 +114,26 @@ class ReleaseNotesBody extends PureComponent<Props, State> {
const { onClose, t } = this.props
return (
<ModalBody onClose={onClose}>
<ModalBody
onClose={onClose}
title={t('releaseNotes.title')}
render={() => (
<Box relative style={{ height: 500 }} px={0} pb={0}>
<TrackPage category="Modal" name="ReleaseNotes" />
<ModalTitle>{t('releaseNotes.title')}</ModalTitle>
<ModalContent relative style={{ height: 500 }} px={0} pb={0}>
<GrowScroll px={5} pb={8}>
<Box px={5} pb={8}>
{this.renderContent()}
</GrowScroll>
</Box>
<GradientBox />
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button onClick={onClose} primary>
{t('common.continue')}
</Button>
</ModalFooter>
</ModalBody>
</Box>
)}
/>
)
}
}

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

@ -43,6 +43,7 @@ class ReleaseNotesModal extends PureComponent<Props> {
return (
<Modal
name={MODAL_RELEASES_NOTES}
centered
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />}
/>
)

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

@ -277,6 +277,7 @@ class SendModal extends PureComponent<Props, State<*>> {
return (
<Modal
name={MODAL_SEND}
centered
refocusWhenChange={stepId}
onHide={this.handleReset}
preventBackdropClick={isModalLocked}

5
src/components/modals/SettingsAccount.js

@ -11,7 +11,10 @@ export default class SettingsAccount extends PureComponent<*, *> {
return (
<Modal
name={MODAL_SETTINGS_ACCOUNT}
render={({ data, onClose }) => <AccountSettingRenderBody data={data} onClose={onClose} />}
centered
render={({ data, onClose }) => (
<AccountSettingRenderBody {...this.props} data={data} onClose={onClose} />
)}
/>
)
}

54
src/components/modals/ShareAnalytics.js

@ -1,20 +1,30 @@
// @flow
import React, { PureComponent } from 'react'
import React, { Fragment, 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 Modal from 'components/base/Modal'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import { connect } from 'react-redux'
import { compose } from 'redux'
import type { T } from 'types/common'
import { closeModal } from '../../reducers/modals'
import ModalBody from '../base/Modal/ModalBody'
type Props = {
t: T,
closeModal: string => void,
}
const mapDispatchToProps = {
closeModal,
}
class ShareAnalytics extends PureComponent<Props, *> {
onClose = () => this.props.closeModal(MODAL_SHARE_ANALYTICS)
render() {
const { t } = this.props
const items = [
@ -56,30 +66,38 @@ class ShareAnalytics extends PureComponent<Props, *> {
},
]
return (
<Modal
name={MODAL_SHARE_ANALYTICS}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle data-e2e="modal_title_shareAnalytics">
{t('onboarding.analytics.shareAnalytics.title')}
</ModalTitle>
<Modal name={MODAL_SHARE_ANALYTICS} centered>
<ModalBody
onClose={this.onClose}
title={t('onboarding.analytics.shareAnalytics.title')}
render={() => (
<Fragment>
<InlineDesc>{t('onboarding.analytics.shareAnalytics.desc')}</InlineDesc>
<ModalContent mx={5}>
<Box mx={5}>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button onClick={onClose} primary data-e2e="modal_buttonClose_shareAnalytics">
</Box>
</Fragment>
)}
renderFooter={() => (
<Fragment>
<Button onClick={this.onClose} primary data-e2e="modal_buttonClose_shareAnalytics">
{t('common.close')}
</Button>
</ModalFooter>
</ModalBody>
</Fragment>
)}
/>
</Modal>
)
}
}
export default translate()(ShareAnalytics)
export default compose(
connect(
null,
mapDispatchToProps,
),
translate(),
)(ShareAnalytics)
export const Ul = styled.ul.attrs({
ff: 'Open Sans|Regular',
@ -93,5 +111,5 @@ export const InlineDesc = styled(Box).attrs({
ff: 'Open Sans|SemiBold',
fontSize: 4,
color: 'dark',
mx: '45px',
mx: '15px',
})``

51
src/components/modals/TechnicalData.js

@ -1,19 +1,30 @@
// @flow
import React, { PureComponent } from 'react'
import React, { Fragment, 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 Modal, { ModalBody } from 'components/base/Modal'
import Button from 'components/base/Button'
import type { T } from 'types/common'
import { connect } from 'react-redux'
import { compose } from 'redux'
import Box from 'components/base/Box'
import { Ul, InlineDesc } from './ShareAnalytics'
import { closeModal } from '../../reducers/modals'
type Props = {
t: T,
closeModal: string => void,
}
const mapDispatchToProps = {
closeModal,
}
class TechnicalData extends PureComponent<Props, *> {
onClose = () => this.props.closeModal(MODAL_TECHNICAL_DATA)
render() {
const { t } = this.props
@ -33,27 +44,35 @@ class TechnicalData extends PureComponent<Props, *> {
]
return (
<Modal
name={MODAL_TECHNICAL_DATA}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle data-e2e="modal_title_TechData">
{t('onboarding.analytics.technicalData.mandatoryContextual.title')}
</ModalTitle>
<Modal name={MODAL_TECHNICAL_DATA} centered>
<ModalBody
onClose={this.onClose}
title={t('onboarding.analytics.technicalData.mandatoryContextual.title')}
render={() => (
<Fragment>
<InlineDesc>{t('onboarding.analytics.technicalData.desc')}</InlineDesc>
<ModalContent mx={5}>
<Box mx={5}>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button onClick={onClose} primary data-e2e="modal_buttonClose_techData">
</Box>
</Fragment>
)}
renderFooter={() => (
<Fragment>
<Button onClick={this.onClose} primary data-e2e="modal_buttonClose_techData">
{t('common.close')}
</Button>
</ModalFooter>
</ModalBody>
</Fragment>
)}
/>
</Modal>
)
}
}
export default translate()(TechnicalData)
export default compose(
connect(
null,
mapDispatchToProps,
),
translate(),
)(TechnicalData)

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

@ -1,12 +1,13 @@
// @flow
/* eslint react/jsx-no-literals: 0 */
import React, { PureComponent, Fragment } from 'react'
import React, { PureComponent } from 'react'
import { translate, Trans } from 'react-i18next'
import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager'
import type { T } from 'types/common'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import Text from 'components/base/Text'
import Button from 'components/base/Button'
import GrowScroll from 'components/base/GrowScroll'
@ -17,6 +18,7 @@ import TrackPage from 'analytics/TrackPage'
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'
import Box from '../../base/Box/Box'
type Props = {
t: T,
@ -35,15 +37,16 @@ class DisclaimerModal extends PureComponent<Props, State> {
render(): React$Node {
const { status, firmware, onClose, t, goToNextStep } = this.props
return (
<Modal
isOpened={status === 'disclaimer'}
onClose={onClose}
render={({ onClose }) => (
<ModalBody onClose={onClose} grow align="center" justify="center" mt={3}>
<Modal isOpened={status === 'disclaimer'} onClose={onClose}>
<TrackPage category="Manager" name="DisclaimerModal" />
<Fragment>
<ModalTitle>{t('manager.firmware.update')}</ModalTitle>
<ModalContent>
<ModalBody
grow
align="center"
justify="center"
mt={3}
title={t('manager.firmware.update')}
render={() => (
<Box>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="manager.firmware.disclaimerTitle">
You are about to install
@ -58,26 +61,27 @@ class DisclaimerModal extends PureComponent<Props, State> {
{t('manager.firmware.disclaimerAppDelete')}
{t('manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
{firmware && firmware.osu ? (
<ModalContent relative pb={0} style={{ height: 250, width: '100%' }}>
<Box relative pb={0} style={{ height: 250, width: '100%' }}>
<GrowScroll pb={5}>
<Notes>
<Markdown>{firmware.osu.notes}</Markdown>
</Notes>
</GrowScroll>
<GradientBox />
</ModalContent>
</Box>
) : null}
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button primary onClick={goToNextStep}>
{t('common.continue')}
</Button>
</ModalFooter>
</Fragment>
</ModalBody>
</Box>
)}
/>
</Modal>
)
}
}

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

@ -107,6 +107,7 @@ class UpdateModal extends PureComponent<Props, State> {
return (
<Modal
onClose={onClose}
centered
onHide={this.handleReset}
isOpened={status === 'install'}
refocusWhenChange={stepId}

1
src/index.ejs

@ -75,6 +75,7 @@
<img class="logo" src="<%= __DEV__ ? '.' : '../static' %>/images/ledgerlive-logo.svg" alt="" />
</div>
<div id="app"></div>
<div id="modals"></div>
<script>
const { remote } = require('electron')
const { name } = remote.getCurrentWindow()

1
src/main/app.js

@ -84,6 +84,7 @@ const defaultWindowOptions = {
backgroundColor: '#fff',
webPreferences: {
blinkFeatures: 'OverlayScrollbars',
devTools,
// Enable, among other things, the ResizeObserver
experimentalFeatures: true,

1
src/reducers/modals.js

@ -44,7 +44,6 @@ const handlers = {
...state,
[name]: {
isOpened: false,
data: undefined,
},
}
},

8
static/i18n/en/app.json

@ -369,10 +369,10 @@
"modal": {
"button": "Done",
"title": "Scan to export to mobile",
"listTitle": "To import accounts on your Ledger Live Mobile app:",
"step1": "Tap the <1><0>+</0></1> button in Accounts",
"listTitle": "On the Ledger Live mobile app:",
"step1": "Tap the <1><0>+</0></1> button in <3><0>Accounts</0></3>",
"step2": "Tap <1><0>Import desktop accounts</0></1>",
"step3": "Scan until the loader hits 100%"
"step3": "Scan the <1><0>LiveQR code</0></1> until the loader hits 100%"
}
},
"display": {
@ -791,7 +791,7 @@
"description": "Please retry. Interacting with Ledger's API server went wrong."
},
"LedgerAPIErrorWithMessage": {
"title": "Oops, {{message}}",
"title": "{{message}}",
"description": "Please retry or contact Ledger Support"
},
"LedgerAPINotAvailable": {

82
yarn.lock

@ -1682,51 +1682,43 @@
resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-4.35.1.tgz#3f162dc05480e444083b6381bd098df187751633"
integrity sha512-2Bo3/NRKyz3ddR07TvZ87VpDJc8fz4+ONLJnhzC0mwIwu+Pxal6SgCBiGtv503oGxkgDuG5PtODZBaehWkGRnQ==
"@ledgerhq/hw-app-btc@^4.32.0":
version "4.32.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-4.32.0.tgz#e883dcaa3ebb4aca1e2cb27acfc47b8db4e85f3f"
integrity sha512-N/RxtkPVjTDwU+lDPQQE7+4YQMXaXStDrpufQbDn0NXoaJ8KgY+QGkOH6bkuwV+LQvc7rEaM7E3p7/t58KJpMg==
"@ledgerhq/hw-app-btc@^4.32.0", "@ledgerhq/hw-app-btc@^4.35.0":
version "4.35.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-4.35.0.tgz#aafd655c988da39f774b0a4706e7f8897222f414"
integrity sha512-oX9YcQAuU+rOJm/lE7YF5+JXNppHcUv23ZltGz5CbWHnhm7Tqo4MOR8N5oSnHKlHW+IawfWCPN5PqdF7RGyQ5w==
dependencies:
"@ledgerhq/hw-transport" "^4.32.0"
"@ledgerhq/hw-transport" "^4.35.0"
create-hash "^1.1.3"
"@ledgerhq/hw-app-btc@^4.34.0":
version "4.34.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-4.34.0.tgz#0bbc46afd29de04ac6a73582fbf9a09fcf5ed117"
integrity sha512-xR4rH8o8YRvyhnTvb8g89NAJQQqXJkApiFtCvduBamu5V+rDvhHYlFu2B+CU6g8lzLFACMDIqJqXbmwT80AGjw==
"@ledgerhq/hw-app-eth@^4.32.0", "@ledgerhq/hw-app-eth@^4.35.0":
version "4.35.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.35.0.tgz#3a8c1b0db87224a24ff5441a0e819122e5585f2d"
integrity sha512-MSDr8+CaoXhtm64ELuI/8wpcfmrMUjzGJgASY6bnjc82vAW+6sHNZlTU0zWRTZxqQUuZ8WpuJP159cf92MWq3g==
dependencies:
"@ledgerhq/hw-transport" "^4.32.0"
create-hash "^1.1.3"
"@ledgerhq/hw-transport" "^4.35.0"
"@ledgerhq/hw-app-eth@^4.32.0":
version "4.32.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.32.0.tgz#7d43ca2c7952f1fb726e02c3b4485be10af481a2"
integrity sha512-d22WinjcsqJNoZSI+6UpTWZ7hl+UhL2dFeVeliCwtBWSj40z6F25MpoviGxPsv0WC7IUjayw+a9jIRcOJ5kkIw==
"@ledgerhq/hw-app-xrp@^4.32.0", "@ledgerhq/hw-app-xrp@^4.35.0":
version "4.35.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-xrp/-/hw-app-xrp-4.35.0.tgz#f6aec06ae53f8732d90f745963a2de96c2ffa432"
integrity sha512-kQLdr9xrYvkFR9+QVyTNtmSGFDfrQ63ac0QhWKEoILiiQ0dxfZ7qCCp/qPJk/sx9H8dMX37X6y+xAnSU1frbfg==
dependencies:
"@ledgerhq/hw-transport" "^4.32.0"
"@ledgerhq/hw-app-xrp@^4.32.0":
version "4.32.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-xrp/-/hw-app-xrp-4.32.0.tgz#260daafa9de1073598ea91ddfeb168dc437edd50"
integrity sha512-MNmLAGUp7Bnj/mjg1Lo5bK1v+q/QPYw7RJAbI4Vl1A4Fsqj6oiZspnSK+BTHGp+CRJavCwumjKuf5y3X5Dp8cA==
dependencies:
"@ledgerhq/hw-transport" "^4.32.0"
"@ledgerhq/hw-transport" "^4.35.0"
bip32-path "0.4.2"
"@ledgerhq/hw-transport-node-hid@^4.32.0":
version "4.33.3"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.33.3.tgz#5e96dca2be0a23d80814303f262398087b208a6a"
integrity sha512-hmNAm7k385RJXY38hVUpzYgGgyk9QjScD3erNlFCTO8FnnxmEJCFUmVhWkv4sTwufuUJSpXL3ZXXNZ44qLMJpg==
"@ledgerhq/hw-transport-node-hid@^4.35.0":
version "4.35.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.35.0.tgz#0eba08e5edd14a8c779ebaf73ec21976ee5f112e"
integrity sha512-Otnymk9B7qCEfjych/SvTvJsMM+DqyoB0saEwL80ukjuGFqMunecrG5w8nC4aCc169IVz70Spkg2uU90TBUCuw==
dependencies:
"@ledgerhq/hw-transport" "^4.32.0"
"@ledgerhq/hw-transport" "^4.35.0"
lodash "^4.17.11"
node-hid "^0.7.2"
usb "^1.3.3"
"@ledgerhq/hw-transport@^4.21.0", "@ledgerhq/hw-transport@^4.32.0":
version "4.32.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.32.0.tgz#592b9dc51459cb1cd31ce9444cf943f627bc4beb"
integrity sha512-Wgsk9UHC4RShqYoDeIEeKgHZOvNCtB0WWIG0xqlVPzS+IcKDkIxtXQw7hTA7GQSuDuGeauVtlbTQ5yat6+2/BA==
"@ledgerhq/hw-transport@^4.21.0", "@ledgerhq/hw-transport@^4.32.0", "@ledgerhq/hw-transport@^4.35.0":
version "4.35.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.35.0.tgz#aa7b851111ed759cd7489fa07a7b34c1773e8314"
integrity sha512-o8ekdoCkHMvOByIKDmAMNDjm8Q5cu+sbqmebPtGrHAPbgIZBUbNA5UupY/Om+xypdxXYnuBw+MF8FyIVOjnIsg==
dependencies:
events "^3.0.0"
@ -2270,6 +2262,11 @@
text-table "^0.2.0"
webpack-log "^1.1.2"
"@yarnpkg/lockfile@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
abab@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@ -5265,6 +5262,11 @@ commander@2.6.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
integrity sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=
commander@^2.10.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
@ -13302,12 +13304,7 @@ react-inspector@^2.2.2:
babel-runtime "^6.26.0"
is-dom "^1.0.9"
react-is@^16.3.1, react-is@^16.4.1:
version "16.4.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
integrity sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==
react-is@^16.3.2:
react-is@^16.3.1, react-is@^16.3.2, react-is@^16.4.1:
version "16.5.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.5.2.tgz#e2a7b7c3f5d48062eb769fcb123505eb928722e3"
integrity sha512-hSl7E6l25GTjNEZATqZIuWOgSnpXb3kD0DVCujmg46K5zLxsbiKaaT6VO9slkSBDPZfYs30lwfJwbOFOnoEnKQ==
@ -17181,6 +17178,15 @@ yargs@~3.10.0:
decamelize "^1.0.0"
window-size "0.1.0"
yarn-deduplicate@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.1.1.tgz#19b4a87654b66f55bf3a4bd6b153b4e4ab1b6e6d"
integrity sha512-2FDJ1dFmtvqhRmfja89ohYzpaheCYg7BFBSyaUq+kxK0y61C9oHv1XaQovCWGJtP2WU8PksQOgzMVV7oQOobzw==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
commander "^2.10.0"
semver "^5.3.0"
yauzl@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"

Loading…
Cancel
Save