UNPKG

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