Skip to content

Commit 45d5ee4

Browse files
authored
Transaction page (#131)
1 parent 11b60bb commit 45d5ee4

File tree

3 files changed

+359
-1
lines changed

3 files changed

+359
-1
lines changed

packages/mobile-app/app/(drawer)/account/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ export default function Balances() {
312312
<Card
313313
key={transaction.hash}
314314
style={styles.transactionCard}
315+
onPress={() =>
316+
router.push(
317+
`/(drawer)/account/transaction/${transaction.hash}`,
318+
)
319+
}
315320
>
316321
<Text category="s1">{transaction.type.toString()}</Text>
317322
<Text category="p2" appearance="hint">

packages/mobile-app/app/(drawer)/account/send/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,10 @@ export default function Send() {
325325
appearance="filled"
326326
style={styles.confirmButton}
327327
onPress={() => {
328-
console.log("will navigate to", sentTxHash);
328+
setTransactionState("idle");
329+
router.replace(
330+
`/(drawer)/account/transaction/${sentTxHash}`,
331+
);
329332
}}
330333
>
331334
View Transaction
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import { StatusBar } from "expo-status-bar";
2+
import { StyleSheet, View, Linking } from "react-native";
3+
import { useLocalSearchParams, Stack } from "expo-router";
4+
import React from "react";
5+
import {
6+
Layout,
7+
Text,
8+
Button,
9+
Divider,
10+
Icon,
11+
IconProps,
12+
Spinner,
13+
} from "@ui-kitten/components";
14+
import { useFacade } from "../../../../data/facades";
15+
import { CurrencyUtils } from "@ironfish/sdk";
16+
import { useQueries } from "@tanstack/react-query";
17+
import { setStringAsync } from "expo-clipboard";
18+
19+
const ExternalLinkIcon = (props: IconProps) => (
20+
<Icon {...props} name="external-link-outline" />
21+
);
22+
23+
const formatTimestamp = (date: Date) => {
24+
return date.toLocaleString(undefined, {
25+
year: "numeric",
26+
month: "short",
27+
day: "numeric",
28+
hour: "2-digit",
29+
minute: "2-digit",
30+
});
31+
};
32+
33+
const CopyableText = ({ text, style }: { text: string; style?: any }) => {
34+
const copyToClipboard = async () => {
35+
await setStringAsync(text);
36+
};
37+
38+
return (
39+
<View style={styles.copyContainer}>
40+
<Text style={[style, styles.copyText]} selectable>
41+
{text}
42+
</Text>
43+
<Button
44+
appearance="ghost"
45+
accessoryLeft={(props) => <Icon {...props} name="copy-outline" />}
46+
onPress={copyToClipboard}
47+
style={styles.copyButton}
48+
/>
49+
</View>
50+
);
51+
};
52+
53+
export default function TransactionDetails() {
54+
const { hash } = useLocalSearchParams<{ hash: string }>();
55+
const facade = useFacade();
56+
57+
const transactionQuery = facade.getTransaction.useQuery(
58+
{
59+
accountName: facade.getAccount.useQuery({}).data?.name ?? "",
60+
hash,
61+
},
62+
{
63+
enabled: !!hash,
64+
},
65+
);
66+
67+
// Get assets for all balance deltas
68+
const assetQueries = useQueries({
69+
queries:
70+
transactionQuery.data?.assetBalanceDeltas.map((delta) => ({
71+
queryKey: facade.getAsset.buildQueryKey({ assetId: delta.assetId }),
72+
queryFn: () => facade.getAsset.resolver({ assetId: delta.assetId }),
73+
enabled: !!delta.assetId,
74+
})) ?? [],
75+
});
76+
77+
const openInExplorer = () => {
78+
Linking.openURL(`https://explorer.ironfish.network/transaction/${hash}`);
79+
};
80+
81+
if (transactionQuery.isLoading || assetQueries.some((q) => q.isLoading)) {
82+
return (
83+
<Layout style={styles.container}>
84+
<Stack.Screen
85+
options={{
86+
headerTitle: "",
87+
headerTransparent: true,
88+
}}
89+
/>
90+
91+
{/* Header Section Skeleton */}
92+
<View style={styles.headerSection}>
93+
<Spinner size="large" style={styles.spinner} />
94+
<Text category="s1" appearance="hint">
95+
Loading transaction details...
96+
</Text>
97+
</View>
98+
99+
{/* Details Section Skeleton */}
100+
<View style={styles.detailsSection}>
101+
{[1, 2, 3, 4].map((i) => (
102+
<React.Fragment key={i}>
103+
<View style={styles.section}>
104+
<View style={styles.skeletonLabel} />
105+
<View style={styles.skeletonValue} />
106+
</View>
107+
<Divider style={styles.divider} />
108+
</React.Fragment>
109+
))}
110+
</View>
111+
</Layout>
112+
);
113+
}
114+
115+
if (!transactionQuery.data) {
116+
return (
117+
<Layout style={styles.container}>
118+
<Text>Transaction not found</Text>
119+
</Layout>
120+
);
121+
}
122+
123+
const transaction = transactionQuery.data;
124+
125+
// Create a map of assetId to asset data
126+
const assetMap = new Map();
127+
assetQueries.forEach((query) => {
128+
if (query.data) {
129+
assetMap.set(query.data.id, query.data);
130+
}
131+
});
132+
133+
// TEMPORARY: Currently assuming the first balance delta represents the main transaction amount
134+
const mainDelta = transaction.assetBalanceDeltas[0];
135+
const asset = assetMap.get(mainDelta.assetId);
136+
const mainAssetName =
137+
asset?.verification.status === "verified"
138+
? asset.verification.symbol
139+
: (asset?.name ?? mainDelta.assetId);
140+
const mainAmount = BigInt(mainDelta.delta);
141+
const isReceived = mainAmount > 0n;
142+
143+
if (transactionQuery.error) {
144+
return (
145+
<Layout style={[styles.container, styles.centerContent]}>
146+
<Text category="h6" style={styles.errorText}>
147+
Failed to load transaction
148+
</Text>
149+
<Button onPress={() => transactionQuery.refetch()}>Try Again</Button>
150+
</Layout>
151+
);
152+
}
153+
154+
return (
155+
<Layout style={styles.container}>
156+
<Stack.Screen
157+
options={{
158+
headerTitle: "",
159+
headerTransparent: true,
160+
}}
161+
/>
162+
163+
{/* Header Section */}
164+
<View style={styles.headerSection}>
165+
{transaction.notes[0]?.sender === transaction.notes[0]?.owner ? (
166+
<>
167+
<Text category="h3" style={styles.transactionType}>
168+
Self Transaction
169+
</Text>
170+
<Text category="s1" appearance="hint" style={styles.timestamp}>
171+
Balances may not be accurately shown
172+
</Text>
173+
<Text category="s1" appearance="hint" style={styles.timestamp}>
174+
{formatTimestamp(new Date(transaction.timestamp))}
175+
</Text>
176+
</>
177+
) : (
178+
<>
179+
<Text category="h3" style={styles.transactionType}>
180+
{isReceived ? "Received" : "Sent"}
181+
</Text>
182+
<Text category="h2" style={styles.mainAmount}>
183+
{CurrencyUtils.render(
184+
(mainAmount < 0n ? -mainAmount : mainAmount).toString(),
185+
false,
186+
)}{" "}
187+
{mainAssetName}
188+
</Text>
189+
<Text category="s1" appearance="hint" style={styles.timestamp}>
190+
{formatTimestamp(new Date(transaction.timestamp))}
191+
</Text>
192+
</>
193+
)}
194+
</View>
195+
196+
{/* Transaction Details */}
197+
<View style={styles.detailsSection}>
198+
<View style={styles.section}>
199+
<Text category="s1" style={styles.label}>
200+
{isReceived ? "From" : "To"}
201+
</Text>
202+
<CopyableText
203+
text={
204+
isReceived
205+
? transaction.notes[0]?.sender
206+
: transaction.notes[0]?.owner
207+
}
208+
style={styles.value}
209+
/>
210+
</View>
211+
212+
<Divider style={styles.divider} />
213+
214+
<View style={styles.section}>
215+
<Text category="s1" style={styles.label}>
216+
Transaction Hash
217+
</Text>
218+
<CopyableText text={hash} style={styles.value} />
219+
</View>
220+
221+
<Divider style={styles.divider} />
222+
223+
<View style={styles.section}>
224+
<Text category="s1" style={styles.label}>
225+
Status
226+
</Text>
227+
<Text style={styles.value}>
228+
{transaction.status.charAt(0).toUpperCase() +
229+
transaction.status.slice(1).toLowerCase()}
230+
</Text>
231+
</View>
232+
233+
<Divider style={styles.divider} />
234+
235+
{transaction.fee && (
236+
<>
237+
<View style={styles.section}>
238+
<Text category="s1" style={styles.label}>
239+
Fee
240+
</Text>
241+
<Text style={styles.value}>
242+
{CurrencyUtils.render(transaction.fee)} $IRON
243+
</Text>
244+
</View>
245+
<Divider style={styles.divider} />
246+
</>
247+
)}
248+
249+
{transaction.notes.some((note) => note.memo) && (
250+
<>
251+
<View style={styles.section}>
252+
<Text category="s1" style={styles.label}>
253+
Memo
254+
</Text>
255+
<Text style={styles.value}>
256+
{transaction.notes.find((note) => note.memo)?.memo}
257+
</Text>
258+
</View>
259+
<Divider style={styles.divider} />
260+
</>
261+
)}
262+
263+
<Button
264+
style={styles.explorerButton}
265+
accessoryRight={ExternalLinkIcon}
266+
onPress={openInExplorer}
267+
>
268+
View on Explorer
269+
</Button>
270+
</View>
271+
272+
<StatusBar style="auto" />
273+
</Layout>
274+
);
275+
}
276+
277+
const styles = StyleSheet.create({
278+
container: {
279+
flex: 1,
280+
},
281+
headerSection: {
282+
padding: 24,
283+
paddingTop: 100,
284+
alignItems: "center",
285+
backgroundColor: "#f5f5f5",
286+
},
287+
transactionType: {
288+
marginBottom: 8,
289+
},
290+
mainAmount: {
291+
marginBottom: 8,
292+
},
293+
timestamp: {
294+
marginBottom: 16,
295+
},
296+
detailsSection: {
297+
padding: 32,
298+
flex: 1,
299+
},
300+
section: {
301+
marginVertical: 8,
302+
},
303+
label: {
304+
marginBottom: 4,
305+
},
306+
value: {
307+
fontSize: 16,
308+
},
309+
divider: {
310+
marginVertical: 8,
311+
},
312+
explorerButton: {
313+
marginTop: 24,
314+
},
315+
copyContainer: {
316+
flexDirection: "row",
317+
alignItems: "center",
318+
},
319+
copyText: {
320+
flex: 1,
321+
},
322+
copyButton: {
323+
padding: 0,
324+
marginLeft: 8,
325+
},
326+
centerContent: {
327+
justifyContent: "center",
328+
alignItems: "center",
329+
},
330+
errorText: {
331+
marginBottom: 16,
332+
textAlign: "center",
333+
},
334+
spinner: {
335+
marginBottom: 16,
336+
},
337+
skeletonLabel: {
338+
height: 20,
339+
width: 100,
340+
backgroundColor: "#f5f5f5",
341+
borderRadius: 4,
342+
marginBottom: 8,
343+
},
344+
skeletonValue: {
345+
height: 24,
346+
backgroundColor: "#f5f5f5",
347+
borderRadius: 4,
348+
width: "100%",
349+
},
350+
});

0 commit comments

Comments
 (0)