Skip to content

Commit e9d67b5

Browse files
committed
feat(suite-native): add walletconnect
1 parent 6aaa59b commit e9d67b5

29 files changed

+588
-329
lines changed

scripts/list-outdated-dependencies/connect-dependencies.txt

+1
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,4 @@ scroll-into-view-if-needed
9393
@reown/walletkit
9494
@walletconnect/core
9595
@walletconnect/utils
96+
@walletconnect/react-native-compat

scripts/list-outdated-dependencies/mobile-dependencies.txt

+5
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,8 @@ fantasticon
7777
@whatwg-node/events
7878
abortcontroller-polyfill
7979
event-target-shim
80+
@react-native-async-storage/async-storage
81+
@types/fast-text-encoding
82+
@types/react-native-get-random-values
83+
fast-text-encoding
84+
react-native-get-random-values

suite-common/connect-popup/src/connectPopupActions.ts

+3
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ const initiateCall = createAction(`${ACTION_PREFIX}/initiateCall`, (payload: Con
1010

1111
const approveCall = createAction(`${ACTION_PREFIX}/approveCall`);
1212

13+
const finishCall = createAction(`${ACTION_PREFIX}/finishCall`);
14+
1315
const rejectCall = createAction(`${ACTION_PREFIX}/rejectCall`, (payload: Error) => ({
1416
payload,
1517
}));
1618

1719
export const connectPopupActions = {
1820
initiateCall,
1921
approveCall,
22+
finishCall,
2023
rejectCall,
2124
} as const;

suite-common/connect-popup/src/connectPopupReducer.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export const prepareConnectPopupReducer = createReducerWithExtraDeps(
2222
.addCase(connectPopupActions.initiateCall, (state, { payload }) => {
2323
state.activeCall = payload;
2424
})
25+
.addCase(connectPopupActions.finishCall, state => {
26+
state.activeCall = { state: 'finished' };
27+
})
2528
.addCase(connectPopupActions.approveCall, state => {
2629
if (state.activeCall?.state === 'request') state.activeCall.confirmation.resolve();
2730
state.activeCall = undefined;

suite-common/connect-popup/src/connectPopupThunks.ts

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export const connectPopupCallThunkInner = createThunk<
8989
// Note: for mobile this needs to be called explicitly, on desktop it's automatically handled by middleware
9090
dispatch(deviceActions.removeButtonRequests({ device }));
9191

92+
dispatch(connectPopupActions.finishCall());
93+
9294
return response;
9395
} catch (error) {
9496
console.error('connectPopupCallThunk', error);

suite-common/connect-popup/src/connectPopupTypes.ts

+3
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ export type ConnectPopupCall =
1818
| {
1919
state: 'deeplink-callback';
2020
callbackUrl: string;
21+
}
22+
| {
23+
state: 'finished';
2124
};

suite-common/walletconnect/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
},
1212
"dependencies": {
1313
"@reduxjs/toolkit": "1.9.5",
14-
"@reown/walletkit": "^1.1.1",
14+
"@reown/walletkit": "^1.2.1",
1515
"@suite-common/connect-popup": "workspace:*",
1616
"@suite-common/redux-utils": "workspace:*",
1717
"@suite-common/wallet-config": "workspace:*",
1818
"@suite-common/wallet-core": "workspace:*",
1919
"@suite-common/wallet-types": "workspace:*",
2020
"@trezor/connect": "workspace:*",
21-
"@walletconnect/core": "^2.17.2",
22-
"@walletconnect/utils": "^2.17.2"
21+
"@walletconnect/core": "^2.18.1",
22+
"@walletconnect/utils": "^2.18.1"
2323
}
2424
}

suite-native/app/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"dependencies": {
2424
"@gorhom/bottom-sheet": "5.0.5",
2525
"@mobily/ts-belt": "^3.13.1",
26+
"@react-native-async-storage/async-storage": "^2.1.1",
2627
"@react-native-community/netinfo": "^11.4.1",
2728
"@react-native/metro-config": "^0.76.1",
2829
"@react-navigation/bottom-tabs": "6.6.1",
@@ -42,6 +43,7 @@
4243
"@suite-common/token-definitions": "workspace:*",
4344
"@suite-common/wallet-core": "workspace:*",
4445
"@suite-common/wallet-types": "workspace:*",
46+
"@suite-common/walletconnect": "workspace:*",
4547
"@suite-native/accounts": "workspace:*",
4648
"@suite-native/alerts": "workspace:*",
4749
"@suite-native/analytics": "workspace:*",
@@ -85,6 +87,7 @@
8587
"@trezor/styles": "workspace:*",
8688
"@trezor/theme": "workspace:*",
8789
"@trezor/trezor-user-env-link": "workspace:*",
90+
"@walletconnect/react-native-compat": "^2.18.1",
8891
"@whatwg-node/events": "0.1.2",
8992
"abortcontroller-polyfill": "1.7.6",
9093
"buffer": "^6.0.3",
@@ -106,6 +109,7 @@
106109
"expo-system-ui": "^4.0.2",
107110
"expo-updates": "0.26.6",
108111
"expo-video": "^2.0.1",
112+
"fast-text-encoding": "^1.0.6",
109113
"jotai": "1.9.1",
110114
"lottie-react-native": "^7.1.0",
111115
"node-libs-browser": "^2.2.1",
@@ -114,6 +118,7 @@
114118
"react-native": "0.76.1",
115119
"react-native-edge-to-edge": "^1.3.1",
116120
"react-native-gesture-handler": "^2.21.0",
121+
"react-native-get-random-values": "^1.11.0",
117122
"react-native-keyboard-controller": "1.16.5",
118123
"react-native-mmkv": "2.12.2",
119124
"react-native-quick-crypto": "^0.7.6",
@@ -135,8 +140,10 @@
135140
"@react-native/babel-preset": "^0.75.2",
136141
"@suite-common/test-utils": "workspace:^",
137142
"@trezor/connect-mobile": "workspace:^",
143+
"@types/fast-text-encoding": "^1",
138144
"@types/jest": "^29.5.12",
139145
"@types/node": "22.10.1",
146+
"@types/react-native-get-random-values": "^1",
140147
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
141148
"babel-plugin-transform-remove-console": "^6.9.4",
142149
"detox": "^20.34.4",

suite-native/app/src/initActions.ts

+6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
initStakeDataThunk,
1010
periodicFetchFiatRatesThunk,
1111
} from '@suite-common/wallet-core';
12+
import { walletConnectInitThunk } from '@suite-common/walletconnect';
1213
import { initAnalyticsThunk } from '@suite-native/analytics';
14+
import { FeatureFlag, selectIsFeatureFlagEnabled } from '@suite-native/feature-flags';
1315
import { selectFiatCurrencyCode } from '@suite-native/settings';
1416
import { setIsAppReady, setIsConnectInitialized } from '@suite-native/state/src/appSlice';
1517

@@ -49,6 +51,10 @@ export const applicationInit = createThunk(
4951

5052
// Create Portfolio Tracker device if it doesn't exist
5153
dispatch(createImportedDeviceThunk());
54+
55+
if (selectIsFeatureFlagEnabled(getState(), FeatureFlag.IsWalletConnectEnabled)) {
56+
dispatch(walletConnectInitThunk());
57+
}
5258
} catch (error) {
5359
console.error(error);
5460
} finally {

suite-native/app/src/navigation/RootStackNavigator.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
import { AddCoinAccountStackNavigator } from '@suite-native/module-add-accounts';
1212
import { DeviceCompromisedModalScreen } from '@suite-native/module-authenticity-checks';
1313
import { AuthorizeDeviceStackNavigator } from '@suite-native/module-authorize-device';
14-
import { ConnectPopupScreen } from '@suite-native/module-connect-popup';
14+
import {
15+
ConnectPopupScreen,
16+
WalletConnectPairScreen,
17+
WalletConnectSessionPopupScreen,
18+
} from '@suite-native/module-connect-popup';
1519
import { DevUtilsStackNavigator } from '@suite-native/module-dev-utils';
1620
import { DeviceSettingsStackNavigator } from '@suite-native/module-device-settings';
1721
import { OnboardingStackNavigator } from '@suite-native/module-onboarding';
@@ -80,6 +84,14 @@ export const RootStackNavigator = () => {
8084
component={DevUtilsStackNavigator}
8185
/>
8286
<RootStack.Screen name={RootStackRoutes.ConnectPopup} component={ConnectPopupScreen} />
87+
<RootStack.Screen
88+
name={RootStackRoutes.WalletConnectSessionPopup}
89+
component={WalletConnectSessionPopupScreen}
90+
/>
91+
<RootStack.Screen
92+
name={RootStackRoutes.WalletConnectPair}
93+
component={WalletConnectPairScreen}
94+
/>
8395
<RootStack.Screen
8496
name={RootStackRoutes.SettingsScreenStack}
8597
component={SettingsStackNavigator}

suite-native/app/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
{
3030
"path": "../../suite-common/wallet-types"
3131
},
32+
{
33+
"path": "../../suite-common/walletconnect"
34+
},
3235
{ "path": "../accounts" },
3336
{ "path": "../alerts" },
3437
{ "path": "../analytics" },

suite-native/feature-flags/src/__tests__/featureFlagsSlice.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('featureFlagsSlice', () => {
2828
isTradingEnabled: true,
2929
isDeviceOnboardingEnabled: true,
3030
IsFwRevisionCheckEnabled: true,
31+
isWalletConnectEnabled: true,
3132
});
3233
});
3334

@@ -54,6 +55,7 @@ describe('featureFlagsSlice', () => {
5455
isTradingEnabled: false,
5556
isDeviceOnboardingEnabled: false,
5657
IsFwRevisionCheckEnabled: false,
58+
isWalletConnectEnabled: false,
5759
});
5860
});
5961

@@ -80,6 +82,7 @@ describe('featureFlagsSlice', () => {
8082
isTradingEnabled: false,
8183
isDeviceOnboardingEnabled: false,
8284
IsFwRevisionCheckEnabled: false,
85+
isWalletConnectEnabled: false,
8386
});
8487
});
8588
});

suite-native/feature-flags/src/featureFlagsSlice.ts

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const FeatureFlag = {
1111
IsDeviceOnboardingEnabled: 'isDeviceOnboardingEnabled',
1212
IsTradingEnabled: 'isTradingEnabled',
1313
IsFwRevisionCheckEnabled: 'IsFwRevisionCheckEnabled',
14+
IsWalletConnectEnabled: 'isWalletConnectEnabled',
1415
} as const;
1516

1617
export type FeatureFlag = (typeof FeatureFlag)[keyof typeof FeatureFlag];
@@ -29,6 +30,7 @@ export const featureFlagsInitialState: FeatureFlagsState = {
2930
[FeatureFlag.IsDeviceOnboardingEnabled]: isDebugEnv() && !isDetoxTestBuild(),
3031
[FeatureFlag.IsTradingEnabled]: isDebugEnv(),
3132
[FeatureFlag.IsFwRevisionCheckEnabled]: isDevelopOrDebugEnv(),
33+
[FeatureFlag.IsWalletConnectEnabled]: isDevelopOrDebugEnv(),
3234
};
3335

3436
export const featureFlagsPersistedKeys: Array<keyof FeatureFlagsState> = [
@@ -39,6 +41,7 @@ export const featureFlagsPersistedKeys: Array<keyof FeatureFlagsState> = [
3941
FeatureFlag.IsDeviceOnboardingEnabled,
4042
FeatureFlag.IsTradingEnabled,
4143
FeatureFlag.IsFwRevisionCheckEnabled,
44+
FeatureFlag.IsWalletConnectEnabled,
4245
];
4346

4447
export const featureFlagsSlice = createSlice({

suite-native/intl/src/en.ts

+28
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ export const en = {
307307
title: 'Trezor Connect Mobile',
308308
callback: 'Callback',
309309
confirm: 'Confirm',
310+
cancel: 'Cancel',
310311
areYouSureMessage: 'Are you sure you want to continue?\nMake sure you trust the source.',
311312
connectionStatus: {
312313
loading: 'Loading...',
@@ -322,6 +323,29 @@ export const en = {
322323
bottomSheets: {
323324
confirmOnDeviceMessage: 'Go to your device and verify the details of the operation.',
324325
},
326+
walletConnect: {
327+
title: 'WalletConnect',
328+
message:
329+
'An external app is trying to connect to your Trezor Suite. Make sure you trust the source!',
330+
connect: 'Connect',
331+
pairingUrl: 'Enter pairing URL',
332+
scanQR: 'Scan QR code',
333+
activeConnections: 'Active connections',
334+
noActiveConnections: 'No active connections',
335+
disconnect: 'Disconnect',
336+
serviceStatus: {
337+
verified: 'Verified',
338+
unknown: 'Unknown',
339+
dangerous: 'Dangerous',
340+
},
341+
errors: {
342+
requestExpired:
343+
'Request has expired. Please go back to the application and try again.',
344+
isScam: 'The request was detected as a scam and was blocked automatically.',
345+
unableToVerify:
346+
'We were unable to verify the request authenticity. Please make sure you trust the source.',
347+
},
348+
},
325349
},
326350
moduleDevice: {
327351
incompatibleFirmwareModalAppendix: {
@@ -556,6 +580,10 @@ export const en = {
556580
title: 'Device checks',
557581
subtitle: 'Authenticity and security checks',
558582
},
583+
walletConnect: {
584+
title: 'WalletConnect',
585+
subtitle: 'Use external apps using the WalletConnect protocol',
586+
},
559587
},
560588
support: {
561589
title: 'Support',

suite-native/module-connect-popup/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@reduxjs/toolkit": "1.9.5",
1515
"@suite-common/suite-types": "workspace:*",
1616
"@suite-common/wallet-core": "workspace:*",
17+
"@suite-common/walletconnect": "workspace:*",
1718
"@suite-native/atoms": "workspace:*",
1819
"@suite-native/config": "workspace:*",
1920
"@suite-native/device": "workspace:*",

suite-native/module-connect-popup/src/hooks/useConnectPopupNavigation.ts

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useRef } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
33

44
import { useNavigation } from '@react-navigation/native';
55
import * as Linking from 'expo-linking';
66

77
import { connectPopupDeeplinkThunk, selectConnectPopupCall } from '@suite-common/connect-popup';
8+
import { selectPendingProposal, walletConnectPairThunk } from '@suite-common/walletconnect';
89
import { isDevelopOrDebugEnv } from '@suite-native/config';
910
import { FeatureFlag, useFeatureFlag } from '@suite-native/feature-flags';
11+
import { useOpenLink } from '@suite-native/link';
1012
import {
1113
RootStackParamList,
1214
RootStackRoutes,
@@ -30,26 +32,51 @@ const isConnectPopupUrl = (url: string): boolean => {
3032
return false;
3133
};
3234

35+
const isWalletConnectUrl = (url: string): boolean =>
36+
url.startsWith('trezorsuitelite://walletconnect');
37+
3338
// TODO: will be necessary to handle if device is not connected/unlocked so we probably want to wait until user unlock device
3439
// we already have some modals like biometrics or coin enabled which are waiting for device to be connected
3540
export const useConnectPopupNavigation = () => {
3641
const featureFlagEnabled = useFeatureFlag(FeatureFlag.IsConnectPopupEnabled);
42+
const featureFlagWalletConnectEnabled = useFeatureFlag(FeatureFlag.IsWalletConnectEnabled);
3743
const navigation = useNavigation<NavigationProp>();
3844
const dispatch = useDispatch();
3945
const connectPopupCall = useSelector(selectConnectPopupCall);
46+
const walletConnectProposal = useSelector(selectPendingProposal);
47+
const lastProposalId = useRef<number | null>(null);
48+
const openLink = useOpenLink();
4049

4150
// Handle deeplink
4251
const url = Linking.useURL();
4352

4453
useEffect(() => {
45-
if (!featureFlagEnabled) return;
46-
if (!url || !isConnectPopupUrl(url)) return;
47-
dispatch(connectPopupDeeplinkThunk({ url }));
48-
}, [url, featureFlagEnabled, dispatch]);
54+
if (!featureFlagEnabled || !url) return;
55+
56+
if (isConnectPopupUrl(url)) {
57+
dispatch(connectPopupDeeplinkThunk({ url }));
58+
} else if (featureFlagWalletConnectEnabled && isWalletConnectUrl(url)) {
59+
const parsedUrl = new URL(url);
60+
const wcUri = parsedUrl?.searchParams?.get('uri');
61+
if (wcUri) dispatch(walletConnectPairThunk({ uri: wcUri }));
62+
}
63+
}, [url, featureFlagEnabled, featureFlagWalletConnectEnabled, dispatch]);
4964

5065
useEffect(() => {
51-
if (connectPopupCall) {
66+
if (connectPopupCall?.state === 'deeplink-callback') {
67+
openLink(connectPopupCall.callbackUrl);
68+
} else if (connectPopupCall) {
5269
navigation.navigate(RootStackRoutes.ConnectPopup);
5370
}
54-
}, [connectPopupCall, navigation]);
71+
}, [connectPopupCall, navigation, openLink]);
72+
useEffect(() => {
73+
if (
74+
walletConnectProposal &&
75+
!walletConnectProposal.expired &&
76+
walletConnectProposal.eventId !== lastProposalId.current
77+
) {
78+
lastProposalId.current = walletConnectProposal.eventId;
79+
navigation.navigate(RootStackRoutes.WalletConnectSessionPopup);
80+
}
81+
}, [walletConnectProposal, navigation]);
5582
};
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './screens/ConnectPopupScreen';
2+
export * from './screens/WalletConnectSessionPopupScreen';
3+
export * from './screens/WalletConnectPairScreen';
24
export * from './hooks/useConnectPopupNavigation';
35
export * from './hooks/useIsConnectPopupOpened';

suite-native/module-connect-popup/src/screens/ConnectPopupScreen.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
33

44
import { useNavigation } from '@react-navigation/native';
5-
import * as Linking from 'expo-linking';
65

76
import { connectPopupActions, selectConnectPopupCall } from '@suite-common/connect-popup';
87
import {
@@ -30,11 +29,8 @@ export const ConnectPopupScreen = () => {
3029
const [showDebug, setShowDebug] = useState<boolean>(false);
3130

3231
useEffect(() => {
33-
if (popupCall?.state === 'deeplink-callback') {
34-
Linking.openURL(popupCall.callbackUrl);
35-
if (navigation.canGoBack()) {
36-
navigation.goBack();
37-
}
32+
if (popupCall?.state == 'finished' && navigation.canGoBack()) {
33+
navigation.goBack();
3834
}
3935
}, [popupCall, navigation]);
4036

0 commit comments

Comments
 (0)