on master help sign in
Merge pull request #100 from urbit/chain-dropdown
7604fc2Christopher Johnson
src/bridge/components/InnerLabelDropdown.js+88−45
@@ -1,71 +1,114 @@
1import React from 'react'
1import React from 'react'
2import PropTypes from 'prop-types';
2import PropTypes from 'prop-types';
3
3
4+import { DropdownItem, DropdownDivider } from '../components/Base'
4
5
5−const InnerLabelDropdown = props => {
6+class InnerLabelDropdown extends React.Component {
6−
7+ constructor(props) {
8+ super(props)
9+
10+ this.state = {
11+ isOpen: false
12+ }
13+
14+ this.toggle = this.toggle.bind(this)
15+ this.close = this.close.bind(this)
16+ }
17+
18+ toggle() {
19+ this.setState({
20+ isOpen: !this.state.isOpen
21+ })
22+ }
23+
24+ close() {
25+ this.setState({
26+ isOpen: false
27+ })
28+ }
29+
30+ selectOption(opt) {
31+ this.close()
32+ this.props.handleUpdate(opt)
33+ }
34+
35+ getOptionElems() {
36+ return this.props.options.map(opt => {
37+ if (opt.type === "divider") {
38+ return <DropdownDivider />
39+ } else {
40+ return (
41+ <DropdownItem onClick={() => this.selectOption(opt.value)}>
42+ { opt.title }
43+ </DropdownItem>
44+ )
45+ }
46+ })
47+ }
48+
49+ render() {
50+ const { props, state } = this
51+
52+ const optionElems = this.getOptionElems()
53+
7− const menuVisibility = props.isOpen === true
54+ const menuVisibility = state.isOpen === true
8− ? 'visible'
55+ ? 'visible'
9− : 'hidden';
56+ : 'hidden';
10−
57+
11− const selectedClasses = props.isOpen === true
58+ const selectedClasses = state.isOpen === true
12− ? 'br-blue'
59+ ? 'br-blue'
13− : 'br-gray-50';
60+ : 'br-gray-50';
14−
61+
62+ const fullWidthClass = props.fullWidth ? 'full-width' : ''
63+
15− return (
64+ return (
16− <div className={'rel table'}>
65+ <div className={`rel table ${fullWidthClass}`}>
17− <div
66+ <div
18− className={`fix full o-0 ${menuVisibility}`}
67+ className={`fix full o-0 ${menuVisibility}`}
19− onClick={props.handleClose}
68+ onClick={this.close}
20− />
69+ />
21−
70+
22− <button
71+ <button
23− className={`h-10 mp-0 bg-white outline-blue sans ${props.className}`}
72+ className={`h-10 mp-0 bg-white outline-blue sans ${props.className} ${fullWidthClass}`}
24− onClick={props.handleToggle}
73+ onClick={this.toggle}
25− disabled={props.disabled}>
74+ disabled={props.disabled}>
26− <div className={'flex'}>
75+ <div className={'flex'}>
27− <div className={`ph-4 flex-center-all b-2 br-0 b-solid h-10 ${selectedClasses}`}>
76+ <div className={`ph-4 flex align-center b-2 br-0 b-solid h-10 ${selectedClasses} ${fullWidthClass}`}>
28− <div className={'text-400 mr-2'}>{props.title}</div>
77+ <div className={'text-400 mr-2'}>{props.title}</div>
29− <div className={'text-600'}>{props.currentSelectionTitle}</div>
78+ <div className={'text-600'}>{props.currentSelectionTitle}</div>
79+ </div>
80+ <div className={'white bg-blue s-10 flex-center-all text-sm'}>{'▼'}</div>
30 </div>
81 </div>
31− <div className={'white bg-blue s-10 flex-center-all text-sm'}>{'▼'}</div>
32− </div>
33− </button>
82+ </button>
34
83
35− <div className={`abs flex-column bg-white t-12 l-0 w-100 ${menuVisibility}`}>
84+ <div className={`abs flex-column bg-white t-12 l-0 w-100 z-1 ${menuVisibility}`}>
36− <div className={'pv-1 b-solid b-gray-50 b-2 bb-0'} />
85+ <div className={'pv-1 b-solid b-gray-50 b-2 bb-0'} />
37
86
38− { props.children }
87+ { optionElems }
39
88
40− <div className={'pv-1 b-solid b-gray-50 b-2 bt-0'} />
89+ <div className={'pv-1 b-solid b-gray-50 b-2 bt-0'} />
41− </div>
90+ </div>
42
91
43− </div>
92+ </div>
44− )
93+ )
94+ }
45}
95}
46
96
47InnerLabelDropdown.propTypes = {
97InnerLabelDropdown.propTypes = {
48 disabled: PropTypes.bool,
98 disabled: PropTypes.bool,
49− isOpen: PropTypes.bool,
50− handleToggle: PropTypes.func,
51− handleClose: PropTypes.func,
52 className: PropTypes.string,
99 className: PropTypes.string,
53 title: PropTypes.string,
100 title: PropTypes.string,
54− children: PropTypes.node,
55 style: PropTypes.object,
101 style: PropTypes.object,
56 currentSelectionTitle: PropTypes.string,
102 currentSelectionTitle: PropTypes.string,
103+ children: PropTypes.node,
57};
104};
58
105
59−
60InnerLabelDropdown.defaultProps = {
106InnerLabelDropdown.defaultProps = {
61− isOpen: false,
107+ disabled: false,
62 className: '',
108 className: '',
63 title: '',
109 title: '',
64− currentSelectionTitle: '',
65− handleToggle: () => {},
66− handleClose: () => {},
67− disabled: false,
68 style: {},
110 style: {},
111+ currentSelectionTitle: '',
69};
112};
70
113
71export default InnerLabelDropdown
114export default InnerLabelDropdown
src/bridge/components/StatelessTransaction.js+86−15
@@ -3,7 +3,8 @@ import React from 'react'
3
3
4import { Code, H3 } from './Base'
4import { Code, H3 } from './Base'
5import { Button } from './Base'
5import { Button } from './Base'
6−import { CheckboxButton, Input, InnerLabel } from './Base'
6+import { CheckboxButton, Input, InnerLabel,
7+ InnerLabelDropdown } from './Base'
7import { Warning } from '../components/Base'
8import { Warning } from '../components/Base'
8
9
9import { addressFromSecp256k1Public } from '../lib/wallet'
10import { addressFromSecp256k1Public } from '../lib/wallet'
@@ -28,10 +29,11 @@ class StatelessTransaction extends React.Component {
28 showGasDetails: false,
29 showGasDetails: false,
29 userApproval: false,
30 userApproval: false,
30 chainId: '',
31 chainId: '',
32+ customChain: false,
31 nonce: '',
33 nonce: '',
32 stx: Nothing(),
34 stx: Nothing(),
33 txn: Nothing(),
35 txn: Nothing(),
34− txError: Nothing(),
36+ txError: Nothing()
35 }
37 }
36
38
37 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
39 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
@@ -46,6 +48,7 @@ class StatelessTransaction extends React.Component {
46 this.setGasLimit = this.setGasLimit.bind(this)
48 this.setGasLimit = this.setGasLimit.bind(this)
47 this.rangeChange = this.rangeChange.bind(this)
49 this.rangeChange = this.rangeChange.bind(this)
48 this.toggleGasDetails = this.toggleGasDetails.bind(this)
50 this.toggleGasDetails = this.toggleGasDetails.bind(this)
51+ this.handleChainUpdate = this.handleChainUpdate.bind(this)
49 }
52 }
50
53
51 componentDidMount() {
54 componentDidMount() {
@@ -106,6 +109,19 @@ class StatelessTransaction extends React.Component {
106 })
109 })
107 }
110 }
108
111
112+ handleChainUpdate(chainId) {
113+ if (chainId === "custom") {
114+ this.setState({
115+ customChain: true
116+ })
117+ } else {
118+ this.setState({
119+ customChain: false,
120+ chainId
121+ })
122+ }
123+ }
124+
109 setUserApproval(){
125 setUserApproval(){
110 const {state} = this
126 const {state} = this
111 this.setState({ userApproval: !state.userApproval })
127 this.setState({ userApproval: !state.userApproval })
@@ -185,13 +201,53 @@ class StatelessTransaction extends React.Component {
185 this.submit()
201 this.submit()
186 }
202 }
187
203
204+ getChainTitle(chainId) {
205+ let map = {
206+ "1": "Mainnet - 1",
207+ "2": "Morden - 2",
208+ "3": "Ropsten - 3",
209+ "4": "Goerli - 4",
210+ "42": "Kovan - 42",
211+ "1337": "Geth private chains - 1337",
212+ "custom": "Custom"
213+ }
214+
215+ return map[chainId]
216+ }
217+
218+ getChainOptions() {
219+ return [{
220+ title: this.getChainTitle("1"),
221+ value: "1"
222+ }, {
223+ title: this.getChainTitle("2"),
224+ value: "2"
225+ }, {
226+ title: this.getChainTitle("3"),
227+ value: "3"
228+ }, {
229+ title: this.getChainTitle("4"),
230+ value: "4"
231+ }, {
232+ title: this.getChainTitle("42"),
233+ value: "42"
234+ }, {
235+ title: this.getChainTitle("1337"),
236+ value: "1337"
237+ }, {
238+ title: this.getChainTitle("custom"),
239+ value: "custom"
240+ }]
241+ }
242+
188 render() {
243 render() {
189 const { web3, canGenerate } = this.props
244 const { web3, canGenerate } = this.props
190 const { gasPrice, gasLimit, nonce, chainId,
245 const { gasPrice, gasLimit, nonce, chainId,
191− txn, stx, userApproval, showGasDetails } = this.state
246+ txn, stx, userApproval, showGasDetails,
247+ customChain } = this.state
192
248
193 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
249 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
194− setUserApproval, sendTxn, createUnsignedTxn } = this
250+ setUserApproval, sendTxn, createUnsignedTxn, handleChainUpdate } = this
195
251
196 const { state } = this
252 const { state } = this
197
253
@@ -199,6 +255,8 @@ class StatelessTransaction extends React.Component {
199 const canApprove = Just.hasInstance(stx)
255 const canApprove = Just.hasInstance(stx)
200 const canSend = Just.hasInstance(stx) && userApproval === true
256 const canSend = Just.hasInstance(stx) && userApproval === true
201
257
258+ const chainOptions = this.getChainOptions()
259+
202 const generateButtonColor =
260 const generateButtonColor =
203 Nothing.hasInstance(txn)
261 Nothing.hasInstance(txn)
204 ? 'blue'
262 ? 'blue'
@@ -294,7 +352,7 @@ class StatelessTransaction extends React.Component {
294
352
295 const nonceDialogue =
353 const nonceDialogue =
296 <Input
354 <Input
297− className={ 'mono mt-4' }
355+ className={ 'mono mt-4 mb-4' }
298 prop-size={ 'md' }
356 prop-size={ 'md' }
299 prop-format={ 'innerLabel' }
357 prop-format={ 'innerLabel' }
300 value={ nonce }
358 value={ nonce }
@@ -305,18 +363,30 @@ class StatelessTransaction extends React.Component {
305 </InnerLabel>
363 </InnerLabel>
306 </Input>
364 </Input>
307
365
366+ const chainDialogueTitle = customChain ? "Custom" : this.getChainTitle(chainId)
308 const chainDialogue =
367 const chainDialogue =
309− <Input
368+ <InnerLabelDropdown
310− className={ 'mono mt-4 mb-8' }
369+ className={'mt-6'}
311− prop-size={ 'md' }
370+ fullWidth={true}
312− prop-format={ 'innerLabel' }
371+ title={'Chain ID'}
313− value={ chainId }
372+ options={chainOptions}
314− onChange={ setChainId }
373+ handleUpdate={handleChainUpdate}
374+ currentSelectionTitle={chainDialogueTitle}
315 >
375 >
376+ </InnerLabelDropdown>
377+
378+ const customChainDialogue = !customChain ? null :
379+ <Input
380+ className={ 'mono mt-4 mb-8' }
381+ prop-size={ 'md' }
382+ prop-format={ 'innerLabel' }
383+ value={ chainId }
384+ onChange={ setChainId }
385+ >
316− <InnerLabel>
386+ <InnerLabel>
317− { 'Chain ID' }
387+ { 'Chain ID' }
318− </InnerLabel>
388+ </InnerLabel>
319− </Input>
389+ </Input>
320
390
321 const onlineParamsDialogue = web3.matchWith({
391 const onlineParamsDialogue = web3.matchWith({
322 Just: _ => <div />,
392 Just: _ => <div />,
@@ -324,6 +394,7 @@ class StatelessTransaction extends React.Component {
324 <React.Fragment>
394 <React.Fragment>
325 { nonceDialogue }
395 { nonceDialogue }
326 { chainDialogue }
396 { chainDialogue }
397+ { customChainDialogue }
327 </React.Fragment>
398 </React.Fragment>
328 })
399 })
329
400
src/bridge/style/extend.css+8
@@ -477,3 +477,11 @@ code {
477 padding: 1rem 2rem;
477 padding: 1rem 2rem;
478 margin-bottom: 4rem;
478 margin-bottom: 4rem;
479}
479}
480+
481+.full-width {
482+ width: 100%;
483+}
484+
485+.z-1 {
486+ z-index: 1
487+}
src/bridge/views/Network.js+43−98
@@ -1,11 +1,7 @@
1import Maybe from 'folktale/maybe'
1import Maybe from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import { Button } from '../components/Base'
3import { Button } from '../components/Base'
4−import {
5− DropdownItem,
6− InnerLabelDropdown,
4+import { InnerLabelDropdown } from '../components/Base'
7− DropdownDivider,
8−} from '../components/Base'
9import { Row, Col, H1, P } from '../components/Base'
5import { Row, Col, H1, P } from '../components/Base'
10import * as azimuth from 'azimuth-js'
6import * as azimuth from 'azimuth-js'
11import Web3 from 'web3'
7import Web3 from 'web3'
@@ -19,21 +15,9 @@ class Network extends React.Component {
19 constructor(props) {
15 constructor(props) {
20 super(props)
16 super(props)
21
17
22− this.state = {
23− dropdownOpen: false
24− };
25−
26− this.toggle = this.toggle.bind(this);
27− this.close = this.close.bind(this);
28 this.handleNetworkChange = this.handleNetworkChange.bind(this);
18 this.handleNetworkChange = this.handleNetworkChange.bind(this);
29 }
19 }
30
20
31− toggle() {
32− this.setState((state, _) => ({
33− dropdownOpen: !state.dropdownOpen
34− }))
35− }
36−
37 handleNetworkChange(network) {
21 handleNetworkChange(network) {
38 const { setNetworkType, setNetwork } = this.props
22 const { setNetworkType, setNetwork } = this.props
39
23
@@ -99,97 +83,58 @@ class Network extends React.Component {
99 }
83 }
100 }
84 }
101
85
102− selectClose(f) {
86+ getNetworkOptions() {
103− f()
87+ return [{
104− this.setState({ dropdownOpen: false })
88+ title: 'Main Network (default)',
89+ value: NETWORK_NAMES.MAINNET
90+ }, {
91+ title: 'Local Node',
92+ value: NETWORK_NAMES.LOCAL
105− }
93+ }, {
106−
94+ title: 'Ropsten',
107−
95+ value: NETWORK_NAMES.ROPSTEN
108− close () {
96+ }, {
109− this.setState({ dropdownOpen: false })
97+ type: 'divider'
98+ }, {
99+ title: 'Offline',
100+ value: NETWORK_NAMES.OFFLINE
101+ }]
110 }
102 }
111
103
112 render() {
104 render() {
113− const { dropdownOpen } = this.state
114 const { networkType, pushRoute } = this.props
105 const { networkType, pushRoute } = this.props
106+ const networkOptions = this.getNetworkOptions()
115
107
116 return (
108 return (
117 <Row>
109 <Row>
118 <Col>
110 <Col>
119 <H1>{ 'Select Network' }</H1>
111 <H1>{ 'Select Network' }</H1>
120
112
121−
122− <P>
113+ <P>
123− {
114+ {
124− "Please select the Ethereum node you'd like to send " +
115+ "Please select the Ethereum node you'd like to send " +
125− "transactions to. For highly valuable keys, please select " +
116+ "transactions to. For highly valuable keys, please select " +
126− "offline mode."
117+ "offline mode."
127− }
118+ }
128− </P>
119+ </P>
129−
120+
130−
131− <InnerLabelDropdown
121+ <InnerLabelDropdown
132− className={'mt-6'}
122+ className={'mt-6'}
133− handleToggle={this.toggle}
134− handleClose={this.close}
135− isOpen={dropdownOpen}
136− title={'Node:'}
123+ title={'Node:'}
124+ handleUpdate={this.handleNetworkChange}
125+ options={networkOptions}
137− currentSelectionTitle={renderNetworkType(networkType)}
126+ currentSelectionTitle={renderNetworkType(networkType)}>
138− >
139−
140− <DropdownItem
141− isSelected={ false }
142− onClick={
143− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.MAINNET))
144− }
145− >
146− { 'Main Network (default)' }
147− </DropdownItem>
148−
149− <DropdownItem
150− isSelected={false}
151− onClick={
152− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.LOCAL))
153− }
154− >
155− { //'Ganache Node @ Port 8545 (Default)'
156− }
157− {'Local Node'}
158− </DropdownItem>
159−
160− <DropdownItem
161− // disabled={ true }
162− isSelected={ false }
163− onClick={
164− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.ROPSTEN))
165− }
166− >
167− { 'Ropsten' }
168− </DropdownItem>
169−
170− <DropdownDivider />
171−
172− <DropdownItem
173− // disabled={ true }
174− isSelected={ false }
175− onClick={
176− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.OFFLINE))
177− }
178− >
179− { 'Offline' }
180− </DropdownItem>
181−
182− </InnerLabelDropdown>
127+ </InnerLabelDropdown>
183−
128+
184− <Button
129+ <Button
185− className={'mt-10'}
130+ className={'mt-10'}
186− onClick={ () => pushRoute(ROUTE_NAMES.WALLET) }
131+ onClick={ () => pushRoute(ROUTE_NAMES.WALLET) }
187− >
132+ >
188− { 'Continue →' }
133+ { 'Continue →' }
189− </Button>
134+ </Button>
190−
135+
191− </Col>
136+ </Col>
192− </Row>
137+ </Row>
193 )
138 )
194 }
139 }
195}
140}
src/bridge/views/Wallet.js+32−84
@@ -1,52 +1,50 @@
1import Maybe from 'folktale/maybe'
1import Maybe from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import { Button, H1, P } from '../components/Base'
3import { Button, H1, P } from '../components/Base'
4−import {
5− InnerLabelDropdown,
4+import { InnerLabelDropdown } from '../components/Base'
6− DropdownItem,
7− DropdownDivider
8−} from '../components/Base'
9import { Row, Col } from '../components/Base'
5import { Row, Col } from '../components/Base'
10
6
11import { ROUTE_NAMES } from '../lib/router'
7import { ROUTE_NAMES } from '../lib/router'
12import { WALLET_NAMES, renderWalletType } from '../lib/wallet'
8import { WALLET_NAMES, renderWalletType } from '../lib/wallet'
13
9
14class Wallet extends React.Component {
10class Wallet extends React.Component {
15−
16− constructor(props) {
17− super(props)
18−
19− this.state = {
20− dropdownOpen: false
21− }
22−
23− this.toggle = this.toggle.bind(this)
24− this.close = this.close.bind(this)
25−
26− }
27−
28 componentDidMount() {
11 componentDidMount() {
29 const { setWallet } = this.props
12 const { setWallet } = this.props
30 setWallet(Maybe.Nothing())
13 setWallet(Maybe.Nothing())
31 }
14 }
32
15
33− toggle() {
16+ getWalletOptions() {
34− this.setState((state, _) => ({
17+ return [{
35− dropdownOpen: !state.dropdownOpen
18+ title: 'Urbit Master Ticket',
36− }))
19+ value: WALLET_NAMES.TICKET
20+ }, {
21+ title: 'Urbit Master Ticket (Shards)',
22+ value: WALLET_NAMES.SHARDS
23+ }, {
24+ type: 'divider'
37− }
25+ }, {
38−
26+ title: 'Ledger',
39− selectClose(f) {
27+ value: WALLET_NAMES.LEDGER
40− f()
28+ }, {
41− this.setState({ dropdownOpen: false })
29+ title: 'Trezor',
30+ value: WALLET_NAMES.TREZOR
31+ }, {
32+ type: 'divider'
42− }
33+ }, {
43−
34+ title: 'BIP39 Mnemonic',
44− close () {
35+ value: WALLET_NAMES.MNEMONIC
45− this.setState({ dropdownOpen: false })
36+ }, {
37+ title: 'Ethereum Private Key',
38+ value: WALLET_NAMES.PRIVATE_KEY
39+ }, {
40+ title: 'Ethereum Keystore File',
41+ value: WALLET_NAMES.KEYSTORE
42+ }]
46 }
43 }
47
44
48 render() {
45 render() {
49− const { props, state } = this
46+ const { props } = this
47+ const walletOptions = this.getWalletOptions()
50
48
51 return (
49 return (
52 <Row>
50 <Row>
@@ -63,60 +61,10 @@ class Wallet extends React.Component {
63
61
64 <InnerLabelDropdown
62 <InnerLabelDropdown
65 className={'mb-10 mt-6'}
63 className={'mb-10 mt-6'}
66− handleToggle={this.toggle}
67− handleClose={this.close}
68− isOpen={state.dropdownOpen}
69 title={'Wallet Type:'}
64 title={'Wallet Type:'}
65+ options={walletOptions}
66+ handleUpdate={props.setWalletType}
70 currentSelectionTitle={renderWalletType(props.walletType)}>
67 currentSelectionTitle={renderWalletType(props.walletType)}>
71−
72− <DropdownItem
73− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.TICKET)) }
74− >
75− { 'Urbit Master Ticket' }
76− </DropdownItem>
77−
78− <DropdownItem
79− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.SHARDS)) }
80− >
81− { 'Urbit Master Ticket (Shards)' }
82− </DropdownItem>
83−
84− <DropdownDivider />
85−
86− <DropdownItem
87− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.LEDGER)) }
88− >
89− { 'Ledger' }
90− </DropdownItem>
91−
92− <DropdownItem
93− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.TREZOR)) }
94− >
95− { 'Trezor' }
96− </DropdownItem>
97−
98− <DropdownDivider />
99−
100− <DropdownItem
101− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.MNEMONIC)) }
102− >
103− { 'BIP39 Mnemonic' }
104− </DropdownItem>
105−
106− <DropdownItem
107− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.PRIVATE_KEY)) }
108− >
109− { 'Ethereum Private Key' }
110− </DropdownItem>
111−
112− <DropdownItem
113− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.KEYSTORE)) }
114− >
115− { 'Ethereum Keystore File' }
116− </DropdownItem>
117−
118−
119−
120 </InnerLabelDropdown>
68 </InnerLabelDropdown>
121
69
122 <Button
70 <Button