Skip to content

Commit aea6975

Browse files
committed
Integrate getTxInfo reports server API
1 parent 209f6ae commit aea6975

File tree

14 files changed

+418
-28
lines changed

14 files changed

+418
-28
lines changed

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: `ReturnKeyTypeButton` to `FlipInputModal2`
6+
- added: Integrated reports server order status to transaction details.
67

78
## 4.32.0 (staging)
89

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"@react-navigation/native": "^6.1.3",
8181
"@react-navigation/stack": "^6.3.12",
8282
"@sentry/react-native": "^6.14.0",
83+
"@tanstack/react-query": "^5.83.0",
8384
"@types/jsrsasign": "^10.5.13",
8485
"@unstoppabledomains/resolution": "^9.3.0",
8586
"@walletconnect/react-native-compat": "^2.11.0",

src/components/App.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import '@ethersproject/shims'
22

33
import { ErrorBoundary, Scope, wrap } from '@sentry/react-native'
4+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
45
import * as React from 'react'
56
import { StyleSheet } from 'react-native'
67
import { GestureHandlerRootView } from 'react-native-gesture-handler'
@@ -12,26 +13,30 @@ import { EdgeCoreManager } from './services/EdgeCoreManager'
1213
import { StatusBarManager } from './services/StatusBarManager'
1314
import { ThemeProvider } from './services/ThemeContext'
1415

16+
const queryClient = new QueryClient()
17+
1518
function MainApp() {
1619
const handleBeforeCapture = useHandler((scope: Scope) => {
1720
scope.setLevel('fatal')
1821
scope.setTag('handled', false)
1922
})
2023

2124
return (
22-
<SafeAreaProvider>
23-
<ThemeProvider>
24-
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
25-
<ErrorBoundary
26-
beforeCapture={handleBeforeCapture}
27-
fallback={<CrashScene />}
28-
>
29-
<StatusBarManager />
30-
<EdgeCoreManager />
31-
</ErrorBoundary>
32-
</GestureHandlerRootView>
33-
</ThemeProvider>
34-
</SafeAreaProvider>
25+
<QueryClientProvider client={queryClient}>
26+
<SafeAreaProvider>
27+
<ThemeProvider>
28+
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
29+
<ErrorBoundary
30+
beforeCapture={handleBeforeCapture}
31+
fallback={<CrashScene />}
32+
>
33+
<StatusBarManager />
34+
<EdgeCoreManager />
35+
</ErrorBoundary>
36+
</GestureHandlerRootView>
37+
</ThemeProvider>
38+
</SafeAreaProvider>
39+
</QueryClientProvider>
3540
)
3641
}
3742

src/components/cards/SwapDetailsCard.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import {
1616
} from '../../selectors/DenominationSelectors'
1717
import { useSelector } from '../../types/reactRedux'
1818
import { getWalletName } from '../../util/CurrencyWalletHelpers'
19+
import { ReportsTxInfo } from '../../util/reportsServer'
1920
import { convertNativeToDisplay, unixToLocaleDateTime } from '../../util/utils'
2021
import { DataSheetModal, DataSheetSection } from '../modals/DataSheetModal'
22+
import { ShimmerText } from '../progress-indicators/ShimmerText'
2123
import { EdgeRow } from '../rows/EdgeRow'
2224
import { Airship, showError } from '../services/AirshipInstance'
2325
import { EdgeText } from '../themed/EdgeText'
@@ -27,12 +29,26 @@ interface Props {
2729
swapData: EdgeTxSwap
2830
transaction: EdgeTransaction
2931
sourceWallet?: EdgeCurrencyWallet
32+
33+
/** The transaction info from the reports server. */
34+
reportsTxInfo?: ReportsTxInfo
35+
/**
36+
* Whether the transaction info from the reports server is loading.
37+
* If not provided, the card will not show the status.
38+
* */
39+
isReportsTxInfoLoading?: boolean
3040
}
3141

3242
const TXID_PLACEHOLDER = '{{TXID}}'
3343

3444
export function SwapDetailsCard(props: Props) {
35-
const { swapData, transaction, sourceWallet } = props
45+
const {
46+
swapData,
47+
transaction,
48+
sourceWallet,
49+
reportsTxInfo,
50+
isReportsTxInfoLoading = false
51+
} = props
3652
const { memos = [], spendTargets = [], tokenId } = transaction
3753

3854
const formattedOrderUri =
@@ -199,7 +215,15 @@ export function SwapDetailsCard(props: Props) {
199215
body: swapData.isEstimate
200216
? lstrings.estimated_quote
201217
: lstrings.fixed_quote
202-
}
218+
},
219+
...(reportsTxInfo == null
220+
? []
221+
: [
222+
{
223+
title: lstrings.transaction_details_exchange_status,
224+
body: reportsTxInfo.status
225+
}
226+
])
203227
]
204228
},
205229
{
@@ -288,6 +312,19 @@ export function SwapDetailsCard(props: Props) {
288312
: lstrings.fixed_quote}
289313
</EdgeText>
290314
</EdgeRow>
315+
{isReportsTxInfoLoading == null ? null : (
316+
<EdgeRow title={lstrings.transaction_details_exchange_status}>
317+
{isReportsTxInfoLoading ? (
318+
<ShimmerText characters={10} />
319+
) : (
320+
<EdgeText>
321+
{reportsTxInfo == null
322+
? lstrings.string_unknown
323+
: reportsTxInfo.status}
324+
</EdgeText>
325+
)}
326+
</EdgeRow>
327+
)}
291328
{swapData.orderUri == null ? null : (
292329
<EdgeRow
293330
rightButtonType="touchable"
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import * as React from 'react'
2+
import { View } from 'react-native'
3+
import LinearGradient from 'react-native-linear-gradient'
4+
import Animated, {
5+
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+
/** Number of characters to represent in size for the shimmer. */
22+
characters?: number
23+
/** Number of lines to represent in size for the shimmer. */
24+
lines?: number
25+
}
26+
27+
export const ShimmerText = (props: Props) => {
28+
const { isShown = true, characters, lines = 1 } = props
29+
const theme = useTheme()
30+
31+
const containerHeight = React.useMemo(
32+
() => theme.rem(lines * 1.5),
33+
[lines, theme]
34+
)
35+
const containerWidth = React.useMemo(
36+
() =>
37+
characters
38+
? characters * theme.rem(0.75)
39+
: Math.floor(Math.random() * 80) + 20 + '%',
40+
[characters, theme]
41+
)
42+
43+
const [containerLayout, handleContainerLayout] = useLayout()
44+
const containerLayoutWidth = containerLayout.width
45+
const gradientWidth = containerLayoutWidth * 6
46+
47+
const offset = useSharedValue(0)
48+
49+
const startAnimation = useHandler(() => {
50+
const duration = 2000
51+
const startPosition = -gradientWidth
52+
const endPosition = containerLayoutWidth
53+
offset.value = startPosition
54+
offset.value = withRepeat(
55+
withSequence(
56+
withTiming(startPosition, { duration: duration / 2 }),
57+
withTiming(endPosition, { duration })
58+
),
59+
-1,
60+
false
61+
)
62+
})
63+
64+
React.useEffect(() => {
65+
if (gradientWidth > 0) startAnimation()
66+
}, [startAnimation, gradientWidth])
67+
68+
return isShown ? (
69+
<ContainerView
70+
width={containerWidth}
71+
height={containerHeight}
72+
onLayout={handleContainerLayout}
73+
>
74+
<Shimmer width={gradientWidth} offset={offset}>
75+
<Gradient
76+
start={{ x: 0, y: 0 }}
77+
end={{ x: 1, y: 1 }}
78+
colors={['rgba(0,0,0,0)', theme.shimmerBackgroundHighlight]}
79+
/>
80+
<Gradient
81+
start={{ x: 1, y: 0 }}
82+
end={{ x: 0, y: 1 }}
83+
colors={['rgba(0,0,0,0)', theme.shimmerBackgroundHighlight]}
84+
/>
85+
</Shimmer>
86+
</ContainerView>
87+
) : null
88+
}
89+
90+
/**
91+
* This is the track of the component that contains a gradient that overflows
92+
* in width.
93+
*/
94+
const ContainerView = styled(View)<{
95+
width: string | number
96+
height: string | number
97+
}>(theme => props => ({
98+
width: props.width,
99+
maxWidth: '100%',
100+
height: props.height,
101+
borderRadius: theme.rem(0.25),
102+
backgroundColor: theme.shimmerBackgroundColor,
103+
overflow: 'hidden'
104+
}))
105+
106+
/**
107+
* This is the animated view that within the {@link ContainerView}. It animates
108+
* by an offset value which represents the horizontal position of the shimmer.
109+
*/
110+
const Shimmer = styled(Animated.View)<{
111+
width: string | number
112+
offset: SharedValue<number>
113+
}>(_ => props => [
114+
{
115+
position: 'absolute',
116+
top: 0,
117+
left: 0,
118+
bottom: 0,
119+
display: 'flex',
120+
flexDirection: 'row',
121+
width: props.width
122+
},
123+
useAnimatedStyle(() => ({
124+
transform: [{ translateX: props.offset.value }]
125+
}))
126+
])
127+
128+
/**
129+
* This is gradient nested within the {@link Shimmer}.
130+
*/
131+
const Gradient = styled(LinearGradient)({
132+
flex: 1,
133+
width: '100%',
134+
height: '100%'
135+
})

src/components/rows/EdgeRow.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { lstrings } from '../../locales/strings'
1010
import { triggerHaptic } from '../../util/haptic'
1111
import { fixSides, mapSides, sidesToMargin } from '../../util/sides'
1212
import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity'
13+
import { ShimmerText } from '../progress-indicators/ShimmerText'
1314
import { showToast } from '../services/AirshipInstance'
1415
import { cacheStyles, Theme, useTheme } from '../services/ThemeContext'
1516
import { EdgeText } from '../themed/EdgeText'
@@ -34,6 +35,7 @@ interface Props {
3435
error?: boolean
3536
icon?: React.ReactNode
3637
loading?: boolean
38+
shimmer?: boolean
3739
maximumHeight?: 'small' | 'medium' | 'large'
3840
rightButtonType?: RowActionIcon
3941
title?: string
@@ -118,7 +120,9 @@ export const EdgeRow = (props: Props) => {
118120
{title}
119121
</EdgeText>
120122
)}
121-
{loading ? (
123+
{props.shimmer ? (
124+
<ShimmerText characters={title?.length} />
125+
) : loading ? (
122126
<ActivityIndicator
123127
style={styles.loader}
124128
color={theme.primaryText}

src/components/scenes/TransactionDetailsScene.tsx

Lines changed: 42 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 {
34
EdgeAccount,
@@ -9,6 +10,7 @@ import {
910
EdgeTxSwap
1011
} from 'edge-core-js'
1112
import * as React from 'react'
13+
import { useMemo } from 'react'
1214
import { View } from 'react-native'
1315
import FastImage from 'react-native-fast-image'
1416
import IonIcon from 'react-native-vector-icons/Ionicons'
@@ -34,8 +36,10 @@ import { getExchangeDenom } from '../../selectors/DenominationSelectors'
3436
import { useSelector } from '../../types/reactRedux'
3537
import { EdgeAppSceneProps } from '../../types/routerTypes'
3638
import { getCurrencyCodeWithAccount } from '../../util/CurrencyInfoHelpers'
39+
import { getEdgeTxSwapFromReportTxInfo } from '../../util/getEdgeTxSwapFromReportTxInfo'
3740
import { matchJson } from '../../util/matchJson'
3841
import { getMemoTitle } from '../../util/memoUtils'
42+
import { fetchGetTxInfo } from '../../util/reportsServer'
3943
import {
4044
convertCurrencyFromExchangeRates,
4145
convertNativeToExchange,
@@ -86,6 +90,37 @@ const TransactionDetailsComponent = (props: Props) => {
8690
const styles = getStyles(theme)
8791
const iconColor = useIconColor({ pluginId: currencyInfo.pluginId, tokenId })
8892

93+
const transactionSwapData =
94+
convertActionToSwapData(account, transaction) ?? transaction.swapData
95+
96+
// Query for transaction info from reports server only if the transaction is
97+
// a receive (we need to get potential swap data) or the transaction has
98+
// swap data (we need to get the status)
99+
const shouldShowTradeDetails =
100+
!transaction.isSend || transactionSwapData != null
101+
const { data: reportsTxInfo, isLoading: isReportsTxInfoLoading } = useQuery({
102+
queryKey: ['txInfo', transaction.txid],
103+
queryFn: async () => {
104+
return await fetchGetTxInfo(transaction)
105+
},
106+
staleTime: query =>
107+
// Only cache if the status has resolved, otherwise we'll always consider
108+
// the data to be stale:
109+
['processing', 'pending', undefined].includes(query.state.data?.status)
110+
? 0 // No cache
111+
: Infinity, // Cache forever
112+
enabled: shouldShowTradeDetails,
113+
retry: false
114+
})
115+
116+
const swapDataFromReports = useMemo(
117+
() =>
118+
reportsTxInfo == null
119+
? null
120+
: getEdgeTxSwapFromReportTxInfo(wallet, transaction, reportsTxInfo),
121+
[reportsTxInfo, transaction, wallet]
122+
)
123+
89124
// Choose a default category based on metadata or the txAction
90125
const {
91126
action,
@@ -96,8 +131,7 @@ const TransactionDetailsComponent = (props: Props) => {
96131
savedData
97132
} = getTxActionDisplayInfo(transaction, account, wallet)
98133

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

102136
const thumbnailPath =
103137
useContactThumbnail(mergedData.name) ?? pluginIdIcons[iconPluginId ?? '']
@@ -546,7 +580,11 @@ const TransactionDetailsComponent = (props: Props) => {
546580
<SwapDetailsCard
547581
swapData={swapData}
548582
transaction={transaction}
549-
sourceWallet={wallet}
583+
sourceWallet={
584+
swapData.payoutWalletId === wallet.id ? undefined : wallet
585+
}
586+
reportsTxInfo={reportsTxInfo}
587+
isReportsTxInfoLoading={isReportsTxInfoLoading}
550588
/>
551589
)}
552590
</EdgeAnim>
@@ -591,6 +629,7 @@ const TransactionDetailsComponent = (props: Props) => {
591629
)}
592630
/>
593631
</EdgeAnim>
632+
594633
<EdgeAnim enter={{ type: 'fadeInDown', distance: 140 }}>
595634
<ButtonsView
596635
layout="column"

0 commit comments

Comments
 (0)