Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(connect-explorer): connect-deeplink / mobile #14481

Merged
merged 3 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/connect-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@trezor/components": "workspace:^",
"@trezor/connect": "workspace:^",
"@trezor/connect-explorer-theme": "workspace:^",
"@trezor/connect-mobile": "workspace:^",
"@trezor/connect-web": "workspace:^",
"@trezor/connect-webextension": "workspace:^",
"@trezor/protobuf": "workspace:^",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions packages/connect-explorer/src/actions/methodActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TSchema } from '@sinclair/typebox';
import JSON5 from 'json5';

import TrezorConnect from '@trezor/connect-web';
import TrezorConnectMobile from '@trezor/connect-mobile';
import { getDeepValue } from '@trezor/schema-utils/src/utils';

import { GetState, Dispatch, Field } from '../types';
Expand Down Expand Up @@ -80,10 +81,11 @@ export const onSetManualMode = (manualMode: boolean) => ({
});

export const onSubmit = () => async (dispatch: Dispatch, getState: GetState) => {
const { method } = getState();
const { method, connect } = getState();
if (!method?.name) throw new Error('method name not specified');
dispatch({ type: SET_METHOD_PROCESSING, payload: true });
const connectMethod = TrezorConnect[method.name];
const trezorConnectImpl = connect.deeplink ? TrezorConnectMobile : TrezorConnect;
const connectMethod = trezorConnectImpl[method.name];
if (typeof connectMethod !== 'function') {
dispatch(
onResponse({
Expand Down
28 changes: 25 additions & 3 deletions packages/connect-explorer/src/actions/trezorConnectActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import TrezorConnect, {
WEBEXTENSION,
} from '@trezor/connect-web';

import TrezorConnectMobile from '@trezor/connect-mobile';

import { TrezorConnectDevice, Dispatch, Field, GetState } from '../types';

import * as ACTIONS from './index';
Expand All @@ -15,7 +17,7 @@ export type TrezorConnectAction =
| { type: typeof DEVICE.CONNECT; device: TrezorConnectDevice }
| { type: typeof DEVICE.CONNECT_UNACQUIRED; device: TrezorConnectDevice }
| { type: typeof DEVICE.DISCONNECT; device: TrezorConnectDevice }
| { type: typeof ACTIONS.ON_CHANGE_CONNECT_OPTIONS; payload: ConnectOptions }
| { type: typeof ACTIONS.ON_CHANGE_CONNECT_OPTIONS; payload: ConnectOptions; deeplink: boolean }
| { type: typeof ACTIONS.ON_HANDSHAKE_CONFIRMED }
| { type: typeof ACTIONS.ON_INIT_ERROR; payload: string }
| {
Expand Down Expand Up @@ -143,6 +145,7 @@ export const init =
// Get default coreMode from URL params (?core-mode=auto)
const urlParams = new URLSearchParams(window.location.search);
const coreMode = (urlParams.get('core-mode') as ConnectOptions['coreMode']) || 'auto';
const deeplink = urlParams.get('deeplink') === 'true';

const connectOptions = {
coreMode,
Expand All @@ -160,20 +163,39 @@ export const init =
};

try {
await TrezorConnect.init(connectOptions);
if (deeplink) {
await TrezorConnectMobile.init({
...connectOptions,
deeplinkOpen(url) {
window.open(url, '_blank');
},
deeplinkCallbackUrl:
(process.env.CONNECT_EXPLORER_FULL_URL || window.location.origin) +
'/callback',
});
const bc = new BroadcastChannel('trezor_connect_callback');
bc.onmessage = e => {
if (e.data.type === 'popup_callback') {
TrezorConnectMobile.handleDeeplink(e.data.url);
}
};
} else {
await TrezorConnect.init(connectOptions);
}
} catch (err) {
dispatch({ type: ACTIONS.ON_INIT_ERROR, payload: err.message });

return;
}

dispatch({ type: ACTIONS.ON_CHANGE_CONNECT_OPTIONS, payload: connectOptions });
dispatch({ type: ACTIONS.ON_CHANGE_CONNECT_OPTIONS, payload: connectOptions, deeplink });
};

export const onSubmitInit = () => async (dispatch: Dispatch, getState: GetState) => {
const { connect } = getState();
// Disposing TrezorConnect to init it again.
await TrezorConnect.dispose();
await TrezorConnectMobile.dispose();

return dispatch(init(connect.options));
};
11 changes: 11 additions & 0 deletions packages/connect-explorer/src/components/BetaOnly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const isBetaOnly = !process.env.CONNECT_EXPLORER_FULL_URL?.startsWith(
'https://connect.trezor.io/9/',
);

export const BetaOnly = (props: React.PropsWithChildren) => {
if (isBetaOnly) {
return <>{props.children}</>;
}

return null;
};
20 changes: 20 additions & 0 deletions packages/connect-explorer/src/components/icons/IconMobile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

function IconMobile() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="none"
viewBox="0 0 32 32"
>
<path
fill="#DF8432"
d="M22 2H10a3 3 0 0 0-3 3v22a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3M10 4h12a1 1 0 0 1 1 1v1H9V5a1 1 0 0 1 1-1m12 24H10a1 1 0 0 1-1-1v-1h14v1a1 1 0 0 1-1 1"
/>
</svg>
);
}

export default IconMobile;
4 changes: 4 additions & 0 deletions packages/connect-explorer/src/pages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@
"test": {
"title": "Test",
"display": "hidden"
},
"callback": {
"title": "Callback",
"display": "hidden"
}
}
20 changes: 20 additions & 0 deletions packages/connect-explorer/src/pages/callback.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect } from 'react';
import { Button } from '@trezor/components';

export const CallbackHandler = () => {
useEffect(() => {
const bc = new BroadcastChannel('trezor_connect_callback');
bc.postMessage({ type: 'popup_callback', url: window.location.href });
}, []);

return <div></div>;

};

# Trezor Connect Callback

Callback received. You can close this window now.

<Button onClick={() => window.close()}>Close</Button>

<CallbackHandler />
71 changes: 71 additions & 0 deletions packages/connect-explorer/src/pages/details/deeplinking.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Callout } from 'nextra/components';
import { BetaOnly } from '../../components/BetaOnly';

# Deep linking specification

<BetaOnly>

To support connecting to Trezor devices from 3rd party apps on mobile devices, Trezor Suite Lite provides a communication interface via deep linking.

<Callout type="info">
**This feature is still in beta and is subject to change. It's currently available only in
development builds of Trezor Suite Lite.**
</Callout>

## Base URL

The base URL for deep linking is different depending on the environment.

| Environment | Base URL |
| ----------- | ------------------------------------------------------ |
| Production | currently unavailable |
| Development | `https://dev.suite.sldev.cz/connect/develop/deeplink/` |
| Local | `trezorsuitelite://connect` |

## Query parameters

The method call is specified using the query parameters.

| Parameter | Type | Required | Description |
| ---------- | ----------- | -------- | -------------------------------------------------------- |
| `method` | string | yes | The name of the Connect method to call (eg. getAddress). |
| `params` | JSON object | yes | The parameters for the method call encoded as JSON. |
| `callback` | string | yes | The URL to redirect to after the method call is made. |

## Callback

To receive the result of the method call, the app must specify a callback URL. The callback URL is called with the result of the method call.

The following query parameters are passed in the callback URL:

| Parameter | Type | Description |
| ---------- | ----------- | --------------------------------------------------------------------------- |
| `id` | integer | ID of the call |
| `response` | JSON object | Result of the method call, equivalent to the object returned by the method. |

## Example

Let's imagine we want to convert the following call to a deep link:

```
const address = await TrezorConnect.getAddress({
coin: 'btc',
path: "m/44'/0'/0'/0/0",
});
```

The parameters would be:

- **method**: `getAddress`
- **params**: `{"coin":"btc","path":"m/44'/0'/0'/0/0"}`
- **callback**: `https://httpbin.org/get` (as an example)

The encoded deep link URL would then be:

```
trezorsuitelite://connect?method=getAddress&params=%7B%22coin%22%3A%22btc%22%2C%22path%22%3A%22m%2F44%27%2F0%27%2F0%27%2F0%2F0%22%7D&callback=https%3A%2F%2Fhttpbin.org%2Fget
```

When the user returns to the app, the callback URL is called with the result of the method call.

</BetaOnly>
143 changes: 142 additions & 1 deletion packages/connect-explorer/src/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
import IconNode from '../components/icons/IconNode';
import IconWeb from '../components/icons/IconWeb';
import IconExtension from '../components/icons/IconExtension';
import IconMobile from '../components/icons/IconMobile';
import ZoomableIllustration from '../components/ZoomableIllustration';
import { BetaOnly } from '../components/BetaOnly';

export const SectionCard = ({ children }) => (
<TrezorCard margin={{ bottom: spacings.xl }}>{children}</TrezorCard>
Expand Down Expand Up @@ -106,10 +108,13 @@ export const ExampleHeading = styled.h3`

Depending on your environment you need to chose the right package and follow the particular guide:

<Cards>
<Cards num={2}>
<Card icon={<IconNode />} title="Node.js" href="#nodejs" />
<Card icon={<IconWeb />} title="Web" href="#web" />
<Card icon={<IconExtension />} title="Web extension" href="#web-extension" />
<BetaOnly>
<Card icon={<IconMobile />} title="Mobile" href="#mobile" />
</BetaOnly>
</Cards>

<CollapsibleBox heading="Still unsure?" margin={{ top: spacings.md }}>
Expand Down Expand Up @@ -498,3 +503,139 @@ export const ExampleHeading = styled.h3`
</CollapsibleBox>

</SectionCard>

<BetaOnly>

<HiddenNextraHeading>

### Mobile

</HiddenNextraHeading>
<SectionCard>
<SdkHeading className='nx-text-slate-900 dark:nx-text-slate-100'>
<IconMobile />
<SdkName>Mobile</SdkName>
<SdkTag>Using deep linking</SdkTag>
<Link href="/readme/connect-mobile" passHref legacyBehavior>
<Button
variant="tertiary"
size="small"
icon="bookOpenText"
target="_self"
href="#">
README
</Button>
</Link>
<Button
as="a"
variant="tertiary"
size="small"
icon="githubLogoAlt"
href="https://github.com/trezor/trezor-suite/tree/develop/packages/connect-mobile"
target="_blank">
View on Github
</Button>
</SdkHeading>

<ZoomableIllustration src="/images/schema-connect-deeplink.svg" $darkMode alt="connect schema when used in deeplink" />

<SdkContainer>
<SdkDescription>
#### About

`@trezor/connect-mobile` is a package that allows you to communicate with Trezor devices from your mobile app.
This package is somewhat different from the other SDKs, as it doesn't provide a direct API to the device. Instead, it uses deep linking to open the Trezor Suite app and communicate with the device.

#### Use deep linking without SDK

If you are not able to use the SDK, for example on non-supported platforms, you can still use deep linking to open the Trezor Suite app and communicate with the device.

This can be done by implementing the following specification.

<Link href="/details/deeplinking" passHref legacyBehavior>
<Button
variant="tertiary"
className="nx-mt-4 nx-mb-8"
size="small"
icon="arrowRight"
iconAlignment="right"
target="_self"
href="#">
Deep linking specification
</Button>
</Link>

</SdkDescription>
<ExamplesAside>
<ExampleHeading>Examples:</ExampleHeading>
- [Expo App example](https://github.com/trezor/trezor-suite/tree/develop/packages/connect-examples/mobile-expo)
</ExamplesAside>
</SdkContainer>

<CollapsibleBox heading="Quick start manual" margin={{ top: spacings.md }}>
<Steps>
{<h3>Installation of the package</h3>}

Simply install the package using your preferred package manager:

```bash
npm install @trezor/connect-mobile
# or
yarn add @trezor/connect-mobile
```

{<h3>Initialization of the API</h3>}

```javascript
import * as Linking from 'expo-linking';

TrezorConnect.init({
manifest: {
email: '[email protected]',
appUrl: 'http://your.application.com',
},
deeplinkOpen: url => {
Linking.openURL(url);
},
deeplinkCallbackUrl: Linking.createURL('/connect'),
});
```

Trezor Connect Manifest requires that you, as a Trezor Connect integrator, share your email and application URL with us. This provides us with the ability to reach you in case of any required maintenance. This subscription is mandatory.

The deeplink SDK needs two extra parameters: `deeplinkOpen` and `deeplinkCallbackUrl`. The `deeplinkOpen` function is used to open the Trezor Suite app, and the `deeplinkCallbackUrl` is the URL that the app will redirect to after the operation is complete.

In this example, we are using the `Linking` API from Expo to open the Trezor Suite app, however this may be different depending on your mobile app's environment.

{<h3>Handling callbacks</h3>}

The app needs to pass results from the callback URL back to the SDK. This can be done by listening to the URL and passing the data to the SDK using the `handleDeeplink` method.

```javascript
useEffect(() => {
const subscription = Linking.addEventListener('url', event => {
TrezorConnect.handleDeeplink(event.url);
});

return () => subscription?.remove();
}, []);
```

{<h3>How to use?</h3>}

Here is an example of how to get the device's public key:

```javascript
TrezorConnect.getPublicKey({
path: "m/44'/0'/0'/0/0",
showOnTrezor: true,
});
```

More methods with detailed explanation can be found on the left under the 'Coin methods' section. You can also try the Method Testing Tool, where you can try interacting with the device by yourself.
</Steps>
</CollapsibleBox>

</SectionCard>

</BetaOnly>
Loading
Loading