Skip to content

Commit

Permalink
feat(suite-native): add walletconnect
Browse files Browse the repository at this point in the history
  • Loading branch information
martykan committed Feb 11, 2025
1 parent a8996a8 commit 33b746f
Show file tree
Hide file tree
Showing 23 changed files with 462 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@ scroll-into-view-if-needed
@reown/walletkit
@walletconnect/core
@walletconnect/utils
@walletconnect/react-native-compat
5 changes: 5 additions & 0 deletions scripts/list-outdated-dependencies/mobile-dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,8 @@ fantasticon
@whatwg-node/events
abortcontroller-polyfill
event-target-shim
@react-native-async-storage/async-storage
@types/fast-text-encoding
@types/react-native-get-random-values
fast-text-encoding
react-native-get-random-values
7 changes: 7 additions & 0 deletions suite-native/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dependencies": {
"@gorhom/bottom-sheet": "5.0.5",
"@mobily/ts-belt": "^3.13.1",
"@react-native-async-storage/async-storage": "^2.1.1",
"@react-native-community/netinfo": "^11.4.1",
"@react-native/metro-config": "^0.76.1",
"@react-navigation/bottom-tabs": "6.6.1",
Expand All @@ -42,6 +43,7 @@
"@suite-common/token-definitions": "workspace:*",
"@suite-common/wallet-core": "workspace:*",
"@suite-common/wallet-types": "workspace:*",
"@suite-common/walletconnect": "workspace:*",
"@suite-native/accounts": "workspace:*",
"@suite-native/alerts": "workspace:*",
"@suite-native/analytics": "workspace:*",
Expand Down Expand Up @@ -84,6 +86,7 @@
"@trezor/styles": "workspace:*",
"@trezor/theme": "workspace:*",
"@trezor/trezor-user-env-link": "workspace:*",
"@walletconnect/react-native-compat": "^2.18.0",
"@whatwg-node/events": "0.1.2",
"abortcontroller-polyfill": "1.7.6",
"buffer": "^6.0.3",
Expand All @@ -105,13 +108,15 @@
"expo-system-ui": "^4.0.2",
"expo-updates": "0.26.6",
"expo-video": "^2.0.1",
"fast-text-encoding": "^1.0.6",
"lottie-react-native": "^7.1.0",
"node-libs-browser": "^2.2.1",
"react": "18.2.0",
"react-intl": "^6.6.8",
"react-native": "0.76.1",
"react-native-edge-to-edge": "^1.3.1",
"react-native-gesture-handler": "^2.21.0",
"react-native-get-random-values": "^1.11.0",
"react-native-keyboard-aware-scroll-view": "0.9.5",
"react-native-mmkv": "2.12.2",
"react-native-quick-crypto": "^0.7.6",
Expand All @@ -133,8 +138,10 @@
"@react-native/babel-preset": "^0.75.2",
"@suite-common/test-utils": "workspace:^",
"@trezor/connect-mobile": "workspace:^",
"@types/fast-text-encoding": "^1",
"@types/jest": "^29.5.12",
"@types/node": "22.10.1",
"@types/react-native-get-random-values": "^1",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"babel-plugin-transform-remove-console": "^6.9.4",
"detox": "^20.25.6",
Expand Down
6 changes: 6 additions & 0 deletions suite-native/app/src/initActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
initStakeDataThunk,
periodicFetchFiatRatesThunk,
} from '@suite-common/wallet-core';
import { walletConnectInitThunk } from '@suite-common/walletconnect';
import { initAnalyticsThunk } from '@suite-native/analytics';
import { FeatureFlag, selectIsFeatureFlagEnabled } from '@suite-native/feature-flags';
import { selectFiatCurrencyCode } from '@suite-native/settings';
import { setIsAppReady, setIsConnectInitialized } from '@suite-native/state/src/appSlice';

Expand Down Expand Up @@ -49,6 +51,10 @@ export const applicationInit = createThunk(

// Create Portfolio Tracker device if it doesn't exist
dispatch(createImportedDeviceThunk());

if (selectIsFeatureFlagEnabled(getState(), FeatureFlag.IsWalletConnectEnabled)) {
dispatch(walletConnectInitThunk());
}
} catch (error) {
console.error(error);
} finally {
Expand Down
14 changes: 13 additions & 1 deletion suite-native/app/src/navigation/RootStackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
import { AddCoinAccountStackNavigator } from '@suite-native/module-add-accounts';
import { DeviceCompromisedModalScreen } from '@suite-native/module-authenticity-checks';
import { AuthorizeDeviceStackNavigator } from '@suite-native/module-authorize-device';
import { ConnectPopupScreen } from '@suite-native/module-connect-popup';
import {
ConnectPopupScreen,
WalletConnectPairScreen,
WalletConnectSessionPopupScreen,
} from '@suite-native/module-connect-popup';
import { DevUtilsStackNavigator } from '@suite-native/module-dev-utils';
import { DeviceSettingsStackNavigator } from '@suite-native/module-device-settings';
import { OnboardingStackNavigator } from '@suite-native/module-onboarding';
Expand Down Expand Up @@ -81,6 +85,14 @@ export const RootStackNavigator = () => {
/>
<RootStack.Screen name={RootStackRoutes.SendStack} component={SendStackNavigator} />
<RootStack.Screen name={RootStackRoutes.ConnectPopup} component={ConnectPopupScreen} />
<RootStack.Screen
name={RootStackRoutes.WalletConnectSessionPopup}
component={WalletConnectSessionPopupScreen}
/>
<RootStack.Screen
name={RootStackRoutes.WalletConnectPair}
component={WalletConnectPairScreen}
/>
<RootStack.Screen
name={RootStackRoutes.SettingsScreenStack}
component={SettingsStackNavigator}
Expand Down
3 changes: 3 additions & 0 deletions suite-native/app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
{
"path": "../../suite-common/wallet-types"
},
{
"path": "../../suite-common/walletconnect"
},
{ "path": "../accounts" },
{ "path": "../alerts" },
{ "path": "../analytics" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('featureFlagsSlice', () => {
isTradingEnabled: true,
isDeviceOnboardingEnabled: true,
IsFwRevisionCheckEnabled: true,
isWalletConnectEnabled: true,
});
});

Expand Down Expand Up @@ -56,6 +57,7 @@ describe('featureFlagsSlice', () => {
isTradingEnabled: false,
isDeviceOnboardingEnabled: false,
IsFwRevisionCheckEnabled: false,
isWalletConnectEnabled: false,
});
});

Expand Down Expand Up @@ -83,6 +85,7 @@ describe('featureFlagsSlice', () => {
isTradingEnabled: false,
isDeviceOnboardingEnabled: false,
IsFwRevisionCheckEnabled: false,
isWalletConnectEnabled: false,
});
});
});
Expand Down
3 changes: 3 additions & 0 deletions suite-native/feature-flags/src/featureFlagsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const FeatureFlag = {
IsDeviceOnboardingEnabled: 'isDeviceOnboardingEnabled',
IsTradingEnabled: 'isTradingEnabled',
IsFwRevisionCheckEnabled: 'IsFwRevisionCheckEnabled',
IsWalletConnectEnabled: 'isWalletConnectEnabled',
} as const;

export type FeatureFlag = (typeof FeatureFlag)[keyof typeof FeatureFlag];
Expand All @@ -31,6 +32,7 @@ export const featureFlagsInitialState: FeatureFlagsState = {
[FeatureFlag.IsDeviceOnboardingEnabled]: isDebugEnv() && !isDetoxTestBuild(),
[FeatureFlag.IsTradingEnabled]: isDebugEnv(),
[FeatureFlag.IsFwRevisionCheckEnabled]: isDevelopOrDebugEnv(),
[FeatureFlag.IsWalletConnectEnabled]: isDevelopOrDebugEnv(),
};

export const featureFlagsPersistedKeys: Array<keyof FeatureFlagsState> = [
Expand All @@ -42,6 +44,7 @@ export const featureFlagsPersistedKeys: Array<keyof FeatureFlagsState> = [
FeatureFlag.IsDeviceOnboardingEnabled,
FeatureFlag.IsTradingEnabled,
FeatureFlag.IsFwRevisionCheckEnabled,
FeatureFlag.IsWalletConnectEnabled,
];

export const featureFlagsSlice = createSlice({
Expand Down
28 changes: 28 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export const en = {
title: 'Trezor Connect Mobile',
callback: 'Callback',
confirm: 'Confirm',
cancel: 'Cancel',
areYouSureMessage: 'Are you sure you want to continue?\nMake sure you trust the source.',
connectionStatus: {
loading: 'Loading...',
Expand All @@ -303,6 +304,29 @@ export const en = {
bottomSheets: {
confirmOnDeviceMessage: 'Go to your device and verify the details of the operation.',
},
walletConnect: {
title: 'WalletConnect',
message:
'An external app is trying to connect to your Trezor Suite. Make sure you trust the source!',
connect: 'Connect',
pairingUrl: 'Enter pairing URL',
scanQR: 'Scan QR code',
activeConnections: 'Active connections',
noActiveConnections: 'No active connections',
disconnect: 'Disconnect',
serviceStatus: {
verified: 'Verified',
unknown: 'Unknown',
dangerous: 'Dangerous',
},
errors: {
requestExpired:
'Request has expired. Please go back to the application and try again.',
isScam: 'The request was detected as a scam and was blocked automatically.',
unableToVerify:
'We were unable to verify the request authenticity. Please make sure you trust the source.',
},
},
},
moduleDevice: {
incompatibleFirmwareModalAppendix: {
Expand Down Expand Up @@ -608,6 +632,10 @@ export const en = {
title: 'Device checks',
subtitle: 'Authenticity and security checks',
},
walletConnect: {
title: 'WalletConnect',
subtitle: 'Use external apps using the WalletConnect protocol',
},
},
support: {
title: 'Support',
Expand Down
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 @@ -14,6 +14,7 @@
"@reduxjs/toolkit": "1.9.5",
"@suite-common/suite-types": "workspace:*",
"@suite-common/wallet-core": "workspace:*",
"@suite-common/walletconnect": "workspace:*",
"@suite-native/atoms": "workspace:*",
"@suite-native/config": "workspace:*",
"@suite-native/device": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNavigation } from '@react-navigation/native';
import * as Linking from 'expo-linking';

import { connectPopupDeeplinkThunk, selectConnectPopupCall } from '@suite-common/connect-popup';
import { selectPendingProposal, walletConnectPairThunk } from '@suite-common/walletconnect';
import { isDevelopOrDebugEnv } from '@suite-native/config';
import { FeatureFlag, useFeatureFlag } from '@suite-native/feature-flags';
import {
Expand All @@ -30,26 +31,38 @@ const isConnectPopupUrl = (url: string): boolean => {
return false;
};

const isWalletConnectUrl = (url: string): boolean =>
url.startsWith('trezorsuitelite://walletconnect');

// TODO: will be necessary to handle if device is not connected/unlocked so we probably want to wait until user unlock device
// we already have some modals like biometrics or coin enabled which are waiting for device to be connected
export const useConnectPopupNavigation = () => {
const featureFlagEnabled = useFeatureFlag(FeatureFlag.IsConnectPopupEnabled);
const featureFlagWalletConnectEnabled = useFeatureFlag(FeatureFlag.IsWalletConnectEnabled);
const navigation = useNavigation<NavigationProp>();
const dispatch = useDispatch();
const connectPopupCall = useSelector(selectConnectPopupCall);
const walletConnectProposal = useSelector(selectPendingProposal);

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

useEffect(() => {
if (!featureFlagEnabled) return;
if (!url || !isConnectPopupUrl(url)) return;
dispatch(connectPopupDeeplinkThunk({ url }));
}, [url, featureFlagEnabled, dispatch]);
if (!featureFlagEnabled || !url) return;

if (isConnectPopupUrl(url)) {
dispatch(connectPopupDeeplinkThunk({ url }));
} else if (featureFlagWalletConnectEnabled && isWalletConnectUrl(url)) {
dispatch(walletConnectPairThunk({ uri: url }));
}
}, [url, featureFlagEnabled, featureFlagWalletConnectEnabled, dispatch]);

useEffect(() => {
if (connectPopupCall) {
navigation.navigate(RootStackRoutes.ConnectPopup);
}
}, [connectPopupCall, navigation]);
if (walletConnectProposal && !walletConnectProposal.expired) {
navigation.navigate(RootStackRoutes.WalletConnectSessionPopup);
}
}, [connectPopupCall, walletConnectProposal, navigation]);
};
2 changes: 2 additions & 0 deletions suite-native/module-connect-popup/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './screens/ConnectPopupScreen';
export * from './screens/WalletConnectSessionPopupScreen';
export * from './screens/WalletConnectPairScreen';
export * from './hooks/useConnectPopupNavigation';
export * from './hooks/useIsConnectPopupOpened';
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import {
selectSessions,
walletConnectDisconnectThunk,
walletConnectPairThunk,
} from '@suite-common/walletconnect';
import { Button, Divider, HStack, IconButton, Input, Text, VStack } from '@suite-native/atoms';
import { Translation } from '@suite-native/intl';
import { Screen, ScreenHeader } from '@suite-native/navigation';
import { ScanQRBottomSheet } from '@suite-native/qr-code';

export const WalletConnectPairScreen = () => {
const dispatch = useDispatch();
const sessions = useSelector(selectSessions);

const [uri, setUri] = useState('');
const [qrVisible, setQrVisible] = useState(false);

const handlePair = () => {
dispatch(walletConnectPairThunk({ uri }));
setUri('');
};
const handleQr = () => {
setQrVisible(true);
};

return (
<Screen
header={
<ScreenHeader
closeActionType="close"
content={
<Text>
<Translation id="moduleConnectPopup.walletConnect.title" />
</Text>
}
/>
}
>
<VStack spacing="sp24">
<VStack>
<Input
value={uri}
onChangeText={setUri}
placeholder="Connection URI..."
rightIcon={
<IconButton
iconName="qrCode"
onPress={handleQr}
colorScheme="tertiaryElevation0"
/>
}
/>
<Button colorScheme="primary" onPress={handlePair}>
<Translation id="moduleConnectPopup.walletConnect.connect" />
</Button>
</VStack>

<Divider />

<VStack>
<Text variant="highlight">
<Translation id="moduleConnectPopup.walletConnect.activeConnections" />:
</Text>
{sessions.map(session => (
<HStack key={session.topic} spacing="sp12">
<VStack flex={1} spacing="sp1">
<Text>{session.peer.metadata.name}</Text>
<Text color="textSubdued">{session.peer.metadata.url}</Text>
<Text color="textSubdued" numberOfLines={1}>
{session.topic}
</Text>
</VStack>
<VStack>
<Button
onPress={() => {
dispatch(
walletConnectDisconnectThunk({ topic: session.topic }),
);
}}
size="small"
colorScheme="redElevation0"
>
<Translation id="moduleConnectPopup.walletConnect.disconnect" />
</Button>
</VStack>
</HStack>
))}
{sessions.length === 0 && (
<Text>
<Translation id="moduleConnectPopup.walletConnect.noActiveConnections" />
</Text>
)}
</VStack>
</VStack>

<ScanQRBottomSheet
title={<Translation id="moduleConnectPopup.walletConnect.title" />}
isVisible={qrVisible}
onCodeScanned={setUri}
onClose={() => setQrVisible(false)}
/>
</Screen>
);
};
Loading

0 comments on commit 33b746f

Please sign in to comment.