Keep the zap dialog open while connecting a wallet (#421)

Co-authored-by: Coracle-Bot <npub1klq6260@nostr.local>
This commit is contained in:
Coracle-Bot 2026-09-03 21:57:18 +00:00 committed by hodlbod
parent bec000b05c
commit 434fc954ce
9 changed files with 267 additions and 249 deletions

View file

@ -260,6 +260,7 @@ starts from. Both take the same options, over and above the scenario's own relay
| `context` | merged over the project's context options: a viewport, a colour scheme, a permission | | `context` | merged over the project's context options: a viewport, a colour scheme, a permission |
| `env` | `VITE_` values applied over the ones derived from the scenario's relays | | `env` | `VITE_` values applied over the ones derived from the scenario's relays |
| `nip07` | a `window.nostr` backed by that identity's own signer, for an extension login | | `nip07` | a `window.nostr` backed by that identity's own signer, for an extension login |
| `webln` | a `window.webln` that answers the connection handshake, for connecting a wallet |
| `relayInfo` | fields merged over a relay's own NIP-11 document, keyed by relay url | | `relayInfo` | fields merged over a relay's own NIP-11 document, keyed by relay url |
| `hosting` | what the hosting backend already knows about this user | | `hosting` | what the hosting backend already knows about this user |
@ -293,6 +294,7 @@ e2e/
the overrides the overrides
session.ts NIP-01 session injection session.ts NIP-01 session injection
nip07.ts a window.nostr backed by a test identity's own signer nip07.ts a window.nostr backed by a test identity's own signer
webln.ts a window.webln that enables and reports what it supports
seed/ seed/
scenario.ts the `seed()` builder and relative-time helpers scenario.ts the `seed()` builder and relative-time helpers
space.ts one space's fixtures: rooms, members, messages, replies, profiles, space.ts one space's fixtures: rooms, members, messages, replies, profiles,

View file

@ -416,6 +416,19 @@ Acceptance:
quote of the shared message. quote of the shared message.
- Sending posts the quoted message in the destination, visible to bob there. - Sending posts the quoted message in the destination, visible to bob there.
### US-115 — Connect a wallet while sending a zap
As alice, I want to connect a wallet from the zap dialog and go on zapping, so
that reaching for one mid-zap is not a dead end.
Acceptance:
- Zapping a message from someone with a lightning address offers "Create
invoice" and a prompt to connect a wallet.
- Connecting one over WebLN reports success and closes only the wallet dialog.
- The zap dialog behind it drops the prompt, offers "Send Zap" instead, and
still holds the amount that was typed before the detour.
## Direct messages ## Direct messages
### US-029 — Start a one-on-one chat ### US-029 — Start a one-on-one chat
@ -1595,12 +1608,13 @@ Features the e2e suite cannot exercise, and what stops it.
**Lightning payments and wallets.** Sending a zap on a message, article, thread **Lightning payments and wallets.** Sending a zap on a message, article, thread
post, comment, or note; contributing to a funding goal; connecting a wallet over post, comment, or note; contributing to a funding goal; connecting a wallet over
WebLN or Nostr Wallet Connect; the wallet page's connection status and balance; Nostr Wallet Connect; the wallet page's connection status and balance;
disconnecting a wallet; paying and receiving invoices. The harness mocks zapper disconnecting a wallet; paying and receiving invoices. The harness mocks zapper
_discovery_ (Dufflepud's `/zapper/info`) but not the LNURL invoice callback or _discovery_ (Dufflepud's `/zapper/info`) and stands up a WebLN provider that
the payment leg, and a connected wallet needs a real extension or an NWC answers the connection handshake, but not the LNURL invoice callback, the
responder on its own relay. Existing zap receipts can be seeded, so a zap total payment leg, or an NWC responder on a relay of its own. Existing zap receipts
rendered on a message is testable; the send-and-settle flow is not. can be seeded, so a zap total rendered on a message is testable; the
send-and-settle flow is not.
**Voice and video rooms.** Creating or joining a Voice room, the mic-preview and **Voice and video rooms.** Creating or joining a Voice room, the mic-preview and
device-picker dialog, mute/camera/screen-share controls, speaking indicators, device-picker dialog, mute/camera/screen-share controls, speaking indicators,

32
e2e/harness/app/webln.ts Normal file
View file

@ -0,0 +1,32 @@
import type {BrowserContext} from "@playwright/test"
// What `getInfo` answers with. `supports` is what src/app/components/WalletConnect.svelte gates the
// connection on, and the node's alias is what the wallet page names the connection by.
export type WebLnInfo = {
supports?: string[]
node?: {alias?: string; pubkey?: string}
version?: string
}
/**
* A WebLN provider on `window`, in the shape a browser extension installs. Connecting is a
* capability handshake and nothing more, so the whole provider answers from a literal in the page —
* paying an invoice and issuing one are past the boundary this harness stops at, and calling either
* here throws rather than pretending.
*
* Install it before the page navigates. WalletConnect reads `window.webln` while it renders, to
* decide whether to offer the button at all.
*/
export const injectWebLn = (context: BrowserContext, info: WebLnInfo = {}) =>
context.addInitScript(
$info => {
Object.assign(window, {
webln: {
enable: () => Promise.resolve(),
getInfo: () => Promise.resolve($info),
getBalance: () => Promise.resolve({balance: 0}),
},
})
},
{supports: ["lightning"], ...info},
)

View file

@ -17,9 +17,11 @@ import type {BlossomOptions, HostingFixtures, RelayInfoOverrides} from "./net/ht
import {assertNoLeaks, installWebSocketRoutes} from "./net/websocket" import {assertNoLeaks, installWebSocketRoutes} from "./net/websocket"
import {boot} from "./app/boot" import {boot} from "./app/boot"
import {injectNip07} from "./app/nip07" import {injectNip07} from "./app/nip07"
import {injectWebLn} from "./app/webln"
import {seed} from "./seed/scenario" import {seed} from "./seed/scenario"
import type {Scenario, SeedTools} from "./seed/scenario" import type {Scenario, SeedTools} from "./seed/scenario"
import type {TestUser} from "./keys" import type {TestUser} from "./keys"
import type {WebLnInfo} from "./app/webln"
export {expect} export {expect}
export {makeTestUser, users} from "./keys" export {makeTestUser, users} from "./keys"
@ -38,6 +40,7 @@ export {
mockLivekit, mockLivekit,
} from "./net/http" } from "./net/http"
export type {DufflepudFixtures, HostingFixtures, HostingHandle, HostingRecord} from "./net/http" export type {DufflepudFixtures, HostingFixtures, HostingHandle, HostingRecord} from "./net/http"
export type {WebLnInfo} from "./app/webln"
// Mirrors encodeRelay in src/app/relays.ts. Importing it reaches the app's module graph, and with // Mirrors encodeRelay in src/app/relays.ts. Importing it reaches the app's module graph, and with
// it sveltekit. // it sveltekit.
@ -62,6 +65,8 @@ export type PageOptions = {
env?: Record<string, string> env?: Record<string, string>
// A NIP-07 provider signing as this user, for a login that goes through an extension. // A NIP-07 provider signing as this user, for a login that goes through an extension.
nip07?: TestUser nip07?: TestUser
// A WebLN provider on window, for a wallet that gets connected through an extension.
webln?: WebLnInfo
// A blossom server, installed before the page boots. mockBlossom called on the page `as()` // A blossom server, installed before the page boots. mockBlossom called on the page `as()`
// returns arrives after src/app/sync.ts has probed and cached a space's own url, so a spec whose // returns arrives after src/app/sync.ts has probed and cached a space's own url, so a spec whose
// server is one the app probes on load has to name it here instead. // server is one the app probes on load has to name it here instead.
@ -181,6 +186,10 @@ export const test = base.extend<HarnessFixtures, HarnessWorkerFixtures>({
await injectNip07(context, options.nip07) await injectNip07(context, options.nip07)
} }
if (options.webln) {
await injectWebLn(context, options.webln)
}
// Headless Chromium reports `Notification.permission` as "denied" even where playwright has // Headless Chromium reports `Notification.permission` as "denied" even where playwright has
// granted the permission at the browser level, so the grant is reflected into the API the app // granted the permission at the browser level, so the grant is reflected into the API the app
// reads. // reads.

View file

@ -1,7 +1,8 @@
import {DAY, HOUR, WEEK} from "@welshman/lib" import {DAY, HOUR, WEEK, bech32ToHex} from "@welshman/lib"
import {MessagingRelayList, RelayList, displayPubkey} from "@welshman/domain" import {getLnUrl} from "@welshman/util"
import {MessagingRelayList, Profile, RelayList, displayPubkey} from "@welshman/domain"
import type {Locator, Page} from "@playwright/test" import type {Locator, Page} from "@playwright/test"
import {expect, makeTestUser, roomPath, spacePath, test, users} from "../harness" import {expect, makeTestUser, mockDufflepud, roomPath, spacePath, test, users} from "../harness"
import type {SeededSpace, TestUser} from "../harness" import type {SeededSpace, TestUser} from "../harness"
// A handle to a seeded event. SeededEvent isn't exported from the harness, and only its id and // A handle to a seeded event. SeededEvent isn't exported from the harness, and only its id and
@ -751,3 +752,67 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
await expect(message(alice, "heads up")).toContainText("the dock is closed on sunday") await expect(message(alice, "heads up")).toContainText("the dock is closed on sunday")
await expect(message(bob, "heads up")).toContainText("the dock is closed on sunday") await expect(message(bob, "heads up")).toContainText("the dock is closed on sunday")
}) })
test("US-115 connect a wallet without losing the zap you were composing", async ({seed, as}) => {
// The lightning address on bob's profile, and the lnurl endpoint it resolves to. Zapping only gets
// as far as a dialog once dufflepud answers with a zapper for that endpoint.
const lud16 = "bob@zap.test"
// A zapper's receipts are signed by the recipient's lightning provider, so it is an identity of
// its own even where, as here, nothing is ever paid.
const provider = makeTestUser("zapper")
const scenario = await seed(({relay, user}) => {
const space = relay("space")
space.room("general", {name: "General"})
space.join(user.alice, "general")
space.join(user.bob, "general")
space.profile(user.alice, {name: "Alice Anchor"})
space.event(user.bob, () =>
space.kind(Profile).writer().update({name: "Bob Barnacle", lud16}).renderTemplate(),
)
space.message(user.bob, "general", "the new sail came in")
})
const {url} = scenario.space("space")
const path = roomPath(url, "general")
const page = await as(users.alice, path, {webln: {node: {alias: "Test Node"}}})
// Registered after the page was opened, so it answers ahead of the empty dufflepud `as()`
// installs, and before the navigation below, since a zapper is looked up once per page load.
await mockDufflepud(page.context(), {
zappers: [
{
lnurl: bech32ToHex(getLnUrl(lud16)!),
info: {pubkey: users.bob.pubkey, nostrPubkey: provider.pubkey, allowsNostr: true},
},
],
})
await page.goto(path)
await expect(message(page, "the new sail came in")).toBeVisible()
await messageActions(page, "the new sail came in").first().click()
const zap = dialog(page, "Send a Zap")
const amount = zap.locator('input[type="number"]')
await expect(zap.getByRole("button", {name: "Create invoice"})).toBeVisible()
await amount.fill("210")
await zap.getByRole("button", {name: "Connect a lightning wallet"}).click()
const connect = dialog(page, "Connect a Wallet")
await connect.getByRole("button", {name: "Connect with WebLN"}).click()
await expect(page.getByRole("alert")).toContainText("Wallet successfully connected!")
await expect(connect).toHaveCount(0)
// The zap dialog was underneath rather than replaced, so it answers to the wallet she now has
// instead of still asking for one, and the amount she had typed survived the detour.
await expect(zap.getByRole("button", {name: "Connect a lightning wallet"})).toHaveCount(0)
await expect(zap.getByRole("button", {name: "Send Zap"})).toBeVisible()
await expect(amount).toHaveValue("210")
})

View file

@ -1,9 +1,11 @@
<script lang="ts"> <script lang="ts">
import {onDestroy} from "svelte"
import {first, removeUndefined, uniq} from "@welshman/lib" import {first, removeUndefined, uniq} from "@welshman/lib"
import {inbox} from "@welshman/util" import {inbox} from "@welshman/util"
import {ZapRequest} from "@welshman/domain" import {ZapRequest} from "@welshman/domain"
import {Zappers} from "@welshman/app" import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@ -16,11 +18,14 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
import QRCode from "@app/components/QRCode.svelte"
import WalletConnect from "@app/components/WalletConnect.svelte"
import ZapForm from "@app/components/ZapForm.svelte" import ZapForm from "@app/components/ZapForm.svelte"
import {payInvoice} from "@app/lightning"
import {zapAmounts} from "@app/settings"
import {pushToast} from "@app/toast"
import {app, domain, network, router} from "@app/core" import {app, domain, network, router} from "@app/core"
import {payInvoice, wallet} from "@app/lightning"
import {pushModal} from "@app/modal"
import {zapAmounts} from "@app/settings"
import {clip, pushToast} from "@app/toast"
type Props = { type Props = {
url?: string url?: string
@ -36,61 +41,107 @@
const back = () => history.back() const back = () => history.back()
const requestInvoice = async () => {
const currentZapper = zapper.get()!
const relays = uniq([
...(url ? [url] : await $router.resolver.relays([inbox(pubkey)])),
...goalRelays,
])
const writer = $domain
.writer(ZapRequest)
.setContent(content)
.setAmount(amount * 1000)
.setLnurl(currentZapper.lnurl)
.setRecipient(pubkey)
.setUrls(relays)
if (eventId) {
writer.setEventId(eventId)
}
const res = await writer.requestInvoice(currentZapper)
if (!res.invoice) {
throw new Error(res.error || "no error given")
}
return {
relays,
invoice: res.invoice,
filters: [currentZapper.getResponseFilter(pubkey, eventId)],
}
}
const payWithWallet = async () => {
const {relays, invoice, filters} = await requestInvoice()
await payInvoice(invoice)
await $network.load({relays, filters})
pushToast({message: "Zap successfully sent!"})
back()
}
const createInvoice = async () => {
const {relays, invoice: created, filters} = await requestInvoice()
invoice = created
paymentController?.abort()
paymentController = new AbortController()
$network.request({
relays,
filters,
signal: paymentController.signal,
onEvent: () => {
pushToast({message: "Payment sent!"})
paymentController?.abort()
back()
},
})
}
const sendZap = async () => { const sendZap = async () => {
loading = true loading = true
try { try {
const currentZapper = zapper.get()! if ($wallet) {
const relays = uniq([ await payWithWallet()
...(url ? [url] : await $router.resolver.relays([inbox(pubkey)])), } else {
...goalRelays, await createInvoice()
])
const writer = $domain
.writer(ZapRequest)
.setContent(content)
.setAmount(amount * 1000)
.setLnurl(currentZapper.lnurl)
.setRecipient(pubkey)
.setUrls(relays)
if (eventId) {
writer.setEventId(eventId)
} }
const res = await writer.requestInvoice(currentZapper)
if (!res.invoice) {
return pushToast({
theme: "error",
message: `Failed to zap: ${res.error || "no error given"}`,
})
}
await payInvoice(res.invoice)
await $network.load({
relays,
filters: [currentZapper.getResponseFilter(pubkey, eventId)],
})
pushToast({message: "Zap successfully sent!"})
back()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
const message = errorMessage(e)
pushToast({ pushToast({
theme: "error", theme: "error",
message: `Failed to zap: ${message}`, message: `Failed to zap: ${errorMessage(e)}`,
}) })
} finally { } finally {
loading = false loading = false
} }
} }
const connectWallet = () => {
pushModal(WalletConnect, {}, {nested: true})
}
const copyInvoice = () => {
if (invoice) {
clip(invoice)
}
}
let amount = $state<number>(first($zapAmounts) ?? 21) let amount = $state<number>(first($zapAmounts) ?? 21)
let content = $state("⚡️") let content = $state("⚡️")
let loading = $state(false) let loading = $state(false)
let invoice = $state<string>()
let paymentController: AbortController | undefined = $state()
onDestroy(() => {
paymentController?.abort()
})
</script> </script>
<Modal> <Modal>
@ -99,22 +150,55 @@
<ModalTitle>Send a Zap</ModalTitle> <ModalTitle>Send a Zap</ModalTitle>
<ModalSubtitle>To <ProfileLink {pubkey} class="text-primary!" /></ModalSubtitle> <ModalSubtitle>To <ProfileLink {pubkey} class="text-primary!" /></ModalSubtitle>
</ModalHeader> </ModalHeader>
<ZapForm bind:amount bind:content />
{#if invoice}
<div class="flex flex-col gap-6">
<div class="flex flex-col items-center gap-4">
<QRCode code={invoice} class="w-full max-w-56" />
<p class="text-content-muted text-center text-sm">
Scan with your lightning wallet, or copy the invoice below.
</p>
</div>
<label class="input flex w-full items-center gap-2">
<input readonly class="min-w-0 grow truncate" value={invoice} />
<Button
class="button button-neutral button-sm button-square shrink-0"
onclick={copyInvoice}>
<Icon icon={Copy} size={4} />
</Button>
</label>
</div>
{:else}
<ZapForm bind:amount bind:content>
{#if !$wallet}
<div class="card card-sm card-flat flex flex-col items-center gap-3 p-4 text-center">
<p class="text-content-muted text-sm">
Connect a wallet to pay instantly without scanning a QR code.
</p>
<Button class="button button-neutral" onclick={connectWallet}>
Connect a lightning wallet
</Button>
</div>
{/if}
</ZapForm>
{/if}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="button button-link" onclick={back}> <Button class="button button-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button class="button button-primary" onclick={sendZap} disabled={loading}> {#if !invoice}
<Spinner {loading}> <Button class="button button-primary" onclick={sendZap} disabled={loading}>
<div class="flex items-center gap-2"> <Spinner {loading}>
{#if !loading} <div class="flex items-center gap-2">
<Icon icon={Bolt} /> {#if !loading}
{/if} <Icon icon={Bolt} />
Send Zap {/if}
</div> {$wallet ? "Send Zap" : "Create invoice"}
</Spinner> </div>
</Button> </Spinner>
</Button>
{/if}
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View file

@ -7,11 +7,9 @@
import {Zappers} from "@welshman/app" import {Zappers} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Zap from "@app/components/Zap.svelte" import Zap from "@app/components/Zap.svelte"
import ZapInvoice from "@app/components/ZapInvoice.svelte"
import InfoZapperError from "@app/components/InfoZapperError.svelte" import InfoZapperError from "@app/components/InfoZapperError.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {app, reader} from "@app/core" import {app, reader} from "@app/core"
import {wallet} from "@app/lightning"
type Props = { type Props = {
url?: string url?: string
@ -37,16 +35,10 @@
try { try {
const zapper = await zapperPromise const zapper = await zapperPromise
if (!zapper?.allowsNostr) { if (zapper?.allowsNostr) {
pushModal(InfoZapperError, {url, pubkey: event.pubkey, eventId: event.id}, {replaceState})
} else if ($wallet) {
pushModal(Zap, {url, pubkey: event.pubkey, eventId: event.id, goalRelays}, {replaceState}) pushModal(Zap, {url, pubkey: event.pubkey, eventId: event.id, goalRelays}, {replaceState})
} else { } else {
pushModal( pushModal(InfoZapperError, {url, pubkey: event.pubkey, eventId: event.id}, {replaceState})
ZapInvoice,
{url, pubkey: event.pubkey, eventId: event.id, goalRelays},
{replaceState},
)
} }
} finally { } finally {
loading = false loading = false

View file

@ -1,178 +0,0 @@
<script lang="ts">
import {onDestroy} from "svelte"
import {first, removeUndefined, uniq} from "@welshman/lib"
import {inbox} from "@welshman/util"
import {ZapRequest} from "@welshman/domain"
import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {errorMessage} from "@lib/util"
import ProfileLink from "@app/components/ProfileLink.svelte"
import QRCode from "@app/components/QRCode.svelte"
import WalletConnect from "@app/components/WalletConnect.svelte"
import ZapForm from "@app/components/ZapForm.svelte"
import {app, domain, network, router} from "@app/core"
import {pushModal} from "@app/modal"
import {zapAmounts} from "@app/settings"
import {clip, pushToast} from "@app/toast"
type Props = {
url?: string
pubkey: string
eventId?: string
// NIP-75 requires a zap to a goal to request its receipt on the goal's own relays.
goalRelays?: string[]
}
const {url, pubkey, eventId, goalRelays = []}: Props = $props()
const zapper = $app.use(Zappers).forPubkey(pubkey, removeUndefined([url]))
const back = () => history.back()
const createInvoice = async () => {
loading = true
try {
const currentZapper = zapper.get()!
const relays = uniq([
...(url ? [url] : await $router.resolver.relays([inbox(pubkey)])),
...goalRelays,
])
const writer = $domain
.writer(ZapRequest)
.setContent(content)
.setAmount(amount * 1000)
.setLnurl(currentZapper.lnurl)
.setRecipient(pubkey)
.setUrls(relays)
if (eventId) {
writer.setEventId(eventId)
}
const res = await writer.requestInvoice(currentZapper)
if (!res.invoice) {
return pushToast({
theme: "error",
message: `Failed to create zap invoice: ${res.error || "no error given"}`,
})
}
invoice = res.invoice
paymentController?.abort()
paymentController = new AbortController()
$network.request({
relays,
signal: paymentController.signal,
filters: [currentZapper.getResponseFilter(pubkey, eventId)],
onEvent: () => {
pushToast({message: "Payment sent!"})
paymentController?.abort()
back()
},
})
} catch (e) {
console.error(e)
const message = errorMessage(e)
pushToast({
theme: "error",
message: `Failed to create zap invoice: ${message}`,
})
} finally {
loading = false
}
}
const connectWallet = () => {
pushModal(WalletConnect)
}
const copyInvoice = () => {
if (invoice) {
clip(invoice)
}
}
let amount = $state<number>(first($zapAmounts) ?? 21)
let content = $state("⚡️")
let loading = $state(false)
let invoice = $state<string>()
let paymentController: AbortController | undefined = $state()
onDestroy(() => {
paymentController?.abort()
})
</script>
<Modal>
<ModalBody>
<ModalHeader>
<ModalTitle>Send a Zap</ModalTitle>
<ModalSubtitle>To <ProfileLink {pubkey} class="text-primary!" /></ModalSubtitle>
</ModalHeader>
{#if invoice}
<div class="flex flex-col gap-6">
<div class="flex flex-col items-center gap-4">
<QRCode code={invoice} class="w-full max-w-56" />
<p class="text-content-muted text-center text-sm">
Scan with your lightning wallet, or copy the invoice below.
</p>
</div>
<label class="input flex w-full items-center gap-2">
<input readonly class="min-w-0 grow truncate" value={invoice} />
<Button
class="button button-neutral button-sm button-square shrink-0"
onclick={copyInvoice}>
<Icon icon={Copy} size={4} />
</Button>
</label>
</div>
{:else}
<ZapForm bind:amount bind:content>
<div class="card card-sm card-flat flex flex-col items-center gap-3 p-4 text-center">
<p class="text-content-muted text-sm">
Connect a wallet to pay instantly without scanning a QR code.
</p>
<Button class="button button-neutral" onclick={connectWallet}>
Connect a lightning wallet
</Button>
</div>
</ZapForm>
{/if}
</ModalBody>
<ModalFooter>
<Button class="button button-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
{#if !invoice}
<Button class="button button-primary" onclick={createInvoice} disabled={loading}>
<Spinner {loading}>
<div class="flex items-center gap-2">
{#if !loading}
<Icon icon={Bolt} />
{/if}
Create invoice
</div>
</Spinner>
</Button>
{/if}
</ModalFooter>
</Modal>

View file

@ -9,10 +9,8 @@
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import HeadphonesRound from "@assets/icons/headphones-round.svg?dataurl" import HeadphonesRound from "@assets/icons/headphones-round.svg?dataurl"
import Zap from "@app/components/Zap.svelte" import Zap from "@app/components/Zap.svelte"
import ZapInvoice from "@app/components/ZapInvoice.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {PLATFORM_NAME, PLATFORM_ABOUT} from "@app/env" import {PLATFORM_NAME, PLATFORM_ABOUT} from "@app/env"
import {wallet} from "@app/lightning"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
@ -22,7 +20,7 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey}) const openProfile = () => pushModal(ProfileDetail, {pubkey})
const zap = () => pushModal($wallet ? Zap : ZapInvoice, {pubkey}) const zap = () => pushModal(Zap, {pubkey})
</script> </script>
<div class="flex flex-col gap-6 p-8 max-w-2xl h-screen justify-center m-auto"> <div class="flex flex-col gap-6 p-8 max-w-2xl h-screen justify-center m-auto">