on master help sign in
Support multiple login methods

on new Login page. Most advanced methods still stubbed.

commit11c42d9489bdc35554bb6bf69701784d6334b5aa authorFang <git@fang.io> dateWed 19 Jun 2019, 00:35 (7 years ago) parent3901e19
5 files changed+560−114
src/bridge/lib/router.js+2 src/bridge/views/Login.js+69−114 src/bridge/views/Login/Advanced.js+115 src/bridge/views/Login/Mnemonic.js+166 src/bridge/views/Login/Ticket.js+208
src/bridge/lib/router.js+2
@@ -85,6 +85,8 @@ export const getRouteBreadcrumb = ({
8585 return 'Send invites';
8686 case ROUTE_NAMES.INVITES_MANAGE:
8787 return 'Manage invites';
88+ case ROUTE_NAMES.LOGIN:
89+ return 'Login to Bridge';
8890 case ROUTE_NAMES.POINT_HOME:
8991 return 'Overview: ' + ob.patp(need.pointCursor({ pointCursor }));
9092 case ROUTE_NAMES.NETWORK:
src/bridge/views/Login.js+69−114
@@ -2,6 +2,7 @@ import { Nothing, Just } from 'folktale/maybe';
22import React from 'react';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
5+
56import {
67 Button,
78 H1,
@@ -12,19 +13,27 @@ import {
1213 Row,
1314 Col,
1415} from '../components/Base';
16+import HorizontalSelector from '../components/HorizontalSelector';
17+
18+import Ticket from './Login/Ticket';
19+import Mnemonic from './Login/Mnemonic';
20+import Advanced from './Login/Advanced';
1521
1622import * as need from '../lib/need';
1723import { compose } from '../lib/lib';
1824import { urbitWalletFromTicket } from '../lib/wallet';
1925import { ROUTE_NAMES } from '../lib/routeNames';
26+import { NETWORK_TYPES } from '../lib/network';
27+
2028import { withHistory } from '../store/history';
2129import { withNetwork } from '../store/network';
30+import { withWallet } from '../store/wallet';
31+import { withPointCursor } from '../store/pointCursor';
2232
23−//TODO should be part of InputWithStatus component
24−const INPUT_STATUS = {
25− SPIN: Symbol('SPIN'),
26− GOOD: Symbol('GOOD'),
27− FAIL: Symbol('FAIL'),
33+const TABS = {
34+ TICKET: Symbol('TICKET'),
35+ MNEMONIC: Symbol('MNEMONIC'),
36+ ADVANCED: Symbol('ADVANCED'),
2837};
2938
3039//TODO support "advances" login methods. design under discussion...
@@ -33,71 +42,45 @@ class Login extends React.Component {
3342 super(props);
3443
3544 this.state = {
36− pointName: '',
37− ticket: '',
38− ticketStatus: Nothing(),
45+ currentTab: TABS.TICKET,
3946 wallet: Nothing(),
4047 };
4148
42− this.handlePointNameInput = this.handlePointNameInput.bind(this);
43− this.handleTicketInput = this.handleTicketInput.bind(this);
44− this.verifyTicket = this.verifyTicket.bind(this);
49+ this.handleTabChange = this.handleTabChange.bind(this);
4550 this.continue = this.continue.bind(this);
4651 }
4752
4853 componentDidMount() {
49− //TODO deduce point name from URL if we can, prefill input if we found it
50−
51− // const { networkType, setNetworkType } = useNetwork();
52− console.log(this.props);
53− // setNetworkType(this.props.initialNetworkType);
54+ //TODO send help, want to use initialNetworkType
55+ this.props.setNetworkType(NETWORK_TYPES.LOCAL);
5456 }
5557
56− handlePointNameInput(pointName) {
57− if (pointName.length < 15) {
58− this.setState({ pointName });
59− this.verifyTicket(pointName, this.state.ticket);
60− }
58+ handleTabChange(currentTab) {
59+ this.setState({ currentTab });
6160 }
6261
63− handleTicketInput(ticket) {
64− this.setState({ ticket });
65− this.verifyTicket(this.state.pointName, ticket);
66− }
67−
68− //TODO maybe want to do this only on-go, because wallet derivation is slow...
69− async verifyTicket(pointName, ticket) {
70− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
71− this.setState({ ticketStatus: Nothing(), wallet: Nothing() });
72− return;
73− }
74− this.setState({ ticketStatus: Just(INPUT_STATUS.SPIN) });
62+ async continue() {
63+ const wallet = need.wallet(this.props);
7564 const contracts = need.contracts(this.props);
76− const pointNumber = ob.patp2dec(pointName);
77− const uhdw = await urbitWalletFromTicket(ticket, pointName);
78− const isOwner = azimuth.azimuth.isOwner(
79− contracts,
80− pointNumber,
81− uhdw.ownership.keys.address
82− );
83− const isTransferProxy = azimuth.azimuth.isTransferProxy(
84− contracts,
85− pointNumber,
86− uhdw.ownership.keys.address
87− );
88− let newState = { wallet: Just(uhdw) };
89− newState.ticketStatus =
90− (await isOwner) || (await isTransferProxy)
91− ? Just(INPUT_STATUS.GOOD)
92− : Just(INPUT_STATUS.FAIL);
93− this.setState(newState);
94− }
9565
96− canContinue() {
97− // this is our only requirement, since we still want people with
98− // non-standard wallet setups to be able to log in
99− return Just.hasInstance(this.state.wallet);
100− }
66+ // if no point cursor set by login logic, try to deduce it
67+ if (Nothing.hasInstance(this.props.pointCursor)) {
68+ const owned = await azimuth.azimuth.getOwnedPoints(
69+ contracts,
70+ wallet.address
71+ );
72+ if (owned.length === 1) {
73+ this.props.setPointCursor(Just(owned[0]));
74+ } else if (owned.length === 0) {
75+ const canOwn = await azimuth.azimuth.getTransferringFor(
76+ contracts,
77+ wallet.address
78+ );
79+ if (canOwn.length === 1) {
80+ this.props.setPointCursor(Just(canOwn[0]));
81+ }
82+ }
83+ }
10184
10285 if (Just.hasInstance(this.props.pointCursor)) {
10386 this.props.history.popAndPush(ROUTE_NAMES.POINT_HOME);
@@ -107,70 +90,40 @@ class Login extends React.Component {
10790 }
10891
10992 render() {
110− const pointInput = (
111− <PointInput
112− className="mono mt-8"
113− prop-size="lg"
114− prop-format="innerLabel"
115− type="text"
116− autoFocus
117− value={this.state.pointName}
118− onChange={this.handlePointNameInput}>
119− <InnerLabel>{'Point'}</InnerLabel>
120− <ValidatedSigil
121− className={'tr-0 mt-05 mr-0 abs'}
122− patp={this.state.pointName}
123− size={68}
124− margin={8}
125− />
126− </PointInput>
93+ const tabOptions = [
94+ { title: 'Ticket', value: TABS.TICKET },
95+ { title: 'Mnemonic', value: TABS.MNEMONIC },
96+ { title: 'Advanced', value: TABS.ADVANCED },
97+ ];
98+
99+ //TODO probably needs a dedicated component because styling
100+ const tabs = (
101+ <HorizontalSelector
102+ options={tabOptions}
103+ onChange={this.handleTabChange}
104+ />
127105 );
128106
129− const ticketStatus = this.state.ticketStatus.matchWith({
130− Nothing: () => <span />,
131− Just: status => {
132− switch (status.value) {
133− case INPUT_STATUS.SPIN:
134− return <span>⋯</span>;
135− case INPUT_STATUS.GOOD:
136− return <span>✓</span>;
137− case INPUT_STATUS.FAIL:
138− return <span>𐄂</span>;
139− default:
140− throw new Error('weird input status ' + status.value);
141− }
142− },
143− });
144−
145− const ticketInput = (
146− //TODO NewTicketInput component, that does dashes etc
147− <TicketInput
148− className="mono mt-8"
149− prop-size="lg"
150− prop-format="innerLabel"
151− type="password"
152− name="ticket"
153− value={this.state.ticket}
154− onChange={this.handleTicketInput}>
155− <InnerLabel>{'Code'}</InnerLabel>
156− {ticketStatus}
157− </TicketInput>
158− );
107+ const login = (() => {
108+ switch (this.state.currentTab) {
109+ case TABS.TICKET:
110+ return <Ticket loginCompleted={this.continue} />;
111+ case TABS.MNEMONIC:
112+ return <Mnemonic loginCompleted={this.continue} />;
113+ case TABS.ADVANCED:
114+ return <Advanced loginCompleted={this.continue} />;
115+ default:
116+ throw new Error('weird tab ' + this.status.currentTab);
117+ }
118+ })();
159119
160120 return (
161121 <Row>
162122 <Col>
163123 <H1>{'Login'}</H1>
164124
165− {pointInput}
166− {ticketInput}
167−
168− <Button
169− className={'mt-10'}
170− disabled={!this.canContinue()}
171− onClick={this.continue}>
172− {'Go →'}
173− </Button>
125+ {tabs}
126+ {login}
174127 </Col>
175128 </Row>
176129 );
@@ -179,5 +132,7 @@ class Login extends React.Component {
179132
180133export default compose(
181134 withHistory,
182− withNetwork
135+ withNetwork,
136+ withWallet,
137+ withPointCursor
183138)(Login);
src/bridge/views/Login/Advanced.jsnew file+115
@@ -0,0 +1,115 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import React from 'react';
3+import { Button, H1, P } from '../../components/Base';
4+import { InnerLabelDropdown } from '../../components/Base';
5+import { Row, Col } from '../../components/Base';
6+
7+import Ticket from './Ticket';
8+import Mnemonic from './Mnemonic';
9+//TODO more
10+
11+import { ROUTE_NAMES } from '../../lib/routeNames';
12+import { BRIDGE_ERROR } from '../../lib/error';
13+import { withHistory } from '../../store/history';
14+import { WALLET_TYPES, renderWalletType } from '../../lib/wallet';
15+import { compose } from '../../lib/lib';
16+import { withWallet } from '../../store/wallet';
17+
18+class Advanced extends React.Component {
19+ constructor(props) {
20+ super(props);
21+
22+ this.state = {
23+ selected: WALLET_TYPES.TICKET,
24+ };
25+
26+ this.handleSelection = this.handleSelection.bind(this);
27+ }
28+
29+ getWalletOptions() {
30+ return [
31+ {
32+ title: 'Urbit Master Ticket',
33+ value: WALLET_TYPES.TICKET,
34+ },
35+ {
36+ title: 'Urbit Master Ticket (Shards)',
37+ value: WALLET_TYPES.SHARDS,
38+ },
39+ {
40+ type: 'divider',
41+ },
42+ {
43+ title: 'Ledger',
44+ value: WALLET_TYPES.LEDGER,
45+ },
46+ {
47+ title: 'Trezor',
48+ value: WALLET_TYPES.TREZOR,
49+ },
50+ {
51+ type: 'divider',
52+ },
53+ {
54+ title: 'BIP39 Mnemonic',
55+ value: WALLET_TYPES.MNEMONIC,
56+ },
57+ {
58+ title: 'Ethereum Private Key',
59+ value: WALLET_TYPES.PRIVATE_KEY,
60+ },
61+ {
62+ title: 'Ethereum Keystore File',
63+ value: WALLET_TYPES.KEYSTORE,
64+ },
65+ ];
66+ }
67+
68+ handleSelection(selected) {
69+ this.setState({ selected });
70+ }
71+
72+ render() {
73+ const inputs = (() => {
74+ switch (this.state.selected) {
75+ case WALLET_TYPES.TICKET:
76+ return (
77+ <Ticket
78+ advanced={true}
79+ loginCompleted={this.props.loginCompleted}
80+ />
81+ );
82+ case WALLET_TYPES.MNEMONIC:
83+ return (
84+ <Mnemonic
85+ advanced={true}
86+ loginCompleted={this.props.loginCompleted}
87+ />
88+ );
89+ default:
90+ return <P>{'Coming soon'}</P>;
91+ }
92+ })();
93+
94+ return (
95+ <Row>
96+ <Col>
97+ <InnerLabelDropdown
98+ className={'mb-10 mt-6'}
99+ title={'Wallet Type:'}
100+ options={this.getWalletOptions()}
101+ handleUpdate={this.handleSelection}
102+ currentSelectionTitle={renderWalletType(this.state.selected)}
103+ />
104+
105+ {inputs}
106+ </Col>
107+ </Row>
108+ );
109+ }
110+}
111+
112+export default compose(
113+ withHistory,
114+ withWallet
115+)(Advanced);
src/bridge/views/Login/Mnemonic.jsnew file+166
@@ -0,0 +1,166 @@
1+import * as bip39 from 'bip39';
2+import { Just, Nothing } from 'folktale/maybe';
3+import React from 'react';
4+import { Button } from '../../components/Base';
5+import {
6+ Input,
7+ MnemonicInput,
8+ InnerLabel,
9+ InputCaption,
10+} from '../../components/Base';
11+import { Row, Col, H1 } from '../../components/Base';
12+
13+import { ROUTE_NAMES } from '../../lib/routeNames';
14+import { withHistory } from '../../store/history';
15+import { DEFAULT_HD_PATH, walletFromMnemonic } from '../../lib/wallet';
16+import { compose } from '../../lib/lib';
17+import { withWallet } from '../../store/wallet';
18+
19+class Mnemonic extends React.Component {
20+ constructor(props) {
21+ super(props);
22+
23+ this.state = {
24+ mnemonic: '',
25+ passphrase: '',
26+ hdpath: DEFAULT_HD_PATH,
27+ };
28+
29+ this.handleMnemonicInput = this.handleMnemonicInput.bind(this);
30+ this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
31+ this.handleHdPathInput = this.handleHdPathInput.bind(this);
32+ }
33+
34+ componentDidMount() {
35+ const { mnemonic, hdpath, passphrase } = this.state;
36+ this.attemptWalletDerivation(mnemonic, hdpath, passphrase);
37+ }
38+
39+ handlePassphraseInput(passphrase) {
40+ this.setState((state, _) => {
41+ const mnemonic = state.mnemonic;
42+ const hdpath = state.hdpath;
43+ this.attemptWalletDerivation(
44+ mnemonic,
45+ hdpath === '' ? DEFAULT_HD_PATH : hdpath,
46+ passphrase
47+ );
48+ return {
49+ passphrase,
50+ };
51+ });
52+ }
53+
54+ handleMnemonicInput(mnemonic) {
55+ this.setState((state, _) => {
56+ const hdpath = state.hdpath;
57+ const passphrase = state.passphrase;
58+ this.attemptWalletDerivation(
59+ mnemonic,
60+ hdpath === '' ? DEFAULT_HD_PATH : hdpath,
61+ passphrase
62+ );
63+ return {
64+ mnemonic,
65+ };
66+ });
67+ }
68+
69+ handleHdPathInput(hdpath) {
70+ this.setState((state, _) => {
71+ const mnemonic = state.mnemonic;
72+ const passphrase = state.passphrase;
73+ this.attemptWalletDerivation(
74+ mnemonic,
75+ hdpath === '' ? DEFAULT_HD_PATH : hdpath,
76+ passphrase
77+ );
78+ return {
79+ hdpath,
80+ };
81+ });
82+ }
83+
84+ attemptWalletDerivation(mnemonic, hdpath, passphrase) {
85+ const { setWallet, setAuthMnemonic, setWalletHdPath } = this.props;
86+ const wallet = walletFromMnemonic(mnemonic, hdpath, passphrase);
87+ setWallet(wallet);
88+ setAuthMnemonic(Just(mnemonic));
89+ setWalletHdPath(hdpath);
90+ }
91+
92+ render() {
93+ const { history, wallet } = this.props;
94+ const { mnemonic, hdpath, passphrase } = this.state;
95+
96+ return (
97+ <Row>
98+ <Col>
99+ <InputCaption>
100+ {'Please enter your BIP39 mnemonic here.'}
101+ </InputCaption>
102+
103+ <MnemonicInput
104+ className="pt-8"
105+ prop-size="md"
106+ prop-format="innerLabel"
107+ type="text"
108+ name="mnemonic"
109+ onChange={this.handleMnemonicInput}
110+ value={mnemonic}
111+ autocomplete="off"
112+ autoFocus>
113+ <InnerLabel>{'Mnemonic'}</InnerLabel>
114+ </MnemonicInput>
115+
116+ {!this.props.advanced ? null : (
117+ <>
118+ <InputCaption>
119+ {`If your wallet requires a passphrase, you may enter it below.`}
120+ </InputCaption>
121+
122+ <Input
123+ className="pt-8"
124+ prop-size="md"
125+ prop-format="innerLabel"
126+ name="passphrase"
127+ type="password"
128+ value={passphrase}
129+ autocomplete="off"
130+ onChange={this.handlePassphraseInput}>
131+ <InnerLabel>{'Passphrase'}</InnerLabel>
132+ </Input>
133+
134+ <InputCaption>
135+ {`If you'd like to use a custom derivation path, you may enter it below.`}
136+ </InputCaption>
137+
138+ <Input
139+ className="pt-8 text-mono"
140+ prop-size="md"
141+ prop-format="innerLabel"
142+ name="hdpath"
143+ value={hdpath}
144+ autocomplete="off"
145+ onChange={this.handleHdPathInput}>
146+ <InnerLabel>{'HD Path'}</InnerLabel>
147+ </Input>
148+ </>
149+ )}
150+
151+ <Button
152+ className={'mt-10'}
153+ disabled={Nothing.hasInstance(wallet)}
154+ onClick={this.props.loginCompleted}>
155+ {'Go →'}
156+ </Button>
157+ </Col>
158+ </Row>
159+ );
160+ }
161+}
162+
163+export default compose(
164+ withHistory,
165+ withWallet
166+)(Mnemonic);
src/bridge/views/Login/Ticket.jsnew file+208
@@ -0,0 +1,208 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import React from 'react';
3+import * as azimuth from 'azimuth-js';
4+import * as ob from 'urbit-ob';
5+
6+import {
7+ Button,
8+ Row,
9+ Col,
10+ H1,
11+ P,
12+ InnerLabel,
13+ ValidatedSigil,
14+ PointInput,
15+ TicketInput,
16+ Input,
17+ InputCaption,
18+} from '../../components/Base';
19+
20+import * as need from '../../lib/need';
21+import { randomPatq, compose } from '../../lib/lib';
22+import { ROUTE_NAMES } from '../../lib/routeNames';
23+import { WALLET_TYPES, urbitWalletFromTicket } from '../../lib/wallet';
24+
25+import { withHistory } from '../../store/history';
26+import { withWallet } from '../../store/wallet';
27+import { withNetwork } from '../../store/network';
28+import { withPointCursor } from '../../store/pointCursor';
29+
30+//TODO should be part of InputWithStatus component
31+const INPUT_STATUS = {
32+ SPIN: Symbol('SPIN'),
33+ GOOD: Symbol('GOOD'),
34+ FAIL: Symbol('FAIL'),
35+};
36+
37+class Ticket extends React.Component {
38+ constructor(props) {
39+ super(props);
40+
41+ this.state = {
42+ pointName: '',
43+ ticket: '',
44+ ticketStatus: Nothing(),
45+ passphrase: '',
46+ wallet: Nothing(),
47+ };
48+
49+ this.handlePointNameInput = this.handlePointNameInput.bind(this);
50+ this.handleTicketInput = this.handleTicketInput.bind(this);
51+ this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
52+ this.verifyTicket = this.verifyTicket.bind(this);
53+ this.continue = this.continue.bind(this);
54+ }
55+
56+ componentDidMount() {
57+ //TODO deduce point name from URL if we can, prefill input if we found it
58+ }
59+
60+ handlePointNameInput(pointName) {
61+ if (pointName.length < 15) {
62+ this.setState({ pointName });
63+ this.verifyTicket(pointName, this.state.ticket, this.state.passphrase);
64+ }
65+ }
66+
67+ handleTicketInput(ticket) {
68+ this.setState({ ticket });
69+ this.verifyTicket(this.state.pointName, ticket, this.state.passphrase);
70+ }
71+
72+ handlePassphraseInput(passphrase) {
73+ this.setState({ passphrase });
74+ this.verifyTicket(this.state.pointName, this.state.ticket, passphrase);
75+ }
76+
77+ //TODO maybe want to do this only on-go, because wallet derivation is slow...
78+ async verifyTicket(pointName, ticket, passphrase) {
79+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
80+ this.setState({ ticketStatus: Nothing(), wallet: Nothing() });
81+ return;
82+ }
83+ this.setState({ ticketStatus: Just(INPUT_STATUS.SPIN) });
84+ const contracts = need.contracts(this.props);
85+ const pointNumber = ob.patp2dec(pointName);
86+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
87+ const isOwner = azimuth.azimuth.isOwner(
88+ contracts,
89+ pointNumber,
90+ uhdw.ownership.keys.address
91+ );
92+ const isTransferProxy = azimuth.azimuth.isTransferProxy(
93+ contracts,
94+ pointNumber,
95+ uhdw.ownership.keys.address
96+ );
97+ let newState = { wallet: Just(uhdw) };
98+ newState.ticketStatus =
99+ (await isOwner) || (await isTransferProxy)
100+ ? Just(INPUT_STATUS.GOOD)
101+ : Just(INPUT_STATUS.FAIL);
102+ this.setState(newState);
103+ }
104+
105+ canContinue() {
106+ // this is our only requirement, since we still want people with
107+ // non-standard wallet setups to be able to log in
108+ return Just.hasInstance(this.state.wallet);
109+ }
110+
111+ continue() {
112+ this.props.setWalletType(WALLET_TYPES.TICKET);
113+ this.props.setUrbitWallet(this.state.wallet.value); //TODO wrap
114+ this.props.setPointCursor(Just(ob.patp2dec(this.state.pointName)));
115+ this.props.loginCompleted();
116+ }
117+
118+ render() {
119+ const { history, wallet } = this.props;
120+
121+ const pointInput = (
122+ <PointInput
123+ className="mono mt-8"
124+ prop-size="lg"
125+ prop-format="innerLabel"
126+ type="text"
127+ autoFocus
128+ value={this.state.pointName}
129+ onChange={this.handlePointNameInput}>
130+ <InnerLabel>{'Point name'}</InnerLabel>
131+ <ValidatedSigil
132+ className={'tr-0 mt-05 mr-0 abs'}
133+ patp={this.state.pointName}
134+ size={68}
135+ margin={8}
136+ />
137+ </PointInput>
138+ );
139+
140+ const ticketStatus = this.state.ticketStatus.matchWith({
141+ Nothing: () => <span />,
142+ Just: status => {
143+ switch (status.value) {
144+ case INPUT_STATUS.SPIN:
145+ return <span>⋯</span>;
146+ case INPUT_STATUS.GOOD:
147+ return <span>✓</span>;
148+ case INPUT_STATUS.FAIL:
149+ return <span>𐄂</span>;
150+ default:
151+ throw new Error('weird input status ' + status.value);
152+ }
153+ },
154+ });
155+
156+ const ticketInput = (
157+ //TODO NewTicketInput component, that does dashes etc
158+ <TicketInput
159+ className="mono mt-8"
160+ prop-size="lg"
161+ prop-format="innerLabel"
162+ type="password"
163+ name="ticket"
164+ value={this.state.ticket}
165+ onChange={this.handleTicketInput}>
166+ <InnerLabel>{'Master ticket'}</InnerLabel>
167+ {ticketStatus}
168+ </TicketInput>
169+ );
170+
171+ // const passphraseInput = !this.props.advanced ? null : (
172+ // <Input
173+ // className="mt-8"
174+ // prop-size="md"
175+ // prop-format="innerLabel"
176+ // name="passphrase"
177+ // type="password"
178+ // value={this.state.passphrase}
179+ // autocomplete="off"
180+ // onChange={this.handlePassphraseInput}>
181+ // <InnerLabel>{'Passphrase'}</InnerLabel>
182+ // </Input>
183+ // );
184+
185+ return (
186+ <Row>
187+ <Col>
188+ {pointInput}
189+ {ticketInput}
190+
191+ <Button
192+ className={'mt-10'}
193+ disabled={!this.canContinue()}
194+ onClick={this.continue}>
195+ {'Go →'}
196+ </Button>
197+ </Col>
198+ </Row>
199+ );
200+ }
201+}
202+
203+export default compose(
204+ withHistory,
205+ withWallet,
206+ withNetwork,
207+ withPointCursor
208+)(Ticket);