mirror of
https://github.com/status-im/safe-react.git
synced 2025-02-17 12:07:09 +00:00
(Feature) Incoming transactions (#333)
* Add `blockNumber` to transactions model * Create `incomingTransaction` node in store and load it along with `transactions` * Add incoming transfers to the Transactions table * Rename `transactionHash` to `executionTxHash` for better incoming/outgoing txs unification in Transactions table * Add incoming transactions details * Add transaction type icon in table row * Add snackbar notification for incoming txs * Make incoming transaction snackbar to show on any tab * Use makeStyles hooks * Fix incoming amounts conversion from wei * Make concurrent promise calls * Use date to calculate transactions ids * Prevent repeating messages - also move logic to display snack bar into the notifications middleware * Merge transactions and incomingTxs to the transactions selector * Show 'Multiple incoming transfers' if they are more than 3 * Prevent incoming transactions snack bar for first-timer users * Set ID as the default order * Use constant for _incoming_ type
This commit is contained in:
parent
1e1592f957
commit
d69e5fca7f
@ -52,6 +52,9 @@ export const getTxServiceHost = () => {
|
||||
export const getTxServiceUriFrom = (safeAddress: string) =>
|
||||
`safes/${safeAddress}/transactions/`
|
||||
|
||||
export const getIncomingTxServiceUriTo = (safeAddress: string) =>
|
||||
`safes/${safeAddress}/incoming-transactions/`
|
||||
|
||||
export const getRelayUrl = () => getConfig()[RELAY_API_URL]
|
||||
|
||||
export const signaturesViaMetamask = () => {
|
||||
|
@ -8,6 +8,7 @@ import loadActiveTokens from '~/logic/tokens/store/actions/loadActiveTokens'
|
||||
import loadDefaultSafe from '~/routes/safe/store/actions/loadDefaultSafe'
|
||||
import loadSafesFromStorage from '~/routes/safe/store/actions/loadSafesFromStorage'
|
||||
import { store } from '~/store'
|
||||
import verifyRecurringUser from '~/utils/verifyRecurringUser'
|
||||
|
||||
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
|
||||
|
||||
@ -21,6 +22,7 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
store.dispatch(loadActiveTokens())
|
||||
store.dispatch(loadSafesFromStorage())
|
||||
store.dispatch(loadDefaultSafe())
|
||||
verifyRecurringUser()
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
||||
|
@ -40,6 +40,7 @@ export type Notifications = {
|
||||
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: Notification,
|
||||
TX_FAILED_MSG: Notification,
|
||||
TX_WAITING_MSG: Notification,
|
||||
TX_INCOMING_MSG: Notification,
|
||||
|
||||
// Approval Transactions
|
||||
TX_CONFIRMATION_PENDING_MSG: Notification,
|
||||
@ -131,6 +132,13 @@ export const NOTIFICATIONS: Notifications = {
|
||||
variant: WARNING, persist: true, preventDuplicate: true,
|
||||
},
|
||||
},
|
||||
TX_INCOMING_MSG: {
|
||||
message: 'Incoming transfer: ',
|
||||
key: 'TX_INCOMING_MSG',
|
||||
options: {
|
||||
variant: SUCCESS, persist: false, autoHideDuration: longDuration, preventDuplicate: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Approval Transactions
|
||||
TX_CONFIRMATION_PENDING_MSG: {
|
||||
|
11
src/logic/safe/transactions/incomingTxHistory.js
Normal file
11
src/logic/safe/transactions/incomingTxHistory.js
Normal file
@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { getIncomingTxServiceUriTo, getTxServiceHost } from '~/config'
|
||||
|
||||
export const buildIncomingTxServiceUrl = (safeAddress: string) => {
|
||||
const host = getTxServiceHost()
|
||||
const address = getWeb3().utils.toChecksumAddress(safeAddress)
|
||||
const base = getIncomingTxServiceUriTo(address)
|
||||
|
||||
return `${host}${base}`
|
||||
}
|
@ -63,7 +63,6 @@ const Layout = (props: Props) => {
|
||||
blacklistedTokens,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchTransactions,
|
||||
activateTokensByBalance,
|
||||
fetchTokens,
|
||||
updateSafe,
|
||||
@ -178,7 +177,6 @@ const Layout = (props: Props) => {
|
||||
owners={safe.owners}
|
||||
nonce={safe.nonce}
|
||||
transactions={transactions}
|
||||
fetchTransactions={fetchTransactions}
|
||||
safeAddress={address}
|
||||
userAddress={userAddress}
|
||||
currentNetwork={network}
|
||||
|
@ -0,0 +1,54 @@
|
||||
// @flow
|
||||
import React from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import EtherscanLink from '~/components/EtherscanLink'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Block from '~/components/layout/Block'
|
||||
import { md, lg } from '~/theme/variables'
|
||||
import { getIncomingTxAmount } from '~/routes/safe/components/Transactions/TxsTable/columns'
|
||||
|
||||
export const TRANSACTIONS_DESC_INCOMING_TEST_ID = 'tx-description-incoming'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
txDataContainer: {
|
||||
padding: `${lg} ${md}`,
|
||||
borderRight: '2px solid rgb(232, 231, 230)',
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
tx: IncomingTransaction,
|
||||
}
|
||||
|
||||
type TransferDescProps = {
|
||||
value: string,
|
||||
from: string,
|
||||
}
|
||||
|
||||
const TransferDescription = ({ value = '', from }: TransferDescProps) => (
|
||||
<Paragraph noMargin data-testid={TRANSACTIONS_DESC_INCOMING_TEST_ID}>
|
||||
<Bold>
|
||||
Received
|
||||
{' '}
|
||||
{value}
|
||||
{' '}
|
||||
from:
|
||||
</Bold>
|
||||
<br />
|
||||
<EtherscanLink type="address" value={from} />
|
||||
</Paragraph>
|
||||
)
|
||||
|
||||
const IncomingTxDescription = ({ tx }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Block className={classes.txDataContainer}>
|
||||
<TransferDescription value={getIncomingTxAmount(tx)} from={tx.from} />
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default IncomingTxDescription
|
@ -18,6 +18,8 @@ import CancelTxModal from './CancelTxModal'
|
||||
import ApproveTxModal from './ApproveTxModal'
|
||||
import { styles } from './style'
|
||||
import { formatDate } from '../columns'
|
||||
import IncomingTxDescription from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/IncomingTxDescription'
|
||||
import { INCOMING_TX_TYPE } from '~/routes/safe/store/models/incomingTransaction'
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
@ -59,15 +61,15 @@ const ExpandedTx = ({
|
||||
const openApproveModal = () => setOpenModal('approveTx')
|
||||
const openCancelModal = () => setOpenModal('cancelTx')
|
||||
const closeModal = () => setOpenModal(null)
|
||||
const thresholdReached = threshold <= tx.confirmations.size
|
||||
const canExecute = nonce === tx.nonce
|
||||
const thresholdReached = tx.type !== INCOMING_TX_TYPE && threshold <= tx.confirmations.size
|
||||
const canExecute = tx.type !== INCOMING_TX_TYPE && nonce === tx.nonce
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.expandedTxBlock}>
|
||||
<Row>
|
||||
<Col xs={6} layout="column">
|
||||
<Block className={classes.txDataContainer}>
|
||||
<Block className={classes.txDataContainer} style={tx.type === INCOMING_TX_TYPE ? { borderRight: '2px solid rgb(232, 231, 230)' } : {}}>
|
||||
<Block align="left" className={classes.txData}>
|
||||
<Bold className={classes.txHash}>TX hash:</Bold>
|
||||
{tx.executionTxHash ? (
|
||||
@ -82,53 +84,70 @@ const ExpandedTx = ({
|
||||
{txStatusToLabel[tx.status]}
|
||||
</Span>
|
||||
</Paragraph>
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX created: </Bold>
|
||||
{formatDate(tx.submissionDate)}
|
||||
</Paragraph>
|
||||
{tx.executionDate && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX executed: </Bold>
|
||||
{formatDate(tx.executionDate)}
|
||||
</Paragraph>
|
||||
)}
|
||||
{tx.refundParams && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX refund: </Bold>
|
||||
max.
|
||||
{' '}
|
||||
{tx.refundParams.fee}
|
||||
{' '}
|
||||
{tx.refundParams.symbol}
|
||||
</Paragraph>
|
||||
)}
|
||||
{tx.operation === 1 && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>Delegate Call</Bold>
|
||||
</Paragraph>
|
||||
)}
|
||||
{tx.operation === 2 && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>Contract Creation</Bold>
|
||||
</Paragraph>
|
||||
{tx.type === INCOMING_TX_TYPE ? (
|
||||
<>
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX fee: </Bold>
|
||||
{tx.fee}
|
||||
</Paragraph>
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX created: </Bold>
|
||||
{formatDate(tx.executionDate)}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX created: </Bold>
|
||||
{formatDate(tx.submissionDate)}
|
||||
</Paragraph>
|
||||
{tx.executionDate && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX executed: </Bold>
|
||||
{formatDate(tx.executionDate)}
|
||||
</Paragraph>
|
||||
)}
|
||||
{tx.refundParams && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX refund: </Bold>
|
||||
max.
|
||||
{' '}
|
||||
{tx.refundParams.fee}
|
||||
{' '}
|
||||
{tx.refundParams.symbol}
|
||||
</Paragraph>
|
||||
)}
|
||||
{tx.operation === 1 && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>Delegate Call</Bold>
|
||||
</Paragraph>
|
||||
)}
|
||||
{tx.operation === 2 && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>Contract Creation</Bold>
|
||||
</Paragraph>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Block>
|
||||
<Hairline />
|
||||
<TxDescription tx={tx} />
|
||||
{tx.type === INCOMING_TX_TYPE ? <IncomingTxDescription tx={tx} /> : <TxDescription tx={tx} />}
|
||||
</Col>
|
||||
<OwnersColumn
|
||||
tx={tx}
|
||||
owners={owners}
|
||||
granted={granted}
|
||||
canExecute={canExecute}
|
||||
threshold={threshold}
|
||||
userAddress={userAddress}
|
||||
thresholdReached={thresholdReached}
|
||||
safeAddress={safeAddress}
|
||||
onTxConfirm={openApproveModal}
|
||||
onTxCancel={openCancelModal}
|
||||
onTxExecute={openApproveModal}
|
||||
/>
|
||||
{tx.type !== INCOMING_TX_TYPE && (
|
||||
<OwnersColumn
|
||||
tx={tx}
|
||||
owners={owners}
|
||||
granted={granted}
|
||||
canExecute={canExecute}
|
||||
threshold={threshold}
|
||||
userAddress={userAddress}
|
||||
thresholdReached={thresholdReached}
|
||||
safeAddress={safeAddress}
|
||||
onTxConfirm={openApproveModal}
|
||||
onTxCancel={openCancelModal}
|
||||
onTxExecute={openApproveModal}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</Block>
|
||||
{openModal === 'cancelTx' && (
|
||||
|
@ -5,11 +5,12 @@ import { BigNumber } from 'bignumber.js'
|
||||
import { List } from 'immutable'
|
||||
import TxType from './TxType'
|
||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { INCOMING_TX_TYPE, type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
import { type SortRow, buildOrderFieldFrom } from '~/components/Table/sorting'
|
||||
import { type Column } from '~/components/Table/TableHead'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
export const TX_TABLE_NONCE_ID = 'nonce'
|
||||
export const TX_TABLE_ID = 'id'
|
||||
export const TX_TABLE_TYPE_ID = 'type'
|
||||
export const TX_TABLE_DATE_ID = 'date'
|
||||
export const TX_TABLE_AMOUNT_ID = 'amount'
|
||||
@ -18,9 +19,10 @@ export const TX_TABLE_RAW_TX_ID = 'tx'
|
||||
export const TX_TABLE_EXPAND_ICON = 'expand'
|
||||
|
||||
type TxData = {
|
||||
nonce: number,
|
||||
id: number,
|
||||
type: React.ReactNode,
|
||||
date: string,
|
||||
dateOrder?: number,
|
||||
amount: number | string,
|
||||
tx: Transaction,
|
||||
status?: string,
|
||||
@ -28,6 +30,11 @@ type TxData = {
|
||||
|
||||
export const formatDate = (date: string): string => format(parseISO(date), 'MMM d, yyyy - HH:mm:ss')
|
||||
|
||||
export const getIncomingTxAmount = (tx: IncomingTransaction) => {
|
||||
const txAmount = tx.value ? `${new BigNumber(tx.value).div(`1e${tx.decimals}`).toFixed()}` : 'n/a'
|
||||
return `${txAmount} ${tx.symbol || 'n/a'}`
|
||||
}
|
||||
|
||||
export const getTxAmount = (tx: Transaction) => {
|
||||
const web3 = getWeb3()
|
||||
const { toBN, fromWei } = web3.utils
|
||||
@ -46,46 +53,56 @@ export const getTxAmount = (tx: Transaction) => {
|
||||
|
||||
export type TransactionRow = SortRow<TxData>
|
||||
|
||||
export const getTxTableData = (transactions: List<Transaction>): List<TransactionRow> => {
|
||||
const rows = transactions.map((tx: Transaction) => {
|
||||
const txDate = tx.isExecuted ? tx.executionDate : tx.submissionDate
|
||||
let txType = 'outgoing'
|
||||
if (tx.modifySettingsTx) {
|
||||
txType = 'settings'
|
||||
} else if (tx.cancellationTx) {
|
||||
txType = 'cancellation'
|
||||
} else if (tx.customTx) {
|
||||
txType = 'custom'
|
||||
} else if (tx.creationTx) {
|
||||
txType = 'creation'
|
||||
const getIncomingTxTableData = (tx: IncomingTransaction): TransactionRow => ({
|
||||
[TX_TABLE_ID]: tx.blockNumber,
|
||||
[TX_TABLE_TYPE_ID]: <TxType txType="incoming" />,
|
||||
[TX_TABLE_DATE_ID]: formatDate(tx.executionDate),
|
||||
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: getTime(parseISO(tx.executionDate)),
|
||||
[TX_TABLE_AMOUNT_ID]: getIncomingTxAmount(tx),
|
||||
[TX_TABLE_STATUS_ID]: tx.status,
|
||||
[TX_TABLE_RAW_TX_ID]: tx,
|
||||
})
|
||||
|
||||
const getTransactionTableData = (tx: Transaction): TransactionRow => {
|
||||
const txDate = tx.isExecuted ? tx.executionDate : tx.submissionDate
|
||||
|
||||
let txType = 'outgoing'
|
||||
if (tx.modifySettingsTx) {
|
||||
txType = 'settings'
|
||||
} else if (tx.cancellationTx) {
|
||||
txType = 'cancellation'
|
||||
} else if (tx.customTx) {
|
||||
txType = 'custom'
|
||||
} else if (tx.creationTx) {
|
||||
txType = 'creation'
|
||||
}
|
||||
|
||||
return {
|
||||
[TX_TABLE_ID]: tx.blockNumber,
|
||||
[TX_TABLE_TYPE_ID]: <TxType txType={txType} />,
|
||||
[TX_TABLE_DATE_ID]: tx.isExecuted
|
||||
? tx.executionDate && formatDate(tx.executionDate)
|
||||
: tx.submissionDate && formatDate(tx.submissionDate),
|
||||
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: txDate ? getTime(parseISO(txDate)) : null,
|
||||
[TX_TABLE_AMOUNT_ID]: getTxAmount(tx),
|
||||
[TX_TABLE_STATUS_ID]: tx.status,
|
||||
[TX_TABLE_RAW_TX_ID]: tx,
|
||||
}
|
||||
}
|
||||
|
||||
export const getTxTableData = (transactions: List<Transaction | IncomingTransaction>): List<TransactionRow> => {
|
||||
return transactions.map((tx) => {
|
||||
if (tx.type === INCOMING_TX_TYPE) {
|
||||
return getIncomingTxTableData(tx)
|
||||
}
|
||||
|
||||
let txIndex = 1
|
||||
if (tx.nonce) {
|
||||
txIndex = tx.nonce + 2
|
||||
} else if (tx.nonce === 0) {
|
||||
txIndex = 2
|
||||
}
|
||||
|
||||
return {
|
||||
[TX_TABLE_NONCE_ID]: txIndex,
|
||||
[TX_TABLE_TYPE_ID]: <TxType txType={txType} />,
|
||||
[TX_TABLE_DATE_ID]: tx.isExecuted
|
||||
? tx.executionDate && formatDate(tx.executionDate)
|
||||
: tx.submissionDate && formatDate(tx.submissionDate),
|
||||
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: txDate ? getTime(parseISO(txDate)) : null,
|
||||
[TX_TABLE_AMOUNT_ID]: getTxAmount(tx),
|
||||
[TX_TABLE_STATUS_ID]: tx.status,
|
||||
[TX_TABLE_RAW_TX_ID]: tx,
|
||||
}
|
||||
return getTransactionTableData(tx)
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export const generateColumns = () => {
|
||||
const nonceColumn: Column = {
|
||||
id: TX_TABLE_NONCE_ID,
|
||||
id: TX_TABLE_ID,
|
||||
disablePadding: false,
|
||||
label: 'Id',
|
||||
custom: false,
|
||||
|
@ -17,7 +17,11 @@ import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
import ExpandedTxComponent from './ExpandedTx'
|
||||
import {
|
||||
getTxTableData, generateColumns, TX_TABLE_NONCE_ID, type TransactionRow, TX_TABLE_RAW_TX_ID,
|
||||
getTxTableData,
|
||||
generateColumns,
|
||||
TX_TABLE_ID,
|
||||
TX_TABLE_RAW_TX_ID,
|
||||
type TransactionRow,
|
||||
} from './columns'
|
||||
import { styles } from './style'
|
||||
import Status from './Status'
|
||||
@ -63,12 +67,19 @@ const TxsTable = ({
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter((c) => !c.custom)
|
||||
const filteredData = getTxTableData(transactions)
|
||||
.sort(({ dateOrder: a }, { dateOrder: b }) => {
|
||||
if (!a || !b) {
|
||||
return 0
|
||||
}
|
||||
return a - b
|
||||
})
|
||||
.map((tx, id) => ({ ...tx, id }))
|
||||
|
||||
return (
|
||||
<Block className={classes.container}>
|
||||
<Table
|
||||
label="Transactions"
|
||||
defaultOrderBy={TX_TABLE_NONCE_ID}
|
||||
defaultOrderBy={TX_TABLE_ID}
|
||||
defaultOrder="desc"
|
||||
defaultRowsPerPage={25}
|
||||
columns={columns}
|
||||
|
@ -1,26 +1,24 @@
|
||||
// @flow
|
||||
import React, { useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { List } from 'immutable'
|
||||
import TxsTable from '~/routes/safe/components/Transactions/TxsTable'
|
||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
|
||||
type Props = {
|
||||
safeAddress: string,
|
||||
threshold: number,
|
||||
transactions: List<Transaction>,
|
||||
transactions: List<Transaction | IncomingTransaction>,
|
||||
owners: List<Owner>,
|
||||
userAddress: string,
|
||||
granted: boolean,
|
||||
createTransaction: Function,
|
||||
processTransaction: Function,
|
||||
fetchTransactions: Function,
|
||||
currentNetwork: string,
|
||||
nonce: number,
|
||||
}
|
||||
|
||||
const TIMEOUT = 5000
|
||||
|
||||
const Transactions = ({
|
||||
transactions = List(),
|
||||
owners,
|
||||
@ -30,36 +28,21 @@ const Transactions = ({
|
||||
safeAddress,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchTransactions,
|
||||
currentNetwork,
|
||||
nonce,
|
||||
}: Props) => {
|
||||
let intervalId: IntervalID
|
||||
|
||||
useEffect(() => {
|
||||
fetchTransactions(safeAddress)
|
||||
|
||||
intervalId = setInterval(() => {
|
||||
fetchTransactions(safeAddress)
|
||||
}, TIMEOUT)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [safeAddress])
|
||||
|
||||
return (
|
||||
<TxsTable
|
||||
transactions={transactions}
|
||||
threshold={threshold}
|
||||
owners={owners}
|
||||
userAddress={userAddress}
|
||||
currentNetwork={currentNetwork}
|
||||
granted={granted}
|
||||
safeAddress={safeAddress}
|
||||
createTransaction={createTransaction}
|
||||
processTransaction={processTransaction}
|
||||
nonce={nonce}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}: Props) => (
|
||||
<TxsTable
|
||||
transactions={transactions}
|
||||
threshold={threshold}
|
||||
owners={owners}
|
||||
userAddress={userAddress}
|
||||
currentNetwork={currentNetwork}
|
||||
granted={granted}
|
||||
safeAddress={safeAddress}
|
||||
createTransaction={createTransaction}
|
||||
processTransaction={processTransaction}
|
||||
nonce={nonce}
|
||||
/>
|
||||
)
|
||||
|
||||
export default Transactions
|
||||
|
@ -99,11 +99,13 @@ class SafeView extends React.Component<Props, State> {
|
||||
activeTokens,
|
||||
fetchTokenBalances,
|
||||
fetchEtherBalance,
|
||||
fetchTransactions,
|
||||
checkAndUpdateSafeOwners,
|
||||
} = this.props
|
||||
checkAndUpdateSafeOwners(safeUrl)
|
||||
fetchTokenBalances(safeUrl, activeTokens)
|
||||
fetchEtherBalance(safeUrl)
|
||||
fetchTransactions(safeUrl)
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -119,7 +121,6 @@ class SafeView extends React.Component<Props, State> {
|
||||
tokens,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchTransactions,
|
||||
activateTokensByBalance,
|
||||
fetchTokens,
|
||||
updateSafe,
|
||||
@ -139,7 +140,6 @@ class SafeView extends React.Component<Props, State> {
|
||||
granted={granted}
|
||||
createTransaction={createTransaction}
|
||||
processTransaction={processTransaction}
|
||||
fetchTransactions={fetchTransactions}
|
||||
activateTokensByBalance={activateTokensByBalance}
|
||||
fetchTokens={fetchTokens}
|
||||
updateSafe={updateSafe}
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
safeActiveTokensSelector,
|
||||
safeBalancesSelector,
|
||||
safeBlacklistedTokensSelector,
|
||||
safeTransactionsSelector,
|
||||
safeIncomingTransactionsSelector,
|
||||
type RouterProps,
|
||||
type SafeSelectorProps,
|
||||
} from '~/routes/safe/store/selectors'
|
||||
@ -14,12 +16,12 @@ import { type Safe } from '~/routes/safe/store/models/safe'
|
||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
import { type GlobalState } from '~/store'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { type Transaction, type TransactionStatus } from '~/routes/safe/store/models/transaction'
|
||||
import { safeParamAddressSelector } from '../store/selectors'
|
||||
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
|
||||
export type SelectorProps = {
|
||||
safe: SafeSelectorProps,
|
||||
@ -30,7 +32,7 @@ export type SelectorProps = {
|
||||
userAddress: string,
|
||||
network: string,
|
||||
safeUrl: string,
|
||||
transactions: List<Transaction>,
|
||||
transactions: List<Transaction | IncomingTransaction>,
|
||||
}
|
||||
|
||||
const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): TransactionStatus => {
|
||||
@ -111,11 +113,12 @@ const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List<Token>
|
||||
},
|
||||
)
|
||||
|
||||
const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction>> = createSelector(
|
||||
const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction | IncomingTransaction>> = createSelector(
|
||||
safeSelector,
|
||||
userAccountSelector,
|
||||
safeTransactionsSelector,
|
||||
(safe, userAddress, transactions) => {
|
||||
safeIncomingTransactionsSelector,
|
||||
(safe, userAddress, transactions, incomingTransactions) => {
|
||||
const extendedTransactions = transactions.map((tx: Transaction) => {
|
||||
let extendedTx = tx
|
||||
|
||||
@ -136,7 +139,7 @@ const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Tran
|
||||
return extendedTx.set('status', getTxStatus(extendedTx, userAddress, safe))
|
||||
})
|
||||
|
||||
return extendedTransactions
|
||||
return List([...extendedTransactions, ...incomingTransactions])
|
||||
},
|
||||
)
|
||||
|
||||
|
6
src/routes/safe/store/actions/addIncomingTransactions.js
Normal file
6
src/routes/safe/store/actions/addIncomingTransactions.js
Normal file
@ -0,0 +1,6 @@
|
||||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_INCOMING_TRANSACTIONS = 'ADD_INCOMING_TRANSACTIONS'
|
||||
|
||||
export const addIncomingTransactions = createAction<string, *>(ADD_INCOMING_TRANSACTIONS)
|
@ -1,21 +1,28 @@
|
||||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import axios from 'axios'
|
||||
import bn from 'bignumber.js'
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import { makeTransaction, type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { makeIncomingTransaction, type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import { buildTxServiceUrl, type TxServiceType } from '~/logic/safe/transactions/txHistory'
|
||||
import { buildIncomingTxServiceUrl } from '~/logic/safe/transactions/incomingTxHistory'
|
||||
import { getOwners } from '~/logic/safe/utils'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { addTransactions } from './addTransactions'
|
||||
import { addIncomingTransactions } from './addIncomingTransactions'
|
||||
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
|
||||
import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
|
||||
import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi'
|
||||
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import { enhanceSnackbarForAction, SUCCESS } from '~/logic/notifications'
|
||||
import { getIncomingTxAmount } from '~/routes/safe/components/Transactions/TxsTable/columns'
|
||||
|
||||
let web3
|
||||
|
||||
@ -32,6 +39,7 @@ type TxServiceModel = {
|
||||
data: string,
|
||||
operation: number,
|
||||
nonce: number,
|
||||
blockNumber: number,
|
||||
safeTxGas: number,
|
||||
baseGas: number,
|
||||
gasPrice: number,
|
||||
@ -46,6 +54,15 @@ type TxServiceModel = {
|
||||
transactionHash: string,
|
||||
}
|
||||
|
||||
type IncomingTxServiceModel = {
|
||||
blockNumber: number,
|
||||
transactionHash: string,
|
||||
to: string,
|
||||
value: number,
|
||||
tokenAddress: string,
|
||||
from: string,
|
||||
}
|
||||
|
||||
export const buildTransactionFrom = async (
|
||||
safeAddress: string,
|
||||
tx: TxServiceModel,
|
||||
@ -122,6 +139,7 @@ export const buildTransactionFrom = async (
|
||||
return makeTransaction({
|
||||
symbol,
|
||||
nonce: tx.nonce,
|
||||
blockNumber: tx.blockNumber,
|
||||
value: tx.value.toString(),
|
||||
confirmations,
|
||||
decimals,
|
||||
@ -172,6 +190,43 @@ const addMockSafeCreationTx = (safeAddress) => [{
|
||||
creationTx: true,
|
||||
}]
|
||||
|
||||
export const buildIncomingTransactionFrom = async (tx: IncomingTxServiceModel) => {
|
||||
let symbol = 'ETH'
|
||||
let decimals = 18
|
||||
|
||||
const whenExecutionDate = web3.eth.getBlock(tx.blockNumber)
|
||||
.then(({ timestamp }) => new Date(timestamp * 1000).toISOString())
|
||||
const whenFee = web3.eth.getTransaction(tx.transactionHash).then((t) => bn(t.gas).div(t.gasPrice).toFixed())
|
||||
const [executionDate, fee] = await Promise.all([whenExecutionDate, whenFee])
|
||||
|
||||
if (tx.tokenAddress) {
|
||||
try {
|
||||
const tokenContract = await getHumanFriendlyToken()
|
||||
const tokenInstance = await tokenContract.at(tx.tokenAddress)
|
||||
const [tokenSymbol, tokenDecimals] = await Promise.all([tokenInstance.symbol(), tokenInstance.decimals()])
|
||||
symbol = tokenSymbol
|
||||
decimals = tokenDecimals
|
||||
} catch (err) {
|
||||
const { methods } = new web3.eth.Contract(ALTERNATIVE_TOKEN_ABI, tx.to)
|
||||
const [tokenSymbol, tokenDecimals] = await Promise.all([methods.symbol, methods.decimals].map((m) => m().call()))
|
||||
symbol = web3.utils.toAscii(tokenSymbol)
|
||||
decimals = tokenDecimals
|
||||
}
|
||||
}
|
||||
|
||||
const { transactionHash, ...incomingTx } = tx
|
||||
|
||||
return makeIncomingTransaction({
|
||||
...incomingTx,
|
||||
symbol,
|
||||
decimals,
|
||||
fee,
|
||||
executionDate,
|
||||
executionTxHash: transactionHash,
|
||||
safeTxHash: transactionHash,
|
||||
})
|
||||
}
|
||||
|
||||
export const loadSafeTransactions = async (safeAddress: string) => {
|
||||
web3 = await getWeb3()
|
||||
|
||||
@ -189,10 +244,23 @@ export const loadSafeTransactions = async (safeAddress: string) => {
|
||||
const txsRecord = await Promise.all(
|
||||
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx)),
|
||||
)
|
||||
|
||||
return Map().set(safeAddress, List(txsRecord))
|
||||
}
|
||||
|
||||
export const loadSafeIncomingTransactions = async (safeAddress: string) => {
|
||||
const url = buildIncomingTxServiceUrl(safeAddress)
|
||||
const response = await axios.get(url)
|
||||
const incomingTransactions: IncomingTxServiceModel[] = response.data.results
|
||||
const incomingTxsRecord = await Promise.all(incomingTransactions.map(buildIncomingTransactionFrom))
|
||||
|
||||
return Map().set(safeAddress, List(incomingTxsRecord))
|
||||
}
|
||||
|
||||
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
const transactions: Map<string, List<Transaction>> = await loadSafeTransactions(safeAddress)
|
||||
return dispatch(addTransactions(transactions))
|
||||
const incomingTransactions: Map<string, List<IncomingTransaction>> = await loadSafeIncomingTransactions(safeAddress)
|
||||
|
||||
dispatch(addTransactions(transactions))
|
||||
dispatch(addIncomingTransactions(incomingTransactions))
|
||||
}
|
||||
|
@ -1,16 +1,24 @@
|
||||
// @flow
|
||||
import type { AnyAction, Store } from 'redux'
|
||||
import { push } from 'connected-react-router'
|
||||
import { Map } from 'immutable'
|
||||
import { type GlobalState } from '~/store/'
|
||||
import { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions'
|
||||
import { ADD_INCOMING_TRANSACTIONS } from '~/routes/safe/store/actions/addIncomingTransactions'
|
||||
import { getAwaitingTransactions } from '~/logic/safe/transactions/awaitingTransactions'
|
||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import { enhanceSnackbarForAction, NOTIFICATIONS } from '~/logic/notifications'
|
||||
import { enhanceSnackbarForAction, NOTIFICATIONS, SUCCESS } from '~/logic/notifications'
|
||||
import closeSnackbarAction from '~/logic/notifications/store/actions/closeSnackbar'
|
||||
import { getIncomingTxAmount } from '~/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||
import { loadFromStorage } from '~/utils/storage'
|
||||
import { SAFES_KEY } from '~/logic/safe/utils'
|
||||
import { RECURRING_USER_KEY } from '~/utils/verifyRecurringUser'
|
||||
|
||||
const watchedActions = [
|
||||
ADD_TRANSACTIONS,
|
||||
ADD_INCOMING_TRANSACTIONS,
|
||||
]
|
||||
|
||||
const notificationsMiddleware = (store: Store<GlobalState>) => (next: Function) => async (action: AnyAction) => {
|
||||
@ -39,6 +47,50 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (next: Function)
|
||||
})
|
||||
break
|
||||
}
|
||||
case ADD_INCOMING_TRANSACTIONS: {
|
||||
action.payload.forEach(async (incomingTransactions, safeAddress) => {
|
||||
const storedSafes = await loadFromStorage(SAFES_KEY)
|
||||
const latestIncomingTxBlock = storedSafes ? storedSafes[safeAddress].latestIncomingTxBlock : 0
|
||||
|
||||
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)
|
||||
const { message, ...TX_INCOMING_MSG } = NOTIFICATIONS.TX_INCOMING_MSG
|
||||
const recurringUser = await loadFromStorage(RECURRING_USER_KEY)
|
||||
|
||||
if (recurringUser) {
|
||||
if (newIncomingTransactions.size > 3) {
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
enhanceSnackbarForAction({
|
||||
...TX_INCOMING_MSG,
|
||||
message: 'Multiple incoming transfers'
|
||||
})
|
||||
)
|
||||
)
|
||||
} else {
|
||||
newIncomingTransactions.forEach((tx) => {
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
enhanceSnackbarForAction({
|
||||
...TX_INCOMING_MSG,
|
||||
message: `${message}${getIncomingTxAmount(tx)}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
updateSafe({
|
||||
address: safeAddress,
|
||||
latestIncomingTxBlock: newIncomingTransactions.size
|
||||
? newIncomingTransactions.last().blockNumber
|
||||
: latestIncomingTxBlock,
|
||||
}),
|
||||
)
|
||||
})
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
39
src/routes/safe/store/models/incomingTransaction.js
Normal file
39
src/routes/safe/store/models/incomingTransaction.js
Normal file
@ -0,0 +1,39 @@
|
||||
// @flow
|
||||
import { Record } from 'immutable'
|
||||
import type { RecordFactory, RecordOf } from 'immutable'
|
||||
|
||||
export const INCOMING_TX_TYPE = 'incoming'
|
||||
|
||||
export type IncomingTransactionProps = {
|
||||
blockNumber: number,
|
||||
executionTxHash: string,
|
||||
safeTxHash: string,
|
||||
to: string,
|
||||
value: number,
|
||||
tokenAddress: string,
|
||||
from: string,
|
||||
symbol: string,
|
||||
decimals: number,
|
||||
fee: string,
|
||||
executionDate: string,
|
||||
type: string,
|
||||
status: string,
|
||||
}
|
||||
|
||||
export const makeIncomingTransaction: RecordFactory<IncomingTransactionProps> = Record({
|
||||
blockNumber: 0,
|
||||
executionTxHash: '',
|
||||
safeTxHash: '',
|
||||
to: '',
|
||||
value: 0,
|
||||
tokenAddress: '',
|
||||
from: '',
|
||||
symbol: '',
|
||||
decimals: 18,
|
||||
fee: '',
|
||||
executionDate: '',
|
||||
type: INCOMING_TX_TYPE,
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
export type IncomingTransaction = RecordOf<IncomingTransactionProps>
|
@ -15,6 +15,7 @@ export type SafeProps = {
|
||||
blacklistedTokens: Set<string>,
|
||||
ethBalance?: string,
|
||||
nonce: number,
|
||||
latestIncomingTxBlock: number,
|
||||
}
|
||||
|
||||
const SafeRecord: RecordFactory<SafeProps> = Record({
|
||||
@ -27,6 +28,7 @@ const SafeRecord: RecordFactory<SafeProps> = Record({
|
||||
blacklistedTokens: new Set(),
|
||||
balances: Map({}),
|
||||
nonce: 0,
|
||||
latestIncomingTxBlock: 0,
|
||||
})
|
||||
|
||||
export type Safe = RecordOf<SafeProps>
|
||||
|
@ -4,6 +4,8 @@ import type { RecordFactory, RecordOf } from 'immutable'
|
||||
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||
|
||||
export const OUTGOING_TX_TYPE = 'outgoing'
|
||||
|
||||
export type TransactionType = 'incoming' | 'outgoing' | 'settings' | 'custom' | 'creation' | 'cancellation'
|
||||
|
||||
export type TransactionStatus =
|
||||
@ -16,6 +18,7 @@ export type TransactionStatus =
|
||||
|
||||
export type TransactionProps = {
|
||||
nonce: number,
|
||||
blockNumber: number,
|
||||
value: string,
|
||||
confirmations: List<Confirmation>,
|
||||
recipient: string,
|
||||
@ -43,10 +46,12 @@ export type TransactionProps = {
|
||||
isTokenTransfer: boolean,
|
||||
decodedParams?: Object,
|
||||
refundParams?: Object,
|
||||
type: string,
|
||||
}
|
||||
|
||||
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||
nonce: 0,
|
||||
blockNumber: 0,
|
||||
value: 0,
|
||||
confirmations: List([]),
|
||||
recipient: '',
|
||||
@ -74,6 +79,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||
isTokenTransfer: false,
|
||||
decodedParams: {},
|
||||
refundParams: null,
|
||||
type: 'outgoing',
|
||||
})
|
||||
|
||||
export type Transaction = RecordOf<TransactionProps>
|
||||
|
16
src/routes/safe/store/reducer/incomingTransactions.js
Normal file
16
src/routes/safe/store/reducer/incomingTransactions.js
Normal file
@ -0,0 +1,16 @@
|
||||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import { handleActions, type ActionType } from 'redux-actions'
|
||||
import { ADD_INCOMING_TRANSACTIONS } from '~/routes/safe/store/actions/addIncomingTransactions'
|
||||
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
|
||||
export const INCOMING_TRANSACTIONS_REDUCER_ID = 'incomingTransactions'
|
||||
|
||||
export type IncomingState = Map<string, List<IncomingTransaction>>
|
||||
|
||||
export default handleActions<IncomingState, *>(
|
||||
{
|
||||
[ADD_INCOMING_TRANSACTIONS]: (state: IncomingState, action: ActionType<Function>): IncomingState => action.payload,
|
||||
},
|
||||
Map(),
|
||||
)
|
@ -6,9 +6,14 @@ import { type GlobalState } from '~/store/index'
|
||||
import { SAFE_PARAM_ADDRESS } from '~/routes/routes'
|
||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
||||
import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
|
||||
import {
|
||||
type IncomingState as IncomingTransactionsState,
|
||||
INCOMING_TRANSACTIONS_REDUCER_ID
|
||||
} from '~/routes/safe/store/reducer/incomingTransactions'
|
||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
|
||||
export type RouterProps = {
|
||||
match: Match,
|
||||
@ -43,6 +48,8 @@ export const defaultSafeSelector: Selector<GlobalState, {}, string> = createSele
|
||||
|
||||
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
const incomingTransactionsSelector = (state: GlobalState): IncomingTransactionsState => state[INCOMING_TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
||||
|
||||
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
|
||||
@ -63,6 +70,22 @@ export const safeTransactionsSelector: Selector<GlobalState, RouterProps, List<T
|
||||
},
|
||||
)
|
||||
|
||||
export const safeIncomingTransactionsSelector: Selector<GlobalState, RouterProps, List<IncomingTransaction>> = createSelector(
|
||||
incomingTransactionsSelector,
|
||||
safeParamAddressSelector,
|
||||
(incomingTransactions: IncomingTransactionsState, address: string): List<IncomingTransaction> => {
|
||||
if (!incomingTransactions) {
|
||||
return List([])
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return List([])
|
||||
}
|
||||
|
||||
return incomingTransactions.get(address) || List([])
|
||||
}
|
||||
)
|
||||
|
||||
export const confirmationsTransactionSelector: Selector<GlobalState, TransactionProps, number> = createSelector(
|
||||
oneTransactionSelector,
|
||||
(tx: Transaction) => {
|
||||
|
@ -12,6 +12,10 @@ import transactions, {
|
||||
type State as TransactionsState,
|
||||
TRANSACTIONS_REDUCER_ID,
|
||||
} from '~/routes/safe/store/reducer/transactions'
|
||||
import incomingTransactions, {
|
||||
type IncomingState as IncomingTransactionsState,
|
||||
INCOMING_TRANSACTIONS_REDUCER_ID,
|
||||
} from '~/routes/safe/store/reducer/incomingTransactions'
|
||||
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider'
|
||||
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens'
|
||||
import notifications, {
|
||||
@ -35,6 +39,7 @@ export type GlobalState = {
|
||||
safes: SafeState,
|
||||
tokens: TokensState,
|
||||
transactions: TransactionsState,
|
||||
incomingTransactions: IncomingTransactionsState,
|
||||
notifications: NotificationsState,
|
||||
}
|
||||
|
||||
@ -46,6 +51,7 @@ const reducers: Reducer<GlobalState> = combineReducers({
|
||||
[SAFE_REDUCER_ID]: safe,
|
||||
[TOKEN_REDUCER_ID]: tokens,
|
||||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||
[INCOMING_TRANSACTIONS_REDUCER_ID]: incomingTransactions,
|
||||
[NOTIFICATIONS_REDUCER_ID]: notifications,
|
||||
[COOKIES_REDUCER_ID]: cookies,
|
||||
})
|
||||
|
18
src/utils/verifyRecurringUser.js
Normal file
18
src/utils/verifyRecurringUser.js
Normal file
@ -0,0 +1,18 @@
|
||||
// @flow
|
||||
import { loadFromStorage, saveToStorage } from '~/utils/storage'
|
||||
|
||||
export const RECURRING_USER_KEY = 'RECURRING_USER'
|
||||
|
||||
const verifyRecurringUser = async () => {
|
||||
const recurringUser = await loadFromStorage(RECURRING_USER_KEY)
|
||||
|
||||
if (recurringUser === undefined) {
|
||||
await saveToStorage(RECURRING_USER_KEY, false)
|
||||
}
|
||||
|
||||
if (recurringUser === false) {
|
||||
await saveToStorage(RECURRING_USER_KEY, true)
|
||||
}
|
||||
}
|
||||
|
||||
export default verifyRecurringUser
|
20
yarn.lock
20
yarn.lock
@ -13112,6 +13112,15 @@ query-string@^5.0.1:
|
||||
object-assign "^4.1.0"
|
||||
strict-uri-encode "^1.0.0"
|
||||
|
||||
query-string@^6.9.0:
|
||||
version "6.9.0"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.9.0.tgz#1c3b727c370cf00f177c99f328fda2108f8fa3dd"
|
||||
integrity sha512-KG4bhCFYapExLsUHrFt+kQVEegF2agm4cpF/VNc6pZVthIfCc/GK8t8VyNIE3nyXG9DK3Tf2EGkxjR6/uRdYsA==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
querystring-es3@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||
@ -14748,6 +14757,11 @@ spdy@^4.0.1:
|
||||
select-hose "^2.0.0"
|
||||
spdy-transport "^3.0.0"
|
||||
|
||||
split-on-first@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
||||
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
@ -14914,6 +14928,11 @@ strict-uri-encode@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
@ -17615,7 +17634,6 @@ websocket@1.0.29, "websocket@github:web3-js/WebSocket-Node#polyfill/globalThis":
|
||||
dependencies:
|
||||
debug "^2.2.0"
|
||||
es5-ext "^0.10.50"
|
||||
gulp "^4.0.2"
|
||||
nan "^2.14.0"
|
||||
typedarray-to-buffer "^3.1.5"
|
||||
yaeti "^0.0.6"
|
||||
|
Loading…
x
Reference in New Issue
Block a user