Skip to content

Commit af089bf

Browse files
feat: mobile app: add yield history filtering by asset type and date range (#692) (#877)
Co-authored-by: Divine-designs <208155579+Divine-designs@users.noreply.github.com>
1 parent f765f37 commit af089bf

6 files changed

Lines changed: 397 additions & 35 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import AsyncStorage from "@react-native-async-storage/async-storage";
2+
import { fetchTransactions } from "../src/services/transactionService";
3+
import { getYieldTransactions } from "../src/services/api";
4+
import type { TransactionFilters } from "../src/types/transaction";
5+
6+
jest.mock("@react-native-async-storage/async-storage", () => {
7+
// eslint-disable-next-line @typescript-eslint/no-require-imports
8+
return require("@react-native-async-storage/async-storage/jest/async-storage-mock");
9+
});
10+
11+
const baseFilters: TransactionFilters = {
12+
type: "all",
13+
status: "all",
14+
search: "",
15+
};
16+
17+
const last30Days = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
18+
.toISOString()
19+
.slice(0, 10);
20+
21+
describe("yield transaction history filtering (#692)", () => {
22+
beforeEach(async () => {
23+
await AsyncStorage.clear();
24+
});
25+
26+
it("filters the client-side list by asset type", async () => {
27+
const page = await fetchTransactions(
28+
{ ...baseFilters, yieldOnly: true, asset: "USDC" },
29+
null
30+
);
31+
expect(page.items.length).toBeGreaterThan(0);
32+
expect(page.items.every((tx) => tx.asset.toUpperCase() === "USDC")).toBe(
33+
true
34+
);
35+
});
36+
37+
it("filters the client-side list by date range", async () => {
38+
const page = await fetchTransactions(
39+
{
40+
...baseFilters,
41+
yieldOnly: true,
42+
dateFrom: last30Days,
43+
dateTo: undefined,
44+
},
45+
null
46+
);
47+
const cutoff = new Date(`${last30Days}T00:00:00`);
48+
expect(page.items.every((tx) => new Date(tx.timestamp) >= cutoff)).toBe(
49+
true
50+
);
51+
});
52+
53+
it("refreshes yield history via api.getYieldTransactions with params", async () => {
54+
const items = await getYieldTransactions({
55+
asset: "XLM",
56+
dateFrom: last30Days,
57+
});
58+
expect(items.every((tx) => tx.type === "yield")).toBe(true);
59+
expect(items.every((tx) => tx.asset.toUpperCase() === "XLM")).toBe(true);
60+
});
61+
62+
it("yield-only filter never returns non-yield transactions", async () => {
63+
const all = await fetchTransactions(baseFilters, null);
64+
const nonYield = all.items.filter((tx) => tx.type !== "yield");
65+
const page = await fetchTransactions(
66+
{ ...baseFilters, yieldOnly: true },
67+
null
68+
);
69+
expect(page.items.every((tx) => tx.type === "yield")).toBe(true);
70+
expect(page.items.length).toBeLessThanOrEqual(
71+
all.items.length - nonYield.length
72+
);
73+
});
74+
});

‎mobileapp/app/(personal)/history.tsx‎

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useCallback } from "react";
1+
import React, { useEffect, useState, useCallback, useMemo } from "react";
22
import {
33
View,
44
Text,
@@ -30,6 +30,25 @@ import {
3030
* it has been pending longer than this, in milliseconds. */
3131
const STUCK_PENDING_MS = 60 * 1000;
3232

33+
// #692 — Yield transaction history filtering presets.
34+
const DATE_PRESETS: { label: string; days: number }[] = [
35+
{ label: "7D", days: 7 },
36+
{ label: "30D", days: 30 },
37+
{ label: "90D", days: 90 },
38+
{ label: "1Y", days: 365 },
39+
];
40+
41+
function presetStartIso(days: number): string {
42+
const d = new Date();
43+
d.setDate(d.getDate() - days);
44+
d.setHours(0, 0, 0, 0);
45+
return d.toISOString().slice(0, 10);
46+
}
47+
48+
function isPresetActive(filters: TransactionFilters, days: number): boolean {
49+
return filters.dateFrom === presetStartIso(days) && !filters.dateTo;
50+
}
51+
3352
// ── Helpers ───────────────────────────────────────────────────────────────────
3453

3554
/**
@@ -112,7 +131,8 @@ function hasActiveFilters(f: TransactionFilters): boolean {
112131
!!f.dateTo ||
113132
!!f.amountMin ||
114133
!!f.amountMax ||
115-
!!f.yieldOnly
134+
!!f.yieldOnly ||
135+
!!f.asset
116136
);
117137
}
118138

@@ -399,6 +419,15 @@ export default function HistoryScreen() {
399419
const listData = groupByDate(items);
400420
const filtersActive = hasActiveFilters(filters);
401421

422+
// #692 — Distinct asset codes present in the filtered result set.
423+
const assetOptions = useMemo(
424+
() => [
425+
"All",
426+
...Array.from(new Set(items.map((tx) => tx.asset.toUpperCase()))).sort(),
427+
],
428+
[items]
429+
);
430+
402431
const renderItem = useCallback(
403432
({ item }: { item: ListItem }) => {
404433
if (item.kind === "header") {
@@ -567,6 +596,111 @@ export default function HistoryScreen() {
567596
</TouchableOpacity>
568597
</View>
569598

599+
{/* #692 — Yield history filters: asset type chips + date range */}
600+
{filters.yieldOnly && (
601+
<View style={styles.yieldFiltersCard}>
602+
<View style={styles.yieldFiltersHeader}>
603+
<Ionicons name="trending-up" size={14} color="#16A34A" />
604+
<Text style={styles.yieldFiltersTitle}>Filter yield history</Text>
605+
</View>
606+
607+
<Text style={styles.yieldFiltersLabel}>Asset type</Text>
608+
<View style={styles.yieldChipRow}>
609+
{assetOptions.map((asset) => {
610+
const selected =
611+
asset === "All" ? !filters.asset : filters.asset === asset;
612+
return (
613+
<TouchableOpacity
614+
key={asset}
615+
style={[styles.assetChip, selected && styles.assetChipActive]}
616+
onPress={() =>
617+
applyFilters({
618+
...filters,
619+
asset: asset === "All" ? undefined : asset,
620+
search: searchText,
621+
})
622+
}
623+
activeOpacity={0.8}
624+
accessibilityRole="button"
625+
accessibilityState={{ selected }}
626+
accessibilityLabel={`Filter by ${asset}`}
627+
>
628+
<Text
629+
style={[
630+
styles.assetChipText,
631+
selected && styles.assetChipTextActive,
632+
]}
633+
>
634+
{asset}
635+
</Text>
636+
</TouchableOpacity>
637+
);
638+
})}
639+
</View>
640+
641+
<Text style={styles.yieldFiltersLabel}>Date range</Text>
642+
<View style={styles.yieldChipRow}>
643+
<TouchableOpacity
644+
style={[
645+
styles.assetChip,
646+
!filters.dateFrom && styles.assetChipActive,
647+
]}
648+
onPress={() =>
649+
applyFilters({
650+
...filters,
651+
dateFrom: undefined,
652+
dateTo: undefined,
653+
search: searchText,
654+
})
655+
}
656+
activeOpacity={0.8}
657+
accessibilityRole="button"
658+
accessibilityState={{ selected: !filters.dateFrom }}
659+
accessibilityLabel="Filter by all dates"
660+
>
661+
<Text
662+
style={[
663+
styles.assetChipText,
664+
!filters.dateFrom && styles.assetChipTextActive,
665+
]}
666+
>
667+
All
668+
</Text>
669+
</TouchableOpacity>
670+
{DATE_PRESETS.map((preset) => {
671+
const active = isPresetActive(filters, preset.days);
672+
return (
673+
<TouchableOpacity
674+
key={preset.label}
675+
style={[styles.assetChip, active && styles.assetChipActive]}
676+
onPress={() =>
677+
applyFilters({
678+
...filters,
679+
dateFrom: presetStartIso(preset.days),
680+
dateTo: undefined,
681+
search: searchText,
682+
})
683+
}
684+
activeOpacity={0.8}
685+
accessibilityRole="button"
686+
accessibilityState={{ selected: active }}
687+
accessibilityLabel={`Filter by last ${preset.label}`}
688+
>
689+
<Text
690+
style={[
691+
styles.assetChipText,
692+
active && styles.assetChipTextActive,
693+
]}
694+
>
695+
{preset.label}
696+
</Text>
697+
</TouchableOpacity>
698+
);
699+
})}
700+
</View>
701+
</View>
702+
)}
703+
570704
{/* Summary row */}
571705
{!loading && items.length > 0 && (
572706
<View style={styles.summaryRow}>
@@ -890,6 +1024,62 @@ const styles = StyleSheet.create({
8901024
paddingHorizontal: 20,
8911025
paddingBottom: Platform.OS === "ios" ? 40 : 24,
8921026
},
1027+
// #692 — Yield history filter card
1028+
yieldFiltersCard: {
1029+
marginHorizontal: 20,
1030+
marginBottom: 10,
1031+
padding: 14,
1032+
borderRadius: 16,
1033+
backgroundColor: "#F0FDF4",
1034+
borderWidth: 1,
1035+
borderColor: "#C8EAD1",
1036+
},
1037+
yieldFiltersHeader: {
1038+
flexDirection: "row",
1039+
alignItems: "center",
1040+
gap: 6,
1041+
marginBottom: 12,
1042+
},
1043+
yieldFiltersTitle: {
1044+
fontSize: 13,
1045+
fontFamily: "Outfit_700Bold",
1046+
color: "#16A34A",
1047+
},
1048+
yieldFiltersLabel: {
1049+
fontSize: 11,
1050+
fontFamily: "Outfit_600SemiBold",
1051+
color: "#6B8D74",
1052+
textTransform: "uppercase",
1053+
letterSpacing: 0.4,
1054+
marginBottom: 8,
1055+
marginTop: 4,
1056+
},
1057+
yieldChipRow: {
1058+
flexDirection: "row",
1059+
flexWrap: "wrap",
1060+
gap: 8,
1061+
marginBottom: 8,
1062+
},
1063+
assetChip: {
1064+
paddingHorizontal: 14,
1065+
paddingVertical: 7,
1066+
borderRadius: 100,
1067+
borderWidth: 1.5,
1068+
borderColor: "#B9DCC4",
1069+
backgroundColor: "#FFFFFF",
1070+
},
1071+
assetChipActive: {
1072+
borderColor: "#16A34A",
1073+
backgroundColor: "#16A34A",
1074+
},
1075+
assetChipText: {
1076+
fontSize: 13,
1077+
fontFamily: "Outfit_600SemiBold",
1078+
color: "#166534",
1079+
},
1080+
assetChipTextActive: {
1081+
color: COLORS.secondary,
1082+
},
8931083
footerLoader: { paddingVertical: 20, alignItems: "center" },
8941084
// Empty
8951085
emptyContainer: {

‎mobileapp/src/components/TransactionFilterSheet.tsx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const STATUS_OPTIONS: { label: string; value: TransactionFilters["status"] }[] =
3636
{ label: "Failed", value: "failed" },
3737
];
3838

39+
const ASSET_OPTIONS: string[] = ["XLM", "USDC", "USDT"];
40+
3941
function ChipGroup<T extends string>({
4042
options,
4143
value,
@@ -181,6 +183,45 @@ export function TransactionFilterSheet({
181183
</TouchableOpacity>
182184
</View>
183185

186+
{/* Asset */}
187+
<Text style={styles.sectionLabel}>Asset</Text>
188+
<View style={chipStyles.row}>
189+
<TouchableOpacity
190+
style={[chipStyles.chip, !local.asset && chipStyles.chipActive]}
191+
onPress={() => update("asset", undefined)}
192+
activeOpacity={0.8}
193+
>
194+
<Text
195+
style={[
196+
chipStyles.chipText,
197+
!local.asset && chipStyles.chipTextActive,
198+
]}
199+
>
200+
All
201+
</Text>
202+
</TouchableOpacity>
203+
{ASSET_OPTIONS.map((asset) => (
204+
<TouchableOpacity
205+
key={asset}
206+
style={[
207+
chipStyles.chip,
208+
local.asset === asset && chipStyles.chipActive,
209+
]}
210+
onPress={() => update("asset", asset)}
211+
activeOpacity={0.8}
212+
>
213+
<Text
214+
style={[
215+
chipStyles.chipText,
216+
local.asset === asset && chipStyles.chipTextActive,
217+
]}
218+
>
219+
{asset}
220+
</Text>
221+
</TouchableOpacity>
222+
))}
223+
</View>
224+
184225
{/* Date range */}
185226
<Text style={styles.sectionLabel}>Date From</Text>
186227
<TextInput

0 commit comments

Comments
 (0)