refactor: components
This commit is contained in:
@@ -16,6 +16,18 @@
|
||||
src: url("{{ static_url_for('static', 'fonts/material-icons-v50.woff2') }}")
|
||||
format('woff2');
|
||||
}
|
||||
|
||||
.wasm-extension-page,
|
||||
.wasm-extension-frame {
|
||||
height: calc(100vh - 56px);
|
||||
min-height: calc(100vh - 56px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wasm-extension-frame {
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<title>{% block title %}{{ SITE_TITLE }}{% endblock %}</title>
|
||||
<meta charset="utf-8" />
|
||||
@@ -44,12 +56,14 @@
|
||||
<lnbits-drawer v-if="g.user && !g.isPublicPage"></lnbits-drawer>
|
||||
{% block page_container %}
|
||||
<q-page-container>
|
||||
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
||||
<q-page
|
||||
:class="$route.path.startsWith('/ext/') ? 'q-pa-none' : ['q-px-md', 'q-py-lg', {'q-px-lg': $q.screen.gt.xs}]"
|
||||
>
|
||||
<lnbits-wallet-new
|
||||
v-if="g.user && !g.isPublicPage"
|
||||
v-if="g.user && !g.isPublicPage && !$route.path.startsWith('/ext/')"
|
||||
></lnbits-wallet-new>
|
||||
<lnbits-header-wallets
|
||||
v-if="g.user && !g.isPublicPage"
|
||||
v-if="g.user && !g.isPublicPage && !$route.path.startsWith('/ext/')"
|
||||
></lnbits-header-wallets>
|
||||
<!-- block page content from static extensions -->
|
||||
<div
|
||||
|
||||
@@ -1,351 +1 @@
|
||||
{% extends "base.html" %} {% block styles %}
|
||||
<style>
|
||||
.wasm-extension-frame {
|
||||
border: 0;
|
||||
display: block;
|
||||
height: calc(100vh - 56px);
|
||||
min-height: calc(100vh - 56px);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block page_container %}
|
||||
<q-page-container>
|
||||
<!-- # todo:revisit this -->
|
||||
<q-page
|
||||
v-if="$route.path.startsWith('{{ normalize_path(request.path) }}')"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<iframe
|
||||
id="lnbits-wasm-extension-frame"
|
||||
class="wasm-extension-frame"
|
||||
data-frame-url="{{ frame_url }}"
|
||||
title="{{ extension.name }}"
|
||||
sandbox="allow-scripts"
|
||||
allow="clipboard-write"
|
||||
referrerpolicy="no-referrer"
|
||||
></iframe>
|
||||
</q-page>
|
||||
<q-page v-else class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
||||
<lnbits-wallet-new v-if="g.user && !g.isPublicPage"></lnbits-wallet-new>
|
||||
<lnbits-header-wallets
|
||||
v-if="g.user && !g.isPublicPage"
|
||||
></lnbits-header-wallets>
|
||||
<router-view :key="$route.path"></router-view>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
;(() => {
|
||||
const bridge = {{ bridge | tojson | safe }}
|
||||
let bridgePort = null
|
||||
const allowedPaymentHashes = new Set()
|
||||
const paymentSubscriptions = new Map()
|
||||
|
||||
function extensionFrameWindow() {
|
||||
return extensionFrame()?.contentWindow
|
||||
}
|
||||
|
||||
function extensionFrame() {
|
||||
return document.getElementById('lnbits-wasm-extension-frame')
|
||||
}
|
||||
|
||||
function loadExtensionFrame() {
|
||||
const frame = extensionFrame()
|
||||
if (!frame) {
|
||||
closeBridgePort()
|
||||
return
|
||||
}
|
||||
if (frame.dataset.loaded === 'true') return
|
||||
frame.dataset.loaded = 'true'
|
||||
frame.src = frame.dataset.frameUrl
|
||||
}
|
||||
|
||||
function startExtensionFrameLoader() {
|
||||
loadExtensionFrame()
|
||||
window.router?.afterEach(() => {
|
||||
window.setTimeout(loadExtensionFrame)
|
||||
})
|
||||
}
|
||||
|
||||
function sendResponse(reply, id, payload) {
|
||||
reply({
|
||||
type: 'lnbits-extension:response',
|
||||
id,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
|
||||
function allowedApiRoute(method, path) {
|
||||
let url
|
||||
try {
|
||||
url = new URL(path, window.location.origin)
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
if (url.origin !== window.location.origin) return false
|
||||
|
||||
method = String(method || 'GET').toUpperCase()
|
||||
return bridge.apiRoutes.some(route => {
|
||||
return (
|
||||
route.method === method &&
|
||||
new RegExp(route.pattern).test(url.pathname)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function callApi(message) {
|
||||
const method = String(message.method || 'GET').toUpperCase()
|
||||
const path = String(message.path || '')
|
||||
if (!allowedApiRoute(method, path)) {
|
||||
throw new Error('Extension API route is not allowed.')
|
||||
}
|
||||
|
||||
const options = {
|
||||
method,
|
||||
headers: {},
|
||||
credentials: 'same-origin'
|
||||
}
|
||||
if (message.body !== undefined && message.body !== null) {
|
||||
options.headers['content-type'] = 'application/json'
|
||||
options.body = JSON.stringify(message.body)
|
||||
}
|
||||
|
||||
const response = await fetch(path, options)
|
||||
const text = await response.text()
|
||||
let data = text
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch (_error) {
|
||||
data = text
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
typeof data === 'object' && data.detail ? data.detail : text
|
||||
)
|
||||
}
|
||||
rememberPaymentHashes(data)
|
||||
return data
|
||||
}
|
||||
|
||||
function notify(message) {
|
||||
const level = ['positive', 'negative', 'warning', 'info'].includes(
|
||||
message.level
|
||||
)
|
||||
? message.level
|
||||
: 'info'
|
||||
if (window.Quasar?.Notify) {
|
||||
window.Quasar.Notify.create({
|
||||
color: level,
|
||||
message: String(message.message || '')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function rememberPaymentHashes(value) {
|
||||
if (!value || typeof value !== 'object') return
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(rememberPaymentHashes)
|
||||
return
|
||||
}
|
||||
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (
|
||||
['paymentHash', 'payment_hash'].includes(key) &&
|
||||
isPaymentHash(item)
|
||||
) {
|
||||
allowedPaymentHashes.add(item)
|
||||
}
|
||||
rememberPaymentHashes(item)
|
||||
}
|
||||
}
|
||||
|
||||
function isPaymentHash(value) {
|
||||
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value)
|
||||
}
|
||||
|
||||
function websocketUrl(path) {
|
||||
const url = new URL(window.location.href)
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
url.pathname = path
|
||||
url.search = ''
|
||||
url.hash = ''
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
function sendBridgeEvent(message) {
|
||||
if (!bridgePort) return
|
||||
bridgePort.postMessage({
|
||||
type: 'lnbits-extension:event',
|
||||
...message
|
||||
})
|
||||
}
|
||||
|
||||
function closePaymentSubscription(subscriptionId) {
|
||||
const subscription = paymentSubscriptions.get(subscriptionId)
|
||||
if (!subscription) return
|
||||
paymentSubscriptions.delete(subscriptionId)
|
||||
try {
|
||||
subscription.socket.close()
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
function closePaymentSubscriptions() {
|
||||
for (const subscriptionId of Array.from(paymentSubscriptions.keys())) {
|
||||
closePaymentSubscription(subscriptionId)
|
||||
}
|
||||
}
|
||||
|
||||
function closeBridgePort() {
|
||||
closePaymentSubscriptions()
|
||||
bridgePort?.close()
|
||||
bridgePort = null
|
||||
}
|
||||
|
||||
function subscribePayment(message) {
|
||||
const subscriptionId = String(message.subscriptionId || '')
|
||||
const paymentHash = String(message.paymentHash || '')
|
||||
|
||||
if (!subscriptionId || !isPaymentHash(paymentHash)) {
|
||||
throw new Error('Invalid payment subscription.')
|
||||
}
|
||||
if (!allowedPaymentHashes.has(paymentHash)) {
|
||||
throw new Error('Payment subscription is not allowed.')
|
||||
}
|
||||
|
||||
closePaymentSubscription(subscriptionId)
|
||||
|
||||
const socket = new WebSocket(
|
||||
websocketUrl(`/api/v1/ws/${encodeURIComponent(paymentHash)}`)
|
||||
)
|
||||
paymentSubscriptions.set(subscriptionId, {paymentHash, socket})
|
||||
|
||||
socket.addEventListener('message', event => {
|
||||
let data = event.data
|
||||
try {
|
||||
data = JSON.parse(event.data)
|
||||
} catch (_error) {}
|
||||
|
||||
sendBridgeEvent({
|
||||
event: 'payment.update',
|
||||
subscriptionId,
|
||||
paymentHash,
|
||||
data
|
||||
})
|
||||
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
(data.pending === false ||
|
||||
['success', 'settled', 'paid'].includes(String(data.status || '')))
|
||||
) {
|
||||
sendBridgeEvent({
|
||||
event: 'payment.settled',
|
||||
subscriptionId,
|
||||
paymentHash,
|
||||
data
|
||||
})
|
||||
closePaymentSubscription(subscriptionId)
|
||||
}
|
||||
})
|
||||
socket.addEventListener('error', () => {
|
||||
sendBridgeEvent({
|
||||
event: 'payment.error',
|
||||
subscriptionId,
|
||||
paymentHash
|
||||
})
|
||||
closePaymentSubscription(subscriptionId)
|
||||
})
|
||||
socket.addEventListener('close', () => {
|
||||
paymentSubscriptions.delete(subscriptionId)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleBridgeRequest(message, reply) {
|
||||
if (!message || message.type !== 'lnbits-extension:request') return
|
||||
|
||||
try {
|
||||
if (message.action === 'context') {
|
||||
sendResponse(reply, message.id, {
|
||||
ok: true,
|
||||
data: {
|
||||
extensionId: bridge.extensionId,
|
||||
public: bridge.public,
|
||||
routeParams: bridge.routeParams,
|
||||
query: bridge.query
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (message.action === 'api') {
|
||||
sendResponse(reply, message.id, {
|
||||
ok: true,
|
||||
data: await callApi(message)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (message.action === 'ui.notify') {
|
||||
notify(message)
|
||||
sendResponse(reply, message.id, {
|
||||
ok: true,
|
||||
data: {ok: true}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (message.action === 'payment.subscribe') {
|
||||
subscribePayment(message)
|
||||
sendResponse(reply, message.id, {
|
||||
ok: true,
|
||||
data: {ok: true}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (message.action === 'payment.unsubscribe') {
|
||||
closePaymentSubscription(String(message.subscriptionId || ''))
|
||||
sendResponse(reply, message.id, {
|
||||
ok: true,
|
||||
data: {ok: true}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('Unknown extension bridge action.')
|
||||
} catch (error) {
|
||||
sendResponse(reply, message.id, {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (event.source !== extensionFrameWindow()) return
|
||||
const message = event.data
|
||||
if (!message || message.type !== 'lnbits-extension:connect') return
|
||||
|
||||
const port = event.ports?.[0]
|
||||
if (!port) return
|
||||
|
||||
closeBridgePort()
|
||||
bridgePort = port
|
||||
bridgePort.addEventListener('message', portEvent => {
|
||||
handleBridgeRequest(portEvent.data, response => {
|
||||
port.postMessage(response)
|
||||
})
|
||||
})
|
||||
bridgePort.start()
|
||||
bridgePort.postMessage({
|
||||
type: 'lnbits-extension:connected',
|
||||
id: message.id
|
||||
})
|
||||
})
|
||||
window.addEventListener('load', startExtensionFrameLoader)
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
Reference in New Issue
Block a user