diff --git a/src/components/PaymentPlanComparison.tsx b/src/components/PaymentPlanComparison.tsx index 0a2d87e..f3e8dad 100644 --- a/src/components/PaymentPlanComparison.tsx +++ b/src/components/PaymentPlanComparison.tsx @@ -18,6 +18,12 @@ export default function PaymentPlanComparison({ }: PaymentPlanComparisonProps) { const [hoveredPlanIndex, setHoveredPlanIndex] = useState(null); + const cheapestPlanIndex = plans.reduce( + (cheapestIndex, plan, index) => + plan.effectiveCost < plans[cheapestIndex].effectiveCost ? index : cheapestIndex, + 0 + ); + return (
@@ -92,10 +98,20 @@ export default function PaymentPlanComparison({
{/* Total Cost */} -
+
Total Cost: - + {formatted.total} USDC
diff --git a/src/components/SubscriptionCalendarPreview.tsx b/src/components/SubscriptionCalendarPreview.tsx index 92b3e13..2d98cb8 100644 --- a/src/components/SubscriptionCalendarPreview.tsx +++ b/src/components/SubscriptionCalendarPreview.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { computeUpcomingDates } from "@/lib/subscriptions"; interface Props { @@ -25,81 +25,97 @@ export default function SubscriptionCalendarPreview({ [intervalDays, count, fromDate] ); - const monthGroups = useMemo(() => { - const groups: Record }> = {}; + const initialView = useMemo(() => { + const first = upcomingDates[0] ?? fromDate ?? new Date(); + return { year: first.getFullYear(), month: first.getMonth() }; + }, [upcomingDates, fromDate]); + + const [viewOffset, setViewOffset] = useState(0); + + const viewDate = useMemo(() => { + return new Date(initialView.year, initialView.month + viewOffset, 1); + }, [initialView, viewOffset]); + + const viewYear = viewDate.getFullYear(); + const viewMonth = viewDate.getMonth(); + + const invoiceDaysInView = useMemo(() => { + const days = new Set(); for (const date of upcomingDates) { - const key = `${date.getFullYear()}-${date.getMonth()}`; - if (!groups[key]) { - groups[key] = { - year: date.getFullYear(), - month: date.getMonth(), - days: new Set(), - }; + if (date.getFullYear() === viewYear && date.getMonth() === viewMonth) { + days.add(date.getDate()); } - groups[key].days.add(date.getDate()); } - return Object.values(groups); - }, [upcomingDates]); + return days; + }, [upcomingDates, viewYear, viewMonth]); + + const monthName = viewDate.toLocaleString("default", { + month: "long", + year: "numeric", + }); + const daysInMonth = getDaysInMonth(viewYear, viewMonth); + const firstDayOfWeek = new Date(viewYear, viewMonth, 1).getDay(); + + const cells: (number | null)[] = []; + for (let i = 0; i < firstDayOfWeek; i++) cells.push(null); + for (let d = 1; d <= daysInMonth; d++) cells.push(d); return (
-

- Upcoming Invoice Dates -

+
+

+ Upcoming Invoice Dates +

+
+ + + {monthName} + + +
+
- {monthGroups.length === 0 ? ( + {upcomingDates.length === 0 ? (

No upcoming dates.

) : ( -
- {monthGroups.map((group) => { - const monthName = new Date(group.year, group.month).toLocaleString( - "default", - { month: "long", year: "numeric" } - ); - const daysInMonth = getDaysInMonth(group.year, group.month); - const firstDayOfWeek = new Date( - group.year, - group.month, - 1 - ).getDay(); - - const cells: (number | null)[] = []; - for (let i = 0; i < firstDayOfWeek; i++) cells.push(null); - for (let d = 1; d <= daysInMonth; d++) cells.push(d); - +
+ {DAY_LABELS.map((label) => ( +
+ {label.charAt(0)} +
+ ))} + {cells.map((day, i) => { + if (day === null) { + return
; + } + const isInvoiceDay = invoiceDaysInView.has(day); return ( -
-

- {monthName} -

-
- {DAY_LABELS.map((label) => ( -
- {label.charAt(0)} -
- ))} - {cells.map((day, i) => { - if (day === null) { - return
; - } - const isInvoiceDay = group.days.has(day); - return ( -
- {day} -
- ); - })} -
+
+ {day}
); })} diff --git a/src/hooks/useInvoiceCostEstimate.ts b/src/hooks/useInvoiceCostEstimate.ts index c907884..c301e92 100644 --- a/src/hooks/useInvoiceCostEstimate.ts +++ b/src/hooks/useInvoiceCostEstimate.ts @@ -13,6 +13,7 @@ export interface CostBreakdown { reserveTopUpXlm: number; platformFeeXlm: number; addOnCostsXlm: number; + pathSpreadXlm: number; totalXlm: number; } @@ -22,6 +23,7 @@ interface UseInvoiceCostEstimateParams { creatorAddress: string; enabledAddOns?: FeatureAddOn[]; feeTierMultiplier?: number; + pathPaymentSpreadXlm?: number; } const BASE_NETWORK_FEE_XLM = 0.00001; // 100 stroops @@ -32,6 +34,7 @@ export function useInvoiceCostEstimate({ creatorAddress, enabledAddOns = [], feeTierMultiplier = 1, + pathPaymentSpreadXlm = 0, }: UseInvoiceCostEstimateParams) { const [existingTrustlines, setExistingTrustlines] = useState>(new Set()); const [creatorBalance, setCreatorBalance] = useState(null); @@ -84,17 +87,26 @@ export function useInvoiceCostEstimate({ const reserveTopUpXlm = calculateReserveTopUp(estimatedNewTrustlines); const addOnCostsXlm = getAddOnCosts(enabledAddOns); + const pathSpreadXlm = pathPaymentSpreadXlm; - const totalXlm = networkFeeXlm + platformFeeXlm + reserveTopUpXlm + addOnCostsXlm; + const totalXlm = + networkFeeXlm + platformFeeXlm + reserveTopUpXlm + addOnCostsXlm + pathSpreadXlm; return { networkFeeXlm, reserveTopUpXlm, platformFeeXlm, addOnCostsXlm, + pathSpreadXlm, totalXlm, }; - }, [invoiceAmountXlm, recipientAddresses.length, feeTierMultiplier, enabledAddOns]); + }, [ + invoiceAmountXlm, + recipientAddresses.length, + feeTierMultiplier, + enabledAddOns, + pathPaymentSpreadXlm, + ]); const isBalanceSufficient = useMemo(() => { if (creatorBalance === null) return null; diff --git a/src/hooks/usePathPayment.ts b/src/hooks/usePathPayment.ts index b3cc7ef..69fe83b 100644 --- a/src/hooks/usePathPayment.ts +++ b/src/hooks/usePathPayment.ts @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useMemo } from 'react'; export interface PathPaymentResult { path: string[]; @@ -12,6 +12,12 @@ export interface PathPaymentResult { slippage: number; // percentage } +export interface SelectedPath { + hops: string[]; + effectiveRate: number; + slippageEstimate: number; +} + interface UsePathPaymentOptions { sourceAsset?: string; destinationAsset: string; @@ -32,6 +38,7 @@ const MOCK_EXCHANGE_RATES: Record> = { export function usePathPayment(options: UsePathPaymentOptions): { paths: PathPaymentResult[]; + selectedPath: SelectedPath | null; loading: boolean; error: string | null; } { @@ -99,5 +106,19 @@ export function usePathPayment(options: UsePathPaymentOptions): { fetchPaths(); }, [fetchPaths]); - return { paths, loading, error }; + const selectedPath = useMemo((): SelectedPath | null => { + if (loading || paths.length === 0) return null; + + const lowestSlippagePath = paths.reduce((lowest, current) => + current.slippage < lowest.slippage ? current : lowest + ); + + return { + hops: lowestSlippagePath.path, + effectiveRate: lowestSlippagePath.exchangeRate, + slippageEstimate: lowestSlippagePath.slippage, + }; + }, [paths, loading]); + + return { paths, selectedPath, loading, error }; }