Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions mobileapp/__tests__/yieldHistory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { fetchTransactions } from "../src/services/transactionService";
import { getYieldTransactions } from "../src/services/api";
import type { TransactionFilters } from "../src/types/transaction";

jest.mock("@react-native-async-storage/async-storage", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require("@react-native-async-storage/async-storage/jest/async-storage-mock");
});

const baseFilters: TransactionFilters = {
type: "all",
status: "all",
search: "",
};

const last30Days = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10);

describe("yield transaction history filtering (#692)", () => {
beforeEach(async () => {
await AsyncStorage.clear();
});

it("filters the client-side list by asset type", async () => {
const page = await fetchTransactions(
{ ...baseFilters, yieldOnly: true, asset: "USDC" },
null
);
expect(page.items.length).toBeGreaterThan(0);
expect(page.items.every((tx) => tx.asset.toUpperCase() === "USDC")).toBe(
true
);
});

it("filters the client-side list by date range", async () => {
const page = await fetchTransactions(
{
...baseFilters,
yieldOnly: true,
dateFrom: last30Days,
dateTo: undefined,
},
null
);
const cutoff = new Date(`${last30Days}T00:00:00`);
expect(page.items.every((tx) => new Date(tx.timestamp) >= cutoff)).toBe(
true
);
});

it("refreshes yield history via api.getYieldTransactions with params", async () => {
const items = await getYieldTransactions({
asset: "XLM",
dateFrom: last30Days,
});
expect(items.every((tx) => tx.type === "yield")).toBe(true);
expect(items.every((tx) => tx.asset.toUpperCase() === "XLM")).toBe(true);
});

it("yield-only filter never returns non-yield transactions", async () => {
const all = await fetchTransactions(baseFilters, null);
const nonYield = all.items.filter((tx) => tx.type !== "yield");
const page = await fetchTransactions(
{ ...baseFilters, yieldOnly: true },
null
);
expect(page.items.every((tx) => tx.type === "yield")).toBe(true);
expect(page.items.length).toBeLessThanOrEqual(
all.items.length - nonYield.length
);
});
});
194 changes: 192 additions & 2 deletions mobileapp/app/(personal)/history.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from "react";
import React, { useEffect, useState, useCallback, useMemo } from "react";
import {
View,
Text,
Expand Down Expand Up @@ -30,6 +30,25 @@ import {
* it has been pending longer than this, in milliseconds. */
const STUCK_PENDING_MS = 60 * 1000;

// #692 — Yield transaction history filtering presets.
const DATE_PRESETS: { label: string; days: number }[] = [
{ label: "7D", days: 7 },
{ label: "30D", days: 30 },
{ label: "90D", days: 90 },
{ label: "1Y", days: 365 },
];

function presetStartIso(days: number): string {
const d = new Date();
d.setDate(d.getDate() - days);
d.setHours(0, 0, 0, 0);
return d.toISOString().slice(0, 10);
}

function isPresetActive(filters: TransactionFilters, days: number): boolean {
return filters.dateFrom === presetStartIso(days) && !filters.dateTo;
}

// ── Helpers ───────────────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -112,7 +131,8 @@ function hasActiveFilters(f: TransactionFilters): boolean {
!!f.dateTo ||
!!f.amountMin ||
!!f.amountMax ||
!!f.yieldOnly
!!f.yieldOnly ||
!!f.asset
);
}

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

// #692 — Distinct asset codes present in the filtered result set.
const assetOptions = useMemo(
() => [
"All",
...Array.from(new Set(items.map((tx) => tx.asset.toUpperCase()))).sort(),
],
[items]
);

const renderItem = useCallback(
({ item }: { item: ListItem }) => {
if (item.kind === "header") {
Expand Down Expand Up @@ -567,6 +596,111 @@ export default function HistoryScreen() {
</TouchableOpacity>
</View>

{/* #692 — Yield history filters: asset type chips + date range */}
{filters.yieldOnly && (
<View style={styles.yieldFiltersCard}>
<View style={styles.yieldFiltersHeader}>
<Ionicons name="trending-up" size={14} color="#16A34A" />
<Text style={styles.yieldFiltersTitle}>Filter yield history</Text>
</View>

<Text style={styles.yieldFiltersLabel}>Asset type</Text>
<View style={styles.yieldChipRow}>
{assetOptions.map((asset) => {
const selected =
asset === "All" ? !filters.asset : filters.asset === asset;
return (
<TouchableOpacity
key={asset}
style={[styles.assetChip, selected && styles.assetChipActive]}
onPress={() =>
applyFilters({
...filters,
asset: asset === "All" ? undefined : asset,
search: searchText,
})
}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityState={{ selected }}
accessibilityLabel={`Filter by ${asset}`}
>
<Text
style={[
styles.assetChipText,
selected && styles.assetChipTextActive,
]}
>
{asset}
</Text>
</TouchableOpacity>
);
})}
</View>

<Text style={styles.yieldFiltersLabel}>Date range</Text>
<View style={styles.yieldChipRow}>
<TouchableOpacity
style={[
styles.assetChip,
!filters.dateFrom && styles.assetChipActive,
]}
onPress={() =>
applyFilters({
...filters,
dateFrom: undefined,
dateTo: undefined,
search: searchText,
})
}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityState={{ selected: !filters.dateFrom }}
accessibilityLabel="Filter by all dates"
>
<Text
style={[
styles.assetChipText,
!filters.dateFrom && styles.assetChipTextActive,
]}
>
All
</Text>
</TouchableOpacity>
{DATE_PRESETS.map((preset) => {
const active = isPresetActive(filters, preset.days);
return (
<TouchableOpacity
key={preset.label}
style={[styles.assetChip, active && styles.assetChipActive]}
onPress={() =>
applyFilters({
...filters,
dateFrom: presetStartIso(preset.days),
dateTo: undefined,
search: searchText,
})
}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityState={{ selected: active }}
accessibilityLabel={`Filter by last ${preset.label}`}
>
<Text
style={[
styles.assetChipText,
active && styles.assetChipTextActive,
]}
>
{preset.label}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
)}

{/* Summary row */}
{!loading && items.length > 0 && (
<View style={styles.summaryRow}>
Expand Down Expand Up @@ -890,6 +1024,62 @@ const styles = StyleSheet.create({
paddingHorizontal: 20,
paddingBottom: Platform.OS === "ios" ? 40 : 24,
},
// #692 — Yield history filter card
yieldFiltersCard: {
marginHorizontal: 20,
marginBottom: 10,
padding: 14,
borderRadius: 16,
backgroundColor: "#F0FDF4",
borderWidth: 1,
borderColor: "#C8EAD1",
},
yieldFiltersHeader: {
flexDirection: "row",
alignItems: "center",
gap: 6,
marginBottom: 12,
},
yieldFiltersTitle: {
fontSize: 13,
fontFamily: "Outfit_700Bold",
color: "#16A34A",
},
yieldFiltersLabel: {
fontSize: 11,
fontFamily: "Outfit_600SemiBold",
color: "#6B8D74",
textTransform: "uppercase",
letterSpacing: 0.4,
marginBottom: 8,
marginTop: 4,
},
yieldChipRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginBottom: 8,
},
assetChip: {
paddingHorizontal: 14,
paddingVertical: 7,
borderRadius: 100,
borderWidth: 1.5,
borderColor: "#B9DCC4",
backgroundColor: "#FFFFFF",
},
assetChipActive: {
borderColor: "#16A34A",
backgroundColor: "#16A34A",
},
assetChipText: {
fontSize: 13,
fontFamily: "Outfit_600SemiBold",
color: "#166534",
},
assetChipTextActive: {
color: COLORS.secondary,
},
footerLoader: { paddingVertical: 20, alignItems: "center" },
// Empty
emptyContainer: {
Expand Down
41 changes: 41 additions & 0 deletions mobileapp/src/components/TransactionFilterSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const STATUS_OPTIONS: { label: string; value: TransactionFilters["status"] }[] =
{ label: "Failed", value: "failed" },
];

const ASSET_OPTIONS: string[] = ["XLM", "USDC", "USDT"];

function ChipGroup<T extends string>({
options,
value,
Expand Down Expand Up @@ -181,6 +183,45 @@ export function TransactionFilterSheet({
</TouchableOpacity>
</View>

{/* Asset */}
<Text style={styles.sectionLabel}>Asset</Text>
<View style={chipStyles.row}>
<TouchableOpacity
style={[chipStyles.chip, !local.asset && chipStyles.chipActive]}
onPress={() => update("asset", undefined)}
activeOpacity={0.8}
>
<Text
style={[
chipStyles.chipText,
!local.asset && chipStyles.chipTextActive,
]}
>
All
</Text>
</TouchableOpacity>
{ASSET_OPTIONS.map((asset) => (
<TouchableOpacity
key={asset}
style={[
chipStyles.chip,
local.asset === asset && chipStyles.chipActive,
]}
onPress={() => update("asset", asset)}
activeOpacity={0.8}
>
<Text
style={[
chipStyles.chipText,
local.asset === asset && chipStyles.chipTextActive,
]}
>
{asset}
</Text>
</TouchableOpacity>
))}
</View>

{/* Date range */}
<Text style={styles.sectionLabel}>Date From</Text>
<TextInput
Expand Down
Loading
Loading