forked from coracle/flotilla
* Pin sharp via pnpm override, add wallet receive * Revert toast success styling * Route receive through wallet connect * Simplify receive invoice validation * Polish receive modal layout * Clarify NWC client config * Adjust wallet action layout on mobile
This commit is contained in:
+4
-1
@@ -92,6 +92,9 @@
|
|||||||
],
|
],
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"sharp"
|
"sharp"
|
||||||
]
|
],
|
||||||
|
"overrides": {
|
||||||
|
"sharp": "0.35.0-rc.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+277
-385
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
|||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {errorMessage} from "@lib/util"
|
||||||
import {payInvoice} from "@app/core/commands"
|
import {payInvoice} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
@@ -29,14 +30,18 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await payInvoice(invoice!.paymentRequest, sats * 1000)
|
if (hasAmount) {
|
||||||
|
await payInvoice(invoice!.paymentRequest)
|
||||||
|
} else {
|
||||||
|
await payInvoice(invoice!.paymentRequest, sats * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
pushToast({message: `Payment sent!`})
|
pushToast({message: `Payment sent!`})
|
||||||
clearModals()
|
clearModals()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
||||||
const message = String(e).replace(/^.*Error: /, "")
|
const message = errorMessage(e)
|
||||||
|
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
@@ -50,6 +55,7 @@
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let invoice: Invoice | undefined = $state()
|
let invoice: Invoice | undefined = $state()
|
||||||
let sats = $state(0)
|
let sats = $state(0)
|
||||||
|
const hasAmount = $derived((invoice?.satoshi ?? 0) > 0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
@@ -60,7 +66,7 @@
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{#if invoice}
|
{#if invoice}
|
||||||
<div class="card2 bg-alt flex flex-col gap-2">
|
<div class="card2 bg-alt flex flex-col gap-2">
|
||||||
{#if $session?.wallet?.type === "webln" && invoice.satoshi === 0}
|
{#if $session?.wallet?.type === "webln" && !hasAmount}
|
||||||
<p class="text-sm opacity-75">
|
<p class="text-sm opacity-75">
|
||||||
Uh oh! It looks like your current wallet doesn't support invoices without an amount. See
|
Uh oh! It looks like your current wallet doesn't support invoices without an amount. See
|
||||||
if you can get a lightning invoice with a pre-set amount.
|
if you can get a lightning invoice with a pre-set amount.
|
||||||
@@ -101,7 +107,13 @@
|
|||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-primary" onclick={confirm} disabled={!invoice || sats === 0 || loading}>
|
<Button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick={confirm}
|
||||||
|
disabled={!invoice ||
|
||||||
|
sats === 0 ||
|
||||||
|
loading ||
|
||||||
|
($session?.wallet?.type === "webln" && !hasAmount)}>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Invoice} from "@getalby/lightning-tools/bolt11"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.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 QRCode from "@app/components/QRCode.svelte"
|
||||||
|
import {createInvoice} from "@app/core/commands"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
const satAmount = Math.floor(Number(sats))
|
||||||
|
|
||||||
|
if (!Number.isFinite(satAmount) || satAmount <= 0) {
|
||||||
|
pushToast({theme: "error", message: "Enter an amount greater than 0."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const paymentRequest = await createInvoice({sats: satAmount})
|
||||||
|
|
||||||
|
invoice = new Invoice({pr: paymentRequest})
|
||||||
|
sats = invoice.satoshi && invoice.satoshi > 0 ? invoice.satoshi : satAmount
|
||||||
|
|
||||||
|
pushToast({message: "Invoice created"})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
|
||||||
|
const message = errorMessage(e)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to create invoice: ${message}`,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
invoice = undefined
|
||||||
|
sats = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let invoice: Invoice | undefined = $state()
|
||||||
|
let sats = $state(10)
|
||||||
|
|
||||||
|
const invoiceHasAmount = $derived((invoice?.satoshi ?? 0) > 0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Receive with Lightning</ModalTitle>
|
||||||
|
<ModalSubtitle>Use your wallet to receive Bitcoin payments over lightning.</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
{#if invoice}
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col items-center gap-6 pt-4">
|
||||||
|
<QRCode code={invoice.paymentRequest} class="w-full max-w-64" />
|
||||||
|
<p class="text-center text-sm opacity-75">Scan with your wallet, or click to copy.</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm opacity-75">
|
||||||
|
Share this invoice to receive <strong>{sats}</strong> satoshis
|
||||||
|
{invoiceHasAmount ? "" : " (payer chooses the exact amount)"}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-2">
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
Amount (satoshis)
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex flex-grow justify-end">
|
||||||
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<Icon icon={Bolt} />
|
||||||
|
<input bind:value={sats} type="number" class="w-14" placeholder="0" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<p class="text-sm opacity-75">Enter an amount to generate an invoice with your wallet.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
{#if invoice}
|
||||||
|
<Button class="btn btn-neutral" onclick={reset} disabled={loading}>Clear Invoice</Button>
|
||||||
|
{:else}
|
||||||
|
<Button class="btn btn-primary" onclick={create} disabled={loading}>
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Bolt} />
|
||||||
|
{/if}
|
||||||
|
Create Invoice
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
|
import {errorMessage} from "@lib/util"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
import {payInvoice} from "@app/core/commands"
|
import {payInvoice} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
||||||
const message = String(e).replace(/^.*Error: /, "")
|
const message = errorMessage(e)
|
||||||
|
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
|
|||||||
@@ -425,6 +425,22 @@ export const publishLeaveRequest = (params: LeaveRequestParams) =>
|
|||||||
|
|
||||||
export const getWebLn = () => (window as any).webln
|
export const getWebLn = () => (window as any).webln
|
||||||
|
|
||||||
|
export const getNwcClient = () => {
|
||||||
|
const $session = session.get()
|
||||||
|
|
||||||
|
if (!$session?.wallet || $session.wallet.type !== "nwc") {
|
||||||
|
throw new Error("No NWC wallet is connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
const {info} = $session.wallet
|
||||||
|
|
||||||
|
if (info.nostrWalletConnectUrl) {
|
||||||
|
return new nwc.NWCClient({nostrWalletConnectUrl: info.nostrWalletConnectUrl})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new nwc.NWCClient(info)
|
||||||
|
}
|
||||||
|
|
||||||
export const payInvoice = async (invoice: string, msats?: number) => {
|
export const payInvoice = async (invoice: string, msats?: number) => {
|
||||||
const $session = session.get()
|
const $session = session.get()
|
||||||
|
|
||||||
@@ -435,7 +451,7 @@ export const payInvoice = async (invoice: string, msats?: number) => {
|
|||||||
if ($session.wallet.type === "nwc") {
|
if ($session.wallet.type === "nwc") {
|
||||||
const params: {invoice: string; amount?: number} = {invoice}
|
const params: {invoice: string; amount?: number} = {invoice}
|
||||||
if (msats) params.amount = msats
|
if (msats) params.amount = msats
|
||||||
return new nwc.NWCClient($session.wallet.info).payInvoice(params)
|
return getNwcClient().payInvoice(params)
|
||||||
} else if ($session.wallet.type === "webln") {
|
} else if ($session.wallet.type === "webln") {
|
||||||
if (msats) throw new Error("Unable to pay zero invoices with webln")
|
if (msats) throw new Error("Unable to pay zero invoices with webln")
|
||||||
return getWebLn()
|
return getWebLn()
|
||||||
@@ -444,6 +460,67 @@ export const payInvoice = async (invoice: string, msats?: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateInvoiceParams = {
|
||||||
|
sats: number
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createInvoice = async ({
|
||||||
|
sats,
|
||||||
|
description = "Receive via lightning",
|
||||||
|
}: CreateInvoiceParams) => {
|
||||||
|
const $session = session.get()
|
||||||
|
|
||||||
|
if (!$session?.wallet) {
|
||||||
|
throw new Error("No wallet is connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
const satAmount = Math.floor(sats)
|
||||||
|
|
||||||
|
if (!Number.isFinite(satAmount) || satAmount <= 0) {
|
||||||
|
throw new Error("Invalid satoshi amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($session.wallet.type === "nwc") {
|
||||||
|
const createdInvoice = await getNwcClient().makeInvoice({
|
||||||
|
amount: satAmount * 1000,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!createdInvoice.invoice) {
|
||||||
|
throw new Error("NWC wallet failed to return an invoice")
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdInvoice.invoice
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($session.wallet.type === "webln") {
|
||||||
|
const webLn = getWebLn()
|
||||||
|
|
||||||
|
if (!webLn) {
|
||||||
|
throw new Error("WebLN not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
await webLn.enable()
|
||||||
|
|
||||||
|
const response = await webLn.makeInvoice({
|
||||||
|
amount: satAmount,
|
||||||
|
defaultMemo: description,
|
||||||
|
})
|
||||||
|
|
||||||
|
const paymentRequest =
|
||||||
|
typeof response === "string" ? response : response?.paymentRequest || response?.pr || ""
|
||||||
|
|
||||||
|
if (!paymentRequest) {
|
||||||
|
throw new Error("Invalid payment request returned from WebLN")
|
||||||
|
}
|
||||||
|
|
||||||
|
return paymentRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unsupported wallet type")
|
||||||
|
}
|
||||||
|
|
||||||
// File upload
|
// File upload
|
||||||
|
|
||||||
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const daysBetween = (start: number, end: number) => [...range(start, end,
|
|||||||
|
|
||||||
export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
|
export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
|
||||||
|
|
||||||
|
export const errorMessage = (err: unknown) => String(err).replace(/^.*Error: /, "")
|
||||||
|
|
||||||
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
||||||
const url = new URL(base)
|
const url = new URL(base)
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
import {LOCALE} from "@welshman/lib"
|
import {LOCALE} from "@welshman/lib"
|
||||||
import {displayRelayUrl, isNWCWallet, fromMsats} from "@welshman/util"
|
import {displayRelayUrl, isNWCWallet, fromMsats} from "@welshman/util"
|
||||||
import {session, pubkey, profilesByPubkey} from "@welshman/app"
|
import {session, pubkey, profilesByPubkey} from "@welshman/app"
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import DownloadMinimalistic from "@assets/icons/download-minimalistic.svg?dataurl"
|
||||||
|
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import WalletPay from "@app/components/WalletPay.svelte"
|
import WalletPay from "@app/components/WalletPay.svelte"
|
||||||
|
import WalletReceive from "@app/components/WalletReceive.svelte"
|
||||||
import WalletConnect from "@app/components/WalletConnect.svelte"
|
import WalletConnect from "@app/components/WalletConnect.svelte"
|
||||||
import WalletDisconnect from "@app/components/WalletDisconnect.svelte"
|
import WalletDisconnect from "@app/components/WalletDisconnect.svelte"
|
||||||
import WalletUpdateReceivingAddress from "@app/components/WalletUpdateReceivingAddress.svelte"
|
import WalletUpdateReceivingAddress from "@app/components/WalletUpdateReceivingAddress.svelte"
|
||||||
@@ -31,6 +33,13 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const pay = () => pushModal(WalletPay)
|
const pay = () => pushModal(WalletPay)
|
||||||
|
const receive = () => {
|
||||||
|
if ($session?.wallet) {
|
||||||
|
pushModal(WalletReceive)
|
||||||
|
} else {
|
||||||
|
pushModal(WalletConnect)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="content column gap-4">
|
<div class="content column gap-4">
|
||||||
@@ -94,10 +103,24 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Button class="btn btn-neutral btn-sm" onclick={disconnect}>
|
<div class="flex flex-col gap-4 lg:flex-row lg:justify-between">
|
||||||
<Icon icon={CloseCircle} />
|
<Button class="btn btn-neutral btn-sm" onclick={disconnect}>
|
||||||
Disconnect Wallet
|
<Icon icon={CloseCircle} />
|
||||||
</Button>
|
Disconnect Wallet
|
||||||
|
</Button>
|
||||||
|
<div class="flex w-full gap-4 lg:w-auto">
|
||||||
|
<Button class="btn btn-primary btn-sm flex-1 justify-center lg:flex-none" onclick={pay}>
|
||||||
|
<Icon icon={UploadMinimalistic} />
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="btn btn-secondary btn-sm flex-1 justify-center lg:flex-none"
|
||||||
|
onclick={receive}>
|
||||||
|
<Icon icon={DownloadMinimalistic} />
|
||||||
|
Receive
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="py-12 text-center opacity-75">No wallet connected</p>
|
<p class="py-12 text-center opacity-75">No wallet connected</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -122,10 +145,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center py-12">
|
|
||||||
<Button class="btn btn-primary" onclick={pay}>
|
|
||||||
<Icon icon={Bolt} />
|
|
||||||
Pay With Lightning
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user