Skip to content

Commit 21db217

Browse files
committed
Integrate getTxInfo reports server API
1 parent 54a9fac commit 21db217

11 files changed

Lines changed: 529 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased (develop)
44

55
- added: `chooseCaip19Asset` EdgeProvider API for precise wallet selection using CAIP-19 identifiers
6+
- added: Integrated reports server order status to transaction details.
67
- changed: Append chain names to token codes in RampCreateScene
78

89
## 4.42.0 (staging)

src/components/cards/SwapDetailsCard.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import {
2020
import { useSelector } from '../../types/reactRedux'
2121
import { getTokenId } from '../../util/CurrencyInfoHelpers'
2222
import { getWalletName } from '../../util/CurrencyWalletHelpers'
23+
import type { ReportsTxInfo } from '../../util/reportsServer'
2324
import { convertNativeToDisplay, unixToLocaleDateTime } from '../../util/utils'
2425
import { DataSheetModal, type DataSheetSection } from '../modals/DataSheetModal'
26+
import { ShimmerText } from '../progress-indicators/ShimmerText'
2527
import { EdgeRow } from '../rows/EdgeRow'
2628
import { Airship, showError } from '../services/AirshipInstance'
2729
import { EdgeText } from '../themed/EdgeText'
@@ -31,6 +33,15 @@ interface Props {
3133
swapData: EdgeTxSwap
3234
transaction: EdgeTransaction
3335
sourceWallet?: EdgeCurrencyWallet
36+
37+
/** The transaction info from the reports server. */
38+
reportsTxInfo?: ReportsTxInfo
39+
40+
/**
41+
* Whether the transaction info from the reports server is loading.
42+
* If not provided, the card will not show the status.
43+
* */
44+
isReportsTxInfoLoading?: boolean
3445
}
3546

3647
const TXID_PLACEHOLDER = '{{TXID}}'
@@ -58,7 +69,12 @@ const upgradeSwapData = (
5869
}
5970

6071
export const SwapDetailsCard: React.FC<Props> = (props: Props) => {
61-
const { transaction, sourceWallet } = props
72+
const {
73+
transaction,
74+
sourceWallet,
75+
reportsTxInfo,
76+
isReportsTxInfoLoading = false
77+
} = props
6278
const { memos = [], spendTargets = [], tokenId } = transaction
6379

6480
const swapData = upgradeSwapData(sourceWallet, props.swapData)
@@ -226,7 +242,15 @@ export const SwapDetailsCard: React.FC<Props> = (props: Props) => {
226242
body: swapData.isEstimate
227243
? lstrings.estimated_quote
228244
: lstrings.fixed_quote
229-
}
245+
},
246+
...(reportsTxInfo == null
247+
? []
248+
: [
249+
{
250+
title: lstrings.transaction_details_exchange_status,
251+
body: reportsTxInfo.swapInfo.status
252+
}
253+
])
230254
]
231255
},
232256
{
@@ -315,6 +339,19 @@ export const SwapDetailsCard: React.FC<Props> = (props: Props) => {
315339
: lstrings.fixed_quote}
316340
</EdgeText>
317341
</EdgeRow>
342+
{isReportsTxInfoLoading == null ? null : (
343+
<EdgeRow title={lstrings.transaction_details_exchange_status}>
344+
{isReportsTxInfoLoading ? (
345+
<ShimmerText characters={10} />
346+
) : (
347+
<EdgeText>
348+
{reportsTxInfo == null
349+
? lstrings.string_unknown
350+
: reportsTxInfo.swapInfo.status}
351+
</EdgeText>
352+
)}
353+
</EdgeRow>
354+
)}
318355
{swapData.orderUri == null ? null : (
319356
<EdgeRow
320357
rightButtonType="touchable"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import * as React from 'react'
2+
import { type DimensionValue, View } from 'react-native'
3+
import LinearGradient from 'react-native-linear-gradient'
4+
import Animated, {
5+
type SharedValue,
6+
useAnimatedStyle,
7+
useSharedValue,
8+
withRepeat,
9+
withSequence,
10+
withTiming
11+
} from 'react-native-reanimated'
12+
13+
import { useHandler } from '../../hooks/useHandler'
14+
import { useLayout } from '../../hooks/useLayout'
15+
import { styled } from '../hoc/styled'
16+
import { useTheme } from '../services/ThemeContext'
17+
18+
interface Props {
19+
/** Whether the component is shown (rendered). Default: true */
20+
isShown?: boolean
21+
22+
/** Number of characters to represent in size for the shimmer. */
23+
characters?: number
24+
25+
/** Number of lines to represent in size for the shimmer. */
26+
lines?: number
27+
}
28+
29+
export const ShimmerText: React.FC<Props> = (props: Props) => {
30+
const { isShown = true, characters, lines = 1 } = props
31+
const theme = useTheme()
32+
33+
const containerHeight: DimensionValue = React.useMemo(
34+
() => theme.rem(lines * 1.5),
35+
[lines, theme]
36+
)
37+
const containerWidth: DimensionValue = React.useMemo(
38+
() =>
39+
characters != null
40+
? characters * theme.rem(0.75)
41+
: ((Math.floor(Math.random() * 80) + 20 + '%') as DimensionValue),
42+
[characters, theme]
43+
)
44+
45+
const [containerLayout, handleContainerLayout] = useLayout()
46+
const containerLayoutWidth = containerLayout.width
47+
const gradientWidth = containerLayoutWidth * 6
48+
49+
const offset = useSharedValue(0)
50+
51+
const startAnimation = useHandler(() => {
52+
const duration = 2000
53+
const startPosition = -gradientWidth
54+
const endPosition = containerLayoutWidth
55+
offset.value = startPosition
56+
offset.value = withRepeat(
57+
withSequence(
58+
withTiming(startPosition, { duration: duration / 2 }),
59+
withTiming(endPosition, { duration })
60+
),
61+
-1,
62+
false
63+
)
64+
})
65+
66+
React.useEffect(() => {
67+
if (gradientWidth > 0) startAnimation()
68+
}, [startAnimation, gradientWidth])
69+
70+
return isShown ? (
71+
<ContainerView
72+
width={containerWidth}
73+
height={containerHeight}
74+
onLayout={handleContainerLayout}
75+
>
76+
<Shimmer width={gradientWidth} offset={offset}>
77+
<Gradient
78+
start={{ x: 0, y: 0 }}
79+
end={{ x: 1, y: 1 }}
80+
colors={['rgba(0,0,0,0)', theme.shimmerBackgroundHighlight]}
81+
/>
82+
<Gradient
83+
start={{ x: 1, y: 0 }}
84+
end={{ x: 0, y: 1 }}
85+
colors={['rgba(0,0,0,0)', theme.shimmerBackgroundHighlight]}
86+
/>
87+
</Shimmer>
88+
</ContainerView>
89+
) : null
90+
}
91+
92+
/**
93+
* This is the track of the component that contains a gradient that overflows
94+
* in width.
95+
*/
96+
const ContainerView = styled(View)<{
97+
width: DimensionValue
98+
height: DimensionValue
99+
}>(theme => props => ({
100+
width: props.width,
101+
maxWidth: '100%',
102+
height: props.height,
103+
borderRadius: theme.rem(0.25),
104+
backgroundColor: theme.shimmerBackgroundColor,
105+
overflow: 'hidden'
106+
}))
107+
108+
/**
109+
* This is the animated view that within the {@link ContainerView}. It animates
110+
* by an offset value which represents the horizontal position of the shimmer.
111+
*/
112+
const Shimmer = styled(Animated.View)<{
113+
width: DimensionValue
114+
offset: SharedValue<number>
115+
}>(_ => props => [
116+
{
117+
position: 'absolute',
118+
top: 0,
119+
left: 0,
120+
bottom: 0,
121+
display: 'flex',
122+
flexDirection: 'row',
123+
width: props.width
124+
},
125+
useAnimatedStyle(() => ({
126+
transform: [{ translateX: props.offset.value }]
127+
}))
128+
])
129+
130+
/**
131+
* This is gradient nested within the {@link Shimmer}.
132+
*/
133+
const Gradient = styled(LinearGradient)({
134+
flex: 1,
135+
width: '100%',
136+
height: '100%'
137+
})

src/components/rows/EdgeRow.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { triggerHaptic } from '../../util/haptic'
1515
import { fixSides, mapSides, sidesToMargin } from '../../util/sides'
1616
import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity'
1717
import { ChevronRightIcon } from '../icons/ThemedIcons'
18+
import { ShimmerText } from '../progress-indicators/ShimmerText'
1819
import { showToast } from '../services/AirshipInstance'
1920
import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext'
2021
import { EdgeText } from '../themed/EdgeText'
@@ -39,6 +40,7 @@ interface Props {
3940
error?: boolean
4041
icon?: React.ReactNode
4142
loading?: boolean
43+
shimmer?: boolean
4244
maximumHeight?: 'small' | 'medium' | 'large'
4345
rightButtonType?: RowActionIcon
4446
title?: string
@@ -57,6 +59,7 @@ export const EdgeRow: React.FC<Props> = (props: Props) => {
5759
error,
5860
icon,
5961
loading = false,
62+
shimmer = false,
6063
marginRem,
6164
maximumHeight = 'medium',
6265
testID,
@@ -123,7 +126,9 @@ export const EdgeRow: React.FC<Props> = (props: Props) => {
123126
{title}
124127
</EdgeText>
125128
)}
126-
{loading ? (
129+
{shimmer ? (
130+
<ShimmerText characters={title?.length} />
131+
) : loading ? (
127132
<ActivityIndicator
128133
style={styles.loader}
129134
color={theme.primaryText}

src/components/scenes/TransactionDetailsScene.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useQuery } from '@tanstack/react-query'
12
import { abs } from 'biggystring'
23
import type {
34
EdgeAccount,
@@ -8,6 +9,7 @@ import type {
89
EdgeTxSwap
910
} from 'edge-core-js'
1011
import * as React from 'react'
12+
import { useMemo } from 'react'
1113
import { View } from 'react-native'
1214
import FastImage from 'react-native-fast-image'
1315
import IonIcon from 'react-native-vector-icons/Ionicons'
@@ -36,6 +38,11 @@ import type { EdgeAppSceneProps } from '../../types/routerTypes'
3638
import { getCurrencyCodeWithAccount } from '../../util/CurrencyInfoHelpers'
3739
import { matchJson } from '../../util/matchJson'
3840
import { getMemoTitle } from '../../util/memoUtils'
41+
import {
42+
queryReportsTxInfo,
43+
toEdgeTxActionSwap,
44+
toEdgeTxSwap
45+
} from '../../util/reportsServer'
3946
import {
4047
convertNativeToExchange,
4148
darkenHexColor,
@@ -86,6 +93,55 @@ export const TransactionDetailsComponent: React.FC<Props> = props => {
8693
const styles = getStyles(theme)
8794
const iconColor = useIconColor({ pluginId: currencyInfo.pluginId, tokenId })
8895

96+
const transactionSwapData = (): EdgeTxSwap | undefined =>
97+
convertActionToSwapData(account, transaction) ?? transaction.swapData
98+
99+
// Query for transaction info from reports server only if the transaction is
100+
// a receive (we need to get potential swap data) or the transaction has
101+
// swap data (we need to get the status)
102+
const shouldShowTradeDetails =
103+
!transaction.isSend || transactionSwapData() != null
104+
const { data: reportsTxInfo, isLoading: isReportsTxInfoLoading } = useQuery({
105+
queryKey: ['txInfo', transaction.txid],
106+
queryFn: async () => {
107+
return await queryReportsTxInfo(wallet, transaction)
108+
},
109+
staleTime: query =>
110+
// Only cache if the status has resolved, otherwise we'll always consider
111+
// the data to be stale:
112+
['processing', 'pending', undefined].includes(
113+
query.state.data?.swapInfo.status
114+
)
115+
? 0 // No cache
116+
: Infinity, // Cache forever
117+
enabled: shouldShowTradeDetails,
118+
retry: false
119+
})
120+
121+
const swapDataFromReports = useMemo(
122+
() =>
123+
reportsTxInfo == null
124+
? undefined
125+
: toEdgeTxSwap(account, wallet, transaction, reportsTxInfo),
126+
[account, reportsTxInfo, transaction, wallet]
127+
)
128+
129+
const edgeTxActionSwapFromReports = useMemo(() => {
130+
if (reportsTxInfo == null) return
131+
return toEdgeTxActionSwap(account, transaction, reportsTxInfo)
132+
}, [account, reportsTxInfo, transaction])
133+
134+
// Update the transaction object with saveAction data from reports server:
135+
if (
136+
edgeTxActionSwapFromReports != null &&
137+
transaction.savedAction !== edgeTxActionSwapFromReports
138+
) {
139+
transaction.savedAction = edgeTxActionSwapFromReports
140+
transaction.assetAction = {
141+
assetActionType: 'swap'
142+
}
143+
}
144+
89145
// Choose a default category based on metadata or the txAction
90146
const {
91147
action,
@@ -96,8 +152,7 @@ export const TransactionDetailsComponent: React.FC<Props> = props => {
96152
savedData
97153
} = getTxActionDisplayInfo(transaction, account, wallet)
98154

99-
const swapData =
100-
convertActionToSwapData(account, transaction) ?? transaction.swapData
155+
const swapData = transactionSwapData() ?? swapDataFromReports
101156

102157
const thumbnailPath =
103158
useContactThumbnail(mergedData.name) ?? pluginIdIcons[iconPluginId ?? '']
@@ -610,7 +665,11 @@ export const TransactionDetailsComponent: React.FC<Props> = props => {
610665
<SwapDetailsCard
611666
swapData={swapData}
612667
transaction={transaction}
613-
sourceWallet={wallet}
668+
sourceWallet={
669+
swapData.payoutWalletId === wallet.id ? undefined : wallet
670+
}
671+
reportsTxInfo={reportsTxInfo ?? undefined}
672+
isReportsTxInfoLoading={isReportsTxInfoLoading}
614673
/>
615674
)}
616675
</EdgeAnim>

src/components/services/AccountCallbackManager.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { convertCurrency } from '../../selectors/WalletSelectors'
2323
import { useDispatch, useSelector } from '../../types/reactRedux'
2424
import type { NavigationBase } from '../../types/routerTypes'
2525
import { makePeriodicTask } from '../../util/PeriodicTask'
26+
import { mergeReportsTxInfo } from '../../util/reportsServer'
2627
import { convertNativeToExchange, datelog, snooze } from '../../util/utils'
2728
import { Airship, showDevError } from './AirshipInstance'
2829

@@ -133,6 +134,17 @@ export const AccountCallbackManager: React.FC<Props> = (props: Props) => {
133134
}
134135
)
135136

137+
const txsNeedingSwapData = transactions.filter(
138+
tx => tx.swapData == null && !tx.isSend
139+
)
140+
if (txsNeedingSwapData.length > 0) {
141+
mergeReportsTxInfo(account, wallet, txsNeedingSwapData).catch(
142+
(err: unknown) => {
143+
console.warn(err)
144+
}
145+
)
146+
}
147+
136148
// Review triggers: deposit & transaction count
137149
for (const tx of transactions) {
138150
const actionType =

src/envConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ export const asEnvConfig = asObject({
515515
port: asOptional(asString, '8008')
516516
})
517517
),
518+
REPORTS_SERVERS: asOptional(asArray(asString)),
518519
THEME_SERVER: asOptional(
519520
asObject({
520521
host: asOptional(asString, 'localhost'),

0 commit comments

Comments
 (0)