on master help sign in
Partially implement invite sending flow
2435a3cFang
src/bridge/lib/contracts.js+4−1
@@ -3,17 +3,20 @@ const CONTRACT_ADDRESSES = {
3 DEV: {
3 DEV: {
4 ecliptic: '0x56db68f29203ff44a803faa2404a44ecbb7a7480',
4 ecliptic: '0x56db68f29203ff44a803faa2404a44ecbb7a7480',
5 azimuth: '0x863d9c2e5c4c133596cfac29d55255f0d0f86381',
5 azimuth: '0x863d9c2e5c4c133596cfac29d55255f0d0f86381',
6− polls: '0x935452c45eda2958976a429c9733c40302995efd'
6+ polls: '0x935452c45eda2958976a429c9733c40302995efd',
7+ delegatedSending: '0xb71c0b6cee1bcae56dfe95cd9d3e41ddd7eafc43'
7 },
8 },
8 ROPSTEN: {
9 ROPSTEN: {
9 ecliptic: '0x8b9f86a28921d9c705b3113a755fb979e1bd1bce',
10 ecliptic: '0x8b9f86a28921d9c705b3113a755fb979e1bd1bce',
10 azimuth: '0x308ab6a6024cf198b57e008d0ac9ad0219886579',
11 azimuth: '0x308ab6a6024cf198b57e008d0ac9ad0219886579',
11 polls: '0xf5DA85f0d285A0F88af2388DD177A221872C8971'
12 polls: '0xf5DA85f0d285A0F88af2388DD177A221872C8971'
13+ //TODO delegatedSending
12 },
14 },
13 MAINNET: {
15 MAINNET: {
14 ecliptic: '0x6ac07b7c4601b5ce11de8dfe6335b871c7c4dd4d',
16 ecliptic: '0x6ac07b7c4601b5ce11de8dfe6335b871c7c4dd4d',
15 azimuth: '0x223c067f8cf28ae173ee5cafea60ca44c335fecb',
17 azimuth: '0x223c067f8cf28ae173ee5cafea60ca44c335fecb',
16 polls: '0x7fecab617c868bb5996d99d95200d2fa708218e4'
18 polls: '0x7fecab617c868bb5996d99d95200d2fa708218e4'
19+ //TODO delegatedSending
17 },
20 },
18}
21}
19
22
src/bridge/lib/router.js+13
@@ -9,6 +9,9 @@ import InviteWallet from '../views/InviteWallet'
9import InviteVerify from '../views/InviteVerify.js'
9import InviteVerify from '../views/InviteVerify.js'
10import InviteTransactions from '../views/InviteTransactions.js'
10import InviteTransactions from '../views/InviteTransactions.js'
11
11
12+import InvitesSend from '../views/InvitesSend.js'
13+import InvitesManage from '../views/InvitesManage.js'
14+
12import AcceptTransfer from '../views/AcceptTransfer'
15import AcceptTransfer from '../views/AcceptTransfer'
13import CancelTransfer from '../views/CancelTransfer'
16import CancelTransfer from '../views/CancelTransfer'
14import CreateGalaxy from '../views/CreateGalaxy'
17import CreateGalaxy from '../views/CreateGalaxy'
@@ -46,6 +49,8 @@ const ROUTE_NAMES = {
46 INVITE_WALLET: Symbol('INVITE_WALLET'),
49 INVITE_WALLET: Symbol('INVITE_WALLET'),
47 INVITE_VERIFY: Symbol('INVITE_VERIFY'),
50 INVITE_VERIFY: Symbol('INVITE_VERIFY'),
48 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
51 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
52+ INVITES_SEND: Symbol('INVITES_SEND'),
53+ INVITES_MANAGE: Symbol('INVITES_MANAGE'),
49 //
54 //
50 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
55 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
51 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
56 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
@@ -81,6 +86,8 @@ const createRoutes = () => {
81 routes[ROUTE_NAMES.INVITE_WALLET] = InviteWallet
86 routes[ROUTE_NAMES.INVITE_WALLET] = InviteWallet
82 routes[ROUTE_NAMES.INVITE_VERIFY] = InviteVerify
87 routes[ROUTE_NAMES.INVITE_VERIFY] = InviteVerify
83 routes[ROUTE_NAMES.INVITE_TRANSACTIONS] = InviteTransactions
88 routes[ROUTE_NAMES.INVITE_TRANSACTIONS] = InviteTransactions
89+ routes[ROUTE_NAMES.INVITES_SEND] = InvitesSend
90+ routes[ROUTE_NAMES.INVITES_MANAGE] = InvitesManage
84 //
91 //
85 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
92 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
86 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
93 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
@@ -129,6 +136,12 @@ const renderRoute = (props, route) => {
129 : route === ROUTE_NAMES.INVITE_TRANSACTIONS
136 : route === ROUTE_NAMES.INVITE_TRANSACTIONS
130 ? 'Setting up new wallet'
137 ? 'Setting up new wallet'
131
138
139+ : route === ROUTE_NAMES.INVITES_SEND
140+ ? 'Send invites'
141+
142+ : route === ROUTE_NAMES.INVITES_MANAGE
143+ ? 'Manage invites'
144+
132 : route === ROUTE_NAMES.NETWORK
145 : route === ROUTE_NAMES.NETWORK
133 ? `${renderNetworkType(networkType)}`
146 ? `${renderNetworkType(networkType)}`
134
147
src/bridge/views/InvitesManage.jsnew file+205
@@ -0,0 +1,205 @@
1+import { Just, Nothing } 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 {
8+ EthereumWallet,
9+ addressFromSecp256k1Public,
10+ urbitWalletFromTicket,
11+ addHexPrefix,
12+ WALLET_NAMES
13+} from '../lib/wallet'
14+
15+import ReactSVGComponents from '../components/ReactSVGComponents'
16+import KeysAndMetadata from './Point/KeysAndMetadata'
17+import Actions from './Point/Actions'
18+import { BRIDGE_ERROR } from '../lib/error'
19+import { Row, Col, Warning, Button, H1, H3, Input,
20+ PointInput, InnerLabel, ValidatedSigil
21+} from '../components/Base'
22+import StatelessTransaction from '../components/StatelessTransaction'
23+
24+
25+class InvitesManage extends React.Component {
26+
27+ constructor(props) {
28+ super(props)
29+
30+ this.state = {
31+ invitesWillWork: Nothing(),
32+ planetInput: '',
33+ targetPlanet: Nothing(),
34+ currentPoolSize: Nothing(),
35+ cachedPoolSizes: {},
36+ targetPoolSize: 5
37+ }
38+
39+ this.checkCurrentInvites = this.checkCurrentInvites.bind(this);
40+ this.handlePointInput = this.handlePointInput.bind(this);
41+ this.updatePoolSize = this.updatePoolSize.bind(this);
42+ this.handlePoolSizeInput = this.handlePoolSizeInput.bind(this);
43+ this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
44+ this.statelessRef = React.createRef();
45+ }
46+
47+ componentDidMount() {
48+ this.point = this.props.pointCursor.matchWith({
49+ Just: (pt) => parseInt(pt.value, 10),
50+ Nothing: () => {
51+ throw BRIDGE_ERROR.MISSING_POINT
52+ }
53+ });
54+ this.contracts = this.props.contracts.matchWith({
55+ Just: cs => cs.value,
56+ Nothing: _ => {
57+ throw BRIDGE_ERROR.MISSING_CONTRACTS
58+ }
59+ });
60+
61+ azimuth.azimuth.isSpawnProxy(
62+ this.contracts,
63+ this.point,
64+ this.contracts.delegatedSending.address
65+ ).then(isSpawnProxy => {
66+ this.setState({invitesWillWork:Just(isSpawnProxy)});
67+ });
68+ }
69+
70+ componentDidUpdate(prevProps) {
71+ //
72+ }
73+
74+ handlePointInput(targetPlanet) {
75+ let newState = {
76+ targetPlanet: Nothing(),
77+ planetInput: targetPlanet,
78+ canGenerate: false,
79+ currentPoolSize: Nothing()
80+ }
81+ if ((targetPlanet.length === 14)
82+ && ob.isValidPatp(targetPlanet)) {
83+ const target = ob.patp2dec(targetPlanet);
84+ //TODO there must be a simpler way to do this check
85+ const isChild = ob.sein(targetPlanet) === ob.patp(this.point);
86+ if (isChild) {
87+ newState.targetPlanet = Just(target);
88+ newState.currentPoolSize = this.checkCurrentInvites(target);
89+ newState.canGenerate = true;
90+ }
91+ }
92+ this.setState(newState);
93+ }
94+
95+ checkCurrentInvites(who) {
96+ const cachedPoolSizes = this.state.cachedPoolSizes;
97+ if (typeof cachedPoolSizes[who] !== 'undefined') {
98+ return Just(cachedPoolSizes[who]);
99+ } else {
100+ azimuth.delegatedSending.getPool(this.contracts, who)
101+ .then(pool => {
102+ azimuth.delegatedSending.invitesInPool(this.contracts, pool)
103+ .then(size => {
104+ this.updatePoolSize(who, size);
105+ });
106+ });
107+ return Nothing();
108+ }
109+ }
110+
111+ updatePoolSize(who, size) {
112+ this.state.cachedPoolSizes[who] = size;
113+ const target = this.state.targetPlanet;
114+ const match = Just.hasInstance(target) ? (who === target.value) : false;
115+ if (match) {
116+ this.setState({currentPoolSize:Just(size)});
117+ }
118+ }
119+
120+ handlePoolSizeInput(targetSize) {
121+ this.setState({targetPoolSize: targetSize});
122+ }
123+
124+ createUnsignedTxn() {
125+ return Just(azimuth.delegatedSending.setPoolSize(
126+ this.contracts,
127+ this.state.targetPlanet.value,
128+ this.state.targetPoolSize
129+ ))
130+ }
131+
132+ render() {
133+
134+ let spawnProxyWarning = null;
135+ if (this.state.invitesWillWork === Just(false)) {
136+ spawnProxyWarning = (
137+ <Warning>
138+ <h3 className={'mb-2'}>{'Warning'}</h3>
139+ {
140+ 'Planets under this star will not be able to send invites ' +
141+ 'until the invite contract ('
142+ }<code>{this.props.contract.delegatedSending.address}</code>{
143+ ') is made spawn proxy for this star.'
144+ }
145+ </Warning>
146+ );
147+ }
148+
149+ let poolSizeText = this.state.currentPoolSize.matchWith({
150+ Just: ps => `(currently ${ps.toString()})`,
151+ Nothing: _ => ''
152+ });
153+
154+ return (
155+ <Row>
156+ <Col>
157+
158+ <p>{ 'manage invites here, for stars. can only give invites to child planets' }</p>
159+
160+ <PointInput
161+ prop-format='innerlabel'
162+ prop-size='lg'
163+ placeholder={ '~sampel-sipnem' }
164+ value={ this.state.planetInput }
165+ onChange={ this.handlePointInput }>
166+ <InnerLabel>{ 'Planet to set invites for' }</InnerLabel>
167+ <ValidatedSigil
168+ patp={ this.state.targetPlanet }
169+ validator={() => this.validatePoint} />
170+ </PointInput>
171+
172+ <Input
173+ prop-format='innerlabel'
174+ prop-size='lg'
175+ value={ this.state.targetPoolSize }
176+ onChange={ this.handlePoolSizeInput }>
177+ <InnerLabel>
178+ { `Available invites ${poolSizeText}` }
179+ </InnerLabel>
180+ </Input>
181+
182+ <StatelessTransaction
183+ // Upper scope
184+ web3={this.props.web3}
185+ contracts={this.props.contracts}
186+ wallet={this.props.wallet}
187+ walletType={this.props.walletType}
188+ walletHdPath={this.props.walletHdPath}
189+ networkType={this.props.networkType}
190+ setTxnHashCursor={this.props.setTxnHashCursor}
191+ setTxnConfirmations={this.props.setTxnConfirmations}
192+ popRoute={this.props.popRoute}
193+ pushRoute={this.props.pushRoute}
194+ // Other
195+ canGenerate={this.state.canGenerate}
196+ createUnsignedTxn={this.createUnsignedTxn}
197+ ref={this.statelessRef} />
198+
199+ </Col>
200+ </Row>
201+ )
202+ }
203+}
204+
205+export default InvitesManage
src/bridge/views/InvitesSend.jsnew file+222
@@ -0,0 +1,222 @@
1+import { Just, Nothing } 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 {
8+ EthereumWallet,
9+ addressFromSecp256k1Public,
10+ urbitWalletFromTicket,
11+ addHexPrefix,
12+ WALLET_NAMES
13+} from '../lib/wallet'
14+
15+import ReactSVGComponents from '../components/ReactSVGComponents'
16+import KeysAndMetadata from './Point/KeysAndMetadata'
17+import Actions from './Point/Actions'
18+import { BRIDGE_ERROR } from '../lib/error'
19+import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
20+import StatelessTransaction from '../components/StatelessTransaction'
21+
22+// for wallet generation
23+import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
24+import * as more from 'more-entropy'
25+import lodash from 'lodash'
26+import { PLANET_ENTROPY_BITS } from '../../walletgen/lib/constants'
27+
28+// for transaction generation and signing
29+import {
30+ signTransaction,
31+ sendSignedTransaction,
32+ waitForTransactionConfirm,
33+ isTransactionConfirmed
34+} from '../lib/txn'
35+import * as tank from '../lib/tank'
36+import Tx from 'ethereumjs-tx'
37+
38+//TODO needs more work to include email sending in the process
39+class InvitesSend extends React.Component {
40+
41+ constructor(props) {
42+ super(props)
43+
44+ this.state = {
45+ invitesAvailable: Nothing(),
46+ fromPool: Nothing(),
47+ haveInvited: Nothing(),
48+ randomPlanet: Nothing(),
49+ inviteWallet: Nothing()
50+ }
51+
52+ this.findInvited = this.findInvited.bind(this);
53+ this.findRandomPlanet = this.findRandomPlanet.bind(this);
54+ this.generateWallet = this.generateWallet.bind(this);
55+ this.canGenerate = this.canGenerate.bind(this);
56+ }
57+
58+ componentDidMount() {
59+ this.point = this.props.pointCursor.matchWith({
60+ Just: (pt) => parseInt(pt.value, 10),
61+ Nothing: () => {
62+ throw BRIDGE_ERROR.MISSING_POINT
63+ }
64+ });
65+ this.contracts = this.props.contracts.matchWith({
66+ Just: cs => cs.value,
67+ Nothing: _ => {
68+ throw BRIDGE_ERROR.MISSING_CONTRACTS
69+ }
70+ });
71+
72+ azimuth.delegatedSending.getPool(this.contracts, this.point)
73+ .then(pool => {
74+ this.setState({fromPool: Just(pool)});
75+ azimuth.delegatedSending.invitesInPool(this.contracts, pool)
76+ .then(count => {
77+ this.setState({invitesAvailable: Just(count)});
78+ });
79+ });
80+ this.findInvited();
81+ this.findRandomPlanet();
82+ this.generateWallet();
83+ }
84+
85+ componentDidUpdate(prevProps) {
86+ //
87+ }
88+
89+ async findInvited() {
90+ let invited = await azimuth.delegatedSending.getInvited(
91+ this.contracts,
92+ this.point
93+ );
94+ invited.map(async point => {
95+ const active = await azimuth.azimuth.isActive(this.contracts, point);
96+ console.log('point active', point, active);
97+ return {point: Number(point), active};
98+ });
99+ invited = await Promise.all(invited);
100+ this.setState({haveInvited: Just(invited)});
101+ }
102+
103+ async findRandomPlanet() {
104+ let res = Nothing();
105+ const kids = azimuth.azimuth.getUnspawnedChildren(
106+ this.contracts,
107+ azimuth.azimuth.getPrefix(this.point)
108+ );
109+ if (kids.length > 0) {
110+ const i = Math.floor(Math.random() * kids.length);
111+ res = Just(Number(kids[i]));
112+ }
113+ this.setState({randomPlanet: res});
114+ return res;
115+ }
116+
117+ //TODO pulled from walletgen/views/Generate, put into lib
118+ async generateWallet() {
119+ const makeTicket = () => {
120+
121+ const bits = PLANET_ENTROPY_BITS
122+
123+ const bytes = bits / 8
124+ const some = new Uint8Array(bytes)
125+ window.crypto.getRandomValues(some)
126+
127+ const gen = new more.Generator()
128+
129+ return new Promise((resolve, reject) => {
130+ gen.generate(bits, result => {
131+ const chunked = lodash.chunk(result, 2)
132+ const desired = chunked.slice(0, bytes) // only take required entropy
133+ const more = lodash.flatMap(desired, arr => arr[0] ^ arr[1])
134+ const entropy = lodash.zipWith(some, more, (x, y) => x ^ y)
135+ const buf = Buffer.from(entropy)
136+ const patq = ob.hex2patq(buf.toString('hex'))
137+ resolve(patq)
138+ reject('Entropy generation failed')
139+ })
140+ })
141+ };
142+
143+ const ticket = await makeTicket(0x10000); // planet-sized ticket
144+ const wallet = await kg.generateWallet({ticket, ship:0});
145+ console.log('found ticket', ticket, wallet, wallet.ownership.keys.address);
146+ this.setState({inviteWallet:
147+ Just({ticket, owner:wallet.ownership.keys.address})
148+ });
149+ }
150+
151+ canGenerate() {
152+ //TODO and canSend()
153+ console.log('can generate', this.state.invitesAvailable, this.state.randomPlanet,);
154+ let res = this.state.invitesAvailable.matchWith({
155+ Nothing: () => false,
156+ Just: invites => ((invites.value > 0)
157+ && Just.hasInstance(this.state.randomPlanet)
158+ && Just.hasInstance(this.state.inviteWallet))
159+ });
160+ return res;
161+ }
162+
163+ render() {
164+
165+ const inviteStatus = this.state.invitesAvailable.matchWith({
166+ Nothing: () => 'Loading...',
167+ Just: (invites) => {
168+ let res = `${invites.value} invites available`;
169+ return res + this.state.fromPool.matchWith({
170+ Nothing: () => '',
171+ Just: (pool) => (pool.value == this.point)
172+ ? ''
173+ : ` (from ${ob.patp(pool.value)})`
174+ });
175+ }
176+ });
177+
178+
179+ const inviteList = this.state.haveInvited.matchWith({
180+ Nothing: () => (<li>{'Loading...'}</li>),
181+ Just: (invited) => invited.value.map(i => {
182+ return (<li>
183+ <span class="invitee">{ob.patp(i.point)}</span>
184+ <span class="status">{i.active ? 'accepted' : 'pending'}</span>
185+ </li>);
186+ })
187+ });
188+
189+ return (
190+ <Row>
191+ <Col>
192+
193+ <p>{ 'send invites here, for planets' }</p>
194+
195+ <p>{ inviteStatus }</p>
196+
197+ <ul>{ inviteList }</ul>
198+
199+ <StatelessTransaction
200+ // Upper scope
201+ web3={this.props.web3}
202+ contracts={this.props.contracts}
203+ wallet={this.props.wallet}
204+ walletType={this.props.walletType}
205+ walletHdPath={this.props.walletHdPath}
206+ networkType={this.props.networkType}
207+ setTxnHashCursor={this.props.setTxnHashCursor}
208+ setTxnConfirmations={this.props.setTxnConfirmations}
209+ popRoute={this.props.popRoute}
210+ pushRoute={this.props.pushRoute}
211+ // Other
212+ canGenerate={this.canGenerate()}
213+ createUnsignedTxn={this.createUnsignedTxn}
214+ ref={this.statelessRef} />
215+
216+ </Col>
217+ </Row>
218+ )
219+ }
220+}
221+
222+export default InvitesSend
src/bridge/views/Point/Actions.js+54−20
@@ -9,9 +9,12 @@ import { BRIDGE_ERROR } from '../../lib/error'
9import { Row, Col, H2, P } from '../../components/Base'
9import { Row, Col, H2, P } from '../../components/Base'
10import { Button } from '../../components/Base'
10import { Button } from '../../components/Base'
11import { ROUTE_NAMES } from '../../lib/router'
11import { ROUTE_NAMES } from '../../lib/router'
12+import { azimuth } from 'azimuth-js'
12
13
13const isPlanet = point =>
14const isPlanet = point =>
14− parseInt(point, 10) > 65535
15+ (azimuth.getPointSize(point) === azimuth.PointSize.Planet);
16+const isStar = point =>
17+ (azimuth.getPointSize(point) === azimuth.PointSize.Star);
15
18
16const Actions = (props) => {
19const Actions = (props) => {
17 const { pushRoute, online, point, pointDetails, wallet } = props
20 const { pushRoute, online, point, pointDetails, wallet } = props
@@ -23,35 +26,30 @@ const Actions = (props) => {
23 }
26 }
24 })
27 })
25
28
26− const canSetSpawnProxy = pointDetails.matchWith({
29+ const isOwner = pointDetails.matchWith({
27 Nothing: _ => false,
30 Nothing: _ => false,
28− Just: details =>
29− eqAddr(details.value.owner, addr) && details.value.active
31+ Just: details => eqAddr(details.value.owner, addr)
30− })
32+ });
31−
32− const canSetManagementProxy = pointDetails.matchWith({
33+ const isActive = pointDetails.matchWith({
33 Nothing: _ => false,
34 Nothing: _ => false,
34− Just: details =>
35+ Just: details => details.value.active
35− eqAddr(details.value.owner, addr) && details.value.active
36− })
36+ });
37+ const isActiveOwner = (isOwner && isActive);
38+ const canSetSpawnProxy = isActiveOwner;
39+ const canSetManagementProxy = isActiveOwner;
37
40
38− const canConfigureKeys = pointDetails.matchWith({
41+ const canManage = (isOwner || pointDetails.matchWith({
39 Nothing: _ => false,
42 Nothing: _ => false,
40− Just: details => {
43+ Just: details => eqAddr(details.value.managementProxy, addr)
41− const canManage =
44+ }));
42− eqAddr(details.value.owner, addr)
45+ const canConfigureKeys = (canManage && isActive);
43− || eqAddr(details.value.managementProxy, addr)
44−
45− return canManage && details.value.active
46− }
47− })
48
46
49 const canIssueChild =
47 const canIssueChild =
50 pointDetails.matchWith({
48 pointDetails.matchWith({
51 Nothing: () => false,
49 Nothing: () => false,
52 Just: (details) => {
50 Just: (details) => {
53 const hasPermission =
51 const hasPermission =
54− eqAddr(details.value.owner, addr)
52+ isOwner
55 || eqAddr(details.value.spawnProxy, addr)
53 || eqAddr(details.value.spawnProxy, addr)
56
54
57 const isBooted =
55 const isBooted =
@@ -64,6 +62,7 @@ const Actions = (props) => {
64 })
62 })
65
63
66 const planet = isPlanet(point)
64 const planet = isPlanet(point)
65+ const star = isStar(point)
67
66
68 const canTransfer =
67 const canTransfer =
69 pointDetails.matchWith({
68 pointDetails.matchWith({
@@ -111,6 +110,36 @@ const Actions = (props) => {
111 }
110 }
112 })
111 })
113
112
113+ let invites = null;
114+ if (planet) {
115+ invites = (
116+ <Button
117+ disabled={(!isActiveOwner || !online)}
118+ prop-size={'sm'}
119+ prop-type={'link'}
120+ onClick={ () => {
121+ pushRoute(ROUTE_NAMES.INVITES_SEND);
122+ }}
123+ >
124+ { 'Send invites' }
125+ </Button>
126+ );
127+ }
128+ if (star) {
129+ invites = (
130+ <Button
131+ disabled={(!(isActiveOwner && canIssueChild) || !online)}
132+ prop-size={'sm'}
133+ prop-type={'link'}
134+ onClick={ () => {
135+ pushRoute(ROUTE_NAMES.INVITES_MANAGE);
136+ }}
137+ >
138+ { 'Manage invites' }
139+ </Button>
140+ );
141+ }
142+
114 return (
143 return (
115 <div>
144 <div>
116 <H2>{ 'Actions' }</H2>
145 <H2>{ 'Actions' }</H2>
@@ -214,6 +243,11 @@ const Actions = (props) => {
214 </Button>
243 </Button>
215
244
216 </Col>
245 </Col>
246+ <Col className={'flex flex-column items-start col-md-4'}>
247+
248+ { invites }
249+
250+ </Col>
217 </Row>
251 </Row>
218 </div>
252 </div>
219 )
253 )