This repository has been archived by the owner on May 10, 2021. It is now read-only.
/
Connect.js
669 lines (620 loc) · 30.6 KB
/
Connect.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
import { Credentials, ContractFactory } from 'uport-credentials'
import { verifyJWT, decodeJWT } from 'did-jwt'
import MobileDetect from 'mobile-detect'
import { isMNID, encode, decode } from 'mnid'
import { transport, message, network } from 'uport-transports'
import PubSub from 'pubsub-js'
import store from 'store'
import UportLite from 'uport-lite'
import { isMobile, ipfsAdd } from './util'
import UportSubprovider from './UportSubprovider'
class Connect {
/**
* Instantiates a new uPort Connect object.
*
* @example
* import Connect from 'uport-connect'
* const connect = new Connect('MydappName')
*
* @param {String} appName The name of your app
* @param {Object} [opts] optional parameters
* @param {String} [opts.description] A short description of your app that can be displayed to users when making requests
* @param {String} [opts.profileImage] A URL for an image to be displayed as the avatar for this app in requests
* @param {String} [opts.bannerImage] A URL for an image to be displayed as the banner for this app in requests
* @param {String} [opts.brandColor] A HEX brand color to be be displayed where branding is required '#000000'
* @param {Object} [opts.network='rinkeby'] network config object or string name, ie. { id: '0x1', rpcUrl: 'https://mainnet.infura.io' } or 'kovan', 'mainnet', 'ropsten', 'rinkeby'.
* @param {String} [opts.accountType] Ethereum account type: "general", "segregated", "keypair", or "none"
* @param {Boolean} [opts.isMobile] Configured by default by detecting client, but can optionally pass boolean to indicate whether this is instantiated on a mobile client
* @param {Boolean} [opts.useStore=true] When true, object state will be written to local storage on each state change
* @param {Object} [opts.store] Storage inteferface with synchronous get() => statObj and set(stateObj) functions, by default store is local storage. For asynchronous storage, set useStore false and handle manually.
* @param {Boolean} [opts.usePush=true] Use the pushTransport when a pushToken is available. Set to false to force connect to use standard transport
* @param {String[]} [opts.vc] An array of verified claims describing this identity
* @param {Function} [opts.transport] Optional custom transport for desktop, non-push requests
* @param {Function} [opts.mobileTransport] Optional custom transport for mobile requests
* @param {Function} [opts.mobileUriHandler] Optional uri handler for mobile requests, if using default transports
* @param {Object} [opts.muportConfig] Configuration object for muport did resolver. See [muport-did-resolver](https://github.com/uport-project/muport-did-resolver)
* @param {Object} [opts.ethrConfig] Configuration object for ethr did resolver. See [ethr-did-resolver](https://github.com/uport-project/ethr-did-resolver)
* @param {Object} [opts.registry] Configuration for uPort DID Resolver (DEPRECATED) See [uport-did-resolver](https://github.com/uport-project/uport-did-resolver)
* @return {Connect} self
*/
constructor (appName, opts = {}) {
// Config
this.appName = appName || 'uport-connect-app'
this.description = opts.description
this.profileImage = opts.profileImage
this.bannerImage = opts.bannerImage
this.brandColor = opts.brandColor
this.network = network.config.network(opts.network)
this.accountType = opts.accountType === 'none' ? undefined : opts.accountType
this.isOnMobile = opts.isMobile === undefined ? isMobile() : opts.isMobile
this.useStore = opts.useStore === undefined ? true : opts.useStore
this.usePush = opts.usePush === undefined ? true : opts.usePush
this.vc = Array.isArray(opts.vc) ? opts.vc : []
// Disallow segregated account on mainnet
if (this.network === network.defaults.networks.mainnet && this.accountType === 'segregated') {
throw new Error('Segregated accounts are not supported on mainnet')
}
// Storage
this.store = opts.store || new LocalStorageStore()
// Initialize private state
this._state = {}
// Load any existing state if any
if (this.useStore) this.loadState()
if (!this.keypair.did) this.keypair = Credentials.createIdentity()
// Transports
this.PubSub = PubSub
this.transport = opts.transport || connectTransport(appName)
this.useDeeplinks = true
this.mobileTransport = opts.mobileTransport || transport.url.send({
uriHandler: opts.mobileUriHandler,
messageToURI: (m) => this.useDeeplinks ? message.util.messageToDeeplinkURI(m) : message.util.messageToUniversalURI(m)
})
this.onloadResponse = opts.onloadResponse || transport.url.getResponse()
this.pushTransport = (this.pushToken && this.publicEncKey) ? pushTransport(this.pushToken, this.publicEncKey) : undefined
transport.url.listenResponse((err, res) => {
if (err) throw err
// Switch to deep links after first universal link success
this.useDeeplinks = true
this.pubResponse(res)
})
// Credential (uport-js) config for verification
this.registry = opts.registry || UportLite({ networks: network.config.networkToNetworkSet(this.network) })
this.resolverConfigs = {registry: this.registry, ethrConfig: opts.ethrConfig, muportConfig: opts.muportConfig }
this.credentials = new Credentials(Object.assign(this.keypair, this.resolverConfigs)) // TODO can resolver configs not be passed through
}
/**
* Instantiates and returns a web3 styple provider wrapped with uPort functionality.
* For more details see uportSubprovider. uPort overrides eth_coinbase and eth_accounts
* to start a get address flow or to return an already received address. It also
* overrides eth_sendTransaction to start the send transaction flow to pass the
* transaction to the uPort app.
*
* @example
* const uportProvider = connect.getProvider()
* const web3 = new Web3(uportProvider)
*
* @param {Object} [provider] An optional web3 style provider to wrap, default is a http provider, non standard provider may cause unexpected behavior, using default is suggested.
* @return {UportSubprovider} A web3 style provider wrapped with uPort functionality
*/
getProvider (provider) {
const subProvider = new UportSubprovider({
requestAddress: () => {
const requestID = 'addressReqProvider'
this.requestDisclosure({accountType: this.accountType || 'keypair'}, requestID)
return this.onResponse(requestID).then(res => res.payload.address)
},
sendTransaction: (txObj) => {
delete txObj['from']
const requestID = 'txReqProvider'
this.sendTransaction(txObj, requestID)
return this.onResponse(requestID).then(res => res.payload)
},
signTypedData: (typedData) => {
const requestID = 'typedDataSigReqProvider'
this.requestTypedDataSignature(typedData, requestID)
return this.onResponse(requestID).then(res => res.payload)
},
personalSign: (data) => {
const requestID = 'personalSignReqProvider'
this.requestPersonalSign(data, requestID)
return this.onResponse(requestID).then(res => res.payload)
},
provider, network: this.network
})
if (this.address) subProvider.setAccount(this.address)
return subProvider
}
/**
* Get response by id of earlier request, returns promise which resolves when first
* reponse with given id is available. If looking for multiple responses of same id,
* listen instead by passing a callback.
*
* @param {String} id id of request you are waiting for a response for
* @param {Function} cb an optional callback function, which is called each time a valid repsonse for a given id is available vs having a single promise returned
* @return {Promise<Object, Error>} promise resolves once valid response for given id is avaiable, otherwise rejects with error, no promised returned if callback given
*/
onResponse(id, cb) {
const parseResponse = (res) => {
if (res.error) return Promise.reject(Object.assign({id}, res))
if (message.util.isJWT(res.payload)) {
const jwt = res.payload
const decoded = decodeJWT(jwt)
if (decoded.payload.claim){
return Promise.resolve(Object.assign({id}, res))
}
return this.verifyResponse(jwt).then(parsedRes => {
// Set identifiers present in the response
// TODO maybe just change name in uport-js
if (parsedRes.boxPub) parsedRes.publicEncKey = parsedRes.boxPub
this.setState(parsedRes)
return {id, payload: parsedRes, data: res.data}
})
} else {
return Promise.resolve(Object.assign({id}, res))
}
}
if (this.onloadResponse && this.onloadResponse.id === id) {
const onloadResponse = this.onloadResponse
this.onloadResponse = null
return parseResponse(onloadResponse)
}
if (cb) {
this.PubSub.subscribe(id, (msg, res) => {
parseResponse(res).then(
(res) => { cb(null, res) },
(err) => { cb(err, null) }
)
})
} else {
return new Promise((resolve, reject) => {
this.PubSub.subscribe(id, (msg, res) => {
this.PubSub.unsubscribe(id)
parseResponse(res).then(resolve, reject)
})
})
}
}
/**
* Push a response payload to uPort connect to be handled. Useful if implementing your own transports
* and you are getting responses with your own functions, listeners, event handlers etc. It will
* parse the response and resolve it to any listening onResponse functions with the matching id. A
* response object in connect is of the form {id, payload, data}, where payload and id required. Payload is the
* response payload (url or JWT) from a uPort client.
*
* @param {Object} response a wrapped response payload, of form {id, res, data}, res and id required
*/
pubResponse (response) {
if (!response || !response.id) throw new Error('Response payload requires an id')
this.PubSub.publish(response.id, {payload: response.payload, data: response.data})
}
/**
* @private
* Verify a jwt and save the resulting doc to this instance, then process the
* disclosure payload with this.credentials
* @param {JWT} token the JWT to be verified
*/
verifyResponse (token) {
return verifyJWT(token, {audience: this.credentials.did}).then(res => {
this.doc = res.doc
return this.credentials.processDisclosurePayload(res)
})
}
/**
* Send a request message to a uPort client. Useful function if you want to pass additional transport options and/or send a request you already created elsewhere.
*
* @param {String} request a request message to send to a uPort client
* @param {String} id id of the request, which you will later use to handle the response
* @param {Object} [opts] optional parameters for a callback, see (specs for more details)[https://github.com/uport-project/specs/blob/develop/messages/index.md]
* @param {String} opts.redirectUrl If on mobile client, the url you want the uPort client to return control to once it completes it's flow. Depending on the params below, this redirect can include the response or it may be returned to the callback in the request token.
* @param {String} opts.data A string of any data you want later returned with the response. It may be contextual to the original request. (i.e. a request id from your server)
* @param {String} opts.type Type specifies the callback action. 'post' to send response to callback in request token or 'redirect' to send response in redirect url.
* @param {Function} opts.cancel When using the default QR send, but handling the response yourself, this function will be called when a user closes the request modal.
*/
send (request, id, {redirectUrl, data, type, cancel} = {}) {
if (!id) throw new Error('Requires request id')
if (this.isOnMobile) {
if (!redirectUrl & !type) type = 'redirect'
this.mobileTransport(request, {id, data, redirectUrl, type})
} else if (this.usePush && this.pushTransport) {
this.pushTransport(request, {data}).then(res => this.PubSub.publish(id, res))
} else {
this.transport(request, {data, cancel}).then(res => this.PubSub.publish(id, res))
}
}
/**
* Builds and returns a contract object which can be used to interact with
* a given contract. Similar to web3.eth.contract. Once specifying .at(address)
* you can call the contract functions with this object. It will create a transaction
* sign request and send it. Functionality limited to function calls which require sending
* a transaction, as these are the only calls which require interaction with a uPort client.
* For reading and/or events use web3 alongside or a similar library.
*
* @example
* const abi = [{"constant":false,"inputs":[{"name":"status","type":"string"}],"name":"updateStatus","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"addr","type":"address"}],"name":"getStatus","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"}]
* const StatusContract = connect.contract(abi).at('0x70A804cCE17149deB6030039798701a38667ca3B')
* const reqId = 'updateStatus'
* StatusContract.updateStatus('helloStatus', reqId)
* connect.onResponse('reqId').then(res => {
* const txHash = res.payload
* })
*
* @param {Object} abi contract ABI
* @return {Object} contract object
*/
contract (abi) {
const txObjHandler = (txObj, id, sendOpts) => {
txObj.fn = txObj.function
delete txObj['function']
return this.sendTransaction(txObj, id, sendOpts)
}
return ContractFactory(txObjHandler.bind(this))(abi)
}
/**
* Given a transaction object (similarly defined as the web3 transaction object)
* it creates a transaction request and sends it. A transaction hash is later
* returned as the response if the user selected to sign it.
*
* @example
* const txobject = {
* to: '0xc3245e75d3ecd1e81a9bfb6558b6dafe71e9f347',
* value: '0.1',
* fn: "setStatus(string 'hello', bytes32 '0xc3245e75d3ecd1e81a9bfb6558b6dafe71e9f347')",
* appName: 'MyDapp'
* }
* connect.sendTransaction(txobject, 'setStatus')
* connect.onResponse('setStatus').then(res => {
* const txHash = res.payload
* })
*
* @param {Object} txObj
* @param {String} [id='txReq'] string to identify request, later used to get response, name of function call is used by default, if not a function call, the default is 'txReq'
* @param {Object} [sendOpts] reference send function options
*/
async sendTransaction (txObj = {}, id, sendOpts) {
if (!txObj.vc) await this.signAndUploadProfile()
txObj = {
vc: this.vc, ...txObj,
to: isMNID(txObj.to) ? txObj.to : encode({network: this.network.id, address: txObj.to}),
rpcUrl: this.network.rpcUrl,
}
// Create default id, where id is function name, or txReq if no function name
if (!id) id = txObj.fn ? txObj.fn.split('(')[0] : 'txReq'
this.credentials.createTxRequest(txObj, {callbackUrl: this.genCallback(id)})
.then(jwt => this.send(jwt, id, sendOpts))
}
/**
* Creates and sends a request for a user to [sign a verification](https://github.com/uport-project/specs/blob/develop/messages/verificationreq.md) and sends the request to the uPort user.
*
* @example
* const unsignedClaim = {
* claim: {
* "Citizen of city X": {
* "Allowed to vote": true,
* "Document": "QmZZBBKPS2NWc6PMZbUk9zUHCo1SHKzQPPX4ndfwaYzmPW"
* }
* },
* sub: "did:ethr:0x413daa771a2fc9c5ae5a66abd144881ef2498c54"
* }
* connect.requestVerificationSignature(unsignedClaim).then(jwt => {
* ...
* })
*
* @param {Object} unsignedClaim unsigned claim object which you want the user to attest
* @param {String} sub the DID which the unsigned claim is about
* @param {String} [id='signVerReq'] string to identify request, later used to get response
* @param {Object} [sendOpts] reference send function options
*/
async requestVerificationSignature (unsignedClaim, opts, id = 'verSigReq', sendOpts) {
await this.signAndUploadProfile()
if (typeof opts === 'string') {
console.warn('The subject argument is deprecated, use option object with {sub: sub, ...}')
opts = {sub: opts}
} else if (!opts || !opts.sub) {
throw new Error(`Missing required field sub in opts. Received: ${opts}`)
}
this.credentials.createVerificationSignatureRequest(unsignedClaim, {...opts, aud: this.did, callbackUrl: this.genCallback(id), vc: this.vc})
.then(jwt => this.send(jwt, id, sendOpts))
}
/**
* Creates and sends a request to a user to sign a piece of ERC712 Typed Data
*
* @param {Object} typedData an object containing unsigned typed, structured data that follows the ERC712 specification
* @param {String} [id='typedDataSigReq'] string to identify request, later used to get response
* @param {Object} [sendOpts] reference send function options
*/
requestTypedDataSignature (typedData, id = 'typedDataSigReq', sendOpts) {
const opts = {
callback: this.genCallback(id),
net: this.network.id
}
if (this.address) opts.from = this.address
this.credentials.createTypedDataSignatureRequest(typedData, opts).then(jwt => this.send(jwt, id, sendOpts))
}
/**
* Creates and sends a request to a user to sign an arbitrary data string
*
* @param {String} data a string representing a piece of arbitrary data
* @param {String} [id='personalSignReq']
* @param {Object} [sendOpts]
*/
requestPersonalSign(data, id='personalSignReq', sendOpts) {
const opts = {
callback: this.genCallback(id),
net: this.network.id
}
if (this.address) opts.from = this.address
this.credentials.createPersonalSignRequest(data, opts).then(jwt => this.send(jwt, id, sendOpts))
}
/**
* Creates a [Selective Disclosure Request JWT](https://github.com/uport-project/specs/blob/develop/messages/sharereq.md) and sends request message to uPort client.
*
* @example
* const req = { requested: ['name', 'country'],
* callbackUrl: 'https://myserver.com',
* notifications: true }
* const reqID = 'disclosureReq'
* connect.requestDisclosure(req, reqID)
* connect.onResponse(reqID).then(jwt => {
* ...
* })
*
* @param {Object} [reqObj={}] request params object
* @param {Array} reqObj.requested an array of attributes for which you are requesting credentials to be shared for
* @param {Array} reqObj.verified an array of attributes for which you are requesting verified credentials to be shared for
* @param {Boolean} reqObj.notifications boolean if you want to request the ability to send push notifications
* @param {String} reqObj.callbackUrl the url which you want to receive the response of this request
* @param {String} reqObj.networkId Override default network id of Ethereum chain of identity eg. 0x4 for rinkeby
* @param {String} reqObj.rpcUrl Override default JSON RPC url for networkId. This is generally only required for use with private chains.
* @param {String} reqObj.accountType Ethereum account type: "general", "keypair", or "none"
* @param {Number} reqObj.expiresIn Seconds until expiry
* @param {String} [id='disclosureReq'] string to identify request, later used to get response
* @param {Object} [sendOpts] reference send function options
*/
async requestDisclosure (reqObj = {}, id = 'disclosureReq', sendOpts) {
if (!reqObj.vc) await this.signAndUploadProfile()
// Augment request object with verified claims, accountType, and a callback url
reqObj = Object.assign({
vc: this.vc,
accountType: this.accountType || 'none',
callbackUrl: this.genCallback(id),
}, reqObj)
if (reqObj.accountType != 'none') {
reqObj.networkId = this.network.id
reqObj.rpcUrl = this.network.rpcUrl
}
// Create and send request
this.credentials.createDisclosureRequest(reqObj, reqObj.expiresIn)
.then(jwt => this.send(jwt, id, sendOpts))
}
/**
* Create and send a verification (credential) about connnected user. Verification is signed by
* temporary session keys created by Connect. If you want to create a verification with a different
* keypair/did use uPort credentials and send it with the Connect send function.
*
* @example
* connect.sendVerification({
* exp: <future timestamp>,
* claim: { name: 'John Smith' }
* }, 'REQUEST_ID')
* connect.onResponse('REQUEST_ID').then(credential => {
* ...
* })
*
* @param {Object} [verification] a unsigned verification object, by default the sub is the connected user
* @param {Object} verification.claim a claim about the subject, single key value or key mapping to object with multiple values (ie { address: {street: ..., zip: ..., country: ...}})
* @param {String} verification.exp time at which this verification expires and is no longer valid (seconds since epoch)
* @param {String} [id='sendVerReq'] string to identify request, later used to get response
* @param {Object} [sendOpts] reference send function options
*/
async sendVerification (verification = {}, id = 'sendVerReq', sendOpts) {
if (!verification.vc) await this.signAndUploadProfile()
// Callback and message form differ for this req, may be reconciled in the future
const cb = this.genCallback(id)
verification = { sub: this.did, vc: this.vc, ...verification }
this.credentials.createVerification(verification).then(jwt => {
const uri = message.util.paramsToQueryString(message.util.messageToURI(jwt), {'callback_url': cb})
this.send(uri, id, sendOpts)
})
}
/**
* Update the internal state of the connect instance and ensure that it is consistent
* with the state saved to localStorage. You can pass in an object containing key-value pairs to update,
* or a function that returns updated key-value pairs as a function of the current state.
*
* @param {Function|Object} Update -- An object, or function specifying updates to the current Connect state (as a function of the current state)
*/
setState(update) {
switch (typeof update) {
case 'object':
this._state = { ...this._state, ...update }
break
case 'function':
this._state = update(this._state)
break
case 'undefined':
break
default:
throw new Error(`Cannot update state with ${update}`)
}
// Normalize address to mnid
const { mnid } = this._state
if (isMNID(mnid)) {
this._state.address = decode(mnid).address
} else if (mnid) {
// Don't allow setting an invalid mnid
throw new Error(`Invalid MNID: ${this._state.mnid}`)
}
if (this.publicEncKey && this.pushToken) {
this.pushTransport = pushTransport(this.pushToken, this.publicEncKey)
}
// Write to localStorage
if (this.useStore) this.store.set(this._state)
}
/**
* Load state from local storage and set this instance's state accordingly.
*/
loadState() {
// replace state
if (this.useStore) this.setState(state => this.store.get())
}
/**
* Clear any user-specific state from the browser, (both the Connect instance and localStorage)
* effectively logging them out. The keypair (app-instance identity) is preserved
*/
logout() {
// Clear explicit state
this.setState(state => ({keypair: state.keypair}))
// Clear all instance variables with references to current state
this.pushTransport = null
}
/**
* Clear the entire state of the connect instance, including the keypair, from memory
* and localStorage. Rebuild this.credentials with a new app-instance identity
*/
reset() {
this.logout()
// Rebuild credentials
this.keypair = Credentials.createIdentity()
this.credentials = new Credentials({...this.keypair, ...this.resolverConfigs})
}
/**
* Accessor methods for Connect state. The state consists of the key-value pairs below
* (did, doc, mnid, address, keypair, pushToken, and publicEncKey)
* @private
*/
get state () { return this._state }
get did () { return this._state.did }
get doc () { return {...this._state.doc} }
get mnid () { return this._state.mnid }
get address () { return this._state.address }
get keypair () { return {...this._state.keypair} }
get verified () { return this._state.verified }
get pushToken () { return this._state.pushToken }
get publicEncKey () { return this._state.publicEncKey }
/**
* Setter methods with appropriate validation
* @private
*/
set state (state) { throw new Error('Use setState to set state object') }
set did (did) { this.setState({did}) }
set doc (doc) { this.setState({doc}) }
set mnid (mnid) { this.setState({mnid}) }
set keypair (keypair) { this.setState({keypair}) }
set verified (verified) { this.setState({verified}) }
set pushToken (pushToken) { this.setState({pushToken}) }
set publicEncKey (publicEncKey) { this.setState({publicEncKey}) }
// Address field alone is deprectated. Allow setting an mnid, but not an unqualified address
set address (address) {
if (isMNID(address)) {
this.setState({mnid: address})
} else {
if (address === this.address) return
throw new Error('Setting an Ethereum address without a network id is not supported. Use an MNID instead.')
}
}
/**
* @private
*/
genCallback (reqId) {
return this.isOnMobile ? windowCallback(reqId) : transport.messageServer.genCallback()
}
/**
* @private
* Sign a profile object with this.credentials, and upload it to ipfs, prepending
* the instance array of verified claims (this.vc) with the ipfs uri. If a profile
* object is not provided, create one on the fly
* @param {Object} [profile] the profile object to be signed and uploaded
* @returns {Promise<String, Error>} a promise resolving to the ipfs hash, or rejecting with an error
*/
signAndUploadProfile (profile) {
if (!profile && this.vc.length > 0) return
profile = profile || {
name: this.appName,
description: this.description,
url: (typeof window !== 'undefined') ? `${window.location.protocol}//${window.location.host}` : undefined,
profileImage: this.profileImage,
bannerImage: this.bannerImage,
brandColor: this.brandColor,
}
// Upload to ipfs
return this.credentials.createVerification({sub: this.keypair.did, claim: profile})
.then(jwt => ipfsAdd(jwt))
.then(hash => {
console.log('uploaded, ', this.vc)
this.vc.unshift(`/ipfs/${hash}`)
return hash
})
}
}
const LOCALSTOREKEY = 'connectState'
class LocalStorageStore {
constructor (key = LOCALSTOREKEY) {
this.key = key
}
get() {
return JSON.parse(store.get(this.key) || '{}')
}
set(stateObj) {
store.set(this.key, JSON.stringify(stateObj))
}
}
/**
* A transport created for uPort connect. Bundles transport functionality from uport-transports. This implements the
* default QR modal flow on desktop clients. If given a request which uses the messaging server Chasqui to relay
* responses, it will by default poll Chasqui and return response. If given a request which specifies another
* callback to receive the response, for example your own server, it will show the request in the default QR
* modal and then instantly return. You can then handle how to fetch the response specific to your implementation.
*
* @param {String} appName App name displayed in QR pop over modal
* @return {Function} Configured connectTransport function
* @param {String} request uPort client request message
* @param {Object} [config={}] Optional config object
* @param {String} config.data Additional data to be returned later with response
* @return {Promise<Object, Error>} Function to close the QR modal
* @private
*/
const connectTransport = (appName) => (request, {data, cancel}) => {
if (transport.messageServer.isMessageServerCallback(request)) {
return transport.qr.chasquiSend({appName})(request).then(res => ({payload: res, data}))
} else {
transport.qr.send(appName)(request, {cancel})
// TODO return close QR func?
return Promise.resolve({data})
}
}
/**
* Wrap push transport from uport-transports, providing stored pushToken and publicEncKey from the
* provided Connect instance
* @param {Connect} connect The Connect instance holding the pushToken and publicEncKey
* @returns {Function} Configured pushTransport function
* @private
*/
const pushTransport = (pushToken, publicEncKey) => {
const send = transport.push.sendAndNotify(pushToken, publicEncKey)
return (request, { type, redirectUrl, data}) => {
if (transport.messageServer.isMessageServerCallback(request)) {
return transport.messageServer.URIHandlerSend(send)(request, {type, redirectUrl})
.then(res => {
transport.ui.close()
return {payload: res, data}
})
} else {
// Return immediately for custom message server
send(request, {type, redirectUrl})
return Promise.resolve({data})
}
}
}
/**
* Gets current window url and formats as request callback
*
* @return {String} Returns window url formatted as callback
* @private
*/
const windowCallback = (id) => {
const md = new MobileDetect(navigator.userAgent)
const chromeAndIOS = (md.userAgent() === 'Chrome' && md.os() === 'iOS')
const callback = chromeAndIOS ? `googlechrome:${window.location.href.substring(window.location.protocol.length)}` : window.location.href
return message.util.paramsToUrlFragment(callback, {id})
}
export default Connect