diff --git a/mobileapp/__tests__/homeYield.test.tsx b/mobileapp/__tests__/homeYield.test.tsx
index 1d47ddd..a8ef38e 100644
--- a/mobileapp/__tests__/homeYield.test.tsx
+++ b/mobileapp/__tests__/homeYield.test.tsx
@@ -61,6 +61,7 @@ jest.mock("@expo/vector-icons", () => ({
}));
const mockFetchYieldBalance = jest.fn();
+const mockFetchYieldEarningsHistory = jest.fn();
const mockUpdateAutoEarn = jest.fn();
jest.mock("../src/services/socialService", () => ({
@@ -72,6 +73,8 @@ jest.mock("../src/services/socialService", () => ({
jest.mock("../src/services/api", () => ({
fetchYieldBalance: (...args: unknown[]) => mockFetchYieldBalance(...args),
+ fetchYieldEarningsHistory: (...args: unknown[]) =>
+ mockFetchYieldEarningsHistory(...args),
updateAutoEarn: (...args: unknown[]) => mockUpdateAutoEarn(...args),
}));
@@ -209,6 +212,7 @@ describe("HomeScreen auto-earn haptics (#907)", () => {
beforeEach(() => {
jest.clearAllMocks();
mockUpdateAutoEarn.mockResolvedValue(undefined);
+ mockFetchYieldEarningsHistory.mockResolvedValue([]);
});
it("fires a light haptic when auto-earn is switched off", async () => {
@@ -273,4 +277,64 @@ describe("HomeScreen auto-earn haptics (#907)", () => {
expect(impactAsync).toHaveBeenCalledTimes(1);
});
+
+ it("updates the Auto-Earn widget after saving the preference", async () => {
+ mockFetchYieldBalance.mockResolvedValue(loadedYield);
+ const { getByLabelText, findByText } = render();
+ await findByText("Your money is working");
+
+ await act(async () => {
+ fireEvent(
+ getByLabelText("Toggle Auto-Earn on Idle Funds"),
+ "valueChange",
+ false
+ );
+ });
+
+ expect(mockUpdateAutoEarn).toHaveBeenCalledWith(false);
+ expect(await findByText("Enable to sweep idle balance into yield")).toBeTruthy();
+ });
+
+ it("rolls back the Auto-Earn widget when the update request fails", async () => {
+ mockFetchYieldBalance.mockResolvedValue(loadedYield);
+ mockUpdateAutoEarn.mockRejectedValue(new Error("Unavailable"));
+ const { getByLabelText, findByText } = render();
+ await findByText("Your money is working");
+
+ await act(async () => {
+ fireEvent(
+ getByLabelText("Toggle Auto-Earn on Idle Funds"),
+ "valueChange",
+ false
+ );
+ });
+
+ expect(await findByText("Idle balance is actively earning yield")).toBeTruthy();
+ });
+
+ it("plots monthly earned and reward history from the API", async () => {
+ const currentPeriod = new Date().toISOString().slice(0, 7);
+ mockFetchYieldBalance.mockResolvedValue(loadedYield);
+ mockFetchYieldEarningsHistory.mockResolvedValue([
+ {
+ type: "EARNED",
+ amount: 500_000,
+ asset: "XLM",
+ created_at: `${currentPeriod}-10 12:00:00`,
+ },
+ {
+ type: "REWARD",
+ amount: 250_000,
+ asset: "XLM",
+ created_at: `${currentPeriod}-12 12:00:00`,
+ },
+ ]);
+ const { getByTestId, getByText, findByText } = render();
+ await findByText("Your money is working");
+ fireEvent.press(getByTestId("earning-balance-card"));
+
+ await findByText(/₦1,200\.00/);
+ expect(mockFetchYieldEarningsHistory).toHaveBeenCalled();
+ expect(getByText("Monthly earnings")).toBeTruthy();
+ });
});
diff --git a/mobileapp/__tests__/yieldApi.test.ts b/mobileapp/__tests__/yieldApi.test.ts
index 682785a..2bd2237 100644
--- a/mobileapp/__tests__/yieldApi.test.ts
+++ b/mobileapp/__tests__/yieldApi.test.ts
@@ -1,5 +1,9 @@
import * as SecureStore from "expo-secure-store";
-import { fetchYieldBalance, updateAutoEarn } from "../src/services/api";
+import {
+ fetchYieldBalance,
+ fetchYieldEarningsHistory,
+ updateAutoEarn,
+} from "../src/services/api";
jest.mock("expo-secure-store", () => ({
getItemAsync: jest.fn().mockResolvedValue(null),
@@ -75,6 +79,40 @@ describe("yield API", () => {
);
});
+ it("loads all pages of earned and reward history", async () => {
+ mockFetch
+ .mockResolvedValueOnce(
+ jsonResponse({
+ items: [
+ { type: "EARNED", amount: 1_000_000, asset: "XLM", created_at: "2026-10-01 00:00:00" },
+ ],
+ total: 101,
+ })
+ )
+ .mockResolvedValueOnce(
+ jsonResponse({
+ items: [
+ { type: "REWARD", amount: 500_000, asset: "XLM", created_at: "2026-10-02 00:00:00" },
+ ],
+ total: 1,
+ })
+ )
+ .mockResolvedValueOnce(jsonResponse({ items: [], total: 101 }));
+
+ const history = await fetchYieldEarningsHistory("2025-11-01", "2026-10-22");
+
+ expect(history).toHaveLength(2);
+ expect(mockFetch).toHaveBeenCalledTimes(3);
+ const requestUrls = mockFetch.mock.calls.map(([url]) => String(url));
+ expect(requestUrls).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining("action=EARNED"),
+ expect.stringContaining("action=REWARD"),
+ expect.stringContaining("offset=1"),
+ ])
+ );
+ });
+
it("throws when saving the Auto-Earn setting fails", async () => {
mockFetch.mockResolvedValueOnce(jsonResponse({}, 500));
diff --git a/mobileapp/app/(personal)/home.tsx b/mobileapp/app/(personal)/home.tsx
index 8569459..d9cfb80 100644
--- a/mobileapp/app/(personal)/home.tsx
+++ b/mobileapp/app/(personal)/home.tsx
@@ -41,7 +41,11 @@ import {
fetchFeed,
addComment,
} from "../../src/services/socialService";
-import { fetchYieldBalance, updateAutoEarn } from "../../src/services/api";
+import {
+ fetchYieldBalance,
+ fetchYieldEarningsHistory,
+ updateAutoEarn,
+} from "../../src/services/api";
import {
BalanceCurrency,
formatBalance,
@@ -89,23 +93,25 @@ interface Comment {
interface MonthlyEarningPoint {
month: string;
+ period: string;
amount: number;
}
-const MONTHLY_EARNINGS: MonthlyEarningPoint[] = [
- { month: "Jan", amount: 140 },
- { month: "Feb", amount: 172 },
- { month: "Mar", amount: 205 },
- { month: "Apr", amount: 238 },
- { month: "May", amount: 261 },
- { month: "Jun", amount: 296 },
- { month: "Jul", amount: 317 },
- { month: "Aug", amount: 334 },
- { month: "Sep", amount: 352 },
- { month: "Oct", amount: 365 },
- { month: "Nov", amount: 388 },
- { month: "Dec", amount: 412 },
-];
+const createRecentMonthlyEarnings = (): MonthlyEarningPoint[] => {
+ const now = new Date();
+ return Array.from({ length: 12 }, (_, index) => {
+ const date = new Date(
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 11 + index, 1)
+ );
+ const year = date.getUTCFullYear();
+ const month = date.getUTCMonth();
+ return {
+ month: date.toLocaleString("en-US", { month: "short", timeZone: "UTC" }),
+ period: `${year}-${String(month + 1).padStart(2, "0")}`,
+ amount: 0,
+ };
+ });
+};
const INITIAL_FEED: FeedItem[] = [
{
@@ -161,9 +167,13 @@ export default function HomeScreen() {
const [yieldError, setYieldError] = useState("");
const [yieldRetryCount, setYieldRetryCount] = useState(0);
const [chartWidth, setChartWidth] = useState(0);
- const [selectedMonthIndex, setSelectedMonthIndex] = useState(
- MONTHLY_EARNINGS.length - 1
+ const [monthlyEarnings, setMonthlyEarnings] = useState(
+ createRecentMonthlyEarnings
);
+ const [earningsHistoryStatus, setEarningsHistoryStatus] = useState<
+ "idle" | "loading" | "success" | "error"
+ >("idle");
+ const [selectedMonthIndex, setSelectedMonthIndex] = useState(11);
const [earningsModalVisible, setEarningsModalVisible] = useState(false);
const [autoYieldEnabled, setAutoYieldEnabled] = useState(true);
const [autoEarnSyncing, setAutoEarnSyncing] = useState(false);
@@ -182,6 +192,7 @@ export default function HomeScreen() {
const earningPulseAnim = useRef(new Animated.Value(1)).current;
const earningOuterGlowAnim = useRef(new Animated.Value(0)).current;
const yieldRequestRef = useRef(0);
+ const earningsHistoryRequestRef = useRef(0);
// Animated values for like heart scale per feed item
const scaleAnims = useRef