feat: serve in iframe

This commit is contained in:
Vlad Stan
2026-07-01 16:57:19 +03:00
parent b9bcddc0b8
commit 850e1c72f6
2 changed files with 348 additions and 17 deletions
+159
View File
@@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block styles %}
<style>
.wasm-extension-frame {
border: 0;
display: block;
min-height: calc(100vh - 170px);
width: 100%;
}
.wasm-extension-public {
margin: 0 auto;
max-width: 1180px;
}
</style>
{% endblock %}
{% block page %}
<div class="{% if public %}wasm-extension-public{% endif %}">
<iframe
id="lnbits-wasm-extension-frame"
class="wasm-extension-frame"
src="{{ frame_url }}"
title="{{ extension.name }}"
sandbox="allow-scripts allow-forms"
referrerpolicy="no-referrer"
></iframe>
</div>
{% endblock %}
{% block scripts %}
<script>
;(() => {
const bridge = {{ bridge | tojson | safe }}
const frame = document.getElementById('lnbits-wasm-extension-frame')
function sendResponse(id, payload) {
frame.contentWindow?.postMessage(
{
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
)
}
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 || '')
})
}
}
window.addEventListener('message', async event => {
if (event.source !== frame.contentWindow) return
const message = event.data
if (!message || message.type !== 'lnbits-extension:request') return
try {
if (message.action === 'context') {
sendResponse(message.id, {
ok: true,
data: {
extensionId: bridge.extensionId,
public: bridge.public,
routeParams: bridge.routeParams,
query: bridge.query
}
})
return
}
if (message.action === 'api') {
sendResponse(message.id, {
ok: true,
data: await callApi(message)
})
return
}
if (message.action === 'ui.notify') {
notify(message)
sendResponse(message.id, {ok: true, data: {ok: true}})
return
}
throw new Error('Unknown extension bridge action.')
} catch (error) {
sendResponse(message.id, {
ok: false,
error: error instanceof Error ? error.message : String(error)
})
}
})
})()
</script>
{% endblock %}