From e86af198886f8a09e56e975e109928331b7aac58 Mon Sep 17 00:00:00 2001 From: meriadec Date: Fri, 19 Oct 2018 16:42:47 +0200 Subject: [PATCH 01/55] Implement shasum & signature check on auto-update --- package.json | 1 + src/config/errors.js | 5 + src/main/autoUpdate.js | 32 ----- src/main/bridge.js | 11 -- src/main/updater/createAppUpdater.js | 86 +++++++++++++ src/main/updater/createElectronAppUpdater.js | 62 ++++++++++ src/main/updater/createMockAppUpdater.js | 40 ++++++ src/main/updater/ledger-pubkey.js | 111 +++++++++++++++++ src/main/updater/mocks/mock-privkey-1.asc | 33 +++++ src/main/updater/mocks/mock-privkey-2.asc | 33 +++++ src/main/updater/mocks/mock-pubkey-1.asc | 19 +++ src/main/updater/mocks/mock-pubkey-2.asc | 19 +++ src/main/updater/mocks/pubkey-1.asc | 52 ++++++++ src/main/updater/mocks/pubkey-2.asc | 52 ++++++++ ...r-live-desktop-1.2.1-linux-x86_64.AppImage | 1 + ...ktop-1.2.1-linux-x86_64.AppImage.sha512sum | 1 + ...-1.2.1-linux-x86_64.AppImage.sha512sum.sig | 16 +++ .../mocks/sigko-shasumok/update-info.json | 3 + ...r-live-desktop-1.2.1-linux-x86_64.AppImage | 1 + ...ktop-1.2.1-linux-x86_64.AppImage.sha512sum | 1 + ...-1.2.1-linux-x86_64.AppImage.sha512sum.sig | 16 +++ .../mocks/sigok-shasumko/update-info.json | 3 + ...r-live-desktop-1.2.1-linux-x86_64.AppImage | 1 + ...ktop-1.2.1-linux-x86_64.AppImage.sha512sum | 1 + ...-1.2.1-linux-x86_64.AppImage.sha512sum.sig | 16 +++ .../mocks/sigok-shasumok/update-info.json | 3 + src/main/updater/pgpHelper.js | 40 ++++++ src/main/updater/updater.spec.js | 114 ++++++++++++++++++ yarn.lock | 111 ++++++++++++++++- 29 files changed, 836 insertions(+), 48 deletions(-) delete mode 100644 src/main/autoUpdate.js create mode 100644 src/main/updater/createAppUpdater.js create mode 100644 src/main/updater/createElectronAppUpdater.js create mode 100644 src/main/updater/createMockAppUpdater.js create mode 100644 src/main/updater/ledger-pubkey.js create mode 100644 src/main/updater/mocks/mock-privkey-1.asc create mode 100644 src/main/updater/mocks/mock-privkey-2.asc create mode 100644 src/main/updater/mocks/mock-pubkey-1.asc create mode 100644 src/main/updater/mocks/mock-pubkey-2.asc create mode 100644 src/main/updater/mocks/pubkey-1.asc create mode 100644 src/main/updater/mocks/pubkey-2.asc create mode 100644 src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage create mode 100644 src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum create mode 100644 src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig create mode 100644 src/main/updater/mocks/sigko-shasumok/update-info.json create mode 100644 src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage create mode 100644 src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum create mode 100644 src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig create mode 100644 src/main/updater/mocks/sigok-shasumko/update-info.json create mode 100644 src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage create mode 100644 src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum create mode 100644 src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig create mode 100644 src/main/updater/mocks/sigok-shasumok/update-info.json create mode 100644 src/main/updater/pgpHelper.js create mode 100644 src/main/updater/updater.spec.js diff --git a/package.json b/package.json index e1702c32..1896a7f7 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "lru-cache": "^4.1.3", "measure-scrollbar": "^1.1.0", "moment": "^2.22.2", + "openpgp": "^4.2.1", "qrcode": "^1.2.0", "qrloop": "0.8.1", "qs": "^6.5.1", diff --git a/src/config/errors.js b/src/config/errors.js index c9ea06a3..493f5fa8 100644 --- a/src/config/errors.js +++ b/src/config/errors.js @@ -51,3 +51,8 @@ export const FeeNotLoaded = createCustomErrorClass('FeeNotLoaded') export const NoDBPathGiven = createCustomErrorClass('NoDBPathGiven') export const DBWrongPassword = createCustomErrorClass('DBWrongPassword') export const DBNotReset = createCustomErrorClass('DBNotReset') + +// auto-update errors +export const UpdateIncorrectHash = createCustomErrorClass('UpdateIncorrectHash') +export const UpdateIncorrectSig = createCustomErrorClass('UpdateIncorrectSig') +export const UpdateFetchFileFail = createCustomErrorClass('UpdateFetchFileFail') diff --git a/src/main/autoUpdate.js b/src/main/autoUpdate.js deleted file mode 100644 index 1b50e078..00000000 --- a/src/main/autoUpdate.js +++ /dev/null @@ -1,32 +0,0 @@ -// @flow - -import { app, BrowserWindow } from 'electron' -import { autoUpdater } from 'electron-updater' - -type SendFunction = (type: string, data: *) => void - -export default (notify: SendFunction) => { - autoUpdater.on('checking-for-update', () => notify('checking')) - autoUpdater.on('update-available', info => notify('updateAvailable', info)) - autoUpdater.on('update-not-available', () => notify('updateNotAvailable')) - autoUpdater.on('error', err => notify('error', err)) - autoUpdater.on('download-progress', progress => notify('downloadProgress', progress)) - autoUpdater.on('update-downloaded', () => notify('downloaded')) - - autoUpdater.checkForUpdatesAndNotify() -} - -export function quitAndInstall() { - setImmediate(() => { - 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') - }) - - autoUpdater.quitAndInstall(false) - }) -} diff --git a/src/main/bridge.js b/src/main/bridge.js index e302d559..e6527fa4 100644 --- a/src/main/bridge.js +++ b/src/main/bridge.js @@ -12,7 +12,6 @@ import user from 'helpers/user' import { resolveLogsDirectory, cleanUpBeforeClosingSync } from 'helpers/log' import { deserializeError } from 'helpers/errors' -import setupAutoUpdater, { quitAndInstall } from './autoUpdate' import { setInternalProcessPID } from './terminator' import { getMainWindow } from './app' @@ -145,13 +144,3 @@ ipcMain.on('sentryLogsChanged', (event, payload) => { if (!p) return p.send({ type: 'sentryLogsChanged', payload }) }) - -// TODO move this to "command" pattern -ipcMain.on('updater', (event, { type, data }) => { - const handler = { - init: setupAutoUpdater, - quitAndInstall, - }[type] - const send = (type: string, data: *) => event.sender.send('updater', { type, data }) - handler(send, data, type) -}) diff --git a/src/main/updater/createAppUpdater.js b/src/main/updater/createAppUpdater.js new file mode 100644 index 00000000..1346040e --- /dev/null +++ b/src/main/updater/createAppUpdater.js @@ -0,0 +1,86 @@ +// @flow + +import { UpdateIncorrectHash, UpdateIncorrectSig } from 'config/errors' +import * as pgpHelper from './pgpHelper' + +type Opts = { + filename: string, + computeHash: () => Promise, + getNextKey: (?string) => Promise, + getNextKeySignature: string => Promise, + getHashFile: () => Promise, + getHashFileSignature: () => Promise, +} + +export default function createAppUpdater(opts: Opts): { verify: () => Promise } { + const { + filename, + computeHash, + getNextKey, + getNextKeySignature, + getHashFile, + getHashFileSignature, + } = opts + + // main logic: + // - fetch hashFile + its signature + // - verify signature + // - compare hash with update hash + // throw if any step fail. + async function verify() { + const [hashFile, hashFileSignature] = await Promise.all([getHashFile(), getHashFileSignature()]) + await verifyHashFileSignature(hashFile, hashFileSignature, await getNextKey()) + await compareHash(hashFile) + } + + // compute the update hash and compare it to the hash located in hash file + async function compareHash(hashFile) { + const computedHash = await computeHash() + const hashFromFile = extractHashFromHashFile(hashFile, filename) + if (hashFromFile !== computedHash) { + throw new UpdateIncorrectHash(computedHash) + } + } + + // verify hash signature with given pubkey + // if it fails, try to get next key and repeat + // if no more key, throw + async function verifyHashFileSignature(hash, sigContent, pubKey) { + try { + await pgpHelper.verify(hash, sigContent, pubKey) + } catch (err) { + try { + const nextPubKey = await getNextPubKey(pubKey) + await verifyHashFileSignature(hash, sigContent, nextPubKey) + } catch (err) { + throw new UpdateIncorrectSig() + } + } + } + + // fetch the next pubkey based on the previous key fingerprint + // also fetch signature, and verify against previous pubkey + async function getNextPubKey(pubKey) { + const fingerprint = await pgpHelper.getFingerprint(pubKey) + const nextPubKey = await getNextKey(fingerprint) + const nextPubKeySignature = await getNextKeySignature(fingerprint) + await pgpHelper.verify(nextPubKey, nextPubKeySignature, pubKey) + return nextPubKey + } + + return { + verify, + } +} + +// a hash file looks like: " \n " +// we only need the hash for the given filename +function extractHashFromHashFile(hashFile, filename) { + let hash + hashFile.split('\n').find(r => { + const row = r.split(/\s+/) + hash = row[1] === filename ? row[0] : '' + return !!hash + }) + return hash +} diff --git a/src/main/updater/createElectronAppUpdater.js b/src/main/updater/createElectronAppUpdater.js new file mode 100644 index 00000000..166b1057 --- /dev/null +++ b/src/main/updater/createElectronAppUpdater.js @@ -0,0 +1,62 @@ +// @flow + +import crypto from 'crypto' +import path from 'path' +import fs from 'fs' + +import network from 'api/network' +import { UpdateFetchFileFail } from 'config/errors' +import { fsReadFile } from 'helpers/fs' +import createAppUpdater from './createAppUpdater' + +import pubKey from './ledger-pubkey' + +export default async ({ feedURL, updateVersion }: { feedURL: string, updateVersion: string }) => { + const { app } = require('electron') + const updateFolder = path.resolve(app.getPath('userData'), '__update__') + const { fileName: filename } = await readUpdateInfos(updateFolder) + + const hashFileURL = `${feedURL}/ledger-live-desktop-${updateVersion}.sha512sum` + const hashSigFileURL = `${feedURL}/ledger-live-desktop-${updateVersion}.sha512sum.sig` + const keysURL = `${feedURL}/pubkeys` + + return createAppUpdater({ + filename, + computeHash: () => sha512sumPath(path.resolve(updateFolder, filename)), + getHashFile: () => getDistantFileContent(hashFileURL), + getHashFileSignature: () => getDistantFileContent(hashSigFileURL), + getNextKey: (fingerprint: ?string) => + fingerprint ? getDistantFileContent(`${keysURL}/${fingerprint}.asc`) : pubKey, + getNextKeySignature: async (fingerprint: string) => + getDistantFileContent(`${keysURL}/${fingerprint}.asc.sig`), + }) +} + +// read the electron-updater file. we basically only need the filename here, +// because the hash file contains hashes for all platforms (better to have +// only 1 file to sign lel) +export async function readUpdateInfos(updateFolder: string) { + const updateInfoPath = path.resolve(updateFolder, 'update-info.json') + const updateInfoContent = await fsReadFile(updateInfoPath) + return JSON.parse(updateInfoContent) +} + +// compute hash for given path. i guess we only need that here +export function sha512sumPath(filePath: string) { + return new Promise((resolve, reject) => { + const sum = crypto.createHash('sha512') + const stream = fs.createReadStream(filePath) + stream.on('data', data => sum.update(data)) + stream.on('end', () => resolve(sum.digest('hex'))) + stream.on('error', reject) + }) +} + +async function getDistantFileContent(url: string) { + try { + const res = await network({ method: 'GET', url }) + return res.data + } catch (err) { + throw new UpdateFetchFileFail(url) + } +} diff --git a/src/main/updater/createMockAppUpdater.js b/src/main/updater/createMockAppUpdater.js new file mode 100644 index 00000000..84b6045a --- /dev/null +++ b/src/main/updater/createMockAppUpdater.js @@ -0,0 +1,40 @@ +// @flow + +import createAppUpdater from './createAppUpdater' + +export default ({ + filename, + computedHash, + hashFile, + signature, + pubKey, + pubKeys, +}: { + filename: string, + computedHash: string, + hashFile: string, + signature: string, + pubKey: string, + pubKeys: Array<{ fingerprint: string, content: string, signature: string }>, +}) => + createAppUpdater({ + filename, + computeHash: () => Promise.resolve(computedHash), + getHashFile: () => Promise.resolve(hashFile), + getHashFileSignature: () => Promise.resolve(signature), + getNextKey: async (fingerprint: ?string) => { + if (!fingerprint) return Promise.resolve(pubKey) + const key = pubKeys.find(k => k.fingerprint === fingerprint) + if (!key) { + throw new Error(`Cannot find key for fingerprint ${fingerprint}`) + } + return key.content + }, + getNextKeySignature: async (fingerprint: string) => { + const key = pubKeys.find(k => k.fingerprint === fingerprint) + if (!key) { + throw new Error(`Cannot find signature for key ${fingerprint}`) + } + return key.signature + }, + }) diff --git a/src/main/updater/ledger-pubkey.js b/src/main/updater/ledger-pubkey.js new file mode 100644 index 00000000..f39f2859 --- /dev/null +++ b/src/main/updater/ledger-pubkey.js @@ -0,0 +1,111 @@ +// TODO: it's actually my key. replace with qorum key. + +export default `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=97OE +-----END PGP PUBLIC KEY BLOCK-----` diff --git a/src/main/updater/mocks/mock-privkey-1.asc b/src/main/updater/mocks/mock-privkey-1.asc new file mode 100644 index 00000000..845fe737 --- /dev/null +++ b/src/main/updater/mocks/mock-privkey-1.asc @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQHYBFv8BAcBBADRLTFoRU3IUz37E70wy2jv7jg7QLSoHnJglcUYzq+RCBnQAfxQ +NOcQtJDKOJyo1v3819o5pxnrxWAutM/CNpjanzcWM1yLfHjrKo8BbHlmIbWBmV7w +8te+rm/VlyNi30D1eIIJy276nByW4rMLcfFe4T055xbEvRNvQzR+KfrCBwARAQAB +AAP8Dv3XMart5TKaGZmTkKCzd6ayHwUHLZlpByUUyC9gA9v5xZ+uzqzL9rWKPwQJ +rkwiTfHGcSVZxT0TJKXO0hOJpthgqbMnHokIhrYeNrDbU4l4tgE74Ik+8F3dGGns +C48Q+6lfACbpLMz7wq+60lLT7tzBKgYGA4G35W9+CY9jz8ECAOX3SpqFpOkoYcjh +GsmBSlTs9Kz4JyZDdigiM7xrroe9zkXtwuGUY2X14le/vCoZl3EP/WYV2CY8uODm +cMTYbaECAOjbZZPjudH9Ux234qdv/lMsiYwZ86aQmSUnaR2tNrTmlMiW60yKbsMC +smP0A+wZvpI9U6hMU8cbNtN7SW8WfqcCAOabL3NCCeQbDQBHI/0oKqrSIkMTQzKL +1MdUcvitzHBiKrOGF0PUgiZ0mNVXsSvLH0grULllAwfws3UeRmYGVwWfE7QiRm9v +IEJhciAoTW9jayBLZXkgMSkgPGZvb0BiYXIuY29tPojOBBMBCAA4FiEERZ/aCM7e +heCvgo2lv5b5SATkBG4FAlv8BAcCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA +CgkQv5b5SATkBG5tlwP/V1nUnbcJloznFVZFQJtLdCd9IG15LYeexd0/B3sBZm5d +Jze29tkNWblHc+xBxAp6PuuRPyvBKZd4grPNCfD8tzFLRv3jeVXrrYjOzb4G6ug6 +15Zd0jFn6taCykCpgLcuKLDXm9lnCIR9cHD0IHhojdjdLjGbL1Kpu/N8xoOJlcOd +AdgEW/wEBwEEAOUUNwoEzjt/+YReQsPmQsKuBi9KxU1dvbcI0OG5xYdlp4ZgG9Gj +hJwvvH1aHiNU3qyRLF2PjL7dmM6xFIppTF8sakW2H34oE2nY9IS0HYdFNaIag0vk +G0isSrdRuVl38EClkvEEXnb/L7y/J3EDiBwW+4jWTd0SnS9C1IX+pzozABEBAAEA +A/9e21eNIP9K2Qu3uZZ46wSS+5+Y/qOjjSENRSvFhHjcP4Y2mN58GPNE/2lUGLhp +FfAc0gJmfZYgxL7NO7tPMxoJeFs7oo7duWua2rugLHUVH55ApiJXzOMlX4rpDNzR +eYsI4WOUgq1bMTjLoy/Zuz00IrItEuPqKFWot9CI+Rz8uQIA8bCtS1bn/2Tg9Bww +td3WQqERz9AOE3eSDynRrZ/APJrphMhEvL2YC03kS8U5x32cSq0vS8fMlUuGZXvf +Nxfq2wIA8qRjmyFCG+V3wUC6lymWj8DAh+BINCmuDRiXCzHQ1D1q+Nid1bO18dvd +tI81lmn7IIn9esKxU8ym/z9o22YRiQH/SHFD/b/KSZKKQnKOrcKtj2gLT0MW/gL2 +mUxenook+hEoZyBwIOEabz2o5rznmXCXnxlgZ2sIX4Ymx67b/0BtuqaSiLYEGAEI +ACAWIQRFn9oIzt6F4K+CjaW/lvlIBOQEbgUCW/wEBwIbDAAKCRC/lvlIBOQEbkeR +BACd1FWQvo1qNTuNC9IwHLLhBQbT8OEHn/TIP2daR0dNjsvcfvpmX4kXEHWFRW7G +CWaoWMNYtWFk8GrGXKDjZ50wLCw9rj/xJdjdz8vDjVZT3Si0MOrDI9WJLbBNrCBO +h52YTF89KXceUoBVqOgVta4ArbiagOK35ezlWjvg6H2slw== +=IP04 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/main/updater/mocks/mock-privkey-2.asc b/src/main/updater/mocks/mock-privkey-2.asc new file mode 100644 index 00000000..726a25d4 --- /dev/null +++ b/src/main/updater/mocks/mock-privkey-2.asc @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQHYBFv8BCEBBADA0YaD4Sa50kD9g2o/t6qkMMBZtmYt0kh/F+rAEBCYQL1H6dhA +FAUt7k9EYLAzUw03ByAB2WR097te1xEos9eUWz9YIUMJxE6Tk66kys0z+X1uZQcc +Gywxrg9EsMWQPwDSOJjFEKsMV81X7UjInh38+Dx/gy8sLUUClXDpo7HZiQARAQAB +AAP+ITy85ETOaR+uJkUd7ofZeV4R+iTv7hwedRIJXDwosCYQurG7nigVUHqQ3WeR +V/eMAp1OziLMJ0GA4fNOuOZvVY3ZuwcQFaejRMkItOgvuc4lZZ5IvZzoQrJ04NOO +mT6dugZpD3MYtnxoEftNvjEE/ezc1Wch5Nu06jo0fqb5tsUCANhuUemRQkqL+1sZ +HdhQT3vgxRae1WebH8pEjczLflNteydGE8+ND38tAYaQOqSj8Gs6v7uH9YUwHMMZ +vVT6/msCAOQSEFQ40mG9x+pTLWqfHFR6VeBKOuz+AnygYKIjKJ8eTgXay/2bT9Pg +qbeDYYTtfB0XFvUDd1b7xLBPZ1QTnNsCAI/8OTu958QxWoApsH4vEj7fuQXbOyYs +LERh5o7rMUeAacWStv5+i6sBLo63KQWLlfo/vjVJFQI1nxLApg8JKIubkbQmRml6 +eiBCYXJyIChNb2NrIEtleSAyKSA8Zml6ekBiYXJyLmNvbT6IzgQTAQgAOBYhBGcP +SZyL0UpLah6eU55NLhDLPtM/BQJb/AQhAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B +AheAAAoJEJ5NLhDLPtM/xMkD/2YwwDlKWS9vAe9Sl9dqU+Rz8OqNTrjhCDUSoGEr +83ltoGkl2unBOWXT8JQVBBokGexUdKhDS/2gEF+WxpAUORE9qVFkARMZgH19bIYf +mNmJY+0t0YttiOvx7GXUnynJ6V3iEX96uMbxfBdIbHF/thzK0yCG/pD9DoLJPB7z +nRA+nQHYBFv8BCEBBACkrayUHg6fK2dMI0gZKdR0WMyxwXXKg+kc1tlsrxqfb/bH +UVtCy+VlpIS2SGcEAfXSIPmeesj5OA4p6e6egPeVFoy6qWOTLR0U9x4GqW/uUsEz +FAiB6IRK1h2HMLsp5wOV8RTf2ENDiObRh3zqEEJ7zANHQm4sDoaRUIHo5RsbuQAR +AQABAAP6A/gywYRC9nfBnu53ybF+L0rHrl47NGU/HISaS5oh8mdsw6u0nzTShAN1 +9n3iy9AvSIAmfDk5+HXvM3fx2kzCCwJLv1Jxm7a9S92L6kzPQB/JdQ5c84cBespY +gzK8phGfCa2O0F+/mdBi5hKNuScM5g55UWpr17v0bbznEuhpF60CAMC4JKO32xUi +l2flVjhBR316i5IJmvUvN9Jw0sLtXuHS4tgaTWWPgThCEKc4iIy1Ngyy72mW4XTa +OzFNitInzw0CANrAbpzgKFawx+Kh2DrOUrj8IuXwf1NtajnfQfCk8nn4nzsXglq5 +A/z9pSHuTGck5PFJT9PGiNH1ohxRciFfdF0CAJZLcDj09dG1UbgF4JelXhIJFYAE +VJQTKLLZq9H2W2ClA07gKkfvN1r++5lbaIB5fNJkY3BKNzuuEKAICx3hMqajrYi2 +BBgBCAAgFiEEZw9JnIvRSktqHp5Tnk0uEMs+0z8FAlv8BCECGwwACgkQnk0uEMs+ +0z8XgAP/c75yAyOCLLKUjD2Ov3Xwn/4YR8/mayZNFGkJ28FRWyxrw4zWVE3TCJVK +8AKUpa8QA9FL0maahWV4EQzcOvO4MKwohbUZ6/XLLd9NhMa9L5pZzsGUT2JQQN/i +yk+I22jpK0+ILJwqwB28EBzwkqRJLSgfPwbeRJ9Ni7D9qf8T1VU= +=vt8y +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/main/updater/mocks/mock-pubkey-1.asc b/src/main/updater/mocks/mock-pubkey-1.asc new file mode 100644 index 00000000..9230034c --- /dev/null +++ b/src/main/updater/mocks/mock-pubkey-1.asc @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EW/wEBwEEANEtMWhFTchTPfsTvTDLaO/uODtAtKgecmCVxRjOr5EIGdAB/FA0 +5xC0kMo4nKjW/fzX2jmnGevFYC60z8I2mNqfNxYzXIt8eOsqjwFseWYhtYGZXvDy +176ub9WXI2LfQPV4ggnLbvqcHJbiswtx8V7hPTnnFsS9E29DNH4p+sIHABEBAAG0 +IkZvbyBCYXIgKE1vY2sgS2V5IDEpIDxmb29AYmFyLmNvbT6IzgQTAQgAOBYhBEWf +2gjO3oXgr4KNpb+W+UgE5ARuBQJb/AQHAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B +AheAAAoJEL+W+UgE5ARubZcD/1dZ1J23CZaM5xVWRUCbS3QnfSBteS2HnsXdPwd7 +AWZuXSc3tvbZDVm5R3PsQcQKej7rkT8rwSmXeIKzzQnw/LcxS0b943lV662Izs2+ +BuroOteWXdIxZ+rWgspAqYC3Liiw15vZZwiEfXBw9CB4aI3Y3S4xmy9SqbvzfMaD +iZXDuI0EW/wEBwEEAOUUNwoEzjt/+YReQsPmQsKuBi9KxU1dvbcI0OG5xYdlp4Zg +G9GjhJwvvH1aHiNU3qyRLF2PjL7dmM6xFIppTF8sakW2H34oE2nY9IS0HYdFNaIa +g0vkG0isSrdRuVl38EClkvEEXnb/L7y/J3EDiBwW+4jWTd0SnS9C1IX+pzozABEB +AAGItgQYAQgAIBYhBEWf2gjO3oXgr4KNpb+W+UgE5ARuBQJb/AQHAhsMAAoJEL+W ++UgE5ARuR5EEAJ3UVZC+jWo1O40L0jAcsuEFBtPw4Qef9Mg/Z1pHR02Oy9x++mZf +iRcQdYVFbsYJZqhYw1i1YWTwasZcoONnnTAsLD2uP/El2N3Py8ONVlPdKLQw6sMj +1YktsE2sIE6HnZhMXz0pdx5SgFWo6BW1rgCtuJqA4rfl7OVaO+DofayX +=jimW +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/main/updater/mocks/mock-pubkey-2.asc b/src/main/updater/mocks/mock-pubkey-2.asc new file mode 100644 index 00000000..e0928360 --- /dev/null +++ b/src/main/updater/mocks/mock-pubkey-2.asc @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EW/wEIQEEAMDRhoPhJrnSQP2Daj+3qqQwwFm2Zi3SSH8X6sAQEJhAvUfp2EAU +BS3uT0RgsDNTDTcHIAHZZHT3u17XESiz15RbP1ghQwnETpOTrqTKzTP5fW5lBxwb +LDGuD0SwxZA/ANI4mMUQqwxXzVftSMieHfz4PH+DLywtRQKVcOmjsdmJABEBAAG0 +JkZpenogQmFyciAoTW9jayBLZXkgMikgPGZpenpAYmFyci5jb20+iM4EEwEIADgW +IQRnD0mci9FKS2oenlOeTS4Qyz7TPwUCW/wEIQIbAwULCQgHAgYVCgkICwIEFgID +AQIeAQIXgAAKCRCeTS4Qyz7TP8TJA/9mMMA5SlkvbwHvUpfXalPkc/DqjU644Qg1 +EqBhK/N5baBpJdrpwTll0/CUFQQaJBnsVHSoQ0v9oBBflsaQFDkRPalRZAETGYB9 +fWyGH5jZiWPtLdGLbYjr8exl1J8pyeld4hF/erjG8XwXSGxxf7YcytMghv6Q/Q6C +yTwe850QPriNBFv8BCEBBACkrayUHg6fK2dMI0gZKdR0WMyxwXXKg+kc1tlsrxqf +b/bHUVtCy+VlpIS2SGcEAfXSIPmeesj5OA4p6e6egPeVFoy6qWOTLR0U9x4GqW/u +UsEzFAiB6IRK1h2HMLsp5wOV8RTf2ENDiObRh3zqEEJ7zANHQm4sDoaRUIHo5Rsb +uQARAQABiLYEGAEIACAWIQRnD0mci9FKS2oenlOeTS4Qyz7TPwUCW/wEIQIbDAAK +CRCeTS4Qyz7TPxeAA/9zvnIDI4IsspSMPY6/dfCf/hhHz+ZrJk0UaQnbwVFbLGvD +jNZUTdMIlUrwApSlrxAD0UvSZpqFZXgRDNw687gwrCiFtRnr9cst302Exr0vmlnO +wZRPYlBA3+LKT4jbaOkrT4gsnCrAHbwQHPCSpEktKB8/Bt5En02LsP2p/xPVVQ== +=Bk7g +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/main/updater/mocks/pubkey-1.asc b/src/main/updater/mocks/pubkey-1.asc new file mode 100644 index 00000000..fd38889b --- /dev/null +++ b/src/main/updater/mocks/pubkey-1.asc @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +sfB6/eqK4Q== +=UKRx +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/main/updater/mocks/pubkey-2.asc b/src/main/updater/mocks/pubkey-2.asc new file mode 100644 index 00000000..44cd4ec0 --- /dev/null +++ b/src/main/updater/mocks/pubkey-2.asc @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +sEQGv7WFrXcyYM38 +=/9tA +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage b/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage new file mode 100644 index 00000000..05b2f3e6 --- /dev/null +++ b/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage @@ -0,0 +1 @@ +sigko-shasumok diff --git a/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum b/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum new file mode 100644 index 00000000..b853635d --- /dev/null +++ b/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum @@ -0,0 +1 @@ +26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage diff --git a/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig b/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig new file mode 100644 index 00000000..1e2574a1 --- /dev/null +++ b/src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEEhhzv+bPVtMW9pE7JhKxNYN+ul88FAlvKAJIACgkQhKxNYN+u +l8/FJQ/+PvYCbEZB77BGBHPvoEjkTbp4lP9RYzIS1Ial0HUZ6CDsMOfFEVsfM7VQ +MTluvUERshVc5OX2Z4Y+G/eMYHInwZX+ytjWQ7/rXa4M/3na97ND/JwQjNmiIzjT +Fxaiis0BqwulOff9bvgu0AKrA6Bq/DiBKV2EuWwNKVADuUhoAru53YWtAg0mFU87 +XRSR6bww1/fDe9pD/PzWNJxonMHjZDz3KYQOh5RXqNE9sgI51hMVpqNW5O1H/N5P +GvqNDB0piyH85CbiZgvYhDVzVBhpuu1+gQLYMYRAsU8Grp5jJmnbTX81kJsqzyrY +1MNwH4v/nr1KIA/H/SwCXLgDX013VPc7U0rD74dZI8IJn2le/72U5jZknPM0B7vl +KBWgEOq3XYSqW5jlY4N4wdHmNvMiTureiLfZmOjV2XLk8aLX+/LpABHLBduGM3hZ +wsj7+av7jFce4w6bBNoew+P3OMS6aHyORTUkMKvGSACI4PchGHSgG6rmGM183o64 +opexXsnit5vIYQKbJATp2nuUmW9TD2kK6mDjk2Hio4wAR6KvawUf4prIkF1rR/M5 +4DrJ3Oq9ntvmApyE8o5t/uOFE/HSuS/+ZGn1nieSPJTME6WtSYWV9gxfLOgErpXD +GoibMFoInfjh/rPE5JwoCuIaBGYOuFS9ytwlYAPcA7sJQlb9vLE= +=KWhh +-----END PGP SIGNATURE----- diff --git a/src/main/updater/mocks/sigko-shasumok/update-info.json b/src/main/updater/mocks/sigko-shasumok/update-info.json new file mode 100644 index 00000000..fb193762 --- /dev/null +++ b/src/main/updater/mocks/sigko-shasumok/update-info.json @@ -0,0 +1,3 @@ +{ + "fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage" +} diff --git a/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage b/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage new file mode 100644 index 00000000..20008850 --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage @@ -0,0 +1 @@ +sigok-shasumok diff --git a/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum b/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum new file mode 100644 index 00000000..8ed6ec6e --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum @@ -0,0 +1 @@ +this_is_ko_26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage diff --git a/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig b/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig new file mode 100644 index 00000000..c8da22d6 --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEESKQ3KFYSBLGhLWtAHS/CMF4ss5kFAlvKBL4ACgkQHS/CMF4s +s5lbKxAAndUSWP3tL+aBl/Jn1Fjr48bPlRHFpqUY1bW59uAEB0C1xkHCvExkH7Wh +oc1nT3DsE+khwfhPeruKiNBcgtLtPWBW0PaY7N1tcafzYVldBZYSRfZotv1wEuqF +vt4kgqzCTPLOLfaUxTP7esDv6oIdHY7bah/0QsgvPiOxtT+/Kb/6OIXGT6IyWJxX +6Z/oc2lzkxmIyALFp53YEejUeCayiATVcoR15MCOK2zofU9RBk0BI2R50cNlefpX +WttXOfFP5l2EecZA0S3vA8am66+nG3jKHA8cLIqc//X1pgFCNbZXxCt2kTiE4ri6 +8XW0ne9tGvHk+fG2/4ZhNK2dqzwSD5Yitd2g5HU3ZOUxVdmH5K2XyFF5Zhixca+c +ivFlH00LEagF8hhxkf2tZWSG8fLvffNmzPdkQ1HySq4FpT/K33twb+MYHAWtwEP6 +/Epyiq7S15aB2SRkdOnBv1VQWf0wGZO/ABKutgF8xrOh13prLLSmXFenIdXGsdnP +2Wlawn6GyvSOcK+s6Z6AuETXXVdGnUQDuwjUyqQXUWp4Fm0xInTTPcgruh7Ip3Ra +vBeQcYr4i/aB37X6fUVogzq4Sy2DvDUnG7uMmaVyDxK6RhQ9/M3Jql/6ffCJPXm5 +J7AqoM/QCHfVkXhvgKKwq8hxb6JLSykjB9h//AonCVRcKW/dqY8= +=kaOO +-----END PGP SIGNATURE----- diff --git a/src/main/updater/mocks/sigok-shasumko/update-info.json b/src/main/updater/mocks/sigok-shasumko/update-info.json new file mode 100644 index 00000000..fb193762 --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumko/update-info.json @@ -0,0 +1,3 @@ +{ + "fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage" +} diff --git a/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage b/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage new file mode 100644 index 00000000..20008850 --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage @@ -0,0 +1 @@ +sigok-shasumok diff --git a/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum b/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum new file mode 100644 index 00000000..b853635d --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum @@ -0,0 +1 @@ +26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage diff --git a/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig b/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig new file mode 100644 index 00000000..d4a145f1 --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEESKQ3KFYSBLGhLWtAHS/CMF4ss5kFAlvJ+ysACgkQHS/CMF4s +s5mepQ/8DkHSiLEdpMx1GHRX7ElRbhefD7ZAbBCaf/1igpx9K/+SrKvVkgywX2us +D+MU6vmxcFLuMMb5NWpY2s7/0iC+fj36x33fCSvRkmmxxXWf+SzyFozX8sAc/vOo +2x1KTR5jvxspZS0/ERX2KH/88hIWH9kzLsrk/qvppWXpJvEVS5hEHKijLST1aw+c +4L01n9dBwTAsMLo8xm7ybcU86yKUsvbd96XJHuQoqJeg4TOqveUuEmh1J/9jaXnO +3ZcPOtOP42jKuIOFglGMTgtFwR/WN7kxb5eS7wDodvTJE0YzpxeugW4WEfJ2nspZ +3ZCW3G4DD38/uN5MGnPEM77pqKqDZJY7wPHTO2PbXa6w5+WkW9cvE38fBikdQsnX +nKIuvsqQRBBsIOmVe4m+VJ3wrBPpk7IrYSp54+M8XWf8EkkyZYxtMhE+1bqPhRqG +aTIw6qJXJ+yDowe8cFaEybs4xcUl2t4QWHIXM5Y09ZFcwhD5UQwtXz9x6jJogR2j +W8MLeNnAUhRxFY/hySq+wJasKMDKbWXGPk3TmMNn8P+6g6HEllM4KucJty2OH50t +nJbYt0da3Br2W8os1RPA71MmfW6nGhzcFhX2ShKLicyw46Tyc0GGaO29oDwgQkRc +wcbFJmZVMj9ZuZArmfhFL53ZQj9kbAF2qWPNt+L369jSmw5tAko= +=z2+Z +-----END PGP SIGNATURE----- diff --git a/src/main/updater/mocks/sigok-shasumok/update-info.json b/src/main/updater/mocks/sigok-shasumok/update-info.json new file mode 100644 index 00000000..fb193762 --- /dev/null +++ b/src/main/updater/mocks/sigok-shasumok/update-info.json @@ -0,0 +1,3 @@ +{ + "fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage" +} diff --git a/src/main/updater/pgpHelper.js b/src/main/updater/pgpHelper.js new file mode 100644 index 00000000..d31f41d6 --- /dev/null +++ b/src/main/updater/pgpHelper.js @@ -0,0 +1,40 @@ +// @flow + +import * as openpgp from 'openpgp' + +export async function getFingerprint(pubKey: string) { + const { keys } = await openpgp.key.readArmored(pubKey) + if (!keys.length) { + throw new Error('No key found in pubKey') + } + return keys[0].getFingerprint() +} + +export async function verify(msgContent: string, sigContent: string, pubKeyContent: string) { + const signature = await openpgp.signature.readArmored(sigContent) + const message = openpgp.message.fromText(msgContent) + const { keys: publicKeys } = await openpgp.key.readArmored(pubKeyContent) + const pgpOpts = { message, publicKeys, signature } + const verified = await openpgp.verify(pgpOpts) + + if (verified.signatures.length === 0) { + throw new Error('No signature found') + } + + if (!verified.signatures.every(sig => sig.valid)) { + throw new Error('Signature check failed') + } +} + +export async function sign(msgContent: string, privKeyContent: string) { + const privKey = (await openpgp.key.readArmored(privKeyContent)).keys[0] + + const options = { + message: openpgp.cleartext.fromText(msgContent), + privateKeys: [privKey], + detached: true, + } + + const signed = await openpgp.sign(options) + return signed.signature +} diff --git a/src/main/updater/updater.spec.js b/src/main/updater/updater.spec.js new file mode 100644 index 00000000..69c5801c --- /dev/null +++ b/src/main/updater/updater.spec.js @@ -0,0 +1,114 @@ +import path from 'path' +import crypto from 'crypto' + +import { fsReadFile } from 'helpers/fs' +import { UpdateIncorrectHash, UpdateIncorrectSig } from 'config/errors' +import createMockAppUpdater from './createMockAppUpdater' +import { sha512sumPath, readUpdateInfos } from './createElectronAppUpdater' +import * as pgpHelper from './pgpHelper' + +const base = path.resolve(__dirname, 'mocks') + +describe('AppUpdater', () => { + describe('simple cases', () => { + test('should not throw if correct hash & correct sig', async () => { + await verifyMockFolder('sigok-shasumok', 'pubkey-1.asc') + }) + + test('should throw if correct hash but incorrect pubkey', async () => { + const p = verifyMockFolder('sigok-shasumok', 'pubkey-2.asc') + await expectFail(p, UpdateIncorrectSig) + }) + + test('should throw if incorrect hash but correct sig', async () => { + const p = verifyMockFolder('sigok-shasumko', 'pubkey-1.asc') + await expectFail(p, UpdateIncorrectHash) + }) + + test('should throw if correct hash but incorrect sig', async () => { + const p = verifyMockFolder('sigko-shasumok', 'pubkey-1.asc') + await expectFail(p, UpdateIncorrectSig) + }) + }) + + describe('key invalidation', () => { + test('should verify successfully by using getNextKey()', async () => { + const filename = 'foo.exe' + const mockPrivKey1 = await fsReadFile(path.resolve(base, 'mock-privkey-1.asc'), 'ascii') + const mockPubKey1 = await fsReadFile(path.resolve(base, 'mock-pubkey-1.asc'), 'ascii') + const mockPrivKey2 = await fsReadFile(path.resolve(base, 'mock-privkey-2.asc'), 'ascii') + const mockPubKey2 = await fsReadFile(path.resolve(base, 'mock-pubkey-2.asc'), 'ascii') + + // This is the desired scenario: + // + // Let's simulate a release. + // + // 1) create binary (lol) and its hash. this is basically the app update + const update = '0010011-content-of-the-app-01110001010111' + + // 2) create hash of update + const hash = sha512sum(update) + const hashFile = `${hash} ${filename}` // yeah, the hash file contains also file name + + // 3) sign the hash with the *KEY 2* (the app will use the *KEY 1* first) + const signature = await pgpHelper.sign(hashFile, mockPrivKey2) + + // 4) sign *KEY 2* with *KEY 1* and get the *KEY 1* fingerprint, ye. + const mockPubKey2Signature = await pgpHelper.sign(mockPubKey2, mockPrivKey1) + const mockPubKey1Fingerprint = await pgpHelper.getFingerprint(mockPubKey1) + + // 5) app updater, with all thoses infos + const updater = await createMockAppUpdater({ + filename, + computedHash: hash, + hashFile, + signature, + pubKey: mockPubKey1, + pubKeys: [ + { + fingerprint: mockPubKey1Fingerprint, + content: mockPubKey2, + signature: mockPubKey2Signature, + }, + ], + }) + + await updater.verify() + }) + }) +}) + +async function expectFail(promise, errType) { + let err + try { + await promise + } catch (e) { + err = e + } + expect(err).toBeDefined() + expect(err).toBeInstanceOf(errType) +} + +async function verifyMockFolder(folderName, pubKeyName, pubKeys = []) { + const mock = path.resolve(base, folderName) + const { fileName: filename } = await readUpdateInfos(mock) + const computedHash = await sha512sumPath(path.resolve(mock, filename)) + const hashFile = await fsReadFile(path.resolve(mock, `${filename}.sha512sum`), 'ascii') + const signature = await fsReadFile(path.resolve(mock, `${filename}.sha512sum.sig`), 'ascii') + const pubKey = await fsReadFile(path.resolve(base, pubKeyName)) + const updater = await createMockAppUpdater({ + filename, + computedHash, + hashFile, + signature, + pubKey, + pubKeys, + }) + await updater.verify() +} + +export function sha512sum(content) { + const sum = crypto.createHash('sha512') + sum.update(content) + return sum.digest('hex') +} diff --git a/yarn.lock b/yarn.lock index 9afa1d4a..cf086e7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1791,6 +1791,13 @@ redux "^4.0.0" reselect "^3.0.1" +"@mattiasbuelens/web-streams-polyfill@0.1.0-alpha.4": + version "0.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.1.0-alpha.4.tgz#fde9688fac83ff40e3be1a01973c867e12a74492" + integrity sha512-WAsiWLWc7ZNS1b0qFAoKSFLeqXesPa60YelVE3pPKc6pZ4iuSW9l6DBxY4hMPQj1dQCBDrUHJj/NDSjE85bTRQ== + dependencies: + "@types/whatwg-streams" "0.0.5" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -2112,6 +2119,11 @@ resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.6.tgz#128d1685a7c34d31ed17010fc87d6a12c1de6976" integrity sha512-5Th3OsZ4gTRdr9Mho83BQ23cex4sRhOR4XTG+m+cJc0FhtUBK9Vn62hBJ+pnQYnSxoPOsKoAPOx6FcphxBC8ng== +"@types/whatwg-streams@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@types/whatwg-streams/-/whatwg-streams-0.0.5.tgz#66c3852b8e33f2a2ca225a6d8fd8096e34d17fe6" + integrity sha512-y1UgRuGO64x/v+UIerA2AMquW/qxaIUD95rbf8FYxtVG//D3381+JexnZfcEiZSqXErdxdPmXpz8srY7gs9Grw== + "@types/ws@^3.2.0": version "3.2.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.2.1.tgz#b0c1579e58e686f83ce0a97bb9463d29705827fb" @@ -2346,6 +2358,13 @@ acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.6.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" integrity sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ== +address-rfc2822@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/address-rfc2822/-/address-rfc2822-2.0.4.tgz#2dbd3b8d6c2de1e957c1a8549dc012d40bbc3431" + integrity sha1-Lb07jWwt4elXwahUncAS1Au8NDE= + dependencies: + email-addresses "^3.0.0" + address@1.0.3, address@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" @@ -2721,6 +2740,10 @@ asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +"asmcrypto.js@github:openpgpjs/asmcrypto": + version "2.3.0" + resolved "https://codeload.github.com/openpgpjs/asmcrypto/tar.gz/6e4e407b9b8ae317925a9e677cc7b4de3e447e83" + asn1.js@^4.0.0: version "4.10.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" @@ -2730,6 +2753,15 @@ asn1.js@^4.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +asn1.js@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.0.1.tgz#7668b56416953f0ce3421adbb3893ace59c96f59" + integrity sha512-aO8EaEgbgqq77IEw+1jfx5c9zTbzvkfuRBuZsSsPnTHMkmd5AI4J6OtITLZFa381jReeaQL67J0GBTUu0+ZTVw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -4240,7 +4272,7 @@ bn.js@^3.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-3.3.0.tgz#1138e577889fdc97bbdab51844f2190dfc0ae3d7" integrity sha1-ETjld4if3Je72rUYRPIZDfwK49c= -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.4.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.11.8, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== @@ -4541,6 +4573,14 @@ buffer@^5.0.3: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.0.8: + version "5.2.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" + integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + buffer@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.0.tgz#53cf98241100099e9eeae20ee6d51d21b16e541e" @@ -6939,6 +6979,23 @@ elliptic@^6.0.0, elliptic@^6.2.3: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +"elliptic@github:openpgpjs/elliptic": + version "6.4.0" + resolved "https://codeload.github.com/openpgpjs/elliptic/tar.gz/e187e706e11fa51bcd20e46e5119054be4e2a4a6" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +email-addresses@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.0.2.tgz#a31280d19baf86669840a0aa45be1d7f6e7df315" + integrity sha512-IMn9dnwLMsgZjdUHswB/UZ0S8LQ/u+2/qjnHJ9tCtp3QHZsIYwJCiJOo2FT0i3CwwK/dtSODYtxuvzV4D9MY5g== + emoji-regex@^6.1.0: version "6.5.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" @@ -8705,6 +8762,14 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" +hash.js@^1.1.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812" + integrity sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -11123,7 +11188,7 @@ mini-css-extract-plugin@^0.4.0: loader-utils "^1.1.0" webpack-sources "^1.1.0" -minimalistic-assert@^1.0.0: +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== @@ -11363,7 +11428,7 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.1.1: +node-fetch@^2.1.1, node-fetch@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5" integrity sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA== @@ -11439,6 +11504,13 @@ node-loader@^0.6.0: resolved "https://registry.yarnpkg.com/node-loader/-/node-loader-0.6.0.tgz#c797ef51095ed5859902b157f6384f6361e05ae8" integrity sha1-x5fvUQle1YWZArFX9jhPY2HgWug= +node-localstorage@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-localstorage/-/node-localstorage-1.3.1.tgz#3177ef42837f398aee5dd75e319b281e40704243" + integrity sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ== + dependencies: + write-file-atomic "^1.1.4" + node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -11783,6 +11855,25 @@ opener@^1.4.3: resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" integrity sha1-XG2ixdflgx6P+jlklQ+NZnSskLg= +openpgp@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-4.2.1.tgz#5d693ca9d90f52f0826447a37596b6a9a5a725da" + integrity sha512-spO7D5LVS8H8ktpX55XDNzvIktMQA7CHZVCt3i4REeP3rbFlzvRbg0cR699yC/hwH0dx09/z+6C+fH4lmbyucw== + dependencies: + "@mattiasbuelens/web-streams-polyfill" "0.1.0-alpha.4" + address-rfc2822 "^2.0.3" + asmcrypto.js "github:openpgpjs/asmcrypto" + asn1.js "^5.0.0" + bn.js "^4.11.8" + buffer "^5.0.8" + elliptic "github:openpgpjs/elliptic" + hash.js "^1.1.3" + node-fetch "^2.1.2" + node-localstorage "~1.3.0" + pako "^1.0.6" + seek-bzip "github:openpgpjs/seek-bzip" + web-stream-tools "github:openpgpjs/web-stream-tools" + opn@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225" @@ -11976,7 +12067,7 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" -pako@~1.0.5: +pako@^1.0.6, pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== @@ -14347,6 +14438,12 @@ secp256k1@^3.0.1: nan "^2.2.1" safe-buffer "^5.1.0" +"seek-bzip@github:openpgpjs/seek-bzip": + version "1.0.5-git" + resolved "https://codeload.github.com/openpgpjs/seek-bzip/tar.gz/3aca608ffedc055a1da1d898ecb244804ef32209" + dependencies: + commander "~2.8.1" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -16272,6 +16369,10 @@ wdio-dot-reporter@~0.0.8: resolved "https://registry.yarnpkg.com/wdio-dot-reporter/-/wdio-dot-reporter-0.0.10.tgz#facfb7c9c5984149951f59cbc3cd0752101cf0e0" integrity sha512-A0TCk2JdZEn3M1DSG9YYbNRcGdx/YRw19lTiRpgwzH4qqWkO/oRDZRmi3Snn4L2j54KKTfPalBhlOtc8fojVgg== +"web-stream-tools@github:openpgpjs/web-stream-tools": + version "0.0.1" + resolved "https://codeload.github.com/openpgpjs/web-stream-tools/tar.gz/9ab800d46add161db496506d67338202ad0114ce" + webdriverio@^4.8.0: version "4.13.1" resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-4.13.1.tgz#624ef4ca569f3c9a5e8e9b11302b4431eda1fb8a" @@ -16721,7 +16822,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^1.2.0: +write-file-atomic@^1.1.4, write-file-atomic@^1.2.0: version "1.3.4" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" integrity sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8= From afb8000eaee306d093b89b3a0331193472c2efeb Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 4 Dec 2018 16:04:56 +0100 Subject: [PATCH 02/55] Ability to run commands from the main process --- src/internals/index.js | 59 ++------------------------------------ src/main/bridge.js | 13 +++++++-- src/main/commandHandler.js | 48 +++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 58 deletions(-) create mode 100644 src/main/commandHandler.js diff --git a/src/internals/index.js b/src/internals/index.js index a35995b8..44aab9a6 100644 --- a/src/internals/index.js +++ b/src/internals/index.js @@ -1,12 +1,12 @@ // @flow import '@babel/polyfill' -import commands from 'commands' import logger from 'logger' import uuid from 'uuid/v4' import { setImplementation } from 'api/network' import sentry from 'sentry/node' import { EXPERIMENTAL_HTTP_ON_RENDERER } from 'config/constants' import { serializeError } from 'helpers/errors' +import { executeCommand, unsubscribeCommand } from 'main/commandHandler' require('../env') @@ -52,64 +52,11 @@ if (EXPERIMENTAL_HTTP_ON_RENDERER) { }) } -const subscriptions = {} - process.on('message', m => { if (m.type === 'command') { - const { data, requestId, id } = m.command - const cmd = commands.find(cmd => cmd.id === id) - if (!cmd) { - logger.warn(`command ${id} not found`) - return - } - const startTime = Date.now() - logger.onCmd('cmd.START', id, 0, data) - try { - subscriptions[requestId] = cmd.impl(data).subscribe({ - next: data => { - logger.onCmd('cmd.NEXT', id, Date.now() - startTime, data) - process.send({ - type: 'cmd.NEXT', - requestId, - data, - }) - }, - complete: () => { - delete subscriptions[requestId] - logger.onCmd('cmd.COMPLETE', id, Date.now() - startTime) - process.send({ - type: 'cmd.COMPLETE', - requestId, - }) - }, - error: error => { - logger.warn('Command error:', error) - delete subscriptions[requestId] - logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error) - process.send({ - type: 'cmd.ERROR', - requestId, - data: serializeError(error), - }) - }, - }) - } catch (error) { - logger.warn('Command error:', error) - delete subscriptions[requestId] - logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error) - process.send({ - type: 'cmd.ERROR', - requestId, - data: serializeError(error), - }) - } + executeCommand(m.command, process.send.bind(process)) } else if (m.type === 'command-unsubscribe') { - const { requestId } = m - const sub = subscriptions[requestId] - if (sub) { - sub.unsubscribe() - delete subscriptions[requestId] - } + unsubscribeCommand(m.requestId) } else if (m.type === 'executeHttpQueryPayload') { const { payload } = m const defer = defers[payload.id] diff --git a/src/main/bridge.js b/src/main/bridge.js index e6527fa4..1d80be18 100644 --- a/src/main/bridge.js +++ b/src/main/bridge.js @@ -10,6 +10,7 @@ import logger from 'logger' import sentry, { captureException } from 'sentry/node' import user from 'helpers/user' import { resolveLogsDirectory, cleanUpBeforeClosingSync } from 'helpers/log' +import { executeCommand, unsubscribeCommand } from 'main/commandHandler' import { deserializeError } from 'helpers/errors' import { setInternalProcessPID } from './terminator' @@ -77,10 +78,18 @@ ipcMain.on('clean-processes', () => { ipcMainListenReceiveCommands({ onUnsubscribe: requestId => { - if (!internalProcess) return - internalProcess.send({ type: 'command-unsubscribe', requestId }) + unsubscribeCommand(requestId) + if (internalProcess) { + internalProcess.send({ type: 'command-unsubscribe', requestId }) + } }, onCommand: (command, notifyCommandEvent) => { + // ability to run command from the main process + if (command.id.startsWith('main:')) { + executeCommand(command, notifyCommandEvent) + return + } + if (!internalProcess) bootInternalProcess() const p = internalProcess invariant(p, 'internalProcess not started !?') diff --git a/src/main/commandHandler.js b/src/main/commandHandler.js new file mode 100644 index 00000000..18abacc7 --- /dev/null +++ b/src/main/commandHandler.js @@ -0,0 +1,48 @@ +import commands from 'commands' +import logger from 'logger' +import { serializeError } from 'helpers/errors' + +const subscriptions = {} + +export function executeCommand(command, send) { + const { data, requestId, id } = command + const cmd = commands.find(cmd => cmd.id === id) + if (!cmd) { + logger.warn(`command ${id} not found`) + return + } + const startTime = Date.now() + logger.onCmd('cmd.START', id, 0, data) + try { + subscriptions[requestId] = cmd.impl(data).subscribe({ + next: data => { + logger.onCmd('cmd.NEXT', id, Date.now() - startTime, data) + send({ type: 'cmd.NEXT', requestId, data }) + }, + complete: () => { + delete subscriptions[requestId] + logger.onCmd('cmd.COMPLETE', id, Date.now() - startTime) + send({ type: 'cmd.COMPLETE', requestId }) + }, + error: error => { + logger.warn('Command error:', error) + delete subscriptions[requestId] + logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error) + send({ type: 'cmd.ERROR', requestId, data: serializeError(error) }) + }, + }) + } catch (error) { + logger.warn('Command error:', error) + delete subscriptions[requestId] + logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error) + send({ type: 'cmd.ERROR', requestId, data: serializeError(error) }) + } +} + +export function unsubscribeCommand(requestId) { + const sub = subscriptions[requestId] + if (sub) { + sub.unsubscribe() + delete subscriptions[requestId] + } +} From c4035ecd747089e779b729fd5bd1903156761024 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sat, 8 Dec 2018 17:00:23 +0100 Subject: [PATCH 03/55] Bump react & react-dom to v16.6.3 This fix a context problem that was preventing UpdateNotifier to correctly update. Feel free to ping me if you want me to show you. --- package.json | 4 ++-- yarn.lock | 32 +++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 1896a7f7..3daa54d1 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,8 @@ "qs": "^6.5.1", "raven": "^2.5.0", "raven-js": "^3.24.2", - "react": "^16.6.1", - "react-dom": "^16.4.1", + "react": "^16.6.3", + "react-dom": "^16.6.3", "react-i18next": "^7.7.0", "react-key-handler": "^1.0.1", "react-markdown": "^3.3.2", diff --git a/yarn.lock b/yarn.lock index cf086e7b..e5ec2274 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13188,15 +13188,15 @@ react-docgen@^3.0.0-beta11: node-dir "^0.1.10" recast "^0.12.6" -react-dom@^16.4.1: - version "16.4.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6" - integrity sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A== +react-dom@^16.6.3: + version "16.6.3" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0" + integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ== dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" + scheduler "^0.11.2" react-error-overlay@^4.0.0: version "4.0.0" @@ -13474,7 +13474,7 @@ react-treebeard@^2.1.0: shallowequal "^0.2.2" velocity-react "^1.3.1" -react@*, react@^16.2.0, react@^16.6.1: +react@*, react@^16.2.0: version "16.6.1" resolved "https://registry.yarnpkg.com/react/-/react-16.6.1.tgz#ee2aef4f0a09e494594882029821049772f915fe" integrity sha512-OtawJThYlvRgm9BXK+xTL7BIlDx8vv21j+fbQDjRRUyok6y7NyjlweGorielTahLZHYIdKUoK2Dp9ByVWuMqxw== @@ -13484,6 +13484,16 @@ react@*, react@^16.2.0, react@^16.6.1: prop-types "^15.6.2" scheduler "^0.11.0" +react@^16.6.3: + version "16.6.3" + resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c" + integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.11.2" + reactcss@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" @@ -14389,6 +14399,14 @@ scheduler@^0.11.0: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.11.2: + version "0.11.3" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b" + integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" From 3d0a47e91408ead75fbbc0d22a38c46ecf598e07 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sat, 8 Dec 2018 17:10:01 +0100 Subject: [PATCH 04/55] Ability to pass NotifComponent to SidebarListItem --- src/components/base/SideBar/SideBarListItem.js | 13 +++---------- src/components/base/SideBar/stories.js | 1 - 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/components/base/SideBar/SideBarListItem.js b/src/components/base/SideBar/SideBarListItem.js index d50f0368..695af3bd 100644 --- a/src/components/base/SideBar/SideBarListItem.js +++ b/src/components/base/SideBar/SideBarListItem.js @@ -11,7 +11,7 @@ export type Props = { icon?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯ disabled?: boolean, iconActiveColor: ?string, - hasNotif?: boolean, + NotifComponent?: React$ComponentType<*>, isActive?: boolean, onClick?: void => void, isActive?: boolean, @@ -24,7 +24,7 @@ class SideBarListItem extends PureComponent { label, desc, iconActiveColor, - hasNotif, + NotifComponent, onClick, isActive, disabled, @@ -47,7 +47,7 @@ class SideBarListItem extends PureComponent { )} {!!desc && desc(this.props)} - {!!hasNotif && } + {NotifComponent && } ) } @@ -85,11 +85,4 @@ const Container = styled(Tabbable).attrs({ }}; ` -const Bullet = styled.div` - background: ${p => p.theme.colors.wallet}; - width: 8px; - height: 8px; - border-radius: 100%; -` - export default SideBarListItem diff --git a/src/components/base/SideBar/stories.js b/src/components/base/SideBar/stories.js index 3f7297b0..edc9a0d1 100644 --- a/src/components/base/SideBar/stories.js +++ b/src/components/base/SideBar/stories.js @@ -33,7 +33,6 @@ const SIDEBAR_ITEMS = [ label: 'Third very very very very long text very long text very long', icon: IconControls, iconActiveColor: '#27a2db', - hasNotif: true, }, { key: 'fourth', From 7d0dbeaabdcf14d4afe9adc1e9e4be1b94e655e8 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 9 Dec 2018 14:28:54 +0100 Subject: [PATCH 05/55] Make ReleaseNotes modal take care of itself --- src/components/modals/ReleaseNotes/index.js | 55 ++++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/src/components/modals/ReleaseNotes/index.js b/src/components/modals/ReleaseNotes/index.js index cf4a4b35..68d08028 100644 --- a/src/components/modals/ReleaseNotes/index.js +++ b/src/components/modals/ReleaseNotes/index.js @@ -1,16 +1,55 @@ // @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 from 'components/base/Modal' import ReleaseNotesBody from './ReleaseNotesBody' -const ReleaseNotesModal = () => ( - } - /> -) +type Props = { + openModal: Function, + saveSettings: Function, + lastUsedVersion: string, +} + +const mapStateToProps = (state: State) => ({ + lastUsedVersion: lastUsedVersionSelector(state), +}) + +const mapDispatchToProps = { + openModal, + saveSettings, +} + +class ReleaseNotesModal extends PureComponent { + 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 ( + } + /> + ) + } +} -export default ReleaseNotesModal +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ReleaseNotesModal) From ce090e14a5c7262258464ddc4629d27011249689 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 9 Dec 2018 14:29:02 +0100 Subject: [PATCH 06/55] Integrate UI for auto-update --- src/commands/autoUpdate.js | 51 +++++++++ src/commands/index.js | 4 + src/commands/quitAndInstallElectronUpdate.js | 30 +++++ src/components/App.js | 17 +-- src/components/DashboardPage/index.js | 4 +- src/components/MainSideBar/index.js | 9 +- .../UpdateNotifier/UpdateDownloaded.js | 103 ------------------ .../UpdateNotifier/UpdateInstalled.js | 48 -------- src/components/UpdateNotifier/index.js | 17 --- src/components/Updater/Banner.js | 75 +++++++++++++ src/components/Updater/DebugUpdater.js | 70 ++++++++++++ src/components/Updater/UpdateDot.js | 88 +++++++++++++++ src/components/Updater/UpdaterContext.js | 95 ++++++++++++++++ src/components/layout/Default.js | 4 + src/reducers/index.js | 4 - src/reducers/update.js | 48 -------- src/renderer/events.js | 25 ----- 17 files changed, 432 insertions(+), 260 deletions(-) create mode 100644 src/commands/autoUpdate.js create mode 100644 src/commands/quitAndInstallElectronUpdate.js delete mode 100644 src/components/UpdateNotifier/UpdateDownloaded.js delete mode 100644 src/components/UpdateNotifier/UpdateInstalled.js delete mode 100644 src/components/UpdateNotifier/index.js create mode 100644 src/components/Updater/Banner.js create mode 100644 src/components/Updater/DebugUpdater.js create mode 100644 src/components/Updater/UpdateDot.js create mode 100644 src/components/Updater/UpdaterContext.js delete mode 100644 src/reducers/update.js diff --git a/src/commands/autoUpdate.js b/src/commands/autoUpdate.js new file mode 100644 index 00000000..9dd08d0b --- /dev/null +++ b/src/commands/autoUpdate.js @@ -0,0 +1,51 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { Observable } from 'rxjs' + +import createElectronAppUpdater from 'main/updater/createElectronAppUpdater' +import type { UpdateStatus } from 'components/Updater/UpdaterContext' + +type Input = {} +type Result = { + status: UpdateStatus, + payload?: *, +} + +const cmd: Command = createCommand('main:autoUpdate', () => + Observable.create(o => { + const { autoUpdater } = require('electron-updater') + + const sendStatus = (status, payload) => { + o.next({ status, payload }) + } + + const handleDownload = async info => { + try { + const appUpdater = await createElectronAppUpdater({ + feedURL: process.env.LL_UPDATE_FEED || 'https://insert.feed.here', + updateVersion: info.version, + }) + await appUpdater.verify() + sendStatus('check-success') + } catch (err) { + // todo delete update file + 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 diff --git a/src/commands/index.js b/src/commands/index.js index 1022021c..07f5733a 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -4,6 +4,7 @@ import invariant from 'invariant' import type { Command } from 'helpers/ipc' import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency' +import autoUpdate from 'commands/autoUpdate' import getAddress from 'commands/getAddress' import getDeviceInfo from 'commands/getDeviceInfo' import getCurrentFirmware from 'commands/getCurrentFirmware' @@ -28,6 +29,7 @@ import listAppVersions from 'commands/listAppVersions' import listCategories from 'commands/listCategories' import listenDevices from 'commands/listenDevices' import ping from 'commands/ping' +import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate' import shouldFlashMcu from 'commands/shouldFlashMcu' import signTransaction from 'commands/signTransaction' import testApdu from 'commands/testApdu' @@ -37,6 +39,7 @@ import uninstallApp from 'commands/uninstallApp' const all: Array> = [ debugAppInfosForCurrency, + autoUpdate, getAddress, getDeviceInfo, getCurrentFirmware, @@ -61,6 +64,7 @@ const all: Array> = [ listCategories, listenDevices, ping, + quitAndInstallElectronUpdate, shouldFlashMcu, signTransaction, testApdu, diff --git a/src/commands/quitAndInstallElectronUpdate.js b/src/commands/quitAndInstallElectronUpdate.js new file mode 100644 index 00000000..4ed02af8 --- /dev/null +++ b/src/commands/quitAndInstallElectronUpdate.js @@ -0,0 +1,30 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { Observable } from 'rxjs' + +type Input = void +type Result = void + +const cmd: Command = 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 diff --git a/src/components/App.js b/src/components/App.js index a6b3d186..4db32693 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -16,6 +16,7 @@ import ThrowBlock from 'components/ThrowBlock' import Default from 'components/layout/Default' import CounterValues from 'helpers/countervalues' import { BridgeSyncProvider } from 'bridge/BridgeSyncContext' +import { UpdaterProvider } from 'components/Updater/UpdaterContext' const App = ({ store, @@ -31,13 +32,15 @@ const App = ({ - - - - - - - + + + + + + + + + diff --git a/src/components/DashboardPage/index.js b/src/components/DashboardPage/index.js index 203651b7..82692d8b 100644 --- a/src/components/DashboardPage/index.js +++ b/src/components/DashboardPage/index.js @@ -24,7 +24,7 @@ import { saveSettings } from 'actions/settings' import TrackPage from 'analytics/TrackPage' import RefreshAccountsOrdering from 'components/RefreshAccountsOrdering' -import UpdateNotifier from 'components/UpdateNotifier' +import UpdateBanner from 'components/Updater/Banner' import BalanceInfos from 'components/BalanceSummary/BalanceInfos' import BalanceSummary from 'components/BalanceSummary' import Box from 'components/base/Box' @@ -84,7 +84,7 @@ class DashboardPage extends PureComponent { return ( - + ({ accounts: accountsSelector(state), - updateStatus: getUpdateStatus(state), developerMode: developerModeSelector(state), }) @@ -54,7 +52,6 @@ type Props = { location: Location, push: string => void, openModal: string => void, - updateStatus: UpdateStatus, developerMode: boolean, } @@ -96,7 +93,7 @@ class MainSideBar extends PureComponent { handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS) render() { - const { t, accounts, location, updateStatus, developerMode } = this.props + const { t, accounts, location, developerMode } = this.props const { pathname } = location const addAccountButton = ( @@ -122,7 +119,7 @@ class MainSideBar extends PureComponent { iconActiveColor="wallet" onClick={this.handleClickDashboard} isActive={pathname === '/'} - hasNotif={updateStatus === 'downloaded'} + NotifComponent={UpdateDot} /> ({ - 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 { - 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 ( - - - - {t('update.newVersionReady')} - - - sendEvent('updater', 'quitAndInstall')} - > - {t('update.relaunch')} - - - - ) - default: - return null - } - } - - render() { - const { updateStatus, ...props } = this.props - - const isToggled = updateStatus === 'downloaded' - - if (!isToggled) { - return null - } - return {this.renderStatus()} - } -} - -export default compose( - connect( - mapStateToProps, - null, - ), - translate(), -)(UpdateDownloaded) diff --git a/src/components/UpdateNotifier/UpdateInstalled.js b/src/components/UpdateNotifier/UpdateInstalled.js deleted file mode 100644 index b0fbcdc5..00000000 --- a/src/components/UpdateNotifier/UpdateInstalled.js +++ /dev/null @@ -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 { - 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) diff --git a/src/components/UpdateNotifier/index.js b/src/components/UpdateNotifier/index.js deleted file mode 100644 index 6ea65461..00000000 --- a/src/components/UpdateNotifier/index.js +++ /dev/null @@ -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 ( - - - - - ) - } -} diff --git a/src/components/Updater/Banner.js b/src/components/Updater/Banner.js new file mode 100644 index 00000000..d0d0321e --- /dev/null +++ b/src/components/Updater/Banner.js @@ -0,0 +1,75 @@ +// @flow + +import React, { PureComponent } from 'react' +import styled from 'styled-components' + +import { radii } from 'styles/theme' + +import TranslatedError from 'components/TranslatedError' +import Box from 'components/base/Box' + +import { withUpdaterContext } from './UpdaterContext' +import type { UpdaterContextType } from './UpdaterContext' + +type Props = { + context: UpdaterContextType, +} + +export const VISIBLE_STATUS = ['download-progress', 'checking', 'check-success', 'error'] + +class UpdaterTopBanner extends PureComponent { + render() { + const { context } = this.props + const { status, quitAndInstall, downloadProgress, error } = context + + if (!VISIBLE_STATUS.includes(status)) return null + + return ( + + {status === 'download-progress' && `Downloading update... ${Math.round(downloadProgress)}%`} + {status === 'checking' && `Verifying update...`} + {status === 'error' && + error && ( +
+ {'Error during update. Please download again.'} + + + +
+ )} + {status === 'check-success' && ( +
+ {'Update ready to install. '} + {'install now'} +
+ )} +
+ ) + } +} + +const Container = styled(Box).attrs({ + py: '8px', + px: 3, + bg: p => (p.status === 'error' ? 'alertRed' : 'wallet'), + color: 'white', + mt: -20, + mb: 20, + fontSize: 4, +})` + border-radius: ${radii[1]}px; +` + +const DownloadLink = styled.span` + color: white; + text-decoration: underline; + cursor: pointer; +` + +const ErrorContainer = styled.div` + margin-top: 10px; + font-family: monospace; + font-size: 10px; +` + +export default withUpdaterContext(UpdaterTopBanner) diff --git a/src/components/Updater/DebugUpdater.js b/src/components/Updater/DebugUpdater.js new file mode 100644 index 00000000..32cf4c19 --- /dev/null +++ b/src/components/Updater/DebugUpdater.js @@ -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 { + render() { + const { context } = this.props + const { status, setStatus, quitAndInstall } = context + return ( +
+

+ DEBUG UPDATE
+ ------------
+

+ status: {status} +
+ {statusToDebug.map(s => ( + + ))} +
+
+ simulate update +
+
+ +
+
+ ) + } +} + +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) diff --git a/src/components/Updater/UpdateDot.js b/src/components/Updater/UpdateDot.js new file mode 100644 index 00000000..01a34bb5 --- /dev/null +++ b/src/components/Updater/UpdateDot.js @@ -0,0 +1,88 @@ +// @flow + +import React from 'react' +import styled, { keyframes } 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 rotate = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +` + +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%; +` + +const Spinner = styled.div` + opacity: 0.5; + position: absolute; + top: -3px; + left: -3px; + animation: ${rotate} 1.5s linear infinite; + width: 14px; + height: 14px; + + &:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + width: 4px; + height: 4px; + background-color: ${colors.wallet}; + border-radius: 50%; + } + + &:after { + content: ''; + position: absolute; + width: 4px; + height: 4px; + background-color: ${colors.wallet}; + border-radius: 50%; + } +` + +function UpdateDot(props: Props) { + const { context } = props + const { status } = context + if (!VISIBLE_STATUS.includes(status)) return null + const showSpinner = status === 'download-progress' || status === 'checking' + return ( +
+ {showSpinner && } + +
+ ) +} + +const styles = { + container: { + position: 'relative', + }, +} + +export default withUpdaterContext(UpdateDot) diff --git a/src/components/Updater/UpdaterContext.js b/src/components/Updater/UpdaterContext.js new file mode 100644 index 00000000..91351a48 --- /dev/null +++ b/src/components/Updater/UpdaterContext.js @@ -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 { + 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 {this.props.children} + } +} + +export const withUpdaterContext = (ComponentToDecorate: React$ComponentType<*>) => (props: *) => ( + + {context => } + +) + +export const UpdaterProvider = Provider diff --git a/src/components/layout/Default.js b/src/components/layout/Default.js index f79744ac..63ccd83c 100644 --- a/src/components/layout/Default.js +++ b/src/components/layout/Default.js @@ -34,6 +34,8 @@ import IsUnlocked from 'components/IsUnlocked' import SideBar from 'components/MainSideBar' import TopBar from 'components/TopBar' import SyncBackground from 'components/SyncBackground' +import DebugUpdater from 'components/Updater/DebugUpdater' + import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations' const Main = styled(GrowScroll).attrs({ @@ -97,6 +99,8 @@ class Default extends Component { ))} + {process.env.DEBUG_UPDATE && } + diff --git a/src/reducers/index.js b/src/reducers/index.js index a84f2d3e..4a1d03ab 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -13,7 +13,6 @@ import currenciesStatus from './currenciesStatus' import devices from './devices' import modals from './modals' import settings from './settings' -import update from './update' import onboarding from './onboarding' import bridgeSync from './bridgeSync' @@ -22,7 +21,6 @@ import type { ApplicationState } from './application' import type { DevicesState } from './devices' import type { ModalsState } from './modals' import type { SettingsState } from './settings' -import type { UpdateState } from './update' import type { OnboardingState } from './onboarding' import type { BridgeSyncState } from './bridgeSync' import type { CurrenciesStatusState } from './currenciesStatus' @@ -36,7 +34,6 @@ export type State = { modals: ModalsState, router: LocationShape, settings: SettingsState, - update: UpdateState, onboarding: OnboardingState, bridgeSync: BridgeSyncState, } @@ -50,7 +47,6 @@ export default combineReducers({ modals, router, settings, - update, onboarding, bridgeSync, }) diff --git a/src/reducers/update.js b/src/reducers/update.js deleted file mode 100644 index 5eac36b5..00000000 --- a/src/reducers/update.js +++ /dev/null @@ -1,48 +0,0 @@ -// @flow - -import { handleActions, createAction } from 'redux-actions' - -export type UpdateStatus = - | 'idle' - | 'checking' - | 'available' - | 'progress' - | 'unavailable' - | 'error' - | 'downloaded' - -export type UpdateState = { - status: UpdateStatus, - data?: Object, -} - -const state: UpdateState = { - status: 'idle', - data: {}, -} - -const handlers = { - UPDATE_SET_STATUS: (state: UpdateState, { payload }: { payload: UpdateState }): UpdateState => - payload, -} - -// Actions - -export const setUpdateStatus = createAction( - 'UPDATE_SET_STATUS', - (status: UpdateStatus, data?: Object): UpdateState => ({ status, data }), -) - -// Selectors - -export function getUpdateStatus(state: { update: UpdateState }): UpdateStatus { - return state.update.status -} - -export function getUpdateData(state: { update: UpdateState }): Object { - return state.update.data || {} -} - -// Default export - -export default handleActions(handlers, state) diff --git a/src/renderer/events.js b/src/renderer/events.js index 59324cd1..f233913c 100644 --- a/src/renderer/events.js +++ b/src/renderer/events.js @@ -21,7 +21,6 @@ import { onSetDeviceBusy } from 'components/DeviceBusyIndicator' import { onSetLibcoreBusy } from 'components/LibcoreBusyIndicator' import { lock } from 'reducers/application' -import { setUpdateStatus } from 'reducers/update' import { addDevice, removeDevice, resetDevices } from 'actions/devices' import listenDevices from 'commands/listenDevices' @@ -32,11 +31,6 @@ const d = { update: debug('lwd:update'), } -type MsgPayload = { - type: string, - data: any, -} - // TODO port remaining to command pattern export function sendEvent(channel: string, msgType: string, data: any) { ipcRenderer.send(channel, { @@ -109,25 +103,6 @@ export default ({ store }: { store: Object }) => { onSetDeviceBusy(busy) }) } - - if (__PROD__) { - // TODO move this to "command" pattern - const updaterHandlers = { - checking: () => store.dispatch(setUpdateStatus('checking')), - updateAvailable: info => store.dispatch(setUpdateStatus('available', info)), - updateNotAvailable: () => store.dispatch(setUpdateStatus('unavailable')), - error: err => store.dispatch(setUpdateStatus('error', err)), - downloadProgress: progress => store.dispatch(setUpdateStatus('progress', progress)), - downloaded: () => store.dispatch(setUpdateStatus('downloaded')), - } - ipcRenderer.on('updater', (event: any, payload: MsgPayload) => { - const { type, data } = payload - updaterHandlers[type](data) - }) - - // Start check of eventual updates - checkUpdates() - } } if (module.hot) { From 41ec097931b1b035827c1ab138916a8610fb2320 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 12 Dec 2018 10:28:13 +0100 Subject: [PATCH 07/55] For now, always allow update even if check failed --- src/commands/autoUpdate.js | 12 +++++++++--- src/config/constants.js | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/commands/autoUpdate.js b/src/commands/autoUpdate.js index 9dd08d0b..e4e466a0 100644 --- a/src/commands/autoUpdate.js +++ b/src/commands/autoUpdate.js @@ -3,6 +3,7 @@ import { createCommand, Command } from 'helpers/ipc' import { Observable } from 'rxjs' +import { UPDATE_CHECK_IGNORE, UPDATE_CHECK_FEED } from 'config/constants' import createElectronAppUpdater from 'main/updater/createElectronAppUpdater' import type { UpdateStatus } from 'components/Updater/UpdaterContext' @@ -23,14 +24,19 @@ const cmd: Command = createCommand('main:autoUpdate', () => const handleDownload = async info => { try { const appUpdater = await createElectronAppUpdater({ - feedURL: process.env.LL_UPDATE_FEED || 'https://insert.feed.here', + feedURL: UPDATE_CHECK_FEED, updateVersion: info.version, }) await appUpdater.verify() sendStatus('check-success') } catch (err) { - // todo delete update file - o.error(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) + } } } diff --git a/src/config/constants.js b/src/config/constants.js index ac43b517..47aa4670 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -95,6 +95,11 @@ export const EXPERIMENTAL_MARKET_INDICATOR_SETTINGS = boolFromEnv( ) export const USE_MOCK_DATA = boolFromEnv('USE_MOCK_DATA') +// Auto update + +export const UPDATE_CHECK_IGNORE = boolFromEnv('UPDATE_IGNORE_CHECK', true) +export const UPDATE_CHECK_FEED = stringFromEnv('UPDATE_CHECK_FEED', 'https://insert.feed.here') + // Other constants export const MAX_ACCOUNT_NAME_SIZE = 50 From d8da84033354b271ff660877732a0527eddc2d9c Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 12 Dec 2018 10:42:06 +0100 Subject: [PATCH 08/55] Remove node_modules cache in release script --- scripts/release.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.sh b/scripts/release.sh index 34a31fcb..e95ec8d3 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -67,6 +67,7 @@ if [[ $(uname) == 'Linux' ]]; then # only run it on one target, to prevent race "failed to create a draft release" fi +runJob "rm -rf ./node_modules/.cache" "Removing node modules cache..." "done" "fail" runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose" if [[ $(uname) == 'Linux' ]]; then From 7c9fb0d314c33146ba223e91dd67eec3d46bd458 Mon Sep 17 00:00:00 2001 From: meriadec Date: Thu, 13 Dec 2018 17:10:14 +0100 Subject: [PATCH 09/55] Polishing the update Banner --- src/commands/autoUpdate.js | 1 + src/components/Updater/Banner.js | 111 ++++++++++++++++++++++------ src/components/Updater/UpdateDot.js | 55 +------------- src/config/urls.js | 2 + src/icons/Donjon.js | 39 ++++++++++ src/icons/TriangleWarning.js | 13 +++- static/i18n/en/app.json | 9 ++- 7 files changed, 150 insertions(+), 80 deletions(-) create mode 100644 src/icons/Donjon.js diff --git a/src/commands/autoUpdate.js b/src/commands/autoUpdate.js index e4e466a0..23bef623 100644 --- a/src/commands/autoUpdate.js +++ b/src/commands/autoUpdate.js @@ -23,6 +23,7 @@ const cmd: Command = createCommand('main:autoUpdate', () => const handleDownload = async info => { try { + sendStatus('checking') const appUpdater = await createElectronAppUpdater({ feedURL: UPDATE_CHECK_FEED, updateVersion: info.version, diff --git a/src/components/Updater/Banner.js b/src/components/Updater/Banner.js index d0d0321e..9c30daae 100644 --- a/src/components/Updater/Banner.js +++ b/src/components/Updater/Banner.js @@ -2,11 +2,17 @@ import React, { PureComponent } from 'react' import styled from 'styled-components' +import { Trans } from 'react-i18next' +import { urls } from 'config/urls' import { radii } from 'styles/theme' +import { openURL } from 'helpers/linking' -import TranslatedError from 'components/TranslatedError' +import Spinner from 'components/base/Spinner' import Box from 'components/base/Box' +import IconUpdate from 'icons/Update' +import IconDonjon from 'icons/Donjon' +import IconWarning from 'icons/TriangleWarning' import { withUpdaterContext } from './UpdaterContext' import type { UpdaterContextType } from './UpdaterContext' @@ -17,38 +23,98 @@ type Props = { export const VISIBLE_STATUS = ['download-progress', 'checking', 'check-success', 'error'] +type Content = { + Icon: React$ComponentType<*>, + message: React$Node, + Right?: React$ComponentType<*>, +} + +type RightProps = { + downloadProgress: number, // eslint-disable-line react/no-unused-prop-types + quitAndInstall: () => void, // eslint-disable-line react/no-unused-prop-types + reDownload: () => void, // eslint-disable-line react/no-unused-prop-types +} + +const CONTENT_BY_STATUS: { [_: string]: Content } = { + 'download-progress': { + Icon: Spinner, + message: , + Right: ({ downloadProgress }: RightProps) => ( + + ), + }, + checking: { + Icon: IconDonjon, + message: , + }, + 'check-success': { + Icon: IconUpdate, + message: , + Right: ({ quitAndInstall }: RightProps) => ( + + + + ), + }, + error: { + Icon: IconWarning, + message: , + Right: ({ reDownload }: RightProps) => ( + + + + ), + }, +} + class UpdaterTopBanner extends PureComponent { + reDownload = () => { + openURL(urls.liveHome) + } + render() { const { context } = this.props - const { status, quitAndInstall, downloadProgress, error } = context + const { status, quitAndInstall, downloadProgress } = context if (!VISIBLE_STATUS.includes(status)) return null + const content: ?Content = CONTENT_BY_STATUS[status] + if (!content) return null + + const { Icon, message, Right } = content + return ( - {status === 'download-progress' && `Downloading update... ${Math.round(downloadProgress)}%`} - {status === 'checking' && `Verifying update...`} - {status === 'error' && - error && ( -
- {'Error during update. Please download again.'} - - - -
- )} - {status === 'check-success' && ( -
- {'Update ready to install. '} - {'install now'} -
+ {Icon && ( + + {/* $FlowFixMe let me do my stuff, flow */} + + + )} + {message} + {Right && ( + + + )}
) } } +const IconContainer = styled.div` + margin-right: 15px; + display: flex; + align-items: center; +` + const Container = styled(Box).attrs({ + horizontal: true, + align: 'center', py: '8px', px: 3, bg: p => (p.status === 'error' ? 'alertRed' : 'wallet'), @@ -56,20 +122,19 @@ const Container = styled(Box).attrs({ mt: -20, mb: 20, fontSize: 4, + ff: 'Open Sans|SemiBold', })` border-radius: ${radii[1]}px; ` -const DownloadLink = styled.span` +const FakeLink = styled.span` color: white; text-decoration: underline; cursor: pointer; ` -const ErrorContainer = styled.div` - margin-top: 10px; - font-family: monospace; - font-size: 10px; +const RightContainer = styled.div` + margin-left: auto; ` export default withUpdaterContext(UpdaterTopBanner) diff --git a/src/components/Updater/UpdateDot.js b/src/components/Updater/UpdateDot.js index 01a34bb5..65a81a98 100644 --- a/src/components/Updater/UpdateDot.js +++ b/src/components/Updater/UpdateDot.js @@ -1,7 +1,7 @@ // @flow import React from 'react' -import styled, { keyframes } from 'styled-components' +import styled from 'styled-components' import { colors } from 'styles/theme' @@ -13,15 +13,6 @@ type Props = { context: UpdaterContextType, } -const rotate = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -` - const getColor = ({ status }: { status: UpdateStatus }) => status === 'error' ? colors.alertRed : colors.wallet @@ -36,53 +27,11 @@ const Dot = styled.div` border-radius: 50%; ` -const Spinner = styled.div` - opacity: 0.5; - position: absolute; - top: -3px; - left: -3px; - animation: ${rotate} 1.5s linear infinite; - width: 14px; - height: 14px; - - &:before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - width: 4px; - height: 4px; - background-color: ${colors.wallet}; - border-radius: 50%; - } - - &:after { - content: ''; - position: absolute; - width: 4px; - height: 4px; - background-color: ${colors.wallet}; - border-radius: 50%; - } -` - function UpdateDot(props: Props) { const { context } = props const { status } = context if (!VISIBLE_STATUS.includes(status)) return null - const showSpinner = status === 'download-progress' || status === 'checking' - return ( -
- {showSpinner && } - -
- ) -} - -const styles = { - container: { - position: 'relative', - }, + return } export default withUpdaterContext(UpdateDot) diff --git a/src/config/urls.js b/src/config/urls.js index 1e0d5376..d022acdf 100644 --- a/src/config/urls.js +++ b/src/config/urls.js @@ -1,6 +1,8 @@ // @flow export const urls = { + liveHome: 'https://www.ledger.com/pages/ledger-live', + // Social twitter: 'https://twitter.com/LedgerHQ', github: 'https://github.com/LedgerHQ/ledger-live-desktop', diff --git a/src/icons/Donjon.js b/src/icons/Donjon.js new file mode 100644 index 00000000..1353a6a7 --- /dev/null +++ b/src/icons/Donjon.js @@ -0,0 +1,39 @@ +// @flow + +import React, { Fragment } from 'react' + +const path = ( + + + + + + + + +) + +export default ({ size, ...p }: { size: number }) => ( + + {path} + +) diff --git a/src/icons/TriangleWarning.js b/src/icons/TriangleWarning.js index c53743bd..72062996 100644 --- a/src/icons/TriangleWarning.js +++ b/src/icons/TriangleWarning.js @@ -9,8 +9,17 @@ const path = ( /> ) -export default ({ height, width, ...p }: { height: number, width: number }) => ( - +export default ({ + height, + width, + size, + ...p +}: { + height?: number, + width?: number, + size?: number, +}) => ( + {path} ) diff --git a/static/i18n/en/app.json b/static/i18n/en/app.json index 5f7804a6..bf20ecbf 100644 --- a/static/i18n/en/app.json +++ b/static/i18n/en/app.json @@ -478,8 +478,13 @@ } }, "update": { - "newVersionReady": "A new update is available", - "relaunch": "Update now" + "downloadInProgress": "Downloading update...", + "downloadProgress": "{{progress}}% completed", + "checking": "Checking update...", + "checkSuccess": "Update ready to install", + "quitAndInstall": "Install now", + "error": "Error during update. Please download again", + "reDownload": "Download again" }, "crash": { "oops": "Oops, something went wrong", From 2f760f230f1724915e63426e3d8b99129069c8f9 Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 18 Dec 2018 00:18:21 +0100 Subject: [PATCH 10/55] Add script to generate shasum file from a release --- scripts/create-release-shasums.sh | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 scripts/create-release-shasums.sh diff --git a/scripts/create-release-shasums.sh b/scripts/create-release-shasums.sh new file mode 100644 index 00000000..d6d2166b --- /dev/null +++ b/scripts/create-release-shasums.sh @@ -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 From bb9556019b5d9d04ddcd9f2f94b74fa67f9845d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 29 Jan 2019 12:09:53 +0100 Subject: [PATCH 11/55] Use logger.critical(error) at more important places --- src/bridge/BridgeSyncContext.js | 1 + src/components/SettingsPage/RepairDeviceButton.js | 2 ++ src/components/modals/AddAccounts/index.js | 4 ++++ src/components/modals/Receive/index.js | 4 ++++ src/components/modals/Send/index.js | 4 ++++ src/components/modals/UpdateFirmware/index.js | 6 +++++- 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/bridge/BridgeSyncContext.js b/src/bridge/BridgeSyncContext.js index b3211ffb..0e93cf44 100644 --- a/src/bridge/BridgeSyncContext.js +++ b/src/bridge/BridgeSyncContext.js @@ -100,6 +100,7 @@ class Provider extends Component { next() }, error: error => { + logger.critical(error) this.props.setAccountSyncState(accountId, { pending: false, error }) next() }, diff --git a/src/components/SettingsPage/RepairDeviceButton.js b/src/components/SettingsPage/RepairDeviceButton.js index 511bed6e..151e0e8f 100644 --- a/src/components/SettingsPage/RepairDeviceButton.js +++ b/src/components/SettingsPage/RepairDeviceButton.js @@ -6,6 +6,7 @@ import { connect } from 'react-redux' import { withRouter } from 'react-router' import { translate } from 'react-i18next' import { push } from 'react-router-redux' +import logger from 'logger' import type { T } from 'types/common' import firmwareRepair from 'commands/firmwareRepair' @@ -50,6 +51,7 @@ class RepairDeviceButton extends PureComponent { this.setState(patch) }, error: error => { + logger.critical(error) this.setState({ error, isLoading: false, progress: 0 }) }, complete: () => { diff --git a/src/components/modals/AddAccounts/index.js b/src/components/modals/AddAccounts/index.js index e6edd600..1e81a90e 100644 --- a/src/components/modals/AddAccounts/index.js +++ b/src/components/modals/AddAccounts/index.js @@ -24,6 +24,7 @@ import { closeModal } from 'reducers/modals' import Modal from 'components/base/Modal' import Stepper from 'components/base/Stepper' import { validateNameEdition } from '@ledgerhq/live-common/lib/account' +import logger from 'logger' import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency' import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device' @@ -165,6 +166,9 @@ class AddAccounts extends PureComponent { handleSetCurrency = (currency: ?Currency) => this.setState({ currency }) handleSetScanStatus = (scanStatus: string, err: ?Error = null) => { + if (err) { + logger.critical(err) + } this.setState({ scanStatus, err }) } diff --git a/src/components/modals/Receive/index.js b/src/components/modals/Receive/index.js index c0beab10..8594fb51 100644 --- a/src/components/modals/Receive/index.js +++ b/src/components/modals/Receive/index.js @@ -8,6 +8,7 @@ import { createStructuredSelector } from 'reselect' import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' +import logger from 'logger' import Track from 'analytics/Track' import type { Account } from '@ledgerhq/live-common/lib/types' @@ -141,6 +142,9 @@ class ReceiveModal extends PureComponent { handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened }) handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => { + if (err && err.name !== 'UserRefusedAddress') { + logger.critical(err) + } this.setState({ isAddressVerified, verifyAddressError: err }) } diff --git a/src/components/modals/Send/index.js b/src/components/modals/Send/index.js index 090ca4b0..b2069f34 100644 --- a/src/components/modals/Send/index.js +++ b/src/components/modals/Send/index.js @@ -12,6 +12,7 @@ import Track from 'analytics/Track' import { updateAccountWithUpdater } from 'actions/accounts' import { MODAL_SEND } from 'config/constants' import { getBridgeForCurrency } from 'bridge' +import logger from 'logger' import type { WalletBridge } from 'bridge/types' import type { T, Device } from 'types/common' @@ -180,6 +181,9 @@ class SendModal extends PureComponent> { } handleTransactionError = (error: Error) => { + if (!(error instanceof UserRefusedOnDevice)) { + logger.critical(error) + } const stepVerificationIndex = this.STEPS.findIndex(step => step.id === 'verification') if (stepVerificationIndex === -1) return this.setState({ error }) diff --git a/src/components/modals/UpdateFirmware/index.js b/src/components/modals/UpdateFirmware/index.js index 371909de..0533cc2a 100644 --- a/src/components/modals/UpdateFirmware/index.js +++ b/src/components/modals/UpdateFirmware/index.js @@ -11,6 +11,7 @@ import type { FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/mana import type { StepProps as DefaultStepProps, Step } from 'components/base/Stepper' import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' +import logger from 'logger' import { FreezeDeviceChangeEvents } from '../../ManagerPage/HookDeviceChange' import StepFullFirmwareInstall from './steps/01-step-install-full-firmware' @@ -82,7 +83,10 @@ class UpdateModal extends PureComponent { t: this.props.t, }) - setError = (e: Error) => this.setState({ error: e }) + setError = (e: Error) => { + logger.critical(e) + this.setState({ error: e }) + } handleReset = () => this.setState({ stepId: 'idCheck', error: null, nonce: this.state.nonce++ }) From 611378b1b69cab6e03f0909b3b6f99c7ba09071e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 29 Jan 2019 14:27:36 +0100 Subject: [PATCH 12/55] Migrate api code in live-common --- package.json | 2 +- src/api/Ethereum.js | 91 ------------------------ src/api/Fees.js | 31 -------- src/api/Ledger.js | 6 -- src/api/Ripple.js | 47 ------------ src/bridge/EthereumJSBridge.js | 4 +- src/bridge/RippleJSBridge.js | 15 ++-- src/components/FeesField/BitcoinKind.js | 2 +- src/components/FeesField/EthereumKind.js | 2 +- src/components/FeesField/RippleKind.js | 5 +- src/components/WithFeesAPI.js | 4 +- yarn.lock | 8 +-- 12 files changed, 22 insertions(+), 195 deletions(-) delete mode 100644 src/api/Ethereum.js delete mode 100644 src/api/Fees.js delete mode 100644 src/api/Ledger.js delete mode 100644 src/api/Ripple.js diff --git a/package.json b/package.json index e6d49112..ee8ce54e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@ledgerhq/hw-transport": "^4.32.0", "@ledgerhq/hw-transport-node-hid": "^4.32.0", "@ledgerhq/ledger-core": "2.0.0-rc.16", - "@ledgerhq/live-common": "4.14.1", + "@ledgerhq/live-common": "4.15.0-beta.0", "animated": "^0.2.2", "async": "^2.6.1", "axios": "^0.18.0", diff --git a/src/api/Ethereum.js b/src/api/Ethereum.js deleted file mode 100644 index bd7b2160..00000000 --- a/src/api/Ethereum.js +++ /dev/null @@ -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, - getAccountNonce: (address: string) => Promise, - broadcastTransaction: (signedTransaction: string) => Promise, - getAccountBalance: (address: string) => Promise, -} - -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) - }, - } -} diff --git a/src/api/Fees.js b/src/api/Fees.js deleted file mode 100644 index 84623f88..00000000 --- a/src/api/Fees.js +++ /dev/null @@ -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 => { - 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 }) -} diff --git a/src/api/Ledger.js b/src/api/Ledger.js deleted file mode 100644 index f5c5e9a6..00000000 --- a/src/api/Ledger.js +++ /dev/null @@ -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 diff --git a/src/api/Ripple.js b/src/api/Ripple.js deleted file mode 100644 index 2c4be71a..00000000 --- a/src/api/Ripple.js +++ /dev/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 } -} diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index 3282d1b4..145d6524 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -22,8 +22,8 @@ import { } from '@ledgerhq/live-common/lib/account' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import eip55 from 'eip55' -import { apiForCurrency } from 'api/Ethereum' -import type { Tx } from 'api/Ethereum' +import { apiForCurrency } from '@ledgerhq/live-common/lib/api/Ethereum' +import type { Tx } from '@ledgerhq/live-common/lib/api/Ethereum' import getAddressCommand from 'commands/getAddress' import signTransactionCommand from 'commands/signTransaction' import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from '@ledgerhq/errors' diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index eff799e6..53b105b5 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -3,6 +3,7 @@ import invariant from 'invariant' import { BigNumber } from 'bignumber.js' import { Observable } from 'rxjs' import React from 'react' +import { RippleAPI } from 'ripple-lib' import bs58check from 'ripple-bs58check' import { computeBinaryTransactionHash } from 'ripple-hashes' import throttle from 'lodash/throttle' @@ -26,7 +27,7 @@ import { parseAPIValue, parseAPICurrencyObject, formatAPICurrencyXRP, -} from 'api/Ripple' +} from '@ledgerhq/live-common/lib/api/Ripple' import FeesRippleKind from 'components/FeesField/RippleKind' import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind' import { @@ -63,7 +64,7 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps) => ( ) async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) { - const api = apiForEndpointConfig(a.endpointConfig) + const api = apiForEndpointConfig(RippleAPI, a.endpointConfig) const { fee } = t if (!fee) throw new FeeNotLoaded() try { @@ -252,7 +253,7 @@ const getServerInfo = (map => endpointConfig => { if (!endpointConfig) endpointConfig = '' if (map[endpointConfig]) return map[endpointConfig]() const f = throttle(async () => { - const api = apiForEndpointConfig(endpointConfig) + const api = apiForEndpointConfig(RippleAPI, endpointConfig) try { await api.connect() const res = await api.getServerInfo() @@ -270,7 +271,7 @@ const getServerInfo = (map => endpointConfig => { const recipientIsNew = async (endpointConfig, recipient) => { if (!isRecipientValid(recipient)) return false - const api = apiForEndpointConfig(endpointConfig) + const api = apiForEndpointConfig(RippleAPI, endpointConfig) try { await api.connect() try { @@ -302,7 +303,7 @@ const RippleJSBridge: WalletBridge = { } async function main() { - const api = apiForEndpointConfig() + const api = apiForEndpointConfig(RippleAPI) try { await api.connect() const serverInfo = await getServerInfo() @@ -423,7 +424,7 @@ const RippleJSBridge: WalletBridge = { } async function main() { - const api = apiForEndpointConfig(endpointConfig) + const api = apiForEndpointConfig(RippleAPI, endpointConfig) try { await api.connect() if (finished) return @@ -617,7 +618,7 @@ const RippleJSBridge: WalletBridge = { getDefaultEndpointConfig: () => defaultEndpoint, validateEndpointConfig: async endpointConfig => { - const api = apiForEndpointConfig(endpointConfig) + const api = apiForEndpointConfig(RippleAPI, endpointConfig) await api.connect() }, } diff --git a/src/components/FeesField/BitcoinKind.js b/src/components/FeesField/BitcoinKind.js index 937e0702..3ff83fe9 100644 --- a/src/components/FeesField/BitcoinKind.js +++ b/src/components/FeesField/BitcoinKind.js @@ -11,7 +11,7 @@ import type { T } from 'types/common' import { FeeNotLoaded } from '@ledgerhq/errors' import InputCurrency from 'components/base/InputCurrency' import Select from 'components/base/Select' -import type { Fees } from 'api/Fees' +import type { Fees } from '@ledgerhq/live-common/lib/api/Fees' import WithFeesAPI from '../WithFeesAPI' import GenericContainer from './GenericContainer' import Box from '../base/Box' diff --git a/src/components/FeesField/EthereumKind.js b/src/components/FeesField/EthereumKind.js index d6ef3d6d..e07fda28 100644 --- a/src/components/FeesField/EthereumKind.js +++ b/src/components/FeesField/EthereumKind.js @@ -6,7 +6,7 @@ import type { Account } from '@ledgerhq/live-common/lib/types' import { FeeNotLoaded } from '@ledgerhq/errors' import InputCurrency from 'components/base/InputCurrency' -import type { Fees } from 'api/Fees' +import type { Fees } from '@ledgerhq/live-common/lib/api/Fees' import WithFeesAPI from '../WithFeesAPI' import GenericContainer from './GenericContainer' diff --git a/src/components/FeesField/RippleKind.js b/src/components/FeesField/RippleKind.js index acf7622e..93907978 100644 --- a/src/components/FeesField/RippleKind.js +++ b/src/components/FeesField/RippleKind.js @@ -1,9 +1,10 @@ // @flow import React, { Component } from 'react' +import { RippleAPI } from 'ripple-lib' import type { BigNumber } from 'bignumber.js' import type { Account } from '@ledgerhq/live-common/lib/types' -import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple' +import { apiForEndpointConfig, parseAPIValue } from '@ledgerhq/live-common/lib/api/Ripple' import { FeeNotLoaded } from '@ledgerhq/errors' import InputCurrency from 'components/base/InputCurrency' import GenericContainer from './GenericContainer' @@ -30,7 +31,7 @@ class FeesField extends Component { } syncId = 0 async sync() { - const api = apiForEndpointConfig(this.props.account.endpointConfig) + const api = apiForEndpointConfig(RippleAPI, this.props.account.endpointConfig) const syncId = ++this.syncId try { await api.connect() diff --git a/src/components/WithFeesAPI.js b/src/components/WithFeesAPI.js index 5a56a347..ff49d05a 100644 --- a/src/components/WithFeesAPI.js +++ b/src/components/WithFeesAPI.js @@ -1,8 +1,8 @@ // @flow import { Component } from 'react' import type { Currency } from '@ledgerhq/live-common/lib/types' -import { getEstimatedFees } from 'api/Fees' -import type { Fees } from 'api/Fees' +import { getEstimatedFees } from '@ledgerhq/live-common/lib/api/Fees' +import type { Fees } from '@ledgerhq/live-common/lib/api/Fees' // FIXME we need to abstract this out like we did for CounterValues export default class WithFeesAPI extends Component< diff --git a/yarn.lock b/yarn.lock index f5c6e48d..1a56f64c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1738,10 +1738,10 @@ bindings "^1.3.0" nan "^2.6.2" -"@ledgerhq/live-common@4.14.1": - version "4.14.1" - resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-4.14.1.tgz#8475702328e7f0694a861de4510a8f1da648e68d" - integrity sha512-mQuptnSup+CHB9hzU1vIv8rllDJjHeDBURID9Z5kZv89F3TFQgCADxCh3AOuubx3Jz4/cK8BIq5cvsmMRCWnuA== +"@ledgerhq/live-common@4.15.0-beta.0": + version "4.15.0-beta.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-4.15.0-beta.0.tgz#44025378f8d6d6c95b53b92868ef0abf14926d4f" + integrity sha512-9NZWMrlC0sM0sdfUoiWR6Mkw4cGeZ2HpSWWNzP7LROjFUJL35VogapDtBDWf9qdDqPdjWjwx6YYF0AABfJd8FQ== dependencies: "@aeternity/ledger-app-api" "0.0.4" "@ledgerhq/errors" "^4.32.0" From 18f48e1ed0236bf5a58a5f1fe84f75704a767608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 29 Jan 2019 16:10:35 +0100 Subject: [PATCH 13/55] log critical error in SelectExchange --- src/components/SelectExchange.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectExchange.js b/src/components/SelectExchange.js index e66cfb84..9424d5f7 100644 --- a/src/components/SelectExchange.js +++ b/src/components/SelectExchange.js @@ -87,7 +87,7 @@ class SelectExchange extends Component< this.setState({ exchanges, isLoading: false }) } } catch (error) { - logger.error(error) + logger.critical(error) if (!this._unmounted && this._loadId === _loadId) { this.setState({ error, isLoading: false }) } From 1d09e8a9e5b54c100a9b60ebb325ff4794bff23b Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross Date: Thu, 31 Jan 2019 13:54:50 +0100 Subject: [PATCH 14/55] Add experimental option to export through ws --- README.md | 1 + package.json | 3 ++- src/components/SettingsPage/sections/Export.js | 10 ++++++++++ src/config/constants.js | 1 + yarn.lock | 8 ++++---- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d2632b44..6da07799 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ SKIP_GENUINE=1 SKIP_ONBOARDING=1 SHOW_LEGACY_NEW_ACCOUNT=1 HIGHLIGHT_I18N=1 +EXPERIMENTAL_WS_EXPORT=0 ## constants GET_CALLS_TIMEOUT=30000 diff --git a/package.json b/package.json index e6d49112..3e1d23db 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "i18next": "^11.2.2", "i18next-node-fs-backend": "^1.0.0", "invariant": "^2.2.4", + "ip": "^1.1.5", "jsqr": "^1.1.1", "lodash": "^4.17.5", "lru-cache": "^4.1.3", @@ -111,7 +112,7 @@ "winston": "^3.0.0", "winston-transport": "^4.2.0", "write-file-atomic": "^2.3.0", - "ws": "^5.1.1", + "ws": "^6.1.3", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/src/components/SettingsPage/sections/Export.js b/src/components/SettingsPage/sections/Export.js index 17932932..28cafdbc 100644 --- a/src/components/SettingsPage/sections/Export.js +++ b/src/components/SettingsPage/sections/Export.js @@ -8,6 +8,7 @@ import type { T } from 'types/common' import TrackPage from 'analytics/TrackPage' import styled from 'styled-components' import { SettingsSection as Section, SettingsSectionHeader as Header } from '../SettingsSection' +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' @@ -15,6 +16,7 @@ import Box from '../../base/Box' import QRCodeExporter from '../../QRCodeExporter' import { BulletRow } from '../../Onboarding/helperComponents' import Text from '../../base/Text' +import SocketExport from '../SocketExport' const BulletRowIcon = styled(Box).attrs({ ff: 'Rubik|Regular', @@ -139,6 +141,14 @@ class SectionExport extends PureComponent { } /> + {EXPERIMENTAL_WS_EXPORT && ( +
} + title="Experimental websocket local export ⚡" + desc="Generate a pairing code and use it on Ledger Live Mobile" + renderRight={} + /> + )} ) diff --git a/src/config/constants.js b/src/config/constants.js index 8f43698d..04fe9796 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -81,6 +81,7 @@ export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT') export const SHOW_MOCK_HSMWARNINGS = boolFromEnv('SHOW_MOCK_HSMWARNINGS') export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N') export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS') +export const EXPERIMENTAL_WS_EXPORT = boolFromEnv('EXPERIMENTAL_WS_EXPORT') export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL') export const EXPERIMENTAL_FIRMWARE_UPDATE = boolFromEnv('EXPERIMENTAL_FIRMWARE_UPDATE') export const EXPERIMENTAL_HTTP_ON_RENDERER = boolFromEnv('EXPERIMENTAL_HTTP_ON_RENDERER') diff --git a/yarn.lock b/yarn.lock index f5c6e48d..85745d44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16809,10 +16809,10 @@ ws@^4.0.0: async-limiter "~1.0.0" safe-buffer "~5.1.0" -ws@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.1.tgz#37827a0ba772d072a843c3615b0ad38bcdb354eb" - integrity sha512-2NkHdPKjDBj3CHdnAGNpmlliryKqF+n9MYXX7/wsVC4yqYocKreKNjydPDvT3wShAZnndlM0RytEfTALCDvz7A== +ws@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.3.tgz#d2d2e5f0e3c700ef2de89080ebc0ac6e1bf3a72d" + integrity sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg== dependencies: async-limiter "~1.0.0" From b43c867554065d5883ac00b1facc62d8d75ff2ce Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross Date: Thu, 31 Jan 2019 14:45:59 +0100 Subject: [PATCH 15/55] Changed the export description (updated other languages but they are in english) --- static/i18n/en/app.json | 2 +- static/i18n/es/app.json | 2 +- static/i18n/fr/app.json | 2 +- static/i18n/ja/app.json | 2 +- static/i18n/ko/app.json | 2 +- static/i18n/ru/app.json | 2 +- static/i18n/zh/app.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/static/i18n/en/app.json b/static/i18n/en/app.json index 9f16aabb..f129b5c0 100644 --- a/static/i18n/en/app.json +++ b/static/i18n/en/app.json @@ -360,7 +360,7 @@ "about": "About" }, "export": { - "desc": "Import accounts on your Ledger Live Mobile app", + "desc": "Export accounts to the Ledger Live mobile app.", "button": "Export", "modal": { "button": "Done", diff --git a/static/i18n/es/app.json b/static/i18n/es/app.json index cc49c77a..6451be0d 100644 --- a/static/i18n/es/app.json +++ b/static/i18n/es/app.json @@ -360,7 +360,7 @@ "about": "Acerca de..." }, "export": { - "desc": "Import accounts on your Ledger Live Mobile app", + "desc": "Export accounts to the Ledger Live mobile app.", "button": "Export", "modal": { "button": "Done", diff --git a/static/i18n/fr/app.json b/static/i18n/fr/app.json index 0b552869..1e836860 100644 --- a/static/i18n/fr/app.json +++ b/static/i18n/fr/app.json @@ -360,7 +360,7 @@ "about": "About" }, "export": { - "desc": "Import accounts on your Ledger Live Mobile app", + "desc": "Export accounts to the Ledger Live mobile app.", "button": "Export", "modal": { "button": "Done", diff --git a/static/i18n/ja/app.json b/static/i18n/ja/app.json index 061d9139..c5338cfe 100644 --- a/static/i18n/ja/app.json +++ b/static/i18n/ja/app.json @@ -360,7 +360,7 @@ "about": "詳細" }, "export": { - "desc": "Import accounts on your Ledger Live Mobile app", + "desc": "Export accounts to the Ledger Live mobile app.", "button": "Export", "modal": { "button": "Done", diff --git a/static/i18n/ko/app.json b/static/i18n/ko/app.json index bd1cb2e8..0703ebb2 100644 --- a/static/i18n/ko/app.json +++ b/static/i18n/ko/app.json @@ -360,7 +360,7 @@ "about": "소개" }, "export": { - "desc": "Import accounts on your Ledger Live Mobile app", + "desc": "Export accounts to the Ledger Live mobile app.", "button": "Export", "modal": { "button": "Done", diff --git a/static/i18n/ru/app.json b/static/i18n/ru/app.json index 3fc7df2d..7f05300e 100644 --- a/static/i18n/ru/app.json +++ b/static/i18n/ru/app.json @@ -360,7 +360,7 @@ "about": "О программе" }, "export": { - "desc": "Import accounts on your Ledger Live Mobile app", + "desc": "Export accounts to the Ledger Live mobile app.", "button": "Export", "modal": { "button": "Done", diff --git a/static/i18n/zh/app.json b/static/i18n/zh/app.json index 184a506a..7b51b7a5 100644 --- a/static/i18n/zh/app.json +++ b/static/i18n/zh/app.json @@ -360,7 +360,7 @@ "about": "关于" }, "export": { - "desc": "Import accounts on your Ledger Live Mobile app", + "desc": "Export accounts to the Ledger Live mobile app.", "button": "Export", "modal": { "button": "Done", From 84fa3bb0d7ee9d0bfad6fd7216b6e65bf6e9e0e9 Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross Date: Thu, 31 Jan 2019 16:01:20 +0100 Subject: [PATCH 16/55] Added extra key for export section title --- src/components/SettingsPage/sections/Export.js | 2 +- static/i18n/en/app.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/SettingsPage/sections/Export.js b/src/components/SettingsPage/sections/Export.js index 17932932..1231b31a 100644 --- a/src/components/SettingsPage/sections/Export.js +++ b/src/components/SettingsPage/sections/Export.js @@ -131,7 +131,7 @@ class SectionExport extends PureComponent {
} - title={t('settings.tabs.export')} + title={t('settings.export.title')} desc={t('settings.export.desc')} renderRight={ - - - - )} - /> + + )} + /> + +
) } } diff --git a/src/components/SettingsPage/PasswordAutoLockSelect.js b/src/components/SettingsPage/PasswordAutoLockSelect.js index 556fa845..8b643caf 100644 --- a/src/components/SettingsPage/PasswordAutoLockSelect.js +++ b/src/components/SettingsPage/PasswordAutoLockSelect.js @@ -30,10 +30,10 @@ class PasswordAutoLockSelect extends PureComponent { } 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`) }, ] diff --git a/src/components/SettingsPage/PasswordModal.js b/src/components/SettingsPage/PasswordModal.js index c55e4056..1b8c3909 100644 --- a/src/components/SettingsPage/PasswordModal.js +++ b/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,13 @@ const INITIAL_STATE = { class PasswordModal extends PureComponent { state = INITIAL_STATE + componentWillReceiveProps(nextProps: Props) { + if (!nextProps.isOpened) { + // CLean the state? + this.setState(prevState => ({ ...prevState, ...INITIAL_STATE })) + } + } + handleSave = (e: SyntheticEvent) => { const { currentPassword, newPassword } = this.state @@ -73,23 +81,17 @@ class PasswordModal extends PureComponent { } render() { - const { t, hasPassword, onClose, ...props } = this.props + const { t, hasPassword, onClose, isOpened, ...props } = this.props const { currentPassword, newPassword, incorrectPassword, confirmPassword } = this.state return ( - ( - - {hasPassword ? ( - {t('password.changePassword.title')} - ) : ( - - {t('password.setPassword.title')} - - )} - + + ( + { onChange={this.handleInputChange} t={t} /> - - + + )} + renderFooter={() => ( + - - - )} - /> + + )} + /> + ) } } diff --git a/src/components/SettingsPage/RepairDeviceButton.js b/src/components/SettingsPage/RepairDeviceButton.js index 511bed6e..1a26d384 100644 --- a/src/components/SettingsPage/RepairDeviceButton.js +++ b/src/components/SettingsPage/RepairDeviceButton.js @@ -10,7 +10,7 @@ import { push } from 'react-router-redux' 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, diff --git a/src/components/SettingsPage/ResetButton.js b/src/components/SettingsPage/ResetButton.js index ba6745b5..2708c75e 100644 --- a/src/components/SettingsPage/ResetButton.js +++ b/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 { { {hasPassword ? ( diff --git a/src/components/SettingsPage/sections/Export.js b/src/components/SettingsPage/sections/Export.js index 1231b31a..7d7bc88b 100644 --- a/src/components/SettingsPage/sections/Export.js +++ b/src/components/SettingsPage/sections/Export.js @@ -10,7 +10,8 @@ import styled from 'styled-components' import { SettingsSection as Section, SettingsSectionHeader as Header } from '../SettingsSection' 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' @@ -97,27 +98,32 @@ class SectionExport extends PureComponent { ] return ( - - {t('settings.export.modal.title')} - - - - - - - {t('settings.export.modal.listTitle')} - + ( + + + + + + + {t('settings.export.modal.listTitle')} + + + + {stepsImportMobile.map(step => )} + - - {stepsImportMobile.map(step => )} + )} + renderFooter={() => ( + + - - - - - + )} + /> ) } @@ -139,7 +145,12 @@ class SectionExport extends PureComponent { } /> - + ) } diff --git a/src/components/base/Modal/ConfirmModal.js b/src/components/base/Modal/ConfirmModal.js index 0512a981..416eb5a7 100644 --- a/src/components/base/Modal/ConfirmModal.js +++ b/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 { @@ -43,23 +46,38 @@ class ConfirmModal extends PureComponent { onConfirm, isLoading, renderIcon, + onClose, t, analyticsName, + centered, ...props } = this.props const realConfirmText = confirmText || t('common.confirm') const realCancelText = cancelText || t('common.cancel') return ( - ( - - - {title} - + + ( + + {!isLoading && } + + + )} + render={() => ( + {subTitle && ( {subTitle} @@ -73,22 +91,11 @@ class ConfirmModal extends PureComponent { {desc} - - - {!isLoading && } - - - - )} - /> + + )} + /> + + ) } } diff --git a/src/components/base/Modal/ModalBody.js b/src/components/base/Modal/ModalBody.js index c0083590..363f10aa 100644 --- a/src/components/base/Modal/ModalBody.js +++ b/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 { - 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 ( - - {onClose && ( - - - - )} - {(!isHidden || !deferHeight) && {children}} - + + + {title} + + (this._content = n)} + onIsScrollableChange={this.animateGradient} + noScroll={noScroll} + > + {render && render(renderProps)} + +
+ +
+ {renderFooter && {renderFooter(renderProps)}} +
) } } -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: 6, +} -const Inner = styled(Box)` - animation: ${appear} 80ms linear; -` +const GRADIENT_WRAPPER_STYLE = { + height: 0, + position: 'relative', + pointerEvents: 'none', +} export default ModalBody diff --git a/src/components/base/Modal/ModalContent.js b/src/components/base/Modal/ModalContent.js new file mode 100644 index 00000000..7b74ddd1 --- /dev/null +++ b/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 ( +
(this._outer = n)} tabIndex={0}> + {children} +
+ ) + } +} + +const CONTENT_STYLE = { + flexShrink: 1, + padding: 20, + paddingBottom: 40, +} + +export default ModalContent diff --git a/src/components/base/Modal/ModalFooter.js b/src/components/base/Modal/ModalFooter.js new file mode 100644 index 00000000..97f7c083 --- /dev/null +++ b/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 }) => ( +
{children}
+) + +export default ModalFooter diff --git a/src/components/base/Modal/ModalHeader.js b/src/components/base/Modal/ModalHeader.js new file mode 100644 index 00000000..e3078e8d --- /dev/null +++ b/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 = +const iconCross = + +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, +}) => ( +
+ {onBack && ( + + {iconAngleLeft} + {t('common.back')} + + )} + {children} + {onClose && ( + + {iconCross} + + )} +
+) + +export default translate()(ModalHeader) diff --git a/src/components/base/Modal/ModalTitle.js b/src/components/base/Modal/ModalTitle.js deleted file mode 100644 index 55cf2fb4..00000000 --- a/src/components/base/Modal/ModalTitle.js +++ /dev/null @@ -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 ( - - {onBack && ( - - - {t('common.back')} - - )} - {children} - - ) -} - -export default translate()(ModalTitle) diff --git a/src/components/base/Modal/RepairModal.js b/src/components/base/Modal/RepairModal.js index a69d839c..bf01337e 100644 --- a/src/components/base/Modal/RepairModal.js +++ b/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 }) => ( - + {desc ? ( {desc} ) : null} - + ) const FlashStep = ({ progress, t }: { progress: number, t: * }) => progress === 0 ? ( - + {'1.'} @@ -74,9 +75,9 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) => alt={t('manager.modal.mcuFirst')} /> - + ) : ( - + @@ -88,11 +89,11 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) => {t('manager.modal.mcuPin')} - + ) const ErrorStep = ({ error }: { error: Error }) => ( - + @@ -118,7 +119,7 @@ const ErrorStep = ({ error }: { error: Error }) => ( - + ) type Props = { @@ -177,38 +178,44 @@ class RepairModal extends PureComponent { return ( ( - - - {title} - {error ? ( - - ) : isLoading ? ( - - ) : ( - - )} - - {!isLoading && !error ? ( - - + + ) : null} + + )} + renderFooter={() => + !isLoading ? ( + + {error ? null : ( <> )} - - ) : null} - - )} - /> + + ) : null + } + /> + ) } } diff --git a/src/components/base/Modal/index.js b/src/components/base/Modal/index.js index cad43fda..ff0ae518 100644 --- a/src/components/base/Modal/index.js +++ b/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 { - 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 => ( - - {children} - - ) - : children => ( - - {children} - - ) +class Modal extends PureComponent { + state = { + animShowHide: new Animated.Value(0), + isInDOM: this.props.isOpened === true, + } -export class Modal extends Component { - 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 ( - ({ - opacity: spring(isVisible ? 1 : 0, springConfig), - scale: spring(isVisible ? 1 : 0.95, springConfig), - })} - > - {(m, isVisible, isAnimated) => ( - - - - {wrap( - (this._wrapper = n)} - onClick={stopPropagation} - width={width} - > - - , - )} - - )} - + 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 = ( + + +
+ + {render && render(renderProps)} + {children} + +
+
) + + 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, diff --git a/src/components/base/Modal/stories.js b/src/components/base/Modal/stories.js index 9e946800..2eb46bb4 100644 --- a/src/components/base/Modal/stories.js +++ b/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 title'} - {'this is the modal content'} - - {'modal footer'} - - - - )} - /> -)) - -stories.add('ConfirmModal', () => ( - ( + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + renderFooter={() => 'footer'} + /> )} - onConfirm={action('onConfirm')} - onReject={action('onReject')} /> )) diff --git a/src/components/base/Stepper/index.js b/src/components/base/Stepper/index.js index 8daf4acb..73511549 100644 --- a/src/components/base/Stepper/index.js +++ b/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 { onBack, shouldPreventClose, shouldRenderFooter, + noScroll, } = step const stepProps: StepProps = { @@ -89,25 +91,27 @@ class Stepper extends PureComponent { : !!shouldPreventClose return ( - - onBack(stepProps) : undefined}>{title} - - - - {children} - - {renderFooter && ( - - - + onBack(stepProps) : undefined} + title={title} + noScroll={noScroll} + render={() => ( + + + + {children} + )} - + renderFooter={renderFooter ? () => : undefined} + /> ) } } diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index eb1e1d0d..552470bd 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/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' @@ -28,13 +28,8 @@ 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 +69,7 @@ const defaultState = { isRemoveAccountModalOpen: false, } -class HelperComp extends PureComponent { +class AccountSettingRenderBody extends PureComponent { state = { ...defaultState, } @@ -84,7 +79,6 @@ class HelperComp extends PureComponent { } getAccount(data: Object): Account { - // FIXME this should be a selector const { accountName } = this.state const account = get(data, 'account', {}) @@ -132,7 +126,6 @@ class HelperComp extends PureComponent { e: SyntheticEvent, ) => { e.preventDefault() - const { updateAccount, setDataModal } = this.props const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state @@ -194,10 +187,10 @@ class HelperComp extends PureComponent { 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, @@ -207,11 +200,12 @@ class HelperComp extends PureComponent { } return ( - -
- - {t('account.settings.title')} - + ( + + {t('account.settings.accountName.title')} @@ -284,8 +278,21 @@ class HelperComp extends PureComponent { value={JSON.stringify(usefulData, null, 2)} /> - - + this.handleRemoveAccount(account)} + title={t('settings.removeAccountModal.title')} + subTitle={t('common.areYouSure')} + desc={t('settings.removeAccountModal.desc')} + /> + + )} + renderFooter={() => ( + - - - this.handleRemoveAccount(account)} - title={t('settings.removeAccountModal.title')} - subTitle={t('common.areYouSure')} - desc={t('settings.removeAccountModal.desc')} - /> -
+ + )} + /> ) } } @@ -321,7 +317,7 @@ export default compose( mapDispatchToProps, ), translate(), -)(HelperComp) +)(AccountSettingRenderBody) export function InputLeft({ currency }: { currency: Currency }) { return ( diff --git a/src/components/modals/AddAccounts/index.js b/src/components/modals/AddAccounts/index.js index e6edd600..0bd842cf 100644 --- a/src/components/modals/AddAccounts/index.js +++ b/src/components/modals/AddAccounts/index.js @@ -43,6 +43,7 @@ const createSteps = () => { footer: StepChooseCurrencyFooter, onBack: null, hideFooter: false, + noScroll: true, }, { id: 'connectDevice', @@ -241,6 +242,7 @@ class AddAccounts extends PureComponent { return ( this.setState({ ...INITIAL_STATE })} diff --git a/src/components/modals/Debug.js b/src/components/modals/Debug.js index 8ed42391..66362152 100644 --- a/src/components/modals/Debug.js +++ b/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 ( - ( - - - developer internal tools - + + ( + + {device && ( @@ -191,6 +190,7 @@ class Debug extends Component<*, *> { > {logs.map(log => ( { ))} + + )} + renderFooter={() => ( + - - - )} - /> + + )} + /> + ) } } diff --git a/src/components/modals/Disclaimer.js b/src/components/modals/Disclaimer.js index ce340847..7e73dc78 100644 --- a/src/components/modals/Disclaimer.js +++ b/src/components/modals/Disclaimer.js @@ -7,13 +7,21 @@ 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 { @@ -21,28 +29,39 @@ class DisclaimerModal extends PureComponent { const { t } = this.props return ( - ( - - {t('disclaimerModal.title')} - + + ( +

{t('disclaimerModal.desc_1')}

{t('disclaimerModal.desc_2')}

-
- - - -
- )} - /> + + )} + /> +
) } } -export default translate()(DisclaimerModal) +export default compose( + connect( + null, + mapDispatchToProps, + ), + translate(), +)(DisclaimerModal) diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index bb77163f..b9f55c04 100644 --- a/src/components/modals/OperationDetails.js +++ b/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 ( - - - {t('operationDetails.title')} - - - - - - - - - - - - - - - - - {t('operationDetails.account')} - {name} + ( + + + + + + - - {t('operationDetails.date')} - {moment(date).format('LLL')} + + - - - - {t('operationDetails.fees')} - {fee ? ( - - - - - - ) : ( - {t('operationDetails.noFees')} - )} - - - {t('operationDetails.status')} - - - {isConfirmed - ? t('operationDetails.confirmed') - : t('operationDetails.notConfirmed')} - - {`(${confirmations})`} - - + + + + {t('operationDetails.account')} + {name} - - - {t('operationDetails.identifier')} - - {hash} - - - - + + {t('operationDetails.date')} + {moment(date).format('LLL')} - - - {t('operationDetails.from')} - + + + + + {t('operationDetails.fees')} + {fee ? ( + + + + + + ) : ( + {t('operationDetails.noFees')} + )} - - - {t('operationDetails.to')} - + + {t('operationDetails.status')} + + + {isConfirmed + ? t('operationDetails.confirmed') + : t('operationDetails.notConfirmed')} + + {`(${confirmations})`} + - {Object.entries(extra).map(([key, value]) => ( - - - - - {value} - - ))} - - - - - {url && ( - + + + {t('operationDetails.identifier')} + + {hash} + + + + + + + + {t('operationDetails.from')} + + + + + {t('operationDetails.to')} + + + {Object.entries(extra).map(([key, value]) => ( + + + + + {value} + + ))} + + )} + renderFooter={() => + url && ( - - )} + ) + } + > + ) }) @@ -255,12 +251,13 @@ type ModalRenderProps = { account: string, operation: string, }, - onClose: Function, + onClose?: Function, } const OperationDetailsWrapper = ({ t }: { t: T }) => ( { const { data, onClose } = props return diff --git a/src/components/modals/Receive/index.js b/src/components/modals/Receive/index.js index c0beab10..5d73ec95 100644 --- a/src/components/modals/Receive/index.js +++ b/src/components/modals/Receive/index.js @@ -193,16 +193,17 @@ class ReceiveModal extends PureComponent { return ( ( + render={() => ( { const { onClose, t } = this.props return ( - - - {t('releaseNotes.title')} - - - {this.renderContent()} - - - - - - - + ( + + + + {this.renderContent()} + + + + )} + renderFooter={() => ( + + + + )} + /> ) } } diff --git a/src/components/modals/ReleaseNotes/index.js b/src/components/modals/ReleaseNotes/index.js index cf4a4b35..3cd2192d 100644 --- a/src/components/modals/ReleaseNotes/index.js +++ b/src/components/modals/ReleaseNotes/index.js @@ -9,6 +9,7 @@ import ReleaseNotesBody from './ReleaseNotesBody' const ReleaseNotesModal = () => ( } /> ) diff --git a/src/components/modals/Send/index.js b/src/components/modals/Send/index.js index 090ca4b0..50c56351 100644 --- a/src/components/modals/Send/index.js +++ b/src/components/modals/Send/index.js @@ -273,6 +273,7 @@ class SendModal extends PureComponent> { return ( { return ( } + centered + render={({ data, onClose }) => ( + + )} /> ) } diff --git a/src/components/modals/ShareAnalytics.js b/src/components/modals/ShareAnalytics.js index 467b060d..c5f315fc 100644 --- a/src/components/modals/ShareAnalytics.js +++ b/src/components/modals/ShareAnalytics.js @@ -1,19 +1,28 @@ // @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 { render() { const { t } = this.props @@ -56,30 +65,41 @@ class ShareAnalytics extends PureComponent { }, ] return ( - ( - - - {t('onboarding.analytics.shareAnalytics.title')} - - {t('onboarding.analytics.shareAnalytics.desc')} - -
    {items.map(item =>
  • {item.desc}
  • )}
-
- - - -
- )} - /> + + )} + /> +
) } } -export default translate()(ShareAnalytics) +export default compose( + connect( + null, + mapDispatchToProps, + ), + translate(), +)(ShareAnalytics) export const Ul = styled.ul.attrs({ ff: 'Open Sans|Regular', diff --git a/src/components/modals/TechnicalData.js b/src/components/modals/TechnicalData.js index 395cdf46..93f2c3ef 100644 --- a/src/components/modals/TechnicalData.js +++ b/src/components/modals/TechnicalData.js @@ -1,16 +1,25 @@ // @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 { @@ -33,27 +42,38 @@ class TechnicalData extends PureComponent { ] return ( - ( - - - {t('onboarding.analytics.technicalData.mandatoryContextual.title')} - - {t('onboarding.analytics.technicalData.desc')} - -
    {items.map(item =>
  • {item.desc}
  • )}
-
- - - -
- )} - /> + + )} + /> +
) } } -export default translate()(TechnicalData) +export default compose( + connect( + null, + mapDispatchToProps, + ), + translate(), +)(TechnicalData) diff --git a/src/components/modals/UpdateFirmware/Disclaimer.js b/src/components/modals/UpdateFirmware/Disclaimer.js index 76d16fae..f18019e5 100644 --- a/src/components/modals/UpdateFirmware/Disclaimer.js +++ b/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 { render(): React$Node { const { status, firmware, onClose, t, goToNextStep } = this.props return ( - ( - - - - {t('manager.firmware.update')} - - - - You are about to install - - {`firmware version ${ - firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : '' - }`} - - - - - {t('manager.firmware.disclaimerAppDelete')} - {t('manager.firmware.disclaimerAppReinstall')} - - + + + ( + + + + You are about to install + + {`firmware version ${ + firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : '' + }`} + + + + + {t('manager.firmware.disclaimerAppDelete')} + {t('manager.firmware.disclaimerAppReinstall')} + {firmware && firmware.osu ? ( - + {firmware.osu.notes} - + ) : null} - - - - - - )} - /> + + )} + renderFooter={() => ( + + + + )} + /> + ) } } diff --git a/src/components/modals/UpdateFirmware/index.js b/src/components/modals/UpdateFirmware/index.js index 371909de..9d2ed400 100644 --- a/src/components/modals/UpdateFirmware/index.js +++ b/src/components/modals/UpdateFirmware/index.js @@ -103,6 +103,7 @@ class UpdateModal extends PureComponent { return (