33import { useEffect , useRef , useState } from "react" ;
44import { splitClient } from "@/lib/stellar" ;
55import { getFreighterPublicKey } from "@/lib/freighter" ;
6+ import {
7+ isSubscribedToInvoice ,
8+ notifyInvoiceReleased ,
9+ requestNotificationPermission ,
10+ subscribeToInvoice ,
11+ } from "@/lib/notifications" ;
612import { formatAmount , parseAmount } from "@stellar-split/sdk" ;
713import PaymentProgress from "@/components/PaymentProgress" ;
814import InstallmentPanel from "@/components/InstallmentPanel" ;
@@ -23,6 +29,23 @@ interface Props {
2329 params : { id : string } ;
2430}
2531
32+ type InvoicePayment = Payment & { pending ?: boolean ; clientKey ?: string } ;
33+ type InvoiceView = Omit < Invoice , "payments" > & { payments : InvoicePayment [ ] } ;
34+
35+ function mergeWithServer ( server : Invoice , local : InvoiceView | null ) : InvoiceView {
36+ const pending = ( local ?. payments ?? [ ] ) . filter ( ( p ) => p . pending ) ;
37+ const unmatchedPending = pending . filter (
38+ ( p ) =>
39+ ! server . payments . some ( ( sp ) => sp . payer === p . payer && sp . amount === p . amount )
40+ ) ;
41+ return {
42+ ...server ,
43+ payments : [ ...server . payments , ...unmatchedPending ] ,
44+ funded :
45+ server . funded + unmatchedPending . reduce ( ( sum , p ) => sum + p . amount , 0n ) ,
46+ } ;
47+ }
48+
2649/**
2750 * Invoice detail page — shows status, payment progress, Pay button,
2851 * reminder system, and webhook configuration (creator only).
@@ -98,17 +121,40 @@ export default function InvoiceDetailPage({ params }: Props) {
98121 const handlePay = async ( e : React . FormEvent ) => {
99122 e . preventDefault ( ) ;
100123 if ( ! publicKey || ! invoice ) return ;
124+ const amount = parseAmount ( payAmount ) ;
125+ const clientKey = `opt-${ Date . now ( ) } ` ;
101126 setError ( null ) ;
127+ setInvoice ( ( prev ) => {
128+ if ( ! prev ) return prev ;
129+ return {
130+ ...prev ,
131+ funded : prev . funded + amount ,
132+ payments : [
133+ ...prev . payments ,
134+ { payer : publicKey , amount, pending : true , clientKey } ,
135+ ] ,
136+ } ;
137+ } ) ;
102138 setPaying ( true ) ;
103139 try {
104140 const result = await splitClient . pay ( {
105141 payer : publicKey ,
106142 invoiceId : id ,
107- amount : parseAmount ( payAmount ) ,
143+ amount,
108144 } ) ;
109145 setTxHash ( result . txHash ) ;
110146 await load ( ) ;
111147 } catch ( err ) {
148+ setInvoice ( ( prev ) => {
149+ if ( ! prev ) return prev ;
150+ const pending = prev . payments . find ( ( p ) => p . clientKey === clientKey ) ;
151+ if ( ! pending ?. pending ) return prev ;
152+ return {
153+ ...prev ,
154+ funded : prev . funded - pending . amount ,
155+ payments : prev . payments . filter ( ( p ) => p . clientKey !== clientKey ) ,
156+ } ;
157+ } ) ;
112158 setError ( String ( err ) ) ;
113159 } finally {
114160 setPaying ( false ) ;
@@ -204,6 +250,59 @@ export default function InvoiceDetailPage({ params }: Props) {
204250 </ p >
205251 </ section >
206252
253+ { /* Release notifications */ }
254+ < section className = "mb-8" >
255+ < button
256+ type = "button"
257+ onClick = { handleNotifyMe }
258+ disabled = { notifySubscribed }
259+ className = "w-full sm:w-auto px-4 py-2 rounded-lg border border-gray-700 hover:border-indigo-500 text-sm font-semibold transition-colors disabled:opacity-60 disabled:cursor-default"
260+ >
261+ { notifySubscribed ? "Notifications enabled" : "Notify me" }
262+ </ button >
263+ { notifyDenied && (
264+ < p className = "text-gray-400 text-sm mt-2" >
265+ Notifications are blocked. Enable them in your browser settings to get
266+ alerts when this invoice is released.
267+ </ p >
268+ ) }
269+ </ section >
270+
271+ { /* Payments */ }
272+ < section className = "mb-8" >
273+ < h2 className = "text-lg font-semibold mb-3" >
274+ Payments ({ invoice . payments . length } )
275+ </ h2 >
276+ { invoice . payments . length === 0 ? (
277+ < p className = "text-gray-500 text-sm" > No payments yet.</ p >
278+ ) : (
279+ < ul className = "flex flex-col gap-2" >
280+ { invoice . payments . map ( ( p , i ) => (
281+ < li
282+ key = { p . clientKey ?? `${ p . payer } -${ i } ` }
283+ className = "flex flex-wrap items-center justify-between gap-2 bg-gray-900 rounded-lg px-4 py-2 text-sm"
284+ >
285+ < span className = "font-mono text-gray-300 truncate max-w-[55%]" >
286+ { p . payer }
287+ </ span >
288+ < div className = "flex items-center gap-2 shrink-0" >
289+ < span className = "text-indigo-300" > { formatAmount ( p . amount ) } USDC</ span >
290+ { p . pending && (
291+ < span className = "inline-flex items-center gap-1 text-xs text-amber-300 bg-amber-950/60 px-2 py-0.5 rounded-full" >
292+ < span
293+ className = "inline-block h-3 w-3 rounded-full border-2 border-amber-300 border-t-transparent animate-spin"
294+ aria-hidden
295+ />
296+ Confirming…
297+ </ span >
298+ ) }
299+ </ div >
300+ </ li >
301+ ) ) }
302+ </ ul >
303+ ) }
304+ </ section >
305+
207306 { /* Recipients */ }
208307 < section aria-labelledby = "recipients-heading" className = "mb-8" >
209308 < h2 id = "recipients-heading" className = "text-lg font-semibold mb-3" > Recipients</ h2 >
0 commit comments