Skip to content

Commit 75fbf41

Browse files
committed
feat(suite): add account selection to walletconnect proposal
1 parent 6a0dab8 commit 75fbf41

File tree

2 files changed

+166
-11
lines changed

2 files changed

+166
-11
lines changed

packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/WalletConnectProposalModal.tsx

+153-9
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,131 @@
1+
import { useEffect, useMemo, useState } from 'react';
2+
import { GroupBase } from 'react-select';
3+
4+
import { TrezorDevice } from '@suite-common/suite-types';
5+
import { AccountType, NetworkType } from '@suite-common/wallet-config';
6+
import { selectAccounts, selectDevices } from '@suite-common/wallet-core';
7+
import { Account } from '@suite-common/wallet-types';
18
import {
29
selectPendingProposal,
310
sessionProposalApproveThunk,
411
sessionProposalRejectThunk,
512
} from '@suite-common/walletconnect';
6-
import { Banner, Card, H2, NewModal, Note, Paragraph, Row, Text } from '@trezor/components';
13+
import { PendingConnectionProposalNetwork } from '@suite-common/walletconnect/src/walletConnectTypes';
14+
import {
15+
Banner,
16+
Card,
17+
H2,
18+
NewModal,
19+
Note,
20+
Option,
21+
Paragraph,
22+
Row,
23+
Select,
24+
Text,
25+
} from '@trezor/components';
726
import { BannerButton } from '@trezor/components/src/components/Banner/BannerButton';
827
import { CoinLogo } from '@trezor/product-components';
928
import { spacings } from '@trezor/theme';
1029

1130
import { onCancel } from 'src/actions/suite/modalActions';
1231
import { goto } from 'src/actions/suite/routerActions';
13-
import { Translation } from 'src/components/suite';
32+
import { AccountLabel, Translation, WalletLabeling } from 'src/components/suite';
33+
import { AccountTypeBadge } from 'src/components/suite/AccountTypeBadge';
1434
import { useDispatch, useSelector } from 'src/hooks/suite';
35+
import { selectAccountLabels } from 'src/reducers/suite/metadataReducer';
1536

1637
interface WalletConnectProposalModalProps {
1738
eventId: number;
1839
}
1940

41+
interface AccountGroup extends GroupBase<Account> {
42+
options: Account[];
43+
label: string;
44+
device: TrezorDevice;
45+
accountType: AccountType;
46+
networkType: NetworkType;
47+
}
48+
2049
export const WalletConnectProposalModal = ({ eventId }: WalletConnectProposalModalProps) => {
2150
const dispatch = useDispatch();
2251
const pendingProposal = useSelector(selectPendingProposal);
52+
const accounts = useSelector(selectAccounts);
53+
const accountLabels = useSelector(selectAccountLabels);
54+
const devices = useSelector(selectDevices);
55+
const [selectedDefaultAccounts, setSelectedDefaultAccounts] = useState<Account[]>([]);
2356

2457
const handleAccept = () => {
25-
dispatch(sessionProposalApproveThunk({ eventId }));
58+
dispatch(sessionProposalApproveThunk({ eventId, selectedDefaultAccounts }));
2659
dispatch(onCancel());
2760
};
2861
const handleReject = () => {
2962
dispatch(sessionProposalRejectThunk({ eventId }));
3063
dispatch(onCancel());
3164
};
65+
const handleSelectAccount = (account: Account) => {
66+
setSelectedDefaultAccounts(prev => {
67+
const newAccounts = prev.filter(prevAccount => prevAccount.symbol !== account.symbol);
68+
69+
return [...newAccounts, account];
70+
});
71+
};
72+
73+
const orderedAccounts = useMemo(
74+
() =>
75+
[...accounts]
76+
.filter(account => account.visible)
77+
.map(account => ({
78+
...account,
79+
accountLabel: accountLabels[account.key],
80+
}))
81+
.sort((a, b) => {
82+
if (a.deviceState !== b.deviceState) {
83+
return a.deviceState.localeCompare(b.deviceState);
84+
}
85+
if (a.accountType !== b.accountType) {
86+
// normal first
87+
if (a.accountType === 'normal' && b.accountType !== 'normal') return -1;
88+
if (a.accountType !== 'normal' && b.accountType === 'normal') return 1;
89+
90+
return a.accountType.localeCompare(b.accountType);
91+
}
92+
93+
return a.index - b.index;
94+
}),
95+
[accounts, accountLabels],
96+
);
97+
useEffect(() => {
98+
const newDefaultAccounts = pendingProposal?.networks
99+
.filter(network => network.status === 'active')
100+
.map(network => orderedAccounts.find(account => account.symbol === network.symbol))
101+
.filter(Boolean) as Account[];
102+
setSelectedDefaultAccounts(newDefaultAccounts);
103+
}, [orderedAccounts, pendingProposal?.networks]);
104+
105+
const buildAccountOptionGroups = (network: PendingConnectionProposalNetwork) => {
106+
const groups: AccountGroup[] = [];
107+
orderedAccounts
108+
.filter(account => account.symbol === network.symbol)
109+
.forEach(account => {
110+
const device = devices.find(d => d.state?.staticSessionId === account.deviceState);
111+
if (!device) return;
112+
const label = `${account.deviceState}-${account.accountType}`;
113+
const group = groups.find(g => g.label === label);
114+
if (group) {
115+
group.options.push(account);
116+
} else {
117+
groups.push({
118+
label,
119+
device,
120+
accountType: account.accountType,
121+
networkType: account.networkType,
122+
options: [account],
123+
});
124+
}
125+
});
126+
127+
return groups;
128+
};
32129

33130
if (!pendingProposal) return null;
34131

@@ -103,12 +200,59 @@ export const WalletConnectProposalModal = ({ eventId }: WalletConnectProposalMod
103200
size={24}
104201
/>
105202
)}
106-
<Text>
107-
{network.name}
108-
{network.required && (
109-
<Text variant="destructive">*</Text>
110-
)}
111-
</Text>
203+
{status === 'active' &&
204+
network.namespaceId.startsWith('solana') ? (
205+
<Select
206+
isSearchable={false}
207+
isClearable={false}
208+
isClean
209+
menuFitContent
210+
size="small"
211+
value={selectedDefaultAccounts.find(
212+
account => account.symbol === network.symbol,
213+
)}
214+
options={buildAccountOptionGroups(network)}
215+
formatGroupLabel={(data: GroupBase<Account>) => (
216+
<Row gap={spacings.xs}>
217+
<WalletLabeling
218+
device={(data as AccountGroup).device}
219+
shouldUseDeviceLabel
220+
/>
221+
<AccountTypeBadge
222+
accountType={
223+
(data as AccountGroup).accountType
224+
}
225+
networkType={
226+
(data as AccountGroup).networkType
227+
}
228+
size="small"
229+
onElevation
230+
/>
231+
</Row>
232+
)}
233+
formatOptionLabel={(account: Account) => (
234+
<AccountLabel
235+
key={account.descriptor}
236+
accountLabel={account.accountLabel}
237+
accountType={account.accountType}
238+
networkType={account.networkType}
239+
symbol={account.symbol}
240+
index={account.index}
241+
path={account.path}
242+
/>
243+
)}
244+
onChange={(option: Option) =>
245+
handleSelectAccount(option)
246+
}
247+
/>
248+
) : (
249+
<Text>
250+
{network.name}
251+
{network.required && (
252+
<Text variant="destructive">*</Text>
253+
)}
254+
</Text>
255+
)}
112256
</Row>
113257
))}
114258
</Row>

suite-common/walletconnect/src/walletConnectThunks.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,11 @@ export const sessionProposalApproveThunk = createThunk<
156156
void,
157157
{
158158
eventId: number;
159+
selectedDefaultAccounts: Account[];
159160
}
160161
>(
161162
`${WALLETCONNECT_MODULE}/sessionProposalApproveThunk`,
162-
async ({ eventId }, { dispatch, getState }) => {
163+
async ({ eventId, selectedDefaultAccounts }, { dispatch, getState }) => {
163164
try {
164165
const pendingProposal = selectPendingProposal(getState());
165166
if (
@@ -171,7 +172,17 @@ export const sessionProposalApproveThunk = createThunk<
171172
}
172173

173174
const accounts = selectAccounts(getState());
174-
const supportedNamespaces = getNamespaces(accounts);
175+
const accountsInPreferentialOrder = [...selectedDefaultAccounts];
176+
accounts.forEach(account => {
177+
if (
178+
!accountsInPreferentialOrder.some(
179+
a => a.descriptor === account.descriptor && a.symbol === account.symbol,
180+
)
181+
) {
182+
accountsInPreferentialOrder.push(account);
183+
}
184+
});
185+
const supportedNamespaces = getNamespaces(accountsInPreferentialOrder);
175186
const approvedNamespaces = buildApprovedNamespaces({
176187
proposal: pendingProposal.params,
177188
supportedNamespaces,

0 commit comments

Comments
 (0)