feat: BTCPay Server payment integration via pow.dgray.io proxy

This commit is contained in:
2026-02-06 14:22:16 +01:00
parent 146945d732
commit fcf22617d0
12 changed files with 515 additions and 35 deletions

170
js/services/btcpay.js Normal file
View File

@@ -0,0 +1,170 @@
const POW_SERVER = 'https://pow.dgray.io'
let modalScriptLoaded = false
let modalScriptLoading = null
/**
* Load BTCPay modal checkout script once
*/
async function ensureModalLoaded() {
if (modalScriptLoaded) return
if (modalScriptLoading) return modalScriptLoading
modalScriptLoading = new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = 'https://pay.xmr.rocks/modal/btcpay.js'
script.onload = () => {
modalScriptLoaded = true
resolve()
}
script.onerror = () => reject(new Error('Failed to load BTCPay checkout'))
document.head.appendChild(script)
})
return modalScriptLoading
}
/**
* Create a payment invoice for a listing
* @param {string} listingId - The listing UUID
* @param {string} [currency='EUR'] - Payment currency
* @returns {Promise<Object>} { invoiceId, checkoutLink, status, expirationTime }
*/
export async function createInvoice(listingId, currency = 'EUR') {
const response = await fetch(`${POW_SERVER}/btcpay/invoice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ listingId, currency })
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.error || 'Failed to create invoice')
}
return response.json()
}
/**
* Check invoice payment status
* @param {string} invoiceId
* @returns {Promise<Object>} { invoiceId, status, additionalStatus }
*/
export async function getInvoiceStatus(invoiceId) {
const response = await fetch(`${POW_SERVER}/btcpay/status?id=${encodeURIComponent(invoiceId)}`)
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.error || 'Failed to check invoice status')
}
return response.json()
}
/**
* Open the BTCPay modal checkout
* @param {string} invoiceId
* @returns {Promise<string>} Final status when modal closes
*/
export async function openCheckout(invoiceId) {
await ensureModalLoaded()
return new Promise((resolve) => {
let lastStatus = null
window.btcpay.onModalReceiveMessage((msg) => {
if (msg?.status) lastStatus = msg.status
})
window.btcpay.onModalWillLeave(() => {
resolve(lastStatus || 'unknown')
})
window.btcpay.showInvoice(invoiceId)
})
}
/**
* Poll invoice status until settled, expired, or timeout
* @param {string} invoiceId
* @param {Object} [options]
* @param {number} [options.interval=4000] - Poll interval in ms
* @param {number} [options.timeout=900000] - Timeout in ms (default 15min)
* @param {Function} [options.onUpdate] - Callback on status change
* @returns {Promise<Object>} Final status object
*/
export async function pollUntilDone(invoiceId, options = {}) {
const {
interval = 4000,
timeout = 900000,
onUpdate = null
} = options
const startTime = Date.now()
let lastStatus = null
while (Date.now() - startTime < timeout) {
const result = await getInvoiceStatus(invoiceId)
if (result.status !== lastStatus) {
lastStatus = result.status
if (onUpdate) onUpdate(result)
}
if (['Settled', 'Expired', 'Invalid'].includes(result.status)) {
return result
}
await new Promise(r => setTimeout(r, interval))
}
throw new Error('Payment check timed out')
}
/**
* Check if a listing has a pending invoice stored locally
* @param {string} listingId
* @returns {Object|null} { invoiceId, createdAt }
*/
export function getPendingInvoice(listingId) {
try {
const data = localStorage.getItem(`dgray_invoice_${listingId}`)
return data ? JSON.parse(data) : null
} catch (e) {
return null
}
}
/**
* Store a pending invoice reference locally
* @param {string} listingId
* @param {string} invoiceId
*/
export function savePendingInvoice(listingId, invoiceId) {
try {
localStorage.setItem(`dgray_invoice_${listingId}`, JSON.stringify({
invoiceId,
createdAt: Date.now()
}))
} catch (e) {
// Storage unavailable
}
}
/**
* Remove pending invoice reference
* @param {string} listingId
*/
export function clearPendingInvoice(listingId) {
localStorage.removeItem(`dgray_invoice_${listingId}`)
}
export default {
createInvoice,
getInvoiceStatus,
openCheckout,
pollUntilDone,
getPendingInvoice,
savePendingInvoice,
clearPendingInvoice
}