WA-279 - Create safe in a three step process (#11)
* WA-279 Creating Stepper component for simulating a Wizard Form with multiple steps * WA-279 Introduced a review middle step when creating a new Safe * WA-279 Added more Layout components (Bold, Span, GnoForm...) * WA-279 Added validation to final-form's create safe form, including isEthAddress validator to form's owners addresses * WA-279 Updated storybook Header and Footer
This commit is contained in:
parent
135cbd1568
commit
d2b5131c9e
|
@ -3,7 +3,9 @@ import { addDecorator, configure } from '@storybook/react'
|
|||
import { withKnobs } from '@storybook/addon-knobs'
|
||||
import { MuiThemeProvider } from 'material-ui/styles'
|
||||
import * as React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import StoryRouter from 'storybook-router'
|
||||
import { store } from '~/store'
|
||||
import theme from '~/theme/mui'
|
||||
import 'index.scss'
|
||||
|
||||
|
@ -14,22 +16,14 @@ import 'index.scss'
|
|||
addDecorator(withKnobs);
|
||||
addDecorator(StoryRouter())
|
||||
|
||||
// Adding Material UI Theme
|
||||
addDecorator(story => (
|
||||
<MuiThemeProvider theme={theme}>
|
||||
{ story() }
|
||||
</MuiThemeProvider>
|
||||
addDecorator((story) => (
|
||||
<Provider store={store}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
{ story() }
|
||||
</MuiThemeProvider>
|
||||
</Provider>
|
||||
))
|
||||
|
||||
/*
|
||||
https://storybook.js.org/addons/introduction/
|
||||
addDecorator((story) => (
|
||||
<div>
|
||||
{ story() }
|
||||
</div>
|
||||
));
|
||||
*/
|
||||
|
||||
const components = require.context('../src/components', true, /\.stories\.((js|ts)x?)$/)
|
||||
const routes = require.context('../src/routes', true, /\.stories\.((js|ts)x?)$/)
|
||||
|
||||
|
@ -38,4 +32,4 @@ function loadStories() {
|
|||
routes.keys().forEach((filename) => routes(filename))
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
||||
configure(loadStories, module)
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"@babel/preset-flow": "^7.0.0-beta.40",
|
||||
"@babel/preset-react": "^7.0.0-beta.40",
|
||||
"@babel/preset-stage-0": "^7.0.0-beta.40",
|
||||
"@sambego/storybook-state": "^1.0.7",
|
||||
"@storybook/addon-actions": "^3.3.15",
|
||||
"@storybook/addon-knobs": "^3.3.15",
|
||||
"@storybook/addon-links": "^3.3.15",
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
// @flow
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import * as React from 'react'
|
||||
import { host } from 'storybook-host'
|
||||
import styles from '~/components/layout/PageFrame/index.scss'
|
||||
import Component from './index'
|
||||
|
||||
const FrameDecorator = story => (
|
||||
<div className={styles.frame}>
|
||||
<div style={{ flex: '1' }} />
|
||||
{ story() }
|
||||
</div>
|
||||
)
|
||||
|
||||
storiesOf('Components', module)
|
||||
.addDecorator(host({
|
||||
title: 'Footer',
|
||||
align: 'center',
|
||||
height: 250,
|
||||
width: '100%',
|
||||
}))
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Footer', () => <Component />)
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
import { select } from '@storybook/addon-knobs'
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import * as React from 'react'
|
||||
import Block from '~/components/layout/Block'
|
||||
import styles from '~/components/layout/PageFrame/index.scss'
|
||||
import Component from './Layout'
|
||||
|
||||
const FrameDecorator = story => (
|
||||
<Block className={styles.frame}>
|
||||
<div className={styles.frame}>
|
||||
{ story() }
|
||||
</Block>
|
||||
</div>
|
||||
)
|
||||
|
||||
storiesOf('Components', module)
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Link from '~/components/layout/Link'
|
||||
|
||||
type NextButtonProps = {
|
||||
text: string,
|
||||
disabled: boolean,
|
||||
}
|
||||
|
||||
const NextButton = ({ text, disabled }: NextButtonProps) => (
|
||||
<Button
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={disabled}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
|
||||
const GoButton = () => (
|
||||
<Link to="/welcome">
|
||||
<NextButton text="GO" disabled={false} />
|
||||
</Link>
|
||||
)
|
||||
|
||||
type ControlProps = {
|
||||
next: string,
|
||||
onPrevious: () => void,
|
||||
firstPage: boolean,
|
||||
submitting: boolean,
|
||||
}
|
||||
|
||||
const ControlButtons = ({
|
||||
next, firstPage, onPrevious, submitting,
|
||||
}: ControlProps) => (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={firstPage || submitting}
|
||||
onClick={onPrevious}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<NextButton text={next} disabled={submitting} />
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
type Props = {
|
||||
finishedTx: boolean,
|
||||
onPrevious: () => void,
|
||||
firstPage: boolean,
|
||||
lastPage: boolean,
|
||||
submitting: boolean,
|
||||
}
|
||||
|
||||
const Controls = ({
|
||||
finishedTx, onPrevious, firstPage, lastPage, submitting,
|
||||
}: Props) => (
|
||||
<React.Fragment>
|
||||
{ finishedTx
|
||||
? <GoButton />
|
||||
: <ControlButtons
|
||||
submitting={submitting}
|
||||
next={lastPage ? 'Finish' : 'Next'}
|
||||
firstPage={firstPage}
|
||||
onPrevious={onPrevious}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
export default Controls
|
|
@ -0,0 +1,14 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
}
|
||||
|
||||
const Step = ({ children }: Props) => (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Step
|
|
@ -0,0 +1,118 @@
|
|||
// @flow
|
||||
import Stepper, { Step as FormStep, StepLabel } from 'material-ui/Stepper'
|
||||
import * as React from 'react'
|
||||
import type { FormApi } from 'react-final-form'
|
||||
import GnoForm from '~/components/forms/GnoForm'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Controls from './Controls'
|
||||
|
||||
export { default as Step } from './Step'
|
||||
|
||||
type Props = {
|
||||
steps: string[],
|
||||
finishedTransaction: boolean,
|
||||
initialValues?: Object,
|
||||
children: React$Node,
|
||||
onSubmit: (values: Object, form: FormApi, callback: ?(errors: ?Object) => void) => ?Object | Promise<?Object> | void,
|
||||
}
|
||||
|
||||
type State = {
|
||||
page: number,
|
||||
values: Object,
|
||||
}
|
||||
|
||||
type PageProps = {
|
||||
children: Function,
|
||||
}
|
||||
|
||||
class GnoStepper extends React.PureComponent<Props, State> {
|
||||
static Page = ({ children }: PageProps) => children
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
values: props.initialValues || {},
|
||||
}
|
||||
}
|
||||
|
||||
getActivePageFrom = (pages: React$Node) => {
|
||||
const activePageProps = React.Children.toArray(pages)[this.state.page].props
|
||||
const { children, ...props } = activePageProps
|
||||
|
||||
return children(props)
|
||||
}
|
||||
|
||||
validate = (values: Object) => {
|
||||
const activePage = React.Children.toArray(this.props.children)[
|
||||
this.state.page
|
||||
]
|
||||
return activePage.props.validate ? activePage.props.validate(values) : {}
|
||||
}
|
||||
|
||||
next = (values: Object) =>
|
||||
this.setState(state => ({
|
||||
page: Math.min(state.page + 1, React.Children.count(this.props.children) - 1),
|
||||
values,
|
||||
}))
|
||||
|
||||
previous = () =>
|
||||
this.setState(state => ({
|
||||
page: Math.max(state.page - 1, 0),
|
||||
}))
|
||||
|
||||
handleSubmit = (values: Object) => {
|
||||
const { children, onSubmit } = this.props
|
||||
const { page } = this.state
|
||||
const isLastPage = page === React.Children.count(children) - 1
|
||||
if (isLastPage) {
|
||||
return onSubmit(values)
|
||||
}
|
||||
|
||||
return this.next(values)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { steps, children, finishedTransaction } = this.props
|
||||
const { page, values } = this.state
|
||||
const activePage = this.getActivePageFrom(children)
|
||||
const isLastPage = page === steps.length - 1
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Stepper activeStep={page} alternativeLabel>
|
||||
{steps.map(label => (
|
||||
<FormStep key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</FormStep>
|
||||
))}
|
||||
</Stepper>
|
||||
<GnoForm
|
||||
onSubmit={this.handleSubmit}
|
||||
initialValues={values}
|
||||
padding={15}
|
||||
validation={this.validate}
|
||||
render={activePage}
|
||||
>
|
||||
{(submitting: boolean) => (
|
||||
<Row align="end" margin="lg" grow>
|
||||
<Col xs={12} center="xs">
|
||||
<Controls
|
||||
submitting={submitting}
|
||||
finishedTx={finishedTransaction}
|
||||
onPrevious={this.previous}
|
||||
firstPage={page === 0}
|
||||
lastPage={isLastPage}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</GnoForm>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default GnoStepper
|
|
@ -6,25 +6,30 @@ import type { FormApi } from 'react-final-form'
|
|||
type Props = {
|
||||
onSubmit: (values: Object, form: FormApi, callback: ?(errors: ?Object) => void) => ?Object | Promise<?Object> | void,
|
||||
children: Function,
|
||||
width: string,
|
||||
padding: number,
|
||||
validation?: (values: Object) => Object | Promise<Object>,
|
||||
initialValues?: Object,
|
||||
render: Function,
|
||||
}
|
||||
|
||||
const calculateWidth = (width: string): $Shape<CSSStyleDeclaration> => ({
|
||||
maxWidth: `${width}px`,
|
||||
const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
||||
padding: `0 ${padding}%`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 0 auto',
|
||||
})
|
||||
|
||||
const GnoForm = ({
|
||||
onSubmit, validation, initialValues, children, width,
|
||||
onSubmit, validation, initialValues, children, padding, render,
|
||||
}: Props) => (
|
||||
<Form
|
||||
validate={validation}
|
||||
onSubmit={onSubmit}
|
||||
initialValues={initialValues}
|
||||
render={({ handleSubmit, ...rest }) => (
|
||||
<form onSubmit={handleSubmit} style={calculateWidth(width)}>
|
||||
{children(rest)}
|
||||
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
||||
{render(rest)}
|
||||
{children(rest.submitting)}
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// @flow
|
||||
import { getWeb3 } from '~/wallets/getWeb3'
|
||||
|
||||
type Field = boolean | number | string
|
||||
|
||||
export const required = (value: Field) => (value ? undefined : 'Required')
|
||||
|
@ -24,5 +26,11 @@ export const maxValue = (max: number) => (value: number) => {
|
|||
|
||||
export const ok = () => undefined
|
||||
|
||||
export const mustBeEthereumAddress = (address: Field) => {
|
||||
const isAddress: boolean = getWeb3().isAddress(address)
|
||||
|
||||
return isAddress ? undefined : 'Address should be a valid Ethereum address'
|
||||
}
|
||||
|
||||
export const composeValidators = (...validators: Function[]) => (value: Field) =>
|
||||
validators.reduce((error, validator) => error || validator(value), undefined)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.block {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sm {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
}
|
||||
|
||||
class Bold extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { children, ...props } = this.props
|
||||
|
||||
return (
|
||||
<b {...props}>
|
||||
{ children }
|
||||
</b>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Bold
|
|
@ -196,4 +196,4 @@
|
|||
|
||||
@media only screen and (min-width: $(screenLg)px) {
|
||||
@mixin row Lg;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const Page = ({ children, align }: Props) => (
|
||||
<main className={cx(styles.container, align)}>
|
||||
<main className={cx(styles.page, align)}>
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
.page {
|
||||
margin: 0px 30px;
|
||||
align-self: flex-end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.center {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.frame {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
background-color: $tertiary;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
background-color: white;
|
||||
padding: $xl;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import classNames from 'classnames/bind'
|
||||
import * as css from './index.scss'
|
||||
import styles from './index.scss'
|
||||
|
||||
const cx = classNames.bind(css)
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
type Props = {
|
||||
center?: boolean,
|
||||
noMargin?: boolean,
|
||||
bold?: boolean,
|
||||
size?: 'sm',
|
||||
size?: 'sm' | 'md' | 'lg',
|
||||
color?: 'soft' | 'medium' | 'dark' | 'primary',
|
||||
children: React$Node
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class Paragraph extends React.PureComponent<Props> {
|
|||
} = this.props
|
||||
|
||||
return (
|
||||
<p className={cx(color, { bold }, { noMargin }, size, { center })} {...props}>
|
||||
<p className={cx(styles.paragraph, { bold }, { noMargin }, size, { center })} {...props}>
|
||||
{ children }
|
||||
</p>
|
||||
)
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
.paragraph {
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: inherit;
|
||||
}
|
||||
|
||||
.soft {
|
||||
color: #888888;
|
||||
}
|
||||
|
@ -26,6 +31,14 @@
|
|||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
.md {
|
||||
font-size: $mediumFontSize;
|
||||
}
|
||||
|
||||
.lg {
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// @flow
|
||||
import classNames from 'classnames/bind'
|
||||
import * as React from 'react'
|
||||
import styles from './index.scss'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
}
|
||||
|
||||
const Pre = ({ children, ...props }: Props) => (
|
||||
<pre className={cx(styles.pre)} {...props}>
|
||||
{children}
|
||||
</pre>
|
||||
)
|
||||
|
||||
export default Pre
|
|
@ -0,0 +1,4 @@
|
|||
.pre {
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import classNames from 'classnames/bind'
|
||||
import React from 'react'
|
||||
import { capitalize } from '~/utils/css'
|
||||
import styles from './index.scss'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
@ -8,12 +9,27 @@ const cx = classNames.bind(styles)
|
|||
type Props = {
|
||||
className?: string,
|
||||
children: React$Node,
|
||||
margin?: 'sm' | 'md' | 'lg' | 'xl',
|
||||
align?: 'center' | 'end' | 'start',
|
||||
grow?: boolean,
|
||||
}
|
||||
|
||||
const Row = ({ children, className, ...props }: Props) => (
|
||||
<div className={cx(styles.row, className)} {...props}>
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
const Row = ({
|
||||
children, className, margin, align, grow, ...props
|
||||
}: Props) => {
|
||||
const rowClassNames = cx(
|
||||
styles.row,
|
||||
margin ? capitalize(margin, 'margin') : undefined,
|
||||
align ? capitalize(align, 'align') : undefined,
|
||||
{ grow },
|
||||
className,
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={rowClassNames} {...props}>
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Row
|
||||
|
|
|
@ -4,3 +4,33 @@
|
|||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.grow {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.marginSm {
|
||||
margin-bottom: $sm;
|
||||
}
|
||||
|
||||
.marginMd {
|
||||
margin-bottom: $md;
|
||||
}
|
||||
|
||||
.marginLg {
|
||||
margin-bottom: $xl;
|
||||
}
|
||||
|
||||
.marginXl {
|
||||
margin-bottom: $xl;
|
||||
}
|
||||
|
||||
.alignStart {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.alignEnd {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.alignCenter {
|
||||
align-items: center;
|
||||
}
|
23
src/index.js
23
src/index.js
|
@ -1,33 +1,16 @@
|
|||
// @flow
|
||||
import 'babel-polyfill'
|
||||
import { createBrowserHistory } from 'history'
|
||||
|
||||
import { MuiThemeProvider } from 'material-ui/styles'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ConnectedRouter, routerMiddleware, routerReducer } from 'react-router-redux'
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import provider, { REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import { ConnectedRouter } from 'react-router-redux'
|
||||
import { history, store } from '~/store'
|
||||
import theme from '~/theme/mui'
|
||||
import AppRoutes from '~/routes'
|
||||
import './index.scss'
|
||||
|
||||
const history = createBrowserHistory()
|
||||
// eslint-disable-next-line
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
||||
const finalCreateStore = composeEnhancers(applyMiddleware(
|
||||
thunk,
|
||||
routerMiddleware(history),
|
||||
))
|
||||
|
||||
const reducers = combineReducers({
|
||||
routing: routerReducer,
|
||||
[REDUCER_ID]: provider,
|
||||
})
|
||||
|
||||
const store = createStore(reducers, finalCreateStore)
|
||||
|
||||
const Root = () => (
|
||||
<Provider store={store}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
|
|
|
@ -4,18 +4,23 @@ html, body {
|
|||
}
|
||||
|
||||
body {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow-x: hidden;
|
||||
color: #1f5f76;
|
||||
display: grid;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: $regularFontSize;
|
||||
grid-template-columns:1fr;
|
||||
grid-template-rows:1fr;
|
||||
height: 100%;
|
||||
font-size: $mediumFontSize;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body>div:first-child {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: calc(100% - (2 * $xl));
|
||||
padding: $xl;
|
||||
background-image: linear-gradient(to bottom, $primary, #1a829d, #1a829d, #1f5f76);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { CircularProgress } from 'material-ui/Progress'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Pre from '~/components/layout/Pre'
|
||||
import Row from '~/components/layout/Row'
|
||||
|
||||
type FormProps = {
|
||||
submitting: boolean,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
address: string,
|
||||
tx: Object,
|
||||
}
|
||||
|
||||
const Deployment = ({ address, tx }: Props) => (
|
||||
<Block>
|
||||
<Paragraph><Bold>Deployed safe to: </Bold>{address}</Paragraph>
|
||||
<Pre>
|
||||
{JSON.stringify(tx, null, 2) }
|
||||
</Pre>
|
||||
</Block>
|
||||
)
|
||||
|
||||
export default ({ address, tx }: Props) => ({ submitting }: FormProps) => {
|
||||
const txFinished = !!address
|
||||
|
||||
return (
|
||||
<Block>
|
||||
{ !txFinished &&
|
||||
<React.Fragment>
|
||||
<Paragraph center size="lg">
|
||||
You are about to create a Safe for keeping your funds more secure.
|
||||
</Paragraph>
|
||||
<Paragraph center size="lg">
|
||||
Remember to check you have enough funds in your wallet.
|
||||
</Paragraph>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Row>
|
||||
<Col xs={12} center={submitting ? 'xs' : undefined} margin="lg">
|
||||
{ submitting
|
||||
? <CircularProgress size={50} />
|
||||
: txFinished && <Deployment address={address} tx={tx} />
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
)
|
||||
}
|
|
@ -1,87 +1,59 @@
|
|||
// @flow
|
||||
import Button from 'material-ui/Button'
|
||||
import * as React from 'react'
|
||||
import { Field } from 'react-final-form'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import GnoForm from '~/components/forms/GnoForm'
|
||||
import Name from './Name'
|
||||
import Owners from './Owners'
|
||||
import Confirmations from './Confirmations'
|
||||
import type { FormApi } from 'react-final-form'
|
||||
import Stepper from '~/components/Stepper'
|
||||
import Confirmation from '~/routes/open/components/FormConfirmation'
|
||||
import Review from '~/routes/open/components/ReviewInformation'
|
||||
import SafeFields, { safeFieldsValidation } from '~/routes/open/components/SafeForm'
|
||||
|
||||
type Props = {
|
||||
onCallSafeContractSubmit: Function,
|
||||
onAddFunds: Function,
|
||||
safeAddress: string,
|
||||
funds: number,
|
||||
userAccount: string,
|
||||
}
|
||||
|
||||
const validation = (values) => {
|
||||
const errors = {}
|
||||
|
||||
if (values.owners < values.confirmations) {
|
||||
errors.confirmations = 'Number of confirmations can not be higher than the number of owners'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const NewSafe = ({ values }: Object) => (
|
||||
<Block margin="md">
|
||||
<Heading tag="h2" margin="lg">Deploy a new Safe</Heading>
|
||||
<Name />
|
||||
<Owners numOwners={values.owners} />
|
||||
<Confirmations />
|
||||
<Block margin="xl">
|
||||
<Button variant="raised" color="primary" type="submit">
|
||||
Create Safe
|
||||
</Button>
|
||||
</Block>
|
||||
</Block>
|
||||
)
|
||||
const getSteps = () => [
|
||||
'Fill Safe Form', 'Review Information', 'Deploy it',
|
||||
]
|
||||
|
||||
const initialValuesFrom = (userAccount: string) => ({
|
||||
owner0Address: userAccount,
|
||||
})
|
||||
|
||||
const Open = ({
|
||||
funds, safeAddress, onCallSafeContractSubmit, onAddFunds, userAccount,
|
||||
}: Props) => (
|
||||
<React.Fragment>
|
||||
<GnoForm
|
||||
onSubmit={onCallSafeContractSubmit}
|
||||
initialValues={initialValuesFrom(userAccount)}
|
||||
width="500"
|
||||
validation={validation}
|
||||
>
|
||||
{ NewSafe }
|
||||
</GnoForm>
|
||||
<GnoForm onSubmit={onAddFunds} width="500">
|
||||
{(pristine, invalid) => (
|
||||
<Block margin="md">
|
||||
<Heading tag="h2" margin="lg">Add Funds to the safe</Heading>
|
||||
<div style={{ margin: '10px 0px' }}>
|
||||
<label style={{ marginRight: '10px' }}>{safeAddress || 'Not safe detected'}</label>
|
||||
</div>
|
||||
{ safeAddress &&
|
||||
<div>
|
||||
<Field name="fundsToAdd" component={TextField} type="text" placeholder="ETH to add" />
|
||||
<Button type="submit" disabled={!safeAddress || pristine || invalid}>
|
||||
Add funds
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
{ safeAddress &&
|
||||
<div style={{ margin: '15px 0px' }}>
|
||||
Total funds in this safe: { funds || 0 } ETH
|
||||
</div>
|
||||
}
|
||||
</Block>
|
||||
)}
|
||||
</GnoForm>
|
||||
</React.Fragment>
|
||||
)
|
||||
type Props = {
|
||||
provider: string,
|
||||
userAccount: string,
|
||||
safeAddress: string,
|
||||
safeTx: string,
|
||||
onCallSafeContractSubmit: (values: Object, form: FormApi, callback: ?(errors: ?Object) => void)
|
||||
=> ?Object | Promise<?Object> | void,
|
||||
}
|
||||
|
||||
export default Open
|
||||
const Layout = ({
|
||||
provider, userAccount, safeAddress, safeTx, onCallSafeContractSubmit,
|
||||
}: Props) => {
|
||||
const steps = getSteps()
|
||||
const initialValues = initialValuesFrom(userAccount)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ provider
|
||||
? (
|
||||
<Stepper
|
||||
onSubmit={onCallSafeContractSubmit}
|
||||
finishedTransaction={!!safeAddress}
|
||||
steps={steps}
|
||||
initialValues={initialValues}
|
||||
>
|
||||
<Stepper.Page validate={safeFieldsValidation}>
|
||||
{ SafeFields }
|
||||
</Stepper.Page>
|
||||
<Stepper.Page>
|
||||
{ Review }
|
||||
</Stepper.Page>
|
||||
<Stepper.Page address={safeAddress} tx={safeTx}>
|
||||
{ Confirmation }
|
||||
</Stepper.Page>
|
||||
</Stepper>
|
||||
)
|
||||
: <div>No metamask detected</div>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import { State, Store } from '@sambego/storybook-state'
|
||||
import * as React from 'react'
|
||||
import styles from '~/components/layout/PageFrame/index.scss'
|
||||
import { getAccountsFrom, getThresholdFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||
import { getProviderInfo } from '~/wallets/getWeb3'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import Component from './Layout'
|
||||
|
||||
const FrameDecorator = story => (
|
||||
<div className={styles.frame}>
|
||||
{ story() }
|
||||
</div>
|
||||
)
|
||||
|
||||
const store = new Store({
|
||||
safeAddress: '',
|
||||
safeTx: '',
|
||||
})
|
||||
|
||||
|
||||
storiesOf('Routes', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Open safe with all props set', () => {
|
||||
getProviderInfo()
|
||||
const provider = 'METAMASK'
|
||||
const userAccount = '0x03db1a8b26d08df23337e9276a36b474510f0023'
|
||||
const onCallSafeContractSubmit = async (values: Object) => {
|
||||
const accounts = getAccountsFrom(values)
|
||||
const numConfirmations = getThresholdFrom(values)
|
||||
const data = {
|
||||
userAccount,
|
||||
accounts,
|
||||
requiredConfirmations: numConfirmations,
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
console.log(`Generating and sending a eth tx based on: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
await sleep(3000)
|
||||
|
||||
store.set({
|
||||
safeAddress: '0x03db1a8b26d08df23337e9276a36b474510f0025',
|
||||
// eslint-disable-next-line
|
||||
safeTx: {"transactionHash":"0x4603de1ab6a92b4ee1fd67189089f5c02f5df5d135bf85af84083c27808c0544","transactionIndex":0,"blockHash":"0x593ce7d85fef2a492e8f759f485c8b66ff803773e77182c68dd45c439b7a956d","blockNumber":19,"gasUsed":3034193,"cumulativeGasUsed":3034193,"contractAddress":"0xfddda33736fb95b587cbfecc1ff4a50f717adc00","logs":[],"status":"0x01","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<State store={store}>
|
||||
<Component
|
||||
provider={provider}
|
||||
userAccount={userAccount}
|
||||
safeAddress={store.get('safeAddress')}
|
||||
safeTx={store.get('safeTx')}
|
||||
onCallSafeContractSubmit={onCallSafeContractSubmit}
|
||||
/>
|
||||
</State>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { getNamesFrom, getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
|
||||
type FormProps = {
|
||||
values: Object,
|
||||
}
|
||||
|
||||
const ReviewInformation = () => ({ values }: FormProps) => {
|
||||
const names = getNamesFrom(values)
|
||||
const addresses = getAccountsFrom(values)
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<Heading tag="h2">Review the Safe information</Heading>
|
||||
<Paragraph>
|
||||
<Bold>Safe Name: </Bold> {values.name}
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Bold>Required confirmations: </Bold> {values.confirmations}
|
||||
</Paragraph>
|
||||
<Heading tag="h3">Owners</Heading>
|
||||
{ names.map((name, index) => (
|
||||
<Row key={`name${(index)}`} margin="md">
|
||||
<Col xs={11} xsOffset={1}margin="sm">
|
||||
<Block>
|
||||
<Paragraph noMargin>{name}</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col xs={11} xsOffset={1} margin="sm">
|
||||
<Block>
|
||||
<Paragraph noMargin>{addresses[index]}</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReviewInformation
|
|
@ -2,7 +2,7 @@
|
|||
import * as React from 'react'
|
||||
import Field from '~/components/forms/Field'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import { composeValidators, minValue, mustBeNumber, required } from '~/components/forms/validator'
|
||||
import { composeValidators, minValue, mustBeNumber, mustBeEthereumAddress, required } from '~/components/forms/validator'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
|
@ -46,7 +46,7 @@ const Owners = ({ numOwners }: Props) => (
|
|||
name={`owner${index}Address`}
|
||||
component={TextField}
|
||||
type="text"
|
||||
validate={required}
|
||||
validate={composeValidators(required, mustBeEthereumAddress)}
|
||||
placeholder="Owner Address*"
|
||||
text="Owner Address"
|
||||
/>
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
import Name from './Name'
|
||||
import Owners from './Owners'
|
||||
import Confirmations from './Confirmations'
|
||||
|
||||
export const safeFieldsValidation = (values: Object) => {
|
||||
const errors = {}
|
||||
|
||||
if (values.owners < values.confirmations) {
|
||||
errors.confirmations = 'Number of confirmations can not be higher than the number of owners'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export default () => ({ values }: Object) => (
|
||||
<Block margin="md">
|
||||
<Heading tag="h2" margin="lg">Deploy a new Safe</Heading>
|
||||
<Name />
|
||||
<Owners numOwners={values.owners} />
|
||||
<Confirmations />
|
||||
</Block>
|
||||
)
|
|
@ -1,14 +1,15 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import contract from 'truffle-contract'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import { getAccountsFrom, getThresholdFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||
import { getWeb3 } from '~/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import Safe from '#/GnosisSafe.json'
|
||||
import Layout from '../components/Layout'
|
||||
import selector from './selector'
|
||||
import { getAccountsFrom, getThresholdFrom } from './safe'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
type Props = {
|
||||
provider: string,
|
||||
|
@ -17,7 +18,7 @@ type Props = {
|
|||
|
||||
type State = {
|
||||
safeAddress: string,
|
||||
funds: number,
|
||||
safeTx: string,
|
||||
}
|
||||
|
||||
class Open extends React.Component<Props, State> {
|
||||
|
@ -26,30 +27,12 @@ class Open extends React.Component<Props, State> {
|
|||
|
||||
this.state = {
|
||||
safeAddress: '',
|
||||
funds: 0,
|
||||
safeTx: '',
|
||||
}
|
||||
|
||||
this.safe = contract(Safe)
|
||||
}
|
||||
|
||||
onAddFunds = async (values: Object) => {
|
||||
const { fundsToAdd } = values
|
||||
const { safeAddress } = this.state
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
const accounts = await promisify(cb => web3.eth.getAccounts(cb))
|
||||
const txData = { from: accounts[0], to: safeAddress, value: web3.toWei(fundsToAdd, 'ether') }
|
||||
await promisify(cb => web3.eth.sendTransaction(txData, cb))
|
||||
const funds = await promisify(cb => web3.eth.getBalance(safeAddress, cb))
|
||||
const fundsInEther = funds ? web3.fromWei(funds.toNumber(), 'ether') : 0
|
||||
this.setState({ funds: fundsInEther })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log(`Errog adding funds to safe${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onCallSafeContractSubmit = async (values) => {
|
||||
try {
|
||||
const { userAccount } = this.props
|
||||
|
@ -65,7 +48,7 @@ class Open extends React.Component<Props, State> {
|
|||
const transactionReceipt = await promisify(cb => web3.eth.getTransactionReceipt(transactionHash, cb))
|
||||
// eslint-disable-next-line
|
||||
console.log(`Transaction Receipt${JSON.stringify(transactionReceipt)}`)
|
||||
this.setState({ safeAddress: safeInstance.address })
|
||||
this.setState({ safeAddress: safeInstance.address, safeTx: transactionReceipt })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log('Error while creating the Safe' + error)
|
||||
|
@ -75,20 +58,18 @@ class Open extends React.Component<Props, State> {
|
|||
safe: any
|
||||
|
||||
render() {
|
||||
const { safeAddress, safeTx } = this.state
|
||||
const { provider, userAccount } = this.props
|
||||
const { safeAddress, funds } = this.state
|
||||
|
||||
return (
|
||||
<PageFrame>
|
||||
{ provider
|
||||
? <Layout
|
||||
userAccount={userAccount}
|
||||
safeAddress={safeAddress}
|
||||
onAddFunds={this.onAddFunds}
|
||||
funds={funds}
|
||||
onCallSafeContractSubmit={this.onCallSafeContractSubmit}
|
||||
/>
|
||||
: <div>No metamask detected</div>
|
||||
}
|
||||
<Layout
|
||||
provider={provider}
|
||||
userAccount={userAccount}
|
||||
safeAddress={safeAddress}
|
||||
safeTx={safeTx}
|
||||
onCallSafeContractSubmit={this.onCallSafeContractSubmit}
|
||||
/>
|
||||
</PageFrame>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import contract from 'truffle-contract'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import { getWeb3 } from '~/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import Safe from '#/GnosisSafe.json'
|
||||
import Layout from '../components/Layout'
|
||||
import selector from './selector'
|
||||
import { getAccountsFrom, getThresholdFrom } from './safe'
|
||||
|
||||
type Props = {
|
||||
provider: string,
|
||||
userAccount: string,
|
||||
}
|
||||
|
||||
type State = {
|
||||
safeAddress: string,
|
||||
funds: number,
|
||||
}
|
||||
|
||||
class Open extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
safeAddress: '',
|
||||
funds: 0,
|
||||
}
|
||||
|
||||
this.safe = contract(Safe)
|
||||
}
|
||||
|
||||
onAddFunds = async (values: Object) => {
|
||||
const { fundsToAdd } = values
|
||||
const { safeAddress } = this.state
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
const accounts = await promisify(cb => web3.eth.getAccounts(cb))
|
||||
const txData = { from: accounts[0], to: safeAddress, value: web3.toWei(fundsToAdd, 'ether') }
|
||||
await promisify(cb => web3.eth.sendTransaction(txData, cb))
|
||||
const funds = await promisify(cb => web3.eth.getBalance(safeAddress, cb))
|
||||
const fundsInEther = funds ? web3.fromWei(funds.toNumber(), 'ether') : 0
|
||||
this.setState({ funds: fundsInEther })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log(`Errog adding funds to safe${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onCallSafeContractSubmit = async (values) => {
|
||||
try {
|
||||
const { userAccount } = this.props
|
||||
const accounts = getAccountsFrom(values)
|
||||
const numConfirmations = getThresholdFrom(values)
|
||||
|
||||
const web3 = getWeb3()
|
||||
this.safe.setProvider(web3.currentProvider)
|
||||
|
||||
const safeInstance = await this.safe.new(accounts, numConfirmations, 0, 0, { from: userAccount, gas: '5000000' })
|
||||
const { transactionHash } = safeInstance
|
||||
|
||||
const transactionReceipt = await promisify(cb => web3.eth.getTransactionReceipt(transactionHash, cb))
|
||||
// eslint-disable-next-line
|
||||
console.log(`Transaction Receipt${JSON.stringify(transactionReceipt)}`)
|
||||
this.setState({ safeAddress: safeInstance.address })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log('Error while creating the Safe' + error)
|
||||
}
|
||||
}
|
||||
|
||||
safe: any
|
||||
|
||||
render() {
|
||||
const { provider, userAccount } = this.props
|
||||
const { safeAddress, funds } = this.state
|
||||
return (
|
||||
<PageFrame>
|
||||
{ provider
|
||||
? <Layout
|
||||
userAccount={userAccount}
|
||||
safeAddress={safeAddress}
|
||||
onAddFunds={this.onAddFunds}
|
||||
funds={funds}
|
||||
onCallSafeContractSubmit={this.onCallSafeContractSubmit}
|
||||
/>
|
||||
: <div>No metamask detected</div>
|
||||
}
|
||||
</PageFrame>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(selector)(Open)
|
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
|
||||
/*
|
||||
onAddFunds = async (values: Object) => {
|
||||
const { fundsToAdd } = values
|
||||
const { safeAddress } = this.state
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
const accounts = await promisify(cb => web3.eth.getAccounts(cb))
|
||||
const txData = { from: accounts[0], to: safeAddress, value: web3.toWei(fundsToAdd, 'ether') }
|
||||
await promisify(cb => web3.eth.sendTransaction(txData, cb))
|
||||
const funds = await promisify(cb => web3.eth.getBalance(safeAddress, cb))
|
||||
const fundsInEther = funds ? web3.fromWei(funds.toNumber(), 'ether') : 0
|
||||
this.setState({ funds: fundsInEther })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log(`Errog adding funds to safe${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<GnoForm onSubmit={onAddFunds} width="500">
|
||||
{(pristine, invalid) => (
|
||||
<Block margin="md">
|
||||
<Heading tag="h2" margin="lg">Add Funds to the safe</Heading>
|
||||
<div style={{ margin: '10px 0px' }}>
|
||||
<label style={{ marginRight: '10px' }}>{safeAddress || 'Not safe detected'}</label>
|
||||
</div>
|
||||
{ safeAddress &&
|
||||
<div>
|
||||
<Field name="fundsToAdd" component={TextField} type="text" placeholder="ETH to add" />
|
||||
<Button type="submit" disabled={!safeAddress || pristine || invalid}>
|
||||
Add funds
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
{ safeAddress &&
|
||||
<div style={{ margin: '15px 0px' }}>
|
||||
Total funds in this safe: { funds || 0 } ETH
|
||||
</div>
|
||||
}
|
||||
</Block>
|
||||
)}
|
||||
</GnoForm>
|
||||
*/
|
|
@ -1,10 +0,0 @@
|
|||
// @flow
|
||||
export const getAccountsFrom = (values: Object): string[] => {
|
||||
const accounts = Object.keys(values).filter(key => /^owner\d+Address$/.test(key))
|
||||
|
||||
return accounts.map(account => values[account])
|
||||
}
|
||||
|
||||
export const getThresholdFrom = (values: Object): number => {
|
||||
return Number(values.confirmations)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
// @flow
|
||||
import { getAccountsFrom, getThresholdFrom } from './safe'
|
||||
|
||||
describe('Test JS', () => {
|
||||
it('return the addresses of owners', () => {
|
||||
const safe = {
|
||||
owner0Address: 'foo',
|
||||
owner1Address: 'bar',
|
||||
owner2Address: 'baz',
|
||||
}
|
||||
|
||||
expect(['foo', 'bar', 'baz']).toEqual(getAccountsFrom(safe))
|
||||
})
|
||||
it('return the number of required confirmations', () => {
|
||||
const safe = {
|
||||
confirmations: '1',
|
||||
}
|
||||
|
||||
expect(1).toEqual(getThresholdFrom(safe))
|
||||
})
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
// @flow
|
||||
export const getAccountsFrom = (values: Object): string[] => {
|
||||
const accounts = Object.keys(values).sort().filter(key => /^owner\d+Address$/.test(key))
|
||||
|
||||
return accounts.map(account => values[account]).slice(0, values.owners)
|
||||
}
|
||||
|
||||
export const getNamesFrom = (values: Object): string[] => {
|
||||
const accounts = Object.keys(values).sort().filter(key => /^owner\d+Name$/.test(key))
|
||||
|
||||
return accounts.map(account => values[account]).slice(0, values.owners)
|
||||
}
|
||||
|
||||
export const getThresholdFrom = (values: Object): number => Number(values.confirmations)
|
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import { getAccountsFrom, getNamesFrom, getThresholdFrom } from './safeDataExtractor'
|
||||
|
||||
describe('Test JS', () => {
|
||||
it('return the addresses of owners', () => {
|
||||
const safe = {
|
||||
owner0Address: 'foo',
|
||||
owner1Address: 'bar',
|
||||
owner2Address: 'baz',
|
||||
owners: 3,
|
||||
}
|
||||
|
||||
expect(getAccountsFrom(safe)).toEqual(['foo', 'bar', 'baz'])
|
||||
})
|
||||
it('return the names of owners', () => {
|
||||
const safe = {
|
||||
owner0Name: 'foo',
|
||||
owner1Name: 'bar',
|
||||
owner2Name: 'baz',
|
||||
owners: 3,
|
||||
}
|
||||
|
||||
expect(getNamesFrom(safe)).toEqual(['foo', 'bar', 'baz'])
|
||||
})
|
||||
it('return first number of owners info based on owners property', () => {
|
||||
const safe = {
|
||||
owner0Name: 'fooName',
|
||||
owner0Address: 'fooAddress',
|
||||
owner1Name: 'barName',
|
||||
owner1Address: 'barAddress',
|
||||
owner2Name: 'bazName',
|
||||
owner2Address: 'bazAddress',
|
||||
owners: 1,
|
||||
}
|
||||
|
||||
expect(getNamesFrom(safe)).toEqual(['fooName'])
|
||||
expect(getAccountsFrom(safe)).toEqual(['fooAddress'])
|
||||
})
|
||||
it('return name and address ordered alphabetically', () => {
|
||||
const safe = {
|
||||
owner1Name: 'barName',
|
||||
owner1Address: 'barAddress',
|
||||
owner0Name: 'fooName',
|
||||
owner2Name: 'bazName',
|
||||
owner2Address: 'bazAddress',
|
||||
owner0Address: 'fooAddress',
|
||||
owners: 1,
|
||||
}
|
||||
|
||||
expect(getNamesFrom(safe)).toEqual(['fooName'])
|
||||
expect(getAccountsFrom(safe)).toEqual(['fooAddress'])
|
||||
})
|
||||
it('return the number of required confirmations', () => {
|
||||
const safe = {
|
||||
confirmations: '1',
|
||||
}
|
||||
|
||||
expect(getThresholdFrom(safe)).toEqual(1)
|
||||
})
|
||||
})
|
|
@ -1,7 +1,6 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import Block from '~/components/layout/Block'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Link from '~/components/layout/Link'
|
||||
|
@ -14,25 +13,23 @@ type Props = {
|
|||
}
|
||||
|
||||
const Welcome = ({ provider }: Props) => (
|
||||
<PageFrame align="center">
|
||||
<Block className={styles.safe}>
|
||||
<Img alt="Safe Box" src={vault} height={330} />
|
||||
<Block className={styles.safeActions}>
|
||||
{ provider &&
|
||||
<Link to="/open">
|
||||
<Button variant="raised" color="primary">
|
||||
Create a new Safe
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
<Link to="/transactions">
|
||||
<Block className={styles.safe}>
|
||||
<Img alt="Safe Box" src={vault} height={330} />
|
||||
<Block className={styles.safeActions} margin="md">
|
||||
{ provider &&
|
||||
<Link to="/open">
|
||||
<Button variant="raised" color="primary">
|
||||
Open a Safe
|
||||
Create a new Safe
|
||||
</Button>
|
||||
</Link>
|
||||
</Block>
|
||||
}
|
||||
<Link to="/transactions">
|
||||
<Button variant="raised" color="primary">
|
||||
Open a Safe
|
||||
</Button>
|
||||
</Link>
|
||||
</Block>
|
||||
</PageFrame>
|
||||
</Block>
|
||||
)
|
||||
|
||||
export default Welcome
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import { select } from '@storybook/addon-knobs'
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import * as React from 'react'
|
||||
import styles from '~/components/layout/PageFrame/index.scss'
|
||||
import Component from './Layout'
|
||||
|
||||
const FrameDecorator = story => (
|
||||
<div className={styles.frame}>
|
||||
{ story() }
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
storiesOf('Routes', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Welcome with Metamask connected', () => {
|
||||
const provider = select('Status by Provider', ['', 'UNKNOWN', 'METAMASK', 'PARITY'], 'METAMASK')
|
||||
return (
|
||||
<Component
|
||||
provider={provider}
|
||||
fetchProvider={() => { }}
|
||||
/>
|
||||
)
|
||||
})
|
|
@ -1,24 +0,0 @@
|
|||
// @flow
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import * as React from 'react'
|
||||
import Component from './Layout'
|
||||
|
||||
storiesOf('Routes', module)
|
||||
.add('Welcome with Metamask connected', () => (
|
||||
<Component
|
||||
provider="METAMASK"
|
||||
fetchProvider={() => { }}
|
||||
/>
|
||||
))
|
||||
.add('Welcome with Parity connected', () => (
|
||||
<Component
|
||||
provider="PARITY"
|
||||
fetchProvider={() => { }}
|
||||
/>
|
||||
))
|
||||
.add('Welcome without provider', () => (
|
||||
<Component
|
||||
provider=""
|
||||
fetchProvider={() => { }}
|
||||
/>
|
||||
))
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import Layout from '../components/Layout'
|
||||
import selector from './selector'
|
||||
|
||||
|
@ -9,7 +10,9 @@ type Props = {
|
|||
}
|
||||
|
||||
const Welcome = ({ provider }: Props) => (
|
||||
<Layout provider={provider} />
|
||||
<PageFrame align="center">
|
||||
<Layout provider={provider} />
|
||||
</PageFrame>
|
||||
)
|
||||
|
||||
export default connect(selector)(Welcome)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// @flow
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { routerMiddleware, routerReducer } from 'react-router-redux'
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import provider, { REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
|
||||
export const history = createBrowserHistory()
|
||||
// eslint-disable-next-line
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
||||
const finalCreateStore = composeEnhancers(applyMiddleware(
|
||||
thunk,
|
||||
routerMiddleware(history),
|
||||
))
|
||||
|
||||
const reducers = combineReducers({
|
||||
routing: routerReducer,
|
||||
[REDUCER_ID]: provider,
|
||||
})
|
||||
|
||||
export const store = createStore(reducers, finalCreateStore)
|
|
@ -22,7 +22,8 @@ module.exports = Object.assign({}, {
|
|||
regularFontWeight: 400,
|
||||
boldFontWeight: 700,
|
||||
smallFontSize: '12px',
|
||||
regularFontSize: '14px',
|
||||
mediumFontSize: '14px',
|
||||
largeFontSize: '18px',
|
||||
screenXs: 480,
|
||||
screenXsMax: 767,
|
||||
screenSm: 768,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
export const upperFirst = (value: string) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
|
||||
type Value = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number | boolean
|
||||
type Value = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'center' | 'end' | 'start' | number | boolean
|
||||
|
||||
export const capitalize = (value: Value, prefix?: string) => {
|
||||
if (!value) {
|
||||
|
|
|
@ -688,6 +688,12 @@
|
|||
lodash "^4.2.0"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@sambego/storybook-state@^1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@sambego/storybook-state/-/storybook-state-1.0.7.tgz#4409793c0d34f1d351af1c8045a2764b5af574c0"
|
||||
dependencies:
|
||||
uuid "^3.1.0"
|
||||
|
||||
"@sindresorhus/is@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
||||
|
|
Loading…
Reference in New Issue