Skip to content

Commit a33c5b0

Browse files
authored
Stabilised tab switches for TokenWidget (#868)
* improved z-indexes * remove unused imports * stabilised tab switch logic * feat: changeset * clean up
1 parent eeccc46 commit a33c5b0

File tree

3 files changed

+159
-130
lines changed

3 files changed

+159
-130
lines changed

.changeset/plain-oranges-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@relayprotocol/relay-kit-ui': patch
3+
---
4+
5+
Stabilised tab switches

packages/ui/src/components/common/TokenSelector/TokenSelector.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,7 @@ import {
66
useMemo,
77
useState
88
} from 'react'
9-
import {
10-
Flex,
11-
Text,
12-
Input,
13-
Box,
14-
Button,
15-
ChainIcon
16-
} from '../../primitives/index.js'
9+
import { Flex, Text, Input, Box, Button } from '../../primitives/index.js'
1710
import { Modal } from '../Modal.js'
1811
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
1912
import {
@@ -24,14 +17,13 @@ import {
2417
import type { Token } from '../../../types/index.js'
2518
import { type ChainFilterValue } from './ChainFilter.js'
2619
import useRelayClient from '../../../hooks/useRelayClient.js'
27-
import { isAddress, type Address } from 'viem'
20+
import { type Address } from 'viem'
2821
import { useDebounceState, useDuneBalances } from '../../../hooks/index.js'
2922
import { useMediaQuery } from 'usehooks-ts'
3023
import { useTokenList } from '@relayprotocol/relay-kit-hooks'
3124
import { EventNames } from '../../../constants/events.js'
3225
import { UnverifiedTokenModal } from '../UnverifiedTokenModal.js'
3326
import { useEnhancedTokensList } from '../../../hooks/useEnhancedTokensList.js'
34-
import ChainFilter from './ChainFilter.js'
3527
import { TokenList } from './TokenList.js'
3628
import { UnsupportedDepositAddressChainIds } from '../../../constants/depositAddresses.js'
3729
import { getRelayUiKitData } from '../../../utils/localStorage.js'

packages/ui/src/components/widgets/TokenWidget/widget/index.tsx

Lines changed: 152 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ const TokenWidget: FC<TokenWidgetProps> = ({
223223
buy: { fromToken?: Token; toToken?: Token }
224224
sell: { fromToken?: Token; toToken?: Token }
225225
}>({ buy: {}, sell: {} })
226+
const prevActiveTabRef = useRef<'buy' | 'sell'>(activeTab)
226227
const autoSelectedFromTokenRef = useRef(false)
227228
const tabRecipientRef = useRef<{
228229
buy: { override?: string; custom?: string }
@@ -398,6 +399,9 @@ const TokenWidget: FC<TokenWidgetProps> = ({
398399
tradeTypeRef.current = tradeType
399400

400401
useEffect(() => {
402+
if (prevActiveTabRef.current !== activeTab) {
403+
return
404+
}
401405
tabTokenStateRef.current[activeTab] = {
402406
fromToken,
403407
toToken
@@ -970,6 +974,153 @@ const TokenWidget: FC<TokenWidgetProps> = ({
970974
walletsLoading
971975
])
972976

977+
const handleTabChange = useCallback(
978+
(nextTab: 'buy' | 'sell', updateActiveTab: boolean) => {
979+
const prevTab = prevActiveTabRef.current ?? activeTab
980+
981+
if (nextTab === prevTab && !updateActiveTab) {
982+
return
983+
}
984+
985+
setAllowUnsupportedOrigin(nextTab === 'buy')
986+
setAllowUnsupportedRecipient(nextTab === 'sell')
987+
988+
if (nextTab !== prevTab) {
989+
const storedNextState = tabTokenStateRef.current[nextTab] ?? {}
990+
const storedNextRecipient = tabRecipientRef.current[nextTab] ?? {}
991+
992+
const prevFromToken = fromToken
993+
const prevToToken = toToken
994+
995+
tabTokenStateRef.current[prevTab] = {
996+
fromToken: prevFromToken,
997+
toToken: prevToToken
998+
}
999+
tabRecipientRef.current[prevTab] = {
1000+
override:
1001+
typeof destinationAddressOverride === 'string'
1002+
? destinationAddressOverride
1003+
: undefined,
1004+
custom:
1005+
typeof customToAddress === 'string'
1006+
? customToAddress
1007+
: undefined
1008+
}
1009+
1010+
let nextFromToken: Token | undefined
1011+
let nextToToken: Token | undefined
1012+
1013+
if (nextTab === 'sell') {
1014+
// Selling the page token: default to previously viewed token (prevToToken)
1015+
nextFromToken =
1016+
storedNextState.fromToken ??
1017+
prevToToken ??
1018+
prevFromToken ??
1019+
undefined
1020+
// Payout token should remain empty unless user explicitly selected it on sell
1021+
nextToToken = storedNextState.toToken ?? undefined
1022+
} else {
1023+
// Buying the page token: default output token is prev page token
1024+
nextToToken =
1025+
storedNextState.toToken ??
1026+
prevFromToken ??
1027+
prevToToken ??
1028+
undefined
1029+
// Payment method stays empty unless explicitly chosen on buy
1030+
nextFromToken = storedNextState.fromToken ?? undefined
1031+
}
1032+
1033+
handleSetFromToken(nextFromToken)
1034+
handleSetToToken(nextToToken)
1035+
setDestinationAddressOverride(storedNextRecipient.override)
1036+
setCustomToAddress(storedNextRecipient.custom)
1037+
1038+
// Auto-select first compatible wallet in buy tab if no destination is set
1039+
if (
1040+
nextTab === 'buy' &&
1041+
multiWalletSupportEnabled &&
1042+
linkedWallets &&
1043+
linkedWallets.length > 0 &&
1044+
!storedNextRecipient.override &&
1045+
!storedNextRecipient.custom
1046+
) {
1047+
const toChainForRecipient = relayClient?.chains?.find(
1048+
(c) => c.id === nextToToken?.chainId
1049+
)
1050+
1051+
if (toChainForRecipient) {
1052+
const compatibleWallets = linkedWallets.filter(
1053+
(wallet) => wallet.vmType === toChainForRecipient.vmType
1054+
)
1055+
1056+
if (compatibleWallets.length > 0) {
1057+
setDestinationAddressOverride(compatibleWallets[0].address)
1058+
}
1059+
}
1060+
}
1061+
1062+
setAmountInputValue('')
1063+
setAmountOutputValue('')
1064+
setUsdInputValue('')
1065+
setUsdOutputValue('')
1066+
setTokenInputCache('')
1067+
setIsUsdInputMode(nextTab === 'buy')
1068+
debouncedAmountInputControls.cancel()
1069+
debouncedAmountOutputControls.cancel()
1070+
setOriginAddressOverride(undefined)
1071+
}
1072+
1073+
if (updateActiveTab) {
1074+
setActiveTab(nextTab)
1075+
}
1076+
1077+
const desiredTradeType: TradeType =
1078+
nextTab === 'buy' ? 'EXPECTED_OUTPUT' : 'EXACT_INPUT'
1079+
1080+
if (tradeType !== desiredTradeType) {
1081+
setTradeType(desiredTradeType)
1082+
}
1083+
1084+
prevActiveTabRef.current = nextTab
1085+
},
1086+
[
1087+
activeTab,
1088+
customToAddress,
1089+
debouncedAmountInputControls,
1090+
debouncedAmountOutputControls,
1091+
destinationAddressOverride,
1092+
fromToken,
1093+
handleSetFromToken,
1094+
handleSetToToken,
1095+
linkedWallets,
1096+
multiWalletSupportEnabled,
1097+
relayClient?.chains,
1098+
setActiveTab,
1099+
setAllowUnsupportedOrigin,
1100+
setAllowUnsupportedRecipient,
1101+
setAmountInputValue,
1102+
setAmountOutputValue,
1103+
setCustomToAddress,
1104+
setDestinationAddressOverride,
1105+
setIsUsdInputMode,
1106+
setOriginAddressOverride,
1107+
setTokenInputCache,
1108+
setTradeType,
1109+
setUsdInputValue,
1110+
setUsdOutputValue,
1111+
toToken,
1112+
tradeType
1113+
]
1114+
)
1115+
1116+
useEffect(() => {
1117+
if (prevActiveTabRef.current === activeTab) {
1118+
return
1119+
}
1120+
1121+
handleTabChange(activeTab, false)
1122+
}, [activeTab, handleTabChange])
1123+
9731124
return (
9741125
<>
9751126
<WidgetContainer
@@ -1054,126 +1205,7 @@ const TokenWidget: FC<TokenWidgetProps> = ({
10541205
value={activeTab}
10551206
onValueChange={(value) => {
10561207
const nextTab = value as 'buy' | 'sell'
1057-
1058-
setAllowUnsupportedOrigin(nextTab === 'buy')
1059-
setAllowUnsupportedRecipient(nextTab === 'sell')
1060-
1061-
if (nextTab !== activeTab) {
1062-
tabTokenStateRef.current[activeTab] = {
1063-
fromToken,
1064-
toToken
1065-
}
1066-
tabRecipientRef.current[activeTab] = {
1067-
override:
1068-
typeof destinationAddressOverride === 'string'
1069-
? destinationAddressOverride
1070-
: undefined,
1071-
custom:
1072-
typeof customToAddress === 'string'
1073-
? customToAddress
1074-
: undefined
1075-
}
1076-
1077-
const currentState =
1078-
tabTokenStateRef.current[activeTab] ?? {}
1079-
const storedNextState =
1080-
tabTokenStateRef.current[nextTab] ?? {}
1081-
const storedNextRecipient =
1082-
tabRecipientRef.current[nextTab] ?? {}
1083-
1084-
const hasStoredNextFromToken =
1085-
'fromToken' in storedNextState
1086-
const hasStoredNextToToken =
1087-
'toToken' in storedNextState
1088-
1089-
let nextFromToken: Token | undefined
1090-
let nextToToken: Token | undefined
1091-
1092-
if (nextTab === 'sell') {
1093-
const sellToken = hasStoredNextFromToken
1094-
? storedNextState.fromToken
1095-
: (currentState.toToken ?? toToken ?? fromToken)
1096-
const receiveToken = hasStoredNextToToken
1097-
? storedNextState.toToken
1098-
: (currentState.fromToken ?? fromToken)
1099-
1100-
nextFromToken = sellToken ?? undefined
1101-
nextToToken = receiveToken ?? undefined
1102-
} else {
1103-
const buyToken = hasStoredNextToToken
1104-
? storedNextState.toToken
1105-
: (currentState.toToken ?? toToken ?? fromToken)
1106-
const payToken = hasStoredNextFromToken
1107-
? storedNextState.fromToken
1108-
: (currentState.fromToken ?? fromToken)
1109-
1110-
nextFromToken = payToken ?? undefined
1111-
nextToToken = buyToken ?? undefined
1112-
}
1113-
1114-
tabTokenStateRef.current[nextTab] = {
1115-
fromToken: nextFromToken,
1116-
toToken: nextToToken
1117-
}
1118-
tabRecipientRef.current[nextTab] = storedNextRecipient
1119-
1120-
handleSetFromToken(nextFromToken)
1121-
handleSetToToken(nextToToken)
1122-
setDestinationAddressOverride(
1123-
storedNextRecipient.override
1124-
)
1125-
setCustomToAddress(storedNextRecipient.custom)
1126-
1127-
// Auto-select first compatible wallet in buy tab if no destination is set
1128-
if (
1129-
nextTab === 'buy' &&
1130-
multiWalletSupportEnabled &&
1131-
linkedWallets &&
1132-
linkedWallets.length > 0 &&
1133-
!storedNextRecipient.override &&
1134-
!storedNextRecipient.custom
1135-
) {
1136-
// Find the destination chain for filtering compatible wallets
1137-
const toChain = relayClient?.chains?.find(
1138-
(c) => c.id === nextToToken?.chainId
1139-
)
1140-
1141-
if (toChain) {
1142-
// Filter wallets compatible with the destination chain VM type
1143-
const compatibleWallets = linkedWallets.filter(
1144-
(wallet) => {
1145-
return wallet.vmType === toChain.vmType
1146-
}
1147-
)
1148-
1149-
// Auto-select the first compatible wallet
1150-
if (compatibleWallets.length > 0) {
1151-
setDestinationAddressOverride(
1152-
compatibleWallets[0].address
1153-
)
1154-
}
1155-
}
1156-
}
1157-
1158-
setAmountInputValue('')
1159-
setAmountOutputValue('')
1160-
setUsdInputValue('')
1161-
setUsdOutputValue('')
1162-
setTokenInputCache('')
1163-
setIsUsdInputMode(nextTab === 'buy')
1164-
debouncedAmountInputControls.cancel()
1165-
debouncedAmountOutputControls.cancel()
1166-
setOriginAddressOverride(undefined)
1167-
}
1168-
1169-
setActiveTab(nextTab)
1170-
1171-
const desiredTradeType: TradeType =
1172-
nextTab === 'buy' ? 'EXPECTED_OUTPUT' : 'EXACT_INPUT'
1173-
1174-
if (tradeType !== desiredTradeType) {
1175-
setTradeType(desiredTradeType)
1176-
}
1208+
handleTabChange(nextTab, true)
11771209

11781210
onAnalyticEvent?.('TAB_SWITCHED', {
11791211
tab: value

0 commit comments

Comments
 (0)