on master help sign in
Get started on invite onboarding flow
086c302Fang
src/bridge/lib/router.js+22−4
@@ -2,12 +2,16 @@ import * as ob from 'urbit-ob'
2
2
3import React from 'react';
3import React from 'react';
4
4
5+import Landing from '../views/Landing'
6+
7+import InviteLogin from '../views/InviteLogin'
8+import InviteClaim from '../views/InviteClaim'
9+
5import AcceptTransfer from '../views/AcceptTransfer'
10import AcceptTransfer from '../views/AcceptTransfer'
6import CancelTransfer from '../views/CancelTransfer'
11import CancelTransfer from '../views/CancelTransfer'
7import CreateGalaxy from '../views/CreateGalaxy'
12import CreateGalaxy from '../views/CreateGalaxy'
8import GenKeyfile from '../views/GenKeyfile'
13import GenKeyfile from '../views/GenKeyfile'
9import IssueChild from '../views/IssueChild'
14import IssueChild from '../views/IssueChild'
10−import Landing from '../views/Landing'
11import Network from '../views/Network'
15import Network from '../views/Network'
12import Mnemonic from '../views/Mnemonic'
16import Mnemonic from '../views/Mnemonic'
13import Ledger from '../views/Ledger'
17import Ledger from '../views/Ledger'
@@ -34,6 +38,11 @@ import { renderNetworkType } from './network'
34import { BRIDGE_ERROR } from './error'
38import { BRIDGE_ERROR } from './error'
35
39
36const ROUTE_NAMES = {
40const ROUTE_NAMES = {
41+ DEFAULT: Symbol('DEFAULT'),
42+ //
43+ INVITE_LOGIN: Symbol('INVITE_LOGIN'),
44+ INVITE_CLAIM: Symbol('INVITE_CLAIM'),
45+ //
37 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
46 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
38 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
47 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
39 LANDING: Symbol('LANDING'),
48 LANDING: Symbol('LANDING'),
@@ -57,12 +66,16 @@ const ROUTE_NAMES = {
57 SET_KEYS: Symbol('SET_KEYS'),
66 SET_KEYS: Symbol('SET_KEYS'),
58 TRANSFER: Symbol('TRANSFER'),
67 TRANSFER: Symbol('TRANSFER'),
59 SENT_TRANSACTION: Symbol('SENT_TRANSACTION'),
68 SENT_TRANSACTION: Symbol('SENT_TRANSACTION'),
60− GEN_KEYFILE: Symbol('GEN_KEYFILE'),
69+ GEN_KEYFILE: Symbol('GEN_KEYFILE')
61− DEFAULT: Symbol('DEFAULT')
62}
70}
63
71
64const createRoutes = () => {
72const createRoutes = () => {
65 const routes = {}
73 const routes = {}
74+ routes[ROUTE_NAMES.DEFAULT] = Landing
75+ //
76+ routes[ROUTE_NAMES.INVITE_LOGIN] = InviteLogin
77+ routes[ROUTE_NAMES.INVITE_CLAIM] = InviteClaim
78+ //
66 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
79 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
67 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
80 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
68 routes[ROUTE_NAMES.LANDING] = Landing
81 routes[ROUTE_NAMES.LANDING] = Landing
@@ -86,7 +99,6 @@ const createRoutes = () => {
86 routes[ROUTE_NAMES.SET_KEYS] = SetKeys
99 routes[ROUTE_NAMES.SET_KEYS] = SetKeys
87 routes[ROUTE_NAMES.TRANSFER] = Transfer
100 routes[ROUTE_NAMES.TRANSFER] = Transfer
88 routes[ROUTE_NAMES.SENT_TRANSACTION] = SentTransaction
101 routes[ROUTE_NAMES.SENT_TRANSACTION] = SentTransaction
89− routes[ROUTE_NAMES.DEFAULT] = Landing
90 routes[ROUTE_NAMES.GEN_KEYFILE] = GenKeyfile
102 routes[ROUTE_NAMES.GEN_KEYFILE] = GenKeyfile
91 return routes
103 return routes
92}
104}
@@ -99,6 +111,12 @@ const renderRoute = (props, route) => {
99 route === ROUTE_NAMES.LANDING
111 route === ROUTE_NAMES.LANDING
100 ? 'Bridge'
112 ? 'Bridge'
101
113
114+ : route === ROUTE_NAMES.INVITE_LOGIN
115+ ? 'Invite code'
116+
117+ : route === ROUTE_NAMES.INVITE_CLAIM
118+ ? 'Claim invite'
119+
102 : route === ROUTE_NAMES.NETWORK
120 : route === ROUTE_NAMES.NETWORK
103 ? `${renderNetworkType(networkType)}`
121 ? `${renderNetworkType(networkType)}`
104
122
src/bridge/views/InviteClaim.jsnew file+139
@@ -0,0 +1,139 @@
1+import Maybe from 'folktale/maybe'
2+import React from 'react'
3+import { pour } from 'sigil-js'
4+import * as ob from 'urbit-ob'
5+import * as azimuth from 'azimuth-js'
6+
7+import { addressFromSecp256k1Public, EthereumWallet } from '../lib/wallet'
8+
9+import PointList from '../components/PointList'
10+import ReactSVGComponents from '../components/ReactSVGComponents'
11+import KeysAndMetadata from './Point/KeysAndMetadata'
12+import Actions from './Point/Actions'
13+import { BRIDGE_ERROR } from '../lib/error'
14+import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
15+
16+
17+class InviteClaim extends React.Component {
18+
19+ constructor(props) {
20+ super(props)
21+
22+ this.state = {
23+ loaded: false,
24+ point: Maybe.Nothing(),
25+ newTicket: Maybe.Nothing(),
26+ isGenerating: true
27+ }
28+ }
29+
30+ componentDidMount() {
31+ const { web3, wallet, contracts, setPointCursor } = this.props
32+
33+ web3.chain(_ =>
34+ contracts.chain(ctrcs =>
35+ wallet.chain(async wal => {
36+ const addr = addressFromSecp256k1Public(wal.publicKey);
37+ console.log('fetching incoming for', addr)
38+ const incoming = await azimuth.azimuth.getTransferringFor(ctrcs, addr)
39+ console.log('incoming', incoming)
40+
41+ this.state.loaded = true;
42+ if (0 === incoming.length) {
43+ setPointCursor(Maybe.Nothing());
44+ } else {
45+ setPointCursor(Maybe.Just(incoming[0]));
46+ this.generateWallet(incoming[0])
47+ if (1 < incoming.length) {
48+ //TODO notify user "...and others"
49+ }
50+ }
51+ })))
52+ }
53+
54+ componentDidUpdate(prevProps) {
55+ //
56+ }
57+
58+ generateWallet(point) {
59+ //TODO combine wg/views/Generate and Download into single lib function?
60+ }
61+
62+ render() {
63+
64+ const { pointCursor } = this.props
65+
66+ //TODO do something about DOM duplication below
67+
68+ if (!this.state.loaded) {
69+ return (
70+ <Row>
71+ <Col>
72+ {'Loading...'}
73+ </Col>
74+ </Row>
75+ )
76+ }
77+
78+ if (!Maybe.Just.hasInstance(pointCursor)) {
79+ return (
80+ <Row>
81+ <Col>
82+ <Warning>
83+ <h3 className={'mb-2'}>{'Warning'}</h3>
84+ {
85+ 'This invite has no claimable balance.'
86+ }
87+ </Warning>
88+ </Col>
89+ </Row>
90+ )
91+ }
92+
93+
94+ const point = pointCursor.matchWith({
95+ Just: (cursor) => cursor.value,
96+ Nothing: () => {
97+ throw BRIDGE_ERROR.MISSING_POINT
98+ }
99+ })
100+
101+ const name = ob.patp(point)
102+
103+ const sigil = pour({
104+ patp: name,
105+ renderer: ReactSVGComponents,
106+ size: 256
107+ })
108+
109+ return (
110+ <Row>
111+ <Col>
112+ <div className={'mt-12 pt-6'}>
113+ { sigil }
114+ </div>
115+ <H1><code>{ name }</code></H1>
116+
117+ <p>{'TODO copy'}</p>
118+
119+ <Button
120+ className={'mt-8'}
121+ prop-size={'lg wide'}
122+ disabled={this.state.isGenerating}
123+ onClick={() => console.log('TODO download wallet')}>
124+
125+ <span className="relative">
126+ {this.state.isGenerating &&
127+ <span className="btn-spinner"></span>
128+ }
129+ {'Generating wallet...'}
130+ </span>
131+ </Button>
132+
133+ </Col>
134+ </Row>
135+ )
136+ }
137+}
138+
139+export default InviteClaim
src/bridge/views/InviteLogin.jsnew file+127
@@ -0,0 +1,127 @@
1+import Maybe from 'folktale/maybe'
2+import React from 'react'
3+import { Button } from '../components/Base'
4+import { InnerLabel, ValidatedSigil, PointInput, TicketInput } from '../components/Base'
5+import { Row, Col, H1, P } from '../components/Base'
6+import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
7+import * as ob from 'urbit-ob'
8+
9+import { ROUTE_NAMES } from '../lib/router'
10+import { DEFAULT_HD_PATH, walletFromMnemonic } from '../lib/wallet'
11+
12+const placeholder = (len) => {
13+ let bytes = window.crypto.getRandomValues(new Uint8Array(len))
14+ let hex = bytes.reduce((acc, byt) =>
15+ acc + byt.toString(16).padStart(2, '0'),
16+ ''
17+ )
18+ return ob.hex2patq(hex)
19+}
20+
21+class InviteLogin extends React.Component {
22+
23+ constructor(props) {
24+ super(props)
25+
26+ this.state = {
27+ ticket: '',
28+ pointName: '',
29+ isUnlocking: false
30+ }
31+
32+ this.pointPlaceholder = placeholder(4)
33+ this.ticketPlaceholder = placeholder(12)
34+
35+ this.handleTicketInput = this.handleTicketInput.bind(this)
36+ this.handlePointNameInput = this.handlePointNameInput.bind(this)
37+ }
38+
39+ handleTicketInput(ticket) {
40+ this.setState({ ticket })
41+ }
42+
43+ handlePointNameInput(pointName) {
44+ if (pointName.length < 15) {
45+ this.setState({ pointName })
46+ }
47+ }
48+
49+ // buttonTriState = (wallet) => {
50+ // if (wallet.isNothing()) return 'blue'
51+ // if (wallet === false) return 'yellow'
52+ // if (Maybe.Nothing.hasInstance(wallet)) return 'green'
53+ // }
54+
55+ async walletFromTicket(ticket, pointName) {
56+ const { setWallet, setUrbitWallet } = this.props
57+
58+ this.setState({
59+ isUnlocking: true
60+ });
61+
62+ const urbitWallet = await kg.generateWallet({
63+ ticket: ticket,
64+ ship: ob.patp2dec('~zod') //NOTE invite wallet
65+ })
66+ const mnemonic = urbitWallet.ownership.seed
67+ const wallet = walletFromMnemonic(mnemonic, DEFAULT_HD_PATH)
68+ setWallet(wallet)
69+ setUrbitWallet(Maybe.Just(urbitWallet))
70+
71+ this.setState({
72+ isUnlocking: false
73+ });
74+ this.props.pushRoute(ROUTE_NAMES.INVITE_CLAIM)
75+ }
76+
77+ render() {
78+ const { wallet } = this.props
79+ const { ticket, pointName } = this.state
80+
81+ const phPoint = this.pointPlaceholder
82+ const phTick = this.ticketPlaceholder
83+
84+ return (
85+ <Row>
86+ <Col>
87+ <H1>{ 'Onboard' }</H1>
88+
89+ <P>
90+ { `Please enter your invite code.` }
91+ </P>
92+
93+ <TicketInput
94+ className='mono mt-8'
95+ prop-size='lg'
96+ prop-format='innerLabel'
97+ type='text'
98+ autoFocus
99+ name='invite'
100+ placeholder={ `e.g. ${phTick}` }
101+ value={ ticket }
102+ onChange={ this.handleTicketInput }>
103+ <InnerLabel>{ 'Invite code' }</InnerLabel>
104+ </TicketInput>
105+
106+ <Button
107+ className={'mt-8'}
108+ prop-size={'lg wide'}
109+ disabled={this.state.isUnlocking}
110+ // prop-color={this.buttonTriState(wallet)}
111+ onClick={() => this.walletFromTicket(ticket, pointName)}>
112+
113+ <span className="relative">
114+ {this.state.isUnlocking &&
115+ <span className="btn-spinner"></span>
116+ }
117+ {'Continue →'}
118+ </span>
119+ </Button>
120+
121+ </Col>
122+ </Row>
123+ )
124+ }
125+}
126+
127+export default InviteLogin
src/bridge/views/Landing.js+14
@@ -18,6 +18,20 @@ const Landing = (props) =>
18
18
19
19
20
20
21+ <Button
22+ prop-type='link'
23+ prop-size='lg'
24+ className={'mb-4 mt-8'}
25+ onClick={ () => props.pushRoute(ROUTE_NAMES.INVITE_LOGIN) }
26+ >
27+ { 'Claim an invite →' }
28+ </Button>
29+
30+ <P>
31+ { "If you were sent an Azimuth invite code, start here." }
32+ </P>
33+
34+
21 <Button
35 <Button
22 prop-type='link'
36 prop-type='link'
23 prop-size='lg'
37 prop-size='lg'