Skip to content

Commit

Permalink
feat(connect-deeplink): create package
Browse files Browse the repository at this point in the history
  • Loading branch information
karliatto authored and martykan committed Sep 26, 2024
1 parent 4986b96 commit 12a72c5
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 71 deletions.
47 changes: 47 additions & 0 deletions packages/connect-deeplink/README.md
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.
17 changes: 17 additions & 0 deletions packages/connect-deeplink/package.json
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:^"
}
}
210 changes: 210 additions & 0 deletions packages/connect-deeplink/src/index.ts
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}&params=${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';
8 changes: 8 additions & 0 deletions packages/connect-deeplink/tsconfig.json
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" }
]
}
1 change: 1 addition & 0 deletions suite-native/module-connect-popup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@suite-native/intl": "workspace:^",
"@suite-native/navigation": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/connect-deeplink": "workspace:*",
"expo-linking": "^6.3.1",
"react": "18.2.0",
"react-native": "0.75.2",
Expand Down
Loading

0 comments on commit 12a72c5

Please sign in to comment.