on master help sign in
Merge branch 'mino-transactions-emails' into mino-send-invites
66956f3Fang
src/bridge/lib/inviteMail.js+21−4
@@ -4,22 +4,39 @@
4// https://stackoverflow.com/a/53011185/1334324
4// https://stackoverflow.com/a/53011185/1334324
5const baseUrl = 'https://localhost:3002'
5const baseUrl = 'https://localhost:3002'
6
6
7−function sendMail(recipient, ticket) {
7+function sendRequest(where, what) {
8 return new Promise((resolve, reject) => {
8 return new Promise((resolve, reject) => {
9− fetch(baseUrl + '/send-ticket', {
9+ fetch(baseUrl + where, {
10 method: 'POST',
10 method: 'POST',
11 cache: 'no-cache',
11 cache: 'no-cache',
12 headers: {
12 headers: {
13 'Content-Type': 'application/json'
13 'Content-Type': 'application/json'
14 },
14 },
15− body: JSON.stringify({recipient,ticket}),
15+ body: JSON.stringify(what)
16+ })
17+ .then(response => {
18+ if (response.ok) {
19+ resolve(response.json());
20+ } else {
21+ reject(response);
22+ }
16 })
23 })
17− .then(response => resolve(response.ok))
18 .catch(reject);
24 .catch(reject);
19 });
25 });
20}
26}
21
27
28+async function hasReceived(recipient) {
29+ const res = await sendRequest('/has-received', {recipient});
30+ console.log('got fetch result', res);
31+ return res.hasReceived;
32+}
33+
34+function sendMail(recipient, ticket, tx) {
35+ return sendRequest('/send-ticket', {recipient,ticket,tx});
36+}
37+
22export {
38export {
39+ hasReceived,
23 sendMail
40 sendMail
24}
41}
25
42
src/bridge/views/InvitesSend.js+63−21
@@ -4,13 +4,26 @@ import * as ob from 'urbit-ob'
4import * as azimuth from 'azimuth-js'
4import * as azimuth from 'azimuth-js'
5
5
6import { BRIDGE_ERROR } from '../lib/error'
6import { BRIDGE_ERROR } from '../lib/error'
7−import { sendMail } from '../lib/inviteMail'
7+import { hasReceived, sendMail } from '../lib/inviteMail'
8+import { Row, Col, Warning, Button, H1, H3,
9+ Input, InnerLabel
8−import { Row, Col, Input, InnerLabel } from '../components/Base'
10+} from '../components/Base'
9import StatelessTransaction from '../components/StatelessTransaction'
11import StatelessTransaction from '../components/StatelessTransaction'
10
12
11// for wallet generation
13// for wallet generation
12import * as wg from '../../walletgen/lib/lib'
14import * as wg from '../../walletgen/lib/lib'
13
15
16+// for transaction generation and signing
17+import {
18+ signTransaction,
19+ sendSignedTransaction,
20+ waitForTransactionConfirm,
21+ isTransactionConfirmed,
22+ hexify
23+} from '../lib/txn'
24+import * as tank from '../lib/tank'
25+import Tx from 'ethereumjs-tx'
26+
14class InvitesSend extends React.Component {
27class InvitesSend extends React.Component {
15
28
16 constructor(props) {
29 constructor(props) {
@@ -22,7 +35,8 @@ class InvitesSend extends React.Component {
22 haveInvited: Nothing(),
35 haveInvited: Nothing(),
23 randomPlanet: Nothing(),
36 randomPlanet: Nothing(),
24 inviteWallet: Nothing(),
37 inviteWallet: Nothing(),
25− targetEmail: ''
38+ targetEmail: '',
39+ targetHasReceived: Nothing()
26 }
40 }
27
41
28 this.findInvited = this.findInvited.bind(this);
42 this.findInvited = this.findInvited.bind(this);
@@ -30,8 +44,8 @@ class InvitesSend extends React.Component {
30 this.generateWallet = this.generateWallet.bind(this);
44 this.generateWallet = this.generateWallet.bind(this);
31 this.canGenerate = this.canGenerate.bind(this);
45 this.canGenerate = this.canGenerate.bind(this);
32 this.handleEmailInput = this.handleEmailInput.bind(this);
46 this.handleEmailInput = this.handleEmailInput.bind(this);
33− this.txnConfirmation = this.txnConfirmation.bind(this);
34 this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
47 this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
48+ this.beforeSend = this.beforeSend.bind(this);
35 }
49 }
36
50
37 componentDidMount() {
51 componentDidMount() {
@@ -102,19 +116,33 @@ class InvitesSend extends React.Component {
102 }
116 }
103
117
104 handleEmailInput(email) {
118 handleEmailInput(email) {
105− //TODO simple .*@.*\..* email validation
119+ let newState = { targetEmail: email };
106− this.setState({ targetEmail: email });
120+ if (email.match(/.*@.*\...+/)) {
121+ hasReceived(email)
122+ .then(res => {
123+ console.log('hasReceived', res);
124+ this.setState({ targetHasReceived: Just(res) });
125+ });
126+ } else {
127+ newState.targetHasReceived = Nothing();
128+ }
129+ this.setState(newState);
107 }
130 }
108
131
109 canGenerate() {
132 canGenerate() {
110 //TODO and canSend()
133 //TODO and canSend()
111− let res = this.state.invitesAvailable.matchWith({
134+ let invites = this.state.invitesAvailable.matchWith({
112 Nothing: () => false,
135 Nothing: () => false,
113− Just: invites => ((invites.value > 0)
136+ Just: invites => (invites.value > 0)
114− && Just.hasInstance(this.state.randomPlanet)
115− && Just.hasInstance(this.state.inviteWallet))
116 });
137 });
138+ let alreadyGotInvited = this.state.targetHasReceived.matchWith({
139+ Nothing: () => false,
140+ Just: has => has.value
141+ });
117− return res;
142+ return invites
143+ && !alreadyGotInvited
144+ && Just.hasInstance(this.state.randomPlanet)
145+ && Just.hasInstance(this.state.inviteWallet);
118 }
146 }
119
147
120 createUnsignedTxn() {
148 createUnsignedTxn() {
@@ -127,15 +155,13 @@ class InvitesSend extends React.Component {
127 return Just(txn)
155 return Just(txn)
128 }
156 }
129
157
130− //TODO find a better way of making the user wait for submission success
158+ async beforeSend(stx) {
131− // and email sending, so that we can catch failure cases and can
159+ const rawTx = hexify(stx.serialize());
132− // always fall back to retrying or "please send them this"
160+ return await sendMail(
133− txnConfirmation(txHash, confirmations) {
161+ this.state.targetEmail,
134− this.props.setTxnConfirmations(txHash, confirmations);
135− if (confirmations === 1) {
136− sendMail(this.state.targetEmail, this.state.inviteWallet.value.ticket)
162+ this.state.inviteWallet.value.ticket,
137− .then(console.log);
163+ rawTx
138− }
164+ );
139 }
165 }
140
166
141 render() {
167 render() {
@@ -163,6 +189,19 @@ class InvitesSend extends React.Component {
163 })
189 })
164 });
190 });
165
191
192+ const recipientWarning = this.state.targetHasReceived.matchWith({
193+ Nothing: () => null,
194+ Just: hasReceived => {
195+ if (!hasReceived.value) return null;
196+ return (
197+ <Warning>
198+ <h3 className={'mb-2'}>{'Warning'}</h3>
199+ { 'That email address has already received an invite!' }
200+ </Warning>
201+ );
202+ }
203+ });
204+
166 return (
205 return (
167 <Row>
206 <Row>
168 <Col>
207 <Col>
@@ -181,6 +220,8 @@ class InvitesSend extends React.Component {
181 </InnerLabel>
220 </InnerLabel>
182 </Input>
221 </Input>
183
222
223+ { recipientWarning }
224+
184 <StatelessTransaction
225 <StatelessTransaction
185 // Upper scope
226 // Upper scope
186 web3={this.props.web3}
227 web3={this.props.web3}
@@ -189,8 +230,9 @@ class InvitesSend extends React.Component {
189 walletType={this.props.walletType}
230 walletType={this.props.walletType}
190 walletHdPath={this.props.walletHdPath}
231 walletHdPath={this.props.walletHdPath}
191 networkType={this.props.networkType}
232 networkType={this.props.networkType}
192− setTxnConfirmations={this.txnConfirmation}
233+ beforeSend={this.beforeSend}
193 onSent={this.props.setTxnHashCursor}
234 onSent={this.props.setTxnHashCursor}
235+ setTxnConfirmations={this.props.setTxnConfirmations}
194 popRoute={this.props.popRoute}
236 popRoute={this.props.popRoute}
195 pushRoute={this.props.pushRoute}
237 pushRoute={this.props.pushRoute}
196 // Other
238 // Other