{invoice.invoiceNumber}
@@ -344,6 +408,7 @@ const InvoiceRow = ({
// Update the table headers based on tab
const TableColumns = ({ type }: { type: "sent" | "received" }) => (
+ Select
Invoice #
{type === "sent" ? "Client" : "From"}
diff --git a/src/components/payment-section.tsx b/src/components/payment-section.tsx
index 27f73eab..7b701fd2 100644
--- a/src/components/payment-section.tsx
+++ b/src/components/payment-section.tsx
@@ -317,7 +317,7 @@ export function PaymentSection({ invoice }: PaymentSectionProps) {
walletProvider as ethers.providers.ExternalProvider,
);
- const signer = await ethersProvider.getSigner();
+ const signer = ethersProvider.getSigner();
try {
const paymentData = await payRequest({
diff --git a/src/lib/constants/chains.ts b/src/lib/constants/chains.ts
index b36a4f19..664b0bbd 100644
--- a/src/lib/constants/chains.ts
+++ b/src/lib/constants/chains.ts
@@ -16,6 +16,16 @@ export const CHAIN_TO_ID = {
POLYGON: 137,
};
+export const NETWORK_TO_ID = {
+ matic: 137,
+ base: 8453,
+ "arbitrum-one": 42161,
+ arbitrum: 42161,
+ optimism: 10,
+ mainnet: 1,
+ sepolia: 11155111,
+};
+
export const ID_TO_APPKIT_NETWORK = {
[sepolia.id]: sepolia,
[base.id]: base,
diff --git a/src/lib/invoice/batch-payment.ts b/src/lib/invoice/batch-payment.ts
new file mode 100644
index 00000000..d8e6fb60
--- /dev/null
+++ b/src/lib/invoice/batch-payment.ts
@@ -0,0 +1,126 @@
+import type { ethers, providers } from "ethers";
+
+interface BatchPaymentResult {
+ success: boolean;
+ error?: string;
+}
+
+export interface BatchPaymentData {
+ ERC20ApprovalTransactions: providers.TransactionRequest[];
+ batchPaymentTransaction: providers.TransactionRequest;
+}
+
+export const handleBatchPayment = async ({
+ signer,
+ batchPaymentData,
+ onSuccess,
+ onError,
+ onStatusChange,
+}: {
+ signer: ethers.Signer;
+ batchPaymentData: BatchPaymentData;
+ onSuccess?: () => void;
+ onError?: () => void;
+ onStatusChange?: (
+ status: "processing" | "success" | "error" | "idle",
+ ) => void;
+}): Promise => {
+ try {
+ onStatusChange?.("processing");
+
+ const isApprovalNeeded =
+ batchPaymentData.ERC20ApprovalTransactions.length > 0;
+
+ if (isApprovalNeeded) {
+ for (const approvalTransaction of batchPaymentData.ERC20ApprovalTransactions) {
+ try {
+ const tx = await signer.sendTransaction(approvalTransaction);
+ await tx.wait();
+ } catch (approvalError: any) {
+ if (approvalError?.code === 4001) {
+ onStatusChange?.("error");
+ onError?.();
+ return { success: false, error: "approval_rejected" };
+ }
+ throw approvalError;
+ }
+ }
+ }
+
+ try {
+ const tx = await signer.sendTransaction(
+ batchPaymentData.batchPaymentTransaction,
+ );
+ await tx.wait();
+
+ onStatusChange?.("success");
+ onSuccess?.();
+
+ return { success: true };
+ } catch (txError: any) {
+ if (txError?.code === 4001) {
+ onStatusChange?.("error");
+ onError?.();
+ return { success: false, error: "transaction_rejected" };
+ }
+ throw txError;
+ }
+ } 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)
+ ) {
+ onStatusChange?.("error");
+ onError?.();
+ return { success: false, error: "insufficient_funds" };
+ }
+
+ if (
+ error?.message?.toLowerCase().includes("network") ||
+ error?.code === "NETWORK_ERROR" ||
+ (error?.event === "error" && error?.type === "network")
+ ) {
+ onStatusChange?.("error");
+ onError?.();
+ return { success: false, error: "network_error" };
+ }
+
+ if (error?.reason) {
+ onStatusChange?.("error");
+ onError?.();
+ return { success: false, error: "contract_error" };
+ }
+
+ 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;
+ }
+ }
+
+ onStatusChange?.("error");
+ onError?.();
+ return { success: false, error: "unknown_error" };
+ }
+};
diff --git a/src/server/routers/invoice.ts b/src/server/routers/invoice.ts
index e575e133..ecd4e1fb 100644
--- a/src/server/routers/invoice.ts
+++ b/src/server/routers/invoice.ts
@@ -401,7 +401,7 @@ export const invoiceRouter = router({
const { paymentIntent, payload } = input;
const response = await apiClient.post(
- `/v2/request/${paymentIntent}/send`,
+ `/v2/request/payment-intents/${paymentIntent}`,
payload,
);
diff --git a/src/server/routers/payment.ts b/src/server/routers/payment.ts
index f347d70e..56eace4e 100644
--- a/src/server/routers/payment.ts
+++ b/src/server/routers/payment.ts
@@ -38,9 +38,15 @@ export const paymentRouter = router({
}),
batchPay: protectedProcedure
.input(
- batchPaymentFormSchema.extend({
- payer: z.string().optional(),
- }),
+ z
+ .object({
+ payouts: batchPaymentFormSchema.shape.payouts.optional(),
+ requestIds: z.array(z.string()).optional(),
+ payer: z.string().optional(),
+ })
+ .refine((data) => data.payouts || data.requestIds, {
+ message: "Either payouts or requestIds must be provided",
+ }),
)
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
@@ -53,12 +59,15 @@ export const paymentRouter = router({
}
const response = await apiClient.post("v2/payouts/batch", {
- requests: input.payouts.map((payout) => ({
- amount: payout.amount.toString(),
- payee: payout.payee,
- invoiceCurrency: payout.invoiceCurrency,
- paymentCurrency: payout.paymentCurrency,
- })),
+ requests: input.payouts
+ ? input.payouts.map((payout) => ({
+ amount: payout.amount.toString(),
+ payee: payout.payee,
+ invoiceCurrency: payout.invoiceCurrency,
+ paymentCurrency: payout.paymentCurrency,
+ }))
+ : undefined,
+ requestIds: input.requestIds,
payer: input.payer,
});