Juan Cortés Ross
6 years ago
committed by
GitHub
267 changed files with 12636 additions and 4134 deletions
@ -1 +1,2 @@ |
|||
package.json |
|||
test-e2e/**/*.json |
|||
|
@ -1,32 +1,62 @@ |
|||
const { NODE_ENV } = process.env |
|||
const { NODE_ENV, CLI } = process.env |
|||
|
|||
const __TEST__ = NODE_ENV === 'test' |
|||
const __CLI__ = !!CLI |
|||
|
|||
module.exports = () => ({ |
|||
presets: [ |
|||
[ |
|||
require('@babel/preset-env'), |
|||
{ |
|||
loose: true, |
|||
modules: __TEST__ ? 'commonjs' : false, |
|||
targets: { |
|||
electron: '1.8', |
|||
node: 'current', |
|||
module.exports = (api) => { |
|||
|
|||
if (api) { |
|||
api.cache(true); |
|||
} |
|||
|
|||
return { |
|||
presets: [ |
|||
[ |
|||
require('@babel/preset-env'), |
|||
{ |
|||
loose: true, |
|||
modules: __TEST__ || __CLI__ ? 'commonjs' : false, |
|||
targets: { |
|||
electron: '1.8', |
|||
node: 'current', |
|||
}, |
|||
}, |
|||
}, |
|||
], |
|||
require('@babel/preset-flow'), |
|||
require('@babel/preset-react'), |
|||
], |
|||
require('@babel/preset-flow'), |
|||
require('@babel/preset-react'), |
|||
require('@babel/preset-stage-0'), |
|||
], |
|||
plugins: [ |
|||
[require('babel-plugin-module-resolver'), { root: ['src'] }], |
|||
[ |
|||
require('babel-plugin-styled-components'), |
|||
{ |
|||
displayName: true, |
|||
ssr: __TEST__, |
|||
}, |
|||
plugins: [ |
|||
[require('babel-plugin-module-resolver'), { root: ['src'] }], |
|||
[ |
|||
require('babel-plugin-styled-components'), |
|||
{ |
|||
displayName: true, |
|||
ssr: __TEST__, |
|||
}, |
|||
], |
|||
// Stage 0
|
|||
"@babel/plugin-proposal-function-bind", |
|||
|
|||
// Stage 1
|
|||
"@babel/plugin-proposal-export-default-from", |
|||
"@babel/plugin-proposal-logical-assignment-operators", |
|||
["@babel/plugin-proposal-optional-chaining", { "loose": false }], |
|||
["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }], |
|||
["@babel/plugin-proposal-nullish-coalescing-operator", { "loose": false }], |
|||
"@babel/plugin-proposal-do-expressions", |
|||
|
|||
// Stage 2
|
|||
["@babel/plugin-proposal-decorators", { "legacy": true }], |
|||
"@babel/plugin-proposal-function-sent", |
|||
"@babel/plugin-proposal-export-namespace-from", |
|||
"@babel/plugin-proposal-numeric-separator", |
|||
"@babel/plugin-proposal-throw-expressions", |
|||
|
|||
// Stage 3
|
|||
"@babel/plugin-syntax-dynamic-import", |
|||
"@babel/plugin-syntax-import-meta", |
|||
["@babel/plugin-proposal-class-properties", { "loose": false }], |
|||
"@babel/plugin-proposal-json-strings" |
|||
], |
|||
], |
|||
}) |
|||
} |
|||
} |
|||
|
@ -0,0 +1,9 @@ |
|||
#!/bin/bash |
|||
|
|||
# TODO: os specific |
|||
export LEDGER_DATA_DIR="$HOME/.config/Electron" |
|||
export LEDGER_LOGS_DIRECTORY="$LEDGER_DATA_DIR/logs" |
|||
export LEDGER_LIVE_SQLITE_PATH="$LEDGER_DATA_DIR/sqlite" |
|||
export CLI=1 |
|||
|
|||
node -r @babel/register -r @babel/polyfill scripts/cli/txBetweenAccounts.js |
@ -0,0 +1,21 @@ |
|||
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
|||
|
|||
export default function getDevice() { |
|||
return new Promise((resolve, reject) => { |
|||
const sub = CommNodeHid.listen({ |
|||
error: err => { |
|||
sub.unsubscribe() |
|||
reject(err) |
|||
}, |
|||
next: async e => { |
|||
if (!e.device) { |
|||
return |
|||
} |
|||
if (e.type === 'add') { |
|||
sub.unsubscribe() |
|||
resolve(e.device) |
|||
} |
|||
}, |
|||
}) |
|||
}) |
|||
} |
@ -0,0 +1,104 @@ |
|||
/* eslint-disable no-console */ |
|||
|
|||
import chalk from 'chalk' |
|||
import path from 'path' |
|||
import fs from 'fs' |
|||
import inquirer from 'inquirer' |
|||
import { formatCurrencyUnit } from '@ledgerhq/live-common/lib/currencies' |
|||
|
|||
import 'globals' |
|||
import withLibcore from 'helpers/withLibcore' |
|||
import accountModel from 'helpers/accountModel' |
|||
import { doSignAndBroadcast } from 'commands/libcoreSignAndBroadcast' |
|||
|
|||
import getDevice from './getDevice' |
|||
|
|||
async function main() { |
|||
try { |
|||
// GET ACCOUNTS
|
|||
const app = await parseAppFile() |
|||
const accounts = app.accounts.map(accountModel.decode) |
|||
|
|||
// GET SENDER ACCOUNT
|
|||
const senderAccount = await chooseAccount(accounts, 'Choose sender account') |
|||
|
|||
// GET RECIPIENT ACCOUNT
|
|||
const recipientAccount = await chooseAccount(accounts, 'Choose recipient account') |
|||
|
|||
// GET AMOUNT & FEE
|
|||
const { amount, feePerByte } = await inquirer.prompt([ |
|||
{ |
|||
type: 'input', |
|||
name: 'amount', |
|||
message: 'Amount', |
|||
default: 0, |
|||
}, |
|||
{ |
|||
type: 'input', |
|||
name: 'feePerByte', |
|||
message: 'Fee per byte', |
|||
default: 0, |
|||
}, |
|||
]) |
|||
|
|||
// GET DEVICE
|
|||
console.log(chalk.blue(`Waiting for device...`)) |
|||
const device = await getDevice() |
|||
console.log(chalk.blue(`Using device with path [${device.path}]`)) |
|||
|
|||
await withLibcore(async core => |
|||
doSignAndBroadcast({ |
|||
accountId: senderAccount.id, |
|||
currencyId: senderAccount.currency.id, |
|||
xpub: senderAccount.xpub, |
|||
freshAddress: senderAccount.freshAddress, |
|||
freshAddressPath: senderAccount.freshAddressPath, |
|||
index: senderAccount.index, |
|||
transaction: { |
|||
amount, |
|||
feePerByte, |
|||
recipient: recipientAccount.freshAddress, |
|||
}, |
|||
deviceId: device.path, |
|||
core, |
|||
isCancelled: () => false, |
|||
onSigned: () => { |
|||
console.log(`>> signed`) |
|||
}, |
|||
onOperationBroadcasted: operation => { |
|||
console.log(`>> broadcasted`, operation) |
|||
}, |
|||
}), |
|||
) |
|||
} catch (err) { |
|||
console.log(`[ERROR]`, err) |
|||
} |
|||
} |
|||
|
|||
async function parseAppFile() { |
|||
const appFilePath = path.resolve(process.env.LEDGER_DATA_DIR, 'app.json') |
|||
const appFileContent = fs.readFileSync(appFilePath, 'utf-8') |
|||
const parsedApp = JSON.parse(appFileContent) |
|||
return parsedApp.data |
|||
} |
|||
|
|||
async function chooseAccount(accounts, msg) { |
|||
const { account } = await inquirer.prompt([ |
|||
{ |
|||
type: 'list', |
|||
choices: accounts.map(account => ({ |
|||
name: `${account.name} | ${chalk.green( |
|||
formatCurrencyUnit(account.unit, account.balance, { |
|||
showCode: true, |
|||
}), |
|||
)}`,
|
|||
value: account, |
|||
})), |
|||
name: 'account', |
|||
message: msg, |
|||
}, |
|||
]) |
|||
return account |
|||
} |
|||
|
|||
main() |
@ -0,0 +1,85 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
/* eslint-disable no-console */ |
|||
|
|||
const util = require('util') |
|||
const exec = util.promisify(require('child_process').exec) |
|||
const octokit = require('@octokit/rest')() |
|||
|
|||
const repo = { |
|||
owner: 'LedgerHQ', |
|||
repo: 'ledger-live-desktop', |
|||
} |
|||
|
|||
async function getTag() { |
|||
const { stdout } = await exec('git tag --points-at HEAD') |
|||
const tag = stdout.replace('\n', '') |
|||
|
|||
if (!tag) { |
|||
throw new Error(`Unable to get current tag. Is your HEAD on a tagged commit?`) |
|||
} |
|||
|
|||
return tag |
|||
} |
|||
|
|||
async function checkDraft(tag) { |
|||
const { status, data } = await octokit.repos.getReleases(repo) |
|||
|
|||
if (status !== 200) { |
|||
throw new Error(`Got HTTP status ${status} when trying to fetch releases list.`) |
|||
} |
|||
|
|||
for (const release of data) { |
|||
if (release.tag_name === tag) { |
|||
if (release.draft) { |
|||
return true |
|||
} |
|||
|
|||
throw new Error(`A release tagged ${tag} exists but is not a draft.`) |
|||
} |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
async function createDraft(tag) { |
|||
const params = { |
|||
...repo, |
|||
tag_name: tag, |
|||
name: tag, |
|||
draft: true, |
|||
prerelease: true, |
|||
} |
|||
|
|||
const { status } = await octokit.repos.createRelease(params) |
|||
|
|||
if (status !== 201) { |
|||
throw new Error(`Got HTTP status ${status} when trying to create the release draft.`) |
|||
} |
|||
} |
|||
|
|||
async function main() { |
|||
try { |
|||
const token = process.env.GH_TOKEN |
|||
const tag = await getTag() |
|||
|
|||
octokit.authenticate({ |
|||
type: 'token', |
|||
token, |
|||
}) |
|||
|
|||
const existingDraft = await checkDraft(tag) |
|||
|
|||
if (!existingDraft) { |
|||
console.log(`No draft exists for ${tag}, creating...`) |
|||
createDraft(tag) |
|||
} else { |
|||
console.log(`A draft already exists for ${tag}, nothing to do.`) |
|||
} |
|||
} catch (e) { |
|||
console.error(e) |
|||
process.exit(1) |
|||
} |
|||
} |
|||
|
|||
main() |
@ -0,0 +1,17 @@ |
|||
#!/bin/sh |
|||
|
|||
|
|||
if [ -z "$CROWDIN_TOKEN" ]; then |
|||
echo "CROWDIN_TOKEN env required" >&2 |
|||
exit 1 |
|||
fi |
|||
|
|||
rm -rf xliffs |
|||
mkdir xliffs |
|||
cd xliffs |
|||
|
|||
for lang in fr es-ES zh-CN ja ko ru; do |
|||
curl "https://api.crowdin.com/api/project/ledger-wallet/export-file?file=develop/static/i18n/en/app.json&language=$lang&format=xliff&key=$CROWDIN_TOKEN" > en-$lang.xliff |
|||
done |
|||
|
|||
zip -r ledger-live-langs.zip *.xliff |
@ -0,0 +1,49 @@ |
|||
#!/bin/bash |
|||
|
|||
# Patch .AppImage to address libcore crash on some |
|||
# distributions, due to loading system libraries |
|||
# instead of embedded ones. |
|||
# |
|||
# see https://github.com/LedgerHQ/ledger-live-desktop/issues/1010 |
|||
|
|||
set -e |
|||
|
|||
BASE_URL=http://mirrors.kernel.org/ubuntu/pool/main/k/krb5 |
|||
PACKAGE_SUFFIX=-2build1_amd64.deb |
|||
TMP_DIR=$(mktemp -d) |
|||
LEDGER_LIVE_VERSION=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g') |
|||
|
|||
cp "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage" "$TMP_DIR" |
|||
pushd "$TMP_DIR" |
|||
|
|||
declare -a LIBRARIES=( |
|||
"libgssapi-krb5-2_1.16" |
|||
"libk5crypto3_1.16" |
|||
"libkrb5-3_1.16" |
|||
"libkrb5support0_1.16" |
|||
) |
|||
|
|||
for PACKAGE in "${LIBRARIES[@]}"; do |
|||
curl -fOL "$BASE_URL/$PACKAGE$PACKAGE_SUFFIX" |
|||
ar p "$PACKAGE$PACKAGE_SUFFIX" data.tar.xz | tar xvJf >/dev/null - ./usr/lib/x86_64-linux-gnu/ |
|||
rm "$PACKAGE$PACKAGE_SUFFIX" |
|||
done |
|||
|
|||
curl -fOL "https://s3-eu-west-1.amazonaws.com/ledger-ledgerlive-resources-dev/public_resources/appimagetool-x86_64.AppImage" |
|||
|
|||
cp "$OLDPWD/scripts/shasums/patch-appimage-sums.txt" . |
|||
sha512sum --quiet --check patch-appimage-sums.txt || exit 1 |
|||
|
|||
./ledger-live-desktop-"$LEDGER_LIVE_VERSION"-linux-x86_64.AppImage --appimage-extract |
|||
cp -a usr/lib/x86_64-linux-gnu/*.so.* squashfs-root/usr/lib |
|||
|
|||
chmod +x appimagetool-x86_64.AppImage |
|||
./appimagetool-x86_64.AppImage squashfs-root "$OLDPWD/dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage" |
|||
|
|||
popd |
|||
|
|||
MD5_SUM=$(sha512sum "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage" | cut -f1 -d\ | xxd -r -p | base64 | paste -sd "") |
|||
sed -i "s|sha512: .*|sha512: ${MD5_SUM}|g" dist/latest-linux.yml |
|||
|
|||
SIZE=$(stat --printf="%s" "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage") |
|||
sed -i "s|size: .*|size: ${SIZE}|g" dist/latest-linux.yml |
@ -0,0 +1,5 @@ |
|||
bebb42401a43971cfe3e31f2c9ee4efee352ce0d29a8ccc95ca1356a58463afd4876b133d9f4295697f96b76eb21b50c1909a073db753569e8969065eb40b306 usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2 |
|||
d7d2b38a46d65a06560241b226f61d81c4df28d56c6841dd34bb428802ace0fc80cf94de1e5117f0b85b2c69b550df61ac999184d5cfe8ecd3bea4d8394d1d21 usr/lib/x86_64-linux-gnu/libkrb5.so.3.3 |
|||
b025b755eb9a64f0d03a8e92c9e4b4f95c2c506bf070cf037841ef8cdb9013e16390d0e17330f2ce8c98c3b1f05b917a3018109acfde7aab50bc9d9fa70ea12b usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1 |
|||
f181e41f306819c10054ff8ceebf4943858f2cd34dea5206b51141877e2f651be3c6435bb02538cbde2cc0415f38e476423a9fd6a428ca9d425e9c662483b9af usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1 |
|||
dd8d81d4c1485209a65a1446225428a1b919478a74fd5698aff64cb8a67992544e62455f849ad73392505707cb94739de00af5ab340a22a87bb752c3808a55d2 appimagetool-x86_64.AppImage |
@ -0,0 +1,61 @@ |
|||
#!/usr/bin/env bash |
|||
# |
|||
# Author: Stefan Buck |
|||
# License: MIT |
|||
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447 |
|||
# |
|||
# |
|||
# This script accepts the following parameters: |
|||
# |
|||
# * owner |
|||
# * repo |
|||
# * filename |
|||
# * github_api_token |
|||
# |
|||
# Script to upload a release asset using the GitHub API v3. |
|||
# |
|||
# Example: |
|||
# |
|||
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground filename=./build.zip |
|||
# |
|||
|
|||
# Check dependencies. |
|||
set -e |
|||
|
|||
# Validate settings. |
|||
[ "$TRACE" ] && set -x |
|||
|
|||
# shellcheck disable=SC2124 |
|||
CONFIG=$@ |
|||
|
|||
for line in $CONFIG; do |
|||
eval "$line" |
|||
done |
|||
|
|||
# Define variables. |
|||
GH_API="https://api.github.com" |
|||
|
|||
# shellcheck disable=SC2154 |
|||
GH_REPO="$GH_API/repos/$owner/$repo" |
|||
|
|||
# shellcheck disable=SC2154 |
|||
AUTH="Authorization: token $github_api_token" |
|||
|
|||
# github_api_token=$GH_TOKEN owner=LedgerHQ repo=ledger-live-desktop tag=v1.2.2 filename=./dist/electron-builder-debug.yml |
|||
LATEST_RELEASE_ID=$(curl -sH "$AUTH" "$GH_API/repos/LedgerHQ/ledger-live-desktop/releases" | grep '"id":' | head -n 1 | sed -E 's/.*: (.*),/\1/') |
|||
|
|||
# Validate token. |
|||
curl -o /dev/null -sH "$AUTH" "$GH_REPO" || { echo "Error: Invalid repo, token or network issue!"; exit 1; } |
|||
|
|||
# Get ID of the asset based on given filename. |
|||
# shellcheck disable=SC2154 |
|||
[ "$LATEST_RELEASE_ID" ] || { echo "Error: Failed to get release id"; exit 1; } |
|||
|
|||
# Upload asset |
|||
echo "Uploading asset... " |
|||
|
|||
# Construct url |
|||
# shellcheck disable=SC2154 |
|||
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$LATEST_RELEASE_ID/assets?name=$(basename "$filename")" |
|||
|
|||
curl --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" "$GH_ASSET" |
@ -0,0 +1,18 @@ |
|||
// @flow
|
|||
|
|||
import { createCommand, Command } from 'helpers/ipc' |
|||
import { of } from 'rxjs' |
|||
|
|||
type Input = void |
|||
type Result = boolean |
|||
|
|||
const cmd: Command<Input, Result> = createCommand('killInternalProcess', () => { |
|||
setTimeout(() => { |
|||
// we assume commands are run on the internal process
|
|||
// special exit code for better identification
|
|||
process.exit(42) |
|||
}) |
|||
return of(true) |
|||
}) |
|||
|
|||
export default cmd |
@ -1,19 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import { createCommand } from 'helpers/ipc' |
|||
import { fromPromise } from 'rxjs/observable/fromPromise' |
|||
import withLibcore from 'helpers/withLibcore' |
|||
import { HardResetFail } from 'config/errors' |
|||
|
|||
const cmd = createCommand('libcoreHardReset', () => |
|||
fromPromise( |
|||
withLibcore(async core => { |
|||
const result = await core.getPoolInstance().eraseDataSince(new Date(0)) |
|||
if (result !== core.ERROR_CODE.FUTURE_WAS_SUCCESSFULL) { |
|||
throw new HardResetFail(`Hard reset fail with ${result} (check core.ERROR_CODE)`) |
|||
} |
|||
}), |
|||
), |
|||
) |
|||
|
|||
export default cmd |
@ -0,0 +1,29 @@ |
|||
// @flow
|
|||
|
|||
import { fromPromise } from 'rxjs/observable/fromPromise' |
|||
import type { AccountRaw, DerivationMode } from '@ledgerhq/live-common/lib/types' |
|||
|
|||
import { createCommand, Command } from 'helpers/ipc' |
|||
import withLibcore from 'helpers/withLibcore' |
|||
import { scanAccountsFromXPUB } from 'helpers/libcore' |
|||
|
|||
type Input = { |
|||
currencyId: string, |
|||
xpub: string, |
|||
derivationMode: DerivationMode, |
|||
seedIdentifier: string, |
|||
} |
|||
|
|||
type Result = AccountRaw |
|||
|
|||
const cmd: Command<Input, Result> = createCommand( |
|||
'libcoreScanFromXPUB', |
|||
({ currencyId, xpub, derivationMode, seedIdentifier }) => |
|||
fromPromise( |
|||
withLibcore(async core => |
|||
scanAccountsFromXPUB({ core, currencyId, xpub, derivationMode, seedIdentifier }), |
|||
), |
|||
), |
|||
) |
|||
|
|||
export default cmd |
@ -0,0 +1,181 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import { compose } from 'redux' |
|||
import { translate } from 'react-i18next' |
|||
import { connect } from 'react-redux' |
|||
import { createStructuredSelector } from 'reselect' |
|||
import styled from 'styled-components' |
|||
import type { Currency } from '@ledgerhq/live-common/lib/types' |
|||
|
|||
import { colors } from 'styles/theme' |
|||
import { openURL } from 'helpers/linking' |
|||
import { CHECK_CUR_STATUS_INTERVAL } from 'config/constants' |
|||
import IconCross from 'icons/Cross' |
|||
import IconTriangleWarning from 'icons/TriangleWarning' |
|||
import IconChevronRight from 'icons/ChevronRight' |
|||
|
|||
import { dismissedBannersSelector } from 'reducers/settings' |
|||
import { currenciesStatusSelector, fetchCurrenciesStatus } from 'reducers/currenciesStatus' |
|||
import { currenciesSelector } from 'reducers/accounts' |
|||
import { dismissBanner } from 'actions/settings' |
|||
import type { CurrencyStatus } from 'reducers/currenciesStatus' |
|||
|
|||
import Box from 'components/base/Box' |
|||
|
|||
const mapStateToProps = createStructuredSelector({ |
|||
dismissedBanners: dismissedBannersSelector, |
|||
accountsCurrencies: currenciesSelector, |
|||
currenciesStatus: currenciesStatusSelector, |
|||
}) |
|||
|
|||
const mapDispatchToProps = { |
|||
dismissBanner, |
|||
fetchCurrenciesStatus, |
|||
} |
|||
|
|||
const getItemKey = (item: CurrencyStatus) => `${item.id}_${item.nonce}` |
|||
|
|||
const CloseIconContainer = styled.div` |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 10px; |
|||
border-bottom-left-radius: 4px; |
|||
|
|||
opacity: 0.5; |
|||
&:hover { |
|||
opacity: 1; |
|||
} |
|||
` |
|||
|
|||
const CloseIcon = (props: *) => ( |
|||
<CloseIconContainer {...props}> |
|||
<IconCross size={16} color="white" /> |
|||
</CloseIconContainer> |
|||
) |
|||
|
|||
type Props = { |
|||
accountsCurrencies: Currency[], |
|||
dismissedBanners: string[], |
|||
dismissBanner: string => void, |
|||
currenciesStatus: CurrencyStatus[], |
|||
fetchCurrenciesStatus: () => Promise<void>, |
|||
t: *, |
|||
} |
|||
|
|||
class CurrenciesStatusBanner extends PureComponent<Props> { |
|||
componentDidMount() { |
|||
this.pollStatus() |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
this.unmounted = true |
|||
if (this.timeout) { |
|||
clearTimeout(this.timeout) |
|||
} |
|||
} |
|||
|
|||
unmounted = false |
|||
timeout: * |
|||
|
|||
pollStatus = async () => { |
|||
await this.props.fetchCurrenciesStatus() |
|||
if (this.unmounted) return |
|||
this.timeout = setTimeout(this.pollStatus, CHECK_CUR_STATUS_INTERVAL) |
|||
} |
|||
|
|||
dismiss = item => this.props.dismissBanner(getItemKey(item)) |
|||
|
|||
render() { |
|||
const { dismissedBanners, accountsCurrencies, currenciesStatus, t } = this.props |
|||
const filtered = currenciesStatus.filter( |
|||
item => |
|||
accountsCurrencies.find(cur => cur.id === item.id) && |
|||
dismissedBanners.indexOf(getItemKey(item)) === -1, |
|||
) |
|||
if (!filtered.length) return null |
|||
return ( |
|||
<Box flow={2} style={styles.container}> |
|||
{filtered.map(r => <BannerItem key={r.id} t={t} item={r} onItemDismiss={this.dismiss} />)} |
|||
</Box> |
|||
) |
|||
} |
|||
} |
|||
|
|||
class BannerItem extends PureComponent<{ |
|||
item: CurrencyStatus, |
|||
onItemDismiss: CurrencyStatus => void, |
|||
t: *, |
|||
}> { |
|||
onLinkClick = () => openURL(this.props.item.link) |
|||
dismiss = () => this.props.onItemDismiss(this.props.item) |
|||
render() { |
|||
const { item, t } = this.props |
|||
return ( |
|||
<Box relative key={item.id} style={styles.banner}> |
|||
<CloseIcon onClick={this.dismiss} /> |
|||
<Box horizontal flow={2}> |
|||
<IconTriangleWarning height={16} width={16} color="white" /> |
|||
<Box shrink ff="Open Sans|SemiBold"> |
|||
{item.message} |
|||
</Box> |
|||
</Box> |
|||
{item.link && <BannerItemLink t={t} onClick={this.onLinkClick} />} |
|||
</Box> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const UnderlinedLink = styled.span` |
|||
border-bottom: 1px solid transparent; |
|||
&:hover { |
|||
border-bottom-color: white; |
|||
} |
|||
` |
|||
|
|||
const BannerItemLink = ({ t, onClick }: { t: *, onClick: void => * }) => ( |
|||
<Box |
|||
mt={2} |
|||
ml={4} |
|||
flow={1} |
|||
horizontal |
|||
align="center" |
|||
cursor="pointer" |
|||
onClick={onClick} |
|||
color="white" |
|||
> |
|||
<IconChevronRight size={16} color="white" /> |
|||
<UnderlinedLink>{t('common.learnMore')}</UnderlinedLink> |
|||
</Box> |
|||
) |
|||
|
|||
const styles = { |
|||
container: { |
|||
position: 'fixed', |
|||
left: 32, |
|||
bottom: 32, |
|||
}, |
|||
banner: { |
|||
background: colors.alertRed, |
|||
overflow: 'hidden', |
|||
borderRadius: 4, |
|||
fontSize: 13, |
|||
padding: 14, |
|||
color: 'white', |
|||
fontWeight: 'bold', |
|||
paddingRight: 50, |
|||
width: 350, |
|||
}, |
|||
} |
|||
|
|||
export default compose( |
|||
connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps, |
|||
), |
|||
translate(), |
|||
)(CurrenciesStatusBanner) |
@ -0,0 +1,68 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import { translate } from 'react-i18next' |
|||
import styled from 'styled-components' |
|||
import { connect } from 'react-redux' |
|||
import { createStructuredSelector } from 'reselect' |
|||
|
|||
import type { CurrencyStatus } from 'reducers/currenciesStatus' |
|||
import { currencyDownStatus } from 'reducers/currenciesStatus' |
|||
import { openURL } from 'helpers/linking' |
|||
import Box from 'components/base/Box' |
|||
import IconTriangleWarning from 'icons/TriangleWarning' |
|||
import IconExternalLink from 'icons/ExternalLink' |
|||
|
|||
type Props = { |
|||
t: *, |
|||
status: ?CurrencyStatus, |
|||
} |
|||
|
|||
const CurrencyDownBox = styled(Box).attrs({ |
|||
horizontal: true, |
|||
align: 'center', |
|||
color: 'white', |
|||
borderRadius: 1, |
|||
fontSize: 1, |
|||
px: 4, |
|||
py: 2, |
|||
mb: 4, |
|||
})` |
|||
background-color: ${p => p.theme.colors.alertRed}; |
|||
` |
|||
|
|||
const Link = styled.span` |
|||
margin-left: 5px; |
|||
margin-right: 5px; |
|||
text-decoration: underline; |
|||
cursor: pointer; |
|||
` |
|||
|
|||
class CurrencyDownStatusAlert extends PureComponent<Props> { |
|||
onClick = () => { |
|||
const { status } = this.props |
|||
if (status) openURL(status.link) |
|||
} |
|||
|
|||
render() { |
|||
const { status, t } = this.props |
|||
if (!status) return null |
|||
return ( |
|||
<CurrencyDownBox> |
|||
<Box mr={2}> |
|||
<IconTriangleWarning height={16} width={16} /> |
|||
</Box> |
|||
<Box style={{ display: 'block' }} ff="Open Sans|SemiBold" fontSize={3} horizontal shrink> |
|||
{status.message} |
|||
<Link onClick={this.onClick}>{t('common.learnMore')}</Link> |
|||
<IconExternalLink size={12} /> |
|||
</Box> |
|||
</CurrencyDownBox> |
|||
) |
|||
} |
|||
} |
|||
export default connect( |
|||
createStructuredSelector({ |
|||
status: currencyDownStatus, |
|||
}), |
|||
)(translate()(CurrencyDownStatusAlert)) |
@ -0,0 +1,287 @@ |
|||
// @flow
|
|||
|
|||
/* eslint-disable react/no-multi-comp */ |
|||
|
|||
import React, { PureComponent, Fragment } from 'react' |
|||
import invariant from 'invariant' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import type { Currency, Account, DerivationMode } from '@ledgerhq/live-common/lib/types' |
|||
|
|||
import { decodeAccount } from 'reducers/accounts' |
|||
import { addAccount } from 'actions/accounts' |
|||
|
|||
import FakeLink from 'components/base/FakeLink' |
|||
import Ellipsis from 'components/base/Ellipsis' |
|||
import Switch from 'components/base/Switch' |
|||
import Spinner from 'components/base/Spinner' |
|||
import Box, { Card } from 'components/base/Box' |
|||
import TranslatedError from 'components/TranslatedError' |
|||
import Button from 'components/base/Button' |
|||
import Input from 'components/base/Input' |
|||
import Label from 'components/base/Label' |
|||
import SelectCurrency from 'components/SelectCurrency' |
|||
import { CurrencyCircleIcon } from 'components/base/CurrencyBadge' |
|||
|
|||
import { idleCallback } from 'helpers/promise' |
|||
|
|||
import scanFromXPUB from 'commands/libcoreScanFromXPUB' |
|||
|
|||
const mapDispatchToProps = { |
|||
addAccount, |
|||
} |
|||
|
|||
type Props = { |
|||
addAccount: Account => void, |
|||
} |
|||
|
|||
type ImportableAccountType = { |
|||
name: string, |
|||
currency: Currency, |
|||
derivationMode: DerivationMode, |
|||
xpub: string, |
|||
} |
|||
|
|||
type State = { |
|||
status: string, |
|||
|
|||
importableAccounts: ImportableAccountType[], |
|||
|
|||
currency: ?Currency, |
|||
xpub: string, |
|||
name: string, |
|||
isSegwit: boolean, |
|||
isUnsplit: boolean, |
|||
|
|||
error: ?Error, |
|||
} |
|||
|
|||
const INITIAL_STATE = { |
|||
status: 'idle', |
|||
|
|||
currency: null, |
|||
xpub: '', |
|||
name: 'dev', |
|||
isSegwit: true, |
|||
isUnsplit: false, |
|||
|
|||
error: null, |
|||
importableAccounts: [], |
|||
} |
|||
|
|||
class AccountImporter extends PureComponent<Props, State> { |
|||
state = INITIAL_STATE |
|||
|
|||
onChangeCurrency = currency => { |
|||
if (currency.family !== 'bitcoin') return |
|||
this.setState({ |
|||
currency, |
|||
isSegwit: !!currency.supportsSegwit, |
|||
isUnsplit: false, |
|||
}) |
|||
} |
|||
|
|||
onChangeXPUB = xpub => this.setState({ xpub }) |
|||
onChangeSegwit = isSegwit => this.setState({ isSegwit }) |
|||
onChangeUnsplit = isUnsplit => this.setState({ isUnsplit }) |
|||
onChangeName = name => this.setState({ name }) |
|||
|
|||
isValid = () => { |
|||
const { currency, xpub, status } = this.state |
|||
return !!currency && !!xpub && status !== 'scanning' |
|||
} |
|||
|
|||
scan = async () => { |
|||
this.setState({ status: 'scanning' }) |
|||
const { importableAccounts } = this.state |
|||
try { |
|||
for (let i = 0; i < importableAccounts.length; i++) { |
|||
const a = importableAccounts[i] |
|||
const scanPayload = { |
|||
seedIdentifier: `dev_${a.xpub}`, |
|||
currencyId: a.currency.id, |
|||
xpub: a.xpub, |
|||
derivationMode: a.derivationMode, |
|||
} |
|||
const rawAccount = await scanFromXPUB.send(scanPayload).toPromise() |
|||
const account = decodeAccount(rawAccount) |
|||
await this.import({ |
|||
...account, |
|||
name: a.name, |
|||
}) |
|||
this.removeImportableAccount(a) |
|||
} |
|||
this.reset() |
|||
} catch (error) { |
|||
this.setState({ status: 'error', error }) |
|||
} |
|||
} |
|||
|
|||
addToScan = () => { |
|||
const { xpub, currency, isSegwit, isUnsplit, name } = this.state |
|||
const derivationMode = isSegwit |
|||
? isUnsplit |
|||
? 'segwit_unsplit' |
|||
: 'segwit' |
|||
: isUnsplit |
|||
? 'unsplit' |
|||
: '' |
|||
const importableAccount = { xpub, currency, derivationMode, name } |
|||
this.setState(({ importableAccounts }) => ({ |
|||
importableAccounts: [...importableAccounts, importableAccount], |
|||
currency: null, |
|||
xpub: '', |
|||
name: 'dev', |
|||
isSegwit: true, |
|||
isUnsplit: false, |
|||
})) |
|||
} |
|||
|
|||
removeImportableAccount = importableAccount => { |
|||
this.setState(({ importableAccounts }) => ({ |
|||
importableAccounts: importableAccounts.filter(i => i.xpub !== importableAccount.xpub), |
|||
})) |
|||
} |
|||
|
|||
import = async account => { |
|||
invariant(account, 'no account') |
|||
await idleCallback() |
|||
this.props.addAccount(account) |
|||
} |
|||
|
|||
reset = () => this.setState(INITIAL_STATE) |
|||
|
|||
render() { |
|||
const { |
|||
currency, |
|||
xpub, |
|||
name, |
|||
isSegwit, |
|||
isUnsplit, |
|||
status, |
|||
error, |
|||
importableAccounts, |
|||
} = this.state |
|||
const supportsSplit = !!currency && !!currency.forkedFrom |
|||
return ( |
|||
<Fragment> |
|||
<Card title="Import from xpub" flow={3}> |
|||
{status === 'idle' || status === 'scanning' ? ( |
|||
<Fragment> |
|||
<Box flow={1}> |
|||
<Label>{'currency'}</Label> |
|||
<SelectCurrency autoFocus value={currency} onChange={this.onChangeCurrency} /> |
|||
</Box> |
|||
{currency && (currency.supportsSegwit || supportsSplit) ? ( |
|||
<Box horizontal justify="flex-end" align="center" flow={3}> |
|||
{supportsSplit && ( |
|||
<Box horizontal align="center" flow={1}> |
|||
<Box ff="Museo Sans|Bold" fontSize={4}> |
|||
{'unsplit'} |
|||
</Box> |
|||
<Switch isChecked={isUnsplit} onChange={this.onChangeUnsplit} /> |
|||
</Box> |
|||
)} |
|||
{currency.supportsSegwit && ( |
|||
<Box horizontal align="center" flow={1}> |
|||
<Box ff="Museo Sans|Bold" fontSize={4}> |
|||
{'segwit'} |
|||
</Box> |
|||
<Switch isChecked={isSegwit} onChange={this.onChangeSegwit} /> |
|||
</Box> |
|||
)} |
|||
</Box> |
|||
) : null} |
|||
<Box flow={1}> |
|||
<Label>{'xpub'}</Label> |
|||
<Input |
|||
placeholder="xpub" |
|||
value={xpub} |
|||
onChange={this.onChangeXPUB} |
|||
onEnter={this.addToScan} |
|||
/> |
|||
</Box> |
|||
<Box flow={1}> |
|||
<Label>{'name'}</Label> |
|||
<Input |
|||
placeholder="name" |
|||
value={name} |
|||
onChange={this.onChangeName} |
|||
onEnter={this.addToScan} |
|||
/> |
|||
</Box> |
|||
<Box align="flex-end"> |
|||
<Button primary small disabled={!this.isValid()} onClick={this.addToScan}> |
|||
{'add to scan'} |
|||
</Button> |
|||
</Box> |
|||
</Fragment> |
|||
) : status === 'error' ? ( |
|||
<Box align="center" justify="center" p={5} flow={4}> |
|||
<Box> |
|||
<TranslatedError error={error} /> |
|||
</Box> |
|||
<Button primary onClick={this.reset} small autoFocus> |
|||
{'Reset'} |
|||
</Button> |
|||
</Box> |
|||
) : null} |
|||
</Card> |
|||
{!!importableAccounts.length && ( |
|||
<Card flow={2}> |
|||
{importableAccounts.map((acc, i) => ( |
|||
<ImportableAccount |
|||
key={acc.xpub} |
|||
importableAccount={acc} |
|||
onRemove={this.removeImportableAccount} |
|||
isLoading={status === 'scanning' && i === 0} |
|||
> |
|||
{acc.xpub} |
|||
</ImportableAccount> |
|||
))} |
|||
{status !== 'scanning' && ( |
|||
<Box mt={4} align="flex-start"> |
|||
<Button primary onClick={this.scan}> |
|||
{'Launch scan'} |
|||
</Button> |
|||
</Box> |
|||
)} |
|||
</Card> |
|||
)} |
|||
</Fragment> |
|||
) |
|||
} |
|||
} |
|||
|
|||
class ImportableAccount extends PureComponent<{ |
|||
importableAccount: ImportableAccountType, |
|||
onRemove: ImportableAccountType => void, |
|||
isLoading: boolean, |
|||
}> { |
|||
remove = () => { |
|||
this.props.onRemove(this.props.importableAccount) |
|||
} |
|||
render() { |
|||
const { importableAccount, isLoading } = this.props |
|||
return ( |
|||
<Box horizontal flow={2} align="center"> |
|||
{isLoading && <Spinner size={16} color="rgba(0, 0, 0, 0.3)" />} |
|||
<CurrencyCircleIcon currency={importableAccount.currency} size={24} /> |
|||
<Box grow ff="Rubik" fontSize={3}> |
|||
<Ellipsis>{`[${importableAccount.name}] ${importableAccount.derivationMode || |
|||
'default'} ${importableAccount.xpub}`}</Ellipsis>
|
|||
</Box> |
|||
{!isLoading && ( |
|||
<FakeLink onClick={this.remove} fontSize={3}> |
|||
{'Remove'} |
|||
</FakeLink> |
|||
)} |
|||
</Box> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default connect( |
|||
null, |
|||
mapDispatchToProps, |
|||
)(AccountImporter) |
@ -0,0 +1,11 @@ |
|||
import React from 'react' |
|||
|
|||
import Box from 'components/base/Box' |
|||
|
|||
import AccountImporter from './AccountImporter' |
|||
|
|||
export default () => ( |
|||
<Box flow={2}> |
|||
<AccountImporter /> |
|||
</Box> |
|||
) |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue