Gaëtan Renaudeau
6 years ago
committed by
GitHub
142 changed files with 3365 additions and 1789 deletions
@ -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 |
@ -0,0 +1,51 @@ |
|||||
|
#!/bin/env bash |
||||
|
|
||||
|
# Fetch release binaries for all platforms |
||||
|
# and produce a .sha512sum file in the current folder |
||||
|
|
||||
|
# exit on error |
||||
|
set -e |
||||
|
|
||||
|
[[ "$GH_TOKEN" == "" ]] && echo "GH_TOKEN is unset" && exit 1 |
||||
|
|
||||
|
function main { |
||||
|
ASSETS_FILTER="(AppImage|zip|exe)" |
||||
|
PKG_VER=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g') |
||||
|
OUTPUT_FILE="ledger-live-desktop-$PKG_VER.sha512sum" |
||||
|
|
||||
|
read -p "> release version ($PKG_VER): " -r RELEASE_VERSION |
||||
|
RELEASE_VERSION=${RELEASE_VERSION:-$PKG_VER} |
||||
|
|
||||
|
RELEASES=$(do_request "/repos/LedgerHQ/ledger-live-desktop/releases") |
||||
|
printf """ |
||||
|
console.log( |
||||
|
(%s).find(r => r.tag_name === 'v%s').assets |
||||
|
.filter(a => a.name.match(/\\.%s$/)) |
||||
|
.map(a => a.browser_download_url) |
||||
|
.join('\\\n') |
||||
|
) |
||||
|
""" "$RELEASES" "$RELEASE_VERSION" "$ASSETS_FILTER" >"$TMP_FILE1" |
||||
|
node "$TMP_FILE1" | tee "$TMP_FILE2" |
||||
|
|
||||
|
pushd "$TMP_DIR" >/dev/null |
||||
|
while IFS= read -r line ; do |
||||
|
curl -L -O "$line" |
||||
|
done < "$TMP_FILE2" |
||||
|
sha512sum -- * > "$OLDPWD/$OUTPUT_FILE" |
||||
|
popd >/dev/null |
||||
|
} |
||||
|
|
||||
|
TMP_DIR=$(mktemp -d) |
||||
|
TMP_FILE1=$(mktemp) |
||||
|
TMP_FILE2=$(mktemp) |
||||
|
|
||||
|
function cleanup { |
||||
|
rm -rf "$TMP_FILE1" "$TMP_FILE2" "$TMP_DIR" |
||||
|
} |
||||
|
|
||||
|
function do_request { |
||||
|
curl -H "Authorization: token $GH_TOKEN" "https://api.github.com$1" |
||||
|
} |
||||
|
|
||||
|
trap cleanup EXIT |
||||
|
main |
@ -1,91 +0,0 @@ |
|||||
// @flow
|
|
||||
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' |
|
||||
import { BigNumber } from 'bignumber.js' |
|
||||
import { LedgerAPINotAvailable } from '@ledgerhq/errors' |
|
||||
import network from './network' |
|
||||
import { blockchainBaseURL } from './Ledger' |
|
||||
|
|
||||
export type Block = { height: number } // TODO more fields actually
|
|
||||
export type Tx = { |
|
||||
hash: string, |
|
||||
received_at: string, |
|
||||
nonce: string, |
|
||||
value: number, |
|
||||
gas: number, |
|
||||
gas_price: number, |
|
||||
cumulative_gas_used: number, |
|
||||
gas_used: number, |
|
||||
from: string, |
|
||||
to: string, |
|
||||
input: string, |
|
||||
index: number, |
|
||||
block?: { |
|
||||
hash: string, |
|
||||
height: number, |
|
||||
time: string, |
|
||||
}, |
|
||||
confirmations: number, |
|
||||
} |
|
||||
|
|
||||
export type API = { |
|
||||
getTransactions: ( |
|
||||
address: string, |
|
||||
blockHash: ?string, |
|
||||
) => Promise<{ |
|
||||
truncated: boolean, |
|
||||
txs: Tx[], |
|
||||
}>, |
|
||||
getCurrentBlock: () => Promise<Block>, |
|
||||
getAccountNonce: (address: string) => Promise<number>, |
|
||||
broadcastTransaction: (signedTransaction: string) => Promise<string>, |
|
||||
getAccountBalance: (address: string) => Promise<BigNumber>, |
|
||||
} |
|
||||
|
|
||||
export const apiForCurrency = (currency: CryptoCurrency): API => { |
|
||||
const baseURL = blockchainBaseURL(currency) |
|
||||
if (!baseURL) { |
|
||||
throw new LedgerAPINotAvailable(`LedgerAPINotAvailable ${currency.id}`, { |
|
||||
currencyName: currency.name, |
|
||||
}) |
|
||||
} |
|
||||
return { |
|
||||
async getTransactions(address, blockHash) { |
|
||||
const { data } = await network({ |
|
||||
method: 'GET', |
|
||||
url: `${baseURL}/addresses/${address}/transactions`, |
|
||||
params: { blockHash, noToken: 1 }, |
|
||||
}) |
|
||||
return data |
|
||||
}, |
|
||||
async getCurrentBlock() { |
|
||||
const { data } = await network({ |
|
||||
method: 'GET', |
|
||||
url: `${baseURL}/blocks/current`, |
|
||||
}) |
|
||||
return data |
|
||||
}, |
|
||||
async getAccountNonce(address) { |
|
||||
const { data } = await network({ |
|
||||
method: 'GET', |
|
||||
url: `${baseURL}/addresses/${address}/nonce`, |
|
||||
}) |
|
||||
return data[0].nonce |
|
||||
}, |
|
||||
async broadcastTransaction(tx) { |
|
||||
const { data } = await network({ |
|
||||
method: 'POST', |
|
||||
url: `${baseURL}/transactions/send`, |
|
||||
data: { tx }, |
|
||||
}) |
|
||||
return data.result |
|
||||
}, |
|
||||
async getAccountBalance(address) { |
|
||||
const { data } = await network({ |
|
||||
method: 'GET', |
|
||||
url: `${baseURL}/addresses/${address}/balance`, |
|
||||
}) |
|
||||
// FIXME precision lost here. nothing we can do easily
|
|
||||
return BigNumber(data[0].balance) |
|
||||
}, |
|
||||
} |
|
||||
} |
|
@ -1,31 +0,0 @@ |
|||||
// @flow
|
|
||||
import invariant from 'invariant' |
|
||||
import LRU from 'lru-cache' |
|
||||
import type { Currency } from '@ledgerhq/live-common/lib/types' |
|
||||
import { FeeEstimationFailed } from '@ledgerhq/errors' |
|
||||
import { blockchainBaseURL } from './Ledger' |
|
||||
import network from './network' |
|
||||
|
|
||||
export type Fees = { |
|
||||
[_: string]: number, |
|
||||
} |
|
||||
|
|
||||
const cache = LRU({ |
|
||||
maxAge: 5 * 60 * 1000, |
|
||||
}) |
|
||||
|
|
||||
export const getEstimatedFees = async (currency: Currency): Promise<Fees> => { |
|
||||
const key = currency.id |
|
||||
let promise = cache.get(key) |
|
||||
if (promise) return promise.then(r => r.data) |
|
||||
const baseURL = blockchainBaseURL(currency) |
|
||||
invariant(baseURL, `Fees for ${currency.id} are not supported`) |
|
||||
promise = network({ method: 'GET', url: `${baseURL}/fees` }) |
|
||||
cache.set(key, promise) |
|
||||
const { data, status } = await promise |
|
||||
if (status < 200 || status >= 300) cache.del(key) |
|
||||
if (data) { |
|
||||
return data |
|
||||
} |
|
||||
throw new FeeEstimationFailed(`FeeEstimationFailed ${status}`, { httpStatus: status }) |
|
||||
} |
|
@ -1,6 +0,0 @@ |
|||||
// @flow
|
|
||||
import type { Currency } from '@ledgerhq/live-common/lib/types' |
|
||||
import { LEDGER_REST_API_BASE } from 'config/constants' |
|
||||
|
|
||||
export const blockchainBaseURL = ({ ledgerExplorerId }: Currency): ?string => |
|
||||
ledgerExplorerId ? `${LEDGER_REST_API_BASE}/blockchain/v2/${ledgerExplorerId}` : null |
|
@ -1,47 +0,0 @@ |
|||||
// @flow
|
|
||||
import logger from 'logger' |
|
||||
import { BigNumber } from 'bignumber.js' |
|
||||
import { RippleAPI } from 'ripple-lib' |
|
||||
import { |
|
||||
parseCurrencyUnit, |
|
||||
getCryptoCurrencyById, |
|
||||
formatCurrencyUnit, |
|
||||
} from '@ledgerhq/live-common/lib/currencies' |
|
||||
|
|
||||
const rippleUnit = getCryptoCurrencyById('ripple').units[0] |
|
||||
|
|
||||
export const defaultEndpoint = 'wss://s2.ripple.com' |
|
||||
|
|
||||
export const apiForEndpointConfig = (endpointConfig: ?string = null) => { |
|
||||
const server = endpointConfig || defaultEndpoint |
|
||||
const api = new RippleAPI({ server }) |
|
||||
api.on('error', (errorCode, errorMessage) => { |
|
||||
logger.warn(`Ripple API error: ${errorCode}: ${errorMessage}`) |
|
||||
}) |
|
||||
return api |
|
||||
} |
|
||||
|
|
||||
export const parseAPIValue = (value: string) => parseCurrencyUnit(rippleUnit, value) |
|
||||
|
|
||||
export const parseAPICurrencyObject = ({ |
|
||||
currency, |
|
||||
value, |
|
||||
}: { |
|
||||
currency: string, |
|
||||
value: string, |
|
||||
}) => { |
|
||||
if (currency !== 'XRP') { |
|
||||
logger.warn(`RippleJS: attempt to parse unknown currency ${currency}`) |
|
||||
return BigNumber(0) |
|
||||
} |
|
||||
return parseAPIValue(value) |
|
||||
} |
|
||||
|
|
||||
export const formatAPICurrencyXRP = (amount: BigNumber) => { |
|
||||
const value = formatCurrencyUnit(rippleUnit, amount, { |
|
||||
showAllDigits: true, |
|
||||
disableRounding: true, |
|
||||
useGrouping: false, |
|
||||
}) |
|
||||
return { currency: 'XRP', value } |
|
||||
} |
|
Binary file not shown.
@ -0,0 +1,59 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { Observable } from 'rxjs' |
||||
|
|
||||
|
// 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 = {} |
||||
|
type Result = { |
||||
|
status: UpdateStatus, |
||||
|
payload?: *, |
||||
|
} |
||||
|
|
||||
|
const cmd: Command<Input, Result> = createCommand('main:autoUpdate', () => |
||||
|
Observable.create(o => { |
||||
|
const { autoUpdater } = require('electron-updater') |
||||
|
|
||||
|
const sendStatus = (status, payload) => { |
||||
|
o.next({ status, payload }) |
||||
|
} |
||||
|
|
||||
|
const handleDownload = async _ => { |
||||
|
try { |
||||
|
sendStatus('checking') |
||||
|
// const appUpdater = await createElectronAppUpdater({
|
||||
|
// feedURL: UPDATE_CHECK_FEED,
|
||||
|
// updateVersion: info.version,
|
||||
|
// })
|
||||
|
// await appUpdater.verify()
|
||||
|
sendStatus('check-success') |
||||
|
} catch (err) { |
||||
|
// don't throw if the check fail for now. it's a white bullet.
|
||||
|
if (UPDATE_CHECK_IGNORE) { |
||||
|
// TODO: track the error
|
||||
|
sendStatus('check-success') |
||||
|
} else { |
||||
|
o.error(err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
autoUpdater.on('checking-for-update', () => sendStatus('checking-for-update')) |
||||
|
autoUpdater.on('update-available', info => sendStatus('update-available', info)) |
||||
|
autoUpdater.on('update-not-available', info => sendStatus('update-not-available', info)) |
||||
|
autoUpdater.on('download-progress', p => sendStatus('download-progress', p)) |
||||
|
autoUpdater.on('update-downloaded', handleDownload) |
||||
|
autoUpdater.on('error', err => o.error(err)) |
||||
|
|
||||
|
autoUpdater.autoInstallOnAppQuit = false |
||||
|
autoUpdater.checkForUpdates() |
||||
|
|
||||
|
return () => {} |
||||
|
}), |
||||
|
) |
||||
|
|
||||
|
export default cmd |
@ -0,0 +1,30 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { Observable } from 'rxjs' |
||||
|
|
||||
|
type Input = void |
||||
|
type Result = void |
||||
|
|
||||
|
const cmd: Command<Input, Result> = createCommand('main:quitAndInstallElectronUpdate', () => |
||||
|
Observable.create(o => { |
||||
|
const { app, BrowserWindow } = require('electron') |
||||
|
const { autoUpdater } = require('electron-updater') |
||||
|
const browserWindows = BrowserWindow.getAllWindows() |
||||
|
|
||||
|
// Fixes quitAndInstall not quitting on macOS, as suggested on
|
||||
|
// https://github.com/electron-userland/electron-builder/issues/1604#issuecomment-306709572
|
||||
|
app.removeAllListeners('window-all-closed') |
||||
|
browserWindows.forEach(browserWindow => { |
||||
|
browserWindow.removeAllListeners('close') |
||||
|
}) |
||||
|
|
||||
|
// couldn't find a way to catch if fail ¯\_(ツ)_/¯
|
||||
|
autoUpdater.quitAndInstall(false) |
||||
|
|
||||
|
o.complete() |
||||
|
return () => {} |
||||
|
}), |
||||
|
) |
||||
|
|
||||
|
export default cmd |
@ -0,0 +1,94 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import WebSocket from 'ws' |
||||
|
import IP from 'ip' |
||||
|
import { createStructuredSelector } from 'reselect' |
||||
|
import { activeAccountsSelector } from 'reducers/accounts' |
||||
|
import { exportSettingsSelector } from 'reducers/settings' |
||||
|
import { encode } from '@ledgerhq/live-common/lib/cross' |
||||
|
import connect from 'react-redux/es/connect/connect' |
||||
|
import Button from '../base/Button' |
||||
|
import QRCode from '../base/QRCode' |
||||
|
|
||||
|
type Props = { |
||||
|
accounts: *, |
||||
|
settings: *, |
||||
|
} |
||||
|
|
||||
|
type State = { |
||||
|
active: boolean, |
||||
|
} |
||||
|
|
||||
|
const mapStateToProps = createStructuredSelector({ |
||||
|
accounts: activeAccountsSelector, |
||||
|
settings: exportSettingsSelector, |
||||
|
}) |
||||
|
|
||||
|
class SocketExport extends PureComponent<Props, State> { |
||||
|
state = { |
||||
|
active: false, |
||||
|
} |
||||
|
|
||||
|
componentWillMount() { |
||||
|
this.resetServer() |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
if (!this.state.active) return |
||||
|
if (!this.server) { |
||||
|
this.resetServer() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
if (this.server) this.server.close() |
||||
|
} |
||||
|
|
||||
|
resetServer = () => { |
||||
|
this.server = new WebSocket.Server({ port: 1234 }) |
||||
|
|
||||
|
const { accounts, settings } = this.props |
||||
|
|
||||
|
const data = encode({ |
||||
|
accounts, |
||||
|
settings, |
||||
|
exporterName: 'desktop', |
||||
|
exporterVersion: __APP_VERSION__, |
||||
|
}) |
||||
|
|
||||
|
// Secret handshake to avoid intruders
|
||||
|
this.secret = Math.random() |
||||
|
.toString(36) |
||||
|
.slice(2) |
||||
|
|
||||
|
if (this.server) { |
||||
|
this.server.on('connection', ws => { |
||||
|
ws.on('message', message => { |
||||
|
if (message === this.secret) { |
||||
|
ws.send(data) |
||||
|
ws.close() |
||||
|
this.setState({ active: false }) |
||||
|
this.server = undefined |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
secret: string |
||||
|
server: * |
||||
|
canvas = React.createRef() |
||||
|
|
||||
|
render() { |
||||
|
return this.state.active ? ( |
||||
|
<QRCode size={50} data={`${this.secret}~${IP.address()}`} /> |
||||
|
) : ( |
||||
|
<Button primary small onClick={() => this.setState({ active: true })}> |
||||
|
{'Generate Code'} |
||||
|
</Button> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(SocketExport) |
@ -0,0 +1,130 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import styled from 'styled-components' |
||||
|
import { connect } from 'react-redux' |
||||
|
import Box from 'components/base/Box' |
||||
|
import { radii } from 'styles/theme' |
||||
|
import IconCross from 'icons/Cross' |
||||
|
import { createStructuredSelector } from 'reselect' |
||||
|
import { dismissBanner } from '../actions/settings' |
||||
|
import { dismissedBannersSelector } from '../reducers/settings' |
||||
|
|
||||
|
export type Content = { |
||||
|
Icon?: React$ComponentType<*>, |
||||
|
message: React$Node, |
||||
|
right?: React$Node, |
||||
|
} |
||||
|
|
||||
|
type Props = { |
||||
|
content?: Content, |
||||
|
status: string, |
||||
|
dismissable: boolean, |
||||
|
bannerId?: string, |
||||
|
dismissedBanners: string[], |
||||
|
dismissBanner: string => void, |
||||
|
} |
||||
|
|
||||
|
const mapStateToProps = createStructuredSelector({ |
||||
|
dismissedBanners: dismissedBannersSelector, |
||||
|
}) |
||||
|
|
||||
|
const mapDispatchToProps = { |
||||
|
dismissBanner, |
||||
|
} |
||||
|
|
||||
|
class TopBanner extends PureComponent<Props> { |
||||
|
static defaultProps = { |
||||
|
status: '', |
||||
|
dismissable: false, |
||||
|
} |
||||
|
|
||||
|
onDismiss = () => { |
||||
|
const { bannerId, dismissBanner } = this.props |
||||
|
|
||||
|
if (bannerId) { |
||||
|
dismissBanner(bannerId) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { dismissedBanners, bannerId, dismissable, content, status } = this.props |
||||
|
|
||||
|
if (!content || (bannerId && dismissedBanners.includes(bannerId))) return null |
||||
|
|
||||
|
const { Icon, message, right } = content |
||||
|
|
||||
|
return ( |
||||
|
<Container status={status}> |
||||
|
{Icon && ( |
||||
|
<IconContainer> |
||||
|
<Icon size={16} /> |
||||
|
</IconContainer> |
||||
|
)} |
||||
|
{message} |
||||
|
<RightContainer>{right}</RightContainer> |
||||
|
{dismissable && ( |
||||
|
<CloseContainer onClick={this.onDismiss}> |
||||
|
<IconCross size={14} /> |
||||
|
</CloseContainer> |
||||
|
)} |
||||
|
</Container> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect( |
||||
|
mapStateToProps, |
||||
|
mapDispatchToProps, |
||||
|
)(TopBanner) |
||||
|
|
||||
|
const IconContainer = styled.div` |
||||
|
margin-right: 15px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
` |
||||
|
|
||||
|
const colorForStatus = { |
||||
|
error: 'alertRed', |
||||
|
dark: '#142533', |
||||
|
} |
||||
|
|
||||
|
const Container = styled(Box).attrs({ |
||||
|
horizontal: true, |
||||
|
align: 'center', |
||||
|
py: '8px', |
||||
|
px: 3, |
||||
|
bg: p => colorForStatus[p.status] || 'wallet', |
||||
|
color: 'white', |
||||
|
mt: -20, |
||||
|
mb: 20, |
||||
|
fontSize: 4, |
||||
|
ff: 'Open Sans|SemiBold', |
||||
|
})` |
||||
|
border-radius: ${radii[1]}px; |
||||
|
` |
||||
|
|
||||
|
const RightContainer = styled.div` |
||||
|
margin-left: auto; |
||||
|
` |
||||
|
|
||||
|
export const FakeLink = styled.span` |
||||
|
color: white; |
||||
|
text-decoration: underline; |
||||
|
cursor: pointer; |
||||
|
` |
||||
|
|
||||
|
const CloseContainer = styled(Box).attrs({ |
||||
|
color: 'white', |
||||
|
})` |
||||
|
z-index: 1; |
||||
|
margin-left: 10px; |
||||
|
cursor: pointer; |
||||
|
&:hover { |
||||
|
color: #eee; |
||||
|
} |
||||
|
|
||||
|
&:active { |
||||
|
color: #eee; |
||||
|
} |
||||
|
` |
@ -1,103 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React, { PureComponent } from 'react' |
|
||||
import { translate } from 'react-i18next' |
|
||||
import { compose } from 'redux' |
|
||||
import { connect } from 'react-redux' |
|
||||
import styled from 'styled-components' |
|
||||
|
|
||||
import { getUpdateStatus, getUpdateData } from 'reducers/update' |
|
||||
import { sendEvent } from 'renderer/events' |
|
||||
import type { State } from 'reducers' |
|
||||
import type { UpdateStatus } from 'reducers/update' |
|
||||
|
|
||||
import { radii } from 'styles/theme' |
|
||||
|
|
||||
import Box from 'components/base/Box' |
|
||||
import Text from 'components/base/Text' |
|
||||
|
|
||||
import UpdateIcon from 'icons/Update' |
|
||||
|
|
||||
import type { T } from 'types/common' |
|
||||
|
|
||||
type Props = { |
|
||||
t: T, |
|
||||
updateStatus: UpdateStatus, |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = (state: State) => ({ |
|
||||
updateStatus: getUpdateStatus(state), |
|
||||
updateData: getUpdateData(state), |
|
||||
}) |
|
||||
|
|
||||
const Container = styled(Box).attrs({ |
|
||||
py: '8px', |
|
||||
px: 3, |
|
||||
bg: 'wallet', |
|
||||
color: 'white', |
|
||||
mt: '-50px', |
|
||||
mb: '35px', |
|
||||
style: p => ({ |
|
||||
transform: `translate3d(0, ${p.offset}%, 0)`, |
|
||||
}), |
|
||||
})` |
|
||||
border-radius: ${radii[1]}px; |
|
||||
` |
|
||||
|
|
||||
const NotifText = styled(Text).attrs({ |
|
||||
ff: 'Open Sans|SemiBold', |
|
||||
fontSize: 4, |
|
||||
})`` |
|
||||
|
|
||||
class UpdateDownloaded extends PureComponent<Props> { |
|
||||
renderStatus() { |
|
||||
const { updateStatus, t } = this.props |
|
||||
switch (updateStatus) { |
|
||||
case 'idle': |
|
||||
case 'checking': |
|
||||
case 'unavailable': |
|
||||
case 'error': |
|
||||
case 'available': |
|
||||
case 'progress': |
|
||||
return null |
|
||||
case 'downloaded': |
|
||||
return ( |
|
||||
<Box horizontal flow={3}> |
|
||||
<UpdateIcon size={16} /> |
|
||||
<Box grow> |
|
||||
<NotifText>{t('update.newVersionReady')}</NotifText> |
|
||||
</Box> |
|
||||
<Box> |
|
||||
<NotifText |
|
||||
style={{ cursor: 'pointer', textDecoration: 'underline' }} |
|
||||
onClick={() => sendEvent('updater', 'quitAndInstall')} |
|
||||
> |
|
||||
{t('update.relaunch')} |
|
||||
</NotifText> |
|
||||
</Box> |
|
||||
</Box> |
|
||||
) |
|
||||
default: |
|
||||
return null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { updateStatus, ...props } = this.props |
|
||||
|
|
||||
const isToggled = updateStatus === 'downloaded' |
|
||||
|
|
||||
if (!isToggled) { |
|
||||
return null |
|
||||
} |
|
||||
return <Container {...props}>{this.renderStatus()}</Container> |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default compose( |
|
||||
connect( |
|
||||
mapStateToProps, |
|
||||
null, |
|
||||
), |
|
||||
translate(), |
|
||||
)(UpdateDownloaded) |
|
@ -1,48 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import { PureComponent } from 'react' |
|
||||
import { connect } from 'react-redux' |
|
||||
import semver from 'semver' |
|
||||
|
|
||||
import { openModal } from 'reducers/modals' |
|
||||
import { lastUsedVersionSelector } from 'reducers/settings' |
|
||||
import { saveSettings } from 'actions/settings' |
|
||||
import { MODAL_RELEASES_NOTES } from 'config/constants' |
|
||||
|
|
||||
import type { State } from 'reducers' |
|
||||
|
|
||||
type Props = { |
|
||||
openModal: Function, |
|
||||
saveSettings: Function, |
|
||||
lastUsedVersion: string, |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = (state: State) => ({ |
|
||||
lastUsedVersion: lastUsedVersionSelector(state), |
|
||||
}) |
|
||||
|
|
||||
const mapDispatchToProps = { |
|
||||
openModal, |
|
||||
saveSettings, |
|
||||
} |
|
||||
|
|
||||
class UpdateInstalled extends PureComponent<Props> { |
|
||||
componentDidMount() { |
|
||||
const { lastUsedVersion, saveSettings, openModal } = this.props |
|
||||
const currentVersion = __APP_VERSION__ |
|
||||
|
|
||||
if (semver.gt(currentVersion, lastUsedVersion)) { |
|
||||
openModal(MODAL_RELEASES_NOTES, currentVersion) |
|
||||
saveSettings({ lastUsedVersion: currentVersion }) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default connect( |
|
||||
mapStateToProps, |
|
||||
mapDispatchToProps, |
|
||||
)(UpdateInstalled) |
|
@ -1,17 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React, { PureComponent, Fragment } from 'react' |
|
||||
|
|
||||
import UpdateDownloaded from './UpdateDownloaded' |
|
||||
import UpdateInstalled from './UpdateInstalled' |
|
||||
|
|
||||
export default class UpdateNotifier extends PureComponent<{}> { |
|
||||
render() { |
|
||||
return ( |
|
||||
<Fragment> |
|
||||
<UpdateDownloaded /> |
|
||||
<UpdateInstalled /> |
|
||||
</Fragment> |
|
||||
) |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,74 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { Trans } from 'react-i18next' |
||||
|
|
||||
|
import { urls } from 'config/urls' |
||||
|
import { openURL } from 'helpers/linking' |
||||
|
|
||||
|
import Spinner from 'components/base/Spinner' |
||||
|
import IconUpdate from 'icons/Update' |
||||
|
import IconDonjon from 'icons/Donjon' |
||||
|
import IconWarning from 'icons/TriangleWarning' |
||||
|
|
||||
|
import { withUpdaterContext } from './UpdaterContext' |
||||
|
import type { UpdaterContextType } from './UpdaterContext' |
||||
|
import TopBanner, { FakeLink } from '../TopBanner' |
||||
|
import type { Content } from '../TopBanner' |
||||
|
|
||||
|
type Props = { |
||||
|
context: UpdaterContextType, |
||||
|
} |
||||
|
|
||||
|
export const VISIBLE_STATUS = ['download-progress', 'checking', 'check-success', 'error'] |
||||
|
|
||||
|
const CONTENT_BY_STATUS = (quitAndInstall, reDownload, progress): { [string]: Content } => ({ |
||||
|
'download-progress': { |
||||
|
Icon: Spinner, |
||||
|
message: <Trans i18nKey="update.downloadInProgress" />, |
||||
|
right: <Trans i18nKey="update.downloadProgress" values={{ progress }} />, |
||||
|
}, |
||||
|
checking: { |
||||
|
Icon: IconDonjon, |
||||
|
message: <Trans i18nKey="update.checking" />, |
||||
|
}, |
||||
|
'check-success': { |
||||
|
Icon: IconUpdate, |
||||
|
message: <Trans i18nKey="update.checkSuccess" />, |
||||
|
right: ( |
||||
|
<FakeLink onClick={quitAndInstall}> |
||||
|
<Trans i18nKey="update.quitAndInstall" /> |
||||
|
</FakeLink> |
||||
|
), |
||||
|
}, |
||||
|
error: { |
||||
|
Icon: IconWarning, |
||||
|
message: <Trans i18nKey="update.error" />, |
||||
|
right: ( |
||||
|
<FakeLink onClick={reDownload}> |
||||
|
<Trans i18nKey="update.reDownload" /> |
||||
|
</FakeLink> |
||||
|
), |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
class UpdaterTopBanner extends PureComponent<Props> { |
||||
|
reDownload = () => { |
||||
|
openURL(urls.liveHome) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { context } = this.props |
||||
|
const { status, quitAndInstall, downloadProgress } = context |
||||
|
if (!VISIBLE_STATUS.includes(status)) return null |
||||
|
|
||||
|
const content: ?Content = CONTENT_BY_STATUS(quitAndInstall, this.reDownload, downloadProgress)[ |
||||
|
status |
||||
|
] |
||||
|
if (!content) return null |
||||
|
|
||||
|
return <TopBanner content={content} status={status} /> |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default withUpdaterContext(UpdaterTopBanner) |
@ -0,0 +1,70 @@ |
|||||
|
// @flow
|
||||
|
/* eslint-disable react/jsx-no-literals */ |
||||
|
|
||||
|
import React, { Component } from 'react' |
||||
|
|
||||
|
import { withUpdaterContext } from './UpdaterContext' |
||||
|
import type { UpdaterContextType } from './UpdaterContext' |
||||
|
|
||||
|
const statusToDebug = ['idle', 'download-progress', 'checking', 'check-success', 'error'] |
||||
|
|
||||
|
type Props = { |
||||
|
context: UpdaterContextType, |
||||
|
} |
||||
|
|
||||
|
class DebugUpdater extends Component<Props> { |
||||
|
render() { |
||||
|
const { context } = this.props |
||||
|
const { status, setStatus, quitAndInstall } = context |
||||
|
return ( |
||||
|
<div style={styles.root}> |
||||
|
<h1> |
||||
|
DEBUG UPDATE<br /> |
||||
|
------------<br /> |
||||
|
</h1> |
||||
|
<b>status:</b> {status} |
||||
|
<div style={{ marginTop: 20 }}> |
||||
|
{statusToDebug.map(s => ( |
||||
|
<button key={s} style={styles.btn} onClick={() => setStatus(s)}> |
||||
|
{status === s ? `[${s}]` : s} |
||||
|
</button> |
||||
|
))} |
||||
|
</div> |
||||
|
<div style={{ marginTop: 20 }}> |
||||
|
<b>simulate update</b> |
||||
|
</div> |
||||
|
<div style={{ marginTop: 20 }}> |
||||
|
<button style={styles.btn} onClick={quitAndInstall}> |
||||
|
{'quit and install'} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const styles = { |
||||
|
root: { |
||||
|
position: 'fixed', |
||||
|
bottom: 0, |
||||
|
right: 0, |
||||
|
padding: 10, |
||||
|
fontSize: 10, |
||||
|
background: 'black', |
||||
|
color: 'white', |
||||
|
fontFamily: 'monospace', |
||||
|
zIndex: 1000, |
||||
|
maxWidth: 250, |
||||
|
}, |
||||
|
btn: { |
||||
|
cursor: 'pointer', |
||||
|
background: 'lightgreen', |
||||
|
color: 'black', |
||||
|
border: 'none', |
||||
|
marginRight: 10, |
||||
|
marginTop: 10, |
||||
|
padding: '0px 10px', |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
export default withUpdaterContext(DebugUpdater) |
@ -0,0 +1,37 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
import styled from 'styled-components' |
||||
|
|
||||
|
import { colors } from 'styles/theme' |
||||
|
|
||||
|
import { withUpdaterContext } from './UpdaterContext' |
||||
|
import { VISIBLE_STATUS } from './Banner' |
||||
|
import type { UpdaterContextType, UpdateStatus } from './UpdaterContext' |
||||
|
|
||||
|
type Props = { |
||||
|
context: UpdaterContextType, |
||||
|
} |
||||
|
|
||||
|
const getColor = ({ status }: { status: UpdateStatus }) => |
||||
|
status === 'error' ? colors.alertRed : colors.wallet |
||||
|
|
||||
|
const getOpacity = ({ status }: { status: UpdateStatus }) => |
||||
|
status === 'download-progress' || status === 'checking' ? 0.5 : 1 |
||||
|
|
||||
|
const Dot = styled.div` |
||||
|
opacity: ${getOpacity}; |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
background-color: ${getColor}; |
||||
|
border-radius: 50%; |
||||
|
` |
||||
|
|
||||
|
function UpdateDot(props: Props) { |
||||
|
const { context } = props |
||||
|
const { status } = context |
||||
|
if (!VISIBLE_STATUS.includes(status)) return null |
||||
|
return <Dot status={status} /> |
||||
|
} |
||||
|
|
||||
|
export default withUpdaterContext(UpdateDot) |
@ -0,0 +1,95 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { Component } from 'react' |
||||
|
|
||||
|
import autoUpdate from 'commands/autoUpdate' |
||||
|
import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate' |
||||
|
|
||||
|
export type UpdateStatus = |
||||
|
| 'idle' |
||||
|
| 'checking-for-update' |
||||
|
| 'update-available' |
||||
|
| 'update-not-available' |
||||
|
| 'download-progress' |
||||
|
| 'update-downloaded' |
||||
|
| 'checking' |
||||
|
| 'check-success' |
||||
|
| 'error' |
||||
|
|
||||
|
export type UpdaterContextType = { |
||||
|
status: UpdateStatus, |
||||
|
downloadProgress: number, |
||||
|
quitAndInstall: () => void, |
||||
|
setStatus: UpdateStatus => void, |
||||
|
error: ?Error, |
||||
|
} |
||||
|
|
||||
|
type UpdaterProviderProps = { |
||||
|
children: *, |
||||
|
} |
||||
|
|
||||
|
type UpdaterProviderState = { |
||||
|
status: UpdateStatus, |
||||
|
downloadProgress: number, |
||||
|
error: ?Error, |
||||
|
} |
||||
|
|
||||
|
const UpdaterContext = React.createContext() |
||||
|
|
||||
|
class Provider extends Component<UpdaterProviderProps, UpdaterProviderState> { |
||||
|
constructor() { |
||||
|
super() |
||||
|
|
||||
|
if (__PROD__) { |
||||
|
this.sub = autoUpdate.send({}).subscribe({ |
||||
|
next: e => { |
||||
|
if (e.status === 'download-progress') { |
||||
|
const downloadProgress = e.payload && e.payload.percent ? e.payload.percent : 0 |
||||
|
this.setState({ status: e.status, downloadProgress }) |
||||
|
} else { |
||||
|
this.setStatus(e.status) |
||||
|
} |
||||
|
}, |
||||
|
error: error => this.setState({ status: 'error', error }), |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
this.state = { |
||||
|
status: 'idle', |
||||
|
downloadProgress: 0, |
||||
|
error: null, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
if (this.sub) { |
||||
|
this.sub.unsubscribe() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
sub = null |
||||
|
|
||||
|
setStatus = (status: UpdateStatus) => this.setState({ status }) |
||||
|
setDownloadProgress = (downloadProgress: number) => this.setState({ downloadProgress }) |
||||
|
quitAndInstall = () => quitAndInstallElectronUpdate.send().toPromise() |
||||
|
|
||||
|
render() { |
||||
|
const { status, downloadProgress, error } = this.state |
||||
|
const value = { |
||||
|
status, |
||||
|
downloadProgress, |
||||
|
error, |
||||
|
setStatus: this.setStatus, |
||||
|
quitAndInstall: this.quitAndInstall, |
||||
|
} |
||||
|
return <UpdaterContext.Provider value={value}>{this.props.children}</UpdaterContext.Provider> |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const withUpdaterContext = (ComponentToDecorate: React$ComponentType<*>) => (props: *) => ( |
||||
|
<UpdaterContext.Consumer> |
||||
|
{context => <ComponentToDecorate {...props} context={context} />} |
||||
|
</UpdaterContext.Consumer> |
||||
|
) |
||||
|
|
||||
|
export const UpdaterProvider = Provider |
@ -1,95 +1,102 @@ |
|||||
// @flow
|
// @flow
|
||||
|
|
||||
import React, { PureComponent } from 'react' |
import React, { PureComponent, Fragment } from 'react' |
||||
import styled, { keyframes } from 'styled-components' |
import Animated from 'animated/lib/targets/react-dom' |
||||
|
import { findDOMNode } from 'react-dom' |
||||
|
|
||||
import Box from 'components/base/Box' |
import ModalContent from './ModalContent' |
||||
import IconCross from 'icons/Cross' |
import ModalHeader from './ModalHeader' |
||||
|
import ModalFooter from './ModalFooter' |
||||
|
|
||||
export const Container = styled(Box).attrs({ |
import type { RenderProps } from './index' |
||||
px: 5, |
|
||||
pb: 5, |
|
||||
})`` |
|
||||
|
|
||||
type Props = { |
type Props = { |
||||
deferHeight?: number, |
title: string, |
||||
onClose?: Function, |
onBack?: void => void, |
||||
children: any, |
onClose?: void => void, |
||||
|
render?: (?RenderProps) => any, |
||||
|
renderFooter?: (?RenderProps) => any, |
||||
|
renderProps?: RenderProps, |
||||
|
noScroll?: boolean, |
||||
|
refocusWhenChange?: any, |
||||
} |
} |
||||
|
|
||||
type State = { |
type State = { |
||||
isHidden: boolean, |
animGradient: Animated.Value, |
||||
} |
} |
||||
|
|
||||
class ModalBody extends PureComponent<Props, State> { |
class ModalBody extends PureComponent<Props, State> { |
||||
static defaultProps = { |
state = { |
||||
onClose: undefined, |
animGradient: new Animated.Value(0), |
||||
} |
} |
||||
|
|
||||
state = { |
componentDidUpdate(prevProps: Props) { |
||||
isHidden: true, |
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() { |
_content = null |
||||
setTimeout(() => { |
|
||||
window.requestAnimationFrame(() => { |
animateGradient = (isScrollable: boolean) => { |
||||
this.setState({ isHidden: false }) |
const anim = { |
||||
}) |
duration: 150, |
||||
}, 150) |
toValue: isScrollable ? 1 : 0, |
||||
|
} |
||||
|
Animated.timing(this.state.animGradient, anim).start() |
||||
} |
} |
||||
|
|
||||
render() { |
render() { |
||||
const { children, onClose, deferHeight, ...props } = this.props |
const { onBack, onClose, title, render, renderFooter, renderProps, noScroll } = this.props |
||||
const { isHidden } = this.state |
const { animGradient } = this.state |
||||
|
|
||||
|
const gradientStyle = { |
||||
|
...GRADIENT_STYLE, |
||||
|
opacity: animGradient, |
||||
|
} |
||||
|
|
||||
return ( |
return ( |
||||
<Body |
<Fragment> |
||||
style={{ height: isHidden && deferHeight ? deferHeight : undefined }} |
<ModalHeader onBack={onBack} onClose={onClose}> |
||||
data-e2e="modalBody" |
{title} |
||||
> |
</ModalHeader> |
||||
{onClose && ( |
<ModalContent |
||||
<CloseContainer onClick={onClose}> |
tabIndex={0} |
||||
<IconCross size={16} /> |
ref={n => (this._content = n)} |
||||
</CloseContainer> |
onIsScrollableChange={this.animateGradient} |
||||
)} |
noScroll={noScroll} |
||||
{(!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({ |
const GRADIENT_STYLE = { |
||||
p: 4, |
background: 'linear-gradient(rgba(255, 255, 255, 0), #ffffff)', |
||||
color: 'fog', |
height: 40, |
||||
})` |
position: 'absolute', |
||||
position: absolute; |
bottom: 0, |
||||
top: 0; |
left: 0, |
||||
right: 0; |
right: 20, |
||||
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 Inner = styled(Box)` |
const GRADIENT_WRAPPER_STYLE = { |
||||
animation: ${appear} 80ms linear; |
height: 0, |
||||
` |
position: 'relative', |
||||
|
pointerEvents: 'none', |
||||
|
} |
||||
|
|
||||
export default ModalBody |
export default ModalBody |
||||
|
@ -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 |
@ -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 |
@ -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) |
@ -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) |
|
@ -1,16 +1,56 @@ |
|||||
// @flow
|
// @flow
|
||||
import React from 'react' |
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { connect } from 'react-redux' |
||||
|
import semver from 'semver' |
||||
|
|
||||
|
import type { State } from 'reducers' |
||||
|
import { openModal } from 'reducers/modals' |
||||
|
import { lastUsedVersionSelector } from 'reducers/settings' |
||||
|
import { saveSettings } from 'actions/settings' |
||||
import { MODAL_RELEASES_NOTES } from 'config/constants' |
import { MODAL_RELEASES_NOTES } from 'config/constants' |
||||
import Modal from 'components/base/Modal' |
import Modal from 'components/base/Modal' |
||||
|
|
||||
import ReleaseNotesBody from './ReleaseNotesBody' |
import ReleaseNotesBody from './ReleaseNotesBody' |
||||
|
|
||||
const ReleaseNotesModal = () => ( |
type Props = { |
||||
<Modal |
openModal: Function, |
||||
name={MODAL_RELEASES_NOTES} |
saveSettings: Function, |
||||
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />} |
lastUsedVersion: string, |
||||
/> |
} |
||||
) |
|
||||
|
const mapStateToProps = (state: State) => ({ |
||||
|
lastUsedVersion: lastUsedVersionSelector(state), |
||||
|
}) |
||||
|
|
||||
|
const mapDispatchToProps = { |
||||
|
openModal, |
||||
|
saveSettings, |
||||
|
} |
||||
|
|
||||
|
class ReleaseNotesModal extends PureComponent<Props> { |
||||
|
componentDidMount() { |
||||
|
const { lastUsedVersion, saveSettings, openModal } = this.props |
||||
|
const currentVersion = __APP_VERSION__ |
||||
|
|
||||
|
if (semver.gt(currentVersion, lastUsedVersion)) { |
||||
|
openModal(MODAL_RELEASES_NOTES, currentVersion) |
||||
|
saveSettings({ lastUsedVersion: currentVersion }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return ( |
||||
|
<Modal |
||||
|
name={MODAL_RELEASES_NOTES} |
||||
|
centered |
||||
|
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
export default ReleaseNotesModal |
export default connect( |
||||
|
mapStateToProps, |
||||
|
mapDispatchToProps, |
||||
|
)(ReleaseNotesModal) |
||||
|
@ -0,0 +1,39 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { Fragment } from 'react' |
||||
|
|
||||
|
const path = ( |
||||
|
<Fragment> |
||||
|
<path |
||||
|
fill="currentColor" |
||||
|
d="m1085.1 267.76v-110.55c0-7.85 3.74-12.45 9-12.21l-144.56-80.23a33.93 33.93 0 0 1 10.11 23.23v110.55z" |
||||
|
/> |
||||
|
<path |
||||
|
fill="currentColor" |
||||
|
d="m1336 406.28v-110.55c0-7.18 3.14-11.6 7.67-12.14l-141.41-78.52a34.34 34.34 0 0 1 8.29 21.4l-0.07 110.53z" |
||||
|
/> |
||||
|
<polygon |
||||
|
fill="currentColor" |
||||
|
transform="translate(206.55 -4.4141e-6)" |
||||
|
points="1254.9 715.08 1167.2 814.98 627.52 515.62 627.52 368.78" |
||||
|
/> |
||||
|
<path |
||||
|
fill="currentColor" |
||||
|
d="m332.03 295.73v110.55l125.52-69.28v-110.55c0-10.18 6.24-21.87 13.94-26.12l97.6-53.87c7.7-4.25 13.94 0.55 13.94 10.73v110.55l125.52-69.24v-110.55c0-10.18 6.24-21.87 13.94-26.12l111.58-61.83-0.11 18.69 0.11 350.09-627.41 346.3-0.11-350.08c0-10.18 6.24-21.87 13.94-26.12l97.59-53.88c7.7-4.25 13.95 0.55 13.95 10.73z" |
||||
|
/> |
||||
|
<path |
||||
|
fill="currentColor" |
||||
|
d="m1175.8 1193.4 2.8-121.54c1.28-55.46-31.41-116.37-72.65-135.34-41.24-19-76 10.88-77.3 66.34l-2.8 121.54z" |
||||
|
/> |
||||
|
<path |
||||
|
fill="currentColor" |
||||
|
d="m294.46 814.91q1.86 205.53 76.34 387.56 67.47 166.41 186.66 290.1 107.94 114.69 236.09 166.43a98.88 98.88 0 0 0 40.48 9v-1152.4z" |
||||
|
/> |
||||
|
</Fragment> |
||||
|
) |
||||
|
|
||||
|
export default ({ size, ...p }: { size: number }) => ( |
||||
|
<svg viewBox="0 0 1668 1668" width={size} {...p}> |
||||
|
{path} |
||||
|
</svg> |
||||
|
) |
@ -0,0 +1,30 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
|
||||
|
export default ({ size = 30, ...p }: { size: number }) => ( |
||||
|
<svg viewBox="0 0 6 16" height={size} width={size} {...p}> |
||||
|
<defs> |
||||
|
<path |
||||
|
id="a" |
||||
|
d="M5.75 6.835a3.509 3.509 0 0 0-1.5-1.105V1.75h-2.5v3.98a3.509 3.509 0 0 0-1.5 1.105V1.666C.25.884.884.25 1.666.25h2.668c.782 0 1.416.634 1.416 1.416v5.169zm-1.5 7.415V9.5a1.25 1.25 0 1 0-2.5 0v4.75h2.5zM3 6.75A2.75 2.75 0 0 1 5.75 9.5v4.834c0 .782-.634 1.416-1.416 1.416H1.666A1.416 1.416 0 0 1 .25 14.334V9.5A2.75 2.75 0 0 1 3 6.75z" |
||||
|
/> |
||||
|
</defs> |
||||
|
<g fill="none" fillRule="evenodd"> |
||||
|
<path |
||||
|
fill="#000" |
||||
|
fillRule="nonzero" |
||||
|
d="M5.75 6.835a3.509 3.509 0 0 0-1.5-1.105V1.75h-2.5v3.98a3.509 3.509 0 0 0-1.5 1.105V1.666C.25.884.884.25 1.666.25h2.668c.782 0 1.416.634 1.416 1.416v5.169zm-1.5 7.415V9.5a1.25 1.25 0 1 0-2.5 0v4.75h2.5zM3 6.75A2.75 2.75 0 0 1 5.75 9.5v4.834c0 .782-.634 1.416-1.416 1.416H1.666A1.416 1.416 0 0 1 .25 14.334V9.5A2.75 2.75 0 0 1 3 6.75z" |
||||
|
/> |
||||
|
<g> |
||||
|
<mask id="b" fill="#fff"> |
||||
|
<use xlinkHref="#a" /> |
||||
|
</mask> |
||||
|
<use fill="#FFF" xlinkHref="#a" /> |
||||
|
<g fill="#FFF" mask="url(#b)"> |
||||
|
<path d="M-5 0h16v16H-5z" /> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
</svg> |
||||
|
) |
@ -1,2 +1,3 @@ |
|||||
export Blue from './Blue' |
export Blue from './Blue' |
||||
export NanoS from './NanoS' |
export NanoS from './NanoS' |
||||
|
export NanoX from './NanoX' |
||||
|
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue