aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-25 13:26:19 -0800
committerAdam Soltys <[email protected]>2026-02-25 13:26:19 -0800
commitb137cdfca84cf68a2b8a620ba1c55337e88a0050 (patch)
treec3f5482b08ce175fc4335d3f99838c06bb510877
parent75bc8ecc68362616a233f0852361b2dc79bcdbba (diff)
downloadcoinos-ui-b137cdfca84cf68a2b8a620ba1c55337e88a0050.tar.gz
coinos-ui-b137cdfca84cf68a2b8a620ba1c55337e88a0050.zip
fix lightning receive redirect
-rw-r--r--src/lib/socket.ts9
-rw-r--r--src/lib/store.ts3
-rw-r--r--src/routes/(app)/[username]/receive/+page.svelte12
-rw-r--r--test/e2e/receive-lightning-redirect.spec.ts53
4 files changed, 74 insertions, 3 deletions
diff --git a/src/lib/socket.ts b/src/lib/socket.ts
index 077fb856..3e71e1ce 100644
--- a/src/lib/socket.ts
+++ b/src/lib/socket.ts
@@ -1,7 +1,7 @@
import { goto, invalidate } from "$app/navigation";
import { navigating, page } from "$app/stores";
import { PUBLIC_SOCKET } from "$env/static/public";
-import { event, importing, invoice, last, paymentSignal, request } from "$lib/store";
+import { event, importing, invoice, last, lastPayment, paymentSignal, request } from "$lib/store";
import { s, sleep, success, wait } from "$lib/utils";
import cookies from "js-cookie";
import { get } from "svelte/store";
@@ -54,6 +54,8 @@ export const messages = (data) => ({
return;
}
+ if (amount > 0 && iid) lastPayment.set({ iid, amount, confirmed });
+
const {
url: { pathname },
} = get(page);
@@ -66,7 +68,10 @@ export const messages = (data) => ({
pathname.includes("/receive") ||
pathname.includes("/invoice")
) {
- if (!get(navigating)) goto(`/invoice/${iid}`);
+ if (!get(navigating) && iid) {
+ const target = confirmed ? `/invoice/${iid}/paid` : `/invoice/${iid}`;
+ goto(target);
+ }
} else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`);
}
},
diff --git a/src/lib/store.ts b/src/lib/store.ts
index 57bb9735..df2c1eba 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -98,3 +98,6 @@ export const showQr = persistLocal("showQr", true);
export const save = writable();
export const importing = writable(new Set<string>());
export const paymentSignal = writable(0);
+export const lastPayment = writable<{ iid: string; amount: number; confirmed: boolean } | null>(
+ null,
+);
diff --git a/src/routes/(app)/[username]/receive/+page.svelte b/src/routes/(app)/[username]/receive/+page.svelte
index 14489c13..a0ab38a2 100644
--- a/src/routes/(app)/[username]/receive/+page.svelte
+++ b/src/routes/(app)/[username]/receive/+page.svelte
@@ -3,7 +3,7 @@
import { btc, loc, post, copy, fail, f, get, types, sat, s, sats } from "$lib/utils";
import { tick, onMount, onDestroy } from "svelte";
import { browser } from "$app/environment";
- import { last, showQr, amountPrompt } from "$lib/store";
+ import { last, lastPayment, showQr, amountPrompt } from "$lib/store";
import Avatar from "$comp/Avatar.svelte";
import InvoiceData from "$comp/InvoiceData.svelte";
import InvoiceActions from "$comp/InvoiceActions.svelte";
@@ -109,10 +109,20 @@
let txt = $derived([types.bitcoin, types.liquid].includes(type) ? hash : text);
onMount(() => {
+ $lastPayment = null;
if ($amountPrompt && !amount) toggleAmount();
let address_type = $page.url.searchParams.get("address_type");
if (address_type) setType(types.bitcoin, address_type);
});
+
+ $effect(() => {
+ if ($lastPayment?.iid) {
+ const { iid, confirmed } = $lastPayment;
+ const target = confirmed ? `/invoice/${iid}/paid` : `/invoice/${iid}`;
+ $lastPayment = null;
+ goto(target);
+ }
+ });
</script>
<div class="invoice container mx-auto max-w-xl px-4 space-y-2">
diff --git a/test/e2e/receive-lightning-redirect.spec.ts b/test/e2e/receive-lightning-redirect.spec.ts
new file mode 100644
index 00000000..d6670dca
--- /dev/null
+++ b/test/e2e/receive-lightning-redirect.spec.ts
@@ -0,0 +1,53 @@
+import { expect, test } from "@playwright/test";
+import { apiBaseUrl, bobUsername, bobPassword, login, lightningPay } from "./helpers";
+
+test("redirects from /receive to /paid when lightning address payment arrives", async ({
+ page,
+}) => {
+ test.setTimeout(90_000);
+
+ // --- Bob: log in and navigate to receive page ---
+ await login(page, bobUsername, bobPassword);
+ await page.goto(`/${bobUsername}/receive`);
+ await page.waitForLoadState("networkidle");
+ console.log(`[e2e] Bob on receive page: ${page.url()}`);
+
+ // Ensure we're on the receive page (not redirected to an invoice)
+ expect(page.url()).toContain(`/${bobUsername}/receive`);
+
+ // Small delay to ensure websocket is connected
+ await page.waitForTimeout(1000);
+
+ // --- External: create and pay LNURL invoice ---
+ // Step 1: Fetch LNURL pay request
+ const lnurlRes = await page.request.get(`${apiBaseUrl}/lnurlp/${bobUsername}`);
+ const lnurlData = await lnurlRes.json();
+ expect(lnurlData.callback).toBeTruthy();
+ console.log(`[e2e] LNURL callback: ${lnurlData.callback}`);
+
+ // Step 2: Call callback with 100 sat (100000 msat) to get bolt11
+ const callbackUrl = lnurlData.callback.replace("https://dev.coinos.io/api", apiBaseUrl);
+ const callbackRes = await page.request.get(`${callbackUrl}?amount=100000`);
+ const callbackData = await callbackRes.json();
+ const bolt11 = callbackData.pr;
+ expect(bolt11).toBeTruthy();
+ console.log(`[e2e] Got bolt11: ${bolt11.substring(0, 40)}...`);
+
+ // Step 3: Pay from external lightning node
+ console.log("[e2e] Paying from external lightning node (clb)...");
+ const payResult = lightningPay(bolt11);
+ console.log(`[e2e] Lightning pay result: ${payResult.substring(0, 100)}`);
+
+ // --- Bob: should be redirected to /paid ---
+ await page.waitForURL(/\/invoice\/[^/]+\/paid/, { timeout: 15_000 });
+ console.log(`[e2e] Bob redirected to: ${page.url()}`);
+
+ // Verify success UI is shown
+ const heading = await page.locator("h1").first().innerText({ timeout: 5_000 });
+ expect(
+ heading.toLowerCase().includes("payment") || heading.toLowerCase().includes("success"),
+ `Expected payment success/detected text, got: ${heading}`,
+ ).toBeTruthy();
+
+ if (process.env.E2E_PAUSE_AT_END === "true") await page.pause();
+});