Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export const LimitOrder = ({
const routeSellAsset = useAppSelector(state => selectAssetById(state, sellAssetId ?? ''))
const routeBuyAsset = useAppSelector(state => selectAssetById(state, buyAssetId ?? ''))

// Check if we have URL params that need to be loaded
const hasUrlParams = Boolean(chainId && assetSubId)

// Initialize state from URL params
useEffect(() => {
if (isInitialized) return
Expand Down Expand Up @@ -201,6 +204,12 @@ export const LimitOrder = ({
[tradeInputRef],
)

// If we have URL params but assets haven't been initialized yet, don't render
// This prevents crashes from components trying to use defaultAsset with empty assetId
if (hasUrlParams && !isInitialized) {
return null
}

return (
<Flex flex={1} width='full' justifyContent='center'>
<Routes>
Expand Down
83 changes: 65 additions & 18 deletions src/context/AppProvider/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import type { LedgerOpenAppEventArgs } from '@shapeshiftoss/chain-adapters'
import { emitter } from '@shapeshiftoss/chain-adapters'
import { useQueries, useQuery } from '@tanstack/react-query'
import difference from 'lodash/difference'
import React, { useEffect, useMemo } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import { useTranslate } from 'react-polyglot'
import { matchPath, useLocation } from 'react-router-dom'

import { useDiscoverAccounts } from './hooks/useDiscoverAccounts'
import { usePortfolioFetch } from './hooks/usePortfolioFetch'
Expand All @@ -26,9 +27,11 @@ import { useUser } from '@/hooks/useUser/useUser'
import { useWallet } from '@/hooks/useWallet/useWallet'
import { walletSupportsChain } from '@/hooks/useWalletSupportsChain/useWalletSupportsChain'
import { getAssetService, initAssetService } from '@/lib/asset-service'
import { LIMIT_ORDER_ROUTE_ASSET_SPECIFIC, TRADE_ROUTE_ASSET_SPECIFIC } from '@/Routes/RoutesCommon'
import { useGetFiatRampsQuery } from '@/state/apis/fiatRamps/fiatRamps'
import { assets } from '@/state/slices/assetsSlice/assetsSlice'
import { limitOrderInput } from '@/state/slices/limitOrderInputSlice/limitOrderInputSlice'
import { selectInputBuyAsset as selectLimitOrderInputBuyAsset } from '@/state/slices/limitOrderInputSlice/selectors'
import {
marketApi,
useFindAllMarketDataQuery,
Expand All @@ -42,6 +45,7 @@ import {
selectPortfolioLoadingStatus,
selectWalletId,
} from '@/state/slices/selectors'
import { selectInputBuyAsset as selectTradeInputBuyAsset } from '@/state/slices/tradeInputSlice/selectors'
import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice'
import { tradeRampInput } from '@/state/slices/tradeRampInputSlice/tradeRampInputSlice'
import { useAppDispatch, useAppSelector } from '@/state/store'
Expand Down Expand Up @@ -72,6 +76,18 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const routeAssetId = useRouteAssetId()
const { isSnapInstalled } = useIsSnapInstalled()
const { close: closeModal, open: openModal } = useModal('ledgerOpenApp')
const location = useLocation()

// Check if current URL is a trade/limit route with asset params - if so, let the component handle initialization
const hasTradeRouteParams = useMemo(() => {
const tradeMatch = matchPath({ path: TRADE_ROUTE_ASSET_SPECIFIC, end: true }, location.pathname)
if (tradeMatch?.params?.chainId) return true
const limitMatch = matchPath(
{ path: LIMIT_ORDER_ROUTE_ASSET_SPECIFIC, end: true },
location.pathname,
)
return Boolean(limitMatch?.params?.chainId)
}, [location.pathname])

// Previously <TransactionsProvider />
useTransactionsSubscriber()
Expand All @@ -98,21 +114,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
)
dispatch(assets.actions.setRelatedAssetIndex(service.relatedAssetIndex))

const btcAsset = service.assetsById[btcAssetId]
const ethAsset = service.assetsById[ethAssetId]
if (btcAsset && ethAsset) {
dispatch(tradeInput.actions.setBuyAsset(btcAsset))
dispatch(tradeInput.actions.setSellAsset(ethAsset))
dispatch(tradeRampInput.actions.setBuyAsset(btcAsset))
dispatch(tradeRampInput.actions.setSellAsset(ethAsset))
}

const foxAsset = service.assetsById[foxAssetId]
const usdcAsset = service.assetsById[usdcAssetId]
if (foxAsset && usdcAsset) {
dispatch(limitOrderInput.actions.setBuyAsset(foxAsset))
dispatch(limitOrderInput.actions.setSellAsset(usdcAsset))
}
// Note: Trade input defaults are now set in a useEffect below, not here.
// This is because assets may be persisted from a previous session, and we need
// to set defaults even when queryFn doesn't run (due to React Query caching).

return null
},
Expand All @@ -134,6 +138,46 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
}
}, [isAssetServiceError, toast, translate])

// Get assets from Redux (may be persisted from previous session)
const assetsById = useAppSelector(assets.selectors.selectAssetsById)

// Check if trade inputs have been initialized with real assets (not defaultAsset)
const tradeInputBuyAsset = useAppSelector(selectTradeInputBuyAsset)
const limitOrderInputBuyAsset = useAppSelector(selectLimitOrderInputBuyAsset)

// Track whether we've set defaults for each input (only set once per app lifecycle)
const hasSetTradeDefaults = useRef(false)
const hasSetLimitDefaults = useRef(false)

// Set trade/limit defaults when assets are available and inputs are not yet initialized
// This handles the case where assets are persisted but trade inputs are not
// Only runs ONCE per input type - refs ensure we don't reset on navigation
useEffect(() => {
if (Object.keys(assetsById).length === 0) return

const btcAsset = assetsById[btcAssetId]
const ethAsset = assetsById[ethAssetId]

// Only set tradeInput defaults once, and only if not already initialized
if (!hasSetTradeDefaults.current && btcAsset && ethAsset && !tradeInputBuyAsset.assetId) {
dispatch(tradeInput.actions.setBuyAsset(btcAsset))
dispatch(tradeInput.actions.setSellAsset(ethAsset))
dispatch(tradeRampInput.actions.setBuyAsset(btcAsset))
dispatch(tradeRampInput.actions.setSellAsset(ethAsset))
hasSetTradeDefaults.current = true
}

const foxAsset = assetsById[foxAssetId]
const usdcAsset = assetsById[usdcAssetId]

// Only set limitOrderInput defaults once, and only if not already initialized
if (!hasSetLimitDefaults.current && foxAsset && usdcAsset && !limitOrderInputBuyAsset.assetId) {
dispatch(limitOrderInput.actions.setBuyAsset(foxAsset))
dispatch(limitOrderInput.actions.setSellAsset(usdcAsset))
hasSetLimitDefaults.current = true
}
}, [assetsById, dispatch, tradeInputBuyAsset.assetId, limitOrderInputBuyAsset.assetId])

useEffect(() => {
const handleLedgerOpenApp = ({ chainId, reject }: LedgerOpenAppEventArgs) => {
const onCancel = () => {
Expand Down Expand Up @@ -308,6 +352,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
}, [dispatch, routeAssetId])

// If the assets aren't loaded, then the app isn't ready to render
// This fixes issues with refreshes on pages that expect assets to already exist
return <>{Boolean(assetIds.length) && children}</>
// Also wait for trade inputs to be initialized (unless we're on a route with URL params, where the component handles it)
const areTradeInputsInitialized =
Boolean(tradeInputBuyAsset.assetId) && Boolean(limitOrderInputBuyAsset.assetId)
const isReady = Boolean(assetIds.length) && (hasTradeRouteParams || areTradeInputsInitialized)
return <>{isReady && children}</>
}