Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ecccfb0
feat: Add payment route selection component and associated logos
aimensahnoun Mar 17, 2025
cc4d7c1
fix: Set initial status to pending for new payment requests in webhook
aimensahnoun Mar 17, 2025
9cced5f
feat: Allow decimal values for price input in invoice form
aimensahnoun Mar 17, 2025
f9a6c3e
refactor: Update price formatting to use string conversion for consis…
aimensahnoun Mar 17, 2025
59ac7d0
feat: Add USDC-base currency to invoice and payment options
aimensahnoun Mar 17, 2025
be01eed
feat: Enhance payment route component with dynamic route types and ba…
aimensahnoun Mar 18, 2025
b2b1023
feat: Implement payment intent handling and network switching in paym…
aimensahnoun Mar 18, 2025
8005847
feat: Add alert for mainnet currency selection in invoice form and im…
aimensahnoun Mar 19, 2025
b8967c9
feat: Add warning for real cryptocurrency transactions in payment sec…
aimensahnoun Mar 19, 2025
36b72ad
Merge branch 'main' of github.com:RequestNetwork/easy-invoice into 36…
aimensahnoun Mar 19, 2025
53214e9
feat: Update payment route component to include native token handling…
aimensahnoun Mar 20, 2025
bc16ae5
Merge branch 'main' into 36-easyinvoice---integrate-payment-routes-an…
aimensahnoun Mar 24, 2025
b8fe2ca
Merge branch '36-easyinvoice---integrate-payment-routes-and-crosschai…
aimensahnoun Mar 24, 2025
34e1fdb
feat: Enhance payment processing flow with improved status handling a…
aimensahnoun Mar 24, 2025
b56659f
feat: Integrate invoice number generation and count retrieval in webh…
aimensahnoun Mar 24, 2025
700fd7a
feat: Replace request logo with sepolia logo and update payment inten…
aimensahnoun Mar 25, 2025
91d0bd4
Merge branch 'main' into 36-easyinvoice---integrate-payment-routes-an…
aimensahnoun Mar 25, 2025
7c7a783
feat: Refactor payment handling by introducing separate functions for…
aimensahnoun Mar 25, 2025
ab17f7c
fix: Handle network switching errors in payment section with user not…
aimensahnoun Mar 25, 2025
8f71889
Merge branch '36-easyinvoice---integrate-payment-routes-and-crosschai…
aimensahnoun Mar 25, 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
Binary file added public/arbitrum-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/base-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/ethereum-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/optimism-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/polygon-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/sepolia-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/app/api/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import crypto from "node:crypto";
import { getInvoiceCount } from "@/lib/invoice";
import { generateInvoiceNumber } from "@/lib/invoice/client";
import { db } from "@/server/db";
import { requestTable } from "@/server/db/schema";
import { eq } from "drizzle-orm";
Expand Down Expand Up @@ -91,9 +93,14 @@ export async function POST(req: Request) {
const newDueDate = new Date(now);
newDueDate.setDate(now.getDate() + daysDifference);

const invoiceCount = await getInvoiceCount(originalRequest.userId);

const invoiceNumber = generateInvoiceNumber(invoiceCount);

await tx.insert(requestTable).values({
id: ulid(),
...requestWithoutId,
invoiceNumber,
issuedDate: now.toISOString(),
dueDate: newDueDate.toISOString(),
paymentReference: paymentReference,
Expand Down
8 changes: 4 additions & 4 deletions src/app/invoices/[ID]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ export default async function PaymentPage({
{item.quantity}
</td>
<td className="py-3 text-right text-sm">
{item.price.toFixed(2)}
{item.price.toString()}
</td>
<td className="py-3 text-right text-sm">
{(item.quantity * item.price).toFixed(2)}
{(item.quantity * item.price).toString()}
</td>
</tr>
))}
Expand All @@ -184,14 +184,14 @@ export default async function PaymentPage({
<div className="flex justify-between py-2">
<span className="text-sm text-neutral-600">Subtotal</span>
<span className="text-sm">
{Number(invoice.amount).toFixed(2)}
{Number(invoice.amount).toString()}
</span>
</div>
<div className="flex justify-between py-2 border-t border-neutral-200">
<span className="text-sm font-medium">Total</span>
<div>
<div className="text-sm text-right font-medium">
{Number(invoice.amount).toFixed(2)}
{Number(invoice.amount).toString()}
</div>
<div className="text-xs text-neutral-500">
{formatCurrencyLabel(invoice.invoiceCurrency)}
Expand Down
11 changes: 9 additions & 2 deletions src/components/app-kit.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"use client";

import { Ethers5Adapter } from "@reown/appkit-adapter-ethers5";
import { sepolia } from "@reown/appkit/networks";
import {
arbitrum,
base,
mainnet,
optimism,
polygon,
sepolia,
} from "@reown/appkit/networks";
import { createAppKit } from "@reown/appkit/react";

const metadata = {
Expand All @@ -20,7 +27,7 @@ if (!projectId) {
createAppKit({
adapters: [new Ethers5Adapter()],
metadata,
networks: [sepolia],
networks: [sepolia, base, mainnet, arbitrum, optimism, polygon],
projectId,
features: {
analytics: false,
Expand Down
34 changes: 33 additions & 1 deletion src/components/invoice-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import { Textarea } from "@/components/ui/textarea";
import {
INVOICE_CURRENCIES,
type InvoiceCurrency,
MAINNET_CURRENCIES,
type MainnetCurrency,
formatCurrencyLabel,
getPaymentCurrenciesForInvoice,
} from "@/lib/currencies";
import type { InvoiceFormValues } from "@/lib/schemas/invoice";
import { Plus, Trash2 } from "lucide-react";
import { Plus, Terminal, Trash2 } from "lucide-react";
import type { UseFormReturn } from "react-hook-form";
import { useFieldArray } from "react-hook-form";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";

type RecurringFrequency = "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";

Expand Down Expand Up @@ -223,6 +226,8 @@ export function InvoiceForm({
valueAsNumber: true,
})}
type="number"
step="any"
min="0"
placeholder="Price"
/>
{form.formState.errors.items?.[index]?.price && (
Expand Down Expand Up @@ -294,6 +299,33 @@ export function InvoiceForm({
</p>
)}
</div>
{MAINNET_CURRENCIES.includes(
form.watch("invoiceCurrency") as MainnetCurrency,
) && (
<div className="space-y-2">
<Alert variant="warning">
<Terminal className="h-4 w-4" />
<AlertTitle>
Warning: You are creating an invoice with real funds
</AlertTitle>
<AlertDescription className="space-y-2">
<p>
You've selected{" "}
<span className="font-bold">
{formatCurrencyLabel(form.watch("invoiceCurrency"))}
</span>
, which operates on a mainnet blockchain and uses real
cryptocurrency.
</p>
<p>
EasyInvoice is a demonstration app only, designed to showcase
Request Network API functionality. Do not use for real
invoicing.
</p>
</AlertDescription>
</Alert>
</div>
)}

{/* Only show payment currency selector for USD invoices */}
{form.watch("invoiceCurrency") === "USD" && (
Expand Down
8 changes: 4 additions & 4 deletions src/components/invoice-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ export function InvoicePreview({ data }: InvoicePreviewProps) {
</td>
<td className="py-3 text-right text-sm">{item.quantity}</td>
<td className="py-3 text-right text-sm">
{(item.price || 0).toFixed(2)}
{(item.price || 0).toString()}
</td>
<td className="py-3 text-right text-sm">
{((item.quantity || 0) * (item.price || 0)).toFixed(2)}
{((item.quantity || 0) * (item.price || 0)).toString()}
</td>
</tr>
))}
Expand All @@ -143,13 +143,13 @@ export function InvoicePreview({ data }: InvoicePreviewProps) {
<div className="w-48">
<div className="flex justify-between py-2">
<span className="text-sm text-neutral-600">Subtotal</span>
<span className="text-sm">{calculateTotal().toFixed(2)}</span>
<span className="text-sm">{calculateTotal().toString()}</span>
</div>
<div className="flex justify-between py-2 border-t border-neutral-200">
<span className="text-sm font-medium">Total</span>
<div>
<div className="text-sm text-right font-medium">
{calculateTotal().toFixed(2)}
{calculateTotal().toString()}
</div>
<div className="text-xs text-neutral-500">
{formatCurrencyLabel(data.invoiceCurrency || "USD")}
Expand Down
117 changes: 117 additions & 0 deletions src/components/payment-route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { PaymentRoute as PaymentRouteType } from "@/lib/types";
import { ArrowRight, Globe, Zap } from "lucide-react";
import Image from "next/image";

interface RouteTypeInfo {
type: "direct" | "same-chain-erc20" | "cross-chain";
label: string;
description: string;
}

interface PaymentRouteProps {
route: PaymentRouteType;
isSelected: boolean;
onClick?: () => void;
variant?: "default" | "selected";
routeType?: RouteTypeInfo;
}

export function PaymentRoute({
route,
isSelected,
onClick,
variant = "default",
routeType,
}: PaymentRouteProps) {
const isDirectPayment =
routeType?.type === "direct" || route.id === "REQUEST_NETWORK_PAYMENT";
const isGasFreePayment = routeType?.type === "same-chain-erc20";

const nativeToken = route.chain === "POLYGON" ? "POL" : "ETH";

// Get the appropriate badge color and icon based on route type
const getBadgeStyles = () => {
if (isDirectPayment) {
return {
bgColor: "bg-blue-100",
textColor: "text-blue-700",
icon: <ArrowRight className="w-3 h-3 mr-1" />,
};
}

if (isGasFreePayment) {
return {
bgColor: "bg-green-100",
textColor: "text-green-700",
icon: <Zap className="w-3 h-3 mr-1" />,
};
}

return {
bgColor: "bg-purple-100",
textColor: "text-purple-700",
icon: <Globe className="w-3 h-3 mr-1" />,
};
};

const { bgColor, textColor, icon } = getBadgeStyles();

return (
<button
type="button"
onClick={onClick}
className={`w-full p-4 border rounded-lg transition-colors ${
variant === "selected"
? "border-2 border-black bg-zinc-50"
: isSelected
? "bg-zinc-50 border-black"
: "bg-white hover:border-zinc-400"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 relative flex items-center justify-center">
<Image
src={`/${route.chain.toLowerCase()}-logo.png`}
alt={`${route.chain} logo`}
width={32}
height={32}
className="object-contain"
/>
</div>
<div className="text-left">
<div className="font-medium flex items-center gap-2">
<span>
{route.chain} {route.token}
</span>
<span
className={`text-xs ${bgColor} ${textColor} px-2 py-0.5 rounded-full flex items-center`}
>
{icon}
{routeType?.label ||
(isDirectPayment ? "Direct Payment" : "via Paygrid")}
</span>
</div>
<div className="text-sm text-zinc-600">
{routeType?.description || `via ${route.token}`}
</div>
</div>
</div>
<div className="text-right">
<div className="font-medium">
{route.fee === 0 ? (
"No fee"
) : (
<span className="text-amber-700">
{route.fee} {isDirectPayment ? nativeToken : route.token} fee
</span>
)}
</div>
<div className="text-sm text-zinc-600">
{typeof route.speed === "number" ? `~${route.speed}s` : "Fast"}
</div>
</div>
</div>
</button>
);
}
Loading