Browse Source

Merge pull request #1745 from juan-cortes/refactor-modal

LL-837 Initial version of the updated refactor of modals
develop
Gaëtan Renaudeau 6 years ago
committed by GitHub
parent
commit
4c09765c56
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/components/CurrentAddress/index.js
  2. 13
      src/components/GenuineCheckModal.js
  3. 4
      src/components/IsUnlocked.js
  4. 199
      src/components/ManagerPage/AppsList.js
  5. 3
      src/components/SettingsPage/CleanButton.js
  6. 67
      src/components/SettingsPage/DisablePasswordModal.js
  7. 8
      src/components/SettingsPage/PasswordAutoLockSelect.js
  8. 1
      src/components/SettingsPage/PasswordForm.js
  9. 49
      src/components/SettingsPage/PasswordModal.js
  10. 2
      src/components/SettingsPage/RepairDeviceButton.js
  11. 3
      src/components/SettingsPage/ResetButton.js
  12. 2
      src/components/SettingsPage/ResetFallbackModal.js
  13. 4
      src/components/SettingsPage/sections/Display.js
  14. 46
      src/components/SettingsPage/sections/Export.js
  15. 59
      src/components/base/Modal/ConfirmModal.js
  16. 143
      src/components/base/Modal/ModalBody.js
  17. 59
      src/components/base/Modal/ModalContent.js
  18. 18
      src/components/base/Modal/ModalFooter.js
  19. 93
      src/components/base/Modal/ModalHeader.js
  20. 75
      src/components/base/Modal/ModalTitle.js
  21. 95
      src/components/base/Modal/RepairModal.js
  22. 345
      src/components/base/Modal/index.js
  23. 75
      src/components/base/Modal/stories.js
  24. 44
      src/components/base/Stepper/index.js
  25. 86
      src/components/modals/AccountSettingRenderBody.js
  26. 2
      src/components/modals/AddAccounts/index.js
  27. 32
      src/components/modals/Debug.js
  28. 47
      src/components/modals/Disclaimer.js
  29. 215
      src/components/modals/OperationDetails.js
  30. 5
      src/components/modals/Receive/index.js
  31. 38
      src/components/modals/ReleaseNotes/ReleaseNotesBody.js
  32. 1
      src/components/modals/ReleaseNotes/index.js
  33. 1
      src/components/modals/Send/index.js
  34. 5
      src/components/modals/SettingsAccount.js
  35. 62
      src/components/modals/ShareAnalytics.js
  36. 59
      src/components/modals/TechnicalData.js
  37. 78
      src/components/modals/UpdateFirmware/Disclaimer.js
  38. 1
      src/components/modals/UpdateFirmware/index.js
  39. 1
      src/index.ejs
  40. 1
      src/main/app.js
  41. 1
      src/reducers/modals.js

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,

199
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,108 +184,103 @@ 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>
{mode === 'installing' ? (
<Box color="grey">
<Update size={30} />
</Box>
) : (
<Box color="grey">
<Trash size={30} />
</Box>
)}
</ModalTitle>
<ModalContent>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`manager.apps.${mode}`, { app })}
</Text>
<Box mt={6}>
<ProgressBar width={150} progress={progress} />
</Box>
</ModalContent>
</Fragment>
) : status === 'error' ? (
<Fragment>
<TrackPage
category="Manager"
name="Error Modal"
error={error && error.name}
app={app}
/>
<ModalContent grow align="center" justify="center" mt={5}>
<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={2}
fontSize={4}
ff="Open Sans"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="description" />
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
) : status === 'success' ? (
<Fragment>
<ModalContent grow align="center" justify="center" mt={5}>
<Box color="positiveGreen">
<CheckCircle size={44} />
</Box>
<Box
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
{t(
`manager.apps.${
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
}`,
{ app },
)}
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
) : null}
</ModalBody>
return ['busy', 'idle'].includes(status) ? (
<Box grow align="center" justify="center">
{mode === 'installing' ? (
<Box color="grey" grow align="center" mb={5}>
<Update size={30} />
</Box>
) : (
<Box color="grey" grow align="center" mb={5}>
<Trash size={30} />
</Box>
)}
/>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`manager.apps.${mode}`, { app })}
</Text>
<Box mt={6}>
<ProgressBar width={150} progress={progress} />
</Box>
</Box>
) : status === 'error' ? (
<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>
<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={2}
fontSize={4}
ff="Open Sans"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="description" />
</Box>
</Box>
</Box>
) : status === 'success' ? (
<Box grow align="center" justify="center" mt={5}>
<Box color="positiveGreen">
<CheckCircle size={44} />
</Box>
<Box
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
{t(`manager.apps.${mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'}`, {
app,
})}
</Box>
</Box>
) : null
}
renderFooter = () => {
const { t } = this.props
return (
<Box horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</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">

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}

67
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,37 +62,33 @@ 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 }) => (
<form onSubmit={this.disablePassword}>
<ModalBody onClose={onClose}>
<ModalTitle data-e2e="disablePassword_modalTitle">
{t('password.disablePassword.title')}
</ModalTitle>
<ModalContent>
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" px={4}>
{t('password.disablePassword.desc')}
<Box px={7} mt={4} flow={3}>
<Box flow={1}>
<Label htmlFor="password">
{t('password.inputFields.currentPassword.label')}
</Label>
<InputPassword
autoFocus
type="password"
id="password"
onChange={this.handleInputChange('currentPassword')}
value={currentPassword}
error={incorrectPassword}
/>
</Box>
<Modal {...props} centered onHide={this.handleReset} onClose={onClose}>
<form onSubmit={this.disablePassword}>
<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}>
<Box flow={1}>
<Label htmlFor="password">
{t('password.inputFields.currentPassword.label')}
</Label>
<InputPassword
autoFocus
type="password"
id="password"
onChange={this.handleInputChange('currentPassword')}
value={currentPassword}
error={incorrectPassword}
/>
</Box>
</Box>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
</Box>
)}
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}

49
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
{...props}
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>
<Modal isOpened={isOpened} centered>
<ModalBody
{...props}
title={hasPassword ? t('password.changePassword.title') : t('password.setPassword.title')}
onHide={this.handleReset}
onClose={onClose}
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>
)
}
}

2
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,

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>

46
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'
@ -108,27 +109,32 @@ class SectionExport extends PureComponent<Props, State> {
]
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('settings.export.modal.title')}</ModalTitle>
<ModalContent flow={2} justify="center" align="center">
<Box flow={2}>
<QRCodeExporter size={330} />
</Box>
<Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}>
<Text ff="Open Sans|SemiBold" color="dark">
{t('settings.export.modal.listTitle')}
</Text>
<ModalBody
onClose={onClose}
title={t('settings.export.modal.title')}
render={() => (
<Box justify="center" align="center">
<Box flow={2}>
<QRCodeExporter size={330} />
</Box>
<Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}>
<Text ff="Open Sans|SemiBold" color="dark">
{t('settings.export.modal.listTitle')}
</Text>
</Box>
<Box style={{ width: 330 }}>
{stepsImportMobile.map(step => <BulletRow key={step.key} step={step} />)}
</Box>
</Box>
<Box style={{ width: 330 }}>
{stepsImportMobile.map(step => <BulletRow key={step.key} step={step} />)}
)}
renderFooter={() => (
<Box>
<Button small onClick={onClose} primary>
{t('settings.export.modal.button')}
</Button>
</Box>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
<Button small onClick={onClose} primary>
{t('settings.export.modal.button')}
</Button>
</ModalFooter>
</ModalBody>
)}
/>
)
}

59
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}
preventBackdropClick={isLoading}
{...props}
render={({ onClose }) => (
<ModalBody onClose={!cancellable && isLoading ? undefined : onClose}>
<TrackPage category="Modal" name={analyticsName} />
<ModalTitle>{title}</ModalTitle>
<ModalContent>
<Modal isOpened={isOpened} centered={centered}>
<ModalBody
preventBackdropClick={isLoading}
{...props}
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>
)
}
}

143
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()
}
}
}
}
componentDidMount() {
setTimeout(() => {
window.requestAnimationFrame(() => {
this.setState({ isHidden: false })
})
}, 150)
_content = null
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"
>
{onClose && (
<CloseContainer onClick={onClose}>
<IconCross size={16} />
</CloseContainer>
)}
{(!isHidden || !deferHeight) && <Inner {...props}>{children}</Inner>}
</Body>
<Fragment>
<ModalHeader onBack={onBack} onClose={onClose}>
{title}
</ModalHeader>
<ModalContent
tabIndex={0}
ref={n => (this._content = n)}
onIsScrollableChange={this.animateGradient}
noScroll={noScroll}
>
{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};
}
&:active {
color: ${p => p.theme.colors.dark};
}
`
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 GRADIENT_STYLE = {
background: 'linear-gradient(rgba(255, 255, 255, 0), #ffffff)',
height: 40,
position: 'absolute',
bottom: 0,
left: 0,
right: 20,
}
const Inner = styled(Box)`
animation: ${appear} 80ms linear;
`
const GRADIENT_WRAPPER_STYLE = {
height: 0,
position: 'relative',
pointerEvents: 'none',
}
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)

95
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,38 +178,44 @@ 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>
{error ? (
<ErrorStep error={error} />
) : isLoading ? (
<FlashStep t={t} progress={progress} />
) : (
<DisclaimerStep desc={desc} />
)}
{!isLoading && !error ? (
<Box py={2} px={5}>
<Select
isSearchable={false}
isClearable={false}
value={selectedOption}
onChange={this.onChange}
autoFocus
options={forceRepairChoices}
renderOption={this.renderOption}
renderValue={this.renderValue}
/>
</Box>
) : null}
{!isLoading ? (
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
{error ? <Button onClick={onReject}>{t(`common.close`)}</Button> : null}
>
<TrackPage category="Modal" name={analyticsName} />
<ModalBody
title={title}
render={() => (
<Box>
{error ? (
<ErrorStep error={error} />
) : isLoading ? (
<FlashStep t={t} progress={progress} />
) : (
<DisclaimerStep desc={desc} />
)}
{!isLoading && !error ? (
<Box py={2} px={5}>
<Select
isSearchable={false}
isClearable={false}
value={selectedOption}
onChange={this.onChange}
autoFocus
options={forceRepairChoices}
renderOption={this.renderOption}
renderValue={this.renderValue}
/>
</Box>
) : 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>
)
}
}

345
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,
const animShowHide = {
duration: 200,
easing: Easing.bezier(0.3, 1.0, 0.5, 0.8),
}
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 domNode = process.env.STORYBOOK_ENV ? document.body : document.getElementById('modals')
type Props = OwnProps & {
isOpened?: boolean,
data?: any,
} & {
onClose?: () => void,
}
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
}
function stopPropagation(e) {
e.stopPropagation()
type State = {
animShowHide: Animated.Value,
isInDOM: boolean,
}
const wrap = EXPERIMENTAL_CENTER_MODAL
? children => (
<Box alignItems="center" justifyContent="center" grow>
{children}
</Box>
)
: children => (
<GrowScroll alignItems="center" full pt={8}>
{children}
</GrowScroll>
)
class Modal extends PureComponent<Props, State> {
state = {
animShowHide: new Animated.Value(0),
isInDOM: this.props.isOpened === true,
}
export class Modal extends Component<Props> {
static defaultProps = {
isOpened: false,
onHide: noop,
preventBackdropClick: false,
static getDerivedStateFromProps(nextProps: Props) {
const patch = {}
if (nextProps.isOpened) {
patch.isInDOM = true
}
return patch
}
shouldComponentUpdate(nextProps: Props) {
if (this.props.isOpened || nextProps.isOpened) {
return true
componentDidMount() {
if (this.props.isOpened) {
this.animateEnter()
}
return false
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 })
})
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()
}
}
_wrapper = null
_lastFocusedElement = null
componentWillUnmount() {
document.removeEventListener('keyup', this.handleKeyup)
}
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;
`
export const ModalContent = styled(Box).attrs({
px: 5,
pb: 5,
selectable: true,
})``
const BACKDROP_STYLE = {
pointerEvents: 'none',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.4)',
zIndex: 100,
}
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,

75
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>
)}
/>
))
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',
<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'}
/>
)}
onConfirm={action('onConfirm')}
onReject={action('onReject')}
/>
))

44
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,25 +91,27 @@ class Stepper extends PureComponent<Props, State> {
: !!shouldPreventClose
return (
<ModalBody onClose={preventClose ? undefined : onClose}>
<ModalTitle onBack={onBack ? () => onBack(stepProps) : undefined}>{title}</ModalTitle>
<ModalContent>
<Breadcrumb
mb={6}
currentStep={stepIndex}
items={steps}
stepsDisabled={disabledSteps}
stepsErrors={errorSteps}
/>
<StepComponent {...stepProps} />
{children}
</ModalContent>
{renderFooter && (
<ModalFooter horizontal align="center" justify="flex-end">
<StepFooter {...stepProps} />
</ModalFooter>
<ModalBody
refocusWhenChange={stepId}
onClose={preventClose ? undefined : onClose}
onBack={onBack ? () => onBack(stepProps) : undefined}
title={title}
noScroll={noScroll}
render={() => (
<Fragment>
<Breadcrumb
mb={6}
currentStep={stepIndex}
items={steps}
stepsDisabled={disabledSteps}
stepsErrors={errorSteps}
/>
<StepComponent {...stepProps} />
{children}
</Fragment>
)}
</ModalBody>
renderFooter={renderFooter ? () => <StepFooter {...stepProps} /> : undefined}
/>
)
}
}

86
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)}>
<TrackPage category="Modal" name="AccountSettings" />
<ModalTitle>{t('account.settings.title')}</ModalTitle>
<ModalContent mb={3}>
<ModalBody
onClose={onClose}
title={t('account.settings.title')}
render={() => (
<Fragment>
<TrackPage category="Modal" name="AccountSettings" />
<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,15 +274,33 @@ 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)}
/>
>
{JSON.stringify(usefulData, null, 2)}
</div>
</Spoiler>
</ModalContent>
<ModalFooter horizontal>
<ConfirmModal
analyticsName="RemoveAccount"
isDanger
isOpened={isRemoveAccountModalOpen}
onClose={this.handleCloseRemoveAccountModal}
onReject={this.handleCloseRemoveAccountModal}
onConfirm={() => this.handleRemoveAccount(account)}
title={t('settings.removeAccountModal.title')}
subTitle={t('common.areYouSure')}
desc={t('settings.removeAccountModal.desc')}
/>
<Space of={20} />
</Fragment>
)}
renderFooter={() => (
<Fragment>
<Button
event="OpenAccountDelete"
danger
@ -294,23 +309,12 @@ class HelperComp extends PureComponent<Props, State> {
>
{t('common.delete')}
</Button>
<Button event="DoneEditingAccount" ml="auto" type="submit" primary>
<Button event="DoneEditingAccount" ml="auto" onClick={onSubmit} primary>
{t('common.apply')}
</Button>
</ModalFooter>
</form>
<ConfirmModal
analyticsName="RemoveAccount"
isDanger
isOpened={isRemoveAccountModalOpen}
onClose={this.handleCloseRemoveAccountModal}
onReject={this.handleCloseRemoveAccountModal}
onConfirm={() => this.handleRemoveAccount(account)}
title={t('settings.removeAccountModal.title')}
subTitle={t('common.areYouSure')}
desc={t('settings.removeAccountModal.desc')}
/>
</ModalBody>
</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 })}

32
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}>
<SyncSkipUnderPriority priority={99999999} />
<ModalTitle>developer internal tools</ModalTitle>
<ModalContent>
<Modal name="MODAL_DEBUG" centered onHide={this.onHide}>
<ModalBody
title="developer internal tools"
render={() => (
<Box>
<SyncSkipUnderPriority priority={99999999} />
<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>
)
}
}

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

215
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,122 +128,120 @@ 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}>
<Box flow={3}>
<Box alignItems="center" mt={1}>
<ConfirmationCheck
marketColor={marketColor}
isConfirmed={isConfirmed}
style={{
transform: 'scale(1.5)',
}}
t={t}
type={type}
withTooltip={false}
/>
<Box my={4} alignItems="center">
<Box>
<FormattedVal
color={amount.isNegative() ? 'smoke' : undefined}
unit={unit}
alwaysShowSign
showCode
val={amount}
fontSize={7}
disableRounding
/>
</Box>
<Box mt={1}>
<CounterValue
color="grey"
fontSize={5}
date={date}
currency={currency}
value={amount}
/>
</Box>
</Box>
</Box>
<Box horizontal flow={2}>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.account')}</OpDetailsTitle>
<OpDetailsData>{name}</OpDetailsData>
<ModalBody
title={t('operationDetails.title')}
onClose={onClose}
render={() => (
<Box flow={3}>
<Box alignItems="center" mt={1}>
<ConfirmationCheck
marketColor={marketColor}
isConfirmed={isConfirmed}
style={{
transform: 'scale(1.5)',
}}
t={t}
type={type}
withTooltip={false}
/>
<Box my={4} alignItems="center">
<Box>
<FormattedVal
color={amount.isNegative() ? 'smoke' : undefined}
unit={unit}
alwaysShowSign
showCode
val={amount}
fontSize={7}
disableRounding
/>
</Box>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle>
<OpDetailsData>{moment(date).format('LLL')}</OpDetailsData>
<Box mt={1}>
<CounterValue
color="grey"
fontSize={5}
date={date}
currency={currency}
value={amount}
/>
</Box>
</Box>
<B />
<Box horizontal flow={2}>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.fees')}</OpDetailsTitle>
{fee ? (
<Fragment>
<OpDetailsData>
<FormattedVal unit={unit} showCode val={fee} color="smoke" />
</OpDetailsData>
</Fragment>
) : (
<OpDetailsData>{t('operationDetails.noFees')}</OpDetailsData>
)}
</Box>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.status')}</OpDetailsTitle>
<OpDetailsData color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}>
<Box>
{isConfirmed
? t('operationDetails.confirmed')
: t('operationDetails.notConfirmed')}
</Box>
<Box>{`(${confirmations})`}</Box>
</OpDetailsData>
</Box>
</Box>
<Box horizontal flow={2}>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.account')}</OpDetailsTitle>
<OpDetailsData>{name}</OpDetailsData>
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle>
<OpDetailsData>
<Ellipsis canSelect>{hash}</Ellipsis>
<GradientHover>
<CopyWithFeedback text={hash} />
</GradientHover>
</OpDetailsData>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle>
<OpDetailsData>{moment(date).format('LLL')}</OpDetailsData>
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.from')}</OpDetailsTitle>
<DataList lines={uniqueSenders} t={t} />
</Box>
<B />
<Box horizontal flow={2}>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.fees')}</OpDetailsTitle>
{fee ? (
<Fragment>
<OpDetailsData>
<FormattedVal unit={unit} showCode val={fee} color="smoke" />
</OpDetailsData>
</Fragment>
) : (
<OpDetailsData>{t('operationDetails.noFees')}</OpDetailsData>
)}
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle>
<DataList lines={recipients} t={t} />
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.status')}</OpDetailsTitle>
<OpDetailsData color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}>
<Box>
{isConfirmed
? t('operationDetails.confirmed')
: t('operationDetails.notConfirmed')}
</Box>
<Box>{`(${confirmations})`}</Box>
</OpDetailsData>
</Box>
{Object.entries(extra).map(([key, value]) => (
<Box key={key}>
<OpDetailsTitle>
<Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} />
</OpDetailsTitle>
<OpDetailsData>{value}</OpDetailsData>
</Box>
))}
</Box>
</GrowScroll>
<GradientBox />
</ModalContent>
{url && (
<ModalFooter horizontal justify="flex-end" flow={2}>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle>
<OpDetailsData>
<Ellipsis canSelect>{hash}</Ellipsis>
<GradientHover>
<CopyWithFeedback text={hash} />
</GradientHover>
</OpDetailsData>
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.from')}</OpDetailsTitle>
<DataList lines={uniqueSenders} t={t} />
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle>
<DataList lines={recipients} t={t} />
</Box>
{Object.entries(extra).map(([key, value]) => (
<Box key={key}>
<OpDetailsTitle>
<Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} />
</OpDetailsTitle>
<OpDetailsData>{value}</OpDetailsData>
</Box>
))}
</Box>
)}
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}

38
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}>
<TrackPage category="Modal" name="ReleaseNotes" />
<ModalTitle>{t('releaseNotes.title')}</ModalTitle>
<ModalContent relative style={{ height: 500 }} px={0} pb={0}>
<GrowScroll px={5} pb={8}>
{this.renderContent()}
</GrowScroll>
<GradientBox />
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button onClick={onClose} primary>
{t('common.continue')}
</Button>
</ModalFooter>
</ModalBody>
<ModalBody
onClose={onClose}
title={t('releaseNotes.title')}
render={() => (
<Box relative style={{ height: 500 }} px={0} pb={0}>
<TrackPage category="Modal" name="ReleaseNotes" />
<Box px={5} pb={8}>
{this.renderContent()}
</Box>
<GradientBox />
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button onClick={onClose} primary>
{t('common.continue')}
</Button>
</Box>
)}
/>
)
}
}

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

@ -9,6 +9,7 @@ import ReleaseNotesBody from './ReleaseNotesBody'
const ReleaseNotesModal = () => (
<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} />
)}
/>
)
}

62
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>
<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 data-e2e="modal_buttonClose_shareAnalytics">
<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>
<Box mx={5}>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
</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',
})``

59
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>
<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 data-e2e="modal_buttonClose_techData">
<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>
<Box mx={5}>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
</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)

78
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,49 +37,51 @@ 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}>
<TrackPage category="Manager" name="DisclaimerModal" />
<Fragment>
<ModalTitle>{t('manager.firmware.update')}</ModalTitle>
<ModalContent>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="manager.firmware.disclaimerTitle">
You are about to install
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware version ${
firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : ''
}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('manager.firmware.disclaimerAppDelete')}
{t('manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<Modal isOpened={status === 'disclaimer'} onClose={onClose}>
<TrackPage category="Manager" name="DisclaimerModal" />
<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
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware version ${
firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : ''
}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('manager.firmware.disclaimerAppDelete')}
{t('manager.firmware.disclaimerAppReinstall')}
</Text>
{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%' }}>
<Button primary onClick={goToNextStep}>
{t('common.continue')}
</Button>
</ModalFooter>
</Fragment>
</ModalBody>
)}
/>
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button primary onClick={goToNextStep}>
{t('common.continue')}
</Button>
</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,
},
}
},

Loading…
Cancel
Save