Add invoice view
This commit is contained in:
parent
a21756389e
commit
4aaada7cb7
@ -18,6 +18,7 @@ func HandleInvoiceStatus(sc context.ServerContext) echo.HandlerFunc {
|
|||||||
invoiceId string
|
invoiceId string
|
||||||
invoice db.Invoice
|
invoice db.Invoice
|
||||||
u db.User
|
u db.User
|
||||||
|
qr string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
invoiceId = c.Param("id")
|
invoiceId = c.Param("id")
|
||||||
@ -29,8 +30,23 @@ func HandleInvoiceStatus(sc context.ServerContext) echo.HandlerFunc {
|
|||||||
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
invoice.Preimage = ""
|
invoice.Preimage = ""
|
||||||
return c.JSON(http.StatusOK, invoice)
|
data := map[string]any{
|
||||||
|
"Id": invoice.Id,
|
||||||
|
"Msats": invoice.Msats,
|
||||||
|
"MsatsReceived": invoice.MsatsReceived,
|
||||||
|
"Hash": invoice.Hash,
|
||||||
|
"PaymentRequest": invoice.PaymentRequest,
|
||||||
|
"CreatedAt": invoice.CreatedAt,
|
||||||
|
"ExpiresAt": invoice.ExpiresAt,
|
||||||
|
"ConfirmedAt": invoice.ConfirmedAt,
|
||||||
|
"HeldSince": invoice.HeldSince,
|
||||||
|
"Qr": qr,
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
108
vue/src/components/Invoice.vue
Normal file
108
vue/src/components/Invoice.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<router-link v-if="success" :to="callbackUrl" class="label success font-mono">
|
||||||
|
<div>Paid</div>
|
||||||
|
<small>Redirecting in {{ redirectTimeout }} ...</small>
|
||||||
|
</router-link>
|
||||||
|
<div class="font-mono my-3">
|
||||||
|
Payment Required
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="label error font-mono">
|
||||||
|
<div>Error</div>
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
</div>
|
||||||
|
<div v-if="invoice">
|
||||||
|
<figure class="flex flex-col m-auto">
|
||||||
|
<a class="m-auto" :href="'lightning:' + invoice.PaymentRequest">
|
||||||
|
<img :src="'data:image/png;base64,' + invoice.Qr" />
|
||||||
|
</a>
|
||||||
|
<figcaption class="flex flex-row my-3 font-mono text-xs">
|
||||||
|
<span class="w-[80%] text-ellipsis overflow-hidden">{{ invoice.PaymentRequest }}</span>
|
||||||
|
<button @click.prevent="copy">{{ label }}</button>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
<div class="grid text-muted text-xs">
|
||||||
|
<span class="mx-3 my-1">payment hash</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.Hash }}
|
||||||
|
</span>
|
||||||
|
<span class="mx-3 my-1">created at</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.CreatedAt }}
|
||||||
|
</span>
|
||||||
|
<span class="mx-3 my-1">expires at</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.ExpiresAt }}
|
||||||
|
</span>
|
||||||
|
<span class="mx-3 my-1">msats</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.Msats }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
// TODO validate callback url
|
||||||
|
const callbackUrl = route.params.callbackUrl ?? '/'
|
||||||
|
|
||||||
|
const INVOICE_POLL = 2000
|
||||||
|
const poll = async () => {
|
||||||
|
const url = window.origin + '/api/invoice/' + route.params.id
|
||||||
|
const res = await fetch(url)
|
||||||
|
const body = await res.json()
|
||||||
|
if (body.ConfirmedAt) {
|
||||||
|
success.value = true
|
||||||
|
clearInterval(interval)
|
||||||
|
setInterval(() => {
|
||||||
|
if (--redirectTimeout.value === 0) {
|
||||||
|
router.push(callbackUrl)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const interval = setInterval(poll, INVOICE_POLL)
|
||||||
|
|
||||||
|
const invoice = ref(null)
|
||||||
|
const redirectTimeout = ref(3)
|
||||||
|
const success = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
const label = ref('copy')
|
||||||
|
let copyTimeout = null
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard?.writeText(invoice.value.PaymentRequest)
|
||||||
|
label.value = 'copied'
|
||||||
|
if (copyTimeout) clearTimeout(copyTimeout)
|
||||||
|
copyTimeout = setTimeout(() => { label.value = 'copy' }, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
await (async () => {
|
||||||
|
const url = window.origin + '/api/invoice/' + route.params.id
|
||||||
|
const res = await fetch(url)
|
||||||
|
const body = await res.json()
|
||||||
|
invoice.value = body
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
img {
|
||||||
|
width: 256px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
margin: 0.75em auto;
|
||||||
|
width: 256px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid {
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -60,3 +60,7 @@ a.selected {
|
|||||||
.error:hover {
|
.error:hover {
|
||||||
background-color: #ff7386;
|
background-color: #ff7386;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
@ -9,6 +9,7 @@ import MarketsView from '@/views/MarketsView'
|
|||||||
import LoginView from '@/views/LoginView'
|
import LoginView from '@/views/LoginView'
|
||||||
import UserView from '@/views/UserView'
|
import UserView from '@/views/UserView'
|
||||||
import MarketView from '@/views/MarketView'
|
import MarketView from '@/views/MarketView'
|
||||||
|
import InvoiceView from '@/views/InvoiceView'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -22,6 +23,9 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/market/:id', component: MarketView
|
path: '/market/:id', component: MarketView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/invoice/:id', component: InvoiceView
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const router = VueRouter.createRouter({
|
const router = VueRouter.createRouter({
|
||||||
|
19
vue/src/views/InvoiceView.vue
Normal file
19
vue/src/views/InvoiceView.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<div class="my-3">
|
||||||
|
<pre>
|
||||||
|
_ _ ___ ____
|
||||||
|
| || | / _ \___ \
|
||||||
|
| || |_| | | |__) |
|
||||||
|
|__ _| |_| / __/
|
||||||
|
|_| \___/_____|</pre>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
<Suspense>
|
||||||
|
<Invoice />
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Invoice from '@/components/Invoice'
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user