Files
cbd420/app/components/Drop.tsx
2025-12-21 09:56:59 +01:00

984 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import Image from 'next/image'
import AuthModal from './AuthModal'
interface DropData {
id: number
item: string
size: number
fill: number
unit: string
ppu: number
image_url: string | null
created_at: string
start_time: string | null
is_upcoming?: boolean
sales_fill?: number // Only confirmed sales
pending_fill?: number // Items on hold (pending orders)
}
interface User {
id: number
username: string
email: string
}
export default function Drop() {
const [drop, setDrop] = useState<DropData | null>(null)
const [loading, setLoading] = useState(true)
const [selectedSize, setSelectedSize] = useState(50)
const [selectedCurrency, setSelectedCurrency] = useState<string>('btc')
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([])
const [loadingCurrencies, setLoadingCurrencies] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [showAuthModal, setShowAuthModal] = useState(false)
const [showPaymentModal, setShowPaymentModal] = useState(false)
const [showSuccessModal, setShowSuccessModal] = useState(false)
const [showErrorModal, setShowErrorModal] = useState(false)
const [errorMessage, setErrorMessage] = useState<string>('')
const [paymentData, setPaymentData] = useState<any>(null)
const [processing, setProcessing] = useState(false)
const [user, setUser] = useState<User | null>(null)
const [checkingAuth, setCheckingAuth] = useState(true)
useEffect(() => {
fetchActiveDrop()
checkAuth()
}, [])
// Poll payment status when payment modal is open
useEffect(() => {
if (!showPaymentModal || !paymentData?.payment_id) return
const checkPaymentStatus = async () => {
try {
const response = await fetch(`/api/payments/check-status?payment_id=${paymentData.payment_id}`, {
credentials: 'include',
})
if (response.ok) {
const status = await response.json()
// If pending order is gone and sale exists, payment was processed
if (!status.has_pending_order && status.has_sale) {
// Close payment modal
setShowPaymentModal(false)
// Show success modal
setShowSuccessModal(true)
// Refresh drop data
await fetchActiveDrop()
}
}
} catch (error) {
console.error('Error checking payment status:', error)
}
}
// Check immediately, then poll every 3 seconds
checkPaymentStatus()
const interval = setInterval(checkPaymentStatus, 3000)
return () => clearInterval(interval)
}, [showPaymentModal, paymentData?.payment_id])
const checkAuth = async () => {
try {
const response = await fetch('/api/auth/session', {
credentials: 'include',
})
if (response.ok) {
const data = await response.json()
setUser(data.user)
}
} catch (error) {
console.error('Error checking auth:', error)
} finally {
setCheckingAuth(false)
}
}
const fetchActiveDrop = async () => {
try {
const response = await fetch('/api/drops/active')
if (response.ok) {
const data = await response.json()
// Handle both null response and actual drop data
setDrop(data) // data can be null if no active drop
} else {
// If response is not ok, log the error
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
console.error('Error fetching active drop:', errorData)
setDrop(null)
}
} catch (error) {
console.error('Error fetching active drop:', error)
setDrop(null)
} finally {
setLoading(false)
}
}
const getProgressPercentage = (fill: number, size: number) => {
return Math.min((fill / size) * 100, 100)
}
const formatSize = (size: number, unit: string) => {
if (unit === 'g' && size >= 1000) {
return `${(size / 1000).toFixed(1)}kg`
}
return `${size}${unit}`
}
const getAvailableSizes = () => {
if (!drop) return []
const sizes = [50, 100, 250] // Always in grams
// Calculate remaining inventory in grams
let remainingInGrams = 0
if (drop.unit === 'kg') {
remainingInGrams = (drop.size - drop.fill) * 1000
} else {
// For 'g' or any other unit, assume same unit
remainingInGrams = drop.size - drop.fill
}
// Only show sizes that don't exceed remaining inventory
return sizes.filter((size) => size <= remainingInGrams)
}
const fetchAvailableCurrencies = async () => {
setLoadingCurrencies(true)
try {
const response = await fetch('/api/payments/currencies')
if (response.ok) {
const data = await response.json()
// When fixed_rate=true, API returns objects with { currency, min_amount, max_amount }
// When fixed_rate=false, API returns array of strings
const currencies: string[] = []
if (Array.isArray(data.currencies)) {
data.currencies.forEach((c: any) => {
let currencyCode: string | null = null
// Handle object format (when fixed_rate=true)
if (typeof c === 'object' && c !== null && c.currency) {
currencyCode = String(c.currency).trim().toLowerCase()
}
// Handle string format (when fixed_rate=false)
else if (typeof c === 'string') {
currencyCode = c.trim().toLowerCase()
}
// Add to array if valid
if (currencyCode && currencyCode.length > 0) {
currencies.push(currencyCode)
}
})
}
setAvailableCurrencies(currencies)
// Set default to BTC if available, otherwise first currency
if (currencies.length > 0) {
const defaultCurrency = currencies.includes('btc') ? 'btc' : currencies[0]
setSelectedCurrency(defaultCurrency)
}
}
} catch (error) {
console.error('Error fetching currencies:', error)
} finally {
setLoadingCurrencies(false)
}
}
const handleJoinDrop = () => {
// Check if user is logged in
if (!user) {
setShowAuthModal(true)
return
}
// Fetch available currencies when opening confirm modal
fetchAvailableCurrencies()
setShowConfirmModal(true)
}
const handleLogin = (loggedInUser: User) => {
setUser(loggedInUser)
setShowAuthModal(false)
// After login, show the confirmation modal
setShowConfirmModal(true)
}
const handleConfirmPurchase = async () => {
if (!drop) return
setProcessing(true)
try {
// Create NOWPayments payment
const response = await fetch('/api/payments/create-invoice', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Important for cookies
body: JSON.stringify({
drop_id: drop.id,
size: selectedSize, // Size in grams
pay_currency: selectedCurrency, // Selected payment currency
}),
})
if (!response.ok) {
const error = await response.json()
if (response.status === 401) {
// User not authenticated - show login modal
setShowConfirmModal(false)
setShowAuthModal(true)
setProcessing(false)
return
}
// Show error modal instead of alert
setErrorMessage(error.error || 'Failed to create payment')
setShowErrorModal(true)
setShowConfirmModal(false)
setProcessing(false)
return
}
const data = await response.json()
// Close confirmation modal
setShowConfirmModal(false)
setProcessing(false)
// Show payment modal with payment details
if (data.pay_address) {
setPaymentData(data)
setShowPaymentModal(true)
} else {
setErrorMessage('Payment created but no payment address received')
setShowErrorModal(true)
await fetchActiveDrop()
}
} catch (error) {
console.error('Error creating payment invoice:', error)
setErrorMessage('Failed to create payment invoice. Please try again.')
setShowErrorModal(true)
setProcessing(false)
}
}
const handleCancelPurchase = () => {
setShowConfirmModal(false)
}
const calculatePrice = () => {
if (!drop) return 0
// ppu is stored as integer where 1000 = $1.00, so divide by 1000 to get actual price
const pricePerUnit = drop.ppu / 1000
if (drop.unit === 'kg') {
return (selectedSize / 1000) * pricePerUnit
}
return selectedSize * pricePerUnit
}
const getTimeUntilStart = () => {
if (!drop || !drop.is_upcoming || !drop.start_time) return null
const startTime = new Date(drop.start_time)
const now = new Date()
const diffMs = startTime.getTime() - now.getTime()
if (diffMs <= 0) return null
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
if (diffDays > 0) {
return `${diffDays} day${diffDays > 1 ? 's' : ''}${diffHours > 0 ? ` ${diffHours} hour${diffHours > 1 ? 's' : ''}` : ''}`
} else if (diffHours > 0) {
return `${diffHours} hour${diffHours > 1 ? 's' : ''}${diffMinutes > 0 ? ` ${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}` : ''}`
} else {
return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}`
}
}
if (loading) {
return (
<div className="drop">
<div style={{ gridColumn: '1 / -1', textAlign: 'center', padding: '40px' }}>
<p style={{ color: 'var(--muted)' }}>Loading...</p>
</div>
</div>
)
}
if (!drop) {
return (
<div className="drop">
<div style={{ gridColumn: '1 / -1', textAlign: 'center', padding: '60px' }}>
<h2 style={{ marginBottom: '16px' }}>Drop Sold Out</h2>
<p style={{ color: 'var(--muted)', marginBottom: '20px' }}>
The current collective drop has been fully reserved.
</p>
<p style={{ color: 'var(--muted)' }}>
Next collective drop coming soon.
</p>
</div>
</div>
)
}
const progressPercentage = getProgressPercentage(drop.fill, drop.size)
const availableSizes = getAvailableSizes()
const timeUntilStart = getTimeUntilStart()
const isUpcoming = drop.is_upcoming && timeUntilStart
// Calculate remaining in the drop's unit
const remaining = drop.size - drop.fill
const hasRemaining = remaining > 0
return (
<div className="drop">
{drop.image_url ? (
<Image
src={drop.image_url}
alt={drop.item}
width={420}
height={420}
style={{ width: '100%', height: 'auto', borderRadius: '16px', objectFit: 'cover' }}
/>
) : (
<div
style={{
width: '100%',
aspectRatio: '1 / 1',
background: 'var(--bg-soft)',
borderRadius: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--muted)',
}}
>
No Image
</div>
)}
<div>
<h2>{drop.item}</h2>
<div className="meta">
{formatSize(drop.size, drop.unit)} Batch
</div>
<div className="price">
{(drop.ppu / 1000).toFixed(2)} CHF / {drop.unit} · incl. 2.5% VAT
</div>
{isUpcoming ? (
<div style={{ marginTop: '30px', padding: '20px', background: 'var(--bg-soft)', borderRadius: '12px', textAlign: 'center' }}>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '16px' }}>
Drop starts in <strong>{timeUntilStart}</strong>
</p>
</div>
) : (
<>
<div className="progress">
<span style={{ width: `${progressPercentage}%` }}></span>
</div>
<div className="meta">
{drop.unit === 'kg' ? drop.fill.toFixed(2) : Math.round(drop.fill)}
{drop.unit} of {drop.size}
{drop.unit} reserved
</div>
{(() => {
const pendingFill = Number(drop.pending_fill) || 0;
console.log(`pending fill:${pendingFill}`)
return pendingFill > 0 && (
<div className="meta" style={{ fontSize: '12px', color: 'var(--muted)', marginTop: '4px' }}>
{drop.unit === 'kg' ? pendingFill.toFixed(2) : Math.round(pendingFill)}
{drop.unit} on hold (10 min checkout window)
</div>
)
})()}
</>
)}
{!isUpcoming && hasRemaining && availableSizes.length > 0 && (
<>
<div className="options">
{availableSizes.map((size) => (
<button
key={size}
className={selectedSize === size ? 'active' : ''}
onClick={() => setSelectedSize(size)}
>
{size}g
</button>
))}
</div>
<button className="cta" onClick={handleJoinDrop}>
Join Drop
</button>
</>
)}
{hasRemaining && availableSizes.length === 0 && (
<div style={{ marginTop: '30px', padding: '20px', background: 'var(--bg-soft)', borderRadius: '12px', textAlign: 'center' }}>
<p style={{ margin: 0, color: 'var(--muted)' }}>
Less than 50{drop.unit} remaining. This drop is almost fully reserved.
</p>
</div>
)}
{!hasRemaining && (
<div style={{ marginTop: '30px', padding: '20px', background: 'var(--bg-soft)', borderRadius: '12px', textAlign: 'center' }}>
<p style={{ margin: 0, color: 'var(--muted)' }}>This drop is fully reserved</p>
</div>
)}
</div>
{/* Confirmation Modal */}
{showConfirmModal && drop && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={handleCancelPurchase}
>
<div
style={{
background: 'var(--card)',
borderRadius: '16px',
padding: '32px',
maxWidth: '500px',
width: '100%',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<h2 style={{ marginTop: 0, marginBottom: '20px' }}>
Confirm Purchase
</h2>
<div style={{ marginBottom: '24px' }}>
<p style={{ marginBottom: '12px', color: 'var(--muted)' }}>
<strong>Item:</strong> {drop.item}
</p>
<p style={{ marginBottom: '12px', color: 'var(--muted)' }}>
<strong>Quantity:</strong> {selectedSize}g
</p>
<p style={{ marginBottom: '12px', color: 'var(--muted)' }}>
<strong>Price per {drop.unit}:</strong> {(drop.ppu / 1000).toFixed(2)} CHF
</p>
{/* Currency Selection */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', color: 'var(--muted)' }}>
<strong>Payment Currency:</strong>
</label>
{loadingCurrencies ? (
<p style={{ color: 'var(--muted)', fontSize: '14px' }}>Loading currencies...</p>
) : (
<select
value={selectedCurrency}
onChange={(e) => setSelectedCurrency(e.target.value)}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
cursor: 'pointer',
}}
>
{availableCurrencies.map((currency, index) => {
// Ensure currency is a string - handle both string and object cases
let currencyStr: string
if (typeof currency === 'string') {
currencyStr = currency.trim().toLowerCase()
} else if (currency && typeof currency === 'object') {
// If it's an object, try to extract a string value
currencyStr = (currency as any).value || (currency as any).code || String(currency).trim().toLowerCase()
} else {
currencyStr = String(currency || '').trim().toLowerCase()
}
// Fallback if we still don't have a valid string
if (!currencyStr || currencyStr === '[object object]' || currencyStr === 'null' || currencyStr === 'undefined') {
console.warn('Invalid currency value:', currency, 'at index', index)
return null
}
return (
<option key={`${currencyStr}-${index}`} value={currencyStr}>
{currencyStr.toUpperCase()}
</option>
)
}).filter(Boolean)}
</select>
)}
</div>
<div
style={{
padding: '16px',
background: 'var(--bg-soft)',
borderRadius: '8px',
marginTop: '16px',
}}
>
<p style={{ margin: 0, fontSize: '18px', fontWeight: 'bold' }}>
Total: {calculatePrice().toFixed(2)} CHF
</p>
<p
style={{
margin: '4px 0 0 0',
fontSize: '14px',
color: 'var(--muted)',
}}
>
incl. 2.5% VAT
</p>
<p
style={{
margin: '8px 0 0 0',
fontSize: '12px',
color: 'var(--muted)',
}}
>
Pay with: {String(selectedCurrency || '').toUpperCase()}
</p>
</div>
</div>
<div
style={{
display: 'flex',
gap: '12px',
justifyContent: 'flex-end',
}}
>
<button
onClick={handleCancelPurchase}
disabled={processing}
style={{
padding: '12px 24px',
background: '#dc2626',
border: 'none',
borderRadius: '14px',
cursor: processing ? 'not-allowed' : 'pointer',
color: '#fff',
fontSize: '15px',
fontWeight: 500,
opacity: processing ? 0.6 : 1,
lineHeight: '1.5',
boxSizing: 'border-box',
display: 'inline-block',
}}
>
Cancel
</button>
<button
onClick={handleConfirmPurchase}
disabled={processing}
style={{
padding: '12px 24px',
background: '#0a7931',
color: '#fff',
border: 'none',
borderRadius: '14px',
cursor: processing ? 'not-allowed' : 'pointer',
fontSize: '15px',
fontWeight: 500,
opacity: processing ? 0.6 : 1,
lineHeight: '1.5',
boxSizing: 'border-box',
display: 'inline-block',
}}
>
{processing ? 'Processing...' : 'Confirm Purchase'}
</button>
</div>
</div>
</div>
)}
{/* Auth Modal */}
<AuthModal
isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)}
onLogin={handleLogin}
/>
{/* Success Modal */}
{showSuccessModal && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1001,
padding: '20px',
}}
onClick={() => setShowSuccessModal(false)}
>
<div
style={{
background: 'var(--card)',
borderRadius: '16px',
padding: '32px',
maxWidth: '500px',
width: '100%',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<h2 style={{ marginTop: 0, marginBottom: '20px', color: '#0a7931' }}>
Payment Confirmed!
</h2>
<p style={{ marginBottom: '24px', color: 'var(--muted)' }}>
Your payment has been successfully processed. Your order is confirmed and will be included in the drop.
</p>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button
onClick={() => {
setShowSuccessModal(false)
setPaymentData(null)
}}
style={{
padding: '12px 24px',
background: '#0a7931',
color: '#fff',
border: 'none',
borderRadius: '14px',
cursor: 'pointer',
fontSize: '15px',
fontWeight: 500,
lineHeight: '1.5',
boxSizing: 'border-box',
display: 'inline-block',
}}
>
Close
</button>
</div>
</div>
</div>
)}
{/* Error Modal */}
{showErrorModal && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1001,
padding: '20px',
}}
onClick={() => {
setShowErrorModal(false)
setErrorMessage('')
// Don't refresh - just return to drop view as is
}}
>
<div
style={{
background: 'var(--card)',
borderRadius: '16px',
padding: '32px',
maxWidth: '500px',
width: '100%',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<h2 style={{ marginTop: 0, marginBottom: '20px', color: '#dc2626' }}>
Error
</h2>
<p style={{ marginBottom: '24px', color: 'var(--muted)' }}>
{errorMessage}
</p>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button
onClick={() => {
setShowErrorModal(false)
setErrorMessage('')
// Don't refresh - just return to drop view as is
// The drop should still be visible with current inventory state
}}
style={{
padding: '12px 24px',
background: '#dc2626',
color: '#fff',
border: 'none',
borderRadius: '14px',
cursor: 'pointer',
fontSize: '15px',
fontWeight: 500,
lineHeight: '1.5',
boxSizing: 'border-box',
display: 'inline-block',
}}
>
Close
</button>
</div>
</div>
</div>
)}
{/* Payment Modal */}
{showPaymentModal && paymentData && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={async () => {
// Check if payment was already processed before cancelling
if (paymentData?.payment_id) {
try {
const statusResponse = await fetch(`/api/payments/check-status?payment_id=${paymentData.payment_id}`, {
credentials: 'include',
})
if (statusResponse.ok) {
const status = await statusResponse.json()
// Only cancel if payment hasn't been processed yet
if (status.has_pending_order && !status.has_sale) {
await fetch('/api/payments/cancel-pending', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
payment_id: paymentData.payment_id,
}),
})
}
}
} catch (error) {
console.error('Error checking/cancelling pending order:', error)
}
}
setShowPaymentModal(false)
// Refresh drop data to show updated inventory
await fetchActiveDrop()
}}
>
<div
style={{
background: 'var(--card)',
borderRadius: '16px',
padding: '32px',
maxWidth: '600px',
width: '100%',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<h2 style={{ marginTop: 0, marginBottom: '20px' }}>
Complete Payment
</h2>
<div style={{ marginBottom: '24px' }}>
<p style={{ marginBottom: '12px', color: 'var(--muted)' }}>
<strong>Amount to Pay:</strong> {paymentData.pay_amount} {paymentData.pay_currency.toUpperCase()}
</p>
<p style={{ marginBottom: '12px', color: 'var(--muted)' }}>
<strong>Price:</strong> {paymentData.price_amount} {paymentData.price_currency.toUpperCase()}
</p>
<div style={{ marginTop: '20px', marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', color: 'var(--muted)' }}>
Send payment to this address:
</label>
<div
style={{
padding: '12px',
background: 'var(--bg-soft)',
borderRadius: '8px',
fontFamily: 'monospace',
fontSize: '14px',
wordBreak: 'break-all',
marginBottom: '12px',
}}
>
{paymentData.pay_address}
</div>
<button
onClick={(e) => {
navigator.clipboard.writeText(paymentData.pay_address)
// Show brief success feedback
const button = e.currentTarget as HTMLButtonElement
const originalText = button.textContent
button.textContent = 'Copied!'
setTimeout(() => {
if (button) button.textContent = originalText
}, 2000)
}}
style={{
padding: '8px 16px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Copy Address
</button>
</div>
{paymentData.payin_extra_id && (
<div style={{ marginTop: '20px', marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', color: 'var(--muted)' }}>
Memo / Destination Tag (Required):
</label>
<div
style={{
padding: '12px',
background: 'var(--bg-soft)',
borderRadius: '8px',
fontFamily: 'monospace',
fontSize: '14px',
marginBottom: '12px',
}}
>
{paymentData.payin_extra_id}
</div>
<button
onClick={(e) => {
navigator.clipboard.writeText(paymentData.payin_extra_id)
// Show brief success feedback
const button = e.currentTarget as HTMLButtonElement
const originalText = button.textContent
button.textContent = 'Copied!'
setTimeout(() => {
if (button) button.textContent = originalText
}, 2000)
}}
style={{
padding: '8px 16px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Copy Memo
</button>
</div>
)}
{paymentData.expiration_estimate_date && (
<p style={{ marginTop: '16px', fontSize: '12px', color: 'var(--muted)' }}>
Payment expires: {new Date(paymentData.expiration_estimate_date).toLocaleString()}
</p>
)}
<div style={{ marginTop: '24px', padding: '16px', background: 'var(--bg-soft)', borderRadius: '8px' }}>
<p style={{ margin: 0, fontSize: '14px', color: 'var(--muted)' }}>
<strong>Status:</strong> {paymentData.payment_status}
</p>
<p style={{ margin: '12px 0 0 0', fontSize: '12px', color: '#dc2626', fontWeight: 500 }}>
Closing this window will cancel your reservation and free up the inventory.
</p>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button
onClick={async () => {
// Check if payment was already processed before cancelling
if (paymentData?.payment_id) {
try {
const statusResponse = await fetch(`/api/payments/check-status?payment_id=${paymentData.payment_id}`, {
credentials: 'include',
})
if (statusResponse.ok) {
const status = await statusResponse.json()
// Only cancel if payment hasn't been processed yet
if (status.has_pending_order && !status.has_sale) {
await fetch('/api/payments/cancel-pending', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
payment_id: paymentData.payment_id,
}),
})
}
}
} catch (error) {
console.error('Error checking/cancelling pending order:', error)
}
}
setShowPaymentModal(false)
// Refresh drop data to show updated inventory
await fetchActiveDrop()
}}
style={{
padding: '12px 24px',
background: '#dc2626',
border: 'none',
borderRadius: '14px',
cursor: 'pointer',
color: '#fff',
fontSize: '15px',
fontWeight: 500,
lineHeight: '1.5',
boxSizing: 'border-box',
display: 'inline-block',
}}
>
Close
</button>
</div>
</div>
</div>
)}
</div>
)
}