Merge pull request #296 from gnosis/189-cookie-banner
Feature #189: Cookie banner
This commit is contained in:
commit
2c42eb56af
|
@ -4,4 +4,4 @@ flow-typed
|
|||
flow-typed/npm
|
||||
config
|
||||
scripts
|
||||
migrations
|
||||
migrations
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// flow-typed signature: a23fa96dc9c75f8931650efff45badee
|
||||
// flow-typed version: c6154227d1/js-cookie_v2.x.x/flow_>=v0.104.x
|
||||
|
||||
declare module 'js-cookie' {
|
||||
declare type CookieOptions = {
|
||||
expires?: number | Date,
|
||||
path?: string,
|
||||
domain?: string,
|
||||
secure?: boolean,
|
||||
...
|
||||
}
|
||||
declare type ConverterFunc = (value: string, name: string) => string;
|
||||
declare type ConverterObj = {
|
||||
read: ConverterFunc,
|
||||
write: ConverterFunc,
|
||||
...
|
||||
};
|
||||
declare class Cookie {
|
||||
defaults: CookieOptions;
|
||||
set(name: string, value: mixed, options?: CookieOptions): void;
|
||||
get(...args: Array<void>): { [key: string]: string, ... };
|
||||
get(name: string, ...args: Array<void>): string | void;
|
||||
remove(name: string, options?: CookieOptions): void;
|
||||
getJSON(name: string): Object;
|
||||
withConverter(converter: ConverterFunc | ConverterObj): this;
|
||||
noConflict(): this;
|
||||
}
|
||||
|
||||
declare module.exports: Cookie;
|
||||
}
|
|
@ -49,9 +49,11 @@
|
|||
"history": "4.10.1",
|
||||
"immortal-db": "^1.0.2",
|
||||
"immutable": "^4.0.0-rc.9",
|
||||
"js-cookie": "^2.2.1",
|
||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
"polished": "^3.4.2",
|
||||
"qrcode.react": "1.0.0",
|
||||
"react": "16.12.0",
|
||||
"react-dom": "16.12.0",
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
// @flow
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import Link from '~/components/layout/Link'
|
||||
import Button from '~/components/layout/Button'
|
||||
import { primary, mainFontFamily } from '~/theme/variables'
|
||||
import type { CookiesProps } from '~/logic/cookies/model/cookie'
|
||||
import { COOKIES_KEY } from '~/logic/cookies/model/cookie'
|
||||
import { loadFromCookie, saveCookie } from '~/logic/cookies/utils'
|
||||
import { cookieBannerOpen } from '~/logic/cookies/store/selectors'
|
||||
import { openCookieBanner } from '~/logic/cookies/store/actions/openCookieBanner'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
backgroundColor: '#fff',
|
||||
bottom: '0',
|
||||
boxShadow: '0 2px 4px 0 rgba(212, 212, 211, 0.59)',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
left: '0',
|
||||
minHeight: '200px',
|
||||
padding: '27px 15px',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
},
|
||||
content: {
|
||||
maxWidth: '100%',
|
||||
width: '830px',
|
||||
},
|
||||
text: {
|
||||
color: primary,
|
||||
fontFamily: mainFontFamily,
|
||||
fontSize: '16px',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1.38',
|
||||
margin: '0 0 25px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
form: {
|
||||
columnGap: '10px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
rowGap: '10px',
|
||||
'@media (min-width: 960px)': {
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
},
|
||||
},
|
||||
formItem: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
link: {
|
||||
textDecoration: 'underline',
|
||||
'&:hover': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
close: {
|
||||
position: 'absolute',
|
||||
right: '12px',
|
||||
top: '12px',
|
||||
},
|
||||
})
|
||||
|
||||
const CookiesBanner = () => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const [localNecessary, setLocalNecessary] = useState(true)
|
||||
const [localAnalytics, setLocalAnalytics] = useState(false)
|
||||
const showBanner = useSelector(cookieBannerOpen)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchCookiesFromStorage() {
|
||||
const cookiesState: ?CookiesProps = await loadFromCookie(COOKIES_KEY)
|
||||
if (cookiesState) {
|
||||
const { acceptedNecessary, acceptedAnalytics } = cookiesState
|
||||
setLocalAnalytics(acceptedAnalytics)
|
||||
setLocalNecessary(acceptedNecessary)
|
||||
const openBanner = acceptedNecessary === false || showBanner
|
||||
dispatch(openCookieBanner(openBanner))
|
||||
} else {
|
||||
dispatch(openCookieBanner(true))
|
||||
}
|
||||
}
|
||||
fetchCookiesFromStorage()
|
||||
}, [showBanner])
|
||||
|
||||
const acceptCookiesHandler = async () => {
|
||||
const newState = {
|
||||
acceptedNecessary: true,
|
||||
acceptedAnalytics: true,
|
||||
}
|
||||
await saveCookie(COOKIES_KEY, newState, 365)
|
||||
dispatch(openCookieBanner(false))
|
||||
}
|
||||
|
||||
const closeCookiesBannerHandler = async () => {
|
||||
const newState = {
|
||||
acceptedNecessary: true,
|
||||
acceptedAnalytics: localAnalytics,
|
||||
}
|
||||
const expDays = localAnalytics ? 365 : 7
|
||||
await saveCookie(COOKIES_KEY, newState, expDays)
|
||||
dispatch(openCookieBanner(false))
|
||||
}
|
||||
|
||||
|
||||
return showBanner ? (
|
||||
<div className={classes.container}>
|
||||
<IconButton onClick={() => closeCookiesBannerHandler()} className={classes.close}><Close /></IconButton>
|
||||
<div className={classes.content}>
|
||||
<p className={classes.text}>
|
||||
We use cookies to give you the best experience and to help improve our website. Please read our
|
||||
{' '}
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/cookie">Cookie Policy</Link>
|
||||
{' '}
|
||||
for more information. By clicking "Accept all", you agree to the storing of cookies on your device
|
||||
to enhance site navigation, analyze site usage and provide customer support.
|
||||
</p>
|
||||
<div className={classes.form}>
|
||||
<div className={classes.formItem}>
|
||||
<FormControlLabel
|
||||
checked={localNecessary}
|
||||
disabled
|
||||
label="Necessary"
|
||||
name="Necessary"
|
||||
onChange={() => setLocalNecessary((prev) => !prev)}
|
||||
value={localNecessary}
|
||||
control={(
|
||||
<Checkbox disabled />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<FormControlLabel
|
||||
label="Analytics"
|
||||
name="Analytics"
|
||||
onChange={() => setLocalAnalytics((prev) => !prev)}
|
||||
value={localAnalytics}
|
||||
control={(
|
||||
<Checkbox checked={localAnalytics} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<Button
|
||||
color="primary"
|
||||
component={Link}
|
||||
minWidth={180}
|
||||
variant="outlined"
|
||||
onClick={() => acceptCookiesHandler()}
|
||||
>
|
||||
Accept All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default CookiesBanner
|
|
@ -75,9 +75,4 @@ class Notifier extends Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default withSnackbar(
|
||||
connect(
|
||||
selector,
|
||||
actions,
|
||||
)(Notifier),
|
||||
)
|
||||
export default withSnackbar(connect(selector, actions)(Notifier))
|
||||
|
|
|
@ -9,32 +9,34 @@ body {
|
|||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local("Averta-Regular"), url(../../assets/fonts/Averta-normal.woff2) format('woff2');
|
||||
src: local("Averta-Regular"),
|
||||
url(../../assets/fonts/Averta-normal.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Averta';
|
||||
font-family: "Averta";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: local("Averta-Extrabold"), url(../../assets/fonts/Averta-ExtraBold.woff2) format('woff2');
|
||||
src: local("Averta-Extrabold"),
|
||||
url(../../assets/fonts/Averta-ExtraBold.woff2) format("woff2");
|
||||
}
|
||||
|
||||
body {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow-x: hidden;
|
||||
color: $fontColor;
|
||||
font-family: 'Averta', monospace;
|
||||
font-size: $mediumFontSize;
|
||||
margin: 0;
|
||||
background-color: $background;
|
||||
text-rendering: geometricPrecision;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-color: $background;
|
||||
bottom: 0;
|
||||
color: $fontColor;
|
||||
font-family: "Averta", monospace;
|
||||
font-size: $mediumFontSize;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-rendering: geometricPrecision;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
body > div:first-child {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Link from '~/components/layout/Link'
|
||||
import { sm, primary } from '~/theme/variables'
|
||||
import { openCookieBanner } from '~/logic/cookies/store/actions/openCookieBanner'
|
||||
import GnoButtonLink from '~/components/layout/ButtonLink'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
|
@ -12,24 +15,42 @@ const useStyles = makeStyles({
|
|||
link: {
|
||||
color: primary,
|
||||
},
|
||||
buttonLink: {
|
||||
textDecoration: 'none',
|
||||
color: primary,
|
||||
},
|
||||
})
|
||||
|
||||
const LegalLinks = () => {
|
||||
type Props = {
|
||||
toggleSidebar: Function,
|
||||
}
|
||||
|
||||
const LegalLinks = (props: Props) => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const openCookiesHandler = () => {
|
||||
dispatch(openCookieBanner(true))
|
||||
props.toggleSidebar()
|
||||
}
|
||||
|
||||
return (
|
||||
<Block className={classes.container} justify="space-around">
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/terms-of-use-072018.html" target="_blank">
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/terms" target="_blank">
|
||||
Terms
|
||||
</Link>
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/privacy-policy-052019.html" target="_blank">
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/privacy" target="_blank">
|
||||
Privacy
|
||||
</Link>
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/licenses-092019.html" target="_blank">
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/licenses" target="_blank">
|
||||
Licenses
|
||||
</Link>
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/imprint.html" target="_blank">
|
||||
<Link className={classes.link} to="https://safe.gnosis.io/imprint" target="_blank">
|
||||
Imprint
|
||||
</Link>
|
||||
<GnoButtonLink className={classes.buttonLink} onClick={openCookiesHandler}>
|
||||
Cookies
|
||||
</GnoButtonLink>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ const Sidebar = ({
|
|||
setDefaultSafe={setDefaultSafeAction}
|
||||
defaultSafe={defaultSafe}
|
||||
/>
|
||||
<LegalLinks />
|
||||
<LegalLinks toggleSidebar={toggleSidebar} />
|
||||
</Drawer>
|
||||
</ClickAwayListener>
|
||||
{children}
|
||||
|
|
|
@ -15,6 +15,7 @@ import AlertIcon from './assets/alert.svg'
|
|||
import CheckIcon from './assets/check.svg'
|
||||
import ErrorIcon from './assets/error.svg'
|
||||
import InfoIcon from './assets/info.svg'
|
||||
import CookiesBanner from '~/components/CookiesBanner'
|
||||
import styles from './index.scss'
|
||||
|
||||
const notificationStyles = {
|
||||
|
@ -92,6 +93,7 @@ const PageFrame = ({ children, classes, currentNetwork }: Props) => {
|
|||
{children}
|
||||
</SidebarProvider>
|
||||
</SnackbarProvider>
|
||||
<CookiesBanner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
12
src/index.js
12
src/index.js
|
@ -1,14 +1,13 @@
|
|||
// @flow
|
||||
import 'babel-polyfill'
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import Root from '~/components/Root'
|
||||
import { store } from '~/store'
|
||||
import loadSafesFromStorage from '~/routes/safe/store/actions/loadSafesFromStorage'
|
||||
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'
|
||||
|
||||
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
|
||||
|
||||
|
@ -18,8 +17,13 @@ if (process.env.NODE_ENV !== 'production') {
|
|||
whyDidYouRender(React)
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
store.dispatch(loadActiveTokens())
|
||||
store.dispatch(loadSafesFromStorage())
|
||||
store.dispatch(loadDefaultSafe())
|
||||
|
||||
ReactDOM.render(<Root />, document.getElementById('root'))
|
||||
const root = document.getElementById('root')
|
||||
|
||||
if (root !== null) {
|
||||
ReactDOM.render(<Root />, root)
|
||||
}
|
||||
|
|
|
@ -75,7 +75,10 @@ export const deploySafeContract = async (safeAccounts: string[], numConfirmation
|
|||
const gasPrice = await calculateGasPrice()
|
||||
|
||||
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, {
|
||||
from: userAccount, gas, gasPrice, value: 0,
|
||||
from: userAccount,
|
||||
gas,
|
||||
gasPrice,
|
||||
value: 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
import type { RecordOf } from 'immutable'
|
||||
|
||||
export const COOKIES_KEY = 'COOKIES'
|
||||
|
||||
export type CookiesProps = {
|
||||
acceptedNecessary: boolean,
|
||||
acceptedAnalytics: boolean,
|
||||
cookieBannerOpen: boolean,
|
||||
}
|
||||
|
||||
export type Cookie = RecordOf<CookiesProps>
|
|
@ -0,0 +1,6 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER'
|
||||
|
||||
export const openCookieBanner = createAction<string, *, *>(OPEN_COOKIE_BANNER, (cookieBannerOpen: boolean) => ({ cookieBannerOpen }))
|
|
@ -0,0 +1,20 @@
|
|||
// @flow
|
||||
import { Map } from 'immutable'
|
||||
import { handleActions, type ActionType } from 'redux-actions'
|
||||
import type { Cookie } from '~/logic/cookies/model/cookie'
|
||||
import { OPEN_COOKIE_BANNER } from '~/logic/cookies/store/actions/openCookieBanner'
|
||||
|
||||
export const COOKIES_REDUCER_ID = 'cookies'
|
||||
|
||||
export type State = Map<string, Map<string, Cookie>>
|
||||
|
||||
export default handleActions<State, *>(
|
||||
{
|
||||
[OPEN_COOKIE_BANNER]: (state: State, action: ActionType<Function>): State => {
|
||||
const { cookieBannerOpen } = action.payload
|
||||
|
||||
return state.set('cookieBannerOpen', cookieBannerOpen)
|
||||
},
|
||||
},
|
||||
Map(),
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
import type { Provider } from '~/logic/wallets/store/model/provider'
|
||||
import { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies'
|
||||
|
||||
export const cookieBannerOpen = (state: any): Provider => state[COOKIES_REDUCER_ID].get('cookieBannerOpen')
|
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import Cookies from 'js-cookie'
|
||||
import { getNetwork } from '~/config'
|
||||
|
||||
const PREFIX = `v1_${getNetwork()}`
|
||||
|
||||
export const loadFromCookie = async (key: string): Promise<*> => {
|
||||
try {
|
||||
const stringifiedValue = await Cookies.get(`${PREFIX}__${key}`)
|
||||
if (stringifiedValue === null || stringifiedValue === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.parse(stringifiedValue)
|
||||
} catch (err) {
|
||||
console.error(`Failed to load ${key} from cookies:`, err)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const saveCookie = async (key: string, value: *, expirationDays: number): Promise<*> => {
|
||||
try {
|
||||
const stringifiedValue = JSON.stringify(value)
|
||||
const expiration = expirationDays ? { expires: expirationDays } : undefined
|
||||
await Cookies.set(`${PREFIX}__${key}`, stringifiedValue, expiration)
|
||||
} catch (err) {
|
||||
console.error(`Failed to save ${key} in cookies:`, err)
|
||||
}
|
||||
}
|
|
@ -8,9 +8,7 @@ export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
|
|||
|
||||
const addSnackbar = createAction<string, *>(ENQUEUE_SNACKBAR)
|
||||
|
||||
const enqueueSnackbar = (notification: NotificationProps) => (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
) => {
|
||||
const enqueueSnackbar = (notification: NotificationProps) => (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
const newNotification = {
|
||||
...notification,
|
||||
key: new Date().getTime(),
|
||||
|
|
|
@ -5,11 +5,10 @@ import { type GlobalState } from '~/store'
|
|||
import { NOTIFICATIONS_REDUCER_ID } from '~/logic/notifications/store/reducer/notifications'
|
||||
import { type Notification } from '~/logic/notifications/store/models/notification'
|
||||
|
||||
const notificationsMapSelector = (
|
||||
state: GlobalState,
|
||||
): Map<string, Notification> => state[NOTIFICATIONS_REDUCER_ID]
|
||||
const notificationsMapSelector = (state: GlobalState): Map<string, Notification> => state[NOTIFICATIONS_REDUCER_ID]
|
||||
|
||||
export const notificationsListSelector: Selector<GlobalState, {}, List<Notification>> = createSelector(
|
||||
notificationsMapSelector,
|
||||
(notifications: Map<string, Notification>): List<Notification> => notifications.toList(),
|
||||
)
|
||||
export const notificationsListSelector: Selector<
|
||||
GlobalState,
|
||||
{},
|
||||
List<Notification>,
|
||||
> = createSelector(notificationsMapSelector, (notifications: Map<string, Notification>): List<Notification> => notifications.toList())
|
||||
|
|
|
@ -68,7 +68,18 @@ export const getExecutionTransaction = async (
|
|||
const web3 = getWeb3()
|
||||
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
|
||||
|
||||
return contract.methods.execTransaction(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, sigs)
|
||||
return contract.methods.execTransaction(
|
||||
to,
|
||||
valueInWei,
|
||||
data,
|
||||
operation,
|
||||
safeTxGas,
|
||||
baseGas,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
sigs,
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(`Error while creating transaction: ${err}`)
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ export const tokenListSelector: Selector<GlobalState, Map<string, Token>, List<T
|
|||
(tokens: Map<string, Token>) => tokens.toList(),
|
||||
)
|
||||
|
||||
export const orderedTokenListSelector: Selector<GlobalState, RouterProps, List<Token>> = createSelector(
|
||||
tokenListSelector,
|
||||
(tokens: List<Token>) => tokens.sortBy((token: Token) => token.get('symbol')),
|
||||
)
|
||||
export const orderedTokenListSelector: Selector<
|
||||
GlobalState,
|
||||
RouterProps,
|
||||
List<Token>,
|
||||
> = createSelector(tokenListSelector, (tokens: List<Token>) => tokens.sortBy((token: Token) => token.get('symbol')))
|
||||
|
|
|
@ -6,40 +6,25 @@ import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
|
|||
|
||||
const providerSelector = (state: any): Provider => state[PROVIDER_REDUCER_ID]
|
||||
|
||||
export const userAccountSelector = createSelector(
|
||||
providerSelector,
|
||||
(provider: Provider) => {
|
||||
const account = provider.get('account')
|
||||
export const userAccountSelector = createSelector(providerSelector, (provider: Provider) => {
|
||||
const account = provider.get('account')
|
||||
|
||||
return account || ''
|
||||
},
|
||||
)
|
||||
return account || ''
|
||||
})
|
||||
|
||||
export const providerNameSelector = createSelector(
|
||||
providerSelector,
|
||||
(provider: Provider) => {
|
||||
const name = provider.get('name')
|
||||
export const providerNameSelector = createSelector(providerSelector, (provider: Provider) => {
|
||||
const name = provider.get('name')
|
||||
|
||||
return name ? name.toLowerCase() : undefined
|
||||
},
|
||||
)
|
||||
return name ? name.toLowerCase() : undefined
|
||||
})
|
||||
|
||||
export const networkSelector = createSelector(
|
||||
providerSelector,
|
||||
(provider: Provider) => {
|
||||
const networkId = provider.get('network')
|
||||
const network = ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
|
||||
export const networkSelector = createSelector(providerSelector, (provider: Provider) => {
|
||||
const networkId = provider.get('network')
|
||||
const network = ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
|
||||
|
||||
return network
|
||||
},
|
||||
)
|
||||
return network
|
||||
})
|
||||
|
||||
export const loadedSelector = createSelector(
|
||||
providerSelector,
|
||||
(provider: Provider) => provider.get('loaded'),
|
||||
)
|
||||
export const loadedSelector = createSelector(providerSelector, (provider: Provider) => provider.get('loaded'))
|
||||
|
||||
export const availableSelector = createSelector(
|
||||
providerSelector,
|
||||
(provider: Provider) => provider.get('available'),
|
||||
)
|
||||
export const availableSelector = createSelector(providerSelector, (provider: Provider) => provider.get('available'))
|
||||
|
|
|
@ -72,8 +72,8 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
|
|||
)
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
export default connect<Object, Object, ?Function, ?Object>(
|
||||
// $FlowFixMe
|
||||
(state) => ({ defaultSafe: defaultSafeSelector(state) }),
|
||||
null,
|
||||
)(withRouter(Routes))
|
||||
|
|
|
@ -55,13 +55,35 @@ const createTransaction = (
|
|||
try {
|
||||
if (isExecution) {
|
||||
tx = await getExecutionTransaction(
|
||||
safeInstance, to, valueInWei, txData, CALL, nonce,
|
||||
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs
|
||||
safeInstance,
|
||||
to,
|
||||
valueInWei,
|
||||
txData,
|
||||
CALL,
|
||||
nonce,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS,
|
||||
from,
|
||||
sigs,
|
||||
)
|
||||
} else {
|
||||
tx = await getApprovalTransaction(
|
||||
safeInstance, to, valueInWei, txData, CALL, nonce,
|
||||
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs
|
||||
safeInstance,
|
||||
to,
|
||||
valueInWei,
|
||||
txData,
|
||||
CALL,
|
||||
nonce,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS,
|
||||
from,
|
||||
sigs,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -43,9 +43,7 @@ const getLocalSafe = async (safeAddress: string) => {
|
|||
return storedSafes[safeAddress]
|
||||
}
|
||||
|
||||
export const checkAndUpdateSafeOwners = (safeAddress: string) => async (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
) => {
|
||||
export const checkAndUpdateSafeOwners = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
// Check if the owner's safe did change and update them
|
||||
const [gnosisSafe, localSafe] = await Promise.all([getGnosisSafeInstanceAt(safeAddress), getLocalSafe(safeAddress)])
|
||||
const remoteOwners = await gnosisSafe.getOwners()
|
||||
|
|
|
@ -15,11 +15,7 @@ import {
|
|||
TX_TYPE_EXECUTION,
|
||||
TX_TYPE_CONFIRMATION,
|
||||
} from '~/logic/safe/transactions'
|
||||
import {
|
||||
type NotificationsQueue,
|
||||
getNotificationsFromTxType,
|
||||
showSnackbar,
|
||||
} from '~/logic/notifications'
|
||||
import { type NotificationsQueue, getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
|
||||
import { getErrorMessage } from '~/test/utils/ethereumErrors'
|
||||
|
||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||
|
@ -40,18 +36,20 @@ export const generateSignaturesFromTxConfirmations = (
|
|||
}
|
||||
|
||||
let sigs = '0x'
|
||||
Object.keys(confirmationsMap).sort().forEach((addr) => {
|
||||
const conf = confirmationsMap[addr]
|
||||
if (conf.signature) {
|
||||
sigs += conf.signature.slice(2)
|
||||
} else {
|
||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||
sigs += `000000000000000000000000${addr.replace(
|
||||
'0x',
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
}
|
||||
})
|
||||
Object.keys(confirmationsMap)
|
||||
.sort()
|
||||
.forEach((addr) => {
|
||||
const conf = confirmationsMap[addr]
|
||||
if (conf.signature) {
|
||||
sigs += conf.signature.slice(2)
|
||||
} else {
|
||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||
sigs += `000000000000000000000000${addr.replace(
|
||||
'0x',
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
}
|
||||
})
|
||||
return sigs
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,10 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
|||
case REMOVE_SAFE_OWNER: {
|
||||
const { safeAddress, ownerAddress } = action.payload
|
||||
const { owners } = safes.get(safeAddress)
|
||||
setOwners(safeAddress, owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()))
|
||||
setOwners(
|
||||
safeAddress,
|
||||
owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()),
|
||||
)
|
||||
break
|
||||
}
|
||||
case REPLACE_SAFE_OWNER: {
|
||||
|
@ -110,7 +113,10 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
|||
const { safeAddress, ownerAddress, ownerName } = action.payload
|
||||
const { owners } = safes.get(safeAddress)
|
||||
const ownerToUpdateIndex = owners.findIndex((o) => o.address.toLowerCase() === ownerAddress.toLowerCase())
|
||||
setOwners(safeAddress, owners.update(ownerToUpdateIndex, (owner) => owner.set('name', ownerName)))
|
||||
setOwners(
|
||||
safeAddress,
|
||||
owners.update(ownerToUpdateIndex, (owner) => owner.set('name', ownerName)),
|
||||
)
|
||||
break
|
||||
}
|
||||
case SET_DEFAULT_SAFE: {
|
||||
|
|
|
@ -46,12 +46,15 @@ export default handleActions<SafeReducerState, *>(
|
|||
const tokenAddress = action.payload
|
||||
|
||||
const newState = state.withMutations((map) => {
|
||||
map.get('safes').keySeq().forEach((safeAddress) => {
|
||||
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
|
||||
const activeTokens = safeActiveTokens.add(tokenAddress)
|
||||
map
|
||||
.get('safes')
|
||||
.keySeq()
|
||||
.forEach((safeAddress) => {
|
||||
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
|
||||
const activeTokens = safeActiveTokens.add(tokenAddress)
|
||||
|
||||
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
|
||||
})
|
||||
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
|
||||
})
|
||||
})
|
||||
|
||||
return newState
|
||||
|
|
|
@ -18,6 +18,8 @@ import notifications, {
|
|||
NOTIFICATIONS_REDUCER_ID,
|
||||
type NotificationReducerState as NotificationsState,
|
||||
} from '~/logic/notifications/store/reducer/notifications'
|
||||
import cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies'
|
||||
|
||||
|
||||
export const history = createBrowserHistory()
|
||||
|
||||
|
@ -44,8 +46,10 @@ const reducers: Reducer<GlobalState> = combineReducers({
|
|||
[TOKEN_REDUCER_ID]: tokens,
|
||||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||
[NOTIFICATIONS_REDUCER_ID]: notifications,
|
||||
[COOKIES_REDUCER_ID]: cookies,
|
||||
})
|
||||
|
||||
export const store: Store<GlobalState> = createStore(reducers, finalCreateStore)
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(reducers, localState, finalCreateStore)
|
||||
|
|
|
@ -115,7 +115,7 @@ export const whenSafeDeployed = (): Promise<string> => new Promise((resolve, rej
|
|||
const interval = setInterval(() => {
|
||||
if (times >= MAX_TIMES_EXECUTED) {
|
||||
clearInterval(interval)
|
||||
reject(new Error('Didn\'t load the safe'))
|
||||
reject(new Error("Didn't load the safe"))
|
||||
}
|
||||
const url = `${window.location}`
|
||||
console.log(url)
|
||||
|
|
|
@ -12,9 +12,7 @@ import { fillAndSubmitSendFundsForm } from './utils/transactions'
|
|||
import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout'
|
||||
import { TRANSACTION_ROW_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable'
|
||||
import { useTestAccountAt, resetTestAccount } from './utils/accounts'
|
||||
import {
|
||||
CONFIRM_TX_BTN_TEST_ID,
|
||||
} from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/ButtonRow'
|
||||
import { CONFIRM_TX_BTN_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/ButtonRow'
|
||||
import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal'
|
||||
|
||||
afterEach(resetTestAccount)
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
// @flow
|
||||
import { createMuiTheme } from '@material-ui/core/styles'
|
||||
import { rgba } from 'polished'
|
||||
import {
|
||||
extraSmallFontSize,
|
||||
mediumFontSize,
|
||||
smallFontSize,
|
||||
disabled,
|
||||
primary,
|
||||
secondary,
|
||||
error,
|
||||
sm,
|
||||
md,
|
||||
lg,
|
||||
bolderFont,
|
||||
regularFont,
|
||||
boldFont,
|
||||
bolderFont,
|
||||
buttonLargeFontSize,
|
||||
disabled,
|
||||
error,
|
||||
extraSmallFontSize,
|
||||
largeFontSize,
|
||||
xs,
|
||||
lg,
|
||||
mainFontFamily,
|
||||
md,
|
||||
mediumFontSize,
|
||||
primary,
|
||||
regularFont,
|
||||
secondary,
|
||||
secondaryFontFamily,
|
||||
secondaryText,
|
||||
sm,
|
||||
smallFontSize,
|
||||
xs,
|
||||
} from './variables'
|
||||
|
||||
export type WithStyles = {
|
||||
|
@ -42,7 +45,7 @@ const palette = {
|
|||
// see https://github.com/mui-org/material-ui/blob/v1-beta/src/styles/createMuiTheme.js
|
||||
export default createMuiTheme({
|
||||
typography: {
|
||||
fontFamily: 'Averta,sans-serif',
|
||||
fontFamily: mainFontFamily,
|
||||
useNextVariants: true,
|
||||
},
|
||||
overrides: {
|
||||
|
@ -53,7 +56,7 @@ export default createMuiTheme({
|
|||
fontWeight: regularFont,
|
||||
},
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
letterSpacing: '0.9px',
|
||||
'&$disabled': {
|
||||
color: disabled,
|
||||
|
@ -109,7 +112,7 @@ export default createMuiTheme({
|
|||
},
|
||||
MuiChip: {
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
},
|
||||
},
|
||||
MuiStepIcon: {
|
||||
|
@ -132,30 +135,30 @@ export default createMuiTheme({
|
|||
},
|
||||
MuiTypography: {
|
||||
body1: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
letterSpacing: '-0.5px',
|
||||
fontSize: mediumFontSize,
|
||||
},
|
||||
body2: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
},
|
||||
},
|
||||
MuiFormHelperText: {
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
color: secondary,
|
||||
fontFamily: secondaryFontFamily,
|
||||
fontSize: '12px',
|
||||
marginTop: '0px',
|
||||
order: 0,
|
||||
padding: `0 0 0 ${md}`,
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
color: secondary,
|
||||
order: 0,
|
||||
marginTop: '0px',
|
||||
zIndex: 1, // for firefox
|
||||
},
|
||||
},
|
||||
MuiInput: {
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
color: primary,
|
||||
fontSize: mediumFontSize,
|
||||
lineHeight: '56px',
|
||||
|
@ -222,7 +225,7 @@ export default createMuiTheme({
|
|||
},
|
||||
MuiTab: {
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
fontWeight: 'normal',
|
||||
fontSize: extraSmallFontSize,
|
||||
'&$selected': {
|
||||
|
@ -244,7 +247,7 @@ export default createMuiTheme({
|
|||
top: '0px',
|
||||
},
|
||||
caption: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
fontSize: mediumFontSize,
|
||||
order: 2,
|
||||
color: disabled,
|
||||
|
@ -270,7 +273,7 @@ export default createMuiTheme({
|
|||
},
|
||||
MuiTableCell: {
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
fontSize: mediumFontSize,
|
||||
borderBottomWidth: '2px',
|
||||
},
|
||||
|
@ -298,7 +301,7 @@ export default createMuiTheme({
|
|||
},
|
||||
MuiMenuItem: {
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
},
|
||||
},
|
||||
MuiListItemIcon: {
|
||||
|
@ -308,17 +311,31 @@ export default createMuiTheme({
|
|||
},
|
||||
MuiListItemText: {
|
||||
primary: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
fontSize: mediumFontSize,
|
||||
fontWeight: bolderFont,
|
||||
color: primary,
|
||||
},
|
||||
secondary: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
fontFamily: secondaryFontFamily,
|
||||
fontSize: smallFontSize,
|
||||
color: disabled,
|
||||
},
|
||||
},
|
||||
MuiCheckbox: {
|
||||
colorSecondary: {
|
||||
'&$disabled': {
|
||||
color: rgba(secondary, 0.5),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiFormControlLabel: {
|
||||
label: {
|
||||
'&$disabled': {
|
||||
color: primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
palette,
|
||||
})
|
||||
|
|
|
@ -1,65 +1,67 @@
|
|||
// @flow
|
||||
const border = '#e8e7e6'
|
||||
const background = '#f7f5f5'
|
||||
const primary = '#001428'
|
||||
const secondary = '#008C73'
|
||||
const fontColor = '#001428'
|
||||
const fancyColor = '#f02525'
|
||||
const warningColor = '#ffc05f'
|
||||
const errorColor = '#f02525'
|
||||
const secondaryTextOrSvg = '#B2B5B2'
|
||||
const border = '#e8e7e6'
|
||||
const connectedColor = '#008C73'
|
||||
const disabled = '#5D6D74'
|
||||
const xs = '4px'
|
||||
const sm = '8px'
|
||||
const md = '16px'
|
||||
const lg = '24px'
|
||||
const xl = '32px'
|
||||
const xxl = '40px'
|
||||
const marginButtonImg = '12px'
|
||||
const errorColor = '#f02525'
|
||||
const fancyColor = '#f02525'
|
||||
const fontColor = '#001428'
|
||||
const headerHeight = '53px'
|
||||
const lg = '24px'
|
||||
const marginButtonImg = '12px'
|
||||
const md = '16px'
|
||||
const primary = '#001428'
|
||||
const secondary = '#008C73'
|
||||
const secondaryTextOrSvg = '#B2B5B2'
|
||||
const sm = '8px'
|
||||
const warningColor = '#ffc05f'
|
||||
const xl = '32px'
|
||||
const xs = '4px'
|
||||
const xxl = '40px'
|
||||
|
||||
module.exports = {
|
||||
primary,
|
||||
secondary,
|
||||
disabled,
|
||||
background,
|
||||
fontColor,
|
||||
secondaryText: secondaryTextOrSvg,
|
||||
fancy: fancyColor,
|
||||
warning: warningColor,
|
||||
error: errorColor,
|
||||
connected: connectedColor,
|
||||
headerHeight,
|
||||
xs,
|
||||
sm,
|
||||
md,
|
||||
lg,
|
||||
xl,
|
||||
xxl,
|
||||
border,
|
||||
marginButtonImg,
|
||||
fontSizeHeadingXs: 13,
|
||||
fontSizeHeadingSm: 16,
|
||||
fontSizeHeadingMd: 20,
|
||||
fontSizeHeadingLg: 32,
|
||||
buttonLargeFontSize: '16px',
|
||||
lightFont: 300,
|
||||
regularFont: 400,
|
||||
bolderFont: 500,
|
||||
boldFont: 700,
|
||||
bolderFont: 500,
|
||||
border,
|
||||
buttonLargeFontSize: '16px',
|
||||
connected: connectedColor,
|
||||
disabled,
|
||||
error: errorColor,
|
||||
extraBoldFont: 800,
|
||||
extraSmallFontSize: '11px',
|
||||
smallFontSize: '12px',
|
||||
mediumFontSize: '14px',
|
||||
largeFontSize: '16px',
|
||||
extraLargeFontSize: '20px',
|
||||
xxlFontSize: '32px',
|
||||
screenXs: 480,
|
||||
screenXsMax: 767,
|
||||
screenSm: 768,
|
||||
screenSmMax: 991,
|
||||
extraSmallFontSize: '11px',
|
||||
fancy: fancyColor,
|
||||
fontColor,
|
||||
fontSizeHeadingLg: 32,
|
||||
fontSizeHeadingMd: 20,
|
||||
fontSizeHeadingSm: 16,
|
||||
fontSizeHeadingXs: 13,
|
||||
headerHeight,
|
||||
largeFontSize: '16px',
|
||||
lg,
|
||||
lightFont: 300,
|
||||
mainFontFamily: 'Averta, sans-serif',
|
||||
marginButtonImg,
|
||||
md,
|
||||
mediumFontSize: '14px',
|
||||
primary,
|
||||
regularFont: 400,
|
||||
screenLg: 1200,
|
||||
screenMd: 992,
|
||||
screenMdMax: 1199,
|
||||
screenLg: 1200,
|
||||
screenSm: 768,
|
||||
screenSmMax: 991,
|
||||
screenXs: 480,
|
||||
screenXsMax: 767,
|
||||
secondary,
|
||||
secondaryFontFamily: 'Averta, monospace',
|
||||
secondaryText: secondaryTextOrSvg,
|
||||
sm,
|
||||
smallFontSize: '12px',
|
||||
warning: warningColor,
|
||||
xl,
|
||||
xs,
|
||||
xxl,
|
||||
xxlFontSize: '32px',
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue