Skip to content
Open
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
14 changes: 8 additions & 6 deletions src/app/api/transaction/list/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ export async function GET(request: Request) {

// Get pagination parameters from URL
const url = new URL(request.url);
const page = url.searchParams.get("page") || "1";
const limit = url.searchParams.get("limit") || "10";
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
Comment on lines +25 to +26
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pagination parameters are parsed with parseInt but lack validation for invalid values. If parseInt fails (e.g., for non-numeric input), it returns NaN, which would be passed to the external API.

Consider adding validation:

const page = Math.max(1, parseInt(url.searchParams.get("page") || "1") || 1);
const limit = Math.max(1, Math.min(100, parseInt(url.searchParams.get("limit") || "10") || 10));

This ensures valid pagination parameters and prevents potential API errors or abuse (e.g., requesting extremely large limits).

Suggested change
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
const page = Math.max(1, parseInt(url.searchParams.get("page") || "1") || 1);
const limit = Math.max(1, Math.min(100, parseInt(url.searchParams.get("limit") || "10") || 10));

Copilot uses AI. Check for mistakes.

// Build API URL with pagination parameters
const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/list`);
apiUrl.searchParams.set("page", page);
apiUrl.searchParams.set("limit", limit);
apiUrl.searchParams.set("page", String(page));
apiUrl.searchParams.set("limit", String(limit));

// Make request to external API to get current user transactions
const response = await fetch(apiUrl.toString(), {
Expand All @@ -46,6 +46,7 @@ export async function GET(request: Request) {
return NextResponse.json(
{
transactions: [],
nextCursor: null,
message: "No transactions found",
},
{ status: 200 }
Expand All @@ -62,11 +63,12 @@ export async function GET(request: Request) {
);
}

const transactions = await response.json();
const data = await response.json();
const transactions = data.transactions || [];
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code extracts transactions from data.transactions, but if the external API returns transactions as a direct array (not wrapped in an object), this will result in an empty array. The previous implementation handled multiple response structures, but this change removes that flexibility.

Consider adding a check to handle both response formats:

const data = await response.json();
const transactions = Array.isArray(data) ? data : (data.transactions || []);

This ensures backward compatibility if the external API response format varies.

Suggested change
const transactions = data.transactions || [];
const transactions = Array.isArray(data) ? data : (data.transactions || []);

Copilot uses AI. Check for mistakes.

return NextResponse.json({
transactions,
message: "Transactions fetched successfully",
nextCursor: transactions.length === limit ? page + 1 : null,
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for determining nextCursor assumes that if transactions.length === limit, there are more pages available. However, this creates a false positive when the last page happens to have exactly limit items. The external API should ideally provide explicit pagination metadata (like hasMore or totalPages).

If the external API doesn't provide this information, consider tracking whether the next page returns empty results to properly set hasNextPage to false. The current implementation may show a "Load More" button that returns no results.

Copilot uses AI. Check for mistakes.
});
} catch (error) {
console.error("❌ Error fetching current transactions:", error);
Expand Down
71 changes: 56 additions & 15 deletions src/app/dashboard/history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { ApiTransaction } from "@/lib/types";
import { cn } from "@/lib/cn";
import ListItemSkeleton from "@/components/skeletons/ListItemSkeleton";
import { formatDate, formatSatoshis } from "@/lib/formatters";
import { useQuery } from "@tanstack/react-query";
import { useInfiniteQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query-keys";

export default function HistoryPage() {
Expand All @@ -16,14 +16,17 @@ export default function HistoryPage() {
const [selectedTransaction, setSelectedTransaction] = useState<ApiTransaction | null>(null);

const {
data: apiTransactions = [],
data,
error,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
} = useQuery<ApiTransaction[]>({
} = useInfiniteQuery({
queryKey: queryKeys.transactions,
queryFn: async () => {
const response = await fetch(`/api/transaction/list`, {
queryFn: async ({ pageParam = 1 }) => {
const response = await fetch(`/api/transaction/list?page=${pageParam}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
Expand All @@ -35,21 +38,16 @@ export default function HistoryPage() {
result.message || `Failed to fetch transactions: ${response.statusText}`
);
}
// Handle different response structures
if (Array.isArray(result)) {
return result;
}
if (result.transactions) {
return Array.isArray(result.transactions)
? result.transactions
: result.transactions?.transactions || [];
}
return [];
return result;
},
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextCursor,
enabled: !!authToken,
staleTime: 1000 * 60 * 5,
});

const apiTransactions = data?.pages.flatMap((page) => page.transactions) ?? [];
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data transformation assumes each page has a transactions property, but there's no safeguard if a page returns undefined or null for transactions. Consider adding a null check:

const apiTransactions = data?.pages.flatMap((page) => page.transactions || []) ?? [];

This prevents potential runtime errors if the API response structure is malformed.

Suggested change
const apiTransactions = data?.pages.flatMap((page) => page.transactions) ?? [];
const apiTransactions = data?.pages.flatMap((page) => page.transactions || []) ?? [];

Copilot uses AI. Check for mistakes.

const errorMessage = error instanceof Error ? error.message : null;

// Get settlement status info
Expand Down Expand Up @@ -316,6 +314,49 @@ export default function HistoryPage() {
</div>
))}

{/* Load More Button */}
{authToken && !errorMessage && apiTransactions.length > 0 && hasNextPage && (
<div className="mt-6 text-center">
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
className="rounded-xl border border-orange-500 bg-orange-500/10 px-6 py-3 font-medium text-orange-400 transition-colors hover:bg-orange-500/20 disabled:cursor-not-allowed disabled:opacity-50"
>
{isFetchingNextPage ? (
<div className="flex items-center gap-2">
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Loading more...
</div>
) : (
"Load More"
)}
</button>
</div>
)}

{/* Transaction Summary */}
{authToken && !errorMessage && apiTransactions.length > 0 && (
<div className="mt-4 text-center text-sm text-tma-text-secondary">
Showing {apiTransactions.length} transaction{apiTransactions.length !== 1 ? "s" : ""}
{!hasNextPage && " (all loaded)"}
</div>
)}

{/* Transaction Detail Modal */}
{selectedTransaction && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4">
Expand Down
Loading