Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
58f13f0
feat: Add Direct Payment page and component for instant payouts
aimensahnoun May 30, 2025
0712bfe
feat: Introduce PayoutTabs component for managing single and batch pa…
aimensahnoun May 30, 2025
95a14af
refactor: Replace INVOICE_CURRENCIES with EXTENDED_INVOICE_CURRENCIES…
aimensahnoun May 30, 2025
3f00285
refactor: Update currency types to use ExtendedInvoiceCurrency and ad…
aimensahnoun May 30, 2025
37bfd7c
feat: Implement Batch Payout component for processing multiple paymen…
aimensahnoun May 30, 2025
388f6de
feat: Add batch payment functionality to BatchPayout component with t…
aimensahnoun May 30, 2025
774b817
feat: Enhance BatchPayout component with table layout for payments, v…
aimensahnoun May 30, 2025
bcb7c3c
Merge branch 'main' of github.com:RequestNetwork/easy-invoice into 27…
aimensahnoun Jun 3, 2025
534b0e9
feat: Introduce DashboardView component for enhanced invoice manageme…
aimensahnoun Jun 3, 2025
16be055
feat: Implement batch payment handling logic in BatchPayout and Dashb…
aimensahnoun Jun 3, 2025
7325916
fix: Update setLastSelectedNetwork type to accept null value in Invoi…
aimensahnoun Jun 3, 2025
fa57139
feat: Add loading indicator and update button text in DashboardView; …
aimensahnoun Jun 4, 2025
027dee9
Merge branch 'main' of github.com:RequestNetwork/easy-invoice into 28…
aimensahnoun Jun 4, 2025
e7b9980
refactor: streamline batch payment handling in BatchPayout and Dashbo…
aimensahnoun Jun 4, 2025
1855a34
refactor: replace extended invoice currency references with standard …
aimensahnoun Jun 4, 2025
8b53219
Merge branch 'main' into 28-easyinvoice---one-time-batch-payments-fro…
aimensahnoun Jun 4, 2025
395ed00
feat: enhance network switching functionality in DashboardView component
aimensahnoun Jun 4, 2025
e9ba614
Merge branch '28-easyinvoice---one-time-batch-payments-from-the-invoi…
aimensahnoun Jun 4, 2025
ca82d80
refactor: enhance type definitions for batch payment handling
aimensahnoun Jun 4, 2025
63a8034
refactor: improve batch payout calculations and streamline payment ha…
aimensahnoun Jun 5, 2025
af3e3e5
fix: handle network selection errors in DashboardView component
aimensahnoun Jun 5, 2025
8207c36
refactor: streamline error handling in batch payment process
aimensahnoun Jun 5, 2025
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
18 changes: 2 additions & 16 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { BackgroundWrapper } from "@/components/background-wrapper";
import { DashboardView } from "@/components/dashboard-view";
import { Footer } from "@/components/footer";
import { Header } from "@/components/header";
import { InvoiceTable } from "@/components/invoice-table";
import { getCurrentSession } from "@/server/auth";
import { api } from "@/trpc/server";
import { PlusCircle } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";

export const metadata: Metadata = {
Expand All @@ -31,19 +29,7 @@ export default async function DashboardPage() {
>
<Header user={user} />
<main className="flex-grow flex flex-col max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 z-10">
<div className="flex justify-between items-center mb-8">
<h1 className="text-4xl font-bold tracking-tight">Dashboard</h1>

<Link
href="/invoices/create"
className="bg-black hover:bg-zinc-800 text-white transition-colors px-4 py-2 rounded-md flex items-center"
>
<PlusCircle className="mr-2 h-4 w-4" />
Create Invoice
</Link>
</div>

<InvoiceTable initialInvoices={invoices} />
<DashboardView invoices={invoices} />
</main>
<Footer />
</BackgroundWrapper>
Expand Down
173 changes: 45 additions & 128 deletions src/components/batch-payout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
formatCurrencyLabel,
getPaymentCurrenciesForPayout,
} from "@/lib/constants/currencies";
import { handleBatchPayment } from "@/lib/invoice/batch-payment";
import {
type BatchPaymentFormValues,
batchPaymentFormSchema,
Expand Down Expand Up @@ -146,7 +147,7 @@ export function BatchPayout() {
const duplicatePayment = (index: number) => {
if (fields.length < MAX_PAYMENTS) {
const payout = form.getValues(`payouts.${index}`);
append({ ...payout, payee: "" });
append({ ...payout });
}
};

Expand All @@ -160,25 +161,35 @@ export function BatchPayout() {
const uniqueAddresses = new Set(
payouts
.filter((payout) => payout.payee)
.map((payout) => ethers.utils.getAddress(payout.payee)),
.map((payout) => payout.payee.toLowerCase()),
);
return uniqueAddresses.size;
};

const getTotalsByCurrency = () => {
const payouts = form.watch("payouts");
const totals: Record<string, ethers.BigNumber> = {};

for (const payout of payouts) {
if (payout.amount > 0) {
const currency = payout.invoiceCurrency;
// Convert amount to BigNumber with 18 decimals of precision

const amount = ethers.utils.parseUnits(payout.amount.toString(), 18);
totals[currency] = (totals[currency] || ethers.BigNumber.from(0)).add(
amount,
);
}
}
return totals;

const humanReadableTotals: Record<string, string> = {};
for (const [currency, bigNumberTotal] of Object.entries(totals)) {
humanReadableTotals[currency] = ethers.utils.formatUnits(
bigNumberTotal,
18,
);
}

return humanReadableTotals;
};

const onSubmit = async (data: BatchPaymentFormValues) => {
Expand All @@ -187,8 +198,6 @@ export function BatchPayout() {
return;
}

setPaymentStatus("processing");

if (!walletProvider) {
toast.error("Please connect your wallet first");
return;
Expand All @@ -197,133 +206,43 @@ export function BatchPayout() {
try {
const ethersProvider = new ethers.providers.Web3Provider(walletProvider);

const signer = await ethersProvider.getSigner();

toast.info("Initiating batch payment...");
const signer = ethersProvider.getSigner();

const batchPaymentData = await batchPay({
...data,
payer: address,
});

toast.info("Initiating payment...");

const isApprovalNeeded =
batchPaymentData.ERC20ApprovalTransactions.length > 0;

if (isApprovalNeeded) {
toast.info("Approval required", {
description: "Please approve the transaction in your wallet",
});

for (const approvalTransaction of batchPaymentData.ERC20ApprovalTransactions) {
try {
const tx = await signer.sendTransaction(approvalTransaction);
await tx.wait();
} catch (approvalError: any) {
if (approvalError?.code === 4001) {
toast.error("Approval rejected", {
description: "You rejected the token approval in your wallet.",
});
setPaymentStatus("error");
return;
}
throw approvalError; // Re-throw to be caught by main error handler
}
}
}

toast.info("Sending batch payment...");

try {
const tx = await signer.sendTransaction(
batchPaymentData.batchPaymentTransaction,
);
await tx.wait();

toast.success("Batch payment successful", {
description: `Successfully processed ${data.payouts.length} payments`,
});

setPaymentStatus("success");

form.reset({
payouts: [
{
payee: "",
amount: 0,
invoiceCurrency: "USD",
paymentCurrency: "ETH-sepolia-sepolia",
},
],
});
setPaymentStatus("idle");
} catch (txError: any) {
if (txError?.code === 4001) {
toast.error("Transaction rejected", {
description: "You rejected the batch payment in your wallet.",
const result = await handleBatchPayment({
signer,
batchPaymentData,
onSuccess: () => {
toast.success("Batch payment successful", {
description: `Successfully processed ${data.payouts.length} payments`,
});
} else {
throw txError; // Re-throw to be caught by main error handler
}
setPaymentStatus("error");
}
} catch (error: any) {
console.error("Payment error:", error);

if (
error?.code === "INSUFFICIENT_FUNDS" ||
error?.message?.toLowerCase().includes("insufficient funds") ||
(error?.code === "SERVER_ERROR" && error?.error?.code === -32000)
) {
toast.error("Insufficient funds", {
description:
"You do not have enough funds to complete this batch payment.",
});
} else if (
error?.message?.toLowerCase().includes("network") ||
error?.code === "NETWORK_ERROR" ||
error?.code === "NETWORK_ERROR" ||
(error?.event === "error" && error?.type === "network")
) {
toast.error("Network error", {
description:
"Network error. Please check your connection and try again.",
});
} else if (error?.reason) {
toast.error("Transaction failed", {
description: `Smart contract error: ${error.reason}`,
});
} else {
let errorMessage =
"There was an error processing your batch payment. Please try again.";

if (error && typeof error === "object") {
if (
"data" in error &&
error.data &&
typeof error.data === "object" &&
"message" in error.data
) {
errorMessage = error.data.message || errorMessage;
} else if ("message" in error) {
errorMessage = error.message || errorMessage;
} else if ("response" in error && error.response?.data?.message) {
errorMessage = error.response.data.message;
} else if (
"error" in error &&
typeof error.error === "object" &&
error.error &&
"message" in error.error
) {
errorMessage = error.error.message;
}
}

toast.error("Batch payment failed", {
description: errorMessage,
});
form.reset({
payouts: [
{
payee: "",
amount: 0,
invoiceCurrency: "USD",
paymentCurrency: "ETH-sepolia-sepolia",
},
],
});
setPaymentStatus("idle");
},
onError: () => {
setPaymentStatus("error");
},
onStatusChange: setPaymentStatus,
});

if (!result.success) {
console.error("Batch payment failed:", result.error);
}
} catch (error) {
console.error("Failed to initiate batch payment:", error);
setPaymentStatus("error");
}
};
Expand Down Expand Up @@ -626,9 +545,7 @@ export function BatchPayout() {
<span className="text-zinc-600">
{formatCurrencyLabel(currency)}:
</span>
<span className="font-medium">
{ethers.utils.formatUnits(total, 18)}
</span>
<span className="font-medium">{total}</span>
</div>
),
)
Expand Down
Loading