mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Redesign approve screen (#7271)
* Redesign approve screen * Add translations to approve screen components * Show account in header of approve screen * Use state prop bool for unlimited vs custom check in edit-approval-permission * Set option to custom on input change in edit-approval-permission * Allow setting of approval amount to unlimited in edit-approval-permission * Fix height of confirm-approval popup * Ensure decimals prop passted to confirm-approve.component is correct type * Ensure first param passed to calcTokenValue in confirm-approve.util is the correct type * Fix e2e test of permission editing * Remove unused code from edit-approval-permission.container
This commit is contained in:
parent
99b8f2d544
commit
2673eef3c4
@ -56,6 +56,10 @@
|
||||
"acceleratingATransaction": {
|
||||
"message": "* Accelerating a transaction by using a higher gas price increases its chances of getting processed by the network faster, but it is not always guaranteed."
|
||||
},
|
||||
"accessAndSpendNotice": {
|
||||
"message": "$1 may access and spend up to this max amount",
|
||||
"description": "$1 is the url of the site requesting ability to spend"
|
||||
},
|
||||
"accessingYourCamera": {
|
||||
"message": "Accessing your camera..."
|
||||
},
|
||||
@ -113,9 +117,20 @@
|
||||
"addAcquiredTokens": {
|
||||
"message": "Add the tokens you've acquired using MetaMask"
|
||||
},
|
||||
"allowOriginSpendToken": {
|
||||
"message": "Allow $1 to spend your $2?",
|
||||
"description": "$1 is the url of the site and $2 is the symbol of the token they are requesting to spend"
|
||||
},
|
||||
"allowWithdrawAndSpend": {
|
||||
"message": "Allow $1 to withdraw and spend up to the following amount:",
|
||||
"description": "The url of the site that requested permission to 'withdraw and spend'"
|
||||
},
|
||||
"amount": {
|
||||
"message": "Amount"
|
||||
},
|
||||
"amountWithColon": {
|
||||
"message": "Amount:"
|
||||
},
|
||||
"appDescription": {
|
||||
"message": "An Ethereum Wallet in your Browser",
|
||||
"description": "The description of the application"
|
||||
@ -384,6 +399,9 @@
|
||||
"customRPC": {
|
||||
"message": "Custom RPC"
|
||||
},
|
||||
"customSpendLimit": {
|
||||
"message": "Custom Spend Limit"
|
||||
},
|
||||
"dataBackupFoundInfo": {
|
||||
"message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts and tokens. Would you like to restore this data now?"
|
||||
},
|
||||
@ -444,6 +462,9 @@
|
||||
"editContact": {
|
||||
"message": "Edit Contact"
|
||||
},
|
||||
"editPermission": {
|
||||
"message": "Edit Permission"
|
||||
},
|
||||
"emailUs": {
|
||||
"message": "Email us!"
|
||||
},
|
||||
@ -486,6 +507,9 @@
|
||||
"enterAnAlias": {
|
||||
"message": "Enter an alias"
|
||||
},
|
||||
"enterMaxSpendLimit": {
|
||||
"message": "Enter Max Spend Limit"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Enter password"
|
||||
},
|
||||
@ -516,6 +540,9 @@
|
||||
"faster": {
|
||||
"message": "Faster"
|
||||
},
|
||||
"feeAssociatedRequest": {
|
||||
"message": "A fee is associated with this request."
|
||||
},
|
||||
"fiat": {
|
||||
"message": "Fiat",
|
||||
"description": "Exchange type"
|
||||
@ -533,6 +560,9 @@
|
||||
"fromShapeShift": {
|
||||
"message": "From ShapeShift"
|
||||
},
|
||||
"functionApprove": {
|
||||
"message": "Function: Approve"
|
||||
},
|
||||
"functionType": {
|
||||
"message": "Function Type"
|
||||
},
|
||||
@ -953,6 +983,9 @@
|
||||
"privateNetwork": {
|
||||
"message": "Private Network"
|
||||
},
|
||||
"proposedApprovalLimit": {
|
||||
"message": "Proposed Approval Limit"
|
||||
},
|
||||
"qrCode": {
|
||||
"message": "Show QR Code"
|
||||
},
|
||||
@ -1212,6 +1245,13 @@
|
||||
"speedUpTransaction": {
|
||||
"message": "Speed up this transaction"
|
||||
},
|
||||
"spendLimitPermission": {
|
||||
"message": "Spend limit permission"
|
||||
},
|
||||
"spendLimitRequestedBy": {
|
||||
"message": "Spend limit requested by $1",
|
||||
"description": "Origin of the site requesting the spend limit"
|
||||
},
|
||||
"switchNetworks": {
|
||||
"message": "Switch Networks"
|
||||
},
|
||||
@ -1308,6 +1348,9 @@
|
||||
"to": {
|
||||
"message": "To"
|
||||
},
|
||||
"toWithColon": {
|
||||
"message": "To:"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 to ETH via ShapeShift",
|
||||
"description": "system will fill in deposit type in start of message"
|
||||
@ -1382,6 +1425,10 @@
|
||||
"message": "We had trouble loading your token balances. You can view them ",
|
||||
"description": "Followed by a link (here) to view token balances"
|
||||
},
|
||||
"trustSiteApprovePermission": {
|
||||
"message": "Do you trust this site? By granting this permission, you’re allowing $1 to withdraw your $2 and automate transactions for you.",
|
||||
"description": "$1 is the url requesting permission and $2 is the symbol of the currency that the request is for"
|
||||
},
|
||||
"tryAgain": {
|
||||
"message": "Try again"
|
||||
},
|
||||
@ -1409,6 +1456,9 @@
|
||||
"unknownCameraError": {
|
||||
"message": "There was an error while trying to access your camera. Please try again..."
|
||||
},
|
||||
"unlimited": {
|
||||
"message": "Unlimited"
|
||||
},
|
||||
"unlock": {
|
||||
"message": "Unlock"
|
||||
},
|
||||
|
3
app/images/user-check.svg
Normal file
3
app/images/user-check.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8C9.1875 8 11 6.21875 11 4C11 1.8125 9.1875 0 7 0C4.78125 0 3 1.8125 3 4C3 6.21875 4.78125 8 7 8ZM9.78125 9H9.25C8.5625 9.34375 7.8125 9.5 7 9.5C6.1875 9.5 5.40625 9.34375 4.71875 9H4.1875C1.875 9 0 10.9062 0 13.2188V14.5C0 15.3438 0.65625 16 1.5 16H12.5C13.3125 16 14 15.3438 14 14.5V13.2188C14 10.9062 12.0938 9 9.78125 9ZM19.875 5L19 4.125C18.875 3.96875 18.625 3.96875 18.5 4.125L15.2188 7.375L13.7812 5.9375C13.6562 5.78125 13.4062 5.78125 13.25 5.9375L12.375 6.8125C12.25 6.9375 12.25 7.1875 12.375 7.34375L14.9375 9.90625C15.0938 10.0625 15.3125 10.0625 15.4688 9.90625L19.875 5.53125C20.0312 5.375 20.0312 5.15625 19.875 5Z" fill="#6A737D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 769 B |
@ -1124,8 +1124,8 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`))
|
||||
await transferTokens.click()
|
||||
const approveTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`))
|
||||
await approveTokens.click()
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
@ -1143,31 +1143,22 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('displays the token approval data', async () => {
|
||||
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
dataTab.click()
|
||||
const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button'))
|
||||
await fullTxDataButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
|
||||
const functionType = await findElement(driver, By.css('.confirm-approve-content__data .confirm-approve-content__small-text'))
|
||||
const functionTypeText = await functionType.getText()
|
||||
assert.equal(functionTypeText, 'Approve')
|
||||
assert.equal(functionTypeText, 'Function: Approve')
|
||||
|
||||
const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
|
||||
const confirmDataDiv = await findElement(driver, By.css('.confirm-approve-content__data__data-block'))
|
||||
const confirmDataText = await confirmDataDiv.getText()
|
||||
assert(confirmDataText.match(/0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef4/))
|
||||
|
||||
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
|
||||
detailsTab.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning'))
|
||||
const approvalWarningText = await approvalWarning.getText()
|
||||
assert(approvalWarningText.match(/By approving this/))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('opens the gas edit modal', async () => {
|
||||
const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')))
|
||||
await configureGas.click()
|
||||
const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer'))
|
||||
await editButtons[0].click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
gasModal = await driver.findElement(By.css('span .modal'))
|
||||
@ -1198,14 +1189,34 @@ describe('MetaMask', function () {
|
||||
await save.click()
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
|
||||
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary'))
|
||||
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
|
||||
const gasFeeInEth = await findElement(driver, By.css('.confirm-approve-content__transaction-details-content__secondary-fee'))
|
||||
assert.equal(await gasFeeInEth.getText(), '0.0006')
|
||||
})
|
||||
|
||||
it('shows the correct recipient', async function () {
|
||||
const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name'))
|
||||
const recipientDiv = senderToRecipientDivs[1]
|
||||
assert.equal(await recipientDiv.getText(), '0x9bc5...fEF4')
|
||||
it('edits the permission', async () => {
|
||||
const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer'))
|
||||
await editButtons[1].click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const permissionModal = await driver.findElement(By.css('span .modal'))
|
||||
|
||||
const radioButtons = await findElements(driver, By.css('.edit-approval-permission__edit-section__radio-button'))
|
||||
await radioButtons[1].click()
|
||||
|
||||
const customInput = await findElement(driver, By.css('input'))
|
||||
await delay(50)
|
||||
await customInput.sendKeys('5')
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const saveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
|
||||
await saveButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.wait(until.stalenessOf(permissionModal))
|
||||
|
||||
const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text'))
|
||||
const amountDiv = permissionInfo[0]
|
||||
assert.equal(await amountDiv.getText(), '5 TST')
|
||||
})
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
@ -1221,7 +1232,7 @@ describe('MetaMask', function () {
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-5\s*TST/))
|
||||
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
|
||||
})
|
||||
@ -1305,9 +1316,13 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('shows the correct recipient', async function () {
|
||||
const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name'))
|
||||
const recipientDiv = senderToRecipientDivs[1]
|
||||
assert.equal(await recipientDiv.getText(), 'Account 2')
|
||||
const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button'))
|
||||
await fullTxDataButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text'))
|
||||
const recipientDiv = permissionInfo[1]
|
||||
assert.equal(await recipientDiv.getText(), '0x2f318C33...C970')
|
||||
})
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
.confirm-page-container-content {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
&__error-container {
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
} from '../../../../../../app/scripts/lib/enums'
|
||||
import NetworkDisplay from '../../network-display'
|
||||
import Identicon from '../../../ui/identicon'
|
||||
import { addressSlicer } from '../../../../helpers/utils/util'
|
||||
|
||||
export default class ConfirmPageContainerHeader extends Component {
|
||||
static contextTypes = {
|
||||
@ -12,13 +14,15 @@ export default class ConfirmPageContainerHeader extends Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accountAddress: PropTypes.string,
|
||||
showAccountInHeader: PropTypes.bool,
|
||||
showEdit: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
renderTop () {
|
||||
const { onEdit, showEdit } = this.props
|
||||
const { onEdit, showEdit, accountAddress, showAccountInHeader } = this.props
|
||||
const windowType = window.METAMASK_UI_TYPE
|
||||
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
|
||||
windowType !== ENVIRONMENT_TYPE_POPUP
|
||||
@ -29,7 +33,8 @@ export default class ConfirmPageContainerHeader extends Component {
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-header__row">
|
||||
<div
|
||||
{ !showAccountInHeader
|
||||
? <div
|
||||
className="confirm-page-container-header__back-button-container"
|
||||
style={{
|
||||
visibility: showEdit ? 'initial' : 'hidden',
|
||||
@ -45,6 +50,22 @@ export default class ConfirmPageContainerHeader extends Component {
|
||||
{ this.context.t('edit') }
|
||||
</span>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
{ showAccountInHeader
|
||||
? <div className="confirm-page-container-header__address-container">
|
||||
<div className="confirm-page-container-header__address-identicon">
|
||||
<Identicon
|
||||
address={accountAddress}
|
||||
diameter={24}
|
||||
/>
|
||||
</div>
|
||||
<div className="confirm-page-container-header__address">
|
||||
{ addressSlicer(accountAddress) }
|
||||
</div>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
{ !isFullScreen && <NetworkDisplay /> }
|
||||
</div>
|
||||
)
|
||||
|
@ -9,6 +9,7 @@
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 4px 13px 4px 13px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__back-button-container {
|
||||
@ -28,4 +29,16 @@
|
||||
font-weight: 400;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&__address-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__address {
|
||||
margin-left: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ export default class ConfirmPageContainer extends Component {
|
||||
subtitleComponent: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
titleComponent: PropTypes.node,
|
||||
hideSenderToRecipient: PropTypes.bool,
|
||||
showAccountInHeader: PropTypes.bool,
|
||||
// Sender to Recipient
|
||||
fromAddress: PropTypes.string,
|
||||
fromName: PropTypes.string,
|
||||
@ -104,6 +106,8 @@ export default class ConfirmPageContainer extends Component {
|
||||
lastTx,
|
||||
ofText,
|
||||
requestsWaitingText,
|
||||
hideSenderToRecipient,
|
||||
showAccountInHeader,
|
||||
} = this.props
|
||||
const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
|
||||
|
||||
@ -124,8 +128,12 @@ export default class ConfirmPageContainer extends Component {
|
||||
<ConfirmPageContainerHeader
|
||||
showEdit={showEdit}
|
||||
onEdit={() => onEdit()}
|
||||
showAccountInHeader={showAccountInHeader}
|
||||
accountAddress={fromAddress}
|
||||
>
|
||||
<SenderToRecipient
|
||||
{ hideSenderToRecipient
|
||||
? null
|
||||
: <SenderToRecipient
|
||||
senderName={fromName}
|
||||
senderAddress={fromAddress}
|
||||
recipientName={toName}
|
||||
@ -134,6 +142,7 @@ export default class ConfirmPageContainer extends Component {
|
||||
recipientNickname={toNickname}
|
||||
assetImage={renderAssetImage ? assetImage : undefined}
|
||||
/>
|
||||
}
|
||||
</ConfirmPageContainerHeader>
|
||||
{
|
||||
contentComponent || (
|
||||
|
@ -5,3 +5,9 @@
|
||||
@import 'confirm-detail-row/index';
|
||||
|
||||
@import 'confirm-page-container-navigation/index';
|
||||
|
||||
.page-container {
|
||||
&__content-component-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '../../ui/button'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class Modal extends PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
contentClass: PropTypes.string,
|
||||
containerClass: PropTypes.string,
|
||||
// Header text
|
||||
headerText: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
@ -36,10 +39,12 @@ export default class Modal extends PureComponent {
|
||||
onCancel,
|
||||
cancelType,
|
||||
cancelText,
|
||||
contentClass,
|
||||
containerClass,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="modal-container">
|
||||
<div className={classnames('modal-container', containerClass)}>
|
||||
{
|
||||
headerText && (
|
||||
<div className="modal-container__header">
|
||||
@ -53,7 +58,7 @@ export default class Modal extends PureComponent {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="modal-container__content">
|
||||
<div className={classnames('modal-container__content', contentClass)}>
|
||||
{ children }
|
||||
</div>
|
||||
<div className="modal-container__footer">
|
||||
|
@ -0,0 +1,170 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Modal from '../../modal'
|
||||
import Identicon from '../../../ui/identicon'
|
||||
import TextField from '../../../ui/text-field'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class EditApprovalPermission extends PureComponent {
|
||||
static propTypes = {
|
||||
hideModal: PropTypes.func.isRequired,
|
||||
selectedIdentity: PropTypes.object,
|
||||
tokenAmount: PropTypes.string,
|
||||
customTokenAmount: PropTypes.string,
|
||||
tokenSymbol: PropTypes.string,
|
||||
tokenBalance: PropTypes.string,
|
||||
setCustomAmount: PropTypes.func,
|
||||
origin: PropTypes.string,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
customSpendLimit: this.props.customTokenAmount,
|
||||
selectedOptionIsUnlimited: !this.props.customTokenAmount,
|
||||
}
|
||||
|
||||
renderModalContent () {
|
||||
const { t } = this.context
|
||||
const {
|
||||
hideModal,
|
||||
selectedIdentity,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
customTokenAmount,
|
||||
origin,
|
||||
} = this.props
|
||||
const { name, address } = selectedIdentity || {}
|
||||
const { selectedOptionIsUnlimited } = this.state
|
||||
|
||||
return (
|
||||
<div className="edit-approval-permission">
|
||||
<div className="edit-approval-permission__header">
|
||||
<div className="edit-approval-permission__title">
|
||||
{ t('editPermission') }
|
||||
</div>
|
||||
<div
|
||||
className="edit-approval-permission__header__close"
|
||||
onClick={() => hideModal()}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-approval-permission__account-info">
|
||||
<div className="edit-approval-permission__account-info__account">
|
||||
<Identicon
|
||||
address={address}
|
||||
diameter={32}
|
||||
/>
|
||||
<div className="edit-approval-permission__account-info__name">{ name }</div>
|
||||
<div>{ t('balance') }</div>
|
||||
</div>
|
||||
<div className="edit-approval-permission__account-info__balance">
|
||||
{`${tokenBalance} ${tokenSymbol}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section">
|
||||
<div className="edit-approval-permission__edit-section__title">
|
||||
{ t('spendLimitPermission') }
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__description">
|
||||
{ t('allowWithdrawAndSpend', [origin]) }
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option">
|
||||
<div
|
||||
className="edit-approval-permission__edit-section__radio-button"
|
||||
onClick={() => this.setState({ selectedOptionIsUnlimited: true })}
|
||||
>
|
||||
<div className={classnames({
|
||||
'edit-approval-permission__edit-section__radio-button-outline': !selectedOptionIsUnlimited,
|
||||
'edit-approval-permission__edit-section__radio-button-outline--selected': selectedOptionIsUnlimited,
|
||||
})} />
|
||||
<div className="edit-approval-permission__edit-section__radio-button-fill" />
|
||||
{ selectedOptionIsUnlimited && <div className="edit-approval-permission__edit-section__radio-button-dot" />}
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option-text">
|
||||
<div className={classnames({
|
||||
'edit-approval-permission__edit-section__option-label': !selectedOptionIsUnlimited,
|
||||
'edit-approval-permission__edit-section__option-label--selected': selectedOptionIsUnlimited,
|
||||
})}>
|
||||
{
|
||||
tokenAmount < tokenBalance
|
||||
? t('proposedApprovalLimit')
|
||||
: t('unlimited')
|
||||
}
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option-description" >
|
||||
{ t('spendLimitRequestedBy', [origin]) }
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option-value" >
|
||||
{`${tokenAmount} ${tokenSymbol}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option">
|
||||
<div
|
||||
className="edit-approval-permission__edit-section__radio-button"
|
||||
onClick={() => this.setState({ selectedOptionIsUnlimited: false })}
|
||||
>
|
||||
<div className={classnames({
|
||||
'edit-approval-permission__edit-section__radio-button-outline': selectedOptionIsUnlimited,
|
||||
'edit-approval-permission__edit-section__radio-button-outline--selected': !selectedOptionIsUnlimited,
|
||||
})} />
|
||||
<div className="edit-approval-permission__edit-section__radio-button-fill" />
|
||||
{ !selectedOptionIsUnlimited && <div className="edit-approval-permission__edit-section__radio-button-dot" />}
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option-text">
|
||||
<div className={classnames({
|
||||
'edit-approval-permission__edit-section__option-label': selectedOptionIsUnlimited,
|
||||
'edit-approval-permission__edit-section__option-label--selected': !selectedOptionIsUnlimited,
|
||||
})}>
|
||||
{ t('customSpendLimit') }
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option-description" >
|
||||
{ t('enterMaxSpendLimit') }
|
||||
</div>
|
||||
<div className="edit-approval-permission__edit-section__option-input" >
|
||||
<TextField
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder={ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }
|
||||
onChange={(event) => {
|
||||
this.setState({ customSpendLimit: event.target.value })
|
||||
if (selectedOptionIsUnlimited) {
|
||||
this.setState({ selectedOptionIsUnlimited: false })
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={ this.state.customSpendLimit }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { setCustomAmount, hideModal, customTokenAmount } = this.props
|
||||
const { selectedOptionIsUnlimited, customSpendLimit } = this.state
|
||||
return (
|
||||
<Modal
|
||||
onSubmit={() => {
|
||||
setCustomAmount(!selectedOptionIsUnlimited ? customSpendLimit : '')
|
||||
hideModal()
|
||||
}}
|
||||
submitText={t('save')}
|
||||
submitType="primary"
|
||||
contentClass="edit-approval-permission-modal-content"
|
||||
containerClass="edit-approval-permission-modal-container"
|
||||
submitDisabled={ (customSpendLimit === customTokenAmount) && !selectedOptionIsUnlimited }
|
||||
>
|
||||
{ this.renderModalContent() }
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
|
||||
import EditApprovalPermission from './edit-approval-permission.component'
|
||||
import { getSelectedIdentity } from '../../../../selectors/selectors'
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const modalStateProps = state.appState.modal.modalState.props || {}
|
||||
return {
|
||||
selectedIdentity: getSelectedIdentity(state),
|
||||
...modalStateProps,
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withModalProps,
|
||||
connect(mapStateToProps)
|
||||
)(EditApprovalPermission)
|
@ -0,0 +1 @@
|
||||
export { default } from './edit-approval-permission.container'
|
167
ui/app/components/app/modals/edit-approval-permission/index.scss
Normal file
167
ui/app/components/app/modals/edit-approval-permission/index.scss
Normal file
@ -0,0 +1,167 @@
|
||||
.edit-approval-permission {
|
||||
width: 100%;
|
||||
|
||||
&__header,
|
||||
&__account-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #d2d8dd;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 24px;
|
||||
|
||||
&__close {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
background-image: url("/images/close-gray.svg");
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
&__account-info {
|
||||
justify-content: space-between;
|
||||
padding: 8px 24px;
|
||||
|
||||
&__account,
|
||||
&__balance {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
color: #24292E;
|
||||
}
|
||||
|
||||
&__account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&__balance {
|
||||
color: #6A737D;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-section {
|
||||
padding: 24px;
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #24292E;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: #6A737D;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__radio-button {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
&__option-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__option-label,
|
||||
&__option-label--selected {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #474B4D;
|
||||
}
|
||||
|
||||
&__option-label--selected {
|
||||
color: #037DD6;
|
||||
}
|
||||
|
||||
&__option-description {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: #6A737D;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&__option-value {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
color: #24292E;
|
||||
}
|
||||
|
||||
&__radio-button {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__radio-button-outline,
|
||||
&__radio-button-outline--selected {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #DADCDD;
|
||||
border-radius: 9px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__radio-button-outline--selected {
|
||||
background: #037DD6;
|
||||
}
|
||||
|
||||
&__radio-button-fill {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 7px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__radio-button-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #037DD6;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-approval-permission-modal-content {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.edit-approval-permission-modal-container {
|
||||
max-height: 550px;
|
||||
width: 100%;
|
||||
}
|
@ -9,3 +9,5 @@
|
||||
@import 'metametrics-opt-in-modal/index';
|
||||
|
||||
@import './add-to-addressbook-modal/index';
|
||||
|
||||
@import './edit-approval-permission/index';
|
||||
|
@ -28,6 +28,7 @@ import ClearApprovedOrigins from './clear-approved-origins'
|
||||
import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
|
||||
import ConfirmDeleteNetwork from './confirm-delete-network'
|
||||
import AddToAddressBookModal from './add-to-addressbook-modal'
|
||||
import EditApprovalPermission from './edit-approval-permission'
|
||||
|
||||
const modalContainerBaseStyle = {
|
||||
transform: 'translate3d(-50%, 0, 0px)',
|
||||
@ -304,6 +305,31 @@ const MODALS = {
|
||||
},
|
||||
},
|
||||
|
||||
EDIT_APPROVAL_PERMISSION: {
|
||||
contents: h(EditApprovalPermission),
|
||||
mobileModalStyle: {
|
||||
width: '95vw',
|
||||
height: '100vh',
|
||||
top: '50px',
|
||||
transform: 'none',
|
||||
left: '0',
|
||||
right: '0',
|
||||
margin: '0 auto',
|
||||
},
|
||||
laptopModalStyle: {
|
||||
width: 'auto',
|
||||
height: '0px',
|
||||
top: '80px',
|
||||
left: '0px',
|
||||
transform: 'none',
|
||||
margin: '0 auto',
|
||||
position: 'relative',
|
||||
},
|
||||
contentStyle: {
|
||||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
|
||||
TRANSACTION_CONFIRMED: {
|
||||
disableBackdropClick: true,
|
||||
contents: h(TransactionConfirmed),
|
||||
|
@ -141,11 +141,11 @@
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
transform: scale(1.1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cursor-pointer:active {
|
||||
transform: scale(.95);
|
||||
transform: scale(.97);
|
||||
}
|
||||
|
||||
.cursor-disabled {
|
||||
|
@ -15,6 +15,7 @@ export default function withTokenTracker (WrappedComponent) {
|
||||
this.state = {
|
||||
string: '',
|
||||
symbol: '',
|
||||
balance: '',
|
||||
error: null,
|
||||
}
|
||||
|
||||
@ -78,8 +79,8 @@ export default function withTokenTracker (WrappedComponent) {
|
||||
if (!this.tracker.running) {
|
||||
return
|
||||
}
|
||||
const [{ string, symbol }] = tokens
|
||||
this.setState({ string, symbol, error: null })
|
||||
const [{ string, symbol, balance }] = tokens
|
||||
this.setState({ string, symbol, error: null, balance })
|
||||
}
|
||||
|
||||
removeListeners () {
|
||||
@ -91,13 +92,13 @@ export default function withTokenTracker (WrappedComponent) {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { string, symbol, error } = this.state
|
||||
|
||||
const { balance, string, symbol, error } = this.state
|
||||
return (
|
||||
<WrappedComponent
|
||||
{ ...this.props }
|
||||
string={string}
|
||||
symbol={symbol}
|
||||
tokenTrackerBalance={balance}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
|
@ -128,6 +128,11 @@ export function calcTokenAmount (value, decimals) {
|
||||
return new BigNumber(String(value)).div(multiplier)
|
||||
}
|
||||
|
||||
export function calcTokenValue (value, decimals) {
|
||||
const multiplier = Math.pow(10, Number(decimals || 0))
|
||||
return new BigNumber(String(value)).times(multiplier)
|
||||
}
|
||||
|
||||
export function getTokenValue (tokenParams = []) {
|
||||
const valueData = tokenParams.find(param => param.name === '_value')
|
||||
return valueData && valueData.value
|
||||
|
@ -0,0 +1,223 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Identicon from '../../../components/ui/identicon'
|
||||
import {
|
||||
addressSummary,
|
||||
} from '../../../helpers/utils/util'
|
||||
|
||||
export default class ConfirmApproveContent extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
amount: PropTypes.string,
|
||||
txFeeTotal: PropTypes.string,
|
||||
tokenAmount: PropTypes.string,
|
||||
customTokenAmount: PropTypes.string,
|
||||
tokenSymbol: PropTypes.string,
|
||||
siteImage: PropTypes.string,
|
||||
tokenAddress: PropTypes.string,
|
||||
showCustomizeGasModal: PropTypes.func,
|
||||
showEditApprovalPermissionModal: PropTypes.func,
|
||||
origin: PropTypes.string,
|
||||
setCustomAmount: PropTypes.func,
|
||||
tokenBalance: PropTypes.string,
|
||||
data: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
showFullTxDetails: false,
|
||||
}
|
||||
|
||||
renderApproveContentCard ({
|
||||
symbol,
|
||||
title,
|
||||
showEdit,
|
||||
onEditClick,
|
||||
content,
|
||||
footer,
|
||||
noBorder,
|
||||
}) {
|
||||
return (
|
||||
<div className={classnames({
|
||||
'confirm-approve-content__card': !noBorder,
|
||||
'confirm-approve-content__card--no-border': noBorder,
|
||||
})}>
|
||||
<div className="confirm-approve-content__card-header">
|
||||
<div className="confirm-approve-content__card-header__symbol">{ symbol }</div>
|
||||
<div className="confirm-approve-content__card-header__title">{ title }</div>
|
||||
{ showEdit && <div
|
||||
className="confirm-approve-content__small-blue-text cursor-pointer"
|
||||
onClick={() => onEditClick()}
|
||||
>Edit</div> }
|
||||
</div>
|
||||
<div className="confirm-approve-content__card-content">
|
||||
{ content }
|
||||
</div>
|
||||
{ footer }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Add "Learn Why" with link to the feeAssociatedRequest text
|
||||
renderTransactionDetailsContent () {
|
||||
const { t } = this.context
|
||||
const {
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
} = this.props
|
||||
return (
|
||||
<div className="confirm-approve-content__transaction-details-content">
|
||||
<div className="confirm-approve-content__small-text">
|
||||
{ t('feeAssociatedRequest') }
|
||||
</div>
|
||||
<div className="confirm-approve-content__transaction-details-content__fee">
|
||||
<div className="confirm-approve-content__transaction-details-content__primary-fee">
|
||||
{ fiatTransactionTotal }
|
||||
</div>
|
||||
<div className="confirm-approve-content__transaction-details-content__secondary-fee">
|
||||
{ ethTransactionTotal }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderPermissionContent () {
|
||||
const { t } = this.context
|
||||
const { customTokenAmount, tokenAmount, tokenSymbol, origin, toAddress } = this.props
|
||||
|
||||
return (
|
||||
<div className="flex-column">
|
||||
<div className="confirm-approve-content__small-text">{ t('accessAndSpendNotice', [origin]) }</div>
|
||||
<div className="flex-row">
|
||||
<div className="confirm-approve-content__label">{ t('amountWithColon') }</div>
|
||||
<div className="confirm-approve-content__medium-text">{ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }</div>
|
||||
</div>
|
||||
<div className="flex-row">
|
||||
<div className="confirm-approve-content__label">{ t('toWithColon') }</div>
|
||||
<div className="confirm-approve-content__medium-text">{ addressSummary(toAddress) }</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderDataContent () {
|
||||
const { t } = this.context
|
||||
const { data } = this.props
|
||||
return (
|
||||
<div className="flex-column">
|
||||
<div className="confirm-approve-content__small-text">{ t('functionApprove') }</div>
|
||||
<div className="confirm-approve-content__small-text confirm-approve-content__data__data-block">{ data }</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const {
|
||||
siteImage,
|
||||
tokenAmount,
|
||||
customTokenAmount,
|
||||
origin,
|
||||
tokenSymbol,
|
||||
showCustomizeGasModal,
|
||||
showEditApprovalPermissionModal,
|
||||
setCustomAmount,
|
||||
tokenBalance,
|
||||
} = this.props
|
||||
const { showFullTxDetails } = this.state
|
||||
|
||||
return (
|
||||
<div className={classnames('confirm-approve-content', {
|
||||
'confirm-approve-content--full': showFullTxDetails,
|
||||
})}>
|
||||
<div className="confirm-approve-content__identicon-wrapper">
|
||||
<Identicon
|
||||
className="confirm-approve-content__identicon"
|
||||
diameter={48}
|
||||
address={origin}
|
||||
image={siteImage}
|
||||
/>
|
||||
</div>
|
||||
<div className="confirm-approve-content__title">
|
||||
{ t('allowOriginSpendToken', [origin, tokenSymbol]) }
|
||||
</div>
|
||||
<div className="confirm-approve-content__description">
|
||||
{ t('trustSiteApprovePermission', [origin, tokenSymbol]) }
|
||||
</div>
|
||||
<div
|
||||
className="confirm-approve-content__edit-submission-button-container"
|
||||
>
|
||||
<div
|
||||
className="confirm-approve-content__medium-link-text cursor-pointer"
|
||||
onClick={() => showEditApprovalPermissionModal({ customTokenAmount, tokenAmount, tokenSymbol, setCustomAmount, tokenBalance, origin })}
|
||||
>
|
||||
{ t('editPermission') }
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-approve-content__card-wrapper">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <i className="fa fa-tag" />,
|
||||
title: 'Transaction Fee',
|
||||
showEdit: true,
|
||||
onEditClick: showCustomizeGasModal,
|
||||
content: this.renderTransactionDetailsContent(),
|
||||
noBorder: !showFullTxDetails,
|
||||
footer: <div
|
||||
className="confirm-approve-content__view-full-tx-button-wrapper"
|
||||
onClick={() => this.setState({ showFullTxDetails: !this.state.showFullTxDetails })}
|
||||
>
|
||||
<div className="confirm-approve-content__view-full-tx-button cursor-pointer">
|
||||
<div className="confirm-approve-content__small-blue-text">
|
||||
View full transaction details
|
||||
</div>
|
||||
<i className={classnames({
|
||||
'fa fa-caret-up': showFullTxDetails,
|
||||
'fa fa-caret-down': !showFullTxDetails,
|
||||
})} />
|
||||
</div>
|
||||
</div>,
|
||||
})}
|
||||
</div>
|
||||
|
||||
{
|
||||
showFullTxDetails
|
||||
? (
|
||||
<div className="confirm-approve-content__full-tx-content">
|
||||
<div className="confirm-approve-content__permission">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <img src="/images/user-check.svg" />,
|
||||
title: 'Permission',
|
||||
content: this.renderPermissionContent(),
|
||||
showEdit: true,
|
||||
onEditClick: () => showEditApprovalPermissionModal({
|
||||
customTokenAmount,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
setCustomAmount,
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
<div className="confirm-approve-content__data">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <i className="fa fa-file" />,
|
||||
title: 'Data',
|
||||
content: this.renderDataContent(),
|
||||
noBorder: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-approve-content.component'
|
306
ui/app/pages/confirm-approve/confirm-approve-content/index.scss
Normal file
306
ui/app/pages/confirm-approve/confirm-approve-content/index.scss
Normal file
@ -0,0 +1,306 @@
|
||||
.confirm-approve-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
|
||||
&__identicon-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 22px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&__full-tx-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
width: 390px;
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&__card-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
line-height: 34px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-top: 22px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
color: #6A737D;
|
||||
text-align: center;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&__card,
|
||||
&__card--no-border {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-bottom: 1px solid #D2D8DD;
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
|
||||
&__bold-text {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__thin-text {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: #6A737D;
|
||||
}
|
||||
}
|
||||
|
||||
&__card--no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__card-header {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&__symbol {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&__symbol--aligned {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__title, &__title-value {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
width: 100%;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&__title--aligned {
|
||||
margin-left: 27px;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__card-content {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__card-content--aligned {
|
||||
margin-left: 42px;
|
||||
}
|
||||
|
||||
&__transaction-total-symbol {
|
||||
width: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
|
||||
&__x {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
background: #037DD6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
div:first-of-type {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
div:last-of-type {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__circle {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #037DD6;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&__transaction-details-content {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.confirm-approve-content__small-text {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
&__fee {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__primary-fee {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
&__secondary-fee {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #8C8E94;
|
||||
}
|
||||
}
|
||||
|
||||
&__view-full-tx-button-wrapper {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
margin-bottom: 16px;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
margin-left: 6px;
|
||||
display: flex;
|
||||
color: #3099f2;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__view-full-tx-button {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
&__edit-submission-button-container {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid #D2D8DD;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&__large-text {
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
color: #24292E;
|
||||
}
|
||||
|
||||
&__medium-link-text {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
color: #037DD6;
|
||||
}
|
||||
|
||||
&__medium-text,
|
||||
&__label {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #24292E;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-weight: bold;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__small-text, &__small-blue-text, &__info-row {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: #6A737D;
|
||||
}
|
||||
|
||||
&__small-blue-text {
|
||||
color: #037DD6;
|
||||
}
|
||||
|
||||
&__info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&__data,
|
||||
&__permission {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__permission {
|
||||
.flex-row {
|
||||
margin-top: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__data {
|
||||
&__data-block {
|
||||
overflow-wrap: break-word;
|
||||
margin-right: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-top: 16px;
|
||||
padding-left: 34px;
|
||||
padding-right: 24px;
|
||||
|
||||
.confirm-approve-content__small-text {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-approve-content--full {
|
||||
height: auto;
|
||||
}
|
@ -1,20 +1,109 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base'
|
||||
import ConfirmApproveContent from './confirm-approve-content'
|
||||
import { getCustomTxParamsData } from './confirm-approve.util'
|
||||
import {
|
||||
calcTokenAmount,
|
||||
} from '../../helpers/utils/token-util'
|
||||
|
||||
export default class ConfirmApprove extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
tokenAddress: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
tokenAmount: PropTypes.number,
|
||||
tokenSymbol: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
contractExchangeRate: PropTypes.number,
|
||||
conversionRate: PropTypes.number,
|
||||
currentCurrency: PropTypes.string,
|
||||
showCustomizeGasModal: PropTypes.func,
|
||||
showEditApprovalPermissionModal: PropTypes.func,
|
||||
origin: PropTypes.string,
|
||||
siteImage: PropTypes.string,
|
||||
tokenTrackerBalance: PropTypes.string,
|
||||
data: PropTypes.string,
|
||||
decimals: PropTypes.number,
|
||||
txData: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
tokenAmount: 0,
|
||||
}
|
||||
|
||||
state = {
|
||||
customPermissionAmount: '',
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { tokenAmount } = this.props
|
||||
|
||||
if (tokenAmount !== prevProps.tokenAmount) {
|
||||
this.setState({ customPermissionAmount: tokenAmount })
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { tokenAmount, tokenSymbol } = this.props
|
||||
const {
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
tokenSymbol,
|
||||
tokenAmount,
|
||||
showCustomizeGasModal,
|
||||
showEditApprovalPermissionModal,
|
||||
origin,
|
||||
siteImage,
|
||||
tokenTrackerBalance,
|
||||
data,
|
||||
decimals,
|
||||
txData,
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
...restProps
|
||||
} = this.props
|
||||
const { customPermissionAmount } = this.state
|
||||
|
||||
const tokensText = `${tokenAmount} ${tokenSymbol}`
|
||||
|
||||
const tokenBalance = tokenTrackerBalance
|
||||
? Number(calcTokenAmount(tokenTrackerBalance, decimals)).toPrecision(9)
|
||||
: ''
|
||||
|
||||
return (
|
||||
<ConfirmTokenTransactionBase
|
||||
tokenAmount={tokenAmount}
|
||||
warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
|
||||
<ConfirmTransactionBase
|
||||
toAddress={toAddress}
|
||||
identiconAddress={tokenAddress}
|
||||
showAccountInHeader={true}
|
||||
title={tokensText}
|
||||
contentComponent={<ConfirmApproveContent
|
||||
siteImage={siteImage}
|
||||
tokenAddress={tokenAddress}
|
||||
setCustomAmount={(newAmount) => {
|
||||
this.setState({ customPermissionAmount: newAmount })
|
||||
}}
|
||||
customTokenAmount={String(customPermissionAmount)}
|
||||
tokenAmount={String(tokenAmount)}
|
||||
origin={origin}
|
||||
tokenSymbol={tokenSymbol}
|
||||
tokenBalance={tokenBalance}
|
||||
showCustomizeGasModal={() => showCustomizeGasModal(txData)}
|
||||
showEditApprovalPermissionModal={showEditApprovalPermissionModal}
|
||||
data={data}
|
||||
toAddress={toAddress}
|
||||
ethTransactionTotal={ethTransactionTotal}
|
||||
fiatTransactionTotal={fiatTransactionTotal}
|
||||
/>}
|
||||
hideSenderToRecipient={true}
|
||||
customTxParamsData={customPermissionAmount
|
||||
? getCustomTxParamsData(data, { customPermissionAmount, tokenAmount, decimals })
|
||||
: null
|
||||
}
|
||||
{...restProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +1,102 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
contractExchangeRateSelector,
|
||||
transactionFeeSelector,
|
||||
} from '../../selectors/confirm-transaction'
|
||||
import { showModal } from '../../store/actions'
|
||||
import { tokenSelector } from '../../selectors/tokens'
|
||||
import {
|
||||
getTokenData,
|
||||
} from '../../helpers/utils/transactions.util'
|
||||
import withTokenTracker from '../../helpers/higher-order-components/with-token-tracker'
|
||||
import {
|
||||
calcTokenAmount,
|
||||
getTokenToAddress,
|
||||
getTokenValue,
|
||||
} from '../../helpers/utils/token-util'
|
||||
import ConfirmApprove from './confirm-approve.component'
|
||||
import { approveTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
|
||||
const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { match: { params = {} } } = ownProps
|
||||
const { id: paramsTransactionId } = params
|
||||
const {
|
||||
confirmTransaction,
|
||||
metamask: { currentCurrency, conversionRate, selectedAddressTxList, approvedOrigins, selectedAddress },
|
||||
} = state
|
||||
|
||||
const {
|
||||
txData: { id: transactionId, txParams: { to: tokenAddress, data } = {} } = {},
|
||||
} = confirmTransaction
|
||||
|
||||
const transaction = selectedAddressTxList.find(({ id }) => id === (Number(paramsTransactionId) || transactionId)) || {}
|
||||
|
||||
const {
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
} = transactionFeeSelector(state, transaction)
|
||||
const tokens = tokenSelector(state)
|
||||
const currentToken = tokens && tokens.find(({ address }) => tokenAddress === address)
|
||||
const { decimals, symbol: tokenSymbol } = currentToken || {}
|
||||
|
||||
const tokenData = getTokenData(data)
|
||||
const tokenValue = tokenData && getTokenValue(tokenData.params)
|
||||
const toAddress = tokenData && getTokenToAddress(tokenData.params)
|
||||
const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toNumber()
|
||||
const contractExchangeRate = contractExchangeRateSelector(state)
|
||||
|
||||
const { origin } = transaction
|
||||
const formattedOrigin = origin
|
||||
? origin[0].toUpperCase() + origin.slice(1)
|
||||
: ''
|
||||
|
||||
const { siteImage } = approvedOrigins[origin] || {}
|
||||
return {
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
tokenAmount,
|
||||
currentCurrency,
|
||||
conversionRate,
|
||||
contractExchangeRate,
|
||||
fiatTransactionTotal,
|
||||
ethTransactionTotal,
|
||||
tokenSymbol,
|
||||
siteImage,
|
||||
token: { address: tokenAddress },
|
||||
userAddress: selectedAddress,
|
||||
origin: formattedOrigin,
|
||||
data,
|
||||
decimals: Number(decimals),
|
||||
txData: transaction,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ConfirmApprove)
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })),
|
||||
showEditApprovalPermissionModal: ({
|
||||
tokenAmount,
|
||||
customTokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
setCustomAmount,
|
||||
origin,
|
||||
}) => dispatch(showModal({
|
||||
name: 'EDIT_APPROVAL_PERMISSION',
|
||||
tokenAmount,
|
||||
customTokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
setCustomAmount,
|
||||
origin,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withTokenTracker,
|
||||
)(ConfirmApprove)
|
||||
|
||||
|
28
ui/app/pages/confirm-approve/confirm-approve.util.js
Normal file
28
ui/app/pages/confirm-approve/confirm-approve.util.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { decimalToHex } from '../../helpers/utils/conversions.util'
|
||||
import { calcTokenValue } from '../../helpers/utils/token-util.js'
|
||||
|
||||
export function getCustomTxParamsData (data, { customPermissionAmount, tokenAmount, decimals }) {
|
||||
if (customPermissionAmount) {
|
||||
const tokenValue = decimalToHex(calcTokenValue(tokenAmount, decimals))
|
||||
|
||||
const re = new RegExp('(^.+)' + tokenValue + '$')
|
||||
const matches = re.exec(data)
|
||||
|
||||
if (!matches || !matches[1]) {
|
||||
return data
|
||||
}
|
||||
let dataWithoutCurrentAmount = matches[1]
|
||||
const customPermissionValue = decimalToHex(calcTokenValue(Number(customPermissionAmount), decimals))
|
||||
|
||||
const differenceInLengths = customPermissionValue.length - tokenValue.length
|
||||
const zeroModifier = dataWithoutCurrentAmount.length - differenceInLengths
|
||||
if (differenceInLengths > 0) {
|
||||
dataWithoutCurrentAmount = dataWithoutCurrentAmount.slice(0, zeroModifier)
|
||||
} else if (differenceInLengths < 0) {
|
||||
dataWithoutCurrentAmount = dataWithoutCurrentAmount.padEnd(zeroModifier, 0)
|
||||
}
|
||||
|
||||
const customTxParamsData = dataWithoutCurrentAmount + customPermissionValue
|
||||
return customTxParamsData
|
||||
}
|
||||
}
|
1
ui/app/pages/confirm-approve/index.scss
Normal file
1
ui/app/pages/confirm-approve/index.scss
Normal file
@ -0,0 +1 @@
|
||||
@import 'confirm-approve-content/index';
|
@ -105,6 +105,8 @@ export default class ConfirmTransactionBase extends Component {
|
||||
getNextNonce: PropTypes.func,
|
||||
nextNonce: PropTypes.number,
|
||||
tryReverseResolveAddress: PropTypes.func.isRequired,
|
||||
hideSenderToRecipient: PropTypes.bool,
|
||||
showAccountInHeader: PropTypes.bool,
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -645,6 +647,8 @@ export default class ConfirmTransactionBase extends Component {
|
||||
warning,
|
||||
unapprovedTxCount,
|
||||
transactionCategory,
|
||||
hideSenderToRecipient,
|
||||
showAccountInHeader,
|
||||
} = this.props
|
||||
const { submitting, submitError, submitWarning } = this.state
|
||||
|
||||
@ -655,6 +659,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
<ConfirmPageContainer
|
||||
fromName={fromName}
|
||||
fromAddress={fromAddress}
|
||||
showAccountInHeader={showAccountInHeader}
|
||||
toName={toName}
|
||||
toAddress={toAddress}
|
||||
toEns={toEns}
|
||||
@ -693,6 +698,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
onCancelAll={() => this.handleCancelAll()}
|
||||
onCancel={() => this.handleCancel()}
|
||||
onSubmit={() => this.handleSubmit()}
|
||||
hideSenderToRecipient={hideSenderToRecipient}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ const customNonceMerge = txData => customNonceValue ? ({
|
||||
}) : txData
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { toAddress: propsToAddress, match: { params = {} } } = ownProps
|
||||
const { toAddress: propsToAddress, customTxParamsData, match: { params = {} } } = ownProps
|
||||
const { id: paramsTransactionId } = params
|
||||
const { showFiatInTestnets } = preferencesSelector(state)
|
||||
const isMainnet = getIsMainnet(state)
|
||||
@ -133,6 +133,17 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const methodData = getKnownMethodData(state, data) || {}
|
||||
|
||||
let fullTxData = { ...txData, ...transaction }
|
||||
if (customTxParamsData) {
|
||||
fullTxData = {
|
||||
...fullTxData,
|
||||
txParams: {
|
||||
...fullTxData.txParams,
|
||||
data: customTxParamsData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
balance,
|
||||
fromAddress,
|
||||
@ -150,7 +161,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
hexTransactionAmount,
|
||||
hexTransactionFee,
|
||||
hexTransactionTotal,
|
||||
txData: { ...txData, ...transaction },
|
||||
txData: fullTxData,
|
||||
tokenData,
|
||||
methodData,
|
||||
tokenProps,
|
||||
|
@ -11,3 +11,5 @@
|
||||
@import 'first-time-flow/index';
|
||||
|
||||
@import 'keychains/index';
|
||||
|
||||
@import 'confirm-approve/index';
|
||||
|
Loading…
Reference in New Issue
Block a user