Upgrade dependencies (#109)
* Upgrade dependencies (#108) * Menus * Remove preload errors * Compile errors * Formatting * Scanning progress * Formatting * Save wallet on successful sync * Update zecwallet lib to blazesync Co-authored-by: Arjun <37590483+denverbdr@users.noreply.github.com>
|
@ -7,6 +7,7 @@ end_of_line = lf
|
|||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
.eslintcache
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# flow-typed
|
||||
flow-typed/npm/*
|
||||
!flow-typed/npm/module_vx.x.x.js
|
||||
|
||||
# App packaged
|
||||
release
|
||||
app/main.prod.js
|
||||
app/main.prod.js.map
|
||||
app/renderer.prod.js
|
||||
app/renderer.prod.js.map
|
||||
app/style.css
|
||||
app/style.css.map
|
||||
dist
|
||||
dll
|
||||
main.js
|
||||
main.js.map
|
||||
|
||||
.idea
|
||||
npm-debug.log.*
|
||||
__snapshots__
|
||||
|
||||
# Package.json
|
||||
package.json
|
||||
.travis.yml
|
21
.eslintrc.js
|
@ -1,21 +0,0 @@
|
|||
module.exports = {
|
||||
extends: 'erb',
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: require.resolve('./configs/webpack.config.eslint.js')
|
||||
}
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"**/*.test.js"
|
||||
],
|
||||
env: {
|
||||
jest: true // now **/*.test.js files' env has both es6 *and* jest
|
||||
},
|
||||
plugins: ["jest"],
|
||||
}
|
||||
]
|
||||
}
|
24
.flowconfig
|
@ -1,24 +0,0 @@
|
|||
[ignore]
|
||||
<PROJECT_ROOT>/app/main.prod.js
|
||||
<PROJECT_ROOT>/app/main.prod.js.map
|
||||
<PROJECT_ROOT>/app/dist/.*
|
||||
<PROJECT_ROOT>/resources/.*
|
||||
<PROJECT_ROOT>/node_modules/webpack-cli
|
||||
<PROJECT_ROOT>/release/.*
|
||||
<PROJECT_ROOT>/dll/.*
|
||||
<PROJECT_ROOT>/release/.*
|
||||
<PROJECT_ROOT>/git/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[options]
|
||||
esproposal.class_static_fields=enable
|
||||
esproposal.class_instance_fields=enable
|
||||
esproposal.export_star_as=enable
|
||||
module.name_mapper.extension='css' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
|
||||
module.name_mapper.extension='styl' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
|
||||
module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
|
||||
module.name_mapper.extension='png' -> '<PROJECT_ROOT>/internals/flow/WebpackAsset.js.flow'
|
||||
module.name_mapper.extension='jpg' -> '<PROJECT_ROOT>/internals/flow/WebpackAsset.js.flow'
|
|
@ -1,4 +0,0 @@
|
|||
<!--
|
||||
Is this a bug report?
|
||||
If so, go back and select the "Bug report" option or your issue WILL be closed.
|
||||
--!>
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please make sure you have the latest ZecWallet.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -1,6 +0,0 @@
|
|||
requiredHeaders:
|
||||
- Prerequisites
|
||||
- Expected Behavior
|
||||
- Current Behavior
|
||||
- Possible Solution
|
||||
- Your Environment
|
|
@ -1,17 +0,0 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pr
|
||||
- discussion
|
||||
- e2e
|
||||
- enhancement
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
|
@ -1,151 +0,0 @@
|
|||
name: Electron CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-16.04, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.46.0
|
||||
override: true
|
||||
components: rustfmt
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: Build and Package@Linux
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
yarn package-linux
|
||||
- name: Mac Certificate-Gen
|
||||
if: contains(matrix.os, 'macos')
|
||||
env:
|
||||
p12base64: ${{ secrets.p12cert }}
|
||||
run: |
|
||||
echo "$p12base64" | openssl base64 -d -out cert.p12
|
||||
- name: Mac Certificate-Install
|
||||
if: contains(matrix.os, 'macos')
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-filepath: 'cert.p12'
|
||||
p12-password: ${{ secrets.p12certpassword }}
|
||||
- name: Build and Package@Mac
|
||||
if: contains(matrix.os, 'macos')
|
||||
env:
|
||||
appleId: ${{ secrets.appleId }}
|
||||
appleIdPassword: ${{ secrets.appleIdPassword }}
|
||||
run: |
|
||||
yarn package-mac
|
||||
- name: Build and Package@Win
|
||||
if: contains(matrix.os, 'windows')
|
||||
run: |
|
||||
yarn package-win
|
||||
- name: Version@Linux@Mac
|
||||
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'macos')
|
||||
run: |
|
||||
./bin/printversion.sh
|
||||
- name: Version@Win
|
||||
if: contains(matrix.os, 'windows')
|
||||
run: |
|
||||
./bin/printversion.ps1
|
||||
- name: Upload artifacts-deb
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet_Lite_${{ env.VERSION }}_amd64.deb
|
||||
- name: Upload artifacts-AppImage
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite-${{ env.VERSION }}.AppImage
|
||||
- name: Upload artifacts-dmg
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'macos')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite-${{ env.VERSION }}.dmg
|
||||
- name: Upload artifacts-zip
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'windows')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite-${{ env.VERSION }}-win.zip
|
||||
- name: Upload artifacts-msi
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'windows')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite ${{ env.VERSION }}.msi
|
||||
|
||||
job_signbinaries:
|
||||
name: Consolidate and sign binaries
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Version
|
||||
run: |
|
||||
./bin/printversion.sh
|
||||
- name: Download ubuntu
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: ubuntu-16.04
|
||||
- name: Download windows
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: windows-latest
|
||||
- name: Download MacOS
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: macos-latest
|
||||
- name: Get gpg key
|
||||
env:
|
||||
gpgbase64: ${{ secrets.gpgkey }}
|
||||
run: |
|
||||
echo "$gpgbase64" | openssl base64 -d -out gpg.key
|
||||
- name: Import gpg key
|
||||
run: |
|
||||
gpg --allow-secret-key-import --import gpg.key
|
||||
mkdir release
|
||||
cp windows-latest/* release/
|
||||
cp ubuntu-16.04/* release/
|
||||
cp macos-latest/* release/
|
||||
- name: Sign binaries
|
||||
env:
|
||||
APP_VERSION: ${{ env.VERSION }}
|
||||
run: |
|
||||
bin/signbinaries.sh
|
||||
tar -cf artifacts-$APP_VERSION.tar.gz release/signatures*.zip release/Zecwallet*
|
||||
- name: Upload final artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: artifacts
|
||||
path: artifacts-${{ env.VERSION }}.tar.gz
|
|
@ -1,55 +1,29 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
# dependencies
|
||||
/node_modules
|
||||
/dist
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/target
|
||||
/native/target
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
# native files
|
||||
native.node
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
# production
|
||||
/build
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
.eslintcache
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# OSX
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# flow-typed
|
||||
flow-typed/npm/*
|
||||
!flow-typed/npm/module_vx.x.x.js
|
||||
|
||||
# App packaged
|
||||
release
|
||||
app/main.prod.js
|
||||
app/main.prod.js.map
|
||||
app/renderer.prod.js
|
||||
app/renderer.prod.js.map
|
||||
app/style.css
|
||||
app/style.css.map
|
||||
dist
|
||||
dll
|
||||
main.js
|
||||
main.js.map
|
||||
|
||||
.idea
|
||||
npm-debug.log.*
|
||||
|
||||
zcashd
|
||||
/build/
|
||||
native/artifacts.json
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"],
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
],
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-prettier"]
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dzannotti.vscode-babel-coloring",
|
||||
"EditorConfig.EditorConfig",
|
||||
"flowtype.flow-for-vscode"
|
||||
]
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
/* eslint-disable camelcase */
|
||||
const fs = require('fs');
|
||||
const electron_notarize = require('electron-notarize');
|
||||
const fs = require("fs");
|
||||
const electron_notarize = require("electron-notarize");
|
||||
|
||||
module.exports = async function(params) {
|
||||
module.exports = async function (params) {
|
||||
// Only notarize the app on Mac OS only.
|
||||
if (process.platform !== 'darwin') {
|
||||
if (process.platform !== "darwin") {
|
||||
return;
|
||||
}
|
||||
// console.log('afterSign hook triggered', params);
|
||||
|
||||
// Same appId in electron-builder.
|
||||
const appId = 'co.zecwallet.lite';
|
||||
const appId = "co.zecwallet.lite";
|
||||
|
||||
const appPath = params.artifactPaths.find(p => p.endsWith('.dmg'));
|
||||
const appPath = params.artifactPaths.find((p) => p.endsWith(".dmg"));
|
||||
|
||||
if (!fs.existsSync(appPath)) {
|
||||
throw new Error(`Cannot find application at: ${appPath}`);
|
||||
|
@ -25,7 +25,7 @@ module.exports = async function(params) {
|
|||
appBundleId: appId,
|
||||
appPath,
|
||||
appleId: process.env.appleId,
|
||||
appleIdPassword: process.env.appleIdPassword
|
||||
appleIdPassword: process.env.appleIdPassword,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
48
app/app.html
|
@ -1,48 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Zecwallet Lite</title>
|
||||
|
||||
<!-- Allow unsafe only for inline script tags, no external content-->
|
||||
<script nonce="1u1na">
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
document.write(
|
||||
`<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; style-src 'self'; " >`
|
||||
);
|
||||
}
|
||||
|
||||
(() => {
|
||||
if (!process.env.START_HOT) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = './dist/style.css';
|
||||
// HACK: Writing the script path should be done with webpack
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="8SHwb">
|
||||
{
|
||||
const scripts = [];
|
||||
|
||||
// Dynamically insert the DLL script in development env in the
|
||||
// renderer process
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
scripts.push('../dll/renderer.dev.dll.js');
|
||||
}
|
||||
|
||||
// Dynamically insert the bundled app script in the renderer process
|
||||
const port = process.env.PORT || 1212;
|
||||
scripts.push(
|
||||
process.env.START_HOT ? `http://localhost:${port}/dist/renderer.dev.js` : './dist/renderer.prod.js'
|
||||
);
|
||||
|
||||
document.write(scripts.map(script => `<script defer src="${script}"><\/script>`).join(''));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
app/app.icns
502
app/companion.js
|
@ -1,502 +0,0 @@
|
|||
/* eslint-disable flowtype/no-weak-types */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import _sodium from 'libsodium-wrappers-sumo';
|
||||
import Store from 'electron-store';
|
||||
import WebSocket from 'ws';
|
||||
import AppState, { ConnectedCompanionApp } from './components/AppState';
|
||||
import Utils from './utils/utils';
|
||||
|
||||
// Wormhole code is sha256(sha256(secret_key))
|
||||
function getWormholeCode(keyHex: string, sodium: any): string {
|
||||
const key = sodium.from_hex(keyHex);
|
||||
|
||||
const pass1 = sodium.crypto_hash_sha256(key);
|
||||
const pass2 = sodium.to_hex(sodium.crypto_hash_sha256(pass1));
|
||||
|
||||
return pass2;
|
||||
}
|
||||
|
||||
// A class that connects to wormhole given a secret key
|
||||
class WormholeClient {
|
||||
keyHex: string;
|
||||
|
||||
wormholeCode: string;
|
||||
|
||||
sodium: any;
|
||||
|
||||
wss: WebSocket = null;
|
||||
|
||||
listner: CompanionAppListener = null;
|
||||
|
||||
keepAliveTimerID: TimerID = null;
|
||||
|
||||
constructor(keyHex: string, sodium: any, listner: CompanionAppListener) {
|
||||
this.keyHex = keyHex;
|
||||
this.sodium = sodium;
|
||||
this.listner = listner;
|
||||
|
||||
this.wormholeCode = getWormholeCode(keyHex, this.sodium);
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.wss = new WebSocket('wss://wormhole.zecqtwallet.com:443');
|
||||
|
||||
this.wss.on('open', () => {
|
||||
// On open, register ourself
|
||||
const reg = { register: getWormholeCode(this.keyHex, this.sodium) };
|
||||
|
||||
// No encryption for the register call
|
||||
this.wss.send(JSON.stringify(reg));
|
||||
|
||||
// Now, do a ping every 4 minutes to keep the connection alive.
|
||||
this.keepAliveTimerID = setInterval(() => {
|
||||
const ping = { ping: 'ping' };
|
||||
this.wss.send(JSON.stringify(ping));
|
||||
}, 4 * 60 * 1000);
|
||||
});
|
||||
|
||||
this.wss.on('message', data => {
|
||||
this.listner.processIncoming(data, this.keyHex, this.wss);
|
||||
});
|
||||
|
||||
this.wss.on('close', (code, reason) => {
|
||||
console.log('Socket closed for ', this.keyHex, code, reason);
|
||||
});
|
||||
|
||||
this.wss.on('error', (ws, err) => {
|
||||
console.log('ws error', err);
|
||||
});
|
||||
}
|
||||
|
||||
getKeyHex(): string {
|
||||
return this.keyHex;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.keepAliveTimerID) {
|
||||
clearInterval(this.keepAliveTimerID);
|
||||
}
|
||||
|
||||
// Close the websocket.
|
||||
if (this.wss) {
|
||||
this.wss.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The singleton Companion App listener, that can spawn a wormhole server
|
||||
// or (multiple) wormhole clients
|
||||
export default class CompanionAppListener {
|
||||
sodium = null;
|
||||
|
||||
fnGetState: () => AppState = null;
|
||||
|
||||
fnSendTransaction: ([]) => string = null;
|
||||
|
||||
fnUpdateConnectedClient: (string, number) => void = null;
|
||||
|
||||
permWormholeClient: WormholeClient = null;
|
||||
|
||||
tmpWormholeClient: WormholeClient = null;
|
||||
|
||||
constructor(
|
||||
fnGetSate: () => AppState,
|
||||
fnSendTransaction: ([]) => string,
|
||||
fnUpdateConnectedClient: (string, number) => void
|
||||
) {
|
||||
this.fnGetState = fnGetSate;
|
||||
this.fnSendTransaction = fnSendTransaction;
|
||||
this.fnUpdateConnectedClient = fnUpdateConnectedClient;
|
||||
}
|
||||
|
||||
async setUp() {
|
||||
await _sodium.ready;
|
||||
this.sodium = _sodium;
|
||||
|
||||
// Create a new wormhole listner
|
||||
const permKeyHex = this.getEncKey();
|
||||
if (permKeyHex) {
|
||||
this.permWormholeClient = new WormholeClient(permKeyHex, this.sodium, this);
|
||||
}
|
||||
|
||||
// At startup, set the last client name/time by loading it
|
||||
const store = new Store();
|
||||
const name = store.get('companion/name');
|
||||
const lastSeen = store.get('companion/lastseen');
|
||||
|
||||
if (name && lastSeen) {
|
||||
const o = new ConnectedCompanionApp();
|
||||
o.name = name;
|
||||
o.lastSeen = lastSeen;
|
||||
|
||||
this.fnUpdateConnectedClient(o);
|
||||
}
|
||||
}
|
||||
|
||||
createTmpClient(keyHex: string) {
|
||||
if (this.tmpWormholeClient) {
|
||||
this.tmpWormholeClient.close();
|
||||
}
|
||||
|
||||
this.tmpWormholeClient = new WormholeClient(keyHex, this.sodium, this);
|
||||
}
|
||||
|
||||
closeTmpClient() {
|
||||
if (this.tmpWormholeClient) {
|
||||
this.tmpWormholeClient.close();
|
||||
|
||||
this.tmpWormholeClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
replacePermClientWithTmp() {
|
||||
if (this.permWormholeClient) {
|
||||
this.permWormholeClient.close();
|
||||
}
|
||||
|
||||
// Replace the stored code with the new one
|
||||
this.permWormholeClient = this.tmpWormholeClient;
|
||||
this.tmpWormholeClient = null;
|
||||
this.setEncKey(this.permWormholeClient.getKeyHex());
|
||||
|
||||
// Reset local nonce
|
||||
const store = new Store();
|
||||
store.delete('companion/localnonce');
|
||||
}
|
||||
|
||||
async processIncoming(data: string, keyHex: string, ws: Websocket) {
|
||||
const dataJson = JSON.parse(data);
|
||||
|
||||
// If the wormhole sends some messages, we ignore them
|
||||
if ('error' in dataJson) {
|
||||
console.log('Incoming data contains an error message', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is a ping, just ignore it
|
||||
if ('ping' in dataJson) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Then, check if the message is encrpted
|
||||
if (!('nonce' in dataJson)) {
|
||||
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
|
||||
ws.send(JSON.stringify(err));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let cmd;
|
||||
|
||||
// If decryption passes and this is a tmp wormhole client, then set it as the permanant client
|
||||
if (this.tmpWormholeClient && keyHex === this.tmpWormholeClient.getKeyHex()) {
|
||||
const { decrypted, nonce } = this.decryptIncoming(data, keyHex, false);
|
||||
if (!decrypted) {
|
||||
console.log('Decryption failed');
|
||||
|
||||
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
|
||||
ws.send(JSON.stringify(err));
|
||||
return;
|
||||
}
|
||||
cmd = JSON.parse(decrypted);
|
||||
|
||||
// Replace the permanant client
|
||||
this.replacePermClientWithTmp();
|
||||
|
||||
this.updateRemoteNonce(nonce);
|
||||
} else {
|
||||
const { decrypted, nonce } = this.decryptIncoming(data, keyHex, true);
|
||||
if (!decrypted) {
|
||||
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
|
||||
ws.send(JSON.stringify(err));
|
||||
|
||||
console.log('Decryption failed');
|
||||
return;
|
||||
}
|
||||
|
||||
cmd = JSON.parse(decrypted);
|
||||
|
||||
this.updateRemoteNonce(nonce);
|
||||
}
|
||||
|
||||
if (cmd.command === 'getInfo') {
|
||||
const response = this.doGetInfo(cmd);
|
||||
ws.send(this.encryptOutgoing(response, keyHex));
|
||||
} else if (cmd.command === 'getTransactions') {
|
||||
const response = this.doGetTransactions();
|
||||
ws.send(this.encryptOutgoing(response, keyHex));
|
||||
} else if (cmd.command === 'sendTx') {
|
||||
const response = await this.doSendTransaction(cmd, ws);
|
||||
ws.send(this.encryptOutgoing(response, keyHex));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new secret key
|
||||
genNewKeyHex(): string {
|
||||
const keyHex = this.sodium.to_hex(this.sodium.crypto_secretbox_keygen());
|
||||
|
||||
return keyHex;
|
||||
}
|
||||
|
||||
getEncKey(): string {
|
||||
// Get the nonce. Increment and store the nonce for next use
|
||||
const store = new Store();
|
||||
const keyHex = store.get('companion/key');
|
||||
|
||||
return keyHex;
|
||||
}
|
||||
|
||||
setEncKey(keyHex: string) {
|
||||
// Get the nonce. Increment and store the nonce for next use
|
||||
const store = new Store();
|
||||
store.set('companion/key', keyHex);
|
||||
}
|
||||
|
||||
saveLastClientName(name: string) {
|
||||
// Save the last client name
|
||||
const store = new Store();
|
||||
store.set('companion/name', name);
|
||||
|
||||
if (name) {
|
||||
const now = Date.now();
|
||||
store.set('companion/lastseen', now);
|
||||
|
||||
const o = new ConnectedCompanionApp();
|
||||
o.name = name;
|
||||
o.lastSeen = now;
|
||||
|
||||
this.fnUpdateConnectedClient(o);
|
||||
} else {
|
||||
this.fnUpdateConnectedClient(null);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectLastClient() {
|
||||
// Remove the permanant connection
|
||||
if (this.permWormholeClient) {
|
||||
this.permWormholeClient.close();
|
||||
}
|
||||
|
||||
this.saveLastClientName(null);
|
||||
this.setEncKey(null);
|
||||
}
|
||||
|
||||
getRemoteNonce(): string {
|
||||
const store = new Store();
|
||||
const nonceHex = store.get('companion/remotenonce');
|
||||
|
||||
return nonceHex;
|
||||
}
|
||||
|
||||
updateRemoteNonce(nonce: string) {
|
||||
if (nonce) {
|
||||
const store = new Store();
|
||||
store.set('companion/remotenonce', nonce);
|
||||
}
|
||||
}
|
||||
|
||||
getLocalNonce(): string {
|
||||
// Get the nonce. Increment and store the nonce for next use
|
||||
const store = new Store();
|
||||
const nonceHex = store.get('companion/localnonce', `01${'00'.repeat(this.sodium.crypto_secretbox_NONCEBYTES - 1)}`);
|
||||
|
||||
// Increment nonce
|
||||
const newNonce = this.sodium.from_hex(nonceHex);
|
||||
|
||||
this.sodium.increment(newNonce);
|
||||
this.sodium.increment(newNonce);
|
||||
store.set('companion/localnonce', this.sodium.to_hex(newNonce));
|
||||
|
||||
return nonceHex;
|
||||
}
|
||||
|
||||
encryptOutgoing(str: string, keyHex: string): string {
|
||||
if (!keyHex) {
|
||||
console.log('No secret key');
|
||||
throw Error('No Secret Key');
|
||||
}
|
||||
|
||||
const nonceHex = this.getLocalNonce();
|
||||
|
||||
const nonce = this.sodium.from_hex(nonceHex);
|
||||
const key = this.sodium.from_hex(keyHex);
|
||||
|
||||
const encrypted = this.sodium.crypto_secretbox_easy(str, nonce, key);
|
||||
const encryptedHex = this.sodium.to_hex(encrypted);
|
||||
|
||||
const resp = {
|
||||
nonce: this.sodium.to_hex(nonce),
|
||||
payload: encryptedHex,
|
||||
to: getWormholeCode(keyHex, this.sodium)
|
||||
};
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
decryptIncoming(msg: string, keyHex: string, checkNonce: boolean): any {
|
||||
const msgJson = JSON.parse(msg);
|
||||
console.log('trying to decrypt', msgJson);
|
||||
|
||||
if (!keyHex) {
|
||||
console.log('No secret key');
|
||||
throw Error('No Secret Key');
|
||||
}
|
||||
|
||||
const key = this.sodium.from_hex(keyHex);
|
||||
const nonce = this.sodium.from_hex(msgJson.nonce);
|
||||
|
||||
if (checkNonce) {
|
||||
const prevNonce = this.sodium.from_hex(this.getRemoteNonce());
|
||||
if (prevNonce && this.sodium.compare(prevNonce, nonce) >= 0) {
|
||||
return { decrypted: null };
|
||||
}
|
||||
}
|
||||
|
||||
const cipherText = this.sodium.from_hex(msgJson.payload);
|
||||
|
||||
const decrypted = this.sodium.to_string(this.sodium.crypto_secretbox_open_easy(cipherText, nonce, key));
|
||||
|
||||
return { decrypted, nonce: msgJson.nonce };
|
||||
}
|
||||
|
||||
doGetInfo(cmd: any): string {
|
||||
const appState = this.fnGetState();
|
||||
|
||||
if (cmd && cmd.name) {
|
||||
this.saveLastClientName(cmd.name);
|
||||
}
|
||||
|
||||
const saplingAddress = appState.addresses.find(a => Utils.isSapling(a));
|
||||
const tAddress = appState.addresses.find(a => Utils.isTransparent(a));
|
||||
const balance = parseFloat(appState.totalBalance.total);
|
||||
const maxspendable = parseFloat(appState.totalBalance.total);
|
||||
const maxzspendable = parseFloat(appState.totalBalance.private);
|
||||
const tokenName = appState.info.currencyName;
|
||||
const zecprice = parseFloat(appState.info.zecPrice);
|
||||
|
||||
const resp = {
|
||||
version: 1.0,
|
||||
command: 'getInfo',
|
||||
saplingAddress,
|
||||
tAddress,
|
||||
balance,
|
||||
maxspendable,
|
||||
maxzspendable,
|
||||
tokenName,
|
||||
zecprice,
|
||||
serverversion: '0.9.2'
|
||||
};
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
doGetTransactions(): string {
|
||||
const appState = this.fnGetState();
|
||||
|
||||
let txlist = [];
|
||||
if (appState.transactions) {
|
||||
// Get only the last 20 txns
|
||||
txlist = appState.transactions.slice(0, 20).map(t => {
|
||||
let memo = t.detailedTxns && t.detailedTxns.length > 0 ? t.detailedTxns[0].memo : '';
|
||||
if (memo) {
|
||||
memo = memo.trimRight();
|
||||
} else {
|
||||
memo = '';
|
||||
}
|
||||
|
||||
const txResp = {
|
||||
type: t.type,
|
||||
datetime: t.time,
|
||||
amount: t.amount.toFixed(8),
|
||||
txid: t.txid,
|
||||
address: t.address,
|
||||
memo,
|
||||
confirmations: t.confirmations
|
||||
};
|
||||
|
||||
return txResp;
|
||||
});
|
||||
}
|
||||
|
||||
const resp = {
|
||||
version: 1.0,
|
||||
command: 'getTransactions',
|
||||
transactions: txlist
|
||||
};
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
doSendTransaction(cmd: any, ws: WebSocket): string {
|
||||
// "command":"sendTx","tx":{"amount":"0.00019927","to":"zs1pzr7ee53jwa3h3yvzdjf7meruujq84w5rsr5kuvye9qg552kdyz5cs5ywy5hxkxcfvy9wln94p6","memo":""}}
|
||||
const inpTx = cmd.tx;
|
||||
const appState = this.fnGetState();
|
||||
|
||||
// eslint-disable-next-line radix
|
||||
let sendingAmount = parseFloat(inpTx.amount);
|
||||
const buildError = (reason: string): string => {
|
||||
const resp = {
|
||||
errorCode: -1,
|
||||
errorMessage: `Couldn't send Tx:${reason}`
|
||||
};
|
||||
|
||||
// console.log('sendtx error', resp);
|
||||
return JSON.stringify(resp);
|
||||
};
|
||||
|
||||
// First, find an address that can send the correct amount.
|
||||
const fromAddress = appState.addressesWithBalance.find(ab => ab.balance > sendingAmount);
|
||||
if (!fromAddress) {
|
||||
return buildError(`No address with sufficient balance to send ${sendingAmount}`);
|
||||
}
|
||||
|
||||
const memo = !inpTx.memo || inpTx.memo.trim() === '' ? null : inpTx.memo;
|
||||
|
||||
// The lightclient expects zats (and not ZEC)
|
||||
sendingAmount = parseInt(sendingAmount * 10 ** 8, 10);
|
||||
|
||||
// Build a sendJSON object
|
||||
const sendJSON = [];
|
||||
if (memo) {
|
||||
sendJSON.push({ address: inpTx.to, amount: sendingAmount, memo });
|
||||
} else {
|
||||
sendJSON.push({ address: inpTx.to, amount: sendingAmount });
|
||||
}
|
||||
|
||||
console.log('sendjson is', sendJSON);
|
||||
|
||||
let resp;
|
||||
|
||||
try {
|
||||
const txid = this.fnSendTransaction(sendJSON);
|
||||
|
||||
// After the transaction is submitted, we return an intermediate success.
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTx',
|
||||
result: 'success'
|
||||
};
|
||||
ws.send(this.encryptOutgoing(JSON.stringify(resp)));
|
||||
|
||||
// And then another one when the Tx was submitted successfully. For lightclient, this is the same,
|
||||
// so we end up sending 2 responses back to back
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTxSubmitted',
|
||||
txid
|
||||
};
|
||||
} catch (err) {
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTxFailed',
|
||||
err
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import React from 'react';
|
||||
import cstyles from './Common.module.css';
|
||||
import Utils from '../utils/utils';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const BalanceBlockHighlight = ({ zecValue, usdValue, topLabel, currencyName, tooltip }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '1em' }} title={tooltip}>
|
||||
{topLabel && (
|
||||
<div className={[cstyles.small].join(' ')}>
|
||||
{topLabel}
|
||||
{tooltip && (
|
||||
<span>
|
||||
|
||||
<i className={[cstyles.green, 'fas', 'fa-info-circle'].join(' ')} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={[cstyles.highlight, cstyles.xlarge].join(' ')}>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small].join(' ')}>{usdValue}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const BalanceBlock = ({ zecValue, usdValue, topLabel, currencyName }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div className={cstyles.padall}>
|
||||
<div className={[cstyles.small].join(' ')}>{topLabel}</div>
|
||||
<div className={[cstyles.highlight, cstyles.large].join(' ')}>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small].join(' ')}>{usdValue}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,110 +0,0 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import QRCode from 'qrcode.react';
|
||||
import dateformat from 'dateformat';
|
||||
import cstyles from './Common.module.css';
|
||||
import styles from './WormholeConnection.module.css';
|
||||
import CompanionAppListener from '../companion';
|
||||
import { ConnectedCompanionApp } from './AppState';
|
||||
|
||||
type Props = {
|
||||
companionAppListener: CompanionAppListener,
|
||||
connectedCompanionApp: ConnectedCompanionApp | null
|
||||
};
|
||||
|
||||
type State = {
|
||||
tempKeyHex: string
|
||||
};
|
||||
|
||||
export default class WormholeConnection extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { tempKeyHex: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// If there is no temp key, create one
|
||||
const { companionAppListener } = this.props;
|
||||
const { tempKeyHex } = this.state;
|
||||
|
||||
if (!tempKeyHex) {
|
||||
const newKey = companionAppListener.genNewKeyHex();
|
||||
companionAppListener.createTmpClient(newKey);
|
||||
this.setState({ tempKeyHex: newKey });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { companionAppListener } = this.props;
|
||||
companionAppListener.closeTmpClient();
|
||||
}
|
||||
|
||||
disconnectCurrentMobile = () => {
|
||||
const { companionAppListener } = this.props;
|
||||
companionAppListener.disconnectLastClient();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tempKeyHex } = this.state;
|
||||
const { connectedCompanionApp } = this.props;
|
||||
|
||||
const clientName = (connectedCompanionApp && connectedCompanionApp.name) || null;
|
||||
const lastSeen = (connectedCompanionApp && connectedCompanionApp.lastSeen) || null;
|
||||
|
||||
let datePart = null;
|
||||
let timePart = null;
|
||||
|
||||
if (lastSeen) {
|
||||
const txDate = new Date(lastSeen);
|
||||
datePart = dateformat(txDate, 'mmm dd, yyyy');
|
||||
timePart = dateformat(txDate, 'hh:MM tt');
|
||||
}
|
||||
|
||||
const connStr = `ws://127.0.0.1:7070,${tempKeyHex},1`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Connect Mobile App</div>
|
||||
|
||||
<div className={styles.qrcodecontainer}>
|
||||
<div>This is your connection code. Scan this QR code from the Zecwallet Companion App.</div>
|
||||
|
||||
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>
|
||||
<QRCode value={connStr} size={256} level="M" className={styles.wormholeqr} />
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.margintoplarge, cstyles.small].join(' ')}>{connStr}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.appinfocontainer}>
|
||||
<div className={styles.appinfo}>
|
||||
{clientName && (
|
||||
<div>
|
||||
<div className={cstyles.flexspacebetween}>
|
||||
<div style={{ flex: 1 }} className={cstyles.sublight}>
|
||||
Current App Connected:
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>{clientName}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ flex: 1 }} className={cstyles.sublight}>
|
||||
Last Seen:
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
{datePart} {timePart}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.disconnectCurrentMobile}>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!clientName && <div>No Companion App Connected</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
.wormholeqr {
|
||||
width: 400px;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.qrcodecontainer {
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.appinfocontainer {
|
||||
width: 50%;
|
||||
margin-left: 25%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.appinfo {
|
||||
margin-top: 20px;
|
||||
border: 1px solid grey;
|
||||
padding: 16px;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
};
|
||||
|
||||
export default class App extends React.Component<Props> {
|
||||
props: Props;
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
return <>{children}</>;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import Routes from '../Routes';
|
||||
|
||||
const Root = () => (
|
||||
<Router>
|
||||
<Routes />
|
||||
</Router>
|
||||
);
|
||||
|
||||
export default hot(Root);
|
14
app/index.js
|
@ -1,14 +0,0 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { AppContainer as ReactHotAppContainer } from 'react-hot-loader';
|
||||
import Root from './containers/Root';
|
||||
import './app.global.css';
|
||||
|
||||
const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer;
|
||||
|
||||
render(
|
||||
<AppContainer>
|
||||
<Root />
|
||||
</AppContainer>,
|
||||
document.getElementById('root')
|
||||
);
|
152
app/main.dev.js
|
@ -1,152 +0,0 @@
|
|||
/* eslint-disable compat/compat */
|
||||
/* eslint global-require: off */
|
||||
|
||||
/**
|
||||
* This module executes inside of electron's main process. You can start
|
||||
* electron renderer process from here and communicate with the other processes
|
||||
* through IPC.
|
||||
*
|
||||
* When running `yarn build` or `yarn build-main`, this file is compiled to
|
||||
* `./app/main.prod.js` using webpack. This gives us some performance wins.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import { app, shell, BrowserWindow, ipcMain } from 'electron';
|
||||
import log from 'electron-log';
|
||||
import MenuBuilder from './menu';
|
||||
|
||||
export default class AppUpdater {
|
||||
constructor() {
|
||||
log.transports.file.level = 'info';
|
||||
}
|
||||
}
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const sourceMapSupport = require('source-map-support');
|
||||
sourceMapSupport.install();
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
require('electron-debug')();
|
||||
}
|
||||
|
||||
const installExtensions = async () => {
|
||||
const installer = require('electron-devtools-installer');
|
||||
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
|
||||
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
|
||||
|
||||
return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(console.log);
|
||||
};
|
||||
|
||||
let waitingForClose = false;
|
||||
let proceedToClose = false;
|
||||
|
||||
const createWindow = async () => {
|
||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
await installExtensions();
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1300,
|
||||
height: 728,
|
||||
minHeight: 500,
|
||||
minWidth: 1100,
|
||||
webPreferences: {
|
||||
// Allow node integration because we're only loading local content here.
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.loadURL(`file://${__dirname}/app.html`);
|
||||
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('new-window', async (eventInner, navigationUrl) => {
|
||||
// In this example, we'll ask the operating system
|
||||
// to open this event's url in the default browser.
|
||||
console.log('attempting to open window', navigationUrl);
|
||||
|
||||
eventInner.preventDefault();
|
||||
|
||||
await shell.openExternal(navigationUrl);
|
||||
});
|
||||
});
|
||||
|
||||
// @TODO: Use 'ready-to-show' event
|
||||
// https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
if (!mainWindow) {
|
||||
throw new Error('"mainWindow" is not defined');
|
||||
}
|
||||
if (process.env.START_MINIMIZED) {
|
||||
mainWindow.minimize();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('close', event => {
|
||||
// If we are clear to close, then return and allow everything to close
|
||||
if (proceedToClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're already waiting for close, then don't allow another close event to actually close the window
|
||||
if (waitingForClose) {
|
||||
console.log('Waiting for close... Timeout in 10s');
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
waitingForClose = true;
|
||||
event.preventDefault();
|
||||
|
||||
ipcMain.on('appquitdone', () => {
|
||||
waitingForClose = false;
|
||||
proceedToClose = true;
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// $FlowFixMe
|
||||
mainWindow.webContents.send('appquitting');
|
||||
|
||||
// Failsafe, timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
waitingForClose = false;
|
||||
proceedToClose = true;
|
||||
console.log('Timeout, quitting');
|
||||
|
||||
app.quit();
|
||||
}, 5 * 1000);
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
const menuBuilder = new MenuBuilder(mainWindow);
|
||||
menuBuilder.buildMenu();
|
||||
|
||||
// Remove this if your app does not use auto updates
|
||||
// eslint-disable-next-line
|
||||
new AppUpdater();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add event listeners...
|
||||
*/
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) createWindow();
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
|
437
app/menu.js
|
@ -1,437 +0,0 @@
|
|||
// @flow
|
||||
import { app, Menu, shell, BrowserWindow } from 'electron';
|
||||
|
||||
export default class MenuBuilder {
|
||||
mainWindow: BrowserWindow;
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
buildMenu() {
|
||||
// if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
// // this.setupDevelopmentEnvironment();
|
||||
// }
|
||||
|
||||
const template = process.platform === 'darwin' ? this.buildDarwinTemplate() : this.buildDefaultTemplate();
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
const selectionMenu = Menu.buildFromTemplate([{ role: 'copy' }, { type: 'separator' }, { role: 'selectall' }]);
|
||||
|
||||
const inputMenu = Menu.buildFromTemplate([
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'selectall' }
|
||||
]);
|
||||
|
||||
this.mainWindow.webContents.on('context-menu', (e, props) => {
|
||||
const { selectionText, isEditable } = props;
|
||||
if (isEditable) {
|
||||
inputMenu.popup(this.mainWindow);
|
||||
} else if (selectionText && selectionText.trim() !== '') {
|
||||
selectionMenu.popup(this.mainWindow);
|
||||
} else if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
const { x, y } = props;
|
||||
|
||||
Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Inspect element',
|
||||
click: () => {
|
||||
this.mainWindow.inspectElement(x, y);
|
||||
}
|
||||
}
|
||||
]).popup(this.mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
buildDarwinTemplate() {
|
||||
const { mainWindow } = this;
|
||||
|
||||
const subMenuAbout = {
|
||||
label: 'Zecwallet Lite',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Zecwallet Lite',
|
||||
selector: 'orderFrontStandardAboutPanel:',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('about');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Hide Zecwallet Lite',
|
||||
accelerator: 'Command+H',
|
||||
selector: 'hide:'
|
||||
},
|
||||
{
|
||||
label: 'Hide Others',
|
||||
accelerator: 'Command+Shift+H',
|
||||
selector: 'hideOtherApplications:'
|
||||
},
|
||||
{ label: 'Show All', selector: 'unhideAllApplications:' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
click: () => {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const subMenuEdit = {
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
|
||||
{ label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
|
||||
{ label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
|
||||
{ label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
|
||||
{
|
||||
label: 'Select All',
|
||||
accelerator: 'Command+A',
|
||||
selector: 'selectAll:'
|
||||
}
|
||||
]
|
||||
};
|
||||
const subMenuViewDev = {
|
||||
label: 'Wallet',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Wallet Seed',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('seed');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Import Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('import');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '&Pay URI',
|
||||
accelerator: 'Ctrl+P',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('payuri');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Export All &Transactions',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportalltx');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Rescan',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('rescan');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'View Lightwalletd Info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Encrypt Wallet',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('encrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Remove Wallet Encryption',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('decrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Unlock',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('unlock');
|
||||
}
|
||||
}
|
||||
// { type: 'separator' },
|
||||
// {
|
||||
// label: 'Toggle Developer Tools',
|
||||
// accelerator: 'Alt+Command+I',
|
||||
// click: () => {
|
||||
// this.mainWindow.toggleDevTools();
|
||||
// }
|
||||
// }
|
||||
]
|
||||
};
|
||||
const subMenuViewProd = {
|
||||
label: 'Wallet',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Wallet Seed',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('seed');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Import Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('import');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '&Pay URI',
|
||||
accelerator: 'Ctrl+P',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('payuri');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Export All &Transactions',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportalltx');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Rescan',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('rescan');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Server info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Encrypt Wallet',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('encrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Remove Wallet Encryption',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('decrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Unlock',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('unlock');
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const subMenuWindow = {
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Minimize',
|
||||
accelerator: 'Command+M',
|
||||
selector: 'performMiniaturize:'
|
||||
},
|
||||
{ label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Bring All to Front', selector: 'arrangeInFront:' }
|
||||
]
|
||||
};
|
||||
const subMenuHelp = {
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Donate',
|
||||
click() {
|
||||
mainWindow.webContents.send('donate');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Check github.com for updates',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/releases');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'File a bug...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/issues');
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const subMenuView = process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd;
|
||||
|
||||
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||
}
|
||||
|
||||
buildDefaultTemplate() {
|
||||
const { mainWindow } = this;
|
||||
|
||||
const templateDefault = [
|
||||
{
|
||||
label: '&File',
|
||||
submenu: [
|
||||
{
|
||||
label: '&Pay URI',
|
||||
accelerator: 'Ctrl+P',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('payuri');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Close',
|
||||
accelerator: 'Ctrl+W',
|
||||
click: () => {
|
||||
this.mainWindow.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '&Wallet',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Wallet Seed',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('seed');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Import Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('import');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Export All &Transactions',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportalltx');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Rescan',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('rescan');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Server info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
// {
|
||||
// label: 'Devtools',
|
||||
// click: () => {
|
||||
// mainWindow.webContents.openDevTools();
|
||||
// }
|
||||
// },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Encrypt Wallet',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('encrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Remove Wallet Encryption',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('decrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Unlock',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('unlock');
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Zecwallet Lite',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('about');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Donate',
|
||||
click() {
|
||||
mainWindow.webContents.send('donate');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Check github.com for updates',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/releases');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'File a bug...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/issues');
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return templateDefault;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"name": "electron-react-boilerplate",
|
||||
"version": "0.18.1",
|
||||
"lockfileVersion": 1
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "zecwallet-lite",
|
||||
"productName": "Zecwallet Lite",
|
||||
"version": "1.6.3",
|
||||
"description": "Zecwallet Lite",
|
||||
"main": "./main.prod.js",
|
||||
"author": {
|
||||
"name": "Aditya Kulkarni",
|
||||
"email": "aditya@zecwallet.co",
|
||||
"url": "https://github.com/adityapk00/zecwallet-electron"
|
||||
},
|
||||
"scripts": {
|
||||
"electron-rebuild": "node -r ../internals/scripts/BabelRegister.js ../internals/scripts/ElectronRebuild.js",
|
||||
"postinstall": "yarn electron-rebuild"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
import { parseZcashURI } from './uris';
|
||||
|
||||
test('ZIP321 case 1', () => {
|
||||
const targets = parseZcashURI(
|
||||
'zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase'
|
||||
);
|
||||
|
||||
expect(targets.length).toBe(1);
|
||||
expect(targets[0].address).toBe(
|
||||
'ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez'
|
||||
);
|
||||
expect(targets[0].message).toBe('Thank you for your purchase');
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBe(1);
|
||||
expect(targets[0].memoString).toBe('This is a simple memo.');
|
||||
});
|
||||
|
||||
test('ZIP321 case 2', () => {
|
||||
const targets = parseZcashURI(
|
||||
'zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok'
|
||||
);
|
||||
|
||||
expect(targets.length).toBe(2);
|
||||
|
||||
expect(targets[0].address).toBe('tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU');
|
||||
expect(targets[0].message).toBeUndefined();
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBe(123.456);
|
||||
expect(targets[0].memoString).toBeUndefined();
|
||||
expect(targets[0].memoBase64).toBeUndefined();
|
||||
|
||||
expect(targets[1].address).toBe(
|
||||
'ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez'
|
||||
);
|
||||
expect(targets[1].message).toBeUndefined();
|
||||
expect(targets[1].label).toBeUndefined();
|
||||
expect(targets[1].amount).toBe(0.789);
|
||||
expect(targets[1].memoString).toBe('This is a unicode memo ✨🦄🏆🎉');
|
||||
});
|
||||
|
||||
test('coinbase URI', () => {
|
||||
const targets = parseZcashURI('zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU');
|
||||
|
||||
expect(targets.length).toBe(1);
|
||||
expect(targets[0].message).toBeUndefined();
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBeUndefined();
|
||||
expect(targets[0].memoString).toBeUndefined();
|
||||
expect(targets[0].memoBase64).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Plain URI', () => {
|
||||
const targets = parseZcashURI('tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU');
|
||||
|
||||
expect(targets.length).toBe(1);
|
||||
expect(targets[0].message).toBeUndefined();
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBeUndefined();
|
||||
expect(targets[0].memoString).toBeUndefined();
|
||||
expect(targets[0].memoBase64).toBeUndefined();
|
||||
});
|
||||
|
||||
test('bad uris', () => {
|
||||
// bad protocol
|
||||
let error = parseZcashURI('badprotocol:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=123.456');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// bad address
|
||||
error = parseZcashURI('zcash:badaddress?amount=123.456');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// no address
|
||||
error = parseZcashURI('zcash:?amount=123.456');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// bad param name
|
||||
error = parseZcashURI('zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?badparam=3');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// index=1 doesn't have amount
|
||||
error = parseZcashURI(
|
||||
'zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=2&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU'
|
||||
);
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// duplicate param
|
||||
error = parseZcashURI('zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=3&amount=3');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// bad index
|
||||
error = parseZcashURI(
|
||||
'zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=2&address.a=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount.a=3'
|
||||
);
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// index=1 is missing
|
||||
error = parseZcashURI(
|
||||
'zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=0.1&address.2=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount.2=2'
|
||||
);
|
||||
expect(typeof error).toBe('string');
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
39
appveyor.yml
|
@ -1,39 +0,0 @@
|
|||
image: Visual Studio 2017
|
||||
|
||||
platform:
|
||||
- x64
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: 10
|
||||
|
||||
cache:
|
||||
- '%LOCALAPPDATA%/Yarn'
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
- flow-typed
|
||||
- '%USERPROFILE%\.electron'
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
build: off
|
||||
|
||||
version: '{build}'
|
||||
|
||||
shallow_clone: true
|
||||
|
||||
clone_depth: 1
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- set CI=true
|
||||
- yarn
|
||||
|
||||
test_script:
|
||||
- yarn package-ci
|
||||
- yarn lint
|
||||
# - yarn flow
|
||||
- yarn test
|
||||
- yarn build-e2e
|
||||
- yarn test-e2e
|
|
@ -1,66 +0,0 @@
|
|||
/* eslint global-require: off */
|
||||
|
||||
const developmentEnvironments = ['development', 'test'];
|
||||
|
||||
const developmentPlugins = [require('react-hot-loader/babel')];
|
||||
|
||||
const productionPlugins = [
|
||||
require('babel-plugin-dev-expression'),
|
||||
|
||||
// babel-preset-react-optimize
|
||||
require('@babel/plugin-transform-react-constant-elements'),
|
||||
require('@babel/plugin-transform-react-inline-elements'),
|
||||
require('babel-plugin-transform-react-remove-prop-types')
|
||||
];
|
||||
|
||||
module.exports = api => {
|
||||
// see docs about api at https://babeljs.io/docs/en/config-files#apicache
|
||||
|
||||
const development = api.env(developmentEnvironments);
|
||||
|
||||
return {
|
||||
presets: [
|
||||
[
|
||||
require('@babel/preset-env'),
|
||||
{
|
||||
targets: { electron: require('electron/package.json').version }
|
||||
}
|
||||
],
|
||||
require('@babel/preset-flow'),
|
||||
[require('@babel/preset-react'), { development }]
|
||||
],
|
||||
plugins: [
|
||||
// Stage 0
|
||||
require('@babel/plugin-proposal-function-bind'),
|
||||
|
||||
// Stage 1
|
||||
require('@babel/plugin-proposal-export-default-from'),
|
||||
require('@babel/plugin-proposal-logical-assignment-operators'),
|
||||
[require('@babel/plugin-proposal-optional-chaining'), { loose: false }],
|
||||
[
|
||||
require('@babel/plugin-proposal-pipeline-operator'),
|
||||
{ proposal: 'minimal' }
|
||||
],
|
||||
[
|
||||
require('@babel/plugin-proposal-nullish-coalescing-operator'),
|
||||
{ loose: false }
|
||||
],
|
||||
require('@babel/plugin-proposal-do-expressions'),
|
||||
|
||||
// Stage 2
|
||||
[require('@babel/plugin-proposal-decorators'), { legacy: true }],
|
||||
require('@babel/plugin-proposal-function-sent'),
|
||||
require('@babel/plugin-proposal-export-namespace-from'),
|
||||
require('@babel/plugin-proposal-numeric-separator'),
|
||||
require('@babel/plugin-proposal-throw-expressions'),
|
||||
|
||||
// Stage 3
|
||||
require('@babel/plugin-syntax-dynamic-import'),
|
||||
require('@babel/plugin-syntax-import-meta'),
|
||||
[require('@babel/plugin-proposal-class-properties'), { loose: true }],
|
||||
require('@babel/plugin-proposal-json-strings'),
|
||||
|
||||
...(development ? developmentPlugins : productionPlugins)
|
||||
]
|
||||
};
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths");
|
||||
|
||||
// Make sure that including paths.js after env.js will read .env variables.
|
||||
delete require.cache[require.resolve("./paths")];
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
throw new Error("The NODE_ENV environment variable is required but was not specified.");
|
||||
}
|
||||
|
||||
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||
const dotenvFiles = [
|
||||
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||
// Don't include `.env.local` for `test` environment
|
||||
// since normally you expect tests to produce the same
|
||||
// results for everyone
|
||||
NODE_ENV !== "test" && `${paths.dotenv}.local`,
|
||||
`${paths.dotenv}.${NODE_ENV}`,
|
||||
paths.dotenv,
|
||||
].filter(Boolean);
|
||||
|
||||
// Load environment variables from .env* files. Suppress warnings using silent
|
||||
// if this file is missing. dotenv will never modify any environment variables
|
||||
// that have already been set. Variable expansion is supported in .env files.
|
||||
// https://github.com/motdotla/dotenv
|
||||
// https://github.com/motdotla/dotenv-expand
|
||||
dotenvFiles.forEach((dotenvFile) => {
|
||||
if (fs.existsSync(dotenvFile)) {
|
||||
require("dotenv-expand")(
|
||||
require("dotenv").config({
|
||||
path: dotenvFile,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We support resolving modules according to `NODE_PATH`.
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
// https://github.com/facebook/create-react-app/issues/253.
|
||||
// It works similar to `NODE_PATH` in Node itself:
|
||||
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
|
||||
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
|
||||
// We also resolve them to make sure all tools using them work consistently.
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
process.env.NODE_PATH = (process.env.NODE_PATH || "")
|
||||
.split(path.delimiter)
|
||||
.filter((folder) => folder && !path.isAbsolute(folder))
|
||||
.map((folder) => path.resolve(appDirectory, folder))
|
||||
.join(path.delimiter);
|
||||
|
||||
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
|
||||
// injected into the application via DefinePlugin in webpack configuration.
|
||||
const REACT_APP = /^REACT_APP_/i;
|
||||
|
||||
function getClientEnvironment(publicUrl) {
|
||||
const raw = Object.keys(process.env)
|
||||
.filter((key) => REACT_APP.test(key))
|
||||
.reduce(
|
||||
(env, key) => {
|
||||
env[key] = process.env[key];
|
||||
return env;
|
||||
},
|
||||
{
|
||||
// Useful for determining whether we’re running in production mode.
|
||||
// Most importantly, it switches React into the correct mode.
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
// Useful for resolving the correct path to static assets in `public`.
|
||||
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
|
||||
// This should only be used as an escape hatch. Normally you would put
|
||||
// images into the `src` and `import` them in code to get their paths.
|
||||
PUBLIC_URL: publicUrl,
|
||||
// We support configuring the sockjs pathname during development.
|
||||
// These settings let a developer run multiple simultaneous projects.
|
||||
// They are used as the connection `hostname`, `pathname` and `port`
|
||||
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
|
||||
// and `sockPort` options in webpack-dev-server.
|
||||
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
|
||||
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
|
||||
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
|
||||
// Whether or not react-refresh is enabled.
|
||||
// react-refresh is not 100% stable at this time,
|
||||
// which is why it's disabled by default.
|
||||
// It is defined here so it is available in the webpackHotDevClient.
|
||||
FAST_REFRESH: process.env.FAST_REFRESH !== "false",
|
||||
}
|
||||
);
|
||||
// Stringify all values so we can feed into webpack DefinePlugin
|
||||
const stringified = {
|
||||
"process.env": Object.keys(raw).reduce((env, key) => {
|
||||
env[key] = JSON.stringify(raw[key]);
|
||||
return env;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return { raw, stringified };
|
||||
}
|
||||
|
||||
module.exports = getClientEnvironment;
|
|
@ -0,0 +1,58 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const chalk = require("react-dev-utils/chalk");
|
||||
const paths = require("./paths");
|
||||
|
||||
// Ensure the certificate and key provided are valid and if not
|
||||
// throw an easy to debug error
|
||||
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
|
||||
let encrypted;
|
||||
try {
|
||||
// publicEncrypt will throw an error with an invalid cert
|
||||
encrypted = crypto.publicEncrypt(cert, Buffer.from("test"));
|
||||
} catch (err) {
|
||||
throw new Error(`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// privateDecrypt will throw an error with an invalid key
|
||||
crypto.privateDecrypt(key, encrypted);
|
||||
} catch (err) {
|
||||
throw new Error(`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Read file and throw an error if it doesn't exist
|
||||
function readEnvFile(file, type) {
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(
|
||||
`You specified ${chalk.cyan(type)} in your env, but the file "${chalk.yellow(file)}" can't be found.`
|
||||
);
|
||||
}
|
||||
return fs.readFileSync(file);
|
||||
}
|
||||
|
||||
// Get the https config
|
||||
// Return cert files if provided in env, otherwise just true or false
|
||||
function getHttpsConfig() {
|
||||
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
|
||||
const isHttps = HTTPS === "true";
|
||||
|
||||
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
|
||||
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
|
||||
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
|
||||
const config = {
|
||||
cert: readEnvFile(crtFile, "SSL_CRT_FILE"),
|
||||
key: readEnvFile(keyFile, "SSL_KEY_FILE"),
|
||||
};
|
||||
|
||||
validateKeyAndCerts({ ...config, keyFile, crtFile });
|
||||
return config;
|
||||
}
|
||||
return isHttps;
|
||||
}
|
||||
|
||||
module.exports = getHttpsConfig;
|
|
@ -0,0 +1,29 @@
|
|||
"use strict";
|
||||
|
||||
const babelJest = require("babel-jest");
|
||||
|
||||
const hasJsxRuntime = (() => {
|
||||
if (process.env.DISABLE_NEW_JSX_TRANSFORM === "true") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
require.resolve("react/jsx-runtime");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
module.exports = babelJest.createTransformer({
|
||||
presets: [
|
||||
[
|
||||
require.resolve("babel-preset-react-app"),
|
||||
{
|
||||
runtime: hasJsxRuntime ? "automatic" : "classic",
|
||||
},
|
||||
],
|
||||
],
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
// This is a custom Jest transformer turning style imports into empty objects.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process() {
|
||||
return "module.exports = {};";
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return "cssTransform";
|
||||
},
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const camelcase = require("camelcase");
|
||||
|
||||
// This is a custom Jest transformer turning file imports into filenames.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
const assetFilename = JSON.stringify(path.basename(filename));
|
||||
|
||||
if (filename.match(/\.svg$/)) {
|
||||
// Based on how SVGR generates a component name:
|
||||
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
|
||||
const pascalCaseFilename = camelcase(path.parse(filename).name, {
|
||||
pascalCase: true,
|
||||
});
|
||||
const componentName = `Svg${pascalCaseFilename}`;
|
||||
return `const React = require('react');
|
||||
module.exports = {
|
||||
__esModule: true,
|
||||
default: ${assetFilename},
|
||||
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
|
||||
return {
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type: 'svg',
|
||||
ref: ref,
|
||||
key: null,
|
||||
props: Object.assign({}, props, {
|
||||
children: ${assetFilename}
|
||||
})
|
||||
};
|
||||
}),
|
||||
};`;
|
||||
}
|
||||
|
||||
return `module.exports = ${assetFilename};`;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,134 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths");
|
||||
const chalk = require("react-dev-utils/chalk");
|
||||
const resolve = require("resolve");
|
||||
|
||||
/**
|
||||
* Get additional module paths based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function getAdditionalModulePaths(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
|
||||
// the default behavior.
|
||||
if (path.relative(paths.appNodeModules, baseUrlResolved) === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow the user set the `baseUrl` to `appSrc`.
|
||||
if (path.relative(paths.appSrc, baseUrlResolved) === "") {
|
||||
return [paths.appSrc];
|
||||
}
|
||||
|
||||
// If the path is equal to the root directory we ignore it here.
|
||||
// We don't want to allow importing from the root directly as source files are
|
||||
// not transpiled outside of `src`. We do allow importing them with the
|
||||
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
|
||||
// an alias.
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise, throw an error.
|
||||
throw new Error(
|
||||
chalk.red.bold(
|
||||
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
|
||||
" Create React App does not support other values at this time."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webpack aliases based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {*} options
|
||||
*/
|
||||
function getWebpackAliases(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === "") {
|
||||
return {
|
||||
src: paths.appSrc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get jest aliases based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {*} options
|
||||
*/
|
||||
function getJestAliases(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === "") {
|
||||
return {
|
||||
"^src/(.*)$": "<rootDir>/src/$1",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getModules() {
|
||||
// Check if TypeScript is setup
|
||||
const hasTsConfig = fs.existsSync(paths.appTsConfig);
|
||||
const hasJsConfig = fs.existsSync(paths.appJsConfig);
|
||||
|
||||
if (hasTsConfig && hasJsConfig) {
|
||||
throw new Error(
|
||||
"You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file."
|
||||
);
|
||||
}
|
||||
|
||||
let config;
|
||||
|
||||
// If there's a tsconfig.json we assume it's a
|
||||
// TypeScript project and set up the config
|
||||
// based on tsconfig.json
|
||||
if (hasTsConfig) {
|
||||
const ts = require(resolve.sync("typescript", {
|
||||
basedir: paths.appNodeModules,
|
||||
}));
|
||||
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
|
||||
// Otherwise we'll check if there is jsconfig.json
|
||||
// for non TS projects.
|
||||
} else if (hasJsConfig) {
|
||||
config = require(paths.appJsConfig);
|
||||
}
|
||||
|
||||
config = config || {};
|
||||
const options = config.compilerOptions || {};
|
||||
|
||||
const additionalModulePaths = getAdditionalModulePaths(options);
|
||||
|
||||
return {
|
||||
additionalModulePaths: additionalModulePaths,
|
||||
webpackAliases: getWebpackAliases(options),
|
||||
jestAliases: getJestAliases(options),
|
||||
hasTsConfig,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getModules();
|
|
@ -0,0 +1,71 @@
|
|||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const getPublicUrlOrPath = require("react-dev-utils/getPublicUrlOrPath");
|
||||
|
||||
// Make sure any symlinks in the project folder are resolved:
|
||||
// https://github.com/facebook/create-react-app/issues/637
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
|
||||
|
||||
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
|
||||
// "public path" at which the app is served.
|
||||
// webpack needs to know it to put the right <script> hrefs into HTML even in
|
||||
// single-page apps that may serve index.html for nested URLs like /todos/42.
|
||||
// We can't use a relative path in HTML because we don't want to load something
|
||||
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||
const publicUrlOrPath = getPublicUrlOrPath(
|
||||
process.env.NODE_ENV === "development",
|
||||
require(resolveApp("package.json")).homepage,
|
||||
process.env.PUBLIC_URL
|
||||
);
|
||||
|
||||
const buildPath = process.env.BUILD_PATH || "build";
|
||||
|
||||
const moduleFileExtensions = [
|
||||
"web.mjs",
|
||||
"mjs",
|
||||
"web.js",
|
||||
"js",
|
||||
"web.ts",
|
||||
"ts",
|
||||
"web.tsx",
|
||||
"tsx",
|
||||
"json",
|
||||
"web.jsx",
|
||||
"jsx",
|
||||
];
|
||||
|
||||
// Resolve file paths in the same order as webpack
|
||||
const resolveModule = (resolveFn, filePath) => {
|
||||
const extension = moduleFileExtensions.find((extension) => fs.existsSync(resolveFn(`${filePath}.${extension}`)));
|
||||
|
||||
if (extension) {
|
||||
return resolveFn(`${filePath}.${extension}`);
|
||||
}
|
||||
|
||||
return resolveFn(`${filePath}.js`);
|
||||
};
|
||||
|
||||
// config after eject: we're in ./config/
|
||||
module.exports = {
|
||||
dotenv: resolveApp(".env"),
|
||||
appPath: resolveApp("."),
|
||||
appBuild: resolveApp(buildPath),
|
||||
appPublic: resolveApp("public"),
|
||||
appHtml: resolveApp("public/index.html"),
|
||||
appIndexJs: resolveModule(resolveApp, "src/index"),
|
||||
appPackageJson: resolveApp("package.json"),
|
||||
appSrc: resolveApp("src"),
|
||||
appTsConfig: resolveApp("tsconfig.json"),
|
||||
appJsConfig: resolveApp("jsconfig.json"),
|
||||
yarnLockFile: resolveApp("yarn.lock"),
|
||||
testsSetup: resolveModule(resolveApp, "src/setupTests"),
|
||||
proxySetup: resolveApp("src/setupProxy.js"),
|
||||
appNodeModules: resolveApp("node_modules"),
|
||||
swSrc: resolveModule(resolveApp, "src/service-worker"),
|
||||
publicUrlOrPath,
|
||||
};
|
||||
|
||||
module.exports.moduleFileExtensions = moduleFileExtensions;
|
|
@ -0,0 +1,17 @@
|
|||
"use strict";
|
||||
|
||||
const { resolveModuleName } = require("ts-pnp");
|
||||
|
||||
exports.resolveModuleName = (typescript, moduleName, containingFile, compilerOptions, resolutionHost) => {
|
||||
return resolveModuleName(moduleName, containingFile, compilerOptions, resolutionHost, typescript.resolveModuleName);
|
||||
};
|
||||
|
||||
exports.resolveTypeReferenceDirective = (typescript, moduleName, containingFile, compilerOptions, resolutionHost) => {
|
||||
return resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost,
|
||||
typescript.resolveTypeReferenceDirective
|
||||
);
|
||||
};
|
|
@ -0,0 +1,719 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const resolve = require("resolve");
|
||||
const PnpWebpackPlugin = require("pnp-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
|
||||
const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
const safePostCssParser = require("postcss-safe-parser");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
|
||||
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
|
||||
const WatchMissingNodeModulesPlugin = require("react-dev-utils/WatchMissingNodeModulesPlugin");
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
|
||||
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
|
||||
const ESLintPlugin = require("eslint-webpack-plugin");
|
||||
const paths = require("./paths");
|
||||
const modules = require("./modules");
|
||||
const getClientEnvironment = require("./env");
|
||||
const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin");
|
||||
const ForkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpackPlugin");
|
||||
const typescriptFormatter = require("react-dev-utils/typescriptFormatter");
|
||||
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
||||
|
||||
const postcssNormalize = require("postcss-normalize");
|
||||
|
||||
const appPackageJson = require(paths.appPackageJson);
|
||||
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
|
||||
|
||||
const webpackDevClientEntry = require.resolve("react-dev-utils/webpackHotDevClient");
|
||||
const reactRefreshOverlayEntry = require.resolve("react-dev-utils/refreshOverlayInterop");
|
||||
|
||||
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
|
||||
// makes for a smoother build process.
|
||||
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== "false";
|
||||
|
||||
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === "true";
|
||||
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === "true";
|
||||
|
||||
const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || "10000");
|
||||
|
||||
// Check if TypeScript is setup
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
|
||||
// Get the path to the uncompiled service worker (if it exists).
|
||||
const swSrc = paths.swSrc;
|
||||
|
||||
// style files regexes
|
||||
const cssRegex = /\.css$/;
|
||||
const cssModuleRegex = /\.module\.css$/;
|
||||
const sassRegex = /\.(scss|sass)$/;
|
||||
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||
|
||||
const hasJsxRuntime = (() => {
|
||||
if (process.env.DISABLE_NEW_JSX_TRANSFORM === "true") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
require.resolve("react/jsx-runtime");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
// This is the production and development configuration.
|
||||
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
||||
module.exports = function (webpackEnv) {
|
||||
const isEnvDevelopment = webpackEnv === "development";
|
||||
const isEnvProduction = webpackEnv === "production";
|
||||
|
||||
// Variable used for enabling profiling in Production
|
||||
// passed into alias object. Uses a flag if passed into the build command
|
||||
const isEnvProductionProfile = isEnvProduction && process.argv.includes("--profile");
|
||||
|
||||
// We will provide `paths.publicUrlOrPath` to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||
|
||||
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
|
||||
|
||||
// common function to get style loaders
|
||||
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||
const loaders = [
|
||||
isEnvDevelopment && require.resolve("style-loader"),
|
||||
isEnvProduction && {
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
// css is located in `static/css`, use '../../' to locate index.html folder
|
||||
// in production `paths.publicUrlOrPath` can be a relative path
|
||||
options: paths.publicUrlOrPath.startsWith(".") ? { publicPath: "../../" } : {},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: cssOptions,
|
||||
},
|
||||
{
|
||||
// Options for PostCSS as we reference these options twice
|
||||
// Adds vendor prefixing based on your specified browser support in
|
||||
// package.json
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebook/create-react-app/issues/2677
|
||||
ident: "postcss",
|
||||
plugins: () => [
|
||||
require("postcss-flexbugs-fixes"),
|
||||
require("postcss-preset-env")({
|
||||
autoprefixer: {
|
||||
flexbox: "no-2009",
|
||||
},
|
||||
stage: 3,
|
||||
}),
|
||||
// Adds PostCSS Normalize as the reset css with default options,
|
||||
// so that it honors browserslist config in package.json
|
||||
// which in turn let's users customize the target behavior as per their needs.
|
||||
postcssNormalize(),
|
||||
],
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
},
|
||||
},
|
||||
].filter(Boolean);
|
||||
if (preProcessor) {
|
||||
loaders.push(
|
||||
{
|
||||
loader: require.resolve("resolve-url-loader"),
|
||||
options: {
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
root: paths.appSrc,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve(preProcessor),
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
return loaders;
|
||||
};
|
||||
|
||||
return {
|
||||
mode: isEnvProduction ? "production" : isEnvDevelopment && "development",
|
||||
// Stop compilation early in production
|
||||
bail: isEnvProduction,
|
||||
devtool: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
? "source-map"
|
||||
: false
|
||||
: isEnvDevelopment && "cheap-module-source-map",
|
||||
// These are the "entry points" to our application.
|
||||
// This means they will be the "root" imports that are included in JS bundle.
|
||||
entry:
|
||||
isEnvDevelopment && !shouldUseReactRefresh
|
||||
? [
|
||||
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||
// When you save a file, the client will either apply hot updates (in case
|
||||
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||
// make a syntax error, this client will display a syntax error overlay.
|
||||
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||
// to bring better experience for Create React App users. You can replace
|
||||
// the line below with these two lines if you prefer the stock client:
|
||||
//
|
||||
// require.resolve('webpack-dev-server/client') + '?/',
|
||||
// require.resolve('webpack/hot/dev-server'),
|
||||
//
|
||||
// When using the experimental react-refresh integration,
|
||||
// the webpack plugin takes care of injecting the dev client for us.
|
||||
webpackDevClientEntry,
|
||||
// Finally, this is your app's code:
|
||||
paths.appIndexJs,
|
||||
// We include the app code last so that if there is a runtime error during
|
||||
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||
// changing JS code would still trigger a refresh.
|
||||
]
|
||||
: paths.appIndexJs,
|
||||
output: {
|
||||
// The build folder.
|
||||
path: isEnvProduction ? paths.appBuild : undefined,
|
||||
// Add /* filename */ comments to generated require()s in the output.
|
||||
pathinfo: isEnvDevelopment,
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
// In development, it does not produce real files.
|
||||
filename: isEnvProduction ? "static/js/[name].[contenthash:8].js" : isEnvDevelopment && "static/js/bundle.js",
|
||||
// TODO: remove this when upgrading to webpack 5
|
||||
futureEmitAssets: true,
|
||||
// There are also additional JS chunk files if you use code splitting.
|
||||
chunkFilename: isEnvProduction
|
||||
? "static/js/[name].[contenthash:8].chunk.js"
|
||||
: isEnvDevelopment && "static/js/[name].chunk.js",
|
||||
// webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath: paths.publicUrlOrPath,
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: isEnvProduction
|
||||
? (info) => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, "/")
|
||||
: isEnvDevelopment && ((info) => path.resolve(info.absoluteResourcePath).replace(/\\/g, "/")),
|
||||
// Prevents conflicts when multiple webpack runtimes (from different apps)
|
||||
// are used on the same page.
|
||||
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
|
||||
// this defaults to 'window', but by setting it to 'this' then
|
||||
// module chunks which are built will work in web workers as well.
|
||||
globalObject: "this",
|
||||
},
|
||||
optimization: {
|
||||
minimize: isEnvProduction,
|
||||
minimizer: [
|
||||
// This is only used in production mode
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
parse: {
|
||||
// We want terser to parse ecma 8 code. However, we don't want it
|
||||
// to apply any minification steps that turns valid ecma 5 code
|
||||
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||
// sections only apply transformations that are ecma 5 safe
|
||||
// https://github.com/facebook/create-react-app/pull/4234
|
||||
ecma: 8,
|
||||
},
|
||||
compress: {
|
||||
ecma: 5,
|
||||
warnings: false,
|
||||
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/2376
|
||||
// Pending further investigation:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false,
|
||||
// Disabled because of an issue with Terser breaking valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/5250
|
||||
// Pending further investigation:
|
||||
// https://github.com/terser-js/terser/issues/120
|
||||
inline: 2,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
// Added for profiling in devtools
|
||||
keep_classnames: isEnvProductionProfile,
|
||||
keep_fnames: isEnvProductionProfile,
|
||||
output: {
|
||||
ecma: 5,
|
||||
comments: false,
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebook/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
},
|
||||
},
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
// This is only used in production mode
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorOptions: {
|
||||
parser: safePostCssParser,
|
||||
map: shouldUseSourceMap
|
||||
? {
|
||||
// `inline: false` forces the sourcemap to be output into a
|
||||
// separate file
|
||||
inline: false,
|
||||
// `annotation: true` appends the sourceMappingURL to the end of
|
||||
// the css file, helping the browser find the sourcemap
|
||||
annotation: true,
|
||||
}
|
||||
: false,
|
||||
},
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ["default", { minifyFontValues: { removeQuotes: false } }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// Automatically split vendor and commons
|
||||
// https://twitter.com/wSokra/status/969633336732905474
|
||||
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
||||
splitChunks: {
|
||||
chunks: "all",
|
||||
name: isEnvDevelopment,
|
||||
},
|
||||
// Keep the runtime chunk separated to enable long term caching
|
||||
// https://twitter.com/wSokra/status/969679223278505985
|
||||
// https://github.com/facebook/create-react-app/issues/5358
|
||||
runtimeChunk: {
|
||||
name: (entrypoint) => `runtime-${entrypoint.name}`,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
// This allows you to set a fallback for where webpack should look for modules.
|
||||
// We placed these paths second because we want `node_modules` to "win"
|
||||
// if there are any conflicts. This matches Node resolution mechanism.
|
||||
// https://github.com/facebook/create-react-app/issues/253
|
||||
modules: ["node_modules", paths.appNodeModules].concat(modules.additionalModulePaths || []),
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
// We also include JSX as a common component filename extension to support
|
||||
// some tools, although we do not recommend using it, see:
|
||||
// https://github.com/facebook/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: paths.moduleFileExtensions
|
||||
.map((ext) => `.${ext}`)
|
||||
.filter((ext) => useTypeScript || !ext.includes("ts")),
|
||||
alias: {
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
"react-native": "react-native-web",
|
||||
// Allows for better profiling with ReactDevTools
|
||||
...(isEnvProductionProfile && {
|
||||
"react-dom$": "react-dom/profiling",
|
||||
"scheduler/tracing": "scheduler/tracing-profiling",
|
||||
}),
|
||||
...(modules.webpackAliases || {}),
|
||||
},
|
||||
plugins: [
|
||||
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
|
||||
// guards against forgotten dependencies and such.
|
||||
PnpWebpackPlugin,
|
||||
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||
// This often causes confusion because we only process files within src/ with babel.
|
||||
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson, reactRefreshOverlayEntry]),
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
plugins: [
|
||||
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
|
||||
// from the current package.
|
||||
PnpWebpackPlugin.moduleLoader(module),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// Disable require.ensure as it's not a standard language feature.
|
||||
{ parser: { requireEnsure: false } },
|
||||
{
|
||||
// "oneOf" will traverse all following loaders until one will
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
// TODO: Merge this config once `image/avif` is in the mime-db
|
||||
// https://github.com/jshttp/mime-db
|
||||
{
|
||||
test: [/\.avif$/],
|
||||
loader: require.resolve("url-loader"),
|
||||
options: {
|
||||
limit: imageInlineSizeLimit,
|
||||
mimetype: "image/avif",
|
||||
name: "static/media/[name].[hash:8].[ext]",
|
||||
},
|
||||
},
|
||||
// "url" loader works like "file" loader except that it embeds assets
|
||||
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||
// A missing `test` is equivalent to a match.
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||
loader: require.resolve("url-loader"),
|
||||
options: {
|
||||
limit: imageInlineSizeLimit,
|
||||
name: "static/media/[name].[hash:8].[ext]",
|
||||
},
|
||||
},
|
||||
// Process application JS with Babel.
|
||||
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
|
||||
{
|
||||
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
customize: require.resolve("babel-preset-react-app/webpack-overrides"),
|
||||
presets: [
|
||||
[
|
||||
require.resolve("babel-preset-react-app"),
|
||||
{
|
||||
runtime: hasJsxRuntime ? "automatic" : "classic",
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
plugins: [
|
||||
[
|
||||
require.resolve("babel-plugin-named-asset-import"),
|
||||
{
|
||||
loaderMap: {
|
||||
svg: {
|
||||
ReactComponent: "@svgr/webpack?-svgo,+titleProp,+ref![path]",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
isEnvDevelopment && shouldUseReactRefresh && require.resolve("react-refresh/babel"),
|
||||
].filter(Boolean),
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
// See #6846 for context on why cacheCompression is disabled
|
||||
cacheCompression: false,
|
||||
compact: isEnvProduction,
|
||||
},
|
||||
},
|
||||
// Load native modules from Neon
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: [
|
||||
{
|
||||
loader: path.resolve("./internals/NativeLoader.js"),
|
||||
options: {
|
||||
name: "[name]-[hash].[ext]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Process any JS outside of the app with Babel.
|
||||
// Unlike the application JS, we only compile the standard ES features.
|
||||
{
|
||||
test: /\.(js|mjs)$/,
|
||||
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
compact: false,
|
||||
presets: [[require.resolve("babel-preset-react-app/dependencies"), { helpers: true }]],
|
||||
cacheDirectory: true,
|
||||
// See #6846 for context on why cacheCompression is disabled
|
||||
cacheCompression: false,
|
||||
|
||||
// Babel sourcemaps are needed for debugging into node_modules
|
||||
// code. Without the options below, debuggers like VSCode
|
||||
// show incorrect code and set breakpoints on the wrong lines.
|
||||
sourceMaps: shouldUseSourceMap,
|
||||
inputSourceMap: shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||
// In production, we use MiniCSSExtractPlugin to extract that CSS
|
||||
// to a file, but in development "style" loader enables hot editing
|
||||
// of CSS.
|
||||
// By default we support CSS Modules with the extension .module.css
|
||||
{
|
||||
test: cssRegex,
|
||||
exclude: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
}),
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
|
||||
// using the extension .module.css
|
||||
{
|
||||
test: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
modules: {
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
}),
|
||||
},
|
||||
// Opt-in support for SASS (using .scss or .sass extensions).
|
||||
// By default we support SASS Modules with the
|
||||
// extensions .module.scss or .module.sass
|
||||
{
|
||||
test: sassRegex,
|
||||
exclude: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 3,
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
},
|
||||
"sass-loader"
|
||||
),
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
// Adds support for CSS Modules, but using SASS
|
||||
// using the extension .module.scss or .module.sass
|
||||
{
|
||||
test: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 3,
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
modules: {
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
},
|
||||
"sass-loader"
|
||||
),
|
||||
},
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
// In production, they would get copied to the `build` folder.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
loader: require.resolve("file-loader"),
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// its runtime that would otherwise be processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||
options: {
|
||||
name: "static/media/[name].[hash:8].[ext]",
|
||||
},
|
||||
},
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
},
|
||||
isEnvProduction
|
||||
? {
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
),
|
||||
// Inlines the webpack runtime script. This script is too small to warrant
|
||||
// a network request.
|
||||
// https://github.com/facebook/create-react-app/issues/5358
|
||||
isEnvProduction && shouldInlineRuntimeChunk && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// It will be an empty string unless you specify "homepage"
|
||||
// in `package.json`, in which case it will be the pathname of that URL.
|
||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||
// This gives some necessary context to module not found errors, such as
|
||||
// the requesting resource.
|
||||
new ModuleNotFoundPlugin(paths.appPath),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||
// It is absolutely essential that NODE_ENV is set to production
|
||||
// during a production build.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
// This is necessary to emit hot updates (CSS and Fast Refresh):
|
||||
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||
// Experimental hot reloading for React .
|
||||
// https://github.com/facebook/react/tree/master/packages/react-refresh
|
||||
isEnvDevelopment &&
|
||||
shouldUseReactRefresh &&
|
||||
new ReactRefreshWebpackPlugin({
|
||||
overlay: {
|
||||
entry: webpackDevClientEntry,
|
||||
// The expected exports are slightly different from what the overlay exports,
|
||||
// so an interop is included here to enable feedback on module-level errors.
|
||||
module: reactRefreshOverlayEntry,
|
||||
// Since we ship a custom dev client and overlay integration,
|
||||
// the bundled socket handling logic can be eliminated.
|
||||
sockIntegration: false,
|
||||
},
|
||||
}),
|
||||
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||
// a plugin that prints an error when you attempt to do this.
|
||||
// See https://github.com/facebook/create-react-app/issues/240
|
||||
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||
// If you require a missing module and then `npm install` it, you still have
|
||||
// to restart the development server for webpack to discover it. This plugin
|
||||
// makes the discovery automatic so you don't have to restart.
|
||||
// See https://github.com/facebook/create-react-app/issues/186
|
||||
isEnvDevelopment && new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||
isEnvProduction &&
|
||||
new MiniCssExtractPlugin({
|
||||
// Options similar to the same options in webpackOptions.output
|
||||
// both options are optional
|
||||
filename: "static/css/[name].[contenthash:8].css",
|
||||
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
|
||||
}),
|
||||
// Generate an asset manifest file with the following content:
|
||||
// - "files" key: Mapping of all asset filenames to their corresponding
|
||||
// output file so that tools can pick it up without having to parse
|
||||
// `index.html`
|
||||
// - "entrypoints" key: Array of files which are included in `index.html`,
|
||||
// can be used to reconstruct the HTML if necessary
|
||||
new ManifestPlugin({
|
||||
fileName: "asset-manifest.json",
|
||||
publicPath: paths.publicUrlOrPath,
|
||||
generate: (seed, files, entrypoints) => {
|
||||
const manifestFiles = files.reduce((manifest, file) => {
|
||||
manifest[file.name] = file.path;
|
||||
return manifest;
|
||||
}, seed);
|
||||
const entrypointFiles = entrypoints.main.filter((fileName) => !fileName.endsWith(".map"));
|
||||
|
||||
return {
|
||||
files: manifestFiles,
|
||||
entrypoints: entrypointFiles,
|
||||
};
|
||||
},
|
||||
}),
|
||||
// Moment.js is an extremely popular library that bundles large locale files
|
||||
// by default due to how webpack interprets its code. This is a practical
|
||||
// solution that requires the user to opt into importing specific locales.
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// You can remove this if you don't use Moment.js:
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
// Generate a service worker script that will precache, and keep up to date,
|
||||
// the HTML & assets that are part of the webpack build.
|
||||
isEnvProduction &&
|
||||
fs.existsSync(swSrc) &&
|
||||
new WorkboxWebpackPlugin.InjectManifest({
|
||||
swSrc,
|
||||
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
|
||||
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
|
||||
// Bump up the default maximum size (2mb) that's precached,
|
||||
// to make lazy-loading failure scenarios less likely.
|
||||
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
}),
|
||||
// TypeScript type checking
|
||||
useTypeScript &&
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: resolve.sync("typescript", {
|
||||
basedir: paths.appNodeModules,
|
||||
}),
|
||||
async: isEnvDevelopment,
|
||||
checkSyntacticErrors: true,
|
||||
resolveModuleNameModule: process.versions.pnp ? `${__dirname}/pnpTs.js` : undefined,
|
||||
resolveTypeReferenceDirectiveModule: process.versions.pnp ? `${__dirname}/pnpTs.js` : undefined,
|
||||
tsconfig: paths.appTsConfig,
|
||||
reportFiles: [
|
||||
// This one is specifically to match during CI tests,
|
||||
// as micromatch doesn't match
|
||||
// '../cra-template-typescript/template/src/App.tsx'
|
||||
// otherwise.
|
||||
"../**/src/**/*.{ts,tsx}",
|
||||
"**/src/**/*.{ts,tsx}",
|
||||
"!**/src/**/__tests__/**",
|
||||
"!**/src/**/?(*.)(spec|test).*",
|
||||
"!**/src/setupProxy.*",
|
||||
"!**/src/setupTests.*",
|
||||
],
|
||||
silent: true,
|
||||
// The formatter is invoked directly in WebpackDevServerUtils during development
|
||||
formatter: isEnvProduction ? typescriptFormatter : undefined,
|
||||
}),
|
||||
!disableESLintPlugin &&
|
||||
new ESLintPlugin({
|
||||
// Plugin options
|
||||
extensions: ["js", "mjs", "jsx", "ts", "tsx"],
|
||||
formatter: require.resolve("react-dev-utils/eslintFormatter"),
|
||||
eslintPath: require.resolve("eslint"),
|
||||
failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
|
||||
context: paths.appSrc,
|
||||
cache: true,
|
||||
cacheLocation: path.resolve(paths.appNodeModules, ".cache/.eslintcache"),
|
||||
// ESLint class options
|
||||
cwd: paths.appPath,
|
||||
resolvePluginsRelativeTo: __dirname,
|
||||
baseConfig: {
|
||||
extends: [require.resolve("eslint-config-react-app/base")],
|
||||
rules: {
|
||||
...(!hasJsxRuntime && {
|
||||
"react/react-in-jsx-scope": "error",
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell webpack to provide empty mocks for them so importing them works.
|
||||
node: {
|
||||
module: "empty",
|
||||
dgram: "empty",
|
||||
dns: "mock",
|
||||
fs: "empty",
|
||||
http2: "empty",
|
||||
net: "empty",
|
||||
tls: "empty",
|
||||
child_process: "empty",
|
||||
},
|
||||
// Turn off performance processing because we utilize
|
||||
// our own hints via the FileSizeReporter
|
||||
performance: false,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,129 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const errorOverlayMiddleware = require("react-dev-utils/errorOverlayMiddleware");
|
||||
const evalSourceMapMiddleware = require("react-dev-utils/evalSourceMapMiddleware");
|
||||
const noopServiceWorkerMiddleware = require("react-dev-utils/noopServiceWorkerMiddleware");
|
||||
const ignoredFiles = require("react-dev-utils/ignoredFiles");
|
||||
const redirectServedPath = require("react-dev-utils/redirectServedPathMiddleware");
|
||||
const paths = require("./paths");
|
||||
const getHttpsConfig = require("./getHttpsConfig");
|
||||
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
const sockHost = process.env.WDS_SOCKET_HOST;
|
||||
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
|
||||
const sockPort = process.env.WDS_SOCKET_PORT;
|
||||
|
||||
module.exports = function (proxy, allowedHost) {
|
||||
return {
|
||||
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
|
||||
// websites from potentially accessing local content through DNS rebinding:
|
||||
// https://github.com/webpack/webpack-dev-server/issues/887
|
||||
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||
// However, it made several existing use cases such as development in cloud
|
||||
// environment or subdomains in development significantly more complicated:
|
||||
// https://github.com/facebook/create-react-app/issues/2271
|
||||
// https://github.com/facebook/create-react-app/issues/2233
|
||||
// While we're investigating better solutions, for now we will take a
|
||||
// compromise. Since our WDS configuration only serves files in the `public`
|
||||
// folder we won't consider accessing them a vulnerability. However, if you
|
||||
// use the `proxy` feature, it gets more dangerous because it can expose
|
||||
// remote code execution vulnerabilities in backends like Django and Rails.
|
||||
// So we will disable the host check normally, but enable it if you have
|
||||
// specified the `proxy` setting. Finally, we let you override it if you
|
||||
// really know what you're doing with a special environment variable.
|
||||
disableHostCheck: !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === "true",
|
||||
// Enable gzip compression of generated files.
|
||||
compress: true,
|
||||
// Silence WebpackDevServer's own logs since they're generally not useful.
|
||||
// It will still show compile warnings and errors with this setting.
|
||||
clientLogLevel: "none",
|
||||
// By default WebpackDevServer serves physical files from current directory
|
||||
// in addition to all the virtual build products that it serves from memory.
|
||||
// This is confusing because those files won’t automatically be available in
|
||||
// production build folder unless we copy them. However, copying the whole
|
||||
// project directory is dangerous because we may expose sensitive files.
|
||||
// Instead, we establish a convention that only files in `public` directory
|
||||
// get served. Our build script will copy `public` into the `build` folder.
|
||||
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
|
||||
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
|
||||
// Note that we only recommend to use `public` folder as an escape hatch
|
||||
// for files like `favicon.ico`, `manifest.json`, and libraries that are
|
||||
// for some reason broken when imported through webpack. If you just want to
|
||||
// use an image, put it in `src` and `import` it from JavaScript instead.
|
||||
contentBase: paths.appPublic,
|
||||
contentBasePublicPath: paths.publicUrlOrPath,
|
||||
// By default files from `contentBase` will not trigger a page reload.
|
||||
watchContentBase: true,
|
||||
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
|
||||
// for the WebpackDevServer client so it can learn when the files were
|
||||
// updated. The WebpackDevServer client is included as an entry point
|
||||
// in the webpack development configuration. Note that only changes
|
||||
// to CSS are currently hot reloaded. JS changes will refresh the browser.
|
||||
hot: true,
|
||||
// Use 'ws' instead of 'sockjs-node' on server since we're using native
|
||||
// websockets in `webpackHotDevClient`.
|
||||
transportMode: "ws",
|
||||
// Prevent a WS client from getting injected as we're already including
|
||||
// `webpackHotDevClient`.
|
||||
injectClient: false,
|
||||
// Enable custom sockjs pathname for websocket connection to hot reloading server.
|
||||
// Enable custom sockjs hostname, pathname and port for websocket connection
|
||||
// to hot reloading server.
|
||||
sockHost,
|
||||
sockPath,
|
||||
sockPort,
|
||||
// It is important to tell WebpackDevServer to use the same "publicPath" path as
|
||||
// we specified in the webpack config. When homepage is '.', default to serving
|
||||
// from the root.
|
||||
// remove last slash so user can land on `/test` instead of `/test/`
|
||||
publicPath: paths.publicUrlOrPath.slice(0, -1),
|
||||
// WebpackDevServer is noisy by default so we emit custom message instead
|
||||
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
|
||||
quiet: true,
|
||||
// Reportedly, this avoids CPU overload on some systems.
|
||||
// https://github.com/facebook/create-react-app/issues/293
|
||||
// src/node_modules is not ignored to support absolute imports
|
||||
// https://github.com/facebook/create-react-app/issues/1065
|
||||
watchOptions: {
|
||||
ignored: ignoredFiles(paths.appSrc),
|
||||
},
|
||||
https: getHttpsConfig(),
|
||||
host,
|
||||
overlay: false,
|
||||
historyApiFallback: {
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebook/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
index: paths.publicUrlOrPath,
|
||||
},
|
||||
public: allowedHost,
|
||||
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
|
||||
proxy,
|
||||
before(app, server) {
|
||||
// Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
|
||||
// middlewares before `redirectServedPath` otherwise will not have any effect
|
||||
// This lets us fetch source contents from webpack for the error overlay
|
||||
app.use(evalSourceMapMiddleware(server));
|
||||
// This lets us open files from the runtime error overlay.
|
||||
app.use(errorOverlayMiddleware());
|
||||
|
||||
if (fs.existsSync(paths.proxySetup)) {
|
||||
// This registers user provided middleware for proxy reasons
|
||||
require(paths.proxySetup)(app);
|
||||
}
|
||||
},
|
||||
after(app) {
|
||||
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
|
||||
app.use(redirectServedPath(paths.publicUrlOrPath));
|
||||
|
||||
// This service worker file is effectively a 'no-op' that will reset any
|
||||
// previous service worker registered for the same host:port combination.
|
||||
// We do this in development to avoid hitting the production cache if
|
||||
// it used the same host and port.
|
||||
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* Base webpack config used across other specific configs
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import { dependencies as externals } from '../app/package.json';
|
||||
|
||||
export default {
|
||||
externals: [...Object.keys(externals || {}), 'bufferutil', 'utf-8-validate'],
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'app'),
|
||||
// https://github.com/webpack/webpack/issues/1114
|
||||
libraryTarget: 'commonjs2'
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine the array of extensions that should be used to resolve modules.
|
||||
*/
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.json'],
|
||||
modules: [path.join(__dirname, '..', 'app'), 'node_modules'],
|
||||
alias: {
|
||||
ws: path.resolve(path.join(__dirname, '..', 'node_modules/ws/index.js'))
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production'
|
||||
}),
|
||||
|
||||
new webpack.NamedModulesPlugin()
|
||||
]
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
/* eslint import/no-unresolved: off, import/no-self-import: off */
|
||||
require('@babel/register');
|
||||
|
||||
module.exports = require('./webpack.config.renderer.dev.babel').default;
|
|
@ -1,72 +0,0 @@
|
|||
/**
|
||||
* Webpack config for production electron main process
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import merge from 'webpack-merge';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
CheckNodeEnv('production');
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
devtool: 'source-map',
|
||||
|
||||
mode: 'production',
|
||||
|
||||
target: 'electron-main',
|
||||
|
||||
entry: './app/main.dev',
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '..'),
|
||||
filename: './app/main.prod.js'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimizer: process.env.E2E_BUILD
|
||||
? []
|
||||
: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
cache: true
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
|
||||
openAnalyzer: process.env.OPEN_ANALYZER === 'true'
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production',
|
||||
DEBUG_PROD: false,
|
||||
START_MINIMIZED: false
|
||||
})
|
||||
],
|
||||
|
||||
/**
|
||||
* Disables webpack processing of __dirname and __filename.
|
||||
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||
* https://github.com/webpack/webpack/issues/2010
|
||||
*/
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
}
|
||||
});
|
|
@ -1,291 +0,0 @@
|
|||
/* eslint global-require: off, import/no-dynamic-require: off */
|
||||
|
||||
/**
|
||||
* Build config for development electron renderer process that uses
|
||||
* Hot-Module-Replacement
|
||||
*
|
||||
* https://webpack.js.org/concepts/hot-module-replacement/
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import webpack from 'webpack';
|
||||
import chalk from 'chalk';
|
||||
import merge from 'webpack-merge';
|
||||
import { spawn, execSync } from 'child_process';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
|
||||
// at the dev webpack config is not accidentally run in a production environment
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
CheckNodeEnv('development');
|
||||
}
|
||||
|
||||
const port = process.env.PORT || 1212;
|
||||
const publicPath = `http://localhost:${port}/dist`;
|
||||
const dll = path.join(__dirname, '..', 'dll');
|
||||
const manifest = path.resolve(dll, 'renderer.json');
|
||||
const requiredByDLLConfig = module.parent.filename.includes('webpack.config.renderer.dev.dll');
|
||||
|
||||
/**
|
||||
* Warn if the DLL is not built
|
||||
*/
|
||||
if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) {
|
||||
console.log(
|
||||
chalk.black.bgYellow.bold('The DLL files are missing. Sit back while we build them for you with "yarn build-dll"')
|
||||
);
|
||||
execSync('yarn build-dll');
|
||||
}
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
devtool: 'inline-source-map',
|
||||
|
||||
mode: 'development',
|
||||
|
||||
target: 'electron-renderer',
|
||||
|
||||
entry: [
|
||||
...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']),
|
||||
`webpack-dev-server/client?http://localhost:${port}/`,
|
||||
'webpack/hot/only-dev-server',
|
||||
require.resolve('../app/index')
|
||||
],
|
||||
|
||||
output: {
|
||||
publicPath: `http://localhost:${port}/dist/`,
|
||||
filename: 'renderer.dev.js'
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.global\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /^((?!\.global).)*\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
sourceMap: true,
|
||||
importLoaders: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// SASS support - compile all .global.scss files and pipe it to style.css
|
||||
{
|
||||
test: /\.global\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
// SASS support - compile all other .scss files and pipe it to style.css
|
||||
{
|
||||
test: /^((?!\.global).)*\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
sourceMap: true,
|
||||
importLoaders: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
// WOFF Font
|
||||
{
|
||||
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// WOFF2 Font
|
||||
{
|
||||
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// TTF Font
|
||||
{
|
||||
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
// EOT Font
|
||||
{
|
||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: 'file-loader'
|
||||
},
|
||||
// SVG Font
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
// Common Image Formats
|
||||
{
|
||||
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
|
||||
use: 'url-loader'
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: [
|
||||
{
|
||||
loader: path.resolve('./internals/scripts/NativeLoader.js'),
|
||||
options: {
|
||||
name: '[name]-[hash].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'react-dom': '@hot-loader/react-dom',
|
||||
ws: path.resolve(path.join(__dirname, '..', 'node_modules/ws/index.js'))
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
requiredByDLLConfig
|
||||
? null
|
||||
: new webpack.DllReferencePlugin({
|
||||
context: path.join(__dirname, '..', 'dll'),
|
||||
manifest: require(manifest),
|
||||
sourceType: 'var'
|
||||
}),
|
||||
|
||||
new webpack.HotModuleReplacementPlugin({
|
||||
multiStep: true
|
||||
}),
|
||||
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*
|
||||
* By default, use 'development' as NODE_ENV. This can be overridden with
|
||||
* 'staging', for example, by changing the ENV variables in the npm scripts
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'development'
|
||||
}),
|
||||
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
debug: true
|
||||
})
|
||||
],
|
||||
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port,
|
||||
publicPath,
|
||||
compress: true,
|
||||
noInfo: true,
|
||||
stats: 'errors-only',
|
||||
inline: true,
|
||||
lazy: false,
|
||||
hot: true,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
contentBase: path.join(__dirname, 'dist'),
|
||||
watchOptions: {
|
||||
aggregateTimeout: 300,
|
||||
ignored: /node_modules/,
|
||||
poll: 100
|
||||
},
|
||||
historyApiFallback: {
|
||||
verbose: true,
|
||||
disableDotRule: false
|
||||
},
|
||||
before() {
|
||||
if (process.env.START_HOT) {
|
||||
console.log('Starting Main Process...');
|
||||
spawn('npm', ['run', 'start-main-dev'], {
|
||||
shell: true,
|
||||
env: process.env,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
.on('close', code => process.exit(code))
|
||||
.on('error', spawnError => console.error(spawnError));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
/* eslint global-require: off, import/no-dynamic-require: off */
|
||||
|
||||
/**
|
||||
* Builds the DLL for development electron renderer process
|
||||
*/
|
||||
|
||||
import webpack from 'webpack';
|
||||
import path from 'path';
|
||||
import merge from 'webpack-merge';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import { dependencies } from '../package.json';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
CheckNodeEnv('development');
|
||||
|
||||
const dist = path.join(__dirname, '..', 'dll');
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
context: path.join(__dirname, '..'),
|
||||
|
||||
devtool: 'eval',
|
||||
|
||||
mode: 'development',
|
||||
|
||||
target: 'electron-renderer',
|
||||
|
||||
externals: ['fsevents', 'crypto-browserify'],
|
||||
|
||||
/**
|
||||
* Use `module` from `webpack.config.renderer.dev.js`
|
||||
*/
|
||||
module: require('./webpack.config.renderer.dev.babel').default.module,
|
||||
|
||||
entry: {
|
||||
renderer: Object.keys(dependencies || {})
|
||||
},
|
||||
|
||||
output: {
|
||||
library: 'renderer',
|
||||
path: dist,
|
||||
filename: '[name].dev.dll.js',
|
||||
libraryTarget: 'var'
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DllPlugin({
|
||||
path: path.join(dist, '[name].json'),
|
||||
name: '[name]'
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'development'
|
||||
}),
|
||||
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
debug: true,
|
||||
options: {
|
||||
context: path.join(__dirname, '..', 'app'),
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'dll')
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
|
@ -1,228 +0,0 @@
|
|||
/**
|
||||
* Build config for electron renderer process
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
import merge from 'webpack-merge';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
CheckNodeEnv('production');
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
devtool: 'source-map',
|
||||
|
||||
mode: 'production',
|
||||
|
||||
target: 'electron-renderer',
|
||||
|
||||
entry: path.join(__dirname, '..', 'app/index'),
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'app/dist'),
|
||||
publicPath: './dist/',
|
||||
filename: 'renderer.prod.js'
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
// Extract all .global.css to style.css as is
|
||||
{
|
||||
test: /\.global\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
publicPath: './'
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Pipe other styles through css modules and append to style.css
|
||||
{
|
||||
test: /^((?!\.global).)*\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Add SASS support - compile all .global.scss files and pipe it to style.css
|
||||
{
|
||||
test: /\.global\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
importLoaders: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Add SASS support - compile all other .scss files and pipe it to style.css
|
||||
{
|
||||
test: /^((?!\.global).)*\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
importLoaders: 1,
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// WOFF Font
|
||||
{
|
||||
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// WOFF2 Font
|
||||
{
|
||||
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// TTF Font
|
||||
{
|
||||
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
// EOT Font
|
||||
{
|
||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: 'file-loader'
|
||||
},
|
||||
// SVG Font
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
// Common Image Formats
|
||||
{
|
||||
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
|
||||
use: 'url-loader'
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: [
|
||||
{
|
||||
loader: path.resolve('./internals/scripts/NativeLoader.js'),
|
||||
options: {
|
||||
name: '[name]-[hash].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimizer: process.env.E2E_BUILD
|
||||
? []
|
||||
: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
cache: true
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorOptions: {
|
||||
map: {
|
||||
inline: false,
|
||||
annotation: true
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production'
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'style.css'
|
||||
}),
|
||||
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
|
||||
openAnalyzer: process.env.OPEN_ANALYZER === 'true'
|
||||
})
|
||||
]
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
declare module 'module' {
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -1,35 +1,36 @@
|
|||
/* eslint-disable func-names */
|
||||
/* eslint-disable prefer-template */
|
||||
const loaderUtils = require('loader-utils');
|
||||
const path = require('path');
|
||||
const loaderUtils = require("loader-utils");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = function(content) {
|
||||
module.exports = function (content) {
|
||||
const loaderOptions = loaderUtils.getOptions(this) || {};
|
||||
|
||||
const from = loaderOptions.from || '.';
|
||||
const name = loaderUtils.interpolateName(this, loaderOptions.name || '[name].[ext]', {
|
||||
const from = loaderOptions.from || ".";
|
||||
const name = loaderUtils.interpolateName(this, loaderOptions.name || "[name].[ext]", {
|
||||
content,
|
||||
context: loaderOptions.context || this.rootContext || (this.config && this.config.context)
|
||||
context: loaderOptions.context || this.rootContext || (this.config && this.config.context),
|
||||
});
|
||||
|
||||
let requirePath;
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
requirePath = path.posix.relative(from, name);
|
||||
if (requirePath[0] !== '.') {
|
||||
requirePath = './dist/' + requirePath;
|
||||
if (requirePath[0] !== ".") {
|
||||
requirePath = "./" + requirePath;
|
||||
}
|
||||
} else {
|
||||
requirePath = '../native/index.node';
|
||||
requirePath = "./src/native.node";
|
||||
}
|
||||
|
||||
console.log(`Require path: ${requirePath}`);
|
||||
|
||||
if (typeof this.emitFile === 'function') {
|
||||
if (typeof this.emitFile === "function") {
|
||||
this.emitFile(name, content, false);
|
||||
this.addDependency(this.resourcePath);
|
||||
} else {
|
||||
throw new Error('emitFile function is not available');
|
||||
throw new Error("emitFile function is not available");
|
||||
}
|
||||
|
||||
return `module.exports = __non_webpack_require__(${loaderUtils.stringifyRequest(this, requirePath)});`;
|
||||
};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
// @flow
|
||||
|
||||
declare export default { [key: string]: string }
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
declare export default string
|
|
@ -1,5 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
require('@babel/register')({
|
||||
cwd: path.join(__dirname, '..', '..')
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
// @flow
|
||||
// Check if the renderer and main bundles are built
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
|
||||
function CheckBuildsExist() {
|
||||
const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js');
|
||||
const rendererPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'app',
|
||||
'dist',
|
||||
'renderer.prod.js'
|
||||
);
|
||||
|
||||
if (!fs.existsSync(mainPath)) {
|
||||
throw new Error(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
'The main process is not built yet. Build it by running "yarn build-main"'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(rendererPath)) {
|
||||
throw new Error(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
'The renderer process is not built yet. Build it by running "yarn build-renderer"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CheckBuildsExist();
|
|
@ -1,51 +0,0 @@
|
|||
// @flow
|
||||
import fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'child_process';
|
||||
import { dependencies } from '../../package.json';
|
||||
|
||||
(() => {
|
||||
if (!dependencies) return;
|
||||
const dependenciesKeys = Object.keys(dependencies);
|
||||
const nativeDeps = fs
|
||||
.readdirSync('node_modules')
|
||||
.filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`));
|
||||
try {
|
||||
// Find the reason for why the dependency is installed. If it is installed
|
||||
// because of a devDependency then that is okay. Warn when it is installed
|
||||
// because of a dependency
|
||||
const { dependencies: dependenciesObject } = JSON.parse(
|
||||
execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
|
||||
);
|
||||
const rootDependencies = Object.keys(dependenciesObject);
|
||||
const filteredRootDependencies = rootDependencies.filter(rootDependency =>
|
||||
dependenciesKeys.includes(rootDependency)
|
||||
);
|
||||
if (filteredRootDependencies.length > 0) {
|
||||
const plural = filteredRootDependencies.length > 1;
|
||||
console.log(`
|
||||
${chalk.whiteBright.bgYellow.bold(
|
||||
'Webpack does not work with native dependencies.'
|
||||
)}
|
||||
${chalk.bold(filteredRootDependencies.join(', '))} ${
|
||||
plural ? 'are native dependencies' : 'is a native dependency'
|
||||
} and should be installed inside of the "./app" folder.
|
||||
First uninstall the packages from "./package.json":
|
||||
${chalk.whiteBright.bgGreen.bold('yarn remove your-package')}
|
||||
${chalk.bold(
|
||||
'Then, instead of installing the package to the root "./package.json":'
|
||||
)}
|
||||
${chalk.whiteBright.bgRed.bold('yarn add your-package')}
|
||||
${chalk.bold('Install the package to "./app/package.json"')}
|
||||
${chalk.whiteBright.bgGreen.bold('cd ./app && yarn add your-package')}
|
||||
Read more about native dependencies at:
|
||||
${chalk.bold(
|
||||
'https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/Module-Structure----Two-package.json-Structure'
|
||||
)}
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Native dependencies could not be checked');
|
||||
}
|
||||
})();
|
|
@ -1,17 +0,0 @@
|
|||
// @flow
|
||||
import chalk from 'chalk';
|
||||
|
||||
export default function CheckNodeEnv(expectedEnv: string) {
|
||||
if (!expectedEnv) {
|
||||
throw new Error('"expectedEnv" not set');
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== expectedEnv) {
|
||||
console.log(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
|
||||
)
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// @flow
|
||||
import chalk from 'chalk';
|
||||
import detectPort from 'detect-port';
|
||||
|
||||
(function CheckPortInUse() {
|
||||
const port: string = process.env.PORT || '1212';
|
||||
|
||||
detectPort(port, (err: ?Error, availablePort: number) => {
|
||||
if (port !== String(availablePort)) {
|
||||
throw new Error(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
`Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -1,7 +0,0 @@
|
|||
// @flow
|
||||
|
||||
if (!/yarn\.js$/.test(process.env.npm_execpath || '')) {
|
||||
console.warn(
|
||||
"\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m"
|
||||
);
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
// @flow
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import { dependencies } from '../../app/package.json';
|
||||
|
||||
(() => {
|
||||
const nodeModulesPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'app',
|
||||
'node_modules'
|
||||
);
|
||||
|
||||
if (
|
||||
Object.keys(dependencies || {}).length > 0 &&
|
||||
fs.existsSync(nodeModulesPath)
|
||||
) {
|
||||
const electronRebuildCmd =
|
||||
'../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .';
|
||||
const cmd =
|
||||
process.platform === 'win32'
|
||||
? electronRebuildCmd.replace(/\//g, '\\')
|
||||
: electronRebuildCmd;
|
||||
execSync(cmd, {
|
||||
cwd: path.join(__dirname, '..', '..', 'app'),
|
||||
stdio: 'inherit'
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -1,3 +0,0 @@
|
|||
target/
|
||||
index.node
|
||||
artifacts.json
|
|
@ -1,22 +1,21 @@
|
|||
[package]
|
||||
name = "zecwalletnative"
|
||||
name = "zecwalletlite"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
authors = ["Zecwallet"]
|
||||
license = "MIT/Apache-2.0"
|
||||
build = "build.rs"
|
||||
exclude = ["index.node"]
|
||||
|
||||
[lib]
|
||||
name = "zecwalletnative"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[build-dependencies]
|
||||
neon-build = "0.3.3"
|
||||
|
||||
[dependencies.neon]
|
||||
version = "0.8"
|
||||
default-features = false
|
||||
features = ["napi-6"]
|
||||
|
||||
[dependencies]
|
||||
zecwalletlitelib = { git = "https://github.com/adityapk00/zecwallet-light-cli", rev = "4a279179f885e9867809ba4bdc037dcfbd9c31aa" }
|
||||
zecwalletlitelib = { git = "https://github.com/adityapk00/zecwallet-light-cli", rev = "87ad71c28e255a365f736e19390994040aff4b41" }
|
||||
#zecwalletlitelib = { path = "../../zecwallet-light-cli/lib" }
|
||||
neon = "0.3.3"
|
||||
lazy_static = "1.4.0"
|
||||
neon-serde = "0.2.0"
|
||||
serde = "1.0.80"
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
neon_build::setup();
|
||||
}
|
|
@ -1,149 +1,215 @@
|
|||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use neon::prelude::Context;
|
||||
use neon::prelude::FunctionContext;
|
||||
use neon::prelude::JsBoolean;
|
||||
use neon::prelude::JsNumber;
|
||||
use neon::prelude::JsResult;
|
||||
use neon::prelude::JsString;
|
||||
use neon::register_module;
|
||||
use neon_serde::export;
|
||||
use zecwalletlitelib::lightclient::lightclient_config::LightClientConfig;
|
||||
|
||||
use std::sync::{Mutex, Arc};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use zecwalletlitelib::{commands, lightclient::{LightClient, LightClientConfig}};
|
||||
use zecwalletlitelib::{commands, lightclient::LightClient};
|
||||
|
||||
// We'll use a MUTEX to store a global lightclient instance,
|
||||
// so we don't have to keep creating it. We need to store it here, in rust
|
||||
// because we can't return such a complex structure back to JS
|
||||
lazy_static! {
|
||||
static ref LIGHTCLIENT: Mutex<RefCell<Option<Arc<LightClient>>>> = Mutex::new(RefCell::new(None));
|
||||
static ref LIGHTCLIENT: Mutex<RefCell<Option<Arc<LightClient>>>> =
|
||||
Mutex::new(RefCell::new(None));
|
||||
}
|
||||
|
||||
register_module!(mut m, {
|
||||
m.export_function("litelib_say_hello", litelib_say_hello)?;
|
||||
m.export_function("litelib_wallet_exists", litelib_wallet_exists)?;
|
||||
m.export_function("litelib_initialize_new", litelib_initialize_new)?;
|
||||
m.export_function("litelib_initialize_existing", litelib_initialize_existing)?;
|
||||
m.export_function(
|
||||
"litelib_initialize_new_from_phrase",
|
||||
litelib_initialize_new_from_phrase,
|
||||
)?;
|
||||
m.export_function("litelib_deinitialize", litelib_deinitialize)?;
|
||||
m.export_function("litelib_execute", litelib_execute)?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
export! {
|
||||
// Check if there is an existing wallet
|
||||
fn litelib_wallet_exists(chain_name: String) -> bool {
|
||||
let config = LightClientConfig::create_unconnected(chain_name, None);
|
||||
fn litelib_say_hello(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
let to = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||
|
||||
config.wallet_exists()
|
||||
}
|
||||
let ret = format!("Hello {}", to);
|
||||
|
||||
/// Create a new wallet and return the seed for the newly created wallet.
|
||||
fn litelib_initialize_new(server_uri: String) -> String {
|
||||
let server = LightClientConfig::get_server_or_default(Some(server_uri));
|
||||
let (config, latest_block_height) = match LightClientConfig::create(server) {
|
||||
Ok((c, h)) => (c, h),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
Ok(cx.string(ret))
|
||||
}
|
||||
|
||||
let lightclient = match LightClient::new(&config, latest_block_height) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
// Check if there is an existing wallet
|
||||
fn litelib_wallet_exists(mut cx: FunctionContext) -> JsResult<JsBoolean> {
|
||||
let chain_name = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||
let config = LightClientConfig::create_unconnected(chain_name, None);
|
||||
|
||||
// Initialize logging
|
||||
let _ = lightclient.init_logging();
|
||||
Ok(cx.boolean(config.wallet_exists()))
|
||||
}
|
||||
|
||||
let seed = match lightclient.do_seed_phrase() {
|
||||
Ok(s) => s.dump(),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
/// Create a new wallet and return the seed for the newly created wallet.
|
||||
fn litelib_initialize_new(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
let server_uri = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||
|
||||
LIGHTCLIENT.lock().unwrap().replace(Some(Arc::new(lightclient)));
|
||||
let resp = || {
|
||||
let server = LightClientConfig::get_server_or_default(Some(server_uri));
|
||||
let (config, latest_block_height) = match LightClientConfig::create(server) {
|
||||
Ok((c, h)) => (c, h),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Return the wallet's seed
|
||||
return seed;
|
||||
}
|
||||
let lightclient = match LightClient::new(&config, latest_block_height) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
/// Restore a wallet from the seed phrase
|
||||
fn litelib_initialize_new_from_phrase(server_uri: String,
|
||||
seed: String, birthday: u64, overwrite: bool) -> String {
|
||||
let server = LightClientConfig::get_server_or_default(Some(server_uri));
|
||||
let (config, _latest_block_height) = match LightClientConfig::create(server) {
|
||||
Ok((c, h)) => (c, h),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
// Initialize logging
|
||||
let _ = lightclient.init_logging();
|
||||
|
||||
let lightclient = match LightClient::new_from_phrase(seed, &config, birthday, overwrite) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
let seed = match lightclient.do_seed_phrase_sync() {
|
||||
Ok(s) => s.dump(),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize logging
|
||||
let _ = lightclient.init_logging();
|
||||
LIGHTCLIENT
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(Some(Arc::new(lightclient)));
|
||||
|
||||
LIGHTCLIENT.lock().unwrap().replace(Some(Arc::new(lightclient)));
|
||||
// Return the wallet's seed
|
||||
seed
|
||||
};
|
||||
|
||||
format!("OK")
|
||||
}
|
||||
Ok(cx.string(resp()))
|
||||
}
|
||||
|
||||
// Initialize a new lightclient and store its value
|
||||
fn litelib_initialize_existing(server_uri: String) -> String {
|
||||
let server = LightClientConfig::get_server_or_default(Some(server_uri));
|
||||
let (config, _latest_block_height) = match LightClientConfig::create(server) {
|
||||
Ok((c, h)) => (c, h),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
/// Restore a wallet from the seed phrase
|
||||
fn litelib_initialize_new_from_phrase(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
let server_uri = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||
let seed = cx.argument::<JsString>(1)?.value(&mut cx);
|
||||
let birthday = cx.argument::<JsNumber>(2)?.value(&mut cx);
|
||||
let overwrite = cx.argument::<JsBoolean>(3)?.value(&mut cx);
|
||||
|
||||
let lightclient = match LightClient::read_from_disk(&config) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
let resp = || {
|
||||
let server = LightClientConfig::get_server_or_default(Some(server_uri));
|
||||
let (config, _latest_block_height) = match LightClientConfig::create(server) {
|
||||
Ok((c, h)) => (c, h),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize logging
|
||||
let _ = lightclient.init_logging();
|
||||
let lightclient =
|
||||
match LightClient::new_from_phrase(seed, &config, birthday as u64, overwrite) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
LIGHTCLIENT.lock().unwrap().replace(Some(Arc::new(lightclient)));
|
||||
// Initialize logging
|
||||
let _ = lightclient.init_logging();
|
||||
|
||||
format!("OK")
|
||||
}
|
||||
LIGHTCLIENT
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(Some(Arc::new(lightclient)));
|
||||
|
||||
fn litelib_deinitialize() -> String {
|
||||
format!("OK")
|
||||
};
|
||||
|
||||
Ok(cx.string(resp()))
|
||||
}
|
||||
|
||||
// Initialize a new lightclient and store its value
|
||||
fn litelib_initialize_existing(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
let server_uri = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||
|
||||
let resp = || {
|
||||
let server = LightClientConfig::get_server_or_default(Some(server_uri));
|
||||
let (config, _latest_block_height) = match LightClientConfig::create(server) {
|
||||
Ok((c, h)) => (c, h),
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
let lightclient = match LightClient::read_from_disk(&config) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
return format!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize logging
|
||||
let _ = lightclient.init_logging();
|
||||
|
||||
LIGHTCLIENT
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(Some(Arc::new(lightclient)));
|
||||
|
||||
format!("OK")
|
||||
};
|
||||
|
||||
Ok(cx.string(resp()))
|
||||
}
|
||||
|
||||
fn litelib_deinitialize(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
LIGHTCLIENT.lock().unwrap().replace(None);
|
||||
|
||||
format!("OK")
|
||||
}
|
||||
|
||||
fn litelib_execute(cmd: String, args_list: String) -> String {
|
||||
let resp: String;
|
||||
{
|
||||
let lightclient: Arc<LightClient>;
|
||||
{
|
||||
let lc = LIGHTCLIENT.lock().unwrap();
|
||||
|
||||
if lc.borrow().is_none() {
|
||||
return format!("Error: Light Client is not initialized");
|
||||
}
|
||||
|
||||
lightclient = lc.borrow().as_ref().unwrap().clone();
|
||||
};
|
||||
|
||||
if cmd == "sync" || cmd == "rescan" || cmd == "import" || cmd == "send" {
|
||||
thread::spawn(move || {
|
||||
let args = if args_list.is_empty() { vec![] } else { vec![&args_list[..]] };
|
||||
commands::do_user_command(&cmd, &args, lightclient.as_ref());
|
||||
});
|
||||
|
||||
return format!("OK");
|
||||
} else {
|
||||
let args = if args_list.is_empty() { vec![] } else { vec![&args_list[..]] };
|
||||
resp = commands::do_user_command(&cmd, &args, lightclient.as_ref()).clone();
|
||||
}
|
||||
};
|
||||
|
||||
return resp;
|
||||
}
|
||||
Ok(cx.string(format!("OK")))
|
||||
}
|
||||
|
||||
fn litelib_execute(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
let cmd = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||
let args_list = cx.argument::<JsString>(1)?.value(&mut cx);
|
||||
|
||||
let resp = || {
|
||||
let lightclient: Arc<LightClient>;
|
||||
{
|
||||
let lc = LIGHTCLIENT.lock().unwrap();
|
||||
|
||||
if lc.borrow().is_none() {
|
||||
return format!("Error: Light Client is not initialized");
|
||||
}
|
||||
|
||||
lightclient = lc.borrow().as_ref().unwrap().clone();
|
||||
};
|
||||
|
||||
if cmd == "sync" || cmd == "rescan" || cmd == "import" || cmd == "send" {
|
||||
thread::spawn(move || {
|
||||
let args = if args_list.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![&args_list[..]]
|
||||
};
|
||||
commands::do_user_command(&cmd, &args, lightclient.as_ref());
|
||||
});
|
||||
|
||||
format!("OK")
|
||||
} else {
|
||||
let args = if args_list.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![&args_list[..]]
|
||||
};
|
||||
commands::do_user_command(&cmd, &args, lightclient.as_ref()).clone()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(cx.string(resp()))
|
||||
}
|
||||
|
|
467
package.json
|
@ -1,90 +1,161 @@
|
|||
{
|
||||
"name": "zecwallet-lite",
|
||||
"name": "zecwalletlite",
|
||||
"productName": "Zecwallet Lite",
|
||||
"version": "1.6.3",
|
||||
"description": "Zecwallet Lite (Electron version)",
|
||||
"scripts": {
|
||||
"build": "yarn neon && concurrently \"yarn build-main\" \"yarn build-renderer\"",
|
||||
"build-dll": "cross-env NODE_ENV=development webpack --config ./configs/webpack.config.renderer.dev.dll.babel.js --colors",
|
||||
"build-e2e": "cross-env E2E_BUILD=true yarn build",
|
||||
"build-main": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.main.prod.babel.js --colors",
|
||||
"build-renderer": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.renderer.prod.babel.js --colors",
|
||||
"dev": "cross-env START_HOT=1 node -r @babel/register ./internals/scripts/CheckPortInUse.js && yarn neon && cross-env START_HOT=1 yarn start-renderer-dev",
|
||||
"electron-rebuild": "electron-rebuild --parallel --force --types prod,dev,optional --module-dir app",
|
||||
"flow": "flow",
|
||||
"flow-typed": "rimraf flow-typed/npm && flow-typed install --overwrite || true",
|
||||
"lint": "cross-env NODE_ENV=development eslint --cache --format=pretty .",
|
||||
"lint-fix": "yarn --silent lint --fix; exit 0",
|
||||
"lint-styles": "stylelint --ignore-path .eslintignore '**/*.*(css|scss)' --syntax scss",
|
||||
"lint-styles-fix": "yarn --silent lint-styles --fix; exit 0",
|
||||
"neon": "electron-build-env neon build --release",
|
||||
"package": "yarn build && electron-builder build --publish never",
|
||||
"package-all": "yarn build && electron-builder build -mwl",
|
||||
"package-ci": "yarn postinstall && yarn build && electron-builder --publish always",
|
||||
"package-mac": "yarn build && electron-builder build --mac --publish never",
|
||||
"package-linux": "yarn build && electron-builder build --linux --publish never",
|
||||
"package-win": "yarn build && electron-builder build --win --x64 --publish never",
|
||||
"postinstall": "node -r @babel/register internals/scripts/CheckNativeDep.js && yarn flow-typed && electron-builder install-app-deps && yarn build-dll && opencollective-postinstall",
|
||||
"postlint-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{js,jsx,json,html,css,less,scss,yml}'",
|
||||
"postlint-styles-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{css,scss}'",
|
||||
"preinstall": "node ./internals/scripts/CheckYarn.js",
|
||||
"prestart": "yarn build",
|
||||
"start": "cross-env NODE_ENV=production electron ./app/main.prod.js",
|
||||
"start-main-dev": "cross-env START_HOT=1 NODE_ENV=development electron -r @babel/register ./app/main.dev.js --remote-debugging-port=9223",
|
||||
"start-renderer-dev": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.renderer.dev.babel.js",
|
||||
"test": "jest"
|
||||
"version": "1.7.0",
|
||||
"private": true,
|
||||
"description": "Zecwallet Lite",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Aditya Kulkarni",
|
||||
"email": "aditya@zecwallet.co",
|
||||
"url": "https://github.com/adityapk00/zecwallet-lite"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx}": [
|
||||
"cross-env NODE_ENV=development eslint --cache --format=pretty",
|
||||
"git add"
|
||||
],
|
||||
"{*.json,.{babelrc,eslintrc,prettierrc,stylelintrc}}": [
|
||||
"prettier --ignore-path .eslintignore --parser json --write",
|
||||
"git add"
|
||||
],
|
||||
"*.{css,scss}": [
|
||||
"stylelint --ignore-path .eslintignore --syntax scss --fix",
|
||||
"prettier --ignore-path .eslintignore --single-quote --write",
|
||||
"git add"
|
||||
],
|
||||
"*.{html,md,yml}": [
|
||||
"prettier --ignore-path .eslintignore --single-quote --write",
|
||||
"git add"
|
||||
"dependencies": {
|
||||
"@babel/core": "7.12.3",
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
||||
"@svgr/webpack": "5.5.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.25",
|
||||
"@types/react": "^17.0.1",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.5.0",
|
||||
"@typescript-eslint/parser": "^4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-named-asset-import": "^0.3.7",
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"bfj": "^7.0.2",
|
||||
"camelcase": "^6.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "4.3.0",
|
||||
"dateformat": "^4.5.1",
|
||||
"dotenv": "8.2.0",
|
||||
"dotenv-expand": "5.1.0",
|
||||
"electron-build-env": "^0.2.0",
|
||||
"electron-is-dev": "^1.2.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-plugin-flowtype": "^5.2.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-testing-library": "^3.9.2",
|
||||
"eslint-webpack-plugin": "^2.5.2",
|
||||
"file-loader": "6.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"jest": "26.6.0",
|
||||
"jest-circus": "26.6.0",
|
||||
"jest-resolve": "26.6.0",
|
||||
"jest-watch-typeahead": "0.6.1",
|
||||
"js-base64": "^3.6.1",
|
||||
"mini-css-extract-plugin": "0.11.3",
|
||||
"neon-cli": "^0.8.3",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.4",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"postcss-flexbugs-fixes": "4.2.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-normalize": "8.0.1",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"postcss-safe-parser": "5.0.2",
|
||||
"progress-stream": "^2.0.0",
|
||||
"prompts": "2.4.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-accessible-accordion": "^3.3.4",
|
||||
"react-app-polyfill": "^2.0.0",
|
||||
"react-dev-utils": "^11.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-modal": "^3.14.3",
|
||||
"react-refresh": "^0.8.3",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-tabs": "^3.2.2",
|
||||
"react-textarea-autosize": "^8.3.3",
|
||||
"request": "^2.88.2",
|
||||
"resolve": "1.18.1",
|
||||
"resolve-url-loader": "^3.1.2",
|
||||
"sass-loader": "^10.0.5",
|
||||
"semver": "7.3.2",
|
||||
"style-loader": "1.3.0",
|
||||
"terser-webpack-plugin": "4.2.3",
|
||||
"ts-pnp": "1.2.0",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.1.3",
|
||||
"url-loader": "4.1.1",
|
||||
"web-vitals": "^1.0.1",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-dev-server": "3.11.1",
|
||||
"webpack-manifest-plugin": "2.2.0",
|
||||
"workbox-webpack-plugin": "5.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn neon && concurrently -k \"cross-env NODE_ENV=production BROWSER=none yarn react-start\" \"wait-on http://localhost:3000 && electronmon .\"",
|
||||
"build": "yarn neon && node scripts/build.js",
|
||||
"test": "node scripts/test.js",
|
||||
"dist:win": "yarn build && electron-builder -w -c.extraMetadata.main=build/electron.js",
|
||||
"dist:mac": "yarn build && electron-builder -m -c.extraMetadata.main=build/electron.js",
|
||||
"dist:linux": "yarn build && electron-builder -l -c.extraMetadata.main=build/electron.js",
|
||||
"react-start": "node scripts/start.js",
|
||||
"neon": "cargo-cp-artifact -nc src/native.node -- cargo build --release --manifest-path native/Cargo.toml --message-format=json-render-diagnostics"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/js-base64": "^3.3.1",
|
||||
"@types/progress-stream": "^2.0.1",
|
||||
"@types/qrcode.react": "^1.0.1",
|
||||
"@types/react-modal": "^3.12.0",
|
||||
"@types/react-router": "^5.1.15",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-tabs": "^2.3.2",
|
||||
"@types/react-textarea-autosize": "^4.3.5",
|
||||
"@types/request": "^2.48.5",
|
||||
"cargo-cp-artifact": "^0.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"electron": "^13.1.2",
|
||||
"electron-builder": "^22.9.1",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electronmon": "^1.1.2",
|
||||
"wait-on": "^5.2.1"
|
||||
},
|
||||
"homepage": "./",
|
||||
"main": "./public/electron.js",
|
||||
"build": {
|
||||
"productName": "Zecwallet Lite",
|
||||
"appId": "co.zecwallet.lite",
|
||||
"afterAllArtifactBuild": "./afterSignHook.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"node_modules/",
|
||||
"app.html",
|
||||
"main.prod.js",
|
||||
"main.prod.js.map",
|
||||
"package.json"
|
||||
"build/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
"x": 130,
|
||||
"y": 220
|
||||
},
|
||||
{
|
||||
"x": 410,
|
||||
"y": 220,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"icon": "./resources/icon.ico",
|
||||
"target": [
|
||||
"zip",
|
||||
"msi"
|
||||
]
|
||||
"directories": {
|
||||
"buildResources": "public"
|
||||
},
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
|
@ -96,16 +167,12 @@
|
|||
],
|
||||
"icon": "./resources/icon.icns"
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"gconf2",
|
||||
"gconf-service",
|
||||
"libnotify4",
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3"
|
||||
],
|
||||
"artifactName": "Zecwallet_Lite_${version}_${arch}.deb"
|
||||
"win": {
|
||||
"icon": "./resources/icon.ico",
|
||||
"target": [
|
||||
"zip",
|
||||
"msi"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"category": "Development",
|
||||
|
@ -125,195 +192,63 @@
|
|||
"MimeType": "x-scheme-handler/zcash;",
|
||||
"Keywords": "zecwallet;"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "release"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/adityapk00/zecwallet-electron.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Aditya Kulkarni",
|
||||
"email": "aditya@zecwallet.co",
|
||||
"url": "https://zecwallet.co"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Aditya Kulkarni",
|
||||
"email": "aditya@zecwallet.co",
|
||||
"url": "https://zecwallet.co"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/adityapk00/zecwallet-electron/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"electron",
|
||||
"boilerplate",
|
||||
"react",
|
||||
"redux",
|
||||
"flow",
|
||||
"sass",
|
||||
"webpack",
|
||||
"hot",
|
||||
"reload"
|
||||
],
|
||||
"homepage": "https://github.com/adityapk00/zecwallet-electron#readme",
|
||||
"jest": {
|
||||
"testURL": "http://localhost/",
|
||||
"roots": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx,ts,tsx}",
|
||||
"!src/**/*.d.ts"
|
||||
],
|
||||
"setupFiles": [
|
||||
"react-app-polyfill/jsdom"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setupTests.ts"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"testRunner": "/Users/adityapk/gittea/experiments/zecwallet-lite/node_modules/jest-circus/runner.js",
|
||||
"transform": {
|
||||
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
|
||||
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
|
||||
"^.+\\.module\\.(css|sass|scss)$"
|
||||
],
|
||||
"modulePaths": [],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||
"^react-native$": "react-native-web",
|
||||
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"web.js",
|
||||
"js",
|
||||
"web.ts",
|
||||
"ts",
|
||||
"web.tsx",
|
||||
"tsx",
|
||||
"json",
|
||||
"web.jsx",
|
||||
"jsx",
|
||||
"json"
|
||||
"node"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
"app/node_modules"
|
||||
"watchPlugins": [
|
||||
"jest-watch-typeahead/filename",
|
||||
"jest-watch-typeahead/testname"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.jsx?$": "babel-jest"
|
||||
},
|
||||
"setupFiles": [
|
||||
"./internals/scripts/CheckBuildsExist.js"
|
||||
"resetMocks": true
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.7.4",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.7.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.7.4",
|
||||
"@babel/plugin-proposal-function-bind": "^7.7.4",
|
||||
"@babel/plugin-proposal-function-sent": "^7.7.4",
|
||||
"@babel/plugin-proposal-json-strings": "^7.7.4",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.7.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.7.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.7.5",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.7.4",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-syntax-import-meta": "^7.7.4",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.7.4",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-flow": "^7.7.4",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-dev-expression": "^0.2.2",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"chalk": "^3.0.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-spawn": "^7.0.1",
|
||||
"css-loader": "^3.3.2",
|
||||
"detect-port": "^1.3.0",
|
||||
"electron": "7.2.4",
|
||||
"electron-build-env": "^0.2.0",
|
||||
"electron-builder": "^22.2.0",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"electron-notarize": "^0.2.1",
|
||||
"electron-rebuild": "^1.8.8",
|
||||
"enzyme": "^3.7.0",
|
||||
"enzyme-adapter-react-16": "^1.7.0",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-config-airbnb": "^18.0.1",
|
||||
"eslint-config-erb": "^0.1.1",
|
||||
"eslint-config-prettier": "^6.6.0",
|
||||
"eslint-formatter-pretty": "^3.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.12.0",
|
||||
"eslint-plugin-compat": "^3.3.0",
|
||||
"eslint-plugin-flowtype": "^4.5.2",
|
||||
"eslint-plugin-import": "^2.19.1",
|
||||
"eslint-plugin-jest": "^23.1.1",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"eslint-plugin-testcafe": "^0.2.1",
|
||||
"fbjs-scripts": "^1.2.0",
|
||||
"file-loader": "^5.0.2",
|
||||
"flow-bin": "^0.133.0",
|
||||
"flow-runtime": "^0.17.0",
|
||||
"flow-typed": "^2.6.2",
|
||||
"husky": "^3.1.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.5.3",
|
||||
"lint-staged": "^9.5.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"neon-cli": "^0.3.3",
|
||||
"node-sass": "^4.13.1",
|
||||
"opencollective-postinstall": "^2.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"prettier": "^1.19.1",
|
||||
"react-test-renderer": "^16.12.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"sinon": "^7.5.0",
|
||||
"spectron": "^9.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"stylelint": "^12.0.0",
|
||||
"stylelint-config-prettier": "^8.0.0",
|
||||
"stylelint-config-standard": "^19.0.0",
|
||||
"terser-webpack-plugin": "^2.3.0",
|
||||
"url-loader": "^3.0.0",
|
||||
"webpack": "^4.41.6",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.9.0",
|
||||
"webpack-merge": "^4.2.2",
|
||||
"yarn": "^1.22.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||
"@hot-loader/react-dom": "^16.11.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"electron-debug": "^3.0.1",
|
||||
"electron-log": "^4.0.0",
|
||||
"electron-store": "^5.1.1",
|
||||
"history": "^4.10.1",
|
||||
"js-base64": "^3.5.2",
|
||||
"libsodium-wrappers-sumo": "^0.7.6",
|
||||
"progress": "^2.0.3",
|
||||
"qrcode.react": "^1.0.0",
|
||||
"react": "^16.12.0",
|
||||
"react-accessible-accordion": "^3.0.1",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-hot-loader": "^4.12.18",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-tabs": "^3.1.0",
|
||||
"react-textarea-autosize": "^7.1.2",
|
||||
"request": "^2.88.2",
|
||||
"source-map-support": "^0.5.16",
|
||||
"typeface-roboto": "^0.0.75",
|
||||
"ws": "^7.2.1"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": ">=7.x",
|
||||
"npm": ">=4.x",
|
||||
"yarn": ">=0.21.3"
|
||||
},
|
||||
"browserslist": "electron 1.6",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,543 @@
|
|||
const { app, BrowserWindow, Menu, shell, ipcMain } = require("electron");
|
||||
const isDev = require("electron-is-dev");
|
||||
const path = require("path");
|
||||
|
||||
class MenuBuilder {
|
||||
mainWindow;
|
||||
|
||||
constructor(mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
buildMenu() {
|
||||
const template = process.platform === "darwin" ? this.buildDarwinTemplate() : this.buildDefaultTemplate();
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
const selectionMenu = Menu.buildFromTemplate([{ role: "copy" }, { type: "separator" }, { role: "selectall" }]);
|
||||
|
||||
const inputMenu = Menu.buildFromTemplate([
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ type: "separator" },
|
||||
{ role: "selectall" },
|
||||
]);
|
||||
|
||||
this.mainWindow.webContents.on("context-menu", (e, props) => {
|
||||
const { selectionText, isEditable } = props;
|
||||
if (isEditable) {
|
||||
inputMenu.popup(this.mainWindow);
|
||||
} else if (selectionText && selectionText.trim() !== "") {
|
||||
selectionMenu.popup(this.mainWindow);
|
||||
} else if (process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true") {
|
||||
const { x, y } = props;
|
||||
|
||||
Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Inspect element",
|
||||
click: () => {
|
||||
this.mainWindow.inspectElement(x, y);
|
||||
},
|
||||
},
|
||||
]).popup(this.mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
buildDarwinTemplate() {
|
||||
const { mainWindow } = this;
|
||||
|
||||
const subMenuAbout = {
|
||||
label: "Zecwallet Lite",
|
||||
submenu: [
|
||||
{
|
||||
label: "About Zecwallet Lite",
|
||||
selector: "orderFrontStandardAboutPanel:",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("about");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ label: "Services", submenu: [] },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Hide Zecwallet Lite",
|
||||
accelerator: "Command+H",
|
||||
selector: "hide:",
|
||||
},
|
||||
{
|
||||
label: "Hide Others",
|
||||
accelerator: "Command+Shift+H",
|
||||
selector: "hideOtherApplications:",
|
||||
},
|
||||
{ label: "Show All", selector: "unhideAllApplications:" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Quit",
|
||||
accelerator: "Command+Q",
|
||||
click: () => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const subMenuEdit = {
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ label: "Undo", accelerator: "Command+Z", selector: "undo:" },
|
||||
{ label: "Redo", accelerator: "Shift+Command+Z", selector: "redo:" },
|
||||
{ type: "separator" },
|
||||
{ label: "Cut", accelerator: "Command+X", selector: "cut:" },
|
||||
{ label: "Copy", accelerator: "Command+C", selector: "copy:" },
|
||||
{ label: "Paste", accelerator: "Command+V", selector: "paste:" },
|
||||
{
|
||||
label: "Select All",
|
||||
accelerator: "Command+A",
|
||||
selector: "selectAll:",
|
||||
},
|
||||
],
|
||||
};
|
||||
const subMenuViewDev = {
|
||||
label: "Wallet",
|
||||
submenu: [
|
||||
{
|
||||
label: "Wallet Seed",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("seed");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Import Private Keys",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("import");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Export All Private Keys",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("exportall");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "&Pay URI",
|
||||
accelerator: "Ctrl+P",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("payuri");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Export All &Transactions",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("exportalltx");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Rescan",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("rescan");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "View Lightwalletd Info",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("zcashd");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Connect Mobile App",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("connectmobile");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Encrypt Wallet",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("encrypt");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Remove Wallet Encryption",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("decrypt");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Unlock",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("unlock");
|
||||
},
|
||||
},
|
||||
// { type: 'separator' },
|
||||
// {
|
||||
// label: 'Toggle Developer Tools',
|
||||
// accelerator: 'Alt+Command+I',
|
||||
// click: () => {
|
||||
// this.mainWindow.toggleDevTools();
|
||||
// }
|
||||
// }
|
||||
],
|
||||
};
|
||||
const subMenuViewProd = {
|
||||
label: "Wallet",
|
||||
submenu: [
|
||||
{
|
||||
label: "Wallet Seed",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("seed");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Import Private Keys",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("import");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Export All Private Keys",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("exportall");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "&Pay URI",
|
||||
accelerator: "Ctrl+P",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("payuri");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Export All &Transactions",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("exportalltx");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Rescan",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("rescan");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Server info",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("zcashd");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Connect Mobile App",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("connectmobile");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Encrypt Wallet",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("encrypt");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Remove Wallet Encryption",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("decrypt");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Unlock",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("unlock");
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const subMenuWindow = {
|
||||
label: "Window",
|
||||
submenu: [
|
||||
{
|
||||
label: "Minimize",
|
||||
accelerator: "Command+M",
|
||||
selector: "performMiniaturize:",
|
||||
},
|
||||
{ label: "Close", accelerator: "Command+W", selector: "performClose:" },
|
||||
{ type: "separator" },
|
||||
{ label: "Bring All to Front", selector: "arrangeInFront:" },
|
||||
],
|
||||
};
|
||||
const subMenuHelp = {
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{
|
||||
label: "Donate",
|
||||
click() {
|
||||
mainWindow.webContents.send("donate");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Check github.com for updates",
|
||||
click() {
|
||||
shell.openExternal("https://github.com/adityapk00/zecwallet-lite/releases");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "File a bug...",
|
||||
click() {
|
||||
shell.openExternal("https://github.com/adityapk00/zecwallet-lite/issues");
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const subMenuView = process.env.NODE_ENV === "development" ? subMenuViewDev : subMenuViewProd;
|
||||
|
||||
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||
}
|
||||
|
||||
buildDefaultTemplate() {
|
||||
const { mainWindow } = this;
|
||||
|
||||
const templateDefault = [
|
||||
{
|
||||
label: "&File",
|
||||
submenu: [
|
||||
{
|
||||
label: "&Pay URI",
|
||||
accelerator: "Ctrl+P",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("payuri");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Close",
|
||||
accelerator: "Ctrl+W",
|
||||
click: () => {
|
||||
this.mainWindow.close();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "&Wallet",
|
||||
submenu: [
|
||||
{
|
||||
label: "Wallet Seed",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("seed");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Import Private Keys",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("import");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Export All Private Keys",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("exportall");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Export All &Transactions",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("exportalltx");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "&Rescan",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("rescan");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Server info",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("zcashd");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Connect Mobile App",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("connectmobile");
|
||||
},
|
||||
},
|
||||
// {
|
||||
// label: 'Devtools',
|
||||
// click: () => {
|
||||
// mainWindow.webContents.openDevTools();
|
||||
// }
|
||||
// },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Encrypt Wallet",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("encrypt");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Remove Wallet Encryption",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("decrypt");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Unlock",
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send("unlock");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{
|
||||
label: "About Zecwallet Lite",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("about");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Donate",
|
||||
click() {
|
||||
mainWindow.webContents.send("donate");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Check github.com for updates",
|
||||
click() {
|
||||
shell.openExternal("https://github.com/adityapk00/zecwallet-lite/releases");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "File a bug...",
|
||||
click() {
|
||||
shell.openExternal("https://github.com/adityapk00/zecwallet-lite/issues");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return templateDefault;
|
||||
}
|
||||
}
|
||||
|
||||
// Conditionally include the dev tools installer to load React Dev Tools
|
||||
let installExtension, REACT_DEVELOPER_TOOLS;
|
||||
|
||||
if (isDev) {
|
||||
const devTools = require("electron-devtools-installer");
|
||||
installExtension = devTools.default;
|
||||
REACT_DEVELOPER_TOOLS = devTools.REACT_DEVELOPER_TOOLS;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1300,
|
||||
height: 728,
|
||||
minHeight: 500,
|
||||
minWidth: 1100,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegrationInWorker: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Load from localhost if in development
|
||||
// Otherwise load index.html file
|
||||
mainWindow.loadURL(isDev ? "http://localhost:3000" : `file://${path.join(__dirname, "../build/index.html")}`);
|
||||
|
||||
const menuBuilder = new MenuBuilder(mainWindow);
|
||||
menuBuilder.buildMenu();
|
||||
|
||||
let waitingForClose = false;
|
||||
let proceedToClose = false;
|
||||
|
||||
mainWindow.on("close", (event) => {
|
||||
// If we are clear to close, then return and allow everything to close
|
||||
if (proceedToClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're already waiting for close, then don't allow another close event to actually close the window
|
||||
if (waitingForClose) {
|
||||
console.log("Waiting for close... Timeout in 10s");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
waitingForClose = true;
|
||||
event.preventDefault();
|
||||
|
||||
ipcMain.on("appquitdone", () => {
|
||||
waitingForClose = false;
|
||||
proceedToClose = true;
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// $FlowFixMe
|
||||
mainWindow.webContents.send("appquitting");
|
||||
|
||||
// Failsafe, timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
waitingForClose = false;
|
||||
proceedToClose = true;
|
||||
console.log("Timeout, quitting");
|
||||
|
||||
app.quit();
|
||||
}, 5 * 1000);
|
||||
});
|
||||
|
||||
// Open DevTools if in dev mode
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools({ mode: "detach" });
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new browser window by invoking the createWindow
|
||||
// function once the Electron application is initialized.
|
||||
// Install REACT_DEVELOPER_TOOLS as well if isDev
|
||||
app.whenReady().then(() => {
|
||||
if (isDev) {
|
||||
installExtension(REACT_DEVELOPER_TOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((error) => console.log(`An error occurred: , ${error}`));
|
||||
}
|
||||
|
||||
createWindow();
|
||||
});
|
||||
|
||||
// Add a new listener that tries to quit the application when
|
||||
// it no longer has any open windows. This listener is a no-op
|
||||
// on macOS due to the operating system's window management behavior.
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Add a new listener that creates a new browser window only if
|
||||
// when the application has no visible windows after being activated.
|
||||
// For example, after launching the application for the first time,
|
||||
// or re-launching the already running application.
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// The code above has been adapted from a starter example in the Electron docs:
|
||||
// https://www.electronjs.org/docs/tutorial/quick-start#create-the-main-script-file
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="script-src 'self' 'unsafe-inline';"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": ["bliss"]
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
"use strict";
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require("../config/env");
|
||||
|
||||
const path = require("path");
|
||||
const chalk = require("react-dev-utils/chalk");
|
||||
const fs = require("fs-extra");
|
||||
const bfj = require("bfj");
|
||||
const webpack = require("webpack");
|
||||
const configFactory = require("../config/webpack.config");
|
||||
const paths = require("../config/paths");
|
||||
const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles");
|
||||
const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
|
||||
const printHostingInstructions = require("react-dev-utils/printHostingInstructions");
|
||||
const FileSizeReporter = require("react-dev-utils/FileSizeReporter");
|
||||
const printBuildError = require("react-dev-utils/printBuildError");
|
||||
|
||||
const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const writeStatsJson = argv.indexOf("--stats") !== -1;
|
||||
|
||||
// Generate configuration
|
||||
const config = configFactory("production");
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require("react-dev-utils/browsersHelper");
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||
})
|
||||
.then((previousFileSizes) => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// Merge with the public folder
|
||||
copyPublicFolder();
|
||||
// Start the webpack build
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow("Compiled with warnings.\n"));
|
||||
console.log(warnings.join("\n\n"));
|
||||
console.log(
|
||||
"\nSearch for the " + chalk.underline(chalk.yellow("keywords")) + " to learn more about each warning."
|
||||
);
|
||||
console.log("To ignore, add " + chalk.cyan("// eslint-disable-next-line") + " to the line before.\n");
|
||||
} else {
|
||||
console.log(chalk.green("Compiled successfully.\n"));
|
||||
}
|
||||
|
||||
console.log("File sizes after gzip:\n");
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrlOrPath;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(appPackage, publicUrl, publicPath, buildFolder, useYarn);
|
||||
},
|
||||
(err) => {
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === "true";
|
||||
if (tscCompileOnError) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"Compiled with the following type errors (you may want to check these before deploying your app):\n"
|
||||
)
|
||||
);
|
||||
printBuildError(err);
|
||||
} else {
|
||||
console.log(chalk.red("Failed to compile.\n"));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
console.log("Creating an optimized production build...");
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
let messages;
|
||||
if (err) {
|
||||
if (!err.message) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let errMessage = err.message;
|
||||
|
||||
// Add additional information for postcss errors
|
||||
if (Object.prototype.hasOwnProperty.call(err, "postcssNode")) {
|
||||
errMessage += "\nCompileError: Begins at CSS selector " + err["postcssNode"].selector;
|
||||
}
|
||||
|
||||
messages = formatWebpackMessages({
|
||||
errors: [errMessage],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(stats.toJson({ all: false, warnings: true, errors: true }));
|
||||
}
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join("\n\n")));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== "string" || process.env.CI.toLowerCase() !== "false") &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"\nTreating warnings as errors because process.env.CI = true.\n" + "Most CI servers set it automatically.\n"
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join("\n\n")));
|
||||
}
|
||||
|
||||
const resolveArgs = {
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
};
|
||||
|
||||
if (writeStatsJson) {
|
||||
return bfj
|
||||
.write(paths.appBuild + "/bundle-stats.json", stats.toJson())
|
||||
.then(() => resolve(resolveArgs))
|
||||
.catch((error) => reject(new Error(error)));
|
||||
}
|
||||
|
||||
return resolve(resolveArgs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: (file) => file !== paths.appHtml,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
"use strict";
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "development";
|
||||
process.env.NODE_ENV = "development";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require("../config/env");
|
||||
|
||||
const fs = require("fs");
|
||||
const chalk = require("react-dev-utils/chalk");
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const clearConsole = require("react-dev-utils/clearConsole");
|
||||
const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles");
|
||||
const { choosePort, createCompiler, prepareProxy, prepareUrls } = require("react-dev-utils/WebpackDevServerUtils");
|
||||
const openBrowser = require("react-dev-utils/openBrowser");
|
||||
const semver = require("semver");
|
||||
const paths = require("../config/paths");
|
||||
const configFactory = require("../config/webpack.config");
|
||||
const createDevServerConfig = require("../config/webpackDevServer.config");
|
||||
const getClientEnvironment = require("../config/env");
|
||||
const react = require(require.resolve("react", { paths: [paths.appPath] }));
|
||||
|
||||
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
|
||||
if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(`Attempting to bind to HOST environment variable: ${chalk.yellow(chalk.bold(process.env.HOST))}`)
|
||||
);
|
||||
console.log(`If this was unintentional, check that you haven't mistakenly set it in your shell.`);
|
||||
console.log(`Learn more here: ${chalk.yellow("https://cra.link/advanced-config")}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require("react-dev-utils/browsersHelper");
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
return choosePort(HOST, DEFAULT_PORT);
|
||||
})
|
||||
.then((port) => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configFactory("development");
|
||||
const protocol = process.env.HTTPS === "true" ? "https" : "http";
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === "true";
|
||||
const urls = prepareUrls(protocol, HOST, port, paths.publicUrlOrPath.slice(0, -1));
|
||||
const devSocket = {
|
||||
warnings: (warnings) => devServer.sockWrite(devServer.sockets, "warnings", warnings),
|
||||
errors: (errors) => devServer.sockWrite(devServer.sockets, "errors", errors),
|
||||
};
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler({
|
||||
appName,
|
||||
config,
|
||||
devSocket,
|
||||
urls,
|
||||
useYarn,
|
||||
useTypeScript,
|
||||
tscCompileOnError,
|
||||
webpack,
|
||||
});
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(proxySetting, paths.appPublic, paths.publicUrlOrPath);
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(proxyConfig, urls.lanUrlForConfig);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// Launch WebpackDevServer.
|
||||
devServer.listen(port, HOST, (err) => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
if (env.raw.FAST_REFRESH && semver.lt(react.version, "16.10.0")) {
|
||||
console.log(chalk.yellow(`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`));
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Starting the development server...\n"));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
["SIGINT", "SIGTERM"].forEach(function (sig) {
|
||||
process.on(sig, function () {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.CI !== "true") {
|
||||
// Gracefully exit when stdin ends
|
||||
process.stdin.on("end", function () {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
"use strict";
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "test";
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.PUBLIC_URL = "";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require("../config/env");
|
||||
|
||||
const jest = require("jest");
|
||||
const execSync = require("child_process").execSync;
|
||||
let argv = process.argv.slice(2);
|
||||
|
||||
function isInGitRepository() {
|
||||
try {
|
||||
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInMercurialRepository() {
|
||||
try {
|
||||
execSync("hg --cwd . root", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch unless on CI or explicitly running all tests
|
||||
if (!process.env.CI && argv.indexOf("--watchAll") === -1 && argv.indexOf("--watchAll=false") === -1) {
|
||||
// https://github.com/facebook/create-react-app/issues/5210
|
||||
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||
argv.push(hasSourceControl ? "--watch" : "--watchAll");
|
||||
}
|
||||
|
||||
jest.run(argv);
|
|
@ -0,0 +1,38 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
||||
test("renders title", () => {
|
||||
render(<App />);
|
||||
const title = screen.getByText(/Built using CRA electron-builder-typescript Template/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import React from "react";
|
||||
import logo from "./logo.svg";
|
||||
import "./App.css";
|
||||
|
||||
//const addon = require('./native.node');
|
||||
import addon from "./native.node";
|
||||
|
||||
function App() {
|
||||
console.log("Addon is:");
|
||||
console.log(addon);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>Built using CRA electron-builder-typescript Template.</p>
|
||||
<p>{addon.litelib_say_hello("Me")}</p>
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>public/electron.js</code> or <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
d
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,11 @@
|
|||
import React from "react";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import Routes from "./Routes";
|
||||
|
||||
const Root = () => (
|
||||
<Router>
|
||||
<Routes />
|
||||
</Router>
|
||||
);
|
||||
|
||||
export default Root;
|
|
@ -2,18 +2,17 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/no-unused-state */
|
||||
import React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import native from '../native/index.node';
|
||||
import { ErrorModal, ErrorModalData } from './components/ErrorModal';
|
||||
import cstyles from './components/Common.module.css';
|
||||
import routes from './constants/routes.json';
|
||||
import App from './containers/App';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import Send from './components/Send';
|
||||
import Receive from './components/Receive';
|
||||
import LoadingScreen from './components/LoadingScreen';
|
||||
import React from "react";
|
||||
import ReactModal from "react-modal";
|
||||
import { Switch, Route } from "react-router";
|
||||
import native from "./native.node";
|
||||
import { ErrorModal, ErrorModalData } from "./components/ErrorModal";
|
||||
import cstyles from "./components/Common.module.css";
|
||||
import routes from "./constants/routes.json";
|
||||
import Dashboard from "./components/Dashboard";
|
||||
import Send, { SendManyJson } from "./components/Send";
|
||||
import Receive from "./components/Receive";
|
||||
import LoadingScreen from "./components/LoadingScreen";
|
||||
import AppState, {
|
||||
AddressBalance,
|
||||
TotalBalance,
|
||||
|
@ -25,73 +24,50 @@ import AppState, {
|
|||
ReceivePageState,
|
||||
AddressBookEntry,
|
||||
PasswordState,
|
||||
ServerSelectState
|
||||
} from './components/AppState';
|
||||
import RPC from './rpc';
|
||||
import Utils from './utils/utils';
|
||||
import { ZcashURITarget } from './utils/uris';
|
||||
import Zcashd from './components/Zcashd';
|
||||
import AddressBook from './components/Addressbook';
|
||||
import AddressbookImpl from './utils/AddressbookImpl';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Transactions from './components/Transactions';
|
||||
import PasswordModal from './components/PasswordModal';
|
||||
import ServerSelectModal from './components/ServerSelectModal';
|
||||
import CompanionAppListener from './companion';
|
||||
import WormholeConnection from './components/WormholeConnection';
|
||||
ServerSelectState,
|
||||
SendProgress,
|
||||
} from "./components/AppState";
|
||||
import RPC from "./rpc";
|
||||
import Utils from "./utils/utils";
|
||||
import { ZcashURITarget } from "./utils/uris";
|
||||
import Zcashd from "./components/Zcashd";
|
||||
import AddressBook from "./components/Addressbook";
|
||||
import AddressbookImpl from "./utils/AddressbookImpl";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
import Transactions from "./components/Transactions";
|
||||
import PasswordModal from "./components/PasswordModal";
|
||||
import ServerSelectModal from "./components/ServerSelectModal";
|
||||
|
||||
type Props = {};
|
||||
|
||||
export default class RouteApp extends React.Component<Props, AppState> {
|
||||
rpc: RPC;
|
||||
|
||||
companionAppListener: CompanionAppListener;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
totalBalance: new TotalBalance(),
|
||||
addressesWithBalance: [],
|
||||
addressPrivateKeys: {},
|
||||
addressViewKeys: {},
|
||||
addresses: [],
|
||||
addressBook: [],
|
||||
transactions: null,
|
||||
sendPageState: new SendPageState(),
|
||||
receivePageState: new ReceivePageState(),
|
||||
rpcConfig: new RPCConfig(),
|
||||
info: new Info(),
|
||||
rescanning: false,
|
||||
location: null,
|
||||
errorModalData: new ErrorModalData(),
|
||||
passwordState: new PasswordState(),
|
||||
serverSelectState: new ServerSelectState(),
|
||||
connectedCompanionApp: null
|
||||
};
|
||||
this.state = new AppState();
|
||||
|
||||
// Create the initial ToAddr box
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
this.state.sendPageState.toaddrs = [new ToAddr(Utils.getNextToAddrID())];
|
||||
|
||||
// Set the Modal's app element
|
||||
ReactModal.setAppElement('#root');
|
||||
ReactModal.setAppElement("#root");
|
||||
|
||||
console.log(native.litelib_wallet_exists('main'));
|
||||
this.rpc = new RPC(
|
||||
this.setTotalBalance,
|
||||
this.setAddressesWithBalances,
|
||||
this.setTransactionList,
|
||||
this.setAllAddresses,
|
||||
this.setInfo,
|
||||
this.setZecPrice
|
||||
);
|
||||
|
||||
console.log(native.litelib_wallet_exists("main"));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.rpc) {
|
||||
this.rpc = new RPC(
|
||||
this.setTotalBalance,
|
||||
this.setAddressesWithBalances,
|
||||
this.setTransactionList,
|
||||
this.setAllAddresses,
|
||||
this.setInfo,
|
||||
this.setZecPrice
|
||||
);
|
||||
}
|
||||
|
||||
// Read the address book
|
||||
(async () => {
|
||||
const addressBook = await AddressbookImpl.readAddressBook();
|
||||
|
@ -99,14 +75,6 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
this.setState({ addressBook });
|
||||
}
|
||||
})();
|
||||
|
||||
// Setup the websocket for the companion app
|
||||
this.companionAppListener = new CompanionAppListener(
|
||||
this.getFullState,
|
||||
this.sendTransaction,
|
||||
this.updateConnectedCompanionApp
|
||||
);
|
||||
this.companionAppListener.setUp();
|
||||
}
|
||||
|
||||
componentWillUnmount() {}
|
||||
|
@ -115,7 +83,7 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
return this.state;
|
||||
};
|
||||
|
||||
openErrorModal = (title: string, body: string) => {
|
||||
openErrorModal = (title: string, body: string | JSX.Element) => {
|
||||
const errorModalData = new ErrorModalData();
|
||||
errorModalData.modalIsOpen = true;
|
||||
errorModalData.title = title;
|
||||
|
@ -147,15 +115,15 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
|
||||
openPassword = (
|
||||
confirmNeeded: boolean,
|
||||
passwordCallback: string => void,
|
||||
passwordCallback: (p: string) => void,
|
||||
closeCallback: () => void,
|
||||
helpText: string
|
||||
helpText?: string | JSX.Element
|
||||
) => {
|
||||
const passwordState = new PasswordState();
|
||||
|
||||
passwordState.showPassword = true;
|
||||
passwordState.confirmNeeded = confirmNeeded;
|
||||
passwordState.helpText = helpText;
|
||||
passwordState.helpText = helpText || "";
|
||||
|
||||
// Set the callbacks, but before calling them back, we close the modals
|
||||
passwordState.passwordCallback = (password: string) => {
|
||||
|
@ -196,7 +164,7 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
// If the unlock succeeded, do the submit
|
||||
successCallback();
|
||||
} else {
|
||||
this.openErrorModal('Wallet unlock failed', 'Could not unlock the wallet with the password.');
|
||||
this.openErrorModal("Wallet unlock failed", "Could not unlock the wallet with the password.");
|
||||
}
|
||||
})();
|
||||
},
|
||||
|
@ -208,23 +176,23 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
}
|
||||
};
|
||||
|
||||
unlockWallet = async (password: string): boolean => {
|
||||
unlockWallet = async (password: string): Promise<boolean> => {
|
||||
const success = await this.rpc.unlockWallet(password);
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
lockWallet = async (): boolean => {
|
||||
lockWallet = async (): Promise<boolean> => {
|
||||
const success = await this.rpc.lockWallet();
|
||||
return success;
|
||||
};
|
||||
|
||||
encryptWallet = async (password): boolean => {
|
||||
encryptWallet = async (password: string): Promise<boolean> => {
|
||||
const success = await this.rpc.encryptWallet(password);
|
||||
return success;
|
||||
};
|
||||
|
||||
decryptWallet = async (password): boolean => {
|
||||
decryptWallet = async (password: string): Promise<boolean> => {
|
||||
const success = await this.rpc.decryptWallet(password);
|
||||
return success;
|
||||
};
|
||||
|
@ -242,18 +210,17 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
if (!sendPageState.fromaddr) {
|
||||
// Find a z-address with the highest balance
|
||||
const defaultAB = addressesWithBalance
|
||||
.filter(ab => Utils.isSapling(ab.address))
|
||||
.reduce((prev, ab) => {
|
||||
.filter((ab) => Utils.isSapling(ab.address))
|
||||
.reduce((prev: AddressBalance | null, ab) => {
|
||||
// We'll start with a sapling address
|
||||
if (prev == null) {
|
||||
if (!prev) {
|
||||
return ab;
|
||||
}
|
||||
// Find the sapling address with the highest balance
|
||||
if (prev.balance < ab.balance) {
|
||||
} else if (prev.balance < ab.balance) {
|
||||
// Find the sapling address with the highest balance
|
||||
return ab;
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return prev;
|
||||
}, null);
|
||||
|
||||
if (defaultAB) {
|
||||
|
@ -278,19 +245,19 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
this.setState({ sendPageState });
|
||||
};
|
||||
|
||||
importPrivKeys = async (keys: string[], birthday: string): boolean => {
|
||||
importPrivKeys = async (keys: string[], birthday: string): Promise<boolean> => {
|
||||
console.log(keys);
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const result = await RPC.doImportPrivKey(keys[i], birthday);
|
||||
if (result === 'OK') {
|
||||
if (result === "OK") {
|
||||
return true;
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else {
|
||||
this.openErrorModal(
|
||||
'Failed to import key',
|
||||
"Failed to import key",
|
||||
<span>
|
||||
A private key failed to import.
|
||||
<br />
|
||||
|
@ -303,6 +270,8 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
setSendTo = (targets: ZcashURITarget[] | ZcashURITarget) => {
|
||||
|
@ -316,10 +285,10 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
// If a single object is passed, accept that as well.
|
||||
let tgts = targets;
|
||||
if (!Array.isArray(tgts)) {
|
||||
tgts = [targets];
|
||||
tgts = [targets as ZcashURITarget];
|
||||
}
|
||||
|
||||
tgts.forEach(tgt => {
|
||||
tgts.forEach((tgt) => {
|
||||
const to = new ToAddr(Utils.getNextToAddrID());
|
||||
if (tgt.address) {
|
||||
to.to = tgt.address;
|
||||
|
@ -340,22 +309,26 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
setRPCConfig = (rpcConfig: RPCConfig) => {
|
||||
this.setState({ rpcConfig });
|
||||
console.log(rpcConfig);
|
||||
|
||||
this.rpc.configure(rpcConfig);
|
||||
};
|
||||
|
||||
setZecPrice = (price: number | null) => {
|
||||
setZecPrice = (price?: number) => {
|
||||
console.log(`Price = ${price}`);
|
||||
const { info } = this.state;
|
||||
|
||||
const newInfo = new Info();
|
||||
Object.assign(newInfo, info);
|
||||
newInfo.zecPrice = price;
|
||||
if (price) {
|
||||
newInfo.zecPrice = price;
|
||||
}
|
||||
|
||||
this.setState({ info: newInfo });
|
||||
};
|
||||
|
||||
setRescanning = (rescanning: boolean) => {
|
||||
setRescanning = (rescanning: boolean, prevSyncId: number) => {
|
||||
this.setState({ rescanning });
|
||||
this.setState({ prevSyncId });
|
||||
};
|
||||
|
||||
setInfo = (newInfo: Info) => {
|
||||
|
@ -371,17 +344,17 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
this.setState({ info: newInfo });
|
||||
};
|
||||
|
||||
sendTransaction = async (sendJson: [], setSendProgress: SendProgress => void): string => {
|
||||
sendTransaction = async (sendJson: SendManyJson[], setSendProgress: (p?: SendProgress) => void): Promise<string> => {
|
||||
try {
|
||||
const txid = await this.rpc.sendTransaction(sendJson, setSendProgress);
|
||||
|
||||
if (txid.toLowerCase().startsWith('error')) {
|
||||
if (txid.toLowerCase().startsWith("error")) {
|
||||
throw txid;
|
||||
}
|
||||
|
||||
return txid;
|
||||
} catch (err) {
|
||||
console.log('route sendtx error', err);
|
||||
console.log("route sendtx error", err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
@ -397,11 +370,11 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
fetchAndSetSinglePrivKey = async (address: string) => {
|
||||
this.openPasswordAndUnlockIfNeeded(async () => {
|
||||
let key = await RPC.getPrivKeyAsString(address);
|
||||
if (key === '') {
|
||||
key = '<No Key Available>';
|
||||
if (key === "") {
|
||||
key = "<No Key Available>";
|
||||
}
|
||||
const addressPrivateKeys = {};
|
||||
addressPrivateKeys[address] = key;
|
||||
const addressPrivateKeys = new Map();
|
||||
addressPrivateKeys.set(address, key);
|
||||
|
||||
this.setState({ addressPrivateKeys });
|
||||
});
|
||||
|
@ -410,8 +383,8 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
fetchAndSetSingleViewKey = async (address: string) => {
|
||||
this.openPasswordAndUnlockIfNeeded(async () => {
|
||||
const key = await RPC.getViewKeyAsString(address);
|
||||
const addressViewKeys = {};
|
||||
addressViewKeys[address] = key;
|
||||
const addressViewKeys = new Map();
|
||||
addressViewKeys.set(address, key);
|
||||
|
||||
this.setState({ addressViewKeys });
|
||||
});
|
||||
|
@ -430,7 +403,7 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
|
||||
removeAddressBookEntry = (label: string) => {
|
||||
const { addressBook } = this.state;
|
||||
const newAddressBook = addressBook.filter(i => i.label !== label);
|
||||
const newAddressBook = addressBook.filter((i) => i.label !== label);
|
||||
|
||||
// Write to disk. This method is async
|
||||
AddressbookImpl.writeAddressBook(newAddressBook);
|
||||
|
@ -458,12 +431,8 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
});
|
||||
};
|
||||
|
||||
updateConnectedCompanionApp = (connectedCompanionApp: ConnectedCompanionApp | null) => {
|
||||
this.setState({ connectedCompanionApp });
|
||||
};
|
||||
|
||||
doRefresh = () => {
|
||||
this.rpc.refresh(0, false);
|
||||
this.rpc.refresh(false);
|
||||
};
|
||||
|
||||
clearTimers = () => {
|
||||
|
@ -484,10 +453,10 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
rpcConfig,
|
||||
info,
|
||||
rescanning,
|
||||
prevSyncId,
|
||||
errorModalData,
|
||||
serverSelectState,
|
||||
passwordState,
|
||||
connectedCompanionApp
|
||||
} = this.state;
|
||||
|
||||
const standardProps = {
|
||||
|
@ -495,11 +464,13 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
closeErrorModal: this.closeErrorModal,
|
||||
setSendTo: this.setSendTo,
|
||||
info,
|
||||
openPasswordAndUnlockIfNeeded: this.openPasswordAndUnlockIfNeeded
|
||||
openPasswordAndUnlockIfNeeded: this.openPasswordAndUnlockIfNeeded,
|
||||
};
|
||||
|
||||
const hasLatestBlock = info && info.latestBlock > 0 ? true : false;
|
||||
|
||||
return (
|
||||
<App>
|
||||
<>
|
||||
<ErrorModal
|
||||
title={errorModalData.title}
|
||||
body={errorModalData.body}
|
||||
|
@ -521,13 +492,11 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
openErrorModal={this.openErrorModal}
|
||||
/>
|
||||
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
{info && info.latestBlock && (
|
||||
<div style={{ overflow: "hidden" }}>
|
||||
{hasLatestBlock && (
|
||||
<div className={cstyles.sidebarcontainer}>
|
||||
<Sidebar
|
||||
info={info}
|
||||
setInfo={this.setInfo}
|
||||
setSendTo={this.setSendTo}
|
||||
setRescanning={this.setRescanning}
|
||||
getPrivKeyAsString={this.getPrivKeyAsString}
|
||||
addresses={addresses}
|
||||
|
@ -542,6 +511,7 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cstyles.contentcontainer}>
|
||||
<Switch>
|
||||
<Route
|
||||
|
@ -549,7 +519,6 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
render={() => (
|
||||
<Send
|
||||
addresses={addresses}
|
||||
setSendTo={this.setSendTo}
|
||||
sendTransaction={this.sendTransaction}
|
||||
sendPageState={sendPageState}
|
||||
setSendPageState={this.setSendPageState}
|
||||
|
@ -619,21 +588,13 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
)}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.CONNECTMOBILE}
|
||||
render={() => (
|
||||
<WormholeConnection
|
||||
companionAppListener={this.companionAppListener}
|
||||
connectedCompanionApp={connectedCompanionApp}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={routes.LOADING}
|
||||
render={() => (
|
||||
<LoadingScreen
|
||||
setRPCConfig={this.setRPCConfig}
|
||||
rescanning={rescanning}
|
||||
prevSyncId={prevSyncId}
|
||||
setRescanning={this.setRescanning}
|
||||
setInfo={this.setInfo}
|
||||
openServerSelectModal={this.openServerSelectModal}
|
||||
|
@ -643,7 +604,7 @@ export default class RouteApp extends React.Component<Props, AppState> {
|
|||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</App>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 747 B After Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
@ -1,40 +1,51 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import React, { Component } from "react";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import {
|
||||
AccordionItemButton,
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemPanel,
|
||||
Accordion
|
||||
} from 'react-accessible-accordion';
|
||||
import styles from './Addressbook.module.css';
|
||||
import cstyles from './Common.module.css';
|
||||
import { AddressBookEntry } from './AppState';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import Utils from '../utils/utils';
|
||||
import { ZcashURITarget } from '../utils/uris';
|
||||
import routes from '../constants/routes.json';
|
||||
Accordion,
|
||||
} from "react-accessible-accordion";
|
||||
import styles from "./Addressbook.module.css";
|
||||
import cstyles from "./Common.module.css";
|
||||
import { AddressBookEntry } from "./AppState";
|
||||
import ScrollPane from "./ScrollPane";
|
||||
import Utils from "../utils/utils";
|
||||
import { ZcashURITarget } from "../utils/uris";
|
||||
import routes from "../constants/routes.json";
|
||||
|
||||
type AddressBookItemProps = {
|
||||
item: AddressBookEntry;
|
||||
removeAddressBookEntry: (label: string) => void;
|
||||
setSendTo: (targets: ZcashURITarget | ZcashURITarget[]) => void;
|
||||
};
|
||||
|
||||
// Internal because we're using withRouter just below
|
||||
const AddressBookItemInteral = ({ item, removeAddressBookEntry, setSendTo, history }) => {
|
||||
const AddressBookItemInteral: React.FC<RouteComponentProps & AddressBookItemProps> = ({
|
||||
item,
|
||||
removeAddressBookEntry,
|
||||
setSendTo,
|
||||
history,
|
||||
}) => {
|
||||
return (
|
||||
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(' ')} uuid={item.label}>
|
||||
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(" ")} uuid={item.label}>
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={cstyles.accordionHeader}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div>{item.label}</div>
|
||||
<div>{item.address}</div>
|
||||
</div>
|
||||
</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel>
|
||||
<div className={[cstyles.well, styles.addressbookentrybuttons].join(' ')}>
|
||||
<div className={[cstyles.well, styles.addressbookentrybuttons].join(" ")}>
|
||||
<button
|
||||
type="button"
|
||||
className={cstyles.primarybutton}
|
||||
onClick={() => {
|
||||
setSendTo(new ZcashURITarget(item.address, null, null));
|
||||
setSendTo(new ZcashURITarget(item.address, undefined, undefined));
|
||||
history.push(routes.SEND);
|
||||
}}
|
||||
>
|
||||
|
@ -50,24 +61,24 @@ const AddressBookItemInteral = ({ item, removeAddressBookEntry, setSendTo, histo
|
|||
};
|
||||
const AddressBookItem = withRouter(AddressBookItemInteral);
|
||||
|
||||
type Props = {
|
||||
addressBook: AddressBookEntry[],
|
||||
addAddressBookEntry: (label: string, address: string) => void,
|
||||
removeAddressBookEntry: (label: string) => void,
|
||||
setSendTo: (targets: ZcashURITarget[] | ZcashURITarget) => void
|
||||
type AddressBookProps = {
|
||||
addressBook: AddressBookEntry[];
|
||||
addAddressBookEntry: (label: string, address: string) => void;
|
||||
removeAddressBookEntry: (label: string) => void;
|
||||
setSendTo: (targets: ZcashURITarget[] | ZcashURITarget) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
currentLabel: string,
|
||||
currentAddress: string,
|
||||
addButtonEnabled: boolean
|
||||
type AddressBookState = {
|
||||
currentLabel: string;
|
||||
currentAddress: string;
|
||||
addButtonEnabled: boolean;
|
||||
};
|
||||
|
||||
export default class AddressBook extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
export default class AddressBook extends Component<AddressBookProps, AddressBookState> {
|
||||
constructor(props: AddressBookProps) {
|
||||
super(props);
|
||||
|
||||
this.state = { currentLabel: '', currentAddress: '', addButtonEnabled: false };
|
||||
this.state = { currentLabel: "", currentAddress: "", addButtonEnabled: false };
|
||||
}
|
||||
|
||||
updateLabel = (currentLabel: string) => {
|
||||
|
@ -78,7 +89,7 @@ export default class AddressBook extends Component<Props, State> {
|
|||
this.setState({ currentLabel });
|
||||
|
||||
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
|
||||
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== '' && currentAddress !== '');
|
||||
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== "" && currentAddress !== "");
|
||||
};
|
||||
|
||||
updateAddress = (currentAddress: string) => {
|
||||
|
@ -87,7 +98,7 @@ export default class AddressBook extends Component<Props, State> {
|
|||
|
||||
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
|
||||
|
||||
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== '' && currentAddress !== '');
|
||||
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== "" && currentAddress !== "");
|
||||
};
|
||||
|
||||
addButtonClicked = () => {
|
||||
|
@ -95,21 +106,21 @@ export default class AddressBook extends Component<Props, State> {
|
|||
const { currentLabel, currentAddress } = this.state;
|
||||
|
||||
addAddressBookEntry(currentLabel, currentAddress);
|
||||
this.setState({ currentLabel: '', currentAddress: '' });
|
||||
this.setState({ currentLabel: "", currentAddress: "" });
|
||||
};
|
||||
|
||||
setAddButtonEnabled = (addButtonEnabled: boolean) => {
|
||||
this.setState({ addButtonEnabled });
|
||||
};
|
||||
|
||||
validate = (currentLabel, currentAddress) => {
|
||||
validate = (currentLabel: string, currentAddress: string) => {
|
||||
const { addressBook } = this.props;
|
||||
|
||||
let labelError = addressBook.find(i => i.label === currentLabel) ? 'Duplicate Label' : null;
|
||||
labelError = currentLabel.length > 12 ? 'Label is too long' : labelError;
|
||||
let labelError = addressBook.find((i) => i.label === currentLabel) ? "Duplicate Label" : null;
|
||||
labelError = currentLabel.length > 12 ? "Label is too long" : labelError;
|
||||
|
||||
const addressIsValid =
|
||||
currentAddress === '' || Utils.isZaddr(currentAddress) || Utils.isTransparent(currentAddress);
|
||||
currentAddress === "" || Utils.isZaddr(currentAddress) || Utils.isTransparent(currentAddress);
|
||||
|
||||
return { labelError, addressIsValid };
|
||||
};
|
||||
|
@ -122,15 +133,15 @@ export default class AddressBook extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Address Book</div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(" ")}>Address Book</div>
|
||||
|
||||
<div className={styles.addressbookcontainer}>
|
||||
<div className={[cstyles.well].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.well].join(" ")}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div className={cstyles.sublight}>Label</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{!labelError ? (
|
||||
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
|
||||
<i className={[cstyles.green, "fas", "fa-check"].join(" ")} />
|
||||
) : (
|
||||
<span className={cstyles.red}>{labelError}</span>
|
||||
)}
|
||||
|
@ -139,17 +150,17 @@ export default class AddressBook extends Component<Props, State> {
|
|||
<input
|
||||
type="text"
|
||||
value={currentLabel}
|
||||
className={[cstyles.inputbox, cstyles.margintopsmall].join(' ')}
|
||||
onChange={e => this.updateLabel(e.target.value)}
|
||||
className={[cstyles.inputbox, cstyles.margintopsmall].join(" ")}
|
||||
onChange={(e) => this.updateLabel(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div className={cstyles.sublight}>Address</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{addressIsValid ? (
|
||||
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
|
||||
<i className={[cstyles.green, "fas", "fa-check"].join(" ")} />
|
||||
) : (
|
||||
<span className={cstyles.red}>Invalid Address</span>
|
||||
)}
|
||||
|
@ -158,8 +169,8 @@ export default class AddressBook extends Component<Props, State> {
|
|||
<input
|
||||
type="text"
|
||||
value={currentAddress}
|
||||
className={[cstyles.inputbox, cstyles.margintopsmall].join(' ')}
|
||||
onChange={e => this.updateAddress(e.target.value)}
|
||||
className={[cstyles.inputbox, cstyles.margintopsmall].join(" ")}
|
||||
onChange={(e) => this.updateAddress(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
|
@ -176,13 +187,13 @@ export default class AddressBook extends Component<Props, State> {
|
|||
|
||||
<ScrollPane offsetHeight={300}>
|
||||
<div className={styles.addressbooklist}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(" ")}>
|
||||
<div>Label</div>
|
||||
<div>Address</div>
|
||||
</div>
|
||||
{addressBook && (
|
||||
<Accordion>
|
||||
{addressBook.map(item => (
|
||||
{addressBook.map((item) => (
|
||||
<AddressBookItem
|
||||
key={item.label}
|
||||
item={item}
|
|
@ -1,5 +1,7 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { ErrorModalData } from "./ErrorModal";
|
||||
|
||||
export class TotalBalance {
|
||||
// Total t address, confirmed and spendable
|
||||
transparent: number;
|
||||
|
@ -18,6 +20,15 @@ export class TotalBalance {
|
|||
|
||||
// Total unconfirmed + spendable
|
||||
total: number;
|
||||
|
||||
constructor() {
|
||||
this.private = 0;
|
||||
this.transparent = 0;
|
||||
this.verifiedPrivate = 0;
|
||||
this.unverifiedPrivate = 0;
|
||||
this.spendablePrivate = 0;
|
||||
this.total = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class AddressBalance {
|
||||
|
@ -26,6 +37,7 @@ export class AddressBalance {
|
|||
balance: number;
|
||||
|
||||
containsPending: boolean;
|
||||
label?: string;
|
||||
|
||||
constructor(address: string, balance: number) {
|
||||
this.address = address;
|
||||
|
@ -51,42 +63,51 @@ export class TxDetail {
|
|||
amount: string;
|
||||
|
||||
memo: string | null;
|
||||
|
||||
constructor() {
|
||||
this.address = "";
|
||||
this.amount = "";
|
||||
this.memo = null;
|
||||
}
|
||||
}
|
||||
|
||||
// List of transactions. TODO: Handle memos, multiple addresses etc...
|
||||
export class Transaction {
|
||||
type: string;
|
||||
|
||||
address: string;
|
||||
|
||||
amount: number;
|
||||
|
||||
position: string;
|
||||
|
||||
confirmations: number;
|
||||
|
||||
txid: string;
|
||||
|
||||
time: number;
|
||||
|
||||
detailedTxns: TxDetail[];
|
||||
zecPrice: any;
|
||||
|
||||
constructor() {
|
||||
this.type = "";
|
||||
this.address = "";
|
||||
this.amount = 0;
|
||||
this.position = "";
|
||||
this.confirmations = 0;
|
||||
this.txid = "";
|
||||
this.time = 0;
|
||||
this.detailedTxns = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class ToAddr {
|
||||
id: number;
|
||||
id?: number;
|
||||
|
||||
to: string;
|
||||
|
||||
amount: number;
|
||||
|
||||
memo: string;
|
||||
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
|
||||
this.to = '';
|
||||
this.to = "";
|
||||
this.amount = 0;
|
||||
this.memo = '';
|
||||
this.memo = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +117,7 @@ export class SendPageState {
|
|||
toaddrs: ToAddr[];
|
||||
|
||||
constructor() {
|
||||
this.fromaddr = '';
|
||||
this.fromaddr = "";
|
||||
this.toaddrs = [];
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +131,7 @@ export class ReceivePageState {
|
|||
rerenderKey: number;
|
||||
|
||||
constructor() {
|
||||
this.newAddress = '';
|
||||
this.newAddress = "";
|
||||
this.rerenderKey = 0;
|
||||
}
|
||||
}
|
||||
|
@ -119,34 +140,41 @@ export class RPCConfig {
|
|||
url: string;
|
||||
|
||||
constructor() {
|
||||
this.url = '';
|
||||
this.url = "";
|
||||
}
|
||||
}
|
||||
|
||||
export class Info {
|
||||
testnet: boolean;
|
||||
|
||||
latestBlock: number;
|
||||
|
||||
connections: number;
|
||||
|
||||
version: number;
|
||||
|
||||
version: string;
|
||||
verificationProgress: number;
|
||||
|
||||
currencyName: string;
|
||||
|
||||
solps: number;
|
||||
|
||||
zecPrice: number;
|
||||
|
||||
encrypted: boolean;
|
||||
|
||||
locked: boolean;
|
||||
|
||||
constructor() {
|
||||
this.testnet = false;
|
||||
this.latestBlock = 0;
|
||||
this.connections = 0;
|
||||
this.version = "";
|
||||
this.verificationProgress = 0;
|
||||
this.currencyName = "";
|
||||
this.solps = 0;
|
||||
this.zecPrice = 0;
|
||||
this.encrypted = false;
|
||||
this.locked = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerSelectState {
|
||||
modalIsOpen: boolean;
|
||||
constructor() {
|
||||
this.modalIsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordState {
|
||||
|
@ -158,23 +186,17 @@ export class PasswordState {
|
|||
|
||||
closeCallback: () => void;
|
||||
|
||||
helpText: string | null;
|
||||
helpText?: string | JSX.Element;
|
||||
|
||||
constructor() {
|
||||
this.showPassword = false;
|
||||
this.confirmNeeded = false;
|
||||
this.passwordCallback = null;
|
||||
this.closeCallback = null;
|
||||
this.helpText = null;
|
||||
this.passwordCallback = (p) => {};
|
||||
this.closeCallback = () => {};
|
||||
this.helpText = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectedCompanionApp {
|
||||
name: string;
|
||||
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export class SendProgress {
|
||||
sendInProgress: boolean;
|
||||
|
||||
|
@ -183,6 +205,13 @@ export class SendProgress {
|
|||
total: number;
|
||||
|
||||
etaSeconds: number;
|
||||
|
||||
constructor() {
|
||||
this.sendInProgress = false;
|
||||
this.progress = 0;
|
||||
this.total = 0;
|
||||
this.etaSeconds = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
|
@ -196,9 +225,9 @@ export default class AppState {
|
|||
|
||||
// A map type that contains address -> privatekey/viewkey mapping, for display on the receive page
|
||||
// This mapping is ephemeral, and will disappear when the user navigates away.
|
||||
addressPrivateKeys;
|
||||
addressPrivateKeys: Map<string, string>;
|
||||
|
||||
addressViewKeys;
|
||||
addressViewKeys: Map<string, string>;
|
||||
|
||||
// List of all addresses in the wallet, including change addresses and addresses
|
||||
// that don't have any balance or are unused
|
||||
|
@ -222,12 +251,37 @@ export default class AppState {
|
|||
// getinfo and getblockchaininfo result
|
||||
info: Info;
|
||||
|
||||
// Error modal data
|
||||
errorModalData: ErrorModalData;
|
||||
|
||||
// Server selection
|
||||
serverSelectState: ServerSelectState;
|
||||
|
||||
// Is the app rescanning?
|
||||
rescanning: boolean;
|
||||
|
||||
// Last sync ID
|
||||
prevSyncId: number;
|
||||
|
||||
// Callbacks for the password dialog box
|
||||
passwordState: PasswordState;
|
||||
|
||||
// The last seen connected companion app
|
||||
connectedCompanionApp: ConnectedCompanionApp;
|
||||
constructor() {
|
||||
this.totalBalance = new TotalBalance();
|
||||
this.addressesWithBalance = [];
|
||||
this.addressPrivateKeys = new Map();
|
||||
this.addressViewKeys = new Map();
|
||||
this.addresses = [];
|
||||
this.addressBook = [];
|
||||
this.transactions = [];
|
||||
this.errorModalData = new ErrorModalData();
|
||||
this.serverSelectState = new ServerSelectState();
|
||||
this.sendPageState = new SendPageState();
|
||||
this.receivePageState = new ReceivePageState();
|
||||
this.rpcConfig = new RPCConfig();
|
||||
this.info = new Info();
|
||||
this.rescanning = false;
|
||||
this.prevSyncId = -1;
|
||||
this.passwordState = new PasswordState();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import React from "react";
|
||||
import cstyles from "./Common.module.css";
|
||||
import Utils from "../utils/utils";
|
||||
|
||||
type BalanceBlockType = {
|
||||
zecValue: number;
|
||||
usdValue: string;
|
||||
currencyName: string;
|
||||
topLabel?: string;
|
||||
tooltip?: string;
|
||||
};
|
||||
export const BalanceBlockHighlight = ({ zecValue, usdValue, topLabel, currencyName, tooltip }: BalanceBlockType) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div style={{ padding: "1em" }} title={tooltip}>
|
||||
{topLabel && (
|
||||
<div className={[cstyles.small].join(" ")}>
|
||||
{topLabel}
|
||||
{tooltip && (
|
||||
<span>
|
||||
|
||||
<i className={[cstyles.green, "fas", "fa-info-circle"].join(" ")} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={[cstyles.highlight, cstyles.xlarge].join(" ")}>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(" ")}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small].join(" ")}>{usdValue}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BalanceBlock = ({ zecValue, usdValue, topLabel, currencyName }: BalanceBlockType) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div className={cstyles.padall}>
|
||||
<div className={[cstyles.small].join(" ")}>{topLabel}</div>
|
||||
<div className={[cstyles.highlight, cstyles.large].join(" ")}>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(" ")}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small].join(" ")}>{usdValue}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -3,45 +3,51 @@
|
|||
/* eslint-disable no-plusplus */
|
||||
/* eslint-disable react/prop-types */
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
AccordionItemButton,
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemPanel,
|
||||
Accordion
|
||||
} from 'react-accessible-accordion';
|
||||
import styles from './Dashboard.module.css';
|
||||
import cstyles from './Common.module.css';
|
||||
import { TotalBalance, Info, AddressBalance } from './AppState';
|
||||
import Utils from '../utils/utils';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import { BalanceBlockHighlight, BalanceBlock } from './BalanceBlocks';
|
||||
Accordion,
|
||||
} from "react-accessible-accordion";
|
||||
import styles from "./Dashboard.module.css";
|
||||
import cstyles from "./Common.module.css";
|
||||
import { TotalBalance, Info, AddressBalance } from "./AppState";
|
||||
import Utils from "../utils/utils";
|
||||
import ScrollPane from "./ScrollPane";
|
||||
import { BalanceBlockHighlight, BalanceBlock } from "./BalanceBlocks";
|
||||
|
||||
const AddressBalanceItem = ({ currencyName, zecPrice, item }) => {
|
||||
type AddressBalanceItemProps = {
|
||||
currencyName: string;
|
||||
zecPrice: number;
|
||||
item: AddressBalance;
|
||||
};
|
||||
|
||||
const AddressBalanceItem = ({ currencyName, zecPrice, item }: AddressBalanceItemProps) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(Math.abs(item.balance));
|
||||
|
||||
return (
|
||||
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(' ')} uuid={item.address}>
|
||||
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(" ")} uuid={item.address}>
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={cstyles.accordionHeader}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div>
|
||||
<div>{Utils.splitStringIntoChunks(item.address, 6).join(' ')}</div>
|
||||
<div>{Utils.splitStringIntoChunks(item.address, 6).join(" ")}</div>
|
||||
{item.containsPending && (
|
||||
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(' ')}>
|
||||
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(" ")}>
|
||||
Some transactions are pending. Balances may change.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={[styles.txamount, cstyles.right].join(' ')}>
|
||||
<div className={[styles.txamount, cstyles.right].join(" ")}>
|
||||
<div>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(" ")}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small, cstyles.padtopsmall].join(' ')}>
|
||||
<div className={[cstyles.sublight, cstyles.small, cstyles.padtopsmall].join(" ")}>
|
||||
{Utils.getZecToUsdString(zecPrice, Math.abs(item.balance))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,20 +60,20 @@ const AddressBalanceItem = ({ currencyName, zecPrice, item }) => {
|
|||
};
|
||||
|
||||
type Props = {
|
||||
totalBalance: TotalBalance,
|
||||
info: Info,
|
||||
addressesWithBalance: AddressBalance[]
|
||||
totalBalance: TotalBalance;
|
||||
info: Info;
|
||||
addressesWithBalance: AddressBalance[];
|
||||
};
|
||||
|
||||
export default class Home extends Component<Props> {
|
||||
render() {
|
||||
const { totalBalance, info, addressesWithBalance } = this.props;
|
||||
|
||||
const anyPending = addressesWithBalance && addressesWithBalance.find(i => i.containsPending);
|
||||
const anyPending = addressesWithBalance && addressesWithBalance.find((i) => i.containsPending);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.well, cstyles.containermargin].join(' ')}>
|
||||
<div className={[cstyles.well, cstyles.containermargin].join(" ")}>
|
||||
<div className={cstyles.balancebox}>
|
||||
<BalanceBlockHighlight
|
||||
zecValue={totalBalance.total}
|
||||
|
@ -89,7 +95,7 @@ export default class Home extends Component<Props> {
|
|||
</div>
|
||||
<div>
|
||||
{anyPending && (
|
||||
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(' ')}>
|
||||
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(" ")}>
|
||||
Some transactions are pending. Balances may change.
|
||||
</div>
|
||||
)}
|
||||
|
@ -99,18 +105,18 @@ export default class Home extends Component<Props> {
|
|||
<div className={styles.addressbalancecontainer}>
|
||||
<ScrollPane offsetHeight={200}>
|
||||
<div className={styles.addressbooklist}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(" ")}>
|
||||
<div>Address</div>
|
||||
<div>Balance</div>
|
||||
</div>
|
||||
{addressesWithBalance &&
|
||||
(addressesWithBalance.length === 0 ? (
|
||||
<div className={[cstyles.center, cstyles.sublight].join(' ')}>No Addresses with a balance</div>
|
||||
<div className={[cstyles.center, cstyles.sublight].join(" ")}>No Addresses with a balance</div>
|
||||
) : (
|
||||
<Accordion>
|
||||
{addressesWithBalance
|
||||
.filter(ab => ab.balance > 0)
|
||||
.map(ab => (
|
||||
.filter((ab) => ab.balance > 0)
|
||||
.map((ab) => (
|
||||
<AddressBalanceItem
|
||||
key={ab.address}
|
||||
item={ab}
|
|
@ -1,21 +1,22 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import Modal from 'react-modal';
|
||||
import React from 'react';
|
||||
import cstyles from './Common.module.css';
|
||||
import Modal from "react-modal";
|
||||
import React from "react";
|
||||
import cstyles from "./Common.module.css";
|
||||
|
||||
export class ErrorModalData {
|
||||
title: string;
|
||||
|
||||
body: string;
|
||||
|
||||
body: string | JSX.Element;
|
||||
modalIsOpen: boolean;
|
||||
closeModal?: () => void;
|
||||
|
||||
constructor() {
|
||||
this.modalIsOpen = false;
|
||||
this.title = "";
|
||||
this.body = "";
|
||||
}
|
||||
}
|
||||
|
||||
export const ErrorModal = ({ title, body, modalIsOpen, closeModal }) => {
|
||||
export const ErrorModal = ({ title, body, modalIsOpen, closeModal }: ErrorModalData) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
|
@ -23,14 +24,14 @@ export const ErrorModal = ({ title, body, modalIsOpen, closeModal }) => {
|
|||
className={cstyles.modal}
|
||||
overlayClassName={cstyles.modalOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'center' }}>
|
||||
<div className={[cstyles.verticalflex].join(" ")}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: "center" }}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cstyles.well}
|
||||
style={{ textAlign: 'center', wordBreak: 'break-all', maxHeight: '400px', overflowY: 'auto' }}
|
||||
style={{ textAlign: "center", wordBreak: "break-all", maxHeight: "400px", overflowY: "auto" }}
|
||||
>
|
||||
{body}
|
||||
</div>
|
|
@ -1,47 +1,39 @@
|
|||
/* eslint-disable radix */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, withRouter } from 'react-router';
|
||||
import { remote, ipcRenderer } from 'electron';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import Store from 'electron-store';
|
||||
import request from 'request';
|
||||
import progress from 'progress-stream';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import native from '../../native/index.node';
|
||||
import routes from '../constants/routes.json';
|
||||
import { RPCConfig, Info } from './AppState';
|
||||
import RPC from '../rpc';
|
||||
import cstyles from './Common.module.css';
|
||||
import styles from './LoadingScreen.module.css';
|
||||
import Logo from '../assets/img/logobig.png';
|
||||
import Utils from '../utils/utils';
|
||||
import React, { Component } from "react";
|
||||
import { Redirect, RouteComponentProps, withRouter } from "react-router";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import request from "request";
|
||||
import progress from "progress-stream";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { promisify } from "util";
|
||||
import native from "../native.node";
|
||||
import routes from "../constants/routes.json";
|
||||
import { RPCConfig, Info } from "./AppState";
|
||||
import RPC from "../rpc";
|
||||
import cstyles from "./Common.module.css";
|
||||
import styles from "./LoadingScreen.module.css";
|
||||
import Logo from "../assets/img/logobig.png";
|
||||
import Utils from "../utils/utils";
|
||||
|
||||
const { remote, ipcRenderer } = window.require("electron");
|
||||
const fs = window.require("fs");
|
||||
|
||||
const locateZcashParamsDir = () => {
|
||||
if (os.platform() === 'darwin') {
|
||||
return path.join(remote.app.getPath('appData'), 'ZcashParams');
|
||||
if (os.platform() === "darwin") {
|
||||
return path.join(remote.app.getPath("appData"), "ZcashParams");
|
||||
}
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
return path.join(remote.app.getPath('home'), '.zcash-params');
|
||||
if (os.platform() === "linux") {
|
||||
return path.join(remote.app.getPath("home"), ".zcash-params");
|
||||
}
|
||||
|
||||
return path.join(remote.app.getPath('appData'), 'ZcashParams');
|
||||
};
|
||||
|
||||
type Props = {
|
||||
setRPCConfig: (rpcConfig: RPCConfig) => void,
|
||||
rescanning: boolean,
|
||||
setRescanning: boolean => void,
|
||||
setInfo: (info: Info) => void,
|
||||
openServerSelectModal: () => void
|
||||
return path.join(remote.app.getPath("appData"), "ZcashParams");
|
||||
};
|
||||
|
||||
class LoadingScreenState {
|
||||
currentStatus: string;
|
||||
currentStatus: string | JSX.Element;
|
||||
|
||||
currentStatusIsError: boolean;
|
||||
|
||||
|
@ -62,21 +54,29 @@ class LoadingScreenState {
|
|||
getinfoRetryCount: number;
|
||||
|
||||
constructor() {
|
||||
this.currentStatus = 'Loading...';
|
||||
this.currentStatus = "Loading...";
|
||||
this.currentStatusIsError = false;
|
||||
this.loadingDone = false;
|
||||
this.rpcConfig = null;
|
||||
this.url = '';
|
||||
this.url = "";
|
||||
this.getinfoRetryCount = 0;
|
||||
this.walletScreen = 0;
|
||||
this.newWalletError = null;
|
||||
this.seed = '';
|
||||
this.seed = "";
|
||||
this.birthday = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingScreen extends Component<Props, LoadingScreenState> {
|
||||
constructor(props: Props) {
|
||||
type Props = {
|
||||
setRPCConfig: (rpcConfig: RPCConfig) => void;
|
||||
rescanning: boolean;
|
||||
prevSyncId: number;
|
||||
setRescanning: (rescan: boolean, prevSyncId: number) => void;
|
||||
setInfo: (info: Info) => void;
|
||||
openServerSelectModal: () => void;
|
||||
};
|
||||
class LoadingScreen extends Component<Props & RouteComponentProps, LoadingScreenState> {
|
||||
constructor(props: Props & RouteComponentProps) {
|
||||
super(props);
|
||||
|
||||
const state = new LoadingScreenState();
|
||||
|
@ -84,10 +84,10 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { rescanning } = this.props;
|
||||
const { rescanning, prevSyncId } = this.props;
|
||||
|
||||
if (rescanning) {
|
||||
this.runSyncStatusPoller();
|
||||
this.runSyncStatusPoller(prevSyncId);
|
||||
} else {
|
||||
(async () => {
|
||||
const success = await this.ensureZcashParams();
|
||||
|
@ -99,21 +99,22 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
}
|
||||
}
|
||||
|
||||
download = (url, dest, name, cb) => {
|
||||
download = (url: string, dest: string, name: string, cb: (msg: string) => void) => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
const sendReq = request.get(url);
|
||||
|
||||
// verify response code
|
||||
sendReq.on('response', response => {
|
||||
sendReq.on("response", (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
return cb(`Response status was ${response.statusCode}`);
|
||||
}
|
||||
|
||||
const totalSize = (parseInt(response.headers['content-length'], 10) / 1024 / 1024).toFixed(0);
|
||||
const len = response.headers["content-length"] || "";
|
||||
const totalSize = (parseInt(len, 10) / 1024 / 1024).toFixed(0);
|
||||
|
||||
const str = progress({ time: 1000 }, pgrs => {
|
||||
const str = progress({ time: 1000 }, (pgrs) => {
|
||||
this.setState({
|
||||
currentStatus: `Downloading ${name}... (${(pgrs.transferred / 1024 / 1024).toFixed(0)} MB / ${totalSize} MB)`
|
||||
currentStatus: `Downloading ${name}... (${(pgrs.transferred / 1024 / 1024).toFixed(0)} MB / ${totalSize} MB)`,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -121,18 +122,20 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
});
|
||||
|
||||
// close() is async, call cb after close completes
|
||||
file.on('finish', () => file.close(cb));
|
||||
file.on("finish", () => file.close());
|
||||
|
||||
// check for request errors
|
||||
sendReq.on('error', err => {
|
||||
fs.unlink(dest);
|
||||
return cb(err.message);
|
||||
sendReq.on("error", (err) => {
|
||||
fs.unlink(dest, () => {
|
||||
cb(err.message);
|
||||
});
|
||||
});
|
||||
|
||||
file.on('error', err => {
|
||||
file.on("error", (err: any) => {
|
||||
// Handle errors
|
||||
fs.unlink(dest); // Delete the file async. (But we don't check the result)
|
||||
return cb(err.message);
|
||||
fs.unlink(dest, () => {
|
||||
cb(err.message);
|
||||
}); // Delete the file async. (But we don't check the result)
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -145,8 +148,14 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
|
||||
// Check for the params
|
||||
const params = [
|
||||
{ name: 'sapling-output.params', url: 'https://params.zecwallet.co/params/sapling-output.params' },
|
||||
{ name: 'sapling-spend.params', url: 'https://params.zecwallet.co/params/sapling-spend.params' }
|
||||
{
|
||||
name: "sapling-output.params",
|
||||
url: "https://params.zecwallet.co/params/sapling-output.params",
|
||||
},
|
||||
{
|
||||
name: "sapling-spend.params",
|
||||
url: "https://params.zecwallet.co/params/sapling-spend.params",
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
|
@ -163,7 +172,9 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
await promisify(this.download)(p.url, fileName, p.name);
|
||||
} catch (err) {
|
||||
console.log(`error: ${err}`);
|
||||
this.setState({ currentStatus: `Error downloading ${p.name}. The error was: ${err}` });
|
||||
this.setState({
|
||||
currentStatus: `Error downloading ${p.name}. The error was: ${err}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -174,9 +185,9 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
|
||||
loadServerURI = () => {
|
||||
// Try to read the default server
|
||||
const store = new Store();
|
||||
let server = store.get('lightd/serveruri', Utils.V3_LIGHTWALLETD);
|
||||
|
||||
//const store = new Store<Record<string, string>>();
|
||||
//let server = store.get('lightd/serveruri', Utils.V3_LIGHTWALLETD);
|
||||
let server = Utils.V3_LIGHTWALLETD;
|
||||
// Automatically upgrade to v2 server if you had the previous v1 server.
|
||||
if (server === Utils.V1_LIGHTWALLETD || server === Utils.V2_LIGHTWALLETD) {
|
||||
server = Utils.V3_LIGHTWALLETD;
|
||||
|
@ -199,14 +210,14 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
this.setupExitHandler();
|
||||
|
||||
// Test to see if the wallet exists
|
||||
if (!native.litelib_wallet_exists('main')) {
|
||||
if (!native.litelib_wallet_exists("main")) {
|
||||
// Show the wallet creation screen
|
||||
this.setState({ walletScreen: 1 });
|
||||
} else {
|
||||
try {
|
||||
const result = native.litelib_initialize_existing(url);
|
||||
console.log(`Intialization: ${result}`);
|
||||
if (result !== 'OK') {
|
||||
if (result !== "OK") {
|
||||
this.setState({
|
||||
currentStatus: (
|
||||
<span>
|
||||
|
@ -215,7 +226,7 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
{result}
|
||||
</span>
|
||||
),
|
||||
currentStatusIsError: true
|
||||
currentStatusIsError: true,
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -223,7 +234,7 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
|
||||
this.getInfo();
|
||||
} catch (e) {
|
||||
console.log('Error initializing', e);
|
||||
console.log("Error initializing", e);
|
||||
this.setState({
|
||||
currentStatus: (
|
||||
<span>
|
||||
|
@ -232,7 +243,7 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
{`${e}`}
|
||||
</span>
|
||||
),
|
||||
currentStatusIsError: true
|
||||
currentStatusIsError: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -240,13 +251,12 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
|
||||
setupExitHandler = () => {
|
||||
// App is quitting, make sure to save the wallet properly.
|
||||
ipcRenderer.on('appquitting', () => {
|
||||
RPC.doSave();
|
||||
ipcRenderer.on("appquitting", () => {
|
||||
RPC.deinitialize();
|
||||
|
||||
// And reply that we're all done after 100ms, to allow cleanup of the rust stuff.
|
||||
setTimeout(() => {
|
||||
ipcRenderer.send('appquitdone');
|
||||
ipcRenderer.send("appquitdone");
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
@ -255,19 +265,22 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
// Try getting the info.
|
||||
try {
|
||||
// Do a sync at start
|
||||
this.setState({ currentStatus: 'Setting things up...' });
|
||||
this.setState({ currentStatus: "Setting things up..." });
|
||||
|
||||
// Grab the previous sync ID.
|
||||
const prevSyncId = JSON.parse(RPC.doSyncStatus()).sync_id;
|
||||
|
||||
// This will do the sync in another thread, so we have to check for sync status
|
||||
RPC.doSync();
|
||||
|
||||
this.runSyncStatusPoller();
|
||||
this.runSyncStatusPoller(prevSyncId);
|
||||
} catch (err) {
|
||||
// Not yet finished loading. So update the state, and setup the next refresh
|
||||
this.setState({ currentStatus: err });
|
||||
}
|
||||
}
|
||||
|
||||
runSyncStatusPoller = () => {
|
||||
runSyncStatusPoller = (prevSyncId: number) => {
|
||||
const me = this;
|
||||
|
||||
const { setRPCConfig, setInfo, setRescanning } = this.props;
|
||||
|
@ -279,24 +292,30 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
const poller = setInterval(() => {
|
||||
const syncstatus = RPC.doSyncStatus();
|
||||
|
||||
if (syncstatus.startsWith('Error')) {
|
||||
if (syncstatus.startsWith("Error")) {
|
||||
// Something went wrong
|
||||
this.setState({ currentStatus: syncstatus, currentStatusIsError: true });
|
||||
this.setState({
|
||||
currentStatus: syncstatus,
|
||||
currentStatusIsError: true,
|
||||
});
|
||||
|
||||
// And cancel the updater
|
||||
clearInterval(poller);
|
||||
} else {
|
||||
const ss = JSON.parse(syncstatus);
|
||||
console.log(ss);
|
||||
|
||||
if (ss.syncing === 'false') {
|
||||
if (ss.sync_id > prevSyncId && !ss.in_progress) {
|
||||
// First, save the wallet so we don't lose the just-synced data
|
||||
RPC.doSave();
|
||||
if (!ss.last_error) {
|
||||
RPC.doSave();
|
||||
}
|
||||
|
||||
// Set the info object, so the sidebar will show
|
||||
console.log(info);
|
||||
setInfo(info);
|
||||
|
||||
setRescanning(false);
|
||||
setRescanning(false, prevSyncId);
|
||||
|
||||
// Configure the RPC, which will setup the refresh
|
||||
const rpcConfig = new RPCConfig();
|
||||
|
@ -310,11 +329,14 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
me.setState({ loadingDone: true });
|
||||
} else {
|
||||
// Still syncing, grab the status and update the status
|
||||
const p = ss.synced_blocks;
|
||||
const t = ss.total_blocks;
|
||||
const currentStatus = `Syncing ${p} / ${t}`;
|
||||
const progress_blocks =
|
||||
(ss.witness_blocks + ss.synced_blocks + ss.trial_decryptions_blocks + ss.txn_scan_blocks) / 4;
|
||||
|
||||
me.setState({ currentStatus });
|
||||
if (progress_blocks && !isNaN(progress_blocks)) {
|
||||
const progress = ((progress_blocks * 100) / ss.total_blocks).toFixed(2);
|
||||
const currentStatus = `Syncing: ${progress}%`;
|
||||
me.setState({ currentStatus });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
@ -324,7 +346,7 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
const { url } = this.state;
|
||||
const result = native.litelib_initialize_new(url);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
if (result.startsWith("Error")) {
|
||||
this.setState({ newWalletError: result });
|
||||
} else {
|
||||
const r = JSON.parse(result);
|
||||
|
@ -342,17 +364,22 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
this.setState({ walletScreen: 3 });
|
||||
};
|
||||
|
||||
updateSeed = e => {
|
||||
updateSeed = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({ seed: e.target.value });
|
||||
};
|
||||
|
||||
updateBirthday = e => {
|
||||
this.setState({ birthday: e.target.value });
|
||||
updateBirthday = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ birthday: parseInt(e.target.value) });
|
||||
};
|
||||
|
||||
restoreWalletBack = () => {
|
||||
// Reset the seed and birthday and try again
|
||||
this.setState({ seed: '', birthday: 0, newWalletError: null, walletScreen: 3 });
|
||||
this.setState({
|
||||
seed: "",
|
||||
birthday: 0,
|
||||
newWalletError: null,
|
||||
walletScreen: 3,
|
||||
});
|
||||
};
|
||||
|
||||
doRestoreWallet = () => {
|
||||
|
@ -361,8 +388,8 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
|
||||
const allowOverwrite = true;
|
||||
|
||||
const result = native.litelib_initialize_new_from_phrase(url, seed, parseInt(birthday), allowOverwrite);
|
||||
if (result.startsWith('Error')) {
|
||||
const result = native.litelib_initialize_new_from_phrase(url, seed, birthday, allowOverwrite);
|
||||
if (result.startsWith("Error")) {
|
||||
this.setState({ newWalletError: result });
|
||||
} else {
|
||||
this.setState({ walletScreen: 0 });
|
||||
|
@ -371,25 +398,18 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
loadingDone,
|
||||
currentStatus,
|
||||
currentStatusIsError,
|
||||
walletScreen,
|
||||
newWalletError,
|
||||
seed,
|
||||
birthday
|
||||
} = this.state;
|
||||
const { loadingDone, currentStatus, currentStatusIsError, walletScreen, newWalletError, seed, birthday } =
|
||||
this.state;
|
||||
|
||||
const { openServerSelectModal } = this.props;
|
||||
|
||||
// If still loading, show the status
|
||||
if (!loadingDone) {
|
||||
return (
|
||||
<div className={[cstyles.verticalflex, cstyles.center, styles.loadingcontainer].join(' ')}>
|
||||
<div className={[cstyles.verticalflex, cstyles.center, styles.loadingcontainer].join(" ")}>
|
||||
{walletScreen === 0 && (
|
||||
<div>
|
||||
<div style={{ marginTop: '100px' }}>
|
||||
<div style={{ marginTop: "100px" }}>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div>{currentStatus}</div>
|
||||
|
@ -403,7 +423,10 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
className={cstyles.primarybutton}
|
||||
onClick={() => {
|
||||
this.setState({ walletScreen: 1 });
|
||||
this.setState({ currentStatus: '', currentStatusIsError: false });
|
||||
this.setState({
|
||||
currentStatus: "",
|
||||
currentStatusIsError: false,
|
||||
});
|
||||
this.restoreExistingWallet();
|
||||
}}
|
||||
>
|
||||
|
@ -419,9 +442,9 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
<div>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(" ")}>
|
||||
<div className={cstyles.verticalflex}>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Create A New Wallet</div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(" ")}>Create A New Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>
|
||||
Creates a new wallet with a new randomly generated seed phrase. Please save the seed phrase
|
||||
carefully, it’s the only way to restore your wallet.
|
||||
|
@ -432,8 +455,8 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(' ')}>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Restore Wallet From Seed</div>
|
||||
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(" ")}>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(" ")}>Restore Wallet From Seed</div>
|
||||
<div className={cstyles.padtopsmall}>
|
||||
If you already have a seed phrase, you can restore it to this wallet. This will rescan the
|
||||
blockchain for all transactions from the seed phrase.
|
||||
|
@ -453,11 +476,11 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
<div>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(" ")}>
|
||||
<div className={cstyles.verticalflex}>
|
||||
{newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Error Creating New Wallet</div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(" ")}>Error Creating New Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>There was an error creating a new wallet</div>
|
||||
<hr />
|
||||
<div className={cstyles.padtopsmall}>{newWalletError}</div>
|
||||
|
@ -467,7 +490,7 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
|
||||
{!newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Your New Wallet</div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(" ")}>Your New Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>
|
||||
This is your new wallet. Below is your seed phrase. PLEASE STORE IT CAREFULLY! The seed phrase
|
||||
is the only way to recover your funds and transactions.
|
||||
|
@ -492,11 +515,11 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
<div>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(" ")}>
|
||||
<div className={cstyles.verticalflex}>
|
||||
{newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Error Restoring Wallet</div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(" ")}>Error Restoring Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>There was an error restoring your seed phrase</div>
|
||||
<hr />
|
||||
<div className={cstyles.padtopsmall}>{newWalletError}</div>
|
||||
|
@ -511,25 +534,25 @@ class LoadingScreen extends Component<Props, LoadingScreenState> {
|
|||
|
||||
{!newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large].join(' ')}>Please enter your seed phrase</div>
|
||||
<TextareaAutosize className={cstyles.inputbox} value={seed} onChange={e => this.updateSeed(e)} />
|
||||
<div className={[cstyles.large].join(" ")}>Please enter your seed phrase</div>
|
||||
<TextareaAutosize
|
||||
className={cstyles.inputbox}
|
||||
value={seed}
|
||||
onChange={(e) => this.updateSeed(e)}
|
||||
/>
|
||||
|
||||
<div className={[cstyles.large, cstyles.margintoplarge].join(' ')}>
|
||||
<div className={[cstyles.large, cstyles.margintoplarge].join(" ")}>
|
||||
Wallet Birthday. If you don’t know this, it is OK to enter ‘0’
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
className={cstyles.inputbox}
|
||||
value={birthday}
|
||||
onChange={e => this.updateBirthday(e)}
|
||||
onChange={(e) => this.updateBirthday(e)}
|
||||
/>
|
||||
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button
|
||||
type="button"
|
||||
className={cstyles.primarybutton}
|
||||
onClick={() => this.doRestoreWallet(false)}
|
||||
>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={() => this.doRestoreWallet()}>
|
||||
Restore Wallet
|
||||
</button>
|
||||
</div>
|
|
@ -1,26 +1,26 @@
|
|||
// @flow
|
||||
import React, { PureComponent } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import cstyles from './Common.module.css';
|
||||
import React, { PureComponent } from "react";
|
||||
import Modal from "react-modal";
|
||||
import cstyles from "./Common.module.css";
|
||||
|
||||
type Props = {
|
||||
modalIsOpen: boolean,
|
||||
confirmNeeded: boolean,
|
||||
passwordCallback: (password: string) => void,
|
||||
closeCallback: () => void,
|
||||
helpText: string | null
|
||||
modalIsOpen: boolean;
|
||||
confirmNeeded: boolean;
|
||||
passwordCallback: (password: string) => void;
|
||||
closeCallback: () => void;
|
||||
helpText?: string | JSX.Element;
|
||||
};
|
||||
|
||||
type State = {
|
||||
password: string,
|
||||
confirmPassword: string
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
};
|
||||
|
||||
export default class PasswordModal extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { password: '', confirmPassword: '' };
|
||||
this.state = { password: "", confirmPassword: "" };
|
||||
}
|
||||
|
||||
enterButton = () => {
|
||||
|
@ -30,7 +30,7 @@ export default class PasswordModal extends PureComponent<Props, State> {
|
|||
passwordCallback(password);
|
||||
|
||||
// Clear the passwords
|
||||
this.setState({ password: '', confirmPassword: '' });
|
||||
this.setState({ password: "", confirmPassword: "" });
|
||||
};
|
||||
|
||||
closeButton = () => {
|
||||
|
@ -39,7 +39,7 @@ export default class PasswordModal extends PureComponent<Props, State> {
|
|||
closeCallback();
|
||||
|
||||
// Clear the passwords
|
||||
this.setState({ password: '', confirmPassword: '' });
|
||||
this.setState({ password: "", confirmPassword: "" });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -55,19 +55,19 @@ export default class PasswordModal extends PureComponent<Props, State> {
|
|||
className={cstyles.modal}
|
||||
overlayClassName={cstyles.modalOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'left' }}>
|
||||
<div className={[cstyles.verticalflex].join(" ")}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: "left" }}>
|
||||
{helpText && <span>{helpText}</span>}
|
||||
{!helpText && <span>Enter Wallet Password</span>}
|
||||
</div>
|
||||
|
||||
<div className={cstyles.well} style={{ textAlign: 'left' }}>
|
||||
<div className={cstyles.well} style={{ textAlign: "left" }}>
|
||||
<div className={cstyles.sublight}>Password</div>
|
||||
<input
|
||||
type="password"
|
||||
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(' ')}
|
||||
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(" ")}
|
||||
value={password}
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
onChange={(e) => this.setState({ password: e.target.value })}
|
||||
/>
|
||||
|
||||
{confirmNeeded && (
|
||||
|
@ -75,19 +75,19 @@ export default class PasswordModal extends PureComponent<Props, State> {
|
|||
<div className={cstyles.sublight}>Confirm Password</div>
|
||||
<input
|
||||
type="password"
|
||||
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(' ')}
|
||||
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(" ")}
|
||||
value={confirmPassword}
|
||||
onChange={e => this.setState({ confirmPassword: e.target.value })}
|
||||
onChange={(e) => this.setState({ confirmPassword: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
{!enabled && <div className={[cstyles.red].join(' ')}>Passwords do not match</div>}
|
||||
{!enabled && <div className={[cstyles.red].join(" ")}>Passwords do not match</div>}
|
||||
<button
|
||||
type="button"
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge].join(' ')}
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge].join(" ")}
|
||||
onClick={this.enterButton}
|
||||
disabled={!enabled}
|
||||
>
|
|
@ -1,21 +1,32 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import React, { Component, useState, useEffect } from 'react';
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
import React, { Component, useState, useEffect } from "react";
|
||||
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemButton,
|
||||
AccordionItemPanel
|
||||
} from 'react-accessible-accordion';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { shell, clipboard } from 'electron';
|
||||
import styles from './Receive.module.css';
|
||||
import cstyles from './Common.module.css';
|
||||
import Utils from '../utils/utils';
|
||||
import { AddressBalance, Info, ReceivePageState, AddressBookEntry } from './AppState';
|
||||
import ScrollPane from './ScrollPane';
|
||||
AccordionItemPanel,
|
||||
} from "react-accessible-accordion";
|
||||
import QRCode from "qrcode.react";
|
||||
import styles from "./Receive.module.css";
|
||||
import cstyles from "./Common.module.css";
|
||||
import Utils from "../utils/utils";
|
||||
import { AddressBalance, Info, ReceivePageState, AddressBookEntry } from "./AppState";
|
||||
import ScrollPane from "./ScrollPane";
|
||||
|
||||
const { shell, clipboard } = window.require("electron");
|
||||
|
||||
type AddressBlockProps = {
|
||||
addressBalance: AddressBalance;
|
||||
currencyName: string;
|
||||
zecPrice: number;
|
||||
privateKey?: string;
|
||||
viewKey?: string;
|
||||
label?: string;
|
||||
fetchAndSetSinglePrivKey: (k: string) => void;
|
||||
fetchAndSetSingleViewKey: (k: string) => void;
|
||||
};
|
||||
const AddressBlock = ({
|
||||
addressBalance,
|
||||
label,
|
||||
|
@ -24,12 +35,12 @@ const AddressBlock = ({
|
|||
privateKey,
|
||||
fetchAndSetSinglePrivKey,
|
||||
viewKey,
|
||||
fetchAndSetSingleViewKey
|
||||
}) => {
|
||||
fetchAndSetSingleViewKey,
|
||||
}: AddressBlockProps) => {
|
||||
const { address } = addressBalance;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [timerID, setTimerID] = useState(null);
|
||||
const [timerID, setTimerID] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -42,7 +53,7 @@ const AddressBlock = ({
|
|||
const balance = addressBalance.balance || 0;
|
||||
|
||||
const openAddress = () => {
|
||||
if (currencyName === 'TAZ') {
|
||||
if (currencyName === "TAZ") {
|
||||
shell.openExternal(`https://chain.so/address/ZECTEST/${address}`);
|
||||
} else {
|
||||
shell.openExternal(`https://zcha.in/accounts/${address}`);
|
||||
|
@ -50,50 +61,50 @@ const AddressBlock = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<AccordionItem key={copied} className={[cstyles.well, styles.receiveblock].join(' ')} uuid={address}>
|
||||
<AccordionItem key={copied ? 1 : 0} className={[cstyles.well, styles.receiveblock].join(" ")} uuid={address}>
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={cstyles.accordionHeader}>{address}</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel className={[styles.receiveDetail].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.verticalflex, cstyles.marginleft].join(' ')}>
|
||||
<AccordionItemPanel className={[styles.receiveDetail].join(" ")}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div className={[cstyles.verticalflex, cstyles.marginleft].join(" ")}>
|
||||
{label && (
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<div className={[cstyles.sublight].join(' ')}>Label</div>
|
||||
<div className={[cstyles.padtopsmall, cstyles.fixedfont].join(' ')}>{label}</div>
|
||||
<div className={[cstyles.sublight].join(" ")}>Label</div>
|
||||
<div className={[cstyles.padtopsmall, cstyles.fixedfont].join(" ")}>{label}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={[cstyles.sublight, cstyles.margintoplarge].join(' ')}>Funds</div>
|
||||
<div className={[cstyles.padtopsmall].join(' ')}>
|
||||
<div className={[cstyles.sublight, cstyles.margintoplarge].join(" ")}>Funds</div>
|
||||
<div className={[cstyles.padtopsmall].join(" ")}>
|
||||
{currencyName} {balance}
|
||||
</div>
|
||||
<div className={[cstyles.padtopsmall].join(' ')}>{Utils.getZecToUsdString(zecPrice, balance)}</div>
|
||||
<div className={[cstyles.padtopsmall].join(" ")}>{Utils.getZecToUsdString(zecPrice, balance)}</div>
|
||||
|
||||
<div className={[cstyles.margintoplarge, cstyles.breakword].join(' ')}>
|
||||
<div className={[cstyles.margintoplarge, cstyles.breakword].join(" ")}>
|
||||
{privateKey && (
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Private Key</div>
|
||||
<div className={[cstyles.sublight].join(" ")}>Private Key</div>
|
||||
<div
|
||||
className={[cstyles.breakword, cstyles.padtopsmall, cstyles.fixedfont, cstyles.flex].join(' ')}
|
||||
style={{ maxWidth: '600px' }}
|
||||
className={[cstyles.breakword, cstyles.padtopsmall, cstyles.fixedfont, cstyles.flex].join(" ")}
|
||||
style={{ maxWidth: "600px" }}
|
||||
>
|
||||
<QRCode value={privateKey} className={[styles.receiveQrcode].join(' ')} />
|
||||
<QRCode value={privateKey} className={[styles.receiveQrcode].join(" ")} />
|
||||
<div>{privateKey}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={[cstyles.margintoplarge, cstyles.breakword].join(' ')}>
|
||||
<div className={[cstyles.margintoplarge, cstyles.breakword].join(" ")}>
|
||||
{viewKey && (
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Viewing Key</div>
|
||||
<div className={[cstyles.sublight].join(" ")}>Viewing Key</div>
|
||||
<div
|
||||
className={[cstyles.breakword, cstyles.padtopsmall, cstyles.fixedfont, cstyles.flex].join(' ')}
|
||||
style={{ maxWidth: '600px' }}
|
||||
className={[cstyles.breakword, cstyles.padtopsmall, cstyles.fixedfont, cstyles.flex].join(" ")}
|
||||
style={{ maxWidth: "600px" }}
|
||||
>
|
||||
<QRCode value={viewKey} className={[styles.receiveQrcode].join(' ')} />
|
||||
<QRCode value={viewKey} className={[styles.receiveQrcode].join(" ")} />
|
||||
<div>{viewKey}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -102,7 +113,7 @@ const AddressBlock = ({
|
|||
|
||||
<div>
|
||||
<button
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge].join(' ')}
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge].join(" ")}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
clipboard.writeText(address);
|
||||
|
@ -114,7 +125,7 @@ const AddressBlock = ({
|
|||
</button>
|
||||
{!privateKey && (
|
||||
<button
|
||||
className={[cstyles.primarybutton].join(' ')}
|
||||
className={[cstyles.primarybutton].join(" ")}
|
||||
type="button"
|
||||
onClick={() => fetchAndSetSinglePrivKey(address)}
|
||||
>
|
||||
|
@ -124,7 +135,7 @@ const AddressBlock = ({
|
|||
|
||||
{Utils.isZaddr(address) && !viewKey && (
|
||||
<button
|
||||
className={[cstyles.primarybutton].join(' ')}
|
||||
className={[cstyles.primarybutton].join(" ")}
|
||||
type="button"
|
||||
onClick={() => fetchAndSetSingleViewKey(address)}
|
||||
>
|
||||
|
@ -133,14 +144,14 @@ const AddressBlock = ({
|
|||
)}
|
||||
|
||||
{Utils.isTransparent(address) && (
|
||||
<button className={[cstyles.primarybutton].join(' ')} type="button" onClick={() => openAddress()}>
|
||||
View on explorer <i className={['fas', 'fa-external-link-square-alt'].join(' ')} />
|
||||
<button className={[cstyles.primarybutton].join(" ")} type="button" onClick={() => openAddress()}>
|
||||
View on explorer <i className={["fas", "fa-external-link-square-alt"].join(" ")} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<QRCode value={address} className={[styles.receiveQrcode].join(' ')} />
|
||||
<QRCode value={address} className={[styles.receiveQrcode].join(" ")} />
|
||||
</div>
|
||||
</div>
|
||||
</AccordionItemPanel>
|
||||
|
@ -149,15 +160,17 @@ const AddressBlock = ({
|
|||
};
|
||||
|
||||
type Props = {
|
||||
addresses: string[],
|
||||
addressesWithBalance: AddressBalance[],
|
||||
addressBook: AddressBookEntry[],
|
||||
info: Info,
|
||||
receivePageState: ReceivePageState,
|
||||
fetchAndSetSinglePrivKey: string => void,
|
||||
fetchAndSetSingleViewKey: string => void,
|
||||
createNewAddress: boolean => void,
|
||||
rerenderKey: number
|
||||
addresses: string[];
|
||||
addressesWithBalance: AddressBalance[];
|
||||
addressBook: AddressBookEntry[];
|
||||
info: Info;
|
||||
addressPrivateKeys: Map<string, string>;
|
||||
addressViewKeys: Map<string, string>;
|
||||
receivePageState: ReceivePageState;
|
||||
fetchAndSetSinglePrivKey: (k: string) => void;
|
||||
fetchAndSetSingleViewKey: (k: string) => void;
|
||||
createNewAddress: (t: boolean) => void;
|
||||
rerenderKey: number;
|
||||
};
|
||||
|
||||
export default class Receive extends Component<Props> {
|
||||
|
@ -173,53 +186,53 @@ export default class Receive extends Component<Props> {
|
|||
fetchAndSetSinglePrivKey,
|
||||
fetchAndSetSingleViewKey,
|
||||
createNewAddress,
|
||||
rerenderKey
|
||||
rerenderKey,
|
||||
} = this.props;
|
||||
|
||||
// Convert the addressBalances into a map.
|
||||
const addressMap = addressesWithBalance.reduce((map, a) => {
|
||||
const addressMap: Map<string, number> = addressesWithBalance.reduce((m, a) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
map[a.address] = a.balance;
|
||||
return map;
|
||||
}, {});
|
||||
m.set(a.address, a.balance);
|
||||
return m;
|
||||
}, new Map());
|
||||
|
||||
const zaddrs = addresses
|
||||
.filter(a => Utils.isSapling(a))
|
||||
.filter((a) => Utils.isSapling(a))
|
||||
.slice(0, 100)
|
||||
.map(a => new AddressBalance(a, addressMap[a]));
|
||||
.map((a) => new AddressBalance(a, addressMap.get(a) || 0));
|
||||
|
||||
let defaultZaddr = zaddrs.length ? zaddrs[0].address : '';
|
||||
let defaultZaddr = zaddrs.length ? zaddrs[0].address : "";
|
||||
if (receivePageState && Utils.isSapling(receivePageState.newAddress)) {
|
||||
defaultZaddr = receivePageState.newAddress;
|
||||
|
||||
// move this address to the front, since the scrollbar will reset when we re-render
|
||||
zaddrs.sort((x, y) => {
|
||||
// eslint-disable-next-line prettier/prettier, no-nested-ternary
|
||||
return x.address === defaultZaddr ? -1 : y.address === defaultZaddr ? 1 : 0
|
||||
// eslint-disable-next-line, no-nested-ternary
|
||||
return x.address === defaultZaddr ? -1 : y.address === defaultZaddr ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
const taddrs = addresses
|
||||
.filter(a => Utils.isTransparent(a))
|
||||
.filter((a) => Utils.isTransparent(a))
|
||||
.slice(0, 100)
|
||||
.map(a => new AddressBalance(a, addressMap[a]));
|
||||
.map((a) => new AddressBalance(a, addressMap.get(a) || 0));
|
||||
|
||||
let defaultTaddr = taddrs.length ? taddrs[0].address : '';
|
||||
let defaultTaddr = taddrs.length ? taddrs[0].address : "";
|
||||
if (receivePageState && Utils.isTransparent(receivePageState.newAddress)) {
|
||||
defaultTaddr = receivePageState.newAddress;
|
||||
|
||||
// move this address to the front, since the scrollbar will reset when we re-render
|
||||
taddrs.sort((x, y) => {
|
||||
// eslint-disable-next-line prettier/prettier, no-nested-ternary
|
||||
return x.address === defaultTaddr ? -1 : y.address === defaultTaddr ? 1 : 0
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
return x.address === defaultTaddr ? -1 : y.address === defaultTaddr ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
const addressBookMap = addressBook.reduce((map, obj) => {
|
||||
const addressBookMap = addressBook.reduce((m, obj) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
map[obj.address] = obj.label;
|
||||
return map;
|
||||
}, {});
|
||||
m.set(obj.address, obj.label);
|
||||
return m;
|
||||
}, new Map());
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -234,24 +247,23 @@ export default class Receive extends Component<Props> {
|
|||
{/* Change the hardcoded height */}
|
||||
<ScrollPane offsetHeight={100}>
|
||||
<Accordion preExpanded={[defaultZaddr]}>
|
||||
{zaddrs.map(a => (
|
||||
{zaddrs.map((a) => (
|
||||
<AddressBlock
|
||||
key={a.address}
|
||||
addressBalance={a}
|
||||
currencyName={info.currencyName}
|
||||
label={addressBookMap[a.address]}
|
||||
label={addressBookMap.get(a.address)}
|
||||
zecPrice={info.zecPrice}
|
||||
privateKey={addressPrivateKeys[a.address]}
|
||||
viewKey={addressViewKeys[a.address]}
|
||||
privateKey={addressPrivateKeys.get(a.address)}
|
||||
viewKey={addressViewKeys.get(a.address)}
|
||||
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
|
||||
fetchAndSetSingleViewKey={fetchAndSetSingleViewKey}
|
||||
rerender={this.rerender}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<button
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(' ')}
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(" ")}
|
||||
onClick={() => createNewAddress(true)}
|
||||
type="button"
|
||||
>
|
||||
|
@ -264,23 +276,22 @@ export default class Receive extends Component<Props> {
|
|||
{/* Change the hardcoded height */}
|
||||
<ScrollPane offsetHeight={100}>
|
||||
<Accordion preExpanded={[defaultTaddr]}>
|
||||
{taddrs.map(a => (
|
||||
{taddrs.map((a) => (
|
||||
<AddressBlock
|
||||
key={a.address}
|
||||
addressBalance={a}
|
||||
currencyName={info.currencyName}
|
||||
zecPrice={info.zecPrice}
|
||||
privateKey={addressPrivateKeys[a.address]}
|
||||
viewKey={addressViewKeys[a.address]}
|
||||
privateKey={addressPrivateKeys.get(a.address)}
|
||||
viewKey={addressViewKeys.get(a.address)}
|
||||
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
|
||||
fetchAndSetSingleViewKey={fetchAndSetSingleViewKey}
|
||||
rerender={this.rerender}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<button
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(' ')}
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(" ")}
|
||||
type="button"
|
||||
onClick={() => createNewAddress(false)}
|
||||
>
|
|
@ -1,14 +1,13 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from "react";
|
||||
|
||||
type PaneState = {
|
||||
height: number
|
||||
height: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.node.isRequired,
|
||||
offsetHeight: number
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
offsetHeight: number;
|
||||
};
|
||||
|
||||
export default class ScrollPane extends Component<Props, PaneState> {
|
||||
|
@ -20,11 +19,11 @@ export default class ScrollPane extends Component<Props, PaneState> {
|
|||
|
||||
componentDidMount() {
|
||||
this.updateDimensions();
|
||||
window.addEventListener('resize', this.updateDimensions);
|
||||
window.addEventListener("resize", this.updateDimensions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.updateDimensions);
|
||||
window.removeEventListener("resize", this.updateDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,7 +40,7 @@ export default class ScrollPane extends Component<Props, PaneState> {
|
|||
const { height } = this.state;
|
||||
|
||||
return (
|
||||
<div className={className} style={{ overflowY: 'auto', overflowX: 'hidden', height }}>
|
||||
<div className={className} style={{ overflowY: "auto", overflowX: "hidden", height }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
|
@ -7,31 +7,45 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
// @flow
|
||||
import React, { PureComponent } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styles from './Send.module.css';
|
||||
import cstyles from './Common.module.css';
|
||||
import { ToAddr, AddressBalance, SendPageState, Info, AddressBookEntry, TotalBalance, SendProgress } from './AppState';
|
||||
import Utils from '../utils/utils';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import ArrowUpLight from '../assets/img/arrow_up_dark.png';
|
||||
import { BalanceBlockHighlight } from './BalanceBlocks';
|
||||
import RPC from '../rpc';
|
||||
import routes from '../constants/routes.json';
|
||||
import { parseZcashURI, ZcashURITarget } from '../utils/uris';
|
||||
import React, { PureComponent } from "react";
|
||||
import Modal from "react-modal";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import styles from "./Send.module.css";
|
||||
import cstyles from "./Common.module.css";
|
||||
import { ToAddr, AddressBalance, SendPageState, Info, AddressBookEntry, TotalBalance, SendProgress } from "./AppState";
|
||||
import Utils from "../utils/utils";
|
||||
import ScrollPane from "./ScrollPane";
|
||||
import ArrowUpLight from "../assets/img/arrow_up_dark.png";
|
||||
import { BalanceBlockHighlight } from "./BalanceBlocks";
|
||||
import RPC from "../rpc";
|
||||
import routes from "../constants/routes.json";
|
||||
import { parseZcashURI, ZcashURITarget } from "../utils/uris";
|
||||
|
||||
type OptionType = {
|
||||
value: string,
|
||||
label: string
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const Spacer = () => {
|
||||
return <div style={{ marginTop: '24px' }} />;
|
||||
return <div style={{ marginTop: "24px" }} />;
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
type ToAddrBoxProps = {
|
||||
toaddr: ToAddr;
|
||||
zecPrice: number;
|
||||
updateToField: (
|
||||
id: number,
|
||||
address: React.ChangeEvent<HTMLInputElement> | null,
|
||||
amount: React.ChangeEvent<HTMLInputElement> | null,
|
||||
memo: React.ChangeEvent<HTMLTextAreaElement> | string | null
|
||||
) => void;
|
||||
fromAddress: string;
|
||||
fromAmount: number;
|
||||
setSendButtonEnable: (sendButtonEnabled: boolean) => void;
|
||||
setMaxAmount: (id: number, total: number) => void;
|
||||
totalAmountAvailable: number;
|
||||
};
|
||||
const ToAddrBox = ({
|
||||
toaddr,
|
||||
zecPrice,
|
||||
|
@ -40,39 +54,37 @@ const ToAddrBox = ({
|
|||
fromAmount,
|
||||
setMaxAmount,
|
||||
setSendButtonEnable,
|
||||
totalAmountAvailable
|
||||
}) => {
|
||||
totalAmountAvailable,
|
||||
}: ToAddrBoxProps) => {
|
||||
const isMemoDisabled = !Utils.isZaddr(toaddr.to);
|
||||
|
||||
const addressIsValid = toaddr.to === '' || Utils.isZaddr(toaddr.to) || Utils.isTransparent(toaddr.to);
|
||||
const addressIsValid = toaddr.to === "" || Utils.isZaddr(toaddr.to) || Utils.isTransparent(toaddr.to);
|
||||
|
||||
let amountError = null;
|
||||
if (toaddr.amount) {
|
||||
if (toaddr.amount < 0) {
|
||||
amountError = 'Amount cannot be negative';
|
||||
amountError = "Amount cannot be negative";
|
||||
}
|
||||
if (toaddr.amount > fromAmount) {
|
||||
amountError = 'Amount Exceeds Balance';
|
||||
amountError = "Amount Exceeds Balance";
|
||||
}
|
||||
if (toaddr.amount < 10 ** -8) {
|
||||
amountError = 'Amount is too small';
|
||||
amountError = "Amount is too small";
|
||||
}
|
||||
const s = toaddr.amount.toString().split('.');
|
||||
const s = toaddr.amount.toString().split(".");
|
||||
if (s && s.length > 1 && s[1].length > 8) {
|
||||
amountError = 'Too Many Decimals';
|
||||
amountError = "Too Many Decimals";
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(toaddr.amount)) {
|
||||
// Amount is empty
|
||||
amountError = 'Amount cannot be empty';
|
||||
amountError = "Amount cannot be empty";
|
||||
}
|
||||
|
||||
let buttonstate;
|
||||
if (!addressIsValid || amountError || toaddr.to === '' || parseFloat(toaddr.amount) === 0 || fromAmount === 0) {
|
||||
let buttonstate = true;
|
||||
if (!addressIsValid || amountError || toaddr.to === "" || toaddr.amount === 0 || fromAmount === 0) {
|
||||
buttonstate = false;
|
||||
} else {
|
||||
buttonstate = true;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -86,19 +98,19 @@ const ToAddrBox = ({
|
|||
return;
|
||||
}
|
||||
|
||||
if (fromAddress) {
|
||||
if (fromAddress && toaddr.id) {
|
||||
updateToField(toaddr.id, null, null, `${toaddr.memo}\nReply-To:\n${fromAddress}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.well, cstyles.verticalflex].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.well, cstyles.verticalflex].join(" ")}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div className={cstyles.sublight}>To</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{addressIsValid ? (
|
||||
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
|
||||
<i className={[cstyles.green, "fas", "fa-check"].join(" ")} />
|
||||
) : (
|
||||
<span className={cstyles.red}>Invalid Address</span>
|
||||
)}
|
||||
|
@ -109,28 +121,28 @@ const ToAddrBox = ({
|
|||
placeholder="Z or T address"
|
||||
className={cstyles.inputbox}
|
||||
value={toaddr.to}
|
||||
onChange={e => updateToField(toaddr.id, e, null, null)}
|
||||
onChange={(e) => updateToField(toaddr.id as number, e, null, null)}
|
||||
/>
|
||||
<Spacer />
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div className={cstyles.sublight}>Amount</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{amountError ? <span className={cstyles.red}>{amountError}</span> : <span>{usdValue}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
className={cstyles.inputbox}
|
||||
value={isNaN(toaddr.amount) ? '' : toaddr.amount}
|
||||
onChange={e => updateToField(toaddr.id, null, e, null)}
|
||||
value={isNaN(toaddr.amount) ? "" : toaddr.amount}
|
||||
onChange={(e) => updateToField(toaddr.id as number, null, e, null)}
|
||||
/>
|
||||
<img
|
||||
className={styles.toaddrbutton}
|
||||
src={ArrowUpLight}
|
||||
alt="Max"
|
||||
onClick={() => setMaxAmount(toaddr.id, totalAmountAvailable)}
|
||||
onClick={() => setMaxAmount(toaddr.id as number, totalAmountAvailable)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -140,7 +152,7 @@ const ToAddrBox = ({
|
|||
|
||||
{!isMemoDisabled && (
|
||||
<div>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(" ")}>
|
||||
<div className={cstyles.sublight}>Memo</div>
|
||||
<div className={cstyles.validationerror}>{toaddr.memo.length}</div>
|
||||
</div>
|
||||
|
@ -148,9 +160,9 @@ const ToAddrBox = ({
|
|||
className={cstyles.inputbox}
|
||||
value={toaddr.memo}
|
||||
disabled={isMemoDisabled}
|
||||
onChange={e => updateToField(toaddr.id, null, null, e)}
|
||||
onChange={(e) => updateToField(toaddr.id as number, null, null, e)}
|
||||
/>
|
||||
<input type="checkbox" onChange={e => e.target.checked && addReplyTo()} />
|
||||
<input type="checkbox" onChange={(e) => e.target.checked && addReplyTo()} />
|
||||
Include Reply-To address
|
||||
</div>
|
||||
)}
|
||||
|
@ -161,13 +173,19 @@ const ToAddrBox = ({
|
|||
);
|
||||
};
|
||||
|
||||
function getSendManyJSON(sendPageState: SendPageState): [] {
|
||||
const json = sendPageState.toaddrs.flatMap(to => {
|
||||
const memo = to.memo || '';
|
||||
const amount = parseInt((parseFloat(to.amount) * 10 ** 8).toFixed(0));
|
||||
export type SendManyJson = {
|
||||
address: string;
|
||||
amount: number;
|
||||
memo?: string;
|
||||
};
|
||||
|
||||
if (memo === '') {
|
||||
return { address: to.to, amount };
|
||||
function getSendManyJSON(sendPageState: SendPageState): SendManyJson[] {
|
||||
const json = sendPageState.toaddrs.flatMap((to) => {
|
||||
const memo = to.memo || "";
|
||||
const amount = parseInt((to.amount * 10 ** 8).toFixed(0));
|
||||
|
||||
if (memo === "") {
|
||||
return { address: to.to, amount, memo: undefined };
|
||||
} else if (memo.length <= 512) {
|
||||
return { address: to.to, amount, memo };
|
||||
} else {
|
||||
|
@ -188,42 +206,57 @@ function getSendManyJSON(sendPageState: SendPageState): [] {
|
|||
}
|
||||
});
|
||||
|
||||
console.log('Sending:');
|
||||
console.log("Sending:");
|
||||
console.log(json);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
const ConfirmModalToAddr = ({ toaddr, info }) => {
|
||||
type ConfirmModalToAddrProps = {
|
||||
toaddr: ToAddr;
|
||||
info: Info;
|
||||
};
|
||||
const ConfirmModalToAddr = ({ toaddr, info }: ConfirmModalToAddrProps) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(toaddr.amount);
|
||||
|
||||
const memo: string = toaddr.memo ? toaddr.memo : '';
|
||||
const memo: string = toaddr.memo ? toaddr.memo : "";
|
||||
|
||||
return (
|
||||
<div className={cstyles.well}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.margintoplarge].join(' ')}>
|
||||
<div className={[styles.confirmModalAddress].join(' ')}>
|
||||
{Utils.splitStringIntoChunks(toaddr.to, 6).join(' ')}
|
||||
<div className={[cstyles.flexspacebetween, cstyles.margintoplarge].join(" ")}>
|
||||
<div className={[styles.confirmModalAddress].join(" ")}>
|
||||
{Utils.splitStringIntoChunks(toaddr.to, 6).join(" ")}
|
||||
</div>
|
||||
<div className={[cstyles.verticalflex, cstyles.right].join(' ')}>
|
||||
<div className={[cstyles.verticalflex, cstyles.right].join(" ")}>
|
||||
<div className={cstyles.large}>
|
||||
<div>
|
||||
<span>
|
||||
{info.currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, styles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
<span className={[cstyles.small, styles.zecsmallpart].join(" ")}>{smallPart}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>{Utils.getZecToUsdString(info.zecPrice, toaddr.amount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.breakword, cstyles.memodiv].join(' ')}>{memo}</div>
|
||||
<div className={[cstyles.sublight, cstyles.breakword, cstyles.memodiv].join(" ")}>{memo}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Internal because we're using withRouter just below
|
||||
const ConfirmModalInternal = ({
|
||||
type ConfirmModalProps = {
|
||||
sendPageState: SendPageState;
|
||||
info: Info;
|
||||
sendTransaction: (sendJson: SendManyJson[], setSendProgress: (p?: SendProgress) => void) => Promise<string>;
|
||||
clearToAddrs: () => void;
|
||||
closeModal: () => void;
|
||||
modalIsOpen: boolean;
|
||||
openErrorModal: (title: string, body: string) => void;
|
||||
openPasswordAndUnlockIfNeeded: (successCallback: () => void | Promise<void>) => void;
|
||||
};
|
||||
|
||||
const ConfirmModalInternal: React.FC<RouteComponentProps & ConfirmModalProps> = ({
|
||||
sendPageState,
|
||||
info,
|
||||
sendTransaction,
|
||||
|
@ -232,10 +265,10 @@ const ConfirmModalInternal = ({
|
|||
modalIsOpen,
|
||||
openErrorModal,
|
||||
openPasswordAndUnlockIfNeeded,
|
||||
history
|
||||
history,
|
||||
}) => {
|
||||
const defaultFee = RPC.getDefaultFee();
|
||||
const sendingTotal = sendPageState.toaddrs.reduce((s, t) => parseFloat(s) + parseFloat(t.amount), 0.0) + defaultFee;
|
||||
const sendingTotal = sendPageState.toaddrs.reduce((s, t) => s + t.amount, 0.0) + defaultFee;
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(sendingTotal);
|
||||
|
||||
const sendButton = () => {
|
||||
|
@ -244,8 +277,8 @@ const ConfirmModalInternal = ({
|
|||
|
||||
// This will be replaced by either a success TXID or error message that the user
|
||||
// has to close manually.
|
||||
openErrorModal('Computing Transaction', 'Please wait...This could take a while');
|
||||
const setSendProgress = (progress: SendProgress) => {
|
||||
openErrorModal("Computing Transaction", "Please wait...This could take a while");
|
||||
const setSendProgress = (progress?: SendProgress) => {
|
||||
if (progress && progress.sendInProgress) {
|
||||
openErrorModal(
|
||||
`Computing Transaction`,
|
||||
|
@ -260,14 +293,14 @@ const ConfirmModalInternal = ({
|
|||
// Then send the Tx async
|
||||
(async () => {
|
||||
const sendJson = getSendManyJSON(sendPageState);
|
||||
let txid = '';
|
||||
let txid = "";
|
||||
|
||||
try {
|
||||
txid = await sendTransaction(sendJson, setSendProgress);
|
||||
console.log(txid);
|
||||
|
||||
openErrorModal(
|
||||
'Successfully Broadcast Transaction',
|
||||
"Successfully Broadcast Transaction",
|
||||
`Transaction was successfully broadcast.\nTXID: ${txid}`
|
||||
);
|
||||
|
||||
|
@ -277,7 +310,7 @@ const ConfirmModalInternal = ({
|
|||
history.push(routes.DASHBOARD);
|
||||
} catch (err) {
|
||||
// If there was an error, show the error modal
|
||||
openErrorModal('Error Sending Transaction', `${err}`);
|
||||
openErrorModal("Error Sending Transaction", `${err}`);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
@ -291,8 +324,8 @@ const ConfirmModalInternal = ({
|
|||
className={styles.confirmModal}
|
||||
overlayClassName={styles.confirmOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={[cstyles.marginbottomlarge, cstyles.center].join(' ')}>Confirm Transaction</div>
|
||||
<div className={[cstyles.verticalflex].join(" ")}>
|
||||
<div className={[cstyles.marginbottomlarge, cstyles.center].join(" ")}>Confirm Transaction</div>
|
||||
<div className={cstyles.flex}>
|
||||
<div
|
||||
className={[
|
||||
|
@ -300,16 +333,16 @@ const ConfirmModalInternal = ({
|
|||
cstyles.xlarge,
|
||||
cstyles.flexspacebetween,
|
||||
cstyles.well,
|
||||
cstyles.maxwidth
|
||||
].join(' ')}
|
||||
cstyles.maxwidth,
|
||||
].join(" ")}
|
||||
>
|
||||
<div>Total</div>
|
||||
<div className={[cstyles.right, cstyles.verticalflex].join(' ')}>
|
||||
<div className={[cstyles.right, cstyles.verticalflex].join(" ")}>
|
||||
<div>
|
||||
<span>
|
||||
{info.currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, styles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
<span className={[cstyles.small, styles.zecsmallpart].join(" ")}>{smallPart}</span>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.normal}>{Utils.getZecToUsdString(info.zecPrice, sendingTotal)}</div>
|
||||
|
@ -318,12 +351,12 @@ const ConfirmModalInternal = ({
|
|||
</div>
|
||||
|
||||
<ScrollPane offsetHeight={400}>
|
||||
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(' ')}>
|
||||
{sendPageState.toaddrs.map(t => (
|
||||
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(" ")}>
|
||||
{sendPageState.toaddrs.map((t) => (
|
||||
<ConfirmModalToAddr key={t.to} toaddr={t} info={info} />
|
||||
))}
|
||||
</div>
|
||||
<ConfirmModalToAddr toaddr={{ to: 'Fee', amount: defaultFee, memo: null }} info={info} />
|
||||
<ConfirmModalToAddr toaddr={{ to: "Fee", amount: defaultFee, memo: "" }} info={info} />
|
||||
</ScrollPane>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
|
@ -342,16 +375,16 @@ const ConfirmModalInternal = ({
|
|||
const ConfirmModal = withRouter(ConfirmModalInternal);
|
||||
|
||||
type Props = {
|
||||
addresses: string[],
|
||||
totalBalance: TotalBalance,
|
||||
addressBook: AddressBookEntry[],
|
||||
sendPageState: SendPageState,
|
||||
setSendTo: (targets: ZcashURITarget[] | ZcashURITarget) => void,
|
||||
sendTransaction: (sendJson: [], setSendProgress: (SendProgress) => void) => string,
|
||||
setSendPageState: (sendPageState: SendPageState) => void,
|
||||
openErrorModal: (title: string, body: string) => void,
|
||||
info: Info,
|
||||
openPasswordAndUnlockIfNeeded: (successCallback: () => void) => void
|
||||
addresses: string[];
|
||||
totalBalance: TotalBalance;
|
||||
addressBook: AddressBookEntry[];
|
||||
sendPageState: SendPageState;
|
||||
setSendTo: (targets: ZcashURITarget[] | ZcashURITarget) => void;
|
||||
sendTransaction: (sendJson: SendManyJson[], setSendProgress: (p?: SendProgress) => void) => Promise<string>;
|
||||
setSendPageState: (sendPageState: SendPageState) => void;
|
||||
openErrorModal: (title: string, body: string) => void;
|
||||
info: Info;
|
||||
openPasswordAndUnlockIfNeeded: (successCallback: () => void) => void;
|
||||
};
|
||||
|
||||
class SendState {
|
||||
|
@ -407,12 +440,17 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
setSendPageState(newState);
|
||||
};
|
||||
|
||||
updateToField = (id: number, address: Event | null, amount: Event | null, memo: Event | string | null) => {
|
||||
updateToField = (
|
||||
id: number,
|
||||
address: React.ChangeEvent<HTMLInputElement> | null,
|
||||
amount: React.ChangeEvent<HTMLInputElement> | null,
|
||||
memo: React.ChangeEvent<HTMLTextAreaElement> | string | null
|
||||
) => {
|
||||
const { sendPageState, setSendPageState, setSendTo } = this.props;
|
||||
|
||||
const newToAddrs = sendPageState.toaddrs.slice(0);
|
||||
// Find the correct toAddr
|
||||
const toAddr = newToAddrs.find(a => a.id === id);
|
||||
const toAddr = newToAddrs.find((a) => a.id === id) as ToAddr;
|
||||
if (address) {
|
||||
// First, check if this is a URI
|
||||
// $FlowFixMe
|
||||
|
@ -422,8 +460,7 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
toAddr.to = address.target.value.replace(/ /g, ''); // Remove spaces
|
||||
toAddr.to = address.target.value.replace(/ /g, ""); // Remove spaces
|
||||
}
|
||||
|
||||
if (amount) {
|
||||
|
@ -438,7 +475,7 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
}
|
||||
|
||||
if (memo) {
|
||||
if (typeof memo === 'string') {
|
||||
if (typeof memo === "string") {
|
||||
toAddr.memo = memo;
|
||||
} else {
|
||||
// $FlowFixMe
|
||||
|
@ -459,18 +496,16 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
|
||||
const newToAddrs = sendPageState.toaddrs.slice(0);
|
||||
|
||||
let totalOtherAmount: number = newToAddrs
|
||||
.filter(a => a.id !== id)
|
||||
.reduce((s, a) => parseFloat(s) + parseFloat(a.amount), 0);
|
||||
let totalOtherAmount: number = newToAddrs.filter((a) => a.id !== id).reduce((s, a) => s + a.amount, 0);
|
||||
|
||||
// Add Fee
|
||||
totalOtherAmount += RPC.getDefaultFee();
|
||||
|
||||
// Find the correct toAddr
|
||||
const toAddr = newToAddrs.find(a => a.id === id);
|
||||
const toAddr = newToAddrs.find((a) => a.id === id) as ToAddr;
|
||||
toAddr.amount = total - totalOtherAmount;
|
||||
if (toAddr.amount < 0) toAddr.amount = 0;
|
||||
toAddr.amount = Utils.maxPrecisionTrimmed(toAddr.amount);
|
||||
//toAddr.amount = Utils.maxPrecisionTrimmed(toAddr.amount);
|
||||
|
||||
// Create the new state object
|
||||
const newState = new SendPageState();
|
||||
|
@ -494,7 +529,7 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
|
||||
getBalanceForAddress = (addr: string, addressesWithBalance: AddressBalance[]): number => {
|
||||
// Find the addr in addressesWithBalance
|
||||
const addressBalance: AddressBalance = addressesWithBalance.find(ab => ab.address === addr);
|
||||
const addressBalance = addressesWithBalance.find((ab) => ab.address === addr) as AddressBalance;
|
||||
|
||||
if (!addressBalance) {
|
||||
return 0;
|
||||
|
@ -506,8 +541,8 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
getLabelForFromAddress = (addr: string, addressesWithBalance: AddressBalance[], currencyName: string) => {
|
||||
// Find the addr in addressesWithBalance
|
||||
const { addressBook } = this.props;
|
||||
const label = addressBook.find(ab => ab.address === addr);
|
||||
const labelStr = label ? ` [ ${label.label} ]` : '';
|
||||
const label = addressBook.find((ab) => ab.address === addr);
|
||||
const labelStr = label ? ` [ ${label.label} ]` : "";
|
||||
|
||||
const balance = this.getBalanceForAddress(addr, addressesWithBalance);
|
||||
|
||||
|
@ -523,24 +558,24 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
info,
|
||||
totalBalance,
|
||||
openErrorModal,
|
||||
openPasswordAndUnlockIfNeeded
|
||||
openPasswordAndUnlockIfNeeded,
|
||||
} = this.props;
|
||||
|
||||
const totalAmountAvailable = totalBalance.transparent + totalBalance.spendablePrivate;
|
||||
const fromaddr = addresses.find(a => Utils.isSapling(a));
|
||||
const fromaddr = addresses.find((a) => Utils.isSapling(a)) as string;
|
||||
|
||||
// If there are unverified funds, then show a tooltip
|
||||
let tooltip: string = '';
|
||||
let tooltip: string = "";
|
||||
if (totalBalance.unverifiedPrivate) {
|
||||
tooltip = `Waiting for confirmation of ZEC ${totalBalance.unverifiedPrivate} with 5 blocks (approx 6 minutes)`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Send</div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(" ")}>Send</div>
|
||||
|
||||
<div className={styles.sendcontainer}>
|
||||
<div className={[cstyles.well, cstyles.balancebox, cstyles.containermargin].join(' ')}>
|
||||
<div className={[cstyles.well, cstyles.balancebox, cstyles.containermargin].join(" ")}>
|
||||
<BalanceBlockHighlight
|
||||
topLabel="Spendable Funds"
|
||||
zecValue={totalAmountAvailable}
|
||||
|
@ -557,7 +592,7 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
</div>
|
||||
|
||||
<ScrollPane className={cstyles.containermargin} offsetHeight={320}>
|
||||
{sendPageState.toaddrs.map(toaddr => {
|
||||
{sendPageState.toaddrs.map((toaddr) => {
|
||||
return (
|
||||
<ToAddrBox
|
||||
key={toaddr.id}
|
||||
|
@ -572,9 +607,9 @@ export default class Send extends PureComponent<Props, SendState> {
|
|||
/>
|
||||
);
|
||||
})}
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<button type="button" onClick={this.addToAddr}>
|
||||
<i className={['fas', 'fa-plus'].join(' ')} />
|
||||
<i className={["fas", "fa-plus"].join(" ")} />
|
||||
</button>
|
||||
</div>
|
||||
</ScrollPane>
|