on master help sign in
Get started on invite onboarding flow
commit086c302d17b9de08faccc6c0756842fe7955bc5c authorFang <git@fang.io> dateWed 3 Apr 2019, 01:13 (8 years ago) parent64dbee8
4 files changed+302−4
src/bridge/lib/router.js+22−4 src/bridge/views/InviteClaim.js+139 src/bridge/views/InviteLogin.js+127 src/bridge/views/Landing.js+14
src/bridge/lib/router.js+22−4
@@ -2,12 +2,16 @@ import * as ob from 'urbit-ob'
22
33import React from 'react';
44
5+import Landing from '../views/Landing'
6+
7+import InviteLogin from '../views/InviteLogin'
8+import InviteClaim from '../views/InviteClaim'
9+
510import AcceptTransfer from '../views/AcceptTransfer'
611import CancelTransfer from '../views/CancelTransfer'
712import CreateGalaxy from '../views/CreateGalaxy'
813import GenKeyfile from '../views/GenKeyfile'
914import IssueChild from '../views/IssueChild'
10−import Landing from '../views/Landing'
1115import Network from '../views/Network'
1216import Mnemonic from '../views/Mnemonic'
1317import Ledger from '../views/Ledger'
@@ -34,6 +38,11 @@ import { renderNetworkType } from './network'
3438import { BRIDGE_ERROR } from './error'
3539
3640const ROUTE_NAMES = {
41+ DEFAULT: Symbol('DEFAULT'),
42+ //
43+ INVITE_LOGIN: Symbol('INVITE_LOGIN'),
44+ INVITE_CLAIM: Symbol('INVITE_CLAIM'),
45+ //
3746 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
3847 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
3948 LANDING: Symbol('LANDING'),
@@ -57,12 +66,16 @@ const ROUTE_NAMES = {
5766 SET_KEYS: Symbol('SET_KEYS'),
5867 TRANSFER: Symbol('TRANSFER'),
5968 SENT_TRANSACTION: Symbol('SENT_TRANSACTION'),
60− GEN_KEYFILE: Symbol('GEN_KEYFILE'),
61− DEFAULT: Symbol('DEFAULT')
69+ GEN_KEYFILE: Symbol('GEN_KEYFILE')
6270}
6371
6472const createRoutes = () => {
6573 const routes = {}
74+ routes[ROUTE_NAMES.DEFAULT] = Landing
75+ //
76+ routes[ROUTE_NAMES.INVITE_LOGIN] = InviteLogin
77+ routes[ROUTE_NAMES.INVITE_CLAIM] = InviteClaim
78+ //
6679 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
6780 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
6881 routes[ROUTE_NAMES.LANDING] = Landing
@@ -86,7 +99,6 @@ const createRoutes = () => {
8699 routes[ROUTE_NAMES.SET_KEYS] = SetKeys
87100 routes[ROUTE_NAMES.TRANSFER] = Transfer
88101 routes[ROUTE_NAMES.SENT_TRANSACTION] = SentTransaction
89− routes[ROUTE_NAMES.DEFAULT] = Landing
90102 routes[ROUTE_NAMES.GEN_KEYFILE] = GenKeyfile
91103 return routes
92104}
@@ -99,6 +111,12 @@ const renderRoute = (props, route) => {
99111 route === ROUTE_NAMES.LANDING
100112 ? 'Bridge'
101113
114+ : route === ROUTE_NAMES.INVITE_LOGIN
115+ ? 'Invite code'
116+
117+ : route === ROUTE_NAMES.INVITE_CLAIM
118+ ? 'Claim invite'
119+
102120 : route === ROUTE_NAMES.NETWORK
103121 ? `${renderNetworkType(networkType)}`
104122
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) =>
1818
1919
2020
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+
2135 <Button
2236 prop-type='link'
2337 prop-size='lg'