Skip to content

Commit aaa2d8d

Browse files
committed
Implement useRampQuotes for quote refetching
1 parent 2ba31ff commit aaa2d8d

File tree

5 files changed

+312
-158
lines changed

5 files changed

+312
-158
lines changed

src/components/scenes/TradeCreateScene.tsx

Lines changed: 15 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useQuery } from '@tanstack/react-query'
21
import { div, mul } from 'biggystring'
32
import * as React from 'react'
43
import { useState } from 'react'
@@ -11,16 +10,14 @@ import { showCountrySelectionModal } from '../../actions/CountryListActions'
1110
import { FLAG_LOGO_URL } from '../../constants/CdnConstants'
1211
import { COUNTRY_CODES, FIAT_COUNTRY } from '../../constants/CountryConstants'
1312
import { useHandler } from '../../hooks/useHandler'
13+
import { useRampQuotes } from '../../hooks/useRampQuotes'
1414
import { useWatch } from '../../hooks/useWatch'
1515
import { lstrings } from '../../locales/strings'
16-
import type {
17-
RampQuoteRequest,
18-
RampQuoteResult
19-
} from '../../plugins/ramps/rampPluginTypes'
16+
import type { RampQuoteRequest } from '../../plugins/ramps/rampPluginTypes'
2017
import { getDefaultFiat } from '../../selectors/SettingsSelectors'
2118
import { useDispatch, useSelector } from '../../types/reactRedux'
2219
import type { BuyTabSceneProps, NavigationBase } from '../../types/routerTypes'
23-
import type { GuiFiatType, NotOK, Ok, Result } from '../../types/types'
20+
import type { GuiFiatType } from '../../types/types'
2421
import { getCurrencyCode } from '../../util/CurrencyInfoHelpers'
2522
import { DECIMAL_PRECISION } from '../../util/utils'
2623
import { DropDownInputButton } from '../buttons/DropDownInputButton'
@@ -49,12 +46,6 @@ export interface TradeCreateParams {
4946
regionCode?: string
5047
}
5148

52-
interface QuoteError {
53-
pluginId: string
54-
pluginDisplayName: string
55-
error: unknown
56-
}
57-
5849
interface Props extends BuyTabSceneProps<'pluginListBuy'> {}
5950

6051
export const TradeCreateScene = (props: Props): React.ReactElement => {
@@ -206,67 +197,16 @@ export const TradeCreateScene = (props: Props): React.ReactElement => {
206197
stateProvinceCode
207198
])
208199

209-
// Fetch quotes when rampQuoteRequest is valid
210-
const { data: quoteResults = [], isLoading: isLoadingQuotes } = useQuery<
211-
Array<Result<RampQuoteResult[], QuoteError>>
212-
>({
213-
queryKey: ['rampQuotes', rampQuoteRequest],
214-
queryFn: async () => {
215-
if (!rampQuoteRequest || Object.keys(rampPlugins).length === 0) {
216-
return []
217-
}
218-
219-
const quotePromises = Object.values(rampPlugins).map(
220-
async (plugin): Promise<Result<RampQuoteResult[], QuoteError>> => {
221-
try {
222-
const quotes = await plugin.fetchQuote(rampQuoteRequest)
223-
return { ok: true, value: quotes }
224-
} catch (error) {
225-
return {
226-
ok: false,
227-
error: {
228-
pluginId: plugin.pluginId,
229-
pluginDisplayName: plugin.rampInfo.pluginDisplayName,
230-
error
231-
}
232-
}
233-
}
234-
}
235-
)
236-
237-
return await Promise.all(quotePromises)
238-
},
239-
enabled: !!rampQuoteRequest,
240-
staleTime: 30000,
241-
gcTime: 300000
200+
// Fetch quotes using the custom hook
201+
const {
202+
quotes: sortedQuotes,
203+
isLoading: isLoadingQuotes,
204+
errors: quoteErrors
205+
} = useRampQuotes({
206+
rampQuoteRequest,
207+
plugins: rampPlugins
242208
})
243209

244-
// Separate successful and unsuccessful quotes
245-
const successfulQuotes = React.useMemo(() => {
246-
return quoteResults.filter(
247-
(result): result is Ok<RampQuoteResult[]> => result.ok
248-
)
249-
}, [quoteResults])
250-
const unsuccessfulQuotes = React.useMemo(() => {
251-
return quoteResults.filter(
252-
(result): result is NotOK<QuoteError> => !result.ok
253-
)
254-
}, [quoteResults])
255-
256-
// Sort all successful quotes by best rate
257-
const sortedQuotes = React.useMemo(() => {
258-
const allQuotes: RampQuoteResult[] = successfulQuotes.flatMap(
259-
result => result.value
260-
)
261-
262-
// Sort by best rate (lowest fiat amount for same crypto amount)
263-
return allQuotes.sort((a, b) => {
264-
const rateA = parseFloat(a.fiatAmount) / parseFloat(a.cryptoAmount)
265-
const rateB = parseFloat(b.fiatAmount) / parseFloat(b.cryptoAmount)
266-
return rateA - rateB
267-
})
268-
}, [successfulQuotes])
269-
270210
// Get the best quote
271211
const bestQuote = sortedQuotes[0]
272212

@@ -593,8 +533,8 @@ export const TradeCreateScene = (props: Props): React.ReactElement => {
593533

594534
{/* Error Alert */}
595535
{!isLoadingQuotes &&
596-
unsuccessfulQuotes.length > 0 &&
597-
successfulQuotes.length === 0 ? (
536+
quoteErrors.length > 0 &&
537+
sortedQuotes.length === 0 ? (
598538
<AlertCardUi4
599539
type="error"
600540
title={lstrings.trade_buy_unavailable_title}
@@ -616,7 +556,8 @@ export const TradeCreateScene = (props: Props): React.ReactElement => {
616556
selectedCryptoCurrencyCode == null ||
617557
userInput === '' ||
618558
lastUsedInput === null ||
619-
(!isLoadingQuotes && sortedQuotes.length === 0)
559+
isLoadingQuotes ||
560+
sortedQuotes.length === 0
620561
}}
621562
/>
622563
</SceneContainer>

src/components/scenes/TradeOptionSelectScene.tsx

Lines changed: 34 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { useQuery } from '@tanstack/react-query'
21
import * as React from 'react'
3-
import { Image, View } from 'react-native'
2+
import { ActivityIndicator, Image, View } from 'react-native'
43
import { sprintf } from 'sprintf-js'
54

65
// TradeOptionSelectScene - Updated layout for design requirements
76
import paymentTypeLogoApplePay from '../../assets/images/paymentTypes/paymentTypeLogoApplePay.png'
7+
import { useRampQuotes } from '../../hooks/useRampQuotes'
88
import { lstrings } from '../../locales/strings'
99
import type {
1010
RampQuoteRequest,
@@ -13,11 +13,11 @@ import type {
1313
} from '../../plugins/ramps/rampPluginTypes'
1414
import { useSelector } from '../../types/reactRedux'
1515
import type { BuyTabSceneProps } from '../../types/routerTypes'
16-
import type { Result } from '../../types/types'
1716
import { getPaymentTypeIcon } from '../../util/paymentTypeIcons'
1817
import { getPaymentTypeDisplayName } from '../../util/paymentTypeUtils'
1918
import { AlertCardUi4 } from '../cards/AlertCard'
2019
import { PaymentOptionCard } from '../cards/PaymentOptionCard'
20+
import { EdgeAnim } from '../common/EdgeAnim'
2121
import { SceneWrapper } from '../common/SceneWrapper'
2222
import { SectionHeader } from '../common/SectionHeader'
2323
import { styled } from '../hoc/styled'
@@ -34,61 +34,24 @@ export interface RampSelectOptionParams {
3434

3535
interface Props extends BuyTabSceneProps<'rampSelectOption'> {}
3636

37-
// Define error type for failed quotes
38-
interface QuoteError {
39-
pluginId: string
40-
pluginDisplayName: string
41-
error: unknown
42-
}
43-
4437
export const TradeOptionSelectScene = (props: Props): React.JSX.Element => {
4538
const { route } = props
4639
const { rampQuoteRequest, quotes: precomputedQuotes } = route.params
4740

41+
const theme = useTheme()
4842
const rampPlugins = useSelector(state => state.rampPlugins.plugins)
4943
const isPluginsLoading = useSelector(state => state.rampPlugins.isLoading)
5044

51-
// Use TanStack Query to fetch quotes only if not provided
52-
const { data: quoteResults = [], isLoading: isLoadingQuotes } = useQuery<
53-
Array<Result<RampQuoteResult[], QuoteError>>
54-
>({
55-
queryKey: ['rampQuotes', rampQuoteRequest],
56-
queryFn: async () => {
57-
// Skip fetching if we already have quotes
58-
if (precomputedQuotes && precomputedQuotes.length > 0) {
59-
return []
60-
}
61-
62-
if (Object.keys(rampPlugins).length === 0) {
63-
return []
64-
}
65-
66-
const quotePromises = Object.values(rampPlugins).map(
67-
async (plugin): Promise<Result<RampQuoteResult[], QuoteError>> => {
68-
try {
69-
const quotes = await plugin.fetchQuote(rampQuoteRequest)
70-
return { ok: true, value: quotes }
71-
} catch (error) {
72-
console.warn(`Failed to get quote from ${plugin.pluginId}:`, error)
73-
return {
74-
ok: false,
75-
error: {
76-
pluginId: plugin.pluginId,
77-
pluginDisplayName: plugin.rampInfo.pluginDisplayName,
78-
error
79-
}
80-
}
81-
}
82-
}
83-
)
84-
85-
return await Promise.all(quotePromises)
86-
},
87-
refetchOnMount: 'always',
88-
refetchInterval: 60000,
89-
enabled: !precomputedQuotes || precomputedQuotes.length === 0,
90-
staleTime: 30000, // Consider data stale after 30 seconds
91-
gcTime: 300000 // Keep in cache for 5 minutes
45+
// Use the new hook with precomputed quotes
46+
const {
47+
quotes: allQuotes,
48+
isLoading: isLoadingQuotes,
49+
isFetching: isFetchingQuotes,
50+
errors: failedQuotes
51+
} = useRampQuotes({
52+
rampQuoteRequest,
53+
plugins: rampPlugins,
54+
precomputedQuotes
9255
})
9356

9457
const handleQuotePress = async (quote: RampQuoteResult) => {
@@ -101,24 +64,6 @@ export const TradeOptionSelectScene = (props: Props): React.JSX.Element => {
10164
}
10265
}
10366

104-
// Use precomputed quotes if available, otherwise use fetched quotes
105-
const allQuotes: RampQuoteResult[] = React.useMemo(() => {
106-
if (precomputedQuotes && precomputedQuotes.length > 0) {
107-
return precomputedQuotes
108-
}
109-
110-
return quoteResults
111-
.filter(
112-
(result): result is { ok: true; value: RampQuoteResult[] } => result.ok
113-
)
114-
.flatMap(result => result.value)
115-
.sort((a, b) => {
116-
const rateA = parseFloat(a.fiatAmount) / parseFloat(a.cryptoAmount)
117-
const rateB = parseFloat(b.fiatAmount) / parseFloat(b.cryptoAmount)
118-
return rateA - rateB
119-
})
120-
}, [precomputedQuotes, quoteResults])
121-
12267
// Get the best quote overall
12368
const bestQuoteOverall = allQuotes[0]
12469

@@ -133,7 +78,7 @@ export const TradeOptionSelectScene = (props: Props): React.JSX.Element => {
13378
})
13479

13580
// Sort quotes within each payment type group
136-
grouped.forEach((quotes, paymentType) => {
81+
grouped.forEach(quotes => {
13782
quotes.sort((a, b) => {
13883
const rateA = parseFloat(a.fiatAmount) / parseFloat(a.cryptoAmount)
13984
const rateB = parseFloat(b.fiatAmount) / parseFloat(b.cryptoAmount)
@@ -144,18 +89,24 @@ export const TradeOptionSelectScene = (props: Props): React.JSX.Element => {
14489
return grouped
14590
}, [allQuotes])
14691

147-
// Get failed quotes for error display
148-
const failedQuotes = quoteResults.filter(
149-
(result): result is { ok: false; error: QuoteError } => !result.ok
150-
)
92+
// Only show loading state if we have no quotes to display
93+
const showLoadingState =
94+
isPluginsLoading || (isLoadingQuotes && allQuotes.length === 0)
15195

15296
return (
15397
<SceneWrapper scroll hasTabs>
15498
<SceneContainer headerTitle={lstrings.trade_option_buy_title}>
15599
<SectionHeader
156100
leftTitle={lstrings.trade_option_select_payment_method}
101+
rightNode={
102+
isFetchingQuotes ? (
103+
<EdgeAnim enter={{ type: 'fadeIn', delay: 200 }}>
104+
<ActivityIndicator size="small" color={theme.primaryText} />
105+
</EdgeAnim>
106+
) : undefined
107+
}
157108
/>
158-
{isPluginsLoading || isLoadingQuotes ? (
109+
{showLoadingState ? (
159110
<>
160111
<ShimmerCard>
161112
<Shimmer />
@@ -192,19 +143,19 @@ export const TradeOptionSelectScene = (props: Props): React.JSX.Element => {
192143
/>
193144
)
194145
)}
195-
{failedQuotes.map(result => {
146+
{failedQuotes.map(error => {
196147
const errorMessage =
197-
result.error.error instanceof Error
198-
? result.error.error.message
199-
: String(result.error.error)
148+
error.error instanceof Error
149+
? error.error.message
150+
: String(error.error)
200151

201152
return (
202153
<AlertCardUi4
203-
key={`error-${result.error.pluginId}`}
154+
key={`error-${error.pluginId}`}
204155
type="error"
205156
title={sprintf(
206157
lstrings.trade_option_provider_failed_s,
207-
result.error.pluginDisplayName
158+
error.pluginDisplayName
208159
)}
209160
body={errorMessage}
210161
marginRem={[0.5, 0.5]}
@@ -247,9 +198,8 @@ const QuoteResult: React.FC<{
247198
const icon = paymentTypeIcon ?? { uri: selectedQuote.partnerIcon }
248199

249200
// Determine custom title rendering
250-
const paymentType = selectedQuote.paymentType
251-
const customTitleKey = paymentTypeToCustomTitleKey[paymentType]
252-
const defaultTitle = getPaymentTypeDisplayName(paymentType)
201+
const customTitleKey = paymentTypeToCustomTitleKey[selectedQuote.paymentType]
202+
const defaultTitle = getPaymentTypeDisplayName(selectedQuote.paymentType)
253203

254204
// Render custom title based on payment type
255205
let titleComponent: React.ReactNode

0 commit comments

Comments
 (0)