diff --git a/app/components/UI/ActionModal/__snapshots__/index.test.js.snap b/app/components/UI/ActionModal/__snapshots__/index.test.js.snap index fb68323237f..3631e412687 100644 --- a/app/components/UI/ActionModal/__snapshots__/index.test.js.snap +++ b/app/components/UI/ActionModal/__snapshots__/index.test.js.snap @@ -53,10 +53,8 @@ exports[`ActionModal should render correctly 1`] = ` @@ -103,7 +103,7 @@ exports[`ActionModal should render correctly 1`] = ` testID="" type="neutral" > - CANCEL + Cancel - CONFIRM + Confirm diff --git a/app/components/UI/ActionModal/index.js b/app/components/UI/ActionModal/index.js index d71d3643108..d53a607343c 100644 --- a/app/components/UI/ActionModal/index.js +++ b/app/components/UI/ActionModal/index.js @@ -2,8 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { StyleSheet, View } from 'react-native'; import Modal from 'react-native-modal'; -import { colors, baseStyles } from '../../../styles/common'; +import { colors } from '../../../styles/common'; import StyledButton from '../StyledButton'; +import { strings } from '../../../../locales/i18n'; const styles = StyleSheet.create({ modal: { @@ -15,10 +16,8 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center' }, - modalText: { - alignSelf: 'center', + modalContainer: { width: '90%', - height: 300, backgroundColor: colors.white, borderRadius: 10 }, @@ -29,6 +28,11 @@ const styles = StyleSheet.create({ flexDirection: 'row', padding: 16 }, + childrenContainer: { + minHeight: 250, + flexDirection: 'row', + alignItems: 'center' + }, button: { flex: 1 }, @@ -49,6 +53,7 @@ export default function ActionModal({ cancelText, children, confirmText, + confirmDisabled, cancelButtonMode, confirmButtonMode, onCancelPress, @@ -66,8 +71,8 @@ export default function ActionModal({ swipeDirection={'down'} > - - {children} + + {children} - {cancelText.toUpperCase()} + {cancelText} - {confirmText.toUpperCase()} + {confirmText} @@ -97,8 +103,9 @@ ActionModal.defaultProps = { confirmButtonMode: 'warning', confirmTestID: '', cancelTestID: '', - cancelText: 'CANCEL', - confirmText: 'CONFIRM' + cancelText: strings('action_view.cancel'), + confirmText: strings('action_view.confirm'), + confirmDisabled: false }; ActionModal.propTypes = { @@ -121,11 +128,15 @@ ActionModal.propTypes = { /** * Type of button to show as the cancel button */ - cancelButtonMode: PropTypes.oneOf(['cancel', 'neutral', 'confirm']), + cancelButtonMode: PropTypes.oneOf(['cancel', 'neutral', 'confirm', 'normal']), /** * Type of button to show as the confirm button */ confirmButtonMode: PropTypes.oneOf(['normal', 'confirm', 'warning']), + /** + * Whether confirm button is disabled + */ + confirmDisabled: PropTypes.bool, /** * Text to show in the confirm button */ diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index d7532a15baa..237c31e5311 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -608,14 +608,14 @@ class TransactionElement extends PureComponent { const { tx } = this.props; const existingGasPrice = tx.transaction ? tx.transaction.gasPrice : '0x0'; const existingGasPriceDecimal = parseInt(existingGasPrice === undefined ? '0x0' : existingGasPrice, 16); - this.mounted && this.props.onCancelAction(true, existingGasPriceDecimal, this.props.tx.id); + this.mounted && this.props.onCancelAction(true, existingGasPriceDecimal, this.props.tx); }; showSpeedUpModal = () => { const { tx } = this.props; const existingGasPrice = tx.transaction ? tx.transaction.gasPrice : '0x0'; const existingGasPriceDecimal = parseInt(existingGasPrice === undefined ? '0x0' : existingGasPrice, 16); - this.mounted && this.props.onSpeedUpAction(true, existingGasPriceDecimal, this.props.tx.id); + this.mounted && this.props.onSpeedUpAction(true, existingGasPriceDecimal, this.props.tx); }; hideSpeedUpModal = () => { diff --git a/app/components/UI/Transactions/index.js b/app/components/UI/Transactions/index.js index 070ec7cc186..6d344e4d5a7 100644 --- a/app/components/UI/Transactions/index.js +++ b/app/components/UI/Transactions/index.js @@ -23,6 +23,9 @@ import TransactionsNotificationManager from '../../../core/TransactionsNotificat import ActionModal from '../ActionModal'; import { CANCEL_RATE, SPEED_UP_RATE } from 'gaba'; import { renderFromWei } from '../../../util/number'; +import { safeToChecksumAddress } from '../../../util/address'; +import { hexToBN } from 'gaba/dist/util'; +import { BN } from 'ethereumjs-util'; const styles = StyleSheet.create({ wrapper: { @@ -54,7 +57,8 @@ const styles = StyleSheet.create({ modalText: { ...fontStyles.normal, fontSize: 14, - textAlign: 'center' + textAlign: 'center', + paddingVertical: 8 }, modalTitle: { ...fontStyles.bold, @@ -64,7 +68,8 @@ const styles = StyleSheet.create({ gasTitle: { ...fontStyles.bold, fontSize: 16, - textAlign: 'center' + textAlign: 'center', + marginVertical: 8 }, cancelFeeWrapper: { backgroundColor: colors.grey000, @@ -75,6 +80,13 @@ const styles = StyleSheet.create({ ...fontStyles.bold, fontSize: 24, textAlign: 'center' + }, + warningText: { + ...fontStyles.normal, + fontSize: 12, + color: colors.red, + paddingVertical: 8, + textAlign: 'center' } }); @@ -85,6 +97,10 @@ const ROW_HEIGHT = (Platform.OS === 'ios' ? 95 : 100) + StyleSheet.hairlineWidth */ class Transactions extends PureComponent { static propTypes = { + /** + * Map of accounts to information objects including balances + */ + accounts: PropTypes.object, /** * Object containing token exchange rates in the format address => exchangeRate */ @@ -161,7 +177,9 @@ class Transactions extends PureComponent { ready: false, refreshing: false, cancelIsOpen: false, - speedUpIsOpen: false + cancelConfirmDisabled: false, + speedUpIsOpen: false, + speedUpConfirmDisabled: false }; existingGasPriceDecimal = null; @@ -265,10 +283,32 @@ class Transactions extends PureComponent { blockExplorer = () => hasBlockExplorer(this.props.networkType); - onSpeedUpAction = (speedUpAction, existingGasPriceDecimal, speedUpTxId) => { + validateBalance = (tx, rate) => { + const { accounts } = this.props; + try { + const checksummedFrom = safeToChecksumAddress(tx.transaction.from); + const balance = accounts[checksummedFrom].balance; + return hexToBN(balance).lt( + hexToBN(tx.transaction.gasPrice) + .mul(new BN(rate * 10)) + .mul(new BN(10)) + .mul(hexToBN(tx.transaction.gas)) + .add(hexToBN(tx.transaction.value)) + ); + } catch (e) { + return false; + } + }; + + onSpeedUpAction = (speedUpAction, existingGasPriceDecimal, tx) => { this.setState({ speedUpIsOpen: speedUpAction }); this.existingGasPriceDecimal = existingGasPriceDecimal; - this.speedUpTxId = speedUpTxId; + this.speedUpTxId = tx.id; + if (this.validateBalance(tx, SPEED_UP_RATE)) { + this.setState({ speedUpIsOpen: speedUpAction, speedUpConfirmDisabled: true }); + } else { + this.setState({ speedUpIsOpen: speedUpAction, speedUpConfirmDisabled: false }); + } }; onSpeedUpCompleted = () => { @@ -277,12 +317,15 @@ class Transactions extends PureComponent { this.speedUpTxId = null; }; - onCancelAction = (cancelAction, existingGasPriceDecimal, cancelTxId) => { - this.setState({ cancelIsOpen: cancelAction }); + onCancelAction = (cancelAction, existingGasPriceDecimal, tx) => { this.existingGasPriceDecimal = existingGasPriceDecimal; - this.cancelTxId = cancelTxId; + this.cancelTxId = tx.id; + if (this.validateBalance(tx, SPEED_UP_RATE)) { + this.setState({ cancelIsOpen: cancelAction, cancelConfirmDisabled: true }); + } else { + this.setState({ cancelIsOpen: cancelAction, cancelConfirmDisabled: false }); + } }; - onCancelCompleted = () => { this.setState({ cancelIsOpen: false }); this.existingGasPriceDecimal = null; @@ -342,6 +385,7 @@ class Transactions extends PureComponent { } const { submittedTransactions, confirmedTransactions, header } = this.props; + const { cancelConfirmDisabled, speedUpConfirmDisabled } = this.state; const transactions = submittedTransactions && submittedTransactions.length ? submittedTransactions.concat(confirmedTransactions) @@ -369,6 +413,7 @@ class Transactions extends PureComponent { onCancelPress={this.onCancelCompleted} onRequestClose={this.onCancelCompleted} onConfirmPress={this.cancelTransaction} + confirmDisabled={cancelConfirmDisabled} > {strings('transaction.cancel_tx_title')} @@ -381,6 +426,9 @@ class Transactions extends PureComponent { {strings('transaction.cancel_tx_message')} + {cancelConfirmDisabled && ( + {strings('transaction.insufficient')} + )} @@ -391,6 +439,7 @@ class Transactions extends PureComponent { onCancelPress={this.onSpeedUpCompleted} onRequestClose={this.onSpeedUpCompleted} onConfirmPress={this.speedUpTransaction} + confirmDisabled={speedUpConfirmDisabled} > {strings('transaction.speedup_tx_title')} @@ -403,6 +452,9 @@ class Transactions extends PureComponent { {strings('transaction.speedup_tx_message')} + {speedUpConfirmDisabled && ( + {strings('transaction.insufficient')} + )} @@ -411,6 +463,7 @@ class Transactions extends PureComponent { } const mapStateToProps = state => ({ + accounts: state.engine.backgroundState.AccountTrackerController.accounts, tokens: state.engine.backgroundState.AssetsController.tokens.reduce((tokens, token) => { tokens[token.address] = token; return tokens; diff --git a/app/components/UI/Transactions/index.test.js b/app/components/UI/Transactions/index.test.js index fd5991c320d..600eeb136cd 100644 --- a/app/components/UI/Transactions/index.test.js +++ b/app/components/UI/Transactions/index.test.js @@ -12,6 +12,9 @@ describe('Transactions', () => { const initialState = { engine: { backgroundState: { + AccountTrackerController: { + accounts: {} + }, AssetsController: { tokens: [] }, diff --git a/app/components/Views/Settings/AdvancedSettings/__snapshots__/index.test.js.snap b/app/components/Views/Settings/AdvancedSettings/__snapshots__/index.test.js.snap index cd20be531ef..5d210e50950 100644 --- a/app/components/Views/Settings/AdvancedSettings/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/AdvancedSettings/__snapshots__/index.test.js.snap @@ -43,6 +43,7 @@ exports[`AdvancedSettings should render correctly 1`] = ` cancelTestID="" cancelText="NEVERMIND" confirmButtonMode="warning" + confirmDisabled={false} confirmTestID="" confirmText="RESET" modalVisible={false} diff --git a/app/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap b/app/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap index e31b33df81c..7febeecbe87 100644 --- a/app/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap @@ -472,6 +472,7 @@ exports[`SecuritySettings should render correctly 1`] = ` cancelTestID="" cancelText="NEVERMIND" confirmButtonMode="warning" + confirmDisabled={false} confirmTestID="" confirmText="CLEAR" modalVisible={false} @@ -522,6 +523,7 @@ exports[`SecuritySettings should render correctly 1`] = ` cancelTestID="" cancelText="NEVERMIND" confirmButtonMode="warning" + confirmDisabled={false} confirmTestID="" confirmText="CLEAR" modalVisible={false} diff --git a/package.json b/package.json index 1a1361839a8..9a482a5df4e 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "ethjs-unit": "0.1.6", "events": "3.0.0", "fuse.js": "3.4.4", - "gaba": "1.9.0", + "gaba": "1.9.2", "https-browserify": "0.0.1", "lottie-react-native": "git+ssh://git@github.com/brunobar79/lottie-react-native.git#7ce6a78ac4ac7b9891bc513cb3f12f8b9c9d9106", "multihashes": "0.4.14", diff --git a/scripts/build.sh b/scripts/build.sh index dcfa475f79b..4031f84d8c8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -139,7 +139,7 @@ buildAndroid(){ buildIosSimulator(){ prebuild_ios - react-native run-ios --simulator "iPhone 11 Pro (13.2)" + react-native run-ios --simulator "iPhone 11 Pro (13.3)" } buildIosDevice(){ diff --git a/yarn.lock b/yarn.lock index fb0bca12acb..189f6f4b453 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4742,10 +4742,10 @@ g-status@^2.0.2: matcher "^1.0.0" simple-git "^1.85.0" -gaba@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/gaba/-/gaba-1.9.0.tgz#ccd9f99c56687b5acd39f9e3ceb435b2a59b6aa1" - integrity sha512-HoVreAdZssL0jNHuzZ7WP+YKZ0riu44jVDWxhQ9hsgPuzxbVEsz9fO/HDxqAdNZS1Cswayq6+ciZ3HSCFWMKbQ== +gaba@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/gaba/-/gaba-1.9.2.tgz#546b3733f686561032004f589ff9da6b11d02000" + integrity sha512-qVcSkRBErmYkiAxy/VGqetTF2zNrc2+dsv7SBcHVt5jYikhw2EMdfsEwEyWsjAM/YGry7mxmozlIS7nvdJXnYQ== dependencies: await-semaphore "^0.1.3" eth-contract-metadata "^1.11.0"