1+ import React , { useEffect , useState } from 'react' ;
2+
3+ function moneyFmt ( value : any , currency = 'USD' ) {
4+ try {
5+ return new Intl . NumberFormat ( undefined , { style : 'currency' , currency } ) . format ( ( value && Number ( value ) ) / 100 || value ) ;
6+ } catch ( e ) {
7+ return String ( value ) ;
8+ }
9+ }
10+
11+ async function fetchFromSession ( endpoint : string , sessionId : string ) {
12+ try {
13+ const res = await fetch ( endpoint , {
14+ method : 'POST' ,
15+ headers : { 'Content-Type' : 'application/json' } ,
16+ body : JSON . stringify ( { action : 'stripe.receipt' , session_id : sessionId } ) ,
17+ } ) ;
18+ if ( ! res . ok ) {
19+ console . error ( 'receipt.client: server returned' , res . status ) ;
20+ return null ;
21+ }
22+ const json = await res . json ( ) . catch ( ( ) => null ) ;
23+ return json ?. data ?. link ?. response || json ?. data || null ;
24+ } catch ( e ) {
25+ console . error ( 'receipt.client: failed to fetch receipt from session' , e ) ;
26+ return null ;
27+ }
28+ }
29+
30+ /**
31+ * Read a stripe session_id, or order_id and display information
32+ *
33+ * @param props.api markket.api_url - api.markket.place
34+ * @returns
35+ */
36+ const ReceiptComponentPage = ( { api } : { api : string } ) => {
37+ const [ data , setData ] = useState < any | null > ( null ) ;
38+ const [ loading , setLoading ] = useState ( false ) ;
39+ const [ error , setError ] = useState < string | null > ( null ) ;
40+
41+ useEffect ( ( ) => {
42+ let mounted = true ;
43+ ( async ( ) => {
44+ setLoading ( true ) ;
45+ try {
46+ const endpoint = new URL ( '/api/markket' , api ) . toString ( ) ;
47+ const qs = new URLSearchParams ( window . location . search ) ;
48+ const sessionId = ( qs . get ( 'session_id' ) || qs . get ( 'session' ) || qs . get ( 'sid' ) || '' ) . trim ( ) ;
49+
50+ if ( ! sessionId ) {
51+ // no session id present — nothing to fetch
52+ if ( mounted ) {
53+ setData ( null ) ;
54+ setError ( null ) ;
55+ }
56+ return ;
57+ }
58+
59+ const resolved = await fetchFromSession ( endpoint , sessionId ) ;
60+
61+ if ( mounted ) {
62+ if ( resolved ) setData ( resolved ) ;
63+ else setError ( 'No receipt data returned from server for this session.' ) ;
64+ }
65+ } catch ( e : any ) {
66+ console . error ( e ) ;
67+ if ( mounted ) setError ( String ( e ?. message || e ) ) ;
68+ } finally {
69+ if ( mounted ) setLoading ( false ) ;
70+ }
71+ } ) ( ) ;
72+
73+ return ( ) => { mounted = false ; } ;
74+ } , [ ] ) ;
75+
76+ const getCurrency = ( d : any ) => ( d ?. currency || d ?. currency_code || ( d ?. link ?. response && d . link . response . currency ) || 'USD' ) . toUpperCase ( ) ;
77+ const getTotal = ( d : any ) => d ?. amount_total ?? d ?. amountTotal ?? d ?. total ?? d ?. amount ?? 0 ;
78+ const getSubtotal = ( d : any ) => d ?. amount_subtotal ?? d ?. amountSubtotal ?? d ?. subtotal ?? 0 ;
79+ const getCustomerEmail = ( d : any ) => d ?. customer_details ?. email || d ?. customer_email || d ?. email || d ?. receipt_email || '' ;
80+ const getCustomerName = ( d : any ) => d ?. customer_details ?. name || d ?. customer || '' ;
81+ const getOrderId = ( d : any ) => d ?. id || d ?. session_id || d ?. transaction || d ?. payment_intent || '' ;
82+ const getCreated = ( d : any ) => d ?. created ?? d ?. created_at ?? null ;
83+ const getShipping = ( d : any ) => d ?. shipping_details || d ?. shipping || d ?. collected_information ?. shipping_details || d ?. link ?. response ?. shipping_details || null ;
84+
85+ return (
86+ < main className = "container my-12" >
87+ < section className = "mx-auto max-w-3xl rounded-lg p-6 bg-surface border" role = "region" aria-labelledby = "receipt-heading" >
88+ < h2 id = "receipt-heading" className = "text-2xl font-semibold" > Order receipt</ h2 >
89+ < p className = "text-sm mt-1 text-muted" > This page decodes a receipt payload from the URL and renders a printable copy for your records.</ p >
90+
91+ { ! loading && ! data && ! error && (
92+ < div id = "receipt-empty" className = "mt-6" >
93+ < p className = "text-muted" > No receipt data detected. Append < code > ?receipt=</ code > followed by a URL-encoded JSON object to the URL (example in console).</ p >
94+ </ div >
95+ ) }
96+
97+ { loading && (
98+ < div className = "mt-6 text-sm text-muted" > Fetching receipt…</ div >
99+ ) }
100+
101+ { error && (
102+ < div className = "mt-6 text-sm text-red-600" > Error loading receipt: { error } </ div >
103+ ) }
104+
105+ { data && (
106+ < div id = "receipt" className = "mt-6" aria-live = "polite" >
107+ < div className = "flex justify-between items-start" >
108+ < div >
109+ < p className = "text-sm text-muted" > Order</ p >
110+ < h3 id = "order-id" className = "text-lg font-medium" > #{ getOrderId ( data ) || '—' } </ h3 >
111+ < p id = "order-date" className = "text-sm text-muted mt-1" > { ( function ( ) {
112+ const ts = getCreated ( data ) ; try { if ( ! ts ) return '—' ; const d = ( typeof ts === 'number' && String ( ts ) . length === 10 ) ? new Date ( ts * 1000 ) : new Date ( ts ) ; return d . toLocaleString ( ) ; } catch ( e ) { return '—' ; }
113+ } ) ( ) } </ p >
114+
115+ < p className = "text-sm text-muted mt-4" > Billed to</ p >
116+ < p id = "order-email" className = "font-medium break-words" > { getCustomerName ( data ) || getCustomerEmail ( data ) || '—' } </ p >
117+ < p id = "order-shipping" className = "text-sm mt-2 text-muted whitespace-pre-line break-words" > { ( function ( ) {
118+ const ship = getShipping ( data ) ; if ( ! ship ) return '—' ; const lines : string [ ] = [ ] ; if ( ship . name ) lines . push ( String ( ship . name ) ) ; const addr = ship . address || ship ; if ( addr ?. line1 ) lines . push ( String ( addr . line1 ) ) ; if ( addr ?. line2 ) lines . push ( String ( addr . line2 ) ) ; const cityParts = [ addr ?. city , addr ?. state , addr ?. postal_code ] . filter ( Boolean ) . join ( ', ' ) ; if ( cityParts ) lines . push ( cityParts ) ; if ( addr ?. country ) lines . push ( String ( addr . country ) ) ; return lines . join ( '\n' ) ;
119+ } ) ( ) } </ p >
120+ </ div >
121+ </ div >
122+
123+ < div className = "mt-6 border-t pt-4" >
124+ < h4 className = "text-sm font-semibold" > Items</ h4 >
125+ < ul id = "order-items" className = "mt-2 space-y-3" >
126+ { ( function ( ) {
127+ const items = data . items || data . line_items || data . line_items ?. data || [ ] ;
128+ if ( ! Array . isArray ( items ) || items . length === 0 ) return null ;
129+ return items . map ( ( it : any , i : number ) => {
130+ const title = it . name || it . description || 'Item' ;
131+ const qty = it . qty ?? it . quantity ?? 1 ;
132+ const p = Number ( it . unit_price ?? it . price ?? it . amount ?? 0 ) ;
133+ const currency = getCurrency ( data ) ;
134+ const price = ( ( ) => { try { return new Intl . NumberFormat ( undefined , { style : 'currency' , currency } ) . format ( ( isNaN ( p ) ? 0 : p ) / 100 ) ; } catch ( e ) { return String ( ( isNaN ( p ) ? 0 : p ) / 100 ) ; } } ) ( ) ;
135+ return (
136+ < li key = { i } className = "flex justify-between items-center" >
137+ < div className = "font-medium" > { title } < div className = "text-sm" > qty: { qty } </ div > </ div >
138+ < div className = "font-medium" > { price } </ div >
139+ </ li >
140+ ) ;
141+ } ) ;
142+ } ) ( ) }
143+ </ ul >
144+ </ div >
145+
146+ < div className = "mt-6 border-t pt-4 flex justify-end gap-6" >
147+ < div >
148+ < p className = "text-sm text-muted" > Subtotal</ p >
149+ < p id = "order-subtotal" className = "font-medium" > { moneyFmt ( getSubtotal ( data ) , getCurrency ( data ) ) } </ p >
150+ </ div >
151+ < div >
152+ < p className = "text-sm text-muted" > Total</ p >
153+ < p id = "order-total" className = "text-xl font-bold" > { moneyFmt ( getTotal ( data ) , getCurrency ( data ) ) } </ p >
154+ </ div >
155+ </ div >
156+
157+ </ div >
158+ ) }
159+ </ section >
160+ </ main >
161+ ) ;
162+ } ;
163+
164+ export default ReceiptComponentPage ;
0 commit comments