Skip to content

Commit 939fb8c

Browse files
committed
feat(suite): add account selection to walletconnect proposal
1 parent 898adbf commit 939fb8c

File tree

2 files changed

+158
-11
lines changed

2 files changed

+158
-11
lines changed

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

+145-9
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,125 @@
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';
1535

1636
interface WalletConnectProposalModalProps {
1737
eventId: number;
1838
}
1939

40+
interface AccountGroup extends GroupBase<Account> {
41+
options: Account[];
42+
label: string;
43+
device: TrezorDevice;
44+
accountType: AccountType;
45+
networkType: NetworkType;
46+
}
47+
2048
export const WalletConnectProposalModal = ({ eventId }: WalletConnectProposalModalProps) => {
2149
const dispatch = useDispatch();
2250
const pendingProposal = useSelector(selectPendingProposal);
51+
const accounts = useSelector(selectAccounts);
52+
const devices = useSelector(selectDevices);
53+
const [selectedDefaultAccounts, setSelectedDefaultAccounts] = useState<Account[]>([]);
2354

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

33124
if (!pendingProposal) return null;
34125

@@ -103,12 +194,57 @@ export const WalletConnectProposalModal = ({ eventId }: WalletConnectProposalMod
103194
size={24}
104195
/>
105196
)}
106-
<Text>
107-
{network.name}
108-
{network.required && (
109-
<Text variant="destructive">*</Text>
110-
)}
111-
</Text>
197+
{status === 'active' &&
198+
network.namespaceId.startsWith('solana') ? (
199+
<Select
200+
isSearchable={false}
201+
isClearable={false}
202+
isClean
203+
menuFitContent
204+
size="small"
205+
value={selectedDefaultAccounts.find(
206+
account => account.symbol === network.symbol,
207+
)}
208+
options={buildAccountOptionGroups(network)}
209+
formatGroupLabel={(data: GroupBase<Account>) => (
210+
<Row gap={spacings.xs}>
211+
<WalletLabeling
212+
device={(data as AccountGroup).device}
213+
/>
214+
<AccountTypeBadge
215+
accountType={
216+
(data as AccountGroup).accountType
217+
}
218+
networkType={
219+
(data as AccountGroup).networkType
220+
}
221+
size="small"
222+
onElevation
223+
/>
224+
</Row>
225+
)}
226+
formatOptionLabel={(account: Account) => (
227+
<AccountLabel
228+
key={account.descriptor}
229+
accountLabel={account.accountLabel}
230+
accountType={account.accountType}
231+
networkType={account.networkType}
232+
symbol={account.symbol}
233+
index={account.index}
234+
/>
235+
)}
236+
onChange={(option: Option) =>
237+
handleSelectAccount(option)
238+
}
239+
/>
240+
) : (
241+
<Text>
242+
{network.name}
243+
{network.required && (
244+
<Text variant="destructive">*</Text>
245+
)}
246+
</Text>
247+
)}
112248
</Row>
113249
))}
114250
</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)