Compare commits

..

3 Commits

5 changed files with 80 additions and 38 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ Members:
## `async fn handle_activity(&self, activity: &Activity)` ## `async fn handle_activity(&self, activity: &Activity)`
- For `create_relay`, `update_relay`, or `deactivate_relay` activity, calls `sync_and_report`. - For `create_relay`, `update_relay`, `activate_relay`, or `deactivate_relay` activity, calls `sync_and_report`.
- All other activity types are ignored (e.g. `fail_relay_sync`, `complete_relay_sync`). - All other activity types are ignored (e.g. `fail_relay_sync`, `complete_relay_sync`).
## `async fn sync_and_report(&self, relay: &Relay, is_new: bool)` ## `async fn sync_and_report(&self, relay: &Relay, is_new: bool)`
-3
View File
@@ -901,10 +901,7 @@ mod tests {
&unknown_status_paid &unknown_status_paid
)); ));
} }
}
#[cfg(test)]
mod tests {
use super::*; use super::*;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
+1 -2
View File
@@ -209,8 +209,7 @@ impl Command {
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
let activity = let activity = Self::insert_activity(&mut tx, activity_type, "relay", relay_id).await?;
Self::insert_activity(&mut tx, "deactivate_relay", "relay", &relay_id).await?;
tx.commit().await?; tx.commit().await?;
self.emit(activity); self.emit(activity);
+23 -7
View File
@@ -56,10 +56,7 @@ impl Infra {
} }
async fn handle_activity(&self, activity: &Activity) -> Result<()> { async fn handle_activity(&self, activity: &Activity) -> Result<()> {
let needs_sync = matches!( let needs_sync = should_sync_relay_activity(activity.activity_type.as_str());
activity.activity_type.as_str(),
"create_relay" | "update_relay" | "deactivate_relay"
);
if needs_sync { if needs_sync {
let Some(relay) = self.query.get_relay(&activity.resource_id).await? else { let Some(relay) = self.query.get_relay(&activity.resource_id).await? else {
@@ -93,7 +90,9 @@ impl Infra {
async fn nip98_auth(&self, url: &str, method: HttpMethod) -> Result<String> { async fn nip98_auth(&self, url: &str, method: HttpMethod) -> Result<String> {
let keys = Keys::parse(&self.api_secret)?; let keys = Keys::parse(&self.api_secret)?;
let server_url = Url::parse(url)?; let server_url = Url::parse(url)?;
let auth = HttpData::new(server_url, method).to_authorization(&keys).await?; let auth = HttpData::new(server_url, method)
.to_authorization(&keys)
.await?;
Ok(auth) Ok(auth)
} }
@@ -150,11 +149,21 @@ impl Infra {
let response = if is_new { let response = if is_new {
let url = format!("{}/relay/{}", base, relay.id); let url = format!("{}/relay/{}", base, relay.id);
let auth = self.nip98_auth(&url, HttpMethod::POST).await?; let auth = self.nip98_auth(&url, HttpMethod::POST).await?;
client.post(&url).header("Authorization", auth).json(&body).send().await? client
.post(&url)
.header("Authorization", auth)
.json(&body)
.send()
.await?
} else { } else {
let url = format!("{}/relay/{}", base, relay.id); let url = format!("{}/relay/{}", base, relay.id);
let auth = self.nip98_auth(&url, HttpMethod::PUT).await?; let auth = self.nip98_auth(&url, HttpMethod::PUT).await?;
client.put(&url).header("Authorization", auth).json(&body).send().await? client
.put(&url)
.header("Authorization", auth)
.json(&body)
.send()
.await?
}; };
if !response.status().is_success() { if !response.status().is_success() {
@@ -165,3 +174,10 @@ impl Infra {
Ok(()) Ok(())
} }
} }
fn should_sync_relay_activity(activity_type: &str) -> bool {
matches!(
activity_type,
"create_relay" | "update_relay" | "activate_relay" | "deactivate_relay"
)
}
+55 -25
View File
@@ -6,6 +6,7 @@ import { getInvoice, getInvoiceBolt11 } from "@/lib/api"
import { tenantNeedsPaymentSetup } from "@/lib/hooks" import { tenantNeedsPaymentSetup } from "@/lib/hooks"
type PayStatus = "idle" | "loading" | "success" | "error" type PayStatus = "idle" | "loading" | "success" | "error"
type Bolt11Status = "idle" | "loading" | "ready" | "error"
type PaymentInvoice = { type PaymentInvoice = {
id: string id: string
@@ -21,20 +22,34 @@ type PaymentDialogProps = {
export default function PaymentDialog(props: PaymentDialogProps) { export default function PaymentDialog(props: PaymentDialogProps) {
const [bolt11, setBolt11] = createSignal("") const [bolt11, setBolt11] = createSignal("")
const [qrDataUrl, setQrDataUrl] = createSignal("") const [qrDataUrl, setQrDataUrl] = createSignal("")
const [bolt11Status, setBolt11Status] = createSignal<Bolt11Status>("idle")
const [bolt11Error, setBolt11Error] = createSignal("")
const [payStatus, setPayStatus] = createSignal<PayStatus>("idle") const [payStatus, setPayStatus] = createSignal<PayStatus>("idle")
const [payError, setPayError] = createSignal("") const [payError, setPayError] = createSignal("")
const [showSetup, setShowSetup] = createSignal(false) const [showSetup, setShowSetup] = createSignal(false)
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false) const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
createEffect(async () => { async function loadBolt11() {
if (!props.open || !props.invoice.id) return if (!props.invoice.id) return
setBolt11Status("loading")
setBolt11Error("")
setBolt11("")
setQrDataUrl("")
try { try {
const { bolt11: invoice } = await getInvoiceBolt11(props.invoice.id) const { bolt11: invoice } = await getInvoiceBolt11(props.invoice.id)
setBolt11(invoice) setBolt11(invoice)
setQrDataUrl(await QRCode.toDataURL(invoice, { width: 256, margin: 2 })) setQrDataUrl(await QRCode.toDataURL(invoice, { width: 256, margin: 2 }))
} catch { setBolt11Status("ready")
// bolt11 generation may fail } catch (e) {
setBolt11Status("error")
setBolt11Error(e instanceof Error ? e.message : "Failed to generate Lightning invoice")
} }
}
createEffect(() => {
if (!props.open || !props.invoice.id) return
void loadBolt11()
}) })
function copyBolt11() { function copyBolt11() {
@@ -62,6 +77,8 @@ export default function PaymentDialog(props: PaymentDialogProps) {
function handleClose() { function handleClose() {
setPayStatus("idle") setPayStatus("idle")
setPayError("") setPayError("")
setBolt11Status("idle")
setBolt11Error("")
setBolt11("") setBolt11("")
setQrDataUrl("") setQrDataUrl("")
setShowSetup(false) setShowSetup(false)
@@ -104,33 +121,46 @@ export default function PaymentDialog(props: PaymentDialogProps) {
when={payStatus() === "success"} when={payStatus() === "success"}
fallback={ fallback={
<div class="w-full space-y-3"> <div class="w-full space-y-3">
<Show <Show when={bolt11Status() === "idle" || bolt11Status() === "loading"}>
when={qrDataUrl()} <div class="flex items-center justify-center py-12 text-sm text-gray-400">Generating invoice...</div>
fallback={<div class="flex items-center justify-center py-12 text-sm text-gray-400">Generating invoice...</div>}
>
<img src={qrDataUrl()} alt="Lightning invoice QR code" class="mx-auto rounded-lg" />
</Show> </Show>
<Show when={bolt11()}> <Show when={bolt11Status() === "error"}>
<div class="flex rounded-lg border border-gray-300"> <div class="rounded-lg border border-red-200 bg-red-50 p-4">
<input <p class="text-sm font-medium text-red-700">Unable to generate invoice</p>
type="text" <p class="mt-1 text-xs text-red-600 wrap-break-word">{bolt11Error()}</p>
readOnly
value={bolt11()}
class="min-w-0 flex-1 rounded-l-lg border-0 px-3 py-2 text-xs text-gray-500 bg-transparent focus:outline-none"
/>
<button <button
type="button" type="button"
class="flex items-center px-3 text-gray-400 hover:text-gray-700" onClick={() => void loadBolt11()}
onClick={copyBolt11} class="mt-3 inline-flex items-center rounded-lg bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700"
title="Copy invoice"
> >
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> Retry
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
</svg>
</button> </button>
</div> </div>
</Show> </Show>
<Show when={bolt11Status() === "ready"}>
<img src={qrDataUrl()} alt="Lightning invoice QR code" class="mx-auto rounded-lg" />
<Show when={bolt11()}>
<div class="flex rounded-lg border border-gray-300">
<input
type="text"
readOnly
value={bolt11()}
class="min-w-0 flex-1 rounded-l-lg border-0 px-3 py-2 text-xs text-gray-500 bg-transparent focus:outline-none"
/>
<button
type="button"
class="flex items-center px-3 text-gray-400 hover:text-gray-700"
onClick={copyBolt11}
title="Copy invoice"
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
</svg>
</button>
</div>
</Show>
</Show>
</div> </div>
} }
> >
@@ -188,7 +218,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
<button <button
type="button" type="button"
onClick={checkPayment} onClick={checkPayment}
disabled={payStatus() === "loading"} disabled={payStatus() === "loading" || bolt11Status() !== "ready"}
class="py-2 px-4 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors" class="py-2 px-4 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
> >
{payStatus() === "loading" ? "Checking..." : "Complete Payment"} {payStatus() === "loading" ? "Checking..." : "Complete Payment"}