Skip to content

Commit

Permalink
chore: unify mobile and desktop connect popup logic
Browse files Browse the repository at this point in the history
  • Loading branch information
martykan committed Feb 11, 2025
1 parent 25b6741 commit a8996a8
Show file tree
Hide file tree
Showing 19 changed files with 190 additions and 240 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { useDispatch, useSelector } from 'src/hooks/suite';
export const ConnectPopupModal = () => {
const dispatch = useDispatch();
const popupCall = useSelector(selectConnectPopupCall);
if (!popupCall) return null;
if (!popupCall || popupCall?.state !== 'request') return null;

const { method, processName, origin } = popupCall;
const { methodTitle, confirmLabel, processName, origin } = popupCall;
const onConfirm = () => dispatch(connectPopupActions.approveCall());
const onCancel = () =>
dispatch(connectPopupActions.rejectCall(ERRORS.TypedError('Method_Cancel')));
Expand All @@ -27,13 +27,13 @@ export const ConnectPopupModal = () => {
<Translation id="TR_CANCEL" />
</NewModal.Button>
<NewModal.Button variant="primary" onClick={onConfirm}>
<Translation id="TR_CONFIRM" />
{confirmLabel || <Translation id="TR_CONFIRM" />}
</NewModal.Button>
</>
}
heading={<Translation id="TR_TREZOR_CONNECT" />}
>
<H2>{method}</H2>
<H2>{methodTitle}</H2>

{processName && (
<Paragraph margin={{ top: spacings.xs }}>
Expand Down
2 changes: 1 addition & 1 deletion suite-common/connect-popup/src/connectPopupMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const prepareConnectPopupMiddleware = createMiddlewareWithExtraDeps(
async (action, { dispatch, next, extra }) => {
await next(action);

if (connectPopupActions.initiateCall.match(action)) {
if (connectPopupActions.initiateCall.match(action) && action.payload.state === 'request') {
dispatch(extra.actions.openModal({ type: 'connect-popup' }));
}
if (
Expand Down
5 changes: 3 additions & 2 deletions suite-common/connect-popup/src/connectPopupReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export const prepareConnectPopupReducer = createReducerWithExtraDeps(
state.activeCall = payload;
})
.addCase(connectPopupActions.approveCall, state => {
state?.activeCall?.confirmation.resolve();
if (state.activeCall?.state === 'request') state.activeCall.confirmation.resolve();
state.activeCall = undefined;
})
.addCase(connectPopupActions.rejectCall, (state, { payload }) => {
state?.activeCall?.confirmation.reject(payload);
if (state.activeCall?.state === 'request')
state.activeCall.confirmation.reject(payload);
state.activeCall = undefined;
});
},
Expand Down
105 changes: 94 additions & 11 deletions suite-common/connect-popup/src/connectPopupThunks.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { AsyncThunkAction } from '@reduxjs/toolkit';

import { CustomThunkAPI, createThunk } from '@suite-common/redux-utils';
import { selectSelectedDevice } from '@suite-common/wallet-core';
import { deviceActions, selectSelectedDevice } from '@suite-common/wallet-core';
import TrezorConnect, { CallMethodParams, CallMethodResponse, ERRORS } from '@trezor/connect';
import { serializeError } from '@trezor/connect/src/constants/errors';
import { TypedError, serializeError } from '@trezor/connect/src/constants/errors';
import { DEEPLINK_VERSION } from '@trezor/connect/src/data/version';
import { createDeferred } from '@trezor/utils';

import { connectPopupActions } from './connectPopupActions';

const CONNECT_POPUP_MODULE = '@common/connect-popup';

type ConnectPopupCallThunkResponse<M extends keyof typeof TrezorConnect> = Promise<{
id: number;
success: boolean;
payload: CallMethodResponse<M>;
}>;

type ConnectPopupCallThunkParams<M extends keyof typeof TrezorConnect> = {
id: number;
processName?: string;
origin?: string;
method: M;
Expand All @@ -29,22 +28,40 @@ export const connectPopupCallThunkInner = createThunk<
ConnectPopupCallThunkParams<keyof typeof TrezorConnect>
>(
`${CONNECT_POPUP_MODULE}/callThunk`,
async ({ id, method, payload, processName, origin }, { dispatch, getState, extra }) => {
async ({ method, payload, processName, origin }, { dispatch, getState, extra }) => {
try {
// @ts-expect-error: method is dynamic
const methodInfo = await TrezorConnect[method]({
...payload,
__info: true,
});
if (!methodInfo.success) {
connectPopupActions.initiateCall({
state: 'call-error',
callError: ERRORS.TypedError(methodInfo.payload.code),
});
throw methodInfo;
}
if (
methodInfo.payload.requiredPermissions.includes('management') ||
methodInfo.payload.requiredPermissions.includes('push_tx')
) {
connectPopupActions.initiateCall({
state: 'call-error',
callError: ERRORS.TypedError('Method_NotAllowed'),
});

return;
}

const confirmation = createDeferred();
dispatch(extra.actions.lockDevice(true));
dispatch(
connectPopupActions.initiateCall({
method: methodInfo.payload.info,
state: 'request',
method,
methodTitle: methodInfo.payload.confirmation?.label ?? methodInfo.payload.info,
confirmLabel: methodInfo.payload.confirmation?.customConfirmButton?.label,
processName,
origin,
confirmation,
Expand All @@ -64,20 +81,20 @@ export const connectPopupCallThunkInner = createThunk<
instance: device.instance,
state: device.state,
},
useEmptyPassphrase: device.useEmptyPassphrase,
...payload,
});

return {
...response,
id,
};
// Note: for mobile this needs to be called explicitly, on desktop it's automatically handled by middleware
dispatch(deviceActions.removeButtonRequests({ device }));

return response;
} catch (error) {
console.error('connectPopupCallThunk', error);

return {
success: false,
payload: serializeError(error),
id,
};
} finally {
dispatch(extra.actions.lockDevice(false));
Expand All @@ -94,3 +111,69 @@ export const connectPopupCallThunk = <M extends keyof typeof TrezorConnect>(
ConnectPopupCallThunkParams<M>,
CustomThunkAPI
> => connectPopupCallThunkInner(params) as any;

export const connectPopupDeeplinkThunk = createThunk<void, { url: string }>(
`${CONNECT_POPUP_MODULE}/deeplinkThunk`,
async ({ url }, { dispatch }) => {
try {
const parsedUrl = new URL(url);
const path = parsedUrl.pathname;
const queryParams = Object.fromEntries(parsedUrl.searchParams.entries());

const version = path && path.split('/').slice(-2, -1)[0];
if (
!queryParams?.method ||
!queryParams?.params ||
!queryParams?.callback ||
typeof queryParams?.params !== 'string' ||
typeof queryParams?.method !== 'string' ||
typeof queryParams?.callback !== 'string' ||
!Object.prototype.hasOwnProperty.call(TrezorConnect, queryParams?.method)
) {
dispatch(
connectPopupActions.initiateCall({
state: 'call-error',
callError: TypedError('Method_InvalidParameter'),
}),
);

return;
}

if (!version || parseInt(version) > DEEPLINK_VERSION) {
dispatch(
connectPopupActions.initiateCall({
state: 'call-error',
callError: TypedError('Deeplink_VersionMismatch'),
}),
);

return;
}

const payload = JSON.parse(queryParams.params);
const { method, callback } = queryParams;
const callbackUrl = new URL(callback);

const response = await dispatch(
connectPopupCallThunk({
processName: 'deeplink',
origin: `${callbackUrl.protocol}//${callbackUrl.host}`,
// @ts-expect-error: method is dynamic
method,
payload,
}),
).unwrap();

callbackUrl.searchParams.set('response', JSON.stringify(response));
dispatch(
connectPopupActions.initiateCall({
state: 'deeplink-callback',
callbackUrl: callbackUrl.toString(),
}),
);
} catch (error) {
console.warn('Ignoring invalid deeplink URL', { error, url });
}
},
);
25 changes: 19 additions & 6 deletions suite-common/connect-popup/src/connectPopupTypes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { TrezorError } from '@trezor/connect/src/constants/errors';
import { Deferred } from '@trezor/utils';

export type ConnectPopupCall = {
method: string;
processName?: string;
origin?: string;
confirmation: Deferred<void>;
};
export type ConnectPopupCall =
| {
state: 'request';
method: string;
methodTitle: string;
confirmLabel: string;
processName?: string;
origin?: string;
confirmation: Deferred<void>;
}
| {
state: 'call-error';
callError: TrezorError;
}
| {
state: 'deeplink-callback';
callbackUrl: string;
};
3 changes: 0 additions & 3 deletions suite-common/walletconnect/src/adapters/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const ethereumRequestThunk = createThunk<
const account = getAccount(address);
const response = await dispatch(
trezorConnectPopupActions.connectPopupCallThunk({
id: 0,
method: 'ethereumSignMessage',
payload: {
path: account.path,
Expand All @@ -62,7 +61,6 @@ const ethereumRequestThunk = createThunk<
const account = getAccount(address);
const response = await dispatch(
trezorConnectPopupActions.connectPopupCallThunk({
id: 0,
method: 'ethereumSignTypedData',
payload: {
path: account.path,
Expand Down Expand Up @@ -120,7 +118,6 @@ const ethereumRequestThunk = createThunk<
};
const signResponse = await dispatch(
trezorConnectPopupActions.connectPopupCallThunk({
id: 0,
method: 'ethereumSignTransaction',
payload,
processName: 'WalletConnect',
Expand Down
1 change: 1 addition & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export const en = {
invalidCallback: 'Invalid callback URL',
invalidParams: 'Invalid parameters from calling app',
versionUnsupported: 'Unsupported version. Please update your Trezor Suite app.',
methodNotAllowed: 'Method not allowed for security reasons.',
},
bottomSheets: {
confirmOnDeviceMessage: 'Go to your device and verify the details of the operation.',
Expand Down
2 changes: 1 addition & 1 deletion suite-native/module-connect-popup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@react-navigation/native": "6.1.18",
"@reduxjs/toolkit": "1.9.5",
"@suite-common/suite-types": "workspace:*",
"@suite-common/wallet-core": "workspace:*",
"@suite-native/atoms": "workspace:*",
Expand All @@ -20,7 +21,6 @@
"@suite-native/feature-flags": "workspace:^",
"@suite-native/intl": "workspace:^",
"@suite-native/navigation": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/connect-mobile": "workspace:*",
"expo-linking": "^7.0.5",
"react": "18.2.0",
Expand Down
7 changes: 7 additions & 0 deletions suite-native/module-connect-popup/redux.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AsyncThunkAction } from '@reduxjs/toolkit';

declare module 'redux' {
export interface Dispatch {
<TThunk extends AsyncThunkAction<any, any, any>>(thunk: TThunk): ReturnType<TThunk>;
}
}
49 changes: 0 additions & 49 deletions suite-native/module-connect-popup/src/hooks/useConnectMethod.ts

This file was deleted.

This file was deleted.

Loading

0 comments on commit a8996a8

Please sign in to comment.