Merge branch 'development' into dont-fetch-ownername-in-fetchTransactions

This commit is contained in:
fernandomg 2020-05-10 17:31:00 -03:00
commit 72d28470b2
32 changed files with 492 additions and 271 deletions

View File

@ -6,6 +6,7 @@ on:
branches:
# this will run on the specified branch
- master
- development
env:
REACT_APP_BLOCKNATIVE_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_KEY }}
@ -14,7 +15,7 @@ env:
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
jobs:
release:
runs-on: ${{ matrix.os }}
@ -28,20 +29,18 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Patch node gyp on windows to support Visual Studio 2019
if: startsWith(matrix.os, 'windows')
shell: powershell
run: |
yarn global add --production windows-build-tools --vs2015 --msvs_version=2015
- name: Install node-gyp
if: startsWith(matrix.os, 'windows')
shell: powershell
run: |
yarn global add node-gyp
yarn config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
@ -70,3 +69,25 @@ jobs:
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
- name: 'Upload Artifacts OSX'
if: contains(github.ref, 'development') && startsWith(matrix.os, 'macos')
uses: actions/upload-artifact@v2
with:
name: Desktop OSX
path: ./dist/Safe[ ]Multisig*.dmg
- name: 'Upload Artifacts Linux'
if: contains(github.ref, 'development') && startsWith(matrix.os, 'ubuntu')
uses: actions/upload-artifact@v2
with:
name: Desktop Linux
path: ./dist/Safe[ ]Multisig*.AppImage
- name: 'Upload Artifacts Windows'
if: contains(github.ref, 'development') && startsWith(matrix.os, 'windows')
uses: actions/upload-artifact@v2
with:
name: Desktop Windows
path: ./dist/Safe[ ]Multisig*.exe

View File

@ -52,7 +52,7 @@ function ensureSlash(path, needsSlash) {
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
const buildDesktop = process.env.BUILD_FOR_DESKTOP
const buildDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP
const homepagePath = require(paths.appPackageJson).homepage
// var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/';

View File

@ -25,7 +25,7 @@
"electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
"preelectron-pack": "yarn build",
"build-mainnet": "cross-env REACT_APP_NETWORK=mainnet yarn build",
"build-desktop": "cross-env BUILD_FOR_DESKTOP=true yarn build-mainnet",
"build-desktop": "cross-env REACT_APP_BUILD_FOR_DESKTOP=true yarn build-mainnet",
"flow": "flow",
"format:staged": "lint-staged",
"lint:check": "eslint './src/**/*.{js,jsx}'",

View File

@ -17,6 +17,8 @@ import { mainFontFamily, md, primary, screenSm } from '~/theme/variables'
import { loadGoogleAnalytics } from '~/utils/googleAnalytics'
import { loadIntercom } from '~/utils/intercom'
const isDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP
const useStyles = makeStyles({
container: {
backgroundColor: '#fff',
@ -111,14 +113,18 @@ const CookiesBanner = () => {
fetchCookiesFromStorage()
}, [showBanner])
useEffect(() => {
if (isDesktop && showBanner) acceptCookiesHandler()
}, [isDesktop, showBanner])
const acceptCookiesHandler = async () => {
const newState = {
acceptedNecessary: true,
acceptedAnalytics: true,
acceptedAnalytics: !isDesktop,
}
await saveCookie(COOKIES_KEY, newState, 365)
dispatch(openCookieBanner(false))
setShowAnalytics(true)
setShowAnalytics(!isDesktop)
}
const closeCookiesBannerHandler = async () => {
@ -193,8 +199,9 @@ const CookiesBanner = () => {
loadIntercom()
loadGoogleAnalytics()
}
if (isDesktop) loadIntercom()
return showBanner ? cookieBannerContent : null
return showBanner && !isDesktop ? cookieBannerContent : null
}
export default CookiesBanner

View File

@ -0,0 +1,51 @@
// @flow
import { makeStyles } from '@material-ui/core/styles'
import { useState } from 'react'
import * as React from 'react'
import QRIcon from '~/assets/icons/qrcode.svg'
import ScanQRModal from '~/components/ScanQRModal'
import Img from '~/components/layout/Img'
type Props = {
handleScan: Function,
}
const useStyles = makeStyles({
qrCodeBtn: {
cursor: 'pointer',
},
})
export const ScanQRWrapper = (props: Props) => {
const classes = useStyles()
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
const onScanFinished = (value) => {
props.handleScan(value, closeQrModal)
}
return (
<>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={onScanFinished} />}
</>
)
}

View File

@ -22,7 +22,7 @@ export const saveAddressBook = async (addressBook: AddressBook) => {
export const getAddressesListFromAdbk = (addressBook: AddressBook) =>
Array.from(addressBook).map((entry) => entry.address)
const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string | null => {
export const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string | null => {
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
if (entry) {
return entry.name

View File

@ -7,15 +7,15 @@ import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currenc
import type { GlobalState } from '~/store'
// eslint-disable-next-line max-len
const fetchCurrencySelectedValue = (currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>) => async (
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: $Keys<typeof AVAILABLE_CURRENCIES>) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
if (AVAILABLE_CURRENCIES.USD === currencyValueSelected) {
return dispatch(setCurrencyRate('1'))
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {
return dispatch(setCurrencyRate(safeAddress, 1))
}
const selectedCurrencyRateInBaseCurrency = await fetchCurrenciesRates(AVAILABLE_CURRENCIES.USD, currencyValueSelected)
dispatch(setCurrencyRate(selectedCurrencyRateInBaseCurrency))
const selectedCurrencyRateInBaseCurrency = await fetchCurrenciesRates(AVAILABLE_CURRENCIES.USD, selectedCurrency)
dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency))
}
export default fetchCurrencySelectedValue
export default fetchCurrencyRate

View File

@ -1,36 +1,42 @@
// @flow
import { List } from 'immutable'
import { batch } from 'react-redux'
import type { Dispatch as ReduxDispatch } from 'redux'
import fetchCurrencySelectedValue from '~/logic/currencyValues/store/actions/fetchCurrencySelectedValue'
import { CURRENCY_SELECTED_KEY } from '~/logic/currencyValues/store/actions/saveCurrencySelected'
import fetchCurrencyRate from '~/logic/currencyValues/store/actions/fetchCurrencyRate'
import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
import { setCurrencyRate } from '~/logic/currencyValues/store/actions/setCurrencyRate'
import { setCurrencySelected } from '~/logic/currencyValues/store/actions/setCurrencySelected'
import { setSelectedCurrency } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import { loadCurrencyValues } from '~/logic/currencyValues/store/utils/currencyValuesStorage'
import fetchSafeTokens from '~/logic/tokens/store/actions/fetchSafeTokens'
import type { GlobalState } from '~/store'
import { loadFromStorage } from '~/utils/storage'
export const fetchCurrencyValues = () => async (dispatch: ReduxDispatch<GlobalState>) => {
export const fetchCurrencyValues = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const currencyStored = await loadFromStorage(CURRENCY_SELECTED_KEY)
if (!currencyStored) {
const storedCurrencies = await loadCurrencyValues()
const storedCurrency = storedCurrencies[safeAddress]
if (!storedCurrency) {
return batch(() => {
dispatch(setCurrencySelected(AVAILABLE_CURRENCIES.USD))
dispatch(setCurrencyRate(1))
dispatch(setCurrencyBalances(safeAddress, List([])))
dispatch(setSelectedCurrency(safeAddress, AVAILABLE_CURRENCIES.USD))
dispatch(setCurrencyRate(safeAddress, 1))
})
}
const { currencyValueSelected } = currencyStored
batch(() => {
dispatch(setCurrencySelected(currencyValueSelected))
dispatch(fetchCurrencySelectedValue(currencyValueSelected))
// Loads the stored state on redux
Object.entries(storedCurrencies).forEach((kv) => {
const safeAddr = kv[0]
const value = kv[1]
const { currencyRate, selectedCurrency } = value
batch(() => {
dispatch(setSelectedCurrency(safeAddr, selectedCurrency))
dispatch(setCurrencyRate(safeAddr, currencyRate))
dispatch(fetchCurrencyRate(safeAddr, selectedCurrency))
dispatch(fetchSafeTokens(safeAddress))
})
})
} catch (err) {
console.error('Error fetching tokens price list', err)
console.error('Error fetching currency values', err)
}
return Promise.resolve()
}
export default fetchCurrencyValues

View File

@ -1,19 +0,0 @@
// @flow
import { Dispatch as ReduxDispatch } from 'redux'
import { setCurrencySelected } from '~/logic/currencyValues/store/actions/setCurrencySelected'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import type { GlobalState } from '~/store'
import { saveToStorage } from '~/utils/storage'
export const CURRENCY_SELECTED_KEY = 'CURRENCY_SELECTED_KEY'
const saveCurrencySelected = (currencySelected: AVAILABLE_CURRENCIES) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
await saveToStorage(CURRENCY_SELECTED_KEY, { currencyValueSelected: currencySelected })
dispatch(setCurrencySelected(currencySelected))
}
export default saveCurrencySelected

View File

@ -1,5 +1,4 @@
// @flow
import { Map } from 'immutable'
import { createAction } from 'redux-actions'
import type { CurrencyValues, CurrencyValuesProps } from '~/logic/currencyValues/store/model/currencyValues'
@ -9,5 +8,8 @@ export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES'
// eslint-disable-next-line max-len
export const setCurrencyBalances = createAction<string, *>(
SET_CURRENCY_BALANCES,
(currencyBalances: Map<string, CurrencyValues>): CurrencyValuesProps => ({ currencyBalances }),
(safeAddress: string, currencyBalances: List<CurrencyValues>): CurrencyValuesProps => ({
safeAddress,
currencyBalances,
}),
)

View File

@ -8,5 +8,5 @@ export const SET_CURRENCY_RATE = 'SET_CURRENCY_RATE'
// eslint-disable-next-line max-len
export const setCurrencyRate = createAction<string, *>(
SET_CURRENCY_RATE,
(currencyRate: string): CurrencyValuesProps => ({ currencyRate }),
(safeAddress: string, currencyRate: string): CurrencyValuesProps => ({ safeAddress, currencyRate }),
)

View File

@ -7,7 +7,10 @@ import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currenc
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
// eslint-disable-next-line max-len
export const setCurrencySelected = createAction<string, *>(
export const setSelectedCurrency = createAction<string, *>(
SET_CURRENT_CURRENCY,
(currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>): CurrencyValuesProps => ({ currencyValueSelected }),
(safeAddress: string, selectedCurrency: $Keys<typeof AVAILABLE_CURRENCIES>): CurrencyValuesProps => ({
safeAddress,
selectedCurrency,
}),
)

View File

@ -0,0 +1,50 @@
// @flow
import { Action, Store } from 'redux'
import fetchCurrencyRate from '~/logic/currencyValues/store/actions/fetchCurrencyRate'
import { SET_CURRENCY_BALANCES } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
import { SET_CURRENCY_RATE } from '~/logic/currencyValues/store/actions/setCurrencyRate'
import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import { currencyValuesSelector } from '~/logic/currencyValues/store/selectors'
import { saveCurrencyValues } from '~/logic/currencyValues/store/utils/currencyValuesStorage'
import type { GlobalState } from '~/routes/safe/store/middleware/safeStorage'
const watchedActions = [SET_CURRENT_CURRENCY, SET_CURRENCY_RATE, SET_CURRENCY_BALANCES]
const currencyValuesStorageMiddleware = (store: Store<GlobalState>) => (next: Function) => async (
action: Action<*>,
) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
const state: GlobalState = store.getState()
const { dispatch } = store
switch (action.type) {
case SET_CURRENT_CURRENCY: {
const { safeAddress, selectedCurrency } = action.payload
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
break
}
case SET_CURRENCY_RATE:
case SET_CURRENCY_BALANCES: {
const currencyValues = currencyValuesSelector(state)
const currencyValuesWithoutBalances = currencyValues.map((currencyValue) => {
const currencyRate = currencyValue.get('currencyRate')
const selectedCurrency = currencyValue.get('selectedCurrency')
return {
currencyRate,
selectedCurrency,
}
})
await saveCurrencyValues(currencyValuesWithoutBalances)
break
}
default:
break
}
}
return handledAction
}
export default currencyValuesStorageMiddleware

View File

@ -1,5 +1,5 @@
// @flow
import type { RecordOf } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import { Record } from 'immutable'
export const AVAILABLE_CURRENCIES = {
@ -45,17 +45,22 @@ export type BalanceCurrencyType = {
balanceInSelectedCurrency: string,
}
export const makeBalanceCurrency = Record({
export const makeBalanceCurrency: RecordFactory<BalanceCurrencyType> = Record({
currencyName: '',
tokenAddress: '',
balanceInBaseCurrency: '',
balanceInSelectedCurrency: '',
})
export type CurrencyValuesProps = {
currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>,
currencyRate: string,
export type CurrencyValuesEntry = {
selectedCurrency: $Keys<typeof AVAILABLE_CURRENCIES>,
currencyRate: number,
currencyValuesList: BalanceCurrencyType[],
}
export type CurrencyValuesProps = {
// Map safe address to currency values entry
currencyValues: Map<string, CurrencyValuesEntry>,
}
export type CurrencyValues = RecordOf<CurrencyValuesProps>

View File

@ -4,7 +4,7 @@ import { type ActionType, handleActions } from 'redux-actions'
import { SET_CURRENCY_BALANCES } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
import { SET_CURRENCY_RATE } from '~/logic/currencyValues/store/actions/setCurrencyRate'
import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setCurrencySelected'
import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import type { State } from '~/logic/tokens/store/reducer/tokens'
export const CURRENCY_VALUES_KEY = 'currencyValues'
@ -12,19 +12,19 @@ export const CURRENCY_VALUES_KEY = 'currencyValues'
export default handleActions<State, *>(
{
[SET_CURRENCY_RATE]: (state: State, action: ActionType<Function>): State => {
const { currencyRate } = action.payload
const { currencyRate, safeAddress } = action.payload
return state.set('currencyRate', currencyRate)
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
},
[SET_CURRENCY_BALANCES]: (state: State, action: ActionType<Function>): State => {
const { currencyBalances } = action.payload
const { currencyBalances, safeAddress } = action.payload
return state.set('currencyBalances', currencyBalances)
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
},
[SET_CURRENT_CURRENCY]: (state: State, action: ActionType<Function>): State => {
const { currencyValueSelected } = action.payload
const { safeAddress, selectedCurrency } = action.payload
return state.set('currencyValueSelected', currencyValueSelected)
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
},
},
Map(),

View File

@ -1,13 +1,38 @@
// @flow
import { List } from 'immutable'
import { type OutputSelector, createSelector } from 'reselect'
import type { CurrencyValuesEntry, CurrencyValuesProps } from '~/logic/currencyValues/store/model/currencyValues'
import { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
import { type GlobalState } from '~/store'
export const currencyValuesListSelector = (state: GlobalState) =>
state[CURRENCY_VALUES_KEY].get('currencyBalances') ? state[CURRENCY_VALUES_KEY].get('currencyBalances') : List([])
export const currencyValuesSelector = (state: GlobalState): CurrencyValuesEntry => state[CURRENCY_VALUES_KEY]
export const currentCurrencySelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyValueSelected')
export const safeFiatBalancesSelector: OutputSelector<GlobalState> = createSelector(
currencyValuesSelector,
safeParamAddressFromStateSelector,
(currencyValues: CurrencyValuesProps, safeAddress: string) => {
if (!currencyValues) return
return currencyValues.get(safeAddress)
},
)
export const currencyRateSelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyRate')
export const safeFiatBalancesListSelector: OutputSelector<GlobalState> = createSelector(
safeFiatBalancesSelector,
(currencyValuesMap: CurrencyValuesProps) => {
if (!currencyValuesMap) return
return currencyValuesMap.get('currencyBalances') ? currencyValuesMap.get('currencyBalances') : List([])
},
)
export const currentCurrencySelector: OutputSelector<GlobalState> = createSelector(
safeFiatBalancesSelector,
(currencyValuesMap?: CurrencyValuesProps) => (currencyValuesMap ? currencyValuesMap.get('selectedCurrency') : null),
)
export const currencyRateSelector: OutputSelector<GlobalState> = createSelector(
safeFiatBalancesSelector,
(currencyValuesMap: CurrencyValuesProps) => (currencyValuesMap ? currencyValuesMap.get('currencyRate') : null),
)

View File

@ -0,0 +1,18 @@
// @flow
import { Map } from 'immutable'
import type { CurrencyValuesEntry } from '~/logic/currencyValues/store/model/currencyValues'
import { loadFromStorage, saveToStorage } from '~/utils/storage'
const CURRENCY_VALUES_STORAGE_KEY = 'CURRENCY_VALUES_STORAGE_KEY'
export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyValuesEntry>) => {
try {
await saveToStorage(CURRENCY_VALUES_STORAGE_KEY, currencyValues)
} catch (err) {
console.error('Error storing currency values info in localstorage', err)
}
}
export const loadCurrencyValues = async () => {
return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {}
}

View File

@ -35,7 +35,6 @@ const fetchSafeTokens = (safeAddress: string) => async (dispatch: ReduxDispatch<
const alreadyActiveTokens = safe.get('activeTokens')
const blacklistedTokens = safe.get('blacklistedTokens')
const currencyValues = state[CURRENCY_VALUES_KEY]
const storedCurrencyBalances = currencyValues.get('currencyBalances')
const { balances, currencyList, ethBalance, tokens } = result.data.reduce(
(acc, { balance, balanceUsd, token, tokenAddress }) => {
@ -78,8 +77,14 @@ const fetchSafeTokens = (safeAddress: string) => async (dispatch: ReduxDispatch<
const updateActiveTokens = activeTokens.equals(alreadyActiveTokens) ? noFunc : update({ activeTokens })
const updateBalances = balances.equals(safeBalances) ? noFunc : update({ balances })
const updateEthBalance = ethBalance === currentEthBalance ? noFunc : update({ ethBalance })
const storedCurrencyBalances =
currencyValues && currencyValues.get(safeAddress)
? currencyValues.get(safeAddress).get('currencyBalances')
: undefined
const updateCurrencies = currencyList.equals(storedCurrencyBalances) ? noFunc : setCurrencyBalances(currencyList)
const updateCurrencies = currencyList.equals(storedCurrencyBalances)
? noFunc
: setCurrencyBalances(safeAddress, currencyList)
const updateTokens = tokens.size === 0 ? noFunc : addTokens(tokens)

View File

@ -4,12 +4,14 @@ import { withStyles } from '@material-ui/core/styles'
import CheckCircle from '@material-ui/icons/CheckCircle'
import * as React from 'react'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import OpenPaper from '~/components/Stepper/OpenPaper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { mustBeEthereumAddress, noErrorsOn, required } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col'
import Paragraph from '~/components/layout/Paragraph'
import { SAFE_MASTER_COPY_ADDRESS_V10, getSafeMasterContract, validateProxy } from '~/logic/contracts/safeContracts'
import { getWeb3 } from '~/logic/wallets/getWeb3'
@ -80,64 +82,77 @@ export const safeFieldsValidation = async (values: Object) => {
return errors
}
const Details = ({ classes, errors, form }: Props) => (
<>
<Block margin="md">
<Paragraph color="primary" noMargin size="md">
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is only
stored locally and will never be shared with Gnosis or any third parties.
<br />
Your connected wallet does not have to be the owner of this Safe. In this case, the interface will provide you a
read-only view.
</Paragraph>
</Block>
<Block className={classes.root}>
<Field
component={TextField}
name={FIELD_LOAD_NAME}
placeholder="Name of the Safe"
text="Safe name"
type="text"
validate={required}
/>
</Block>
<Block className={classes.root} margin="lg">
<AddressInput
component={TextField}
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
inputAdornment={
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: (
<InputAdornment position="end">
<CheckCircle className={classes.check} />
</InputAdornment>
),
}
}
name={FIELD_LOAD_ADDRESS}
placeholder="Safe Address*"
text="Safe Address"
type="text"
/>
</Block>
<Block margin="sm">
<Paragraph className={classes.links} color="primary" noMargin size="md">
By continuing you consent with the{' '}
<a href="https://safe.gnosis.io/terms" rel="noopener noreferrer" target="_blank">
terms of use
</a>{' '}
and{' '}
<a href="https://safe.gnosis.io/privacy" rel="noopener noreferrer" target="_blank">
privacy policy
</a>
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
</Paragraph>
</Block>
</>
)
const Details = ({ classes, errors, form }: Props) => {
const handleScan = (value, closeQrModal) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, value)
closeQrModal()
}
return (
<>
<Block margin="md">
<Paragraph color="primary" noMargin size="md">
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is
only stored locally and will never be shared with Gnosis or any third parties.
<br />
Your connected wallet does not have to be the owner of this Safe. In this case, the interface will provide you
a read-only view.
</Paragraph>
</Block>
<Block className={classes.root}>
<Col xs={11}>
<Field
component={TextField}
name={FIELD_LOAD_NAME}
placeholder="Name of the Safe"
text="Safe name"
type="text"
validate={required}
/>
</Col>
</Block>
<Block className={classes.root} margin="lg">
<Col xs={11}>
<AddressInput
component={TextField}
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
inputAdornment={
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: (
<InputAdornment position="end">
<CheckCircle className={classes.check} />
</InputAdornment>
),
}
}
name={FIELD_LOAD_ADDRESS}
placeholder="Safe Address*"
text="Safe Address"
type="text"
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Block>
<Block margin="sm">
<Paragraph className={classes.links} color="primary" noMargin size="md">
By continuing you consent with the{' '}
<a href="https://safe.gnosis.io/terms" rel="noopener noreferrer" target="_blank">
terms of use
</a>{' '}
and{' '}
<a href="https://safe.gnosis.io/privacy" rel="noopener noreferrer" target="_blank">
privacy policy
</a>
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
</Paragraph>
</Block>
</>
)
}
const DetailsForm = withStyles(styles)(Details)

View File

@ -252,12 +252,17 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
const isTxMined = async (txHash) => {
const web3 = getWeb3()
const txResult = await web3.eth.getTransaction(txHash)
if (txResult.blockNumber === null) {
return false
}
const receipt = await web3.eth.getTransactionReceipt(txHash)
if (!receipt.status) {
throw Error('TX status reverted')
}
const txResult = await web3.eth.getTransaction(txHash)
return txResult.blockNumber !== null
return true
}
let interval = setInterval(async () => {

View File

@ -8,6 +8,7 @@ import { useSelector } from 'react-redux'
import { styles } from './style'
import Modal from '~/components/Modal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -15,6 +16,7 @@ import TextField from '~/components/forms/TextField'
import { composeValidators, minMaxLength, required, uniqueAddress } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
@ -81,34 +83,53 @@ const CreateEditEntryModalComponent = ({
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted}>
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.container}>
<Row margin="md">
<Field
className={classes.addressInput}
component={TextField}
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
name="name"
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
testId={CREATE_ENTRY_INPUT_NAME_ID}
text={entryToEdit ? 'Entry*' : 'New entry*'}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
<Col xs={11}>
<Field
className={classes.addressInput}
component={TextField}
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
name="name"
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
testId={CREATE_ENTRY_INPUT_NAME_ID}
text={entryToEdit ? 'Entry*' : 'New entry*'}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
</Col>
</Row>
<Row margin="md">
<AddressInput
className={classes.addressInput}
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
disabled={!!entryToEdit}
fieldMutator={mutators.setOwnerAddress}
name="address"
placeholder="Owner address*"
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
text="Owner address*"
validators={entryToEdit ? undefined : [entryDoesntExist]}
/>
<Col xs={11}>
<AddressInput
className={classes.addressInput}
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
disabled={!!entryToEdit}
fieldMutator={mutators.setOwnerAddress}
name="address"
placeholder="Owner address*"
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
text="Owner address*"
validators={entryToEdit ? undefined : [entryDoesntExist]}
/>
</Col>
{!entryToEdit ? (
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
) : null}
</Row>
</Block>
<Hairline />

View File

@ -1,6 +1,5 @@
// @flow
import { Divider } from '@material-ui/core'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import { ClickAwayListener, Divider } from '@material-ui/core'
import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem'
import { makeStyles } from '@material-ui/core/styles'
@ -25,6 +24,7 @@ const useStyles = makeStyles({
'&:hover': {
backgroundColor: '#F0EFEE',
},
outline: 'none',
},
increasedPopperZindex: {
zIndex: 2001,
@ -43,9 +43,7 @@ const EllipsisTransactionDetails = ({ address, knownAddress }: EllipsisTransacti
const dispatch = useDispatch()
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
}
const handleClick = (event) => setAnchorEl(event.currentTarget)
const closeMenuHandler = () => setAnchorEl(null)
@ -56,8 +54,8 @@ const EllipsisTransactionDetails = ({ address, knownAddress }: EllipsisTransacti
return (
<ClickAwayListener onClickAway={closeMenuHandler}>
<div className={classes.container} onClick={handleClick} onKeyDown={handleClick} role="menu" tabIndex={0}>
<MoreHorizIcon />
<div className={classes.container} role="menu" tabIndex={0}>
<MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} />
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
<MenuItem disabled onClick={closeMenuHandler}>
Send Again

View File

@ -13,6 +13,7 @@ import GnoForm from '~/components/forms/GnoForm'
import { required } from '~/components/forms/validator'
import Img from '~/components/layout/Img'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
import { isValid as isURLValid } from '~/utils/url'
const FORM_ID = 'add-apps-form'
@ -51,9 +52,7 @@ type Props = {
}
const urlValidator = (value: string) => {
return /(?:^|[ \t])((https?:\/\/)?(?:localhost|[\w-]+(?:\.[\w-]+)+)(:\d+)?(\/\S*)?)/gm.test(value)
? undefined
: 'Please, provide a valid url'
return isURLValid(value) ? undefined : 'Please, provide a valid url'
}
const composeValidatorsApps = (...validators: Function[]): FieldValidator => (value: Field, values, meta) => {
@ -92,7 +91,15 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
}
const uniqueAppValidator = (value) => {
const exists = appList.find((a) => a.url === value.trim())
const exists = appList.find((a) => {
try {
const currentUrl = new URL(a.url)
const newUrl = new URL(value)
return currentUrl.href === newUrl.href
} catch (error) {
return 'There was a problem trying to validate the URL existence.'
}
})
return exists ? 'This app is already registered.' : undefined
}

View File

@ -3,7 +3,14 @@ import axios from 'axios'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
const gnosisAppsUrl = process.env.REACT_APP_GNOSIS_APPS_URL
const removeLastTrailingSlash = (url: string) => {
if (url.substr(-1) === '/') {
return url.substr(0, url.length - 1)
}
return url
}
const gnosisAppsUrl = removeLastTrailingSlash(process.env.REACT_APP_GNOSIS_APPS_URL)
export const staticAppsList = [
{ url: `${gnosisAppsUrl}/compound`, disabled: false },
{ url: `${gnosisAppsUrl}/aave`, disabled: false },
@ -29,40 +36,42 @@ export const getAppInfoFromUrl = async (appUrl: string) => {
return res
}
let cleanedUpAppUrl = appUrl.trim()
if (cleanedUpAppUrl.substr(-1) === '/') {
cleanedUpAppUrl = cleanedUpAppUrl.substr(0, cleanedUpAppUrl.length - 1)
res.url = cleanedUpAppUrl
}
res.url = appUrl.trim()
let noTrailingSlashUrl = removeLastTrailingSlash(res.url)
try {
const appInfo = await axios.get(`${cleanedUpAppUrl}/manifest.json`)
const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`)
// verify imported app fulfil safe requirements
if (!appInfo || !appInfo.data || !appInfo.data.name || !appInfo.data.description) {
throw Error('The app does not fulfil the structure required.')
}
// the DB origin field has a limit of 100 characters
const originFieldSize = 100
const jsonDataLength = 20
const remainingSpace = originFieldSize - res.url.length - jsonDataLength
res = {
...res,
...appInfo.data,
id: JSON.stringify({ url: cleanedUpAppUrl, name: appInfo.data.name }),
id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }),
error: false,
}
if (appInfo.data.iconPath) {
try {
const iconInfo = await axios.get(`${cleanedUpAppUrl}/${appInfo.data.iconPath}`)
const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 })
if (/image\/\w/gm.test(iconInfo.headers['content-type'])) {
res.iconUrl = `${cleanedUpAppUrl}/${appInfo.data.iconPath}`
res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}`
}
} catch (error) {
console.error(`It was not possible to fetch icon from app ${cleanedUpAppUrl}`)
console.error(`It was not possible to fetch icon from app ${res.url}`)
}
}
return res
} catch (error) {
console.error(`It was not possible to fetch app from ${cleanedUpAppUrl}: ${error.message}`)
console.error(`It was not possible to fetch app from ${res.url}: ${error.message}`)
return res
}
}

View File

@ -19,8 +19,8 @@ import Button from '~/components/layout/Button'
import Row from '~/components/layout/Row'
import {
currencyRateSelector,
currencyValuesListSelector,
currentCurrencySelector,
safeFiatBalancesListSelector,
} from '~/logic/currencyValues/store/selectors'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
import AssetTableCell from '~/routes/safe/components/Balances/AssetTableCell'
@ -46,16 +46,16 @@ const Coins = (props: Props) => {
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const currencySelected = useSelector(currentCurrencySelector)
const selectedCurrency = useSelector(currentCurrencySelector)
const currencyRate = useSelector(currencyRateSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const currencyValues = useSelector(currencyValuesListSelector)
const currencyValues = useSelector(safeFiatBalancesListSelector)
const granted = useSelector(grantedSelector)
const [filteredData, setFilteredData] = React.useState(List())
React.useMemo(() => {
setFilteredData(getBalanceData(activeTokens, currencySelected, currencyValues, currencyRate))
}, [currencySelected, currencyRate, activeTokens.hashCode(), currencyValues.hashCode()])
setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate))
}, [selectedCurrency, currencyRate, activeTokens.hashCode(), currencyValues])
return (
<TableContainer>

View File

@ -9,20 +9,21 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import WhenFieldChanges from '~/components/WhenFieldChanges'
import GnoForm from '~/components/forms/GnoForm'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
import { nftTokensSelector, safeActiveSelectorMap } from '~/logic/collectibles/store/selectors'
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
@ -60,7 +61,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const nftAssets: NFTAssetsState = useSelector(safeActiveSelectorMap)
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const addressBook: AddressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
@ -85,14 +86,6 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
onNext(values)
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
return (
<>
<Row align="center" className={classes.heading} grow>
@ -112,14 +105,18 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const { assetAddress } = formState.values
const selectedNFTTokens = nftTokens.filter((nftToken) => nftToken.assetAddress === assetAddress)
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -200,16 +197,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -256,7 +244,6 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -10,11 +10,10 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
import TextField from '~/components/forms/TextField'
@ -25,9 +24,11 @@ import Button from '~/components/layout/Button'
import ButtonLink from '~/components/layout/ButtonLink'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { safeSelector } from '~/routes/safe/store/selectors'
@ -45,13 +46,13 @@ const useStyles = makeStyles(styles)
const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Props) => {
const classes = useStyles()
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
})
const [pristine, setPristine] = useState<boolean>(true)
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
const addressBook: AddressBook = useSelector(getAddressBook)
React.useMemo(() => {
if (selectedEntry === null && pristine) {
@ -65,14 +66,6 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
}
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
const formMutators = {
setMax: (args, state, utils) => {
utils.changeValue(state, 'value', () => ethBalance)
@ -103,14 +96,18 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
shouldDisableSubmitButton = !selectedEntry.address
}
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -184,16 +181,7 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -252,7 +240,6 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -11,11 +11,10 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
import TextField from '~/components/forms/TextField'
@ -25,9 +24,11 @@ import Button from '~/components/layout/Button'
import ButtonLink from '~/components/layout/ButtonLink'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import { type Token } from '~/logic/tokens/store/model/token'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -62,7 +63,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
const classes = useStyles()
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const tokens: Token = useSelector(extendedSafeTokensSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const addressBook: AddressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
@ -85,14 +86,6 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
onNext(submitValues)
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
return (
<>
<Row align="center" className={classes.heading} grow>
@ -112,14 +105,18 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
const { token: tokenAddress } = formState.values
const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress)
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -198,16 +195,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -276,7 +264,6 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -13,18 +13,19 @@ import { useDispatch, useSelector } from 'react-redux'
import CheckIcon from './img/check.svg'
import fetchCurrencySelectedValue from '~/logic/currencyValues/store/actions/fetchCurrencySelectedValue'
import saveCurrencySelected from '~/logic/currencyValues/store/actions/saveCurrencySelected'
import { setSelectedCurrency } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import { currentCurrencySelector } from '~/logic/currencyValues/store/selectors'
import { useDropdownStyles } from '~/routes/safe/components/DropdownCurrency/style'
import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
import { DropdownListTheme } from '~/theme/mui'
const DropdownCurrency = () => {
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const dispatch = useDispatch()
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const currencyValueSelected = useSelector(currentCurrencySelector)
const selectedCurrency = useSelector(currentCurrencySelector)
const [searchParams, setSearchParams] = useState('')
const classes = useDropdownStyles()
@ -41,17 +42,16 @@ const DropdownCurrency = () => {
}
const onCurrentCurrencyChangedHandler = (newCurrencySelectedName: AVAILABLE_CURRENCIES) => {
dispatch(fetchCurrencySelectedValue(newCurrencySelectedName))
dispatch(saveCurrencySelected(newCurrencySelectedName))
dispatch(setSelectedCurrency(safeAddress, newCurrencySelectedName))
handleClose()
}
return !currencyValueSelected ? null : (
return !selectedCurrency ? null : (
<MuiThemeProvider theme={DropdownListTheme}>
<>
<button className={classes.button} onClick={handleClick} type="button">
<span className={classNames(classes.buttonInner, anchorEl && classes.openMenuButton)}>
{currencyValueSelected}
{selectedCurrency}
</span>
</button>
<Menu
@ -108,7 +108,7 @@ const DropdownCurrency = () => {
/>
</ListItemIcon>
<ListItemText primary={currencyName} />
{currencyName === currencyValueSelected ? (
{currencyName === selectedCurrency ? (
<ListItemIcon className={classes.iconRight}>
<img alt="checked" src={CheckIcon} />
</ListItemIcon>

View File

@ -7,6 +7,7 @@ import { useSelector } from 'react-redux'
import { styles } from './style'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -59,6 +60,16 @@ const OwnerForm = ({ classes, onClose, onSubmit }: Props) => {
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.formContainer}>
@ -91,6 +102,9 @@ const OwnerForm = ({ classes, onClose, onSubmit }: Props) => {
validators={[ownerDoesntExist]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</Block>
<Hairline />

View File

@ -11,6 +11,7 @@ import { styles } from './style'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -65,6 +66,17 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Prop
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.formContainer}>
@ -126,6 +138,9 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Prop
validators={[ownerDoesntExist]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</Block>
<Hairline />

View File

@ -15,6 +15,7 @@ import {
nftTokensReducer,
} from '~/logic/collectibles/store/reducer/collectibles'
import cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies'
import currencyValuesStorageMiddleware from '~/logic/currencyValues/store/middleware'
import currencyValues, { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
import currentSession, {
CURRENT_SESSION_REDUCER_ID,
@ -55,6 +56,7 @@ const finalCreateStore = composeEnhancers(
providerWatcher,
notificationsMiddleware,
addressBookMiddleware,
currencyValuesStorageMiddleware,
),
)