-
-
Notifications
You must be signed in to change notification settings - Fork 276
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(connect-deeplink): create package
- Loading branch information
Showing
10 changed files
with
344 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# @trezor/connect-deeplink | ||
|
||
[![NPM](https://img.shields.io/npm/v/@trezor/connect-deeplink.svg)](https://www.npmjs.org/package/@trezor/connect-deeplink) | ||
|
||
The `@trezor/connect-deeplink` package provides an implementation of `@trezor/connect` which uses deep links to communicate with the Trezor Suite Lite app. | ||
|
||
Currently the library is still under development, only supports read-only methods and does not communicate with the production Suite Lite app. | ||
|
||
To run a dev version of the Suite mobile app follow the instructions in [@suite-native/app](https://github.com/trezor/trezor-suite/blob/develop/suite-native/app/README.md) | ||
|
||
|
||
## Using the Library | ||
|
||
To use the library, you need to initialize it with the `deeplinkOpen` and `deeplinkCallbackUrl` settings. | ||
|
||
```javascript | ||
import TrezorConnect from '@trezor/connect-deeplink'; | ||
|
||
TrezorConnect.init({ | ||
manifest: { | ||
email: '[email protected]', | ||
appUrl: 'http://your.application.com', | ||
}, | ||
deeplinkOpen: url => { | ||
// eslint-disable-next-line no-console | ||
console.log('deeplinkOpen', url); | ||
Linking.openURL(url); | ||
}, | ||
deeplinkCallbackUrl: Linking.createURL('/connect'), | ||
}); | ||
``` | ||
|
||
To receive the deep link callback, you need to add a listener which will call `TrezorConnect.handleDeeplink` with the deep link URL. | ||
|
||
```javascript | ||
useEffect(() => { | ||
const subscription = Linking.addEventListener('url', event => { | ||
TrezorConnect.handleDeeplink(event.url); | ||
}); | ||
|
||
return () => subscription?.remove(); | ||
}, []); | ||
``` | ||
## Example | ||
The [connect-deeplink-example](https://github.com/trezor/trezor-suite/tree/develop/packages/connect-examples/connect-deeplink-example) shows how to use the library in a React Native + Expo app. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "@trezor/connect-deeplink", | ||
"version": "1.0.0", | ||
"private": true, | ||
"license": "See LICENSE.md in repo root", | ||
"sideEffects": false, | ||
"main": "src/index", | ||
"scripts": { | ||
"depcheck": "yarn g:depcheck", | ||
"lint:js": "yarn g:eslint '**/*.{ts,tsx,js}'", | ||
"type-check": "yarn g:tsc --build" | ||
}, | ||
"dependencies": { | ||
"@trezor/connect": "workspace:^", | ||
"@trezor/utils": "workspace:^" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
import EventEmitter from 'events'; | ||
|
||
import * as ERRORS from '@trezor/connect/src/constants/errors'; | ||
import { parseConnectSettings } from '@trezor/connect/src/data/connectSettings'; | ||
import type { CallMethodPayload } from '@trezor/connect/src/events/call'; | ||
import { ConnectFactoryDependencies, factory } from '@trezor/connect/src/factory'; | ||
import type { TrezorConnect as TrezorConnectType } from '@trezor/connect/src/types'; | ||
import type { | ||
ConnectSettings, | ||
ConnectSettingsPublic, | ||
Manifest, | ||
Response, | ||
} from '@trezor/connect/src/types'; | ||
import { Login } from '@trezor/connect/src/types/api/requestLogin'; | ||
import { Deferred, createDeferred } from '@trezor/utils'; | ||
|
||
export class TrezorConnectDeeplink implements ConnectFactoryDependencies { | ||
public eventEmitter = new EventEmitter(); | ||
private _settings: ConnectSettings; | ||
private messagePromises: Record<number, Deferred<any>> = {}; | ||
private messageID = 0; | ||
private schema = 'trezorsuitelite'; | ||
|
||
public constructor() { | ||
this._settings = { | ||
...parseConnectSettings(), | ||
deeplinkOpen: () => { | ||
throw ERRORS.TypedError('Init_NotInitialized'); | ||
}, | ||
}; | ||
} | ||
|
||
public manifest(manifest: Manifest) { | ||
this._settings = { | ||
...this._settings, | ||
...parseConnectSettings({ | ||
...this._settings, | ||
manifest, | ||
}), | ||
}; | ||
} | ||
|
||
public init(settings: Partial<ConnectSettingsPublic>) { | ||
if (!settings.deeplinkOpen) { | ||
throw new Error('TrezorConnect native requires "deeplinkOpen" setting.'); | ||
} | ||
this._settings = { | ||
...parseConnectSettings({ ...this._settings, ...settings }), | ||
deeplinkOpen: settings.deeplinkOpen, | ||
deeplinkCallbackUrl: settings.deeplinkCallbackUrl, | ||
}; | ||
|
||
return Promise.resolve(); | ||
} | ||
|
||
public call(params: CallMethodPayload) { | ||
this.messageID++; | ||
this.messagePromises[this.messageID] = createDeferred(); | ||
const { method, ...restParams } = params; | ||
if (!this._settings) { | ||
throw new Error('TrezorConnect not initialized.'); | ||
} | ||
if (!this._settings.deeplinkOpen) { | ||
throw new Error('TrezorConnect native requires "deeplinkOpen" setting.'); | ||
} | ||
if (!this._settings.deeplinkCallbackUrl) { | ||
throw new Error('TrezorConnect native requires "deeplinkCallbackUrl" setting.'); | ||
} | ||
const callbackUrl = this.buildCallbackUrl(this._settings.deeplinkCallbackUrl, { | ||
id: this.messageID, | ||
}); | ||
const url = this.buildUrl(method, restParams, callbackUrl); | ||
this._settings.deeplinkOpen(url); | ||
|
||
return this.messagePromises[this.messageID].promise; | ||
} | ||
|
||
public requestLogin(): Response<Login> { | ||
throw ERRORS.TypedError('Method_InvalidPackage'); | ||
} | ||
|
||
public uiResponse() { | ||
throw ERRORS.TypedError('Method_InvalidPackage'); | ||
} | ||
|
||
public renderWebUSBButton() {} | ||
|
||
public disableWebUSB() { | ||
throw ERRORS.TypedError('Method_InvalidPackage'); | ||
} | ||
|
||
public requestWebUSBDevice() { | ||
throw ERRORS.TypedError('Method_InvalidPackage'); | ||
} | ||
|
||
public cancel(error?: string) { | ||
this.resolveMessagePromises({ | ||
success: false, | ||
error, | ||
}); | ||
} | ||
|
||
public dispose() { | ||
this.eventEmitter.removeAllListeners(); | ||
this._settings = parseConnectSettings(); | ||
|
||
return Promise.resolve(undefined); | ||
} | ||
|
||
public handleDeeplink(url: string): void { | ||
let id; | ||
let parsedUrl; | ||
try { | ||
parsedUrl = new URL(url); | ||
id = parsedUrl.searchParams.get('id'); | ||
if (!id || isNaN(Number(id))) throw new Error('Missing `id` parameter.'); | ||
id = Number(id); | ||
} catch (error) { | ||
this.resolveMessagePromises({ | ||
success: false, | ||
error, | ||
}); | ||
|
||
return; | ||
} | ||
|
||
const responseParam = parsedUrl.searchParams.get('response'); | ||
if (!responseParam) { | ||
this.messagePromises[id].resolve({ | ||
id, | ||
success: false, | ||
error: 'The provided url is missing `response` parameter.', | ||
}); | ||
delete this.messagePromises[id]; | ||
|
||
return; | ||
} | ||
|
||
let parsedParams; | ||
try { | ||
parsedParams = JSON.parse(responseParam); | ||
} catch {} | ||
|
||
if (!parsedParams) { | ||
this.messagePromises[id].resolve({ | ||
id, | ||
success: false, | ||
error: 'Error parsing deeplink params.', | ||
}); | ||
delete this.messagePromises[id]; | ||
} | ||
|
||
const { success, payload } = parsedParams; | ||
this.messagePromises[id].resolve({ id, payload, success }); | ||
delete this.messagePromises[id]; | ||
} | ||
|
||
resolveMessagePromises(resolvePayload: Record<string, any>) { | ||
Object.keys(this.messagePromises).forEach(id => { | ||
this.messagePromises[id as any].resolve({ | ||
id, | ||
payload: resolvePayload, | ||
}); | ||
delete this.messagePromises[id as any]; | ||
}); | ||
} | ||
|
||
private buildUrl(method: string, params: any, callback: string) { | ||
return `${this.schema}://connect?method=${method}¶ms=${encodeURIComponent( | ||
JSON.stringify(params), | ||
)}&callback=${encodeURIComponent(callback)}`; | ||
} | ||
|
||
private buildCallbackUrl(url: string, params: Record<string, string | number>) { | ||
try { | ||
const urlWithParams = new URL(url); | ||
Object.entries(params).forEach(([key, value]) => { | ||
urlWithParams.searchParams.set(key, value.toString()); | ||
}); | ||
|
||
return urlWithParams.toString(); | ||
} catch { | ||
throw new Error('Provided "deeplinkCallbackUrl" is not valid.'); | ||
} | ||
} | ||
} | ||
|
||
const impl = new TrezorConnectDeeplink(); | ||
const TrezorConnect: TrezorConnectType & { | ||
handleDeeplink: (url: string) => void; | ||
} = { | ||
...factory({ | ||
eventEmitter: impl.eventEmitter, | ||
init: impl.init.bind(impl), | ||
call: impl.call.bind(impl), | ||
manifest: impl.manifest.bind(impl), | ||
requestLogin: impl.requestLogin.bind(impl), | ||
uiResponse: impl.uiResponse.bind(impl), | ||
renderWebUSBButton: impl.renderWebUSBButton.bind(impl), | ||
disableWebUSB: impl.disableWebUSB.bind(impl), | ||
requestWebUSBDevice: impl.requestWebUSBDevice.bind(impl), | ||
cancel: impl.cancel.bind(impl), | ||
dispose: impl.dispose.bind(impl), | ||
}), | ||
handleDeeplink: impl.handleDeeplink.bind(impl), | ||
}; | ||
|
||
// eslint-disable-next-line import/no-default-export | ||
export default TrezorConnect; | ||
export * from '@trezor/connect/src/exports'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"extends": "../../tsconfig.base.json", | ||
"compilerOptions": { "outDir": "libDev" }, | ||
"references": [ | ||
{ "path": "../connect" }, | ||
{ "path": "../utils" } | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.