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
34 changes: 34 additions & 0 deletions mobileapp/__tests__/TransferConfirmationCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,38 @@ describe("TransferConfirmationCard", () => {
expect(getByText("Privacy")).toBeTruthy();
expect(getByText("FRIENDS")).toBeTruthy();
});

it("renders real name and username parameters when real name is provided", () => {
const { getByText, getByTestId } = render(
<TransferConfirmationCard
recipient={{
...mockRecipient,
name: "Tolu Adebayo",
isVerified: true,
}}
amount="500"
tokenSymbol="XLM"
/>
);

expect(getByTestId("confirmation-real-name")).toBeTruthy();
expect(getByText("Tolu Adebayo")).toBeTruthy();
expect(getByText("tolu.zaps")).toBeTruthy();
});

it("displays verification badge for trusted profiles", () => {
const { getByLabelText } = render(
<TransferConfirmationCard
recipient={{
...mockRecipient,
display_name: "Trusted User",
isVerified: true,
}}
amount="250"
tokenSymbol="USDC"
/>
);

expect(getByLabelText("Verified recipient")).toBeTruthy();
});
});
49 changes: 49 additions & 0 deletions mobileapp/__tests__/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import {
completePrivySignup,
linkPrivyAddress,
stellarAddressFromPrivyPublicKey,
getRecentRecipients,
saveRecentRecipient,
getCachedRecentRecipientsSync,
clearRecentRecipientsCache,
} from "../src/services/api";

jest.mock("@react-native-async-storage/async-storage", () => ({
Expand Down Expand Up @@ -181,3 +185,48 @@ describe("Privy signup API service", () => {
expect(mockAsyncStorage.setItem).not.toHaveBeenCalled();
});
});

describe("Recent recipient local cache helper (Issue #914)", () => {
beforeEach(async () => {
jest.clearAllMocks();
await clearRecentRecipientsCache();
});

it("stores and fetches recent recipients from local persistent storage", async () => {
await saveRecentRecipient("alice.zaps");
await saveRecentRecipient("bob.zaps");

const cached = await getRecentRecipients();
expect(cached).toEqual(["bob.zaps", "alice.zaps"]);
expect(mockAsyncStorage.setItem).toHaveBeenCalledWith(
"recent_recipient_usernames",
JSON.stringify(["bob.zaps", "alice.zaps"])
);
});

it("fetches from cache instantly without waiting for network", () => {
saveRecentRecipient("instant.zaps");
const syncResult = getCachedRecentRecipientsSync();
expect(syncResult).toContain("instant.zaps");
});

it("limits cached contacts to 20 maximum", async () => {
for (let i = 1; i <= 25; i++) {
await saveRecentRecipient(`user${i}.zaps`);
}

const cached = await getRecentRecipients();
expect(cached.length).toBe(20);
expect(cached[0]).toBe("user25.zaps");
expect(cached).not.toContain("user1.zaps");
});

it("deduplicates and moves re-used recipient to the top", async () => {
await saveRecentRecipient("user1.zaps");
await saveRecentRecipient("user2.zaps");
await saveRecentRecipient("user1.zaps");

const cached = await getRecentRecipients();
expect(cached).toEqual(["user1.zaps", "user2.zaps"]);
});
});
126 changes: 126 additions & 0 deletions mobileapp/__tests__/transactions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react-native";
import TransactionsScreen from "../app/(personal)/transactions";
import type { Transaction } from "../src/types/transaction";

jest.mock("@react-native-async-storage/async-storage", () =>
require("@react-native-async-storage/async-storage/jest/async-storage-mock")
);

jest.mock("expo-router", () => ({
useRouter: jest.fn(() => ({
push: jest.fn(),
back: jest.fn(),
})),
}));

jest.mock("../src/services/stellarWallet", () => ({
getLocalKeypair: jest.fn(() => null),
checkFreighter: jest.fn(() => Promise.resolve(false)),
speedUpTransaction: jest.fn(() => Promise.resolve()),
}));

jest.mock("../src/components/TransactionFilterSheet", () => ({
TransactionFilterSheet: () => null,
}));

const mockTransactions: Transaction[] = [
{
id: "tx-sdp-1",
type: "mass_payout",
status: "completed",
amount: "100.00",
asset: "USDC",
fiatValue: "150,000",
fiatCurrency: "NGN",
address: "GABC1234567890DEF1234567890ABCDEF1234567890ABCDEF1234567890AB",
addressLabel: "Relief Payout",
timestamp: new Date().toISOString(),
senderName: "Mercy Relief Org",
senderAnchor: "Anchor Global",
},
{
id: "tx-standard-1",
type: "sent",
status: "completed",
amount: "25.00",
asset: "XLM",
fiatValue: "5,000",
fiatCurrency: "NGN",
address: "GDEF1234567890DEF1234567890ABCDEF1234567890ABCDEF1234567890CD",
addressLabel: "Tolu",
timestamp: new Date().toISOString(),
},
{
id: "tx-standard-2",
type: "received",
status: "completed",
amount: "50.00",
asset: "USDC",
fiatValue: "75,000",
fiatCurrency: "NGN",
address: "GXYZ1234567890DEF1234567890ABCDEF1234567890ABCDEF1234567890EF",
addressLabel: "Amina",
timestamp: new Date().toISOString(),
},
];

const mockSetFilters = jest.fn();
const mockRefresh = jest.fn();
const mockLoadMore = jest.fn();

jest.mock("../src/hooks/useTransactions", () => ({
useTransactions: () => ({
transactions: mockTransactions,
loading: false,
refreshing: false,
loadingMore: false,
filters: { type: "all", status: "all", search: "" },
setFilters: mockSetFilters,
refresh: mockRefresh,
loadMore: mockLoadMore,
}),
}));

describe("TransactionsScreen Disbursement History (Issue #921)", () => {
it("renders distinct section grouping for Received Mass Payouts separate from standard transfers", () => {
const { getByTestId, getByText } = render(<TransactionsScreen />);

expect(getByTestId("mass-payout-section-header")).toBeTruthy();
expect(getByText("Received Mass Payouts (SDP)")).toBeTruthy();
expect(getByText("Standard Transfers")).toBeTruthy();
});

it("renders unique badge and label for SDP disbursements", () => {
const { getByTestId, getByText } = render(<TransactionsScreen />);

expect(getByTestId("sdp-disbursement-badge")).toBeTruthy();
expect(getByText("SDP Disbursement")).toBeTruthy();
});

it("renders sender company and anchor information for mass payouts", () => {
const { getByText, getByTestId } = render(<TransactionsScreen />);

expect(getByText("Mercy Relief Org")).toBeTruthy();
expect(getByTestId("sender-anchor-info")).toBeTruthy();
expect(getByText("Anchor: Anchor Global")).toBeTruthy();
});

it("applies conditional list item rendering based on transaction type", () => {
const { getByTestId } = render(<TransactionsScreen />);

expect(getByTestId("tx-row-tx-sdp-1")).toBeTruthy();
expect(getByTestId("tx-row-tx-standard-1")).toBeTruthy();
expect(getByTestId("tx-row-tx-standard-2")).toBeTruthy();
});

it("switches view when SDP Disbursements tab is selected", () => {
const { getByTestId, queryByText } = render(<TransactionsScreen />);

const sdpTab = getByTestId("mass-payout-tab");
fireEvent.press(sdpTab);

expect(getByTestId("mass-payout-section-header")).toBeTruthy();
expect(queryByText("Standard Transfers")).toBeNull();
});
});
12 changes: 6 additions & 6 deletions mobileapp/app/(personal)/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,16 @@ const TransactionRow = React.memo(function TransactionRow({
{isMassPayout && item.senderName ? item.senderName : label}
</Text>
{isMassPayout && (
<View style={styles.massPayoutBadge}>
<Ionicons name="people" size={10} color="#7C3AED" />
<Text style={styles.massPayoutBadgeText}>Mass Payout</Text>
<View style={styles.massPayoutBadge} testID="sdp-disbursement-badge">
<Ionicons name="shield-checkmark" size={10} color="#7C3AED" />
<Text style={styles.massPayoutBadgeText}>SDP Disbursement</Text>
</View>
)}
</View>
{/* Anchor/sender info for SDP disbursements */}
{isMassPayout && item.senderAnchor && (
<Text style={styles.senderAnchor} numberOfLines={1}>
via {item.senderAnchor}
{isMassPayout && (
<Text style={styles.senderAnchor} numberOfLines={1} testID="sender-anchor-info">
{item.senderAnchor ? `via ${item.senderAnchor}` : "Stellar Disbursement Platform"}
</Text>
)}
<View style={styles.txMeta}>
Expand Down
37 changes: 32 additions & 5 deletions mobileapp/app/(personal)/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@

// Balance figures derived from the raw XLM tokens, redrawn in the active
// denomination the moment the currency preference changes.
const availableBalanceDisplay = useMemo(

Check failure on line 267 in mobileapp/app/(personal)/home.tsx

View workflow job for this annotation

GitHub Actions / Frontend (React Native)

'availableBalanceDisplay' is assigned a value but never used
() => formatBalance(availableBalanceXlm, displayCurrency),
[availableBalanceXlm, displayCurrency]
);
Expand All @@ -273,7 +273,7 @@
[totalYieldEarnedXlm, displayCurrency]
);

const toggleCurrency = useCallback(() => {

Check failure on line 276 in mobileapp/app/(personal)/home.tsx

View workflow job for this annotation

GitHub Actions / Frontend (React Native)

'toggleCurrency' is assigned a value but never used
void Haptics.selectionAsync().catch(() => undefined);
setDisplayCurrency((prev) => {
const next = prev === "NGN" ? "XLM" : "NGN";
Expand Down Expand Up @@ -1027,14 +1027,22 @@
</View>
<View style={styles.autoEarnTitleWrap}>
<View style={styles.autoEarnTitleRow}>
<Text style={styles.autoEarnCardTitle}>
Auto-Earn on Idle Funds
</Text>
<TouchableOpacity
onPress={openAutoEarnDescModal}
activeOpacity={0.8}
accessibilityLabel="Learn about Auto-Earn yield rules"
accessibilityRole="button"
>
<Text style={styles.autoEarnCardTitle}>
Auto-Earn on Idle Funds
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={toggleAutoEarnTooltip}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
accessibilityLabel="Learn about Auto-Earn"
accessibilityLabel="Quick definition tooltip"
accessibilityRole="button"
testID="auto-earn-tooltip-trigger"
>
<Ionicons
name="information-circle-outline"
Expand All @@ -1051,14 +1059,33 @@
</View>
<Switch
value={autoYieldEnabled}
onValueChange={handleAutoYieldToggle}
onValueChange={(val: boolean) => {
if (val) {
openAutoEarnDescModal();
} else {
void handleAutoYieldToggle(false);
}
}}
disabled={autoEarnSyncing}
trackColor={{ false: "#E2E8F0", true: "#34D399" }}
thumbColor={COLORS.white}
accessibilityLabel="Toggle Auto-Earn on Idle Funds"
accessibilityRole="switch"
/>
</View>
{autoEarnTooltipVisible && (
<View style={styles.autoEarnTooltip} testID="auto-earn-tooltip">
<Ionicons
name="information-circle"
size={16}
color="#166534"
style={{ marginRight: 6 }}
/>
<Text style={styles.autoEarnTooltipText}>
Auto-Earn automatically sweeps idle funds into yield-earning vaults at competitive APYs while remaining instantly spendable.
</Text>
</View>
)}
{autoEarnSyncing && (
<View style={styles.autoEarnSyncingRow}>
<Ionicons name="sync" size={13} color="#64748B" />
Expand Down Expand Up @@ -1460,7 +1487,7 @@

return (
<>
{projections.map((proj, idx) => {

Check failure on line 1490 in mobileapp/app/(personal)/home.tsx

View workflow job for this annotation

GitHub Actions / Frontend (React Native)

'idx' is defined but never used. Allowed unused args must match /^_/u
const barHeight = (proj.value / maxValue) * 110;
return (
<View key={proj.label} style={styles.compoundingBar}>
Expand Down
Loading
Loading