diff --git a/.arclint b/.arclint
index 661e856a4..707da7f5e 100644
--- a/.arclint
+++ b/.arclint
@@ -1,303 +1,304 @@
{
"linters": {
"generated": {
"type": "generated"
},
"clang-format": {
"type": "clang-format",
"version": ">=10.0",
"bin": ["clang-format-10", "clang-format"],
"include": "(^src/.*\\.(h|c|cpp|mm)$)",
"exclude": [
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"autopep8": {
"type": "autopep8",
"version": ">=1.3.4",
"include": "(\\.py$)",
"exclude": [
"(^contrib/gitian-builder/)",
"(^contrib/apple-sdk-tools/)"
],
"flags": [
"--aggressive",
"--ignore=W503,W504"
]
},
"flake8": {
"type": "flake8",
"version": ">=3.0",
"include": "(\\.py$)",
"exclude": [
"(^contrib/gitian-builder/)",
"(^contrib/apple-sdk-tools/)"
],
"flags": [
"--ignore=E303,E305,E501,E704,W503,W504"
]
},
"lint-format-strings": {
"type": "lint-format-strings",
"include": "(^src/.*\\.(h|c|cpp)$)",
"exclude": [
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)",
"(^src/test/fuzz/strprintf.cpp$)"
]
},
"check-doc": {
"type": "check-doc",
"include": "(^src/.*\\.(h|c|cpp)$)"
},
"lint-tests": {
"type": "lint-tests",
"include": "(^src/(seeder/|rpc/|wallet/)?test/.*\\.(cpp)$)"
},
"lint-python-format": {
"type": "lint-python-format",
"include": "(\\.py$)",
"exclude": [
"(^test/lint/lint-python-format\\.py$)",
"(^contrib/gitian-builder/)",
"(^contrib/apple-sdk-tools/)"
]
},
"phpcs": {
"type": "phpcs",
"include": "(\\.php$)",
"exclude": [
"(^arcanist/__phutil_library_.+\\.php$)"
],
"phpcs.standard": "arcanist/phpcs.xml"
},
"lint-locale-dependence": {
"type": "lint-locale-dependence",
"include": "(^src/.*\\.(h|cpp)$)",
"exclude": [
"(^src/(crypto/ctaes/|leveldb/|secp256k1/|tinyformat.h|univalue/))",
"(^src/bench/nanobench.h$)"
]
},
"lint-cheader": {
"type": "lint-cheader",
"include": "(^src/.*\\.(h|cpp)$)",
"exclude": [
"(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"spelling": {
"type": "spelling",
"exclude": [
"(^build-aux/m4/)",
"(^depends/)",
"(^doc/release-notes/)",
"(^contrib/gitian-builder/)",
"(^src/(qt/locale|secp256k1|univalue|leveldb)/)",
"(^test/lint/dictionary/)"
],
"spelling.dictionaries": [
"test/lint/dictionary/english.json"
]
},
"lint-assert-with-side-effects": {
"type": "lint-assert-with-side-effects",
"include": "(^src/.*\\.(h|cpp)$)",
"exclude": [
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"lint-include-quotes": {
"type": "lint-include-quotes",
"include": "(^src/.*\\.(h|cpp)$)",
"exclude": [
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"lint-include-guard": {
"type": "lint-include-guard",
"include": "(^src/.*\\.h$)",
"exclude": [
"(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)",
"(^src/tinyformat.h$)"
]
},
"lint-include-source": {
"type": "lint-include-source",
"include": "(^src/.*\\.(h|c|cpp)$)",
"exclude": [
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"lint-stdint": {
"type": "lint-stdint",
"include": "(^src/.*\\.(h|c|cpp)$)",
"exclude": [
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)",
"(^src/compat/assumptions.h$)"
]
},
"lint-source-filename": {
"type": "lint-source-filename",
"include": "(^src/.*\\.(h|c|cpp)$)",
"exclude": [
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"lint-boost-dependencies": {
"type": "lint-boost-dependencies",
"include": "(^src/.*\\.(h|cpp)$)"
},
"check-rpc-mappings": {
"type": "check-rpc-mappings",
"include": "(^src/(rpc/|wallet/rpc).*\\.cpp$)"
},
"lint-python-encoding": {
"type": "lint-python-encoding",
"include": "(\\.py$)",
"exclude": [
"(^contrib/gitian-builder/)",
"(^contrib/apple-sdk-tools/)"
]
},
"lint-python-shebang": {
"type": "lint-python-shebang",
"include": "(\\.py$)",
"exclude": [
"(__init__\\.py$)",
"(^contrib/gitian-builder/)",
"(^contrib/apple-sdk-tools/)"
]
},
"lint-bash-shebang": {
"type": "lint-bash-shebang",
"include": "(\\.sh$)",
"exclude": [
"(^contrib/gitian-builder/)"
]
},
"shellcheck": {
"type": "shellcheck",
"version": ">=0.7.0",
"flags": [
"--external-sources",
"--source-path=SCRIPTDIR"
],
"include": "(\\.sh$)",
"exclude": [
"(^contrib/gitian-builder/)",
"(^src/(secp256k1|univalue)/)"
]
},
"lint-shell-locale": {
"type": "lint-shell-locale",
"include": "(\\.sh$)",
"exclude": [
"(^contrib/gitian-builder/)",
"(^src/(secp256k1|univalue)/)",
"(^cmake/utils/log-and-print-on-failure.sh)"
]
},
"lint-cpp-void-parameters": {
"type": "lint-cpp-void-parameters",
"include": "(^src/.*\\.(h|cpp)$)",
"exclude": [
"(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)",
"(^src/compat/glibc_compat.cpp$)"
]
},
"lint-logs": {
"type": "lint-logs",
"include": "(^src/.*\\.(h|cpp)$)"
},
"lint-qt": {
"type": "lint-qt",
"include": "(^src/qt/.*\\.(h|cpp)$)",
"exclude": [
"(^src/qt/(locale|forms|res)/)"
]
},
"lint-doxygen": {
"type": "lint-doxygen",
"include": "(^src/.*\\.(h|c|cpp)$)",
"exclude": [
"(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"lint-whitespace": {
"type": "lint-whitespace",
"include": "(\\.(ac|am|cmake|conf|in|include|json|m4|md|openrc|php|pl|sh|txt|yml)$)",
"exclude": [
"(^contrib/gitian-builder/)",
"(^src/(secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"lint-cppcheck": {
"type": "lint-cppcheck",
"include": "(^src/.*\\.(h|c|cpp)$)",
"exclude": [
"(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)",
"(^src/bench/nanobench.h$)"
]
},
"yamllint": {
"type": "yamllint",
"include": "(\\.(yml|yaml)$)",
"exclude": "(^src/(secp256k1|univalue|leveldb)/)"
},
"lint-check-nonfatal": {
"type": "lint-check-nonfatal",
"include": [
"(^src/rpc/.*\\.(h|c|cpp)$)",
"(^src/wallet/rpc*.*\\.(h|c|cpp)$)"
],
"exclude": "(^src/rpc/server.cpp)"
},
"lint-markdown": {
"type": "lint-markdown",
"include": [
"(\\.md$)"
],
"exclude": "(^contrib/gitian-builder/)"
},
"lint-python-mypy": {
"type": "lint-python-mypy",
"version": ">=0.780",
"include": "(\\.py$)",
"exclude": "(^contrib/)",
"flags": [
"--ignore-missing-imports"
]
},
"lint-python-mutable-default": {
"type": "lint-python-mutable-default",
"include": "(\\.py$)",
"exclude": [
"(^contrib/gitian-builder/)",
"(^contrib/apple-sdk-tools/)"
]
},
"prettier": {
"type": "prettier",
+ "version":">=2.4.1",
"include": "(^web/.*\\.(css|html|js|json|jsx|md|scss|ts|tsx)$)",
"exclude": "(^web/.*/translations/.*\\.json$)"
},
"lint-python-isort": {
"type": "lint-python-isort",
"version": ">=5.6.4",
"include": "(\\.py$)",
"exclude": "(^contrib/)"
}
}
}
diff --git a/web/cashtab-components/src/atoms/Button/Button.stories.tsx b/web/cashtab-components/src/atoms/Button/Button.stories.tsx
index ddda61472..a61c9f4a1 100644
--- a/web/cashtab-components/src/atoms/Button/Button.stories.tsx
+++ b/web/cashtab-components/src/atoms/Button/Button.stories.tsx
@@ -1,58 +1,56 @@
// @flow
import React from 'react';
import { storiesOf } from '@storybook/react/dist/client/preview';
import Button from './Button';
import Text from '../Text';
const ButtonText = 'CashTab Pay';
storiesOf('Button', module)
.add(
'default',
() => (
),
{
- notes:
- 'Button is a stateful controlled component which is the primary visual indicator for the badger payment process',
+ notes: 'Button is a stateful controlled component which is the primary visual indicator for the badger payment process',
},
)
.add(
'payment pending',
() => (
),
{
notes: 'Awaiting a confirmation or cancellation of Badger popup',
},
)
.add(
'payment complete',
() => (
),
{
notes: 'Payment received, at least on the front-end',
},
)
.add(
'install prompt',
() => (
),
{
- notes:
- 'CashTab extension not installed, prompt user to install CashTab',
+ notes: 'CashTab extension not installed, prompt user to install CashTab',
},
);
diff --git a/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.stories.tsx b/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.stories.tsx
index 61c1721e1..ab2682e65 100644
--- a/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.stories.tsx
+++ b/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.stories.tsx
@@ -1,86 +1,84 @@
import React from 'react';
import { storiesOf } from '@storybook/react/dist/client/preview';
import { text, number } from '@storybook/addon-knobs';
import ButtonQR from './ButtonQR';
import Text from '../Text';
const ButtonText = 'CashTab Pay';
storiesOf('ButtonQR', module)
.add(
'default - all knobs',
() => (
{ButtonText}
),
{
- notes:
- 'Button is a stateful controlled component which is the primary visual indicator for the Cashtab payment process',
+ notes: 'Button is a stateful controlled component which is the primary visual indicator for the Cashtab payment process',
},
)
.add(
'payment pending',
() => (
{ButtonText}
),
{
notes: 'Awaiting a confirmation or cancellation of Cashtab popup',
},
)
.add(
'payment complete',
() => (
{ButtonText}
),
{
notes: 'Payment received, at least on the front-end',
},
)
.add(
'install prompt',
() => (
{ButtonText}
),
{
- notes:
- 'Cashtab plugin not installed, prompt user to install Cashtab',
+ notes: 'Cashtab plugin not installed, prompt user to install Cashtab',
},
);
diff --git a/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.tsx b/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.tsx
index ab703111d..ac3414128 100644
--- a/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.tsx
+++ b/web/cashtab-components/src/atoms/ButtonQR/ButtonQR.tsx
@@ -1,203 +1,198 @@
import * as React from 'react';
import styled, { css, keyframes } from 'styled-components';
import QRCode from 'qrcode.react';
import type { ButtonStates } from '../../hoc/CashtabBase';
import colors from '../../styles/colors';
import CheckSVG from '../../images/CheckSVG';
import LoadSVG from '../../images/LoadSVG';
import Text from '../Text';
import Ticker from '../../atoms/Ticker/';
type Props = {
step: ButtonStates;
children: React.ReactNode;
toAddress: string;
amountSatoshis?: number;
sizeQR: number;
onClick?: Function;
};
// May not need this wrapper.
const Wrapper = styled.div`
display: inline-block;
`;
const Main = styled.div`
display: grid;
position: relative;
`;
const QRCodeWrapper = styled.div`
padding: 12px 12px 9px;
border: 1px solid ${colors.fg500};
border-radius: 5px 5px 0 0;
border-bottom: none;
background-color: white;
display: flex;
align-items: center;
justify-content: center;
`;
const A = styled.a`
color: inherit;
text-decoration: none;
`;
const ButtonElement = styled('button')<{ isFresh: boolean; disabled: boolean }>`
cursor: pointer;
border: none;
position: relative;
border-radius: 0 0 5px 5px;
outline: none;
background-color: ${colors.brand500};
border-right: 1px solid ${colors.fg500};
border-left: 1px solid ${colors.fg500};
border-bottom: 1px solid ${colors.fg500};
padding: 12px 20px;
color: ${colors.bg100};
&:hover {
background-color: ${colors.brand500};
color: ${colors.bg100};
}
&:active {
background-color: ${colors.brand700};
color: ${colors.bg100};
}
`;
const cover = css`
position: absolute;
border-radius: 0 0 5px 5px;
top: 0;
bottom: 0;
right: 0;
left: 0;
font-size: 28px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
`;
const PendingCover = styled.div`
${cover};
border: 1px solid ${colors.pending700};
border-radius: 5px;
background-color: ${colors.pending500};
`;
const CompleteCover = styled.div`
${cover};
border-radius: 5px;
border: 1px solid ${colors.success700};
background-color: ${colors.success500};
`;
const WarningCover = styled.div`
${cover};
font-size: 16px;
border-color: ${colors.brand700};
background-color: ${colors.brand500};
cursor: pointer;
&:active {
background-color: ${colors.brand700};
color: ${colors.bg100};
}
`;
const spinAnimation = keyframes`
from {transform:rotate(0deg);}
to {transform:rotate(360deg);}
}
`;
const PendingSpinner = styled.div`
animation: ${spinAnimation} 3s linear infinite;
display: flex;
align-items: center;
justify-content: center;
`;
class ButtonQR extends React.PureComponent {
static defaultProps = {
sizeQR: 125,
};
render() {
- const {
- children,
- step,
- toAddress,
- amountSatoshis,
- sizeQR,
- } = this.props;
+ const { children, step, toAddress, amountSatoshis, sizeQR } =
+ this.props;
const widthQR = sizeQR >= 125 ? sizeQR : 125; // Minimum width 125
// QR code source
const uriBase = toAddress;
let uri = amountSatoshis
? `${uriBase}?amount=${amountSatoshis / 1e8}`
: uriBase;
// State booleans
const isFresh = step === 'fresh';
const isPending = step === 'pending';
const isComplete = step === 'complete';
const isInstall = step === 'install';
return (
{isPending && (
)}
{isComplete && (
)}
disabled={!isFresh}
isFresh={isFresh}
{...this.props}
>
{children}
{isInstall && (
Install CashTab & refresh
)}
);
}
}
export default ButtonQR;
diff --git a/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx b/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx
index a81b32edf..963cea7e1 100644
--- a/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx
+++ b/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx
@@ -1,437 +1,420 @@
import * as React from 'react';
import debounce from 'lodash/debounce';
import {
fiatToSatoshis,
adjustAmount,
getAddressUnconfirmed,
getTokenInfo,
} from '../../utils/cashtab-helpers';
import Ticker from '../../atoms/Ticker';
import type { CurrencyCode } from '../../utils/currency-helpers';
declare global {
interface Window {
bitcoinAbc: any;
}
}
interface sendParamsArr {
to: string;
protocol: ValidCoinTypes;
value?: string;
assetId?: string;
opReturn?: string[];
}
const SECOND = 1000;
const PRICE_UPDATE_INTERVAL = 60 * SECOND;
const REPEAT_TIMEOUT = 4 * SECOND;
const URI_CHECK_INTERVAL = 10 * SECOND;
// Whitelist of valid coinType.
type ValidCoinTypes = string;
// TODO - Install is a Cashtab state, others are payment states. Separate them to be independent
type ButtonStates = 'fresh' | 'pending' | 'complete' | 'expired' | 'install';
type CashtabBaseProps = {
to: string;
stepControlled?: ButtonStates;
// Both present to price in fiat equivalent
currency: CurrencyCode;
price?: number;
// Both present to price in coinType absolute amount
coinType: ValidCoinTypes;
tokenId?: string;
amount?: number;
isRepeatable: boolean;
repeatTimeout: number;
watchAddress: boolean;
opReturn?: string[];
showQR: boolean; // Intent to show QR. Only show if amount is BCH or fiat as OP_RETURN and SLP do not work with QR
successFn?: Function;
failFn?: Function;
};
interface IState {
step: ButtonStates;
errors: string[];
satoshis?: number; // Used when converting fiat to BCH
coinSymbol?: string;
coinName?: string;
coinDecimals?: number;
unconfirmedCount?: number;
intervalPrice?: NodeJS.Timeout;
intervalUnconfirmed?: NodeJS.Timeout;
intervalTimer?: NodeJS.Timeout;
}
const CashtabBase = (Wrapped: React.ComponentType) => {
return class extends React.Component {
static defaultProps = {
currency: 'USD',
coinType: Ticker.coinSymbol,
isRepeatable: false,
watchAddress: false,
showQR: true,
repeatTimeout: REPEAT_TIMEOUT,
};
state = {
step: 'fresh' as ButtonStates,
satoshis: undefined,
coinSymbol: undefined,
coinDecimals: undefined,
coinName: undefined,
unconfirmedCount: undefined,
intervalPrice: undefined,
intervalUnconfirmed: undefined,
intervalTimer: undefined,
errors: [],
};
addError = (error: string) => {
const { errors } = this.state;
this.setState({ errors: [...errors, error] });
};
startRepeatable = () => {
const { repeatTimeout } = this.props;
setTimeout(() => this.setState({ step: 'fresh' }), repeatTimeout);
};
paymentSendSuccess = () => {
const { isRepeatable } = this.props;
const { intervalUnconfirmed, unconfirmedCount } = this.state;
let unconfirmedCountInt;
if (typeof unconfirmedCount === 'undefined') {
unconfirmedCountInt = 0;
} else {
unconfirmedCountInt = unconfirmedCount;
}
this.setState({
step: 'complete',
unconfirmedCount: unconfirmedCountInt
? unconfirmedCountInt + 1
: 1,
});
if (isRepeatable) {
this.startRepeatable();
} else {
intervalUnconfirmed && clearInterval(intervalUnconfirmed);
}
};
getCashTabProviderStatus = () => {
console.log(window.bitcoinAbc);
if (
window &&
window.bitcoinAbc &&
window.bitcoinAbc === 'cashtab'
) {
return true;
}
return false;
};
handleClick = () => {
const { amount, to, opReturn, coinType, tokenId } = this.props;
const { satoshis } = this.state;
// Satoshis might not set be set during server rendering
if (!amount && !satoshis) {
return;
}
const walletProviderStatus = this.getCashTabProviderStatus();
if (typeof window === `undefined` || !walletProviderStatus) {
this.setState({ step: 'install' });
if (typeof window !== 'undefined') {
window.open(Ticker.installLink);
}
return;
}
if (walletProviderStatus) {
this.setState({ step: 'fresh' });
// Do not pass a token quantity to send, this is not yet supported in Cashtab
if (coinType === Ticker.tokenName) {
return;
}
return window.postMessage(
{
type: 'FROM_PAGE',
text: 'CashTab',
txInfo: {
address: to,
value: satoshis
? parseFloat(
(
satoshis! *
10 ** (-1 * Ticker.coinDecimals)
).toFixed(2),
)
: amount,
},
},
'*',
);
}
const sendParams: sendParamsArr = {
to,
protocol: coinType,
value:
amount?.toString() ||
adjustAmount(satoshis, Ticker.coinDecimals, true),
};
if (coinType === Ticker.tokenTicker) {
sendParams.assetId = tokenId;
}
if (opReturn && opReturn.length) {
sendParams.opReturn = opReturn;
}
this.setState({ step: 'pending' });
/* May match this functionality later, may handle differently as above for Cashtab
console.info('Cashtab sendAssets begin', sendParams);
sendAssets(sendParams)
.then(({ txid }: any) => {
console.info('Cashtab send success:', txid);
successFn && successFn(txid);
this.paymentSendSuccess();
})
.catch((err: any) => {
console.info('Cashtab send cancel', err);
failFn && failFn(err);
this.setState({ step: 'fresh' });
});
*/
};
updateSatoshisFiat = debounce(
async () => {
const { price, currency } = this.props;
if (!price) return;
const satoshis = await fiatToSatoshis(currency, price);
this.setState({ satoshis });
},
250,
{ leading: true, trailing: true },
);
setupSatoshisFiat = () => {
const { intervalPrice } = this.state;
intervalPrice && clearInterval(intervalPrice);
this.updateSatoshisFiat();
const intervalPriceNext = setInterval(
() => this.updateSatoshisFiat(),
PRICE_UPDATE_INTERVAL,
);
this.setState({ intervalPrice: intervalPriceNext });
};
setupWatchAddress = async () => {
const { to } = this.props;
const { intervalUnconfirmed } = this.state;
intervalUnconfirmed && clearInterval(intervalUnconfirmed);
const initialUnconfirmed = await getAddressUnconfirmed(to);
this.setState({ unconfirmedCount: initialUnconfirmed.length });
// Watch UTXO interval
const intervalUnconfirmedNext = setInterval(async () => {
const prevUnconfirmedCount = this.state.unconfirmedCount;
const targetTransactions = await getAddressUnconfirmed(to);
const unconfirmedCount = targetTransactions.length;
this.setState({ unconfirmedCount });
if (
prevUnconfirmedCount != null &&
unconfirmedCount > prevUnconfirmedCount
) {
this.paymentSendSuccess();
}
}, URI_CHECK_INTERVAL);
this.setState({ intervalUnconfirmed: intervalUnconfirmedNext });
};
setupCoinMeta = async () => {
const { coinType, tokenId } = this.props;
if (coinType === Ticker.coinSymbol) {
this.setState({
coinSymbol: Ticker.coinSymbol,
coinDecimals: Ticker.coinDecimals,
coinName: Ticker.coinName,
});
} else if (coinType === Ticker.tokenTicker && tokenId) {
this.setState({
coinSymbol: undefined,
coinName: undefined,
coinDecimals: undefined,
});
const tokenInfo = await getTokenInfo(tokenId);
const { symbol, decimals, name } = tokenInfo;
this.setState({
coinSymbol: symbol,
coinDecimals: decimals,
coinName: name,
});
}
};
confirmCashTabProviderStatus = () => {
const cashTabStatus = this.getCashTabProviderStatus();
if (cashTabStatus) {
this.setState({ step: 'fresh' });
}
};
async componentDidMount() {
if (typeof window !== 'undefined') {
const { price, watchAddress } = this.props;
// setup state, intervals, and listeners
watchAddress && this.setupWatchAddress();
price && this.setupSatoshisFiat();
this.setupCoinMeta(); // normal call for setupCoinMeta()
// Occasionially the cashtab window object is not available on componentDidMount, check later
// TODO make this less hacky
setTimeout(this.confirmCashTabProviderStatus, 750);
// Detect CashTab and determine if button should show install CTA
const walletProviderStatus = this.getCashTabProviderStatus();
if (walletProviderStatus) {
this.setState({ step: 'fresh' });
} else {
this.setState({ step: 'install' });
}
}
}
componentWillUnmount() {
- const {
- intervalPrice,
- intervalUnconfirmed,
- intervalTimer,
- } = this.state;
+ const { intervalPrice, intervalUnconfirmed, intervalTimer } =
+ this.state;
intervalPrice && clearInterval(intervalPrice);
intervalUnconfirmed && clearInterval(intervalUnconfirmed);
intervalTimer && clearInterval(intervalTimer);
}
componentDidUpdate(prevProps: CashtabBaseProps, prevState: IState) {
if (typeof window !== 'undefined') {
- const {
- currency,
- price,
- isRepeatable,
- watchAddress,
- } = this.props;
+ const { currency, price, isRepeatable, watchAddress } =
+ this.props;
const prevCurrency = prevProps.currency;
const prevPrice = prevProps.price;
const prevIsRepeatable = prevProps.isRepeatable;
const prevWatchAddress = prevProps.watchAddress;
// Fiat price or currency changes
if (currency !== prevCurrency || price !== prevPrice) {
this.setupSatoshisFiat();
}
if (isRepeatable && isRepeatable !== prevIsRepeatable) {
this.startRepeatable();
}
if (watchAddress !== prevWatchAddress) {
if (watchAddress) {
this.setupWatchAddress();
} else {
const { intervalUnconfirmed } = this.state;
intervalUnconfirmed &&
clearInterval(intervalUnconfirmed);
}
}
}
}
render() {
- const {
- amount,
- showQR,
- opReturn,
- coinType,
- stepControlled,
- } = this.props;
- const {
- step,
- satoshis,
- coinDecimals,
- coinSymbol,
- coinName,
- } = this.state;
+ const { amount, showQR, opReturn, coinType, stepControlled } =
+ this.props;
+ const { step, satoshis, coinDecimals, coinSymbol, coinName } =
+ this.state;
let calculatedAmount =
adjustAmount(amount, coinDecimals, false) || satoshis;
// Only show QR if all requested features can be encoded in the BIP44 URI
const shouldShowQR =
showQR &&
coinType === Ticker.coinSymbol &&
(!opReturn || !opReturn.length);
return (
);
}
};
};
export type { CashtabBaseProps, ButtonStates, ValidCoinTypes, IState };
export default CashtabBase;
diff --git a/web/cashtab-components/webpack.config.js b/web/cashtab-components/webpack.config.js
index a8930df7a..75c144fea 100644
--- a/web/cashtab-components/webpack.config.js
+++ b/web/cashtab-components/webpack.config.js
@@ -1,79 +1,79 @@
const path = require('path');
-const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
- .BundleAnalyzerPlugin;
+const BundleAnalyzerPlugin =
+ require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const env = process.env.NODE_ENV;
const config = {
entry: {
main: ['./src/index.ts'],
},
plugins: [],
externals: {
'react': {
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'react-dom': {
commonjs2: 'react-dom',
commonjs: 'react-dom',
amd: 'react-dom',
},
'styled-components': {
commonjs2: 'styled-components',
commonjs: 'styled-components',
amd: 'styled-components',
},
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: [/node_modules/, '/**/stories.*/'],
},
{
test: /\.(png|gif|jpg|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 50000,
},
},
},
],
},
resolve: {
extensions: [
'.tsx',
'.ts',
'.js',
'.json',
'.png',
'.gif',
'.jpg',
'.svg',
],
},
output: {
path: path.resolve(__dirname, 'dist/'),
publicPath: '',
filename: 'index.js',
library: '',
libraryTarget: 'commonjs',
},
optimization: {
minimize: true,
},
};
if (env === 'analyse') {
config.plugins.push(new BundleAnalyzerPlugin());
}
if (env === 'production') {
config.mode = 'production';
}
module.exports = config;
diff --git a/web/cashtab/config/webpack.config.js b/web/cashtab/config/webpack.config.js
index 0674bee08..863dee2d3 100644
--- a/web/cashtab/config/webpack.config.js
+++ b/web/cashtab/config/webpack.config.js
@@ -1,765 +1,764 @@
'use strict';
const fs = require('fs');
const isWsl = require('is-wsl');
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 WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
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 workboxPlugin = require('workbox-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';
// 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 imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000',
);
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// 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';
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
plugins: [new ESLintPlugin()],
};
// 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.
// In development, we always serve from the root. This makes config easier.
const publicPath = isEnvProduction
? paths.servedPath
: isEnvDevelopment && '/';
// Some apps do not use client-side routing with pushState.
// For these, "homepage" can be set to "." to enable relative asset paths.
const shouldUseRelativeAssetPaths = publicPath === './';
// `publicUrl` is just like `publicPath`, but we will provide it 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.
const publicUrl = isEnvProduction
? publicPath.slice(0, -1)
: isEnvDevelopment && '';
// Get environment variables to inject into our app.
const env = getClientEnvironment(publicUrl);
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: shouldUseRelativeAssetPaths
? { publicPath: '../../' }
: {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
loader: require.resolve('less-loader'),
options: {
modifyVars: {
'@layout-sider-background': '#131720',
'@layout-trigger-background': '#20242D',
'@light': '#7F7F7F',
'@dark': '#000',
'@heading-color': 'fade(@light, 85)',
'@text-color': 'fade(@light, 65)',
'@text-color-secondary': 'fade(@light, 45)',
'@disabled-color': 'fade(@light, 25)',
'@primary-5': '#40a9ff',
'@primary-color': '#20242D',
'@outline-color': '@primary-color',
'@icon-color': 'fade(@light, 65)',
'@icon-color-hover': 'fade(@light, 85)',
'@primary-6': '#096dd9',
'@border-color-base': '@border-color-split',
'@btn-default-color': '@heading-color',
'@btn-default-bg': '#444457',
'@btn-default-border': '#444457',
'@btn-ghost-color': 'fade(@light, 45)',
'@btn-ghost-border': 'fade(@light, 45)',
'@input-color': '@text-color',
'@input-bg': '#3b3b4d',
'@input-disabled-bg': '#4c4c61',
'@input-placeholder-color': '@text-color-secondary',
'@input-hover-border-color': 'fade(@light, 10)',
'@checkbox-check-color': '#3b3b4d',
'@checkbox-color': '@primary-color',
'@select-border-color': '#3b3b4d',
'@item-active-bg': '#272733',
'@border-color-split': '#17171f',
'@menu-dark-bg': '#131720',
'@body-background': '#FBFBFD',
'@component-background': '#FFFFFF',
'@layout-body-background': '#FBFBFD',
'@tooltip-bg': '#191922',
'@tooltip-arrow-color': '#191922',
'@popover-bg': '#2d2d3b',
'@success-color': '#00a854',
'@info-color': '@primary-color',
'@warning-color': '#ffbf00',
'@error-color': '#f04134',
'@menu-bg': '#20242D',
'@menu-item-active-bg': 'fade(@light, 5)',
'@menu-highlight-color': '@light',
'@card-background': '@component-background',
'@card-hover-border': '#383847',
'@card-actions-background': '#FBFCFD',
'@tail-color': 'fade(@light, 10)',
'@radio-button-bg': 'transparent',
'@radio-button-checked-bg': 'transparent',
'@radio-dot-color': '@primary-color',
'@table-row-hover-bg': '#383847',
'@item-hover-bg': '#383847',
'@alert-text-color': 'fade(@dark, 65%)',
'@tabs-horizontal-padding': '12px 0',
// zIndex': 'notification > popover > tooltip
'@zindex-notification': '1063',
'@zindex-popover': '1061',
'@zindex-tooltip': '1060',
// width
'@anchor-border-width': '1px',
// margin
'@form-item-margin-bottom': '24px',
'@menu-item-vertical-margin': '0px',
'@menu-item-boundary-margin': '0px',
// size
'@font-size-base': '14px',
'@font-size-lg': '16px',
'@screen-xl': '1208px',
'@screen-lg': '1024px',
'@screen-md': '768px',
// 移动
'@screen-sm': '767.9px',
// 超小屏
'@screen-xs': '375px',
'@alert-message-color': '@popover-bg',
'@background-color-light': '@popover-bg',
'@layout-header-background': '@menu-dark-bg',
// 官网
'@site-text-color': '@text-color',
'@site-border-color-split': 'fade(@light, 5)',
'@site-heading-color': '@heading-color',
'@site-header-box-shadow':
'0 0.3px 0.9px rgba(0, 0, 0, 0.12), 0 1.6px 3.6px rgba(0, 0, 0, 0.12)',
'@home-text-color': '@text-color',
//自定义需要找设计师
'@gray-8': '@text-color',
'@background-color-base': '#FBFBFD',
'@skeleton-color': 'rgba(0,0,0,0.8)',
// pro
'@pro-header-box-shadow': '@site-header-box-shadow',
},
javascriptEnabled: true,
},
},
{
// 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,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
{
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: [
// 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'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// 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.
].filter(Boolean),
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',
// We inferred the "public path" (such as / or /my-project) from homepage.
// We use "/" in development.
publicPath: publicPath,
// 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}`,
},
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,
},
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,
},
},
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
// Disabled on WSL (Windows Subsystem for Linux) due to an issue with Terser
// https://github.com/webpack-contrib/terser-webpack-plugin/issues/21
parallel: !isWsl,
}),
// 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,
},
}),
],
// 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: false,
},
// 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 || []),
+ 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',
'@hooks': path.resolve(paths.appPath, 'src/hooks/'),
'@components': path.resolve(paths.appPath, 'src/components/'),
'@utils': path.resolve(paths.appPath, 'src/utils/'),
'@assets': path.resolve(paths.appPath, 'src/assets/'),
},
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]),
],
},
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: [
// "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',
),
plugins: [
[
require.resolve(
'babel-plugin-named-asset-import',
),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
],
// 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,
},
},
// 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,
// If an error happens in a package, it's possible to be
// because it was compiled. Thus, we don't want the browser
// debugger to show the original code. Instead, the code
// being evaluated would be much more helpful.
sourceMaps: false,
},
},
// "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