refactor: components

This commit is contained in:
Vlad Stan
2026-07-08 11:55:11 +03:00
parent da412a2321
commit f3cdf85f3b
7 changed files with 580 additions and 380 deletions
+150 -26
View File
@@ -140,6 +140,8 @@ def _mount_wasm_extension_static(app: FastAPI, extension: WasmExtension) -> None
def _register_wasm_extension_ui_routes(app: FastAPI, extension: WasmExtension) -> None:
_add_wasm_extension_frame_config_route(app, extension)
for route_index, route_config in enumerate(extension.config.get("ui_routes") or []):
route_path = _wasm_extension_ui_route_path(extension, route_config.get("path"))
entrypoint = _wasm_extension_entrypoint(
@@ -147,15 +149,12 @@ def _register_wasm_extension_ui_routes(app: FastAPI, extension: WasmExtension) -
)
frame_path = f"/ext-frame/{extension.id}/{route_index}"
auth = _wasm_extension_route_auth(extension, route_config.get("auth"))
path_params = route_config.get("path_params") or {}
_add_wasm_extension_frame_route(app, extension, frame_path, entrypoint)
_add_wasm_extension_wrapper_route(
app,
extension,
route_path,
frame_path,
auth,
path_params,
)
@@ -222,6 +221,52 @@ def _add_wasm_extension_api_route(
)
def _add_wasm_extension_frame_config_route(
app: FastAPI,
extension: WasmExtension,
) -> None:
route_path = _wasm_extension_frame_config_path(extension)
if _has_route(app, route_path, "POST"):
return
async def create_wasm_extension_frame_config(
request: Request,
access_token: Annotated[str | None, Depends(check_access_token)],
usr: UUID4 | None = None,
) -> dict[str, Any]:
try:
body = await _read_json_object(request)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
ui_route = _match_wasm_extension_ui_route(extension, body.get("path"))
auth = ui_route["auth"]
if auth == "user":
account = await check_account_exists(request, access_token, usr)
user_id: str | None = account.id
else:
user_id = await _optional_wasm_user_id(request, access_token, usr)
return _wasm_extension_frame_config(
extension,
ui_route["frame_path"],
auth,
ui_route["path_params"],
ui_route["route_params"],
_read_wasm_extension_route_query(body.get("query")),
user_id,
)
app.add_api_route(
route_path,
create_wasm_extension_frame_config,
methods=["POST"],
name=f"{extension.id}:frame-config",
include_in_schema=False,
)
async def _read_api_payload(
request: Request,
path_params: dict[str, str],
@@ -307,9 +352,7 @@ def _add_wasm_extension_wrapper_route(
app: FastAPI,
extension: WasmExtension,
route_path: str,
frame_path: str,
auth: str,
path_params: dict[str, str],
) -> None:
if _has_route(app, route_path, "GET"):
return
@@ -322,25 +365,16 @@ def _add_wasm_extension_wrapper_route(
return _wasm_extension_wrapper_response(
request,
extension,
frame_path,
auth,
path_params,
user.json() if user else None,
account.id,
)
async def serve_public_wasm_extension_page(
request: Request,
user_id: str | None = Depends(_optional_wasm_user_id),
) -> Any:
async def serve_public_wasm_extension_page(request: Request) -> Any:
return _wasm_extension_wrapper_response(
request,
extension,
frame_path,
auth,
path_params,
None,
user_id,
)
app.add_api_route(
@@ -398,11 +432,8 @@ def _add_wasm_extension_frame_route(
def _wasm_extension_wrapper_response(
request: Request,
extension: WasmExtension,
frame_path: str,
auth: str,
path_params: dict[str, str],
user_json: str | None,
user_id: str | None,
) -> Any:
public = auth == "public"
response = template_renderer().TemplateResponse(
@@ -410,14 +441,6 @@ def _wasm_extension_wrapper_response(
"wasm_extension.html",
{
"extension": extension,
"frame_url": _wasm_extension_frame_url(extension, frame_path, user_id),
"bridge": {
"extensionId": extension.id,
"public": public,
"routeParams": _read_api_path_params(request, path_params),
"query": _read_api_query_params(request),
"apiRoutes": _wasm_extension_bridge_api_routes(extension, public),
},
"public": public,
"user": user_json,
},
@@ -541,6 +564,107 @@ def _wasm_extension_bridge_api_routes(
return routes
def _wasm_extension_frame_config_path(extension: WasmExtension) -> str:
return f"/api/v1/ext/{extension.id}/_ui/frame"
def _match_wasm_extension_ui_route(
extension: WasmExtension,
path: Any,
) -> dict[str, Any]:
if not isinstance(path, str) or not path.startswith("/"):
raise HTTPException(status_code=404, detail="Not found")
for route_index, route_config in enumerate(extension.config.get("ui_routes") or []):
route_path = _wasm_extension_ui_route_path(extension, route_config.get("path"))
route_params = _path_template_params(route_path, path)
if route_params is None:
continue
return {
"frame_path": f"/ext-frame/{extension.id}/{route_index}",
"auth": _wasm_extension_route_auth(extension, route_config.get("auth")),
"path_params": route_config.get("path_params") or {},
"route_params": route_params,
}
raise HTTPException(status_code=404, detail="Not found")
def _path_template_params(template: str, path: str) -> dict[str, str] | None:
template_parts = _path_parts(template)
path_parts = _path_parts(path)
if len(template_parts) != len(path_parts):
return None
params: dict[str, str] = {}
for template_part, path_part in zip(template_parts, path_parts, strict=False):
if template_part.startswith("{") and template_part.endswith("}"):
param_name = template_part[1:-1]
if not param_name:
return None
params[param_name] = path_part
continue
if template_part != path_part:
return None
return params
def _path_parts(path: str) -> list[str]:
return [part for part in path.strip("/").split("/") if part]
def _wasm_extension_frame_config(
extension: WasmExtension,
frame_path: str,
auth: str,
path_params: dict[str, str],
route_params: dict[str, str],
query: dict[str, Any],
user_id: str | None,
) -> dict[str, Any]:
public = auth == "public"
return {
"extension": {
"id": extension.id,
"name": extension.name,
},
"frameUrl": _wasm_extension_frame_url(extension, frame_path, user_id),
"bridge": {
"extensionId": extension.id,
"public": public,
"routeParams": _map_wasm_extension_route_params(route_params, path_params),
"query": query,
"apiRoutes": _wasm_extension_bridge_api_routes(extension, public),
},
}
def _map_wasm_extension_route_params(
route_params: dict[str, str],
path_params: dict[str, str],
) -> dict[str, str]:
payload: dict[str, str] = {}
for key, value in route_params.items():
target = path_params.get(key) or _snake_to_camel(key)
payload[target] = value
return payload
def _read_wasm_extension_route_query(query: Any) -> dict[str, Any]:
if not isinstance(query, dict):
return {}
payload: dict[str, Any] = {}
for key, value in query.items():
if value is None:
continue
payload[_snake_to_camel(str(key))] = value
return payload
def _path_template_pattern(path: str) -> str:
pattern = re.sub(r"\\{[^/{}]+\\}", r"[^/]+", re.escape(path))
return f"^{pattern}$"
+10
View File
@@ -139,6 +139,16 @@ const routes = [
name: 'PageError',
component: PageError
},
{
path: '/ext/:extId',
name: 'WasmExtensionRoot',
component: window.WasmExtensionComponent
},
{
path: '/ext/:extId/:pathMatch(.*)*',
name: 'WasmExtension',
component: window.WasmExtensionComponent
},
{
path: '/:pathMatch(.*)*',
name: 'DynamicComponent',
@@ -0,0 +1,400 @@
window.WasmExtensionComponent = {
template: `
<div class="wasm-extension-page relative-position">
<q-inner-loading :showing="loading && !frameUrl">
<q-spinner-dots size="40px"></q-spinner-dots>
</q-inner-loading>
<q-banner v-if="error" class="q-ma-md bg-negative text-white">
{{ error }}
</q-banner>
<iframe
v-else-if="frameUrl"
ref="frame"
:key="frameUrl"
class="wasm-extension-frame"
:src="frameUrl"
:title="extensionName || 'Extension'"
sandbox="allow-scripts"
allow="clipboard-write"
referrerpolicy="no-referrer"
></iframe>
</div>
`,
data() {
return {
allowedPaymentHashes: new Set(),
bridge: {
apiRoutes: [],
extensionId: '',
public: false,
query: {},
routeParams: {}
},
bridgePort: null,
error: '',
extensionName: '',
frameUrl: '',
handleWindowMessage: null,
loading: false,
loadId: 0,
paymentSubscriptions: new Map()
}
},
created() {
this.handleWindowMessage = event => this.onWindowMessage(event)
window.addEventListener('message', this.handleWindowMessage)
},
unmounted() {
window.removeEventListener('message', this.handleWindowMessage)
this.closeBridgePort()
},
watch: {
'$route.fullPath': {
immediate: true,
handler() {
this.loadFrameConfig()
}
}
},
methods: {
emptyBridge() {
return {
apiRoutes: [],
extensionId: '',
public: false,
query: {},
routeParams: {}
}
},
plainBridgeContext() {
return {
extensionId: String(this.bridge.extensionId || ''),
public: Boolean(this.bridge.public),
routeParams: this.plainValue(this.bridge.routeParams || {}),
query: this.plainValue(this.bridge.query || {})
}
},
plainValue(value) {
try {
return JSON.parse(JSON.stringify(value))
} catch (_error) {
return {}
}
},
async loadFrameConfig() {
const extId = String(this.$route.params.extId || '')
const loadId = ++this.loadId
this.loading = true
this.error = ''
this.frameUrl = ''
this.bridge = this.emptyBridge()
this.allowedPaymentHashes.clear()
this.closeBridgePort()
try {
const response = await fetch(
`/api/v1/ext/${encodeURIComponent(extId)}/_ui/frame`,
{
method: 'POST',
headers: {'content-type': 'application/json'},
credentials: 'same-origin',
body: JSON.stringify({
path: this.$route.path,
query: this.$route.query || {}
})
}
)
const text = await response.text()
let data = {}
if (text) {
try {
data = JSON.parse(text)
} catch (_error) {
data = {detail: text}
}
}
if (!response.ok) {
throw new Error(data?.detail || 'Failed to load extension page.')
}
if (loadId !== this.loadId) return
this.bridge = data.bridge || this.emptyBridge()
this.extensionName = data.extension?.name || extId
this.frameUrl = data.frameUrl
} catch (error) {
if (loadId !== this.loadId) return
console.error('[lnbits wasm extension] Failed to load frame.', error)
this.error = error instanceof Error ? error.message : String(error)
} finally {
if (loadId === this.loadId) {
this.loading = false
}
}
},
extensionFrameWindow() {
return this.$refs.frame?.contentWindow
},
sendResponse(reply, id, payload) {
reply({
type: 'lnbits-extension:response',
id,
...payload
})
},
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 (this.bridge.apiRoutes || []).some(route => {
return (
route.method === method &&
new RegExp(route.pattern).test(url.pathname)
)
})
},
async callApi(message) {
const method = String(message.method || 'GET').toUpperCase()
const path = String(message.path || '')
if (!this.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
)
}
this.rememberPaymentHashes(data)
return data
},
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 || '')
})
}
},
rememberPaymentHashes(value) {
if (!value || typeof value !== 'object') return
if (Array.isArray(value)) {
value.forEach(item => this.rememberPaymentHashes(item))
return
}
for (const [key, item] of Object.entries(value)) {
if (
['paymentHash', 'payment_hash'].includes(key) &&
this.isPaymentHash(item)
) {
this.allowedPaymentHashes.add(item)
}
this.rememberPaymentHashes(item)
}
},
isPaymentHash(value) {
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value)
},
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()
},
sendBridgeEvent(message) {
if (!this.bridgePort) return
this.bridgePort.postMessage({
type: 'lnbits-extension:event',
...message
})
},
closePaymentSubscription(subscriptionId) {
const subscription = this.paymentSubscriptions.get(subscriptionId)
if (!subscription) return
this.paymentSubscriptions.delete(subscriptionId)
try {
subscription.socket.close()
} catch (_error) {}
},
closePaymentSubscriptions() {
for (const subscriptionId of Array.from(
this.paymentSubscriptions.keys()
)) {
this.closePaymentSubscription(subscriptionId)
}
},
closeBridgePort() {
this.closePaymentSubscriptions()
this.bridgePort?.close()
this.bridgePort = null
},
subscribePayment(message) {
const subscriptionId = String(message.subscriptionId || '')
const paymentHash = String(message.paymentHash || '')
if (!subscriptionId || !this.isPaymentHash(paymentHash)) {
throw new Error('Invalid payment subscription.')
}
if (!this.allowedPaymentHashes.has(paymentHash)) {
throw new Error('Payment subscription is not allowed.')
}
this.closePaymentSubscription(subscriptionId)
const socket = new WebSocket(
this.websocketUrl(`/api/v1/ws/${encodeURIComponent(paymentHash)}`)
)
this.paymentSubscriptions.set(subscriptionId, {paymentHash, socket})
socket.addEventListener('message', event => {
let data = event.data
try {
data = JSON.parse(event.data)
} catch (_error) {}
this.sendBridgeEvent({
event: 'payment.update',
subscriptionId,
paymentHash,
data
})
if (
data &&
typeof data === 'object' &&
(data.pending === false ||
['success', 'settled', 'paid'].includes(String(data.status || '')))
) {
this.sendBridgeEvent({
event: 'payment.settled',
subscriptionId,
paymentHash,
data
})
this.closePaymentSubscription(subscriptionId)
}
})
socket.addEventListener('error', () => {
this.sendBridgeEvent({
event: 'payment.error',
subscriptionId,
paymentHash
})
this.closePaymentSubscription(subscriptionId)
})
socket.addEventListener('close', () => {
this.paymentSubscriptions.delete(subscriptionId)
})
},
async handleBridgeRequest(message, reply) {
if (!message || message.type !== 'lnbits-extension:request') return
try {
if (message.action === 'context') {
this.sendResponse(reply, message.id, {
ok: true,
data: this.plainBridgeContext()
})
return
}
if (message.action === 'api') {
this.sendResponse(reply, message.id, {
ok: true,
data: await this.callApi(message)
})
return
}
if (message.action === 'ui.notify') {
this.notify(message)
this.sendResponse(reply, message.id, {
ok: true,
data: {ok: true}
})
return
}
if (message.action === 'payment.subscribe') {
this.subscribePayment(message)
this.sendResponse(reply, message.id, {
ok: true,
data: {ok: true}
})
return
}
if (message.action === 'payment.unsubscribe') {
this.closePaymentSubscription(String(message.subscriptionId || ''))
this.sendResponse(reply, message.id, {
ok: true,
data: {ok: true}
})
return
}
throw new Error('Unknown extension bridge action.')
} catch (error) {
this.sendResponse(reply, message.id, {
ok: false,
error: error instanceof Error ? error.message : String(error)
})
}
},
onWindowMessage(event) {
if (event.source !== this.extensionFrameWindow()) return
const message = event.data
if (!message || message.type !== 'lnbits-extension:connect') return
const port = event.ports?.[0]
if (!port) return
this.closeBridgePort()
this.bridgePort = port
this.bridgePort.addEventListener('message', portEvent => {
this.handleBridgeRequest(portEvent.data, response => {
port.postMessage(response)
})
})
this.bridgePort.start()
this.bridgePort.postMessage({
type: 'lnbits-extension:connected',
id: message.id
})
}
}
}
+1
View File
@@ -96,6 +96,7 @@
"js/components/extension-settings.js",
"js/components/data-fields.js",
"js/components.js",
"js/wasm-extension-component.js",
"js/init-app.js"
],
"css": ["vendor/quasar.css", "css/base.css"]
+17 -3
View File
@@ -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
View File
@@ -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" %}
+1
View File
@@ -149,6 +149,7 @@
"js/components/extension-settings.js",
"js/components/data-fields.js",
"js/components.js",
"js/wasm-extension-component.js",
"js/init-app.js"
],
"css": [