Refactor ReceiveDialog.vue for Lightning invoice creation

- Updated the form to create Lightning invoices instead of LNURL addresses, changing the validation schema and input fields accordingly.
- Introduced new state management for created invoices and adjusted the submission logic to handle invoice creation.
- Enhanced the UI to display invoice details, including amount, memo, and QR code generation for the invoice.
- Removed unused components and streamlined the dialog's functionality for a more focused user experience.

These changes improve the functionality and user interface of the ReceiveDialog component, facilitating easier invoice management for Bitcoin payments.
This commit is contained in:
padreug 2025-09-18 21:44:24 +02:00
parent 27070c0390
commit 21e1c8f7c0
3 changed files with 280 additions and 318 deletions

View file

@ -3,20 +3,15 @@ import { ref, computed } from 'vue'
import { useForm } from 'vee-validate' import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod' import * as z from 'zod'
import { nip19 } from 'nostr-tools'
import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator' import { QrCode, Copy, Check, Loader2 } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { QrCode, Copy, Check, RefreshCw, Trash2, Plus } from 'lucide-vue-next'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import type { PayLink } from '../services/WalletService' import type { Invoice } from '../services/WalletService'
interface Props { interface Props {
open: boolean open: boolean
@ -36,22 +31,16 @@ const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) as any
// Form validation schema // Form validation schema
const formSchema = toTypedSchema(z.object({ const formSchema = toTypedSchema(z.object({
description: z.string().min(1, "Description is required").max(200, "Description too long"), amount: z.number().min(1, "Amount must be at least 1 sat").max(10000000, "Amount too large"),
minAmount: z.number().min(1, "Minimum amount must be at least 1 sat"), memo: z.string().max(640, "Description too long").optional()
maxAmount: z.number().min(1, "Maximum amount must be at least 1 sat"),
username: z.string().optional(),
allowComments: z.boolean().default(false)
})) }))
// Form setup // Form setup
const form = useForm({ const form = useForm({
validationSchema: formSchema, validationSchema: formSchema,
initialValues: { initialValues: {
description: '', amount: undefined,
minAmount: 1, memo: ''
maxAmount: 100000,
username: '',
allowComments: false
} }
}) })
@ -59,36 +48,32 @@ const { resetForm, meta } = form
const isFormValid = computed(() => meta.value.valid) const isFormValid = computed(() => meta.value.valid)
// State // State
const activeTab = ref('create') const createdInvoice = ref<Invoice | null>(null)
const selectedPayLink = ref<PayLink | null>(null)
const copiedField = ref<string | null>(null) const copiedField = ref<string | null>(null)
const qrCode = ref<string | null>(null) const qrCode = ref<string | null>(null)
const isLoadingQR = ref(false) const isLoadingQR = ref(false)
// Computed // Computed
const existingPayLinks = computed(() => walletService?.payLinks?.value || []) const isCreating = computed(() => walletService?.isCreatingInvoice?.value || false)
const isCreating = computed(() => walletService?.isCreatingPayLink?.value || false)
const error = computed(() => walletService?.error?.value) const error = computed(() => walletService?.error?.value)
// Methods // Methods
const onSubmit = form.handleSubmit(async (formValues) => { const onSubmit = form.handleSubmit(async (formValues) => {
try { try {
const payLink = await walletService.createReceiveAddress({ const invoice = await walletService.createInvoice({
description: formValues.description, amount: formValues.amount,
minAmount: formValues.minAmount, memo: formValues.memo || 'Payment request'
maxAmount: formValues.maxAmount, // Let server use default expiry
username: formValues.username || undefined,
allowComments: formValues.allowComments
}) })
if (payLink) { if (invoice) {
selectedPayLink.value = payLink createdInvoice.value = invoice
activeTab.value = 'existing' console.log(createdInvoice.value)
resetForm() await generateQRCode(invoice.payment_request)
toastService?.success('Receive address created successfully!') toastService?.success('Invoice created successfully!')
} }
} catch (error) { } catch (error) {
console.error('Failed to create receive address:', error) console.error('Failed to create invoice:', error)
} }
}) })
@ -97,7 +82,7 @@ async function copyToClipboard(text: string, field: string) {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
copiedField.value = field copiedField.value = field
toastService?.success('Copied to clipboard!') toastService?.success('Copied to clipboard!')
setTimeout(() => { setTimeout(() => {
copiedField.value = null copiedField.value = null
}, 2000) }, 2000)
@ -107,29 +92,13 @@ async function copyToClipboard(text: string, field: string) {
} }
} }
function encodeLNURL(url: string): string { async function generateQRCode(paymentRequest: string) {
try { if (!paymentRequest) return
// Convert URL to bytes
const bytes = new TextEncoder().encode(url)
// Encode as bech32 with 'lnurl' prefix
const bech32 = nip19.encodeBytes('lnurl', bytes)
// Return with lightning: prefix in uppercase
return `lightning:${bech32.toUpperCase()}`
} catch (error) {
console.error('Failed to encode LNURL:', error)
return url // Fallback to original URL
}
}
async function generateQRCode(data: string) {
if (!data) return
isLoadingQR.value = true isLoadingQR.value = true
try { try {
// Encode LNURL with proper bech32 format and lightning: prefix
const encodedLNURL = encodeLNURL(data)
// Use the existing PaymentService QR code generation // Use the existing PaymentService QR code generation
qrCode.value = await paymentService?.generateQRCode(encodedLNURL) qrCode.value = await paymentService?.generateQRCode(paymentRequest)
} catch (error) { } catch (error) {
console.error('Failed to generate QR code:', error) console.error('Failed to generate QR code:', error)
toastService?.error('Failed to generate QR code') toastService?.error('Failed to generate QR code')
@ -138,41 +107,34 @@ async function generateQRCode(data: string) {
} }
} }
async function deletePayLink(link: PayLink) {
if (await walletService.deletePayLink(link.id)) {
toastService?.success('Receive address deleted')
if (selectedPayLink.value?.id === link.id) {
selectedPayLink.value = null
}
}
}
async function selectPayLink(link: PayLink) {
selectedPayLink.value = link
// Generate QR code when selecting a pay link
if (link.lnurl) {
await generateQRCode(link.lnurl)
}
}
function closeDialog() { function closeDialog() {
emit('update:open', false) emit('update:open', false)
resetForm() resetForm()
selectedPayLink.value = null createdInvoice.value = null
activeTab.value = 'create' qrCode.value = null
}
function createAnother() {
resetForm()
createdInvoice.value = null
qrCode.value = null
} }
// Handle dialog open/close state changes // Handle dialog open/close state changes
function onOpenChange(open: boolean) { function onOpenChange(open: boolean) {
if (open && walletService) { if (!open) {
walletService.refresh() closeDialog()
} else if (!open) { }
// Dialog is being closed (including X button) - just emit the update }
emit('update:open', false)
// Clean up state // Format expiry time for display
resetForm() function formatExpiry(seconds: number): string {
selectedPayLink.value = null if (seconds < 3600) {
activeTab.value = 'create' return `${Math.round(seconds / 60)} minutes`
} else if (seconds < 86400) {
return `${Math.round(seconds / 3600)} hours`
} else {
return `${Math.round(seconds / 86400)} days`
} }
} }
</script> </script>
@ -186,245 +148,162 @@ function onOpenChange(open: boolean) {
Receive Bitcoin Receive Bitcoin
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
Create LNURL addresses and Lightning addresses to receive Bitcoin payments. Create a Lightning invoice to receive Bitcoin payments.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Tabs v-model="activeTab" class="w-full"> <!-- Invoice Creation Form -->
<TabsList class="grid w-full grid-cols-2"> <div v-if="!createdInvoice" class="space-y-4">
<TabsTrigger value="create">Create New</TabsTrigger> <form @submit="onSubmit" class="space-y-4">
<TabsTrigger value="existing">Existing Addresses</TabsTrigger> <FormField v-slot="{ componentField }" name="amount">
</TabsList> <FormItem>
<FormLabel>Amount (sats) *</FormLabel>
<FormControl>
<Input
type="number"
min="1"
max="10000000"
placeholder="Enter amount in sats"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Amount to request in satoshis</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<TabsContent value="create" class="space-y-4"> <FormField v-slot="{ componentField }" name="memo">
<form @submit="onSubmit" class="space-y-4"> <FormItem>
<FormField v-slot="{ componentField }" name="description"> <FormLabel>Description</FormLabel>
<FormItem> <FormControl>
<FormLabel>Description *</FormLabel> <Textarea
<FormControl> placeholder="e.g., Payment for services (optional)"
<Textarea v-bind="componentField"
placeholder="e.g., Tips for my work" />
v-bind="componentField" </FormControl>
/> <FormDescription>What is this payment for? (optional)</FormDescription>
</FormControl> <FormMessage />
<FormDescription>What are you receiving payments for?</FormDescription> </FormItem>
<FormMessage /> </FormField>
</FormItem>
</FormField>
<div class="grid grid-cols-2 gap-4"> <div v-if="error" class="text-destructive text-sm">
<FormField v-slot="{ componentField }" name="minAmount"> {{ error }}
<FormItem> </div>
<FormLabel>Min Amount (sats) *</FormLabel>
<FormControl>
<Input
type="number"
min="1"
placeholder="1"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="maxAmount"> <div class="flex gap-2 pt-4">
<FormItem> <Button
<FormLabel>Max Amount (sats) *</FormLabel> type="submit"
<FormControl> :disabled="!isFormValid || isCreating"
<Input class="flex-1"
type="number" >
min="1" <Loader2 v-if="isCreating" class="w-4 h-4 mr-2 animate-spin" />
placeholder="100000" {{ isCreating ? 'Creating Invoice...' : 'Create Invoice' }}
v-bind="componentField" </Button>
/> <Button type="button" variant="outline" @click="closeDialog">
</FormControl> Cancel
<FormMessage />
</FormItem>
</FormField>
</div>
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>Lightning Address Username (Optional)</FormLabel>
<FormControl>
<Input
placeholder="e.g., satoshi"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Creates a Lightning address like: satoshi@yourdomain.com</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="allowComments" type="checkbox">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-3">
<div class="space-y-0.5">
<FormLabel>Allow Comments</FormLabel>
<FormDescription>Let senders include a message with their payment</FormDescription>
</div>
<FormControl>
<Switch :checked="value" @update:checked="handleChange" />
</FormControl>
</FormItem>
</FormField>
<div v-if="error" class="text-destructive text-sm">
{{ error }}
</div>
<div class="flex gap-2 pt-4">
<Button
type="submit"
:disabled="!isFormValid || isCreating"
class="flex-1"
>
<RefreshCw v-if="isCreating" class="w-4 h-4 mr-2 animate-spin" />
<Plus v-else class="w-4 h-4 mr-2" />
{{ isCreating ? 'Creating...' : 'Create Receive Address' }}
</Button>
<Button type="button" variant="outline" @click="closeDialog">
Cancel
</Button>
</div>
</form>
</TabsContent>
<TabsContent value="existing" class="space-y-4">
<div v-if="existingPayLinks.length === 0" class="text-center py-8 text-muted-foreground">
<QrCode class="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No receive addresses created yet</p>
<Button variant="outline" @click="activeTab = 'create'" class="mt-4">
Create Your First Address
</Button> </Button>
</div> </div>
</form>
</div>
<div v-else class="space-y-4"> <!-- Created Invoice Display -->
<!-- Pay Links List --> <div v-else class="space-y-6">
<div class="grid gap-2 max-h-48 overflow-y-auto"> <!-- Invoice Details -->
<div <div class="bg-muted/50 rounded-lg p-4 space-y-3">
v-for="link in existingPayLinks" <div class="flex items-center justify-between">
:key="link.id" <h4 class="font-medium">Invoice Details</h4>
@click="selectPayLink(link)" <div class="text-sm text-muted-foreground">
class="flex items-center justify-between p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors" Expires in {{ formatExpiry(createdInvoice.expiry || 3600) }}
:class="{ 'bg-accent border-primary': selectedPayLink?.id === link.id }"
>
<div class="flex-1">
<p class="font-medium">{{ link.description }}</p>
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<span>{{ link.min }}-{{ link.max }} sats</span>
<Badge v-if="link.username" variant="secondary">{{ link.username }}</Badge>
</div>
</div>
<Button
variant="ghost"
size="sm"
@click.stop="deletePayLink(link)"
class="text-destructive hover:text-destructive"
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Selected Pay Link Details -->
<div v-if="selectedPayLink" class="border rounded-lg p-4 space-y-4">
<div class="flex items-center justify-between">
<h4 class="font-medium">{{ selectedPayLink.description }}</h4>
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<span>{{ selectedPayLink.min }}-{{ selectedPayLink.max }} sats</span>
</div>
</div>
<Separator />
<!-- QR Code Display -->
<div class="flex flex-col items-center space-y-4">
<div v-if="isLoadingQR" class="w-48 h-48 flex items-center justify-center bg-muted rounded-lg">
<RefreshCw class="h-8 w-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="qrCode" class="bg-white p-4 rounded-lg">
<img
:src="qrCode"
alt="LNURL QR Code"
class="w-48 h-48"
/>
</div>
<div v-else class="w-48 h-48 flex items-center justify-center bg-muted rounded-lg">
<QrCode class="h-12 w-12 text-muted-foreground opacity-50" />
</div>
<!-- Encoded LNURL (for QR) -->
<div class="w-full space-y-2">
<Label>LNURL (Encoded)</Label>
<div class="flex gap-2">
<Input
:value="selectedPayLink.lnurl ? encodeLNURL(selectedPayLink.lnurl) : ''"
readonly
class="font-mono text-xs"
/>
<Button
variant="outline"
size="sm"
@click="copyToClipboard(selectedPayLink.lnurl ? encodeLNURL(selectedPayLink.lnurl) : '', 'encoded-lnurl')"
>
<Check v-if="copiedField === 'encoded-lnurl'" class="h-4 w-4 text-green-600" />
<Copy v-else class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Raw LNURL -->
<div class="w-full space-y-2">
<Label>LNURL (Raw)</Label>
<div class="flex gap-2">
<Input
:value="selectedPayLink.lnurl"
readonly
class="font-mono text-xs"
/>
<Button
variant="outline"
size="sm"
@click="copyToClipboard(selectedPayLink.lnurl || '', 'lnurl')"
>
<Check v-if="copiedField === 'lnurl'" class="h-4 w-4 text-green-600" />
<Copy v-else class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Lightning Address (if available) -->
<div v-if="selectedPayLink.lnaddress" class="w-full space-y-2">
<Label>Lightning Address</Label>
<div class="flex gap-2">
<Input
:value="selectedPayLink.lnaddress"
readonly
class="font-mono"
/>
<Button
variant="outline"
size="sm"
@click="copyToClipboard(selectedPayLink.lnaddress || '', 'lnaddress')"
>
<Check v-if="copiedField === 'lnaddress'" class="h-4 w-4 text-green-600" />
<Copy v-else class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="flex justify-end pt-4"> <div class="grid grid-cols-2 gap-4 text-sm">
<Button variant="outline" @click="closeDialog"> <div>
Close <span class="text-muted-foreground">Amount:</span>
<span class="font-medium ml-2">{{ createdInvoice.amount }} sats</span>
</div>
<div>
<span class="text-muted-foreground">Status:</span>
<span class="font-medium ml-2 text-yellow-600">Pending</span>
</div>
</div>
<div>
<span class="text-muted-foreground text-sm">Description:</span>
<p class="font-medium">{{ createdInvoice.memo }}</p>
</div>
</div>
<!-- QR Code Display -->
<div class="flex flex-col items-center space-y-4">
<div v-if="isLoadingQR" class="w-64 h-64 flex items-center justify-center bg-muted rounded-lg">
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="qrCode" class="bg-white p-4 rounded-lg">
<img
:src="qrCode"
alt="Lightning Invoice QR Code"
class="w-64 h-64"
/>
</div>
<div v-else class="w-64 h-64 flex items-center justify-center bg-muted rounded-lg">
<QrCode class="h-12 w-12 text-muted-foreground opacity-50" />
</div>
</div>
<!-- Lightning Invoice (BOLT11) -->
<div class="space-y-2">
<Label>Lightning Invoice (BOLT11)</Label>
<div class="flex gap-2">
<Input
:value="createdInvoice.payment_request"
readonly
class="font-mono text-xs"
/>
<Button
variant="outline"
size="sm"
@click="copyToClipboard(createdInvoice.payment_request, 'bolt11')"
>
<Check v-if="copiedField === 'bolt11'" class="h-4 w-4 text-green-600" />
<Copy v-else class="h-4 w-4" />
</Button> </Button>
</div> </div>
</TabsContent> <p class="text-xs text-muted-foreground">
</Tabs> Share this invoice with the sender to receive payment
</p>
</div>
<!-- Payment Hash -->
<div class="space-y-2">
<Label>Payment Hash</Label>
<div class="flex gap-2">
<Input
:value="createdInvoice.payment_hash"
readonly
class="font-mono text-xs"
/>
<Button
variant="outline"
size="sm"
@click="copyToClipboard(createdInvoice.payment_hash, 'hash')"
>
<Check v-if="copiedField === 'hash'" class="h-4 w-4 text-green-600" />
<Copy v-else class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-2 pt-4">
<Button @click="createAnother" variant="outline" class="flex-1">
Create Another
</Button>
<Button @click="closeDialog" class="flex-1">
Done
</Button>
</div>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>

View file

@ -21,6 +21,23 @@ export interface SendPaymentRequest {
comment?: string comment?: string
} }
export interface CreateInvoiceRequest {
amount: number
memo: string
expiry?: number // Optional expiry in seconds
}
export interface Invoice {
payment_hash: string
bolt11: string // The BOLT11 invoice
payment_request: string // Same as bolt11, for compatibility
checking_id: string
amount: number
memo: string
time: number
expiry: number | null
}
export interface PaymentTransaction { export interface PaymentTransaction {
id: string id: string
amount: number amount: number
@ -47,6 +64,7 @@ export default class WalletService extends BaseService {
private _transactions = ref<PaymentTransaction[]>([]) private _transactions = ref<PaymentTransaction[]>([])
private _isCreatingPayLink = ref(false) private _isCreatingPayLink = ref(false)
private _isSendingPayment = ref(false) private _isSendingPayment = ref(false)
private _isCreatingInvoice = ref(false)
private _error = ref<string | null>(null) private _error = ref<string | null>(null)
// Public reactive getters // Public reactive getters
@ -54,6 +72,7 @@ export default class WalletService extends BaseService {
readonly transactions = computed(() => this._transactions.value) readonly transactions = computed(() => this._transactions.value)
readonly isCreatingPayLink = computed(() => this._isCreatingPayLink.value) readonly isCreatingPayLink = computed(() => this._isCreatingPayLink.value)
readonly isSendingPayment = computed(() => this._isSendingPayment.value) readonly isSendingPayment = computed(() => this._isSendingPayment.value)
readonly isCreatingInvoice = computed(() => this._isCreatingInvoice.value)
readonly error = computed(() => this._error.value) readonly error = computed(() => this._error.value)
protected async onInitialize(): Promise<void> { protected async onInitialize(): Promise<void> {
@ -148,6 +167,63 @@ export default class WalletService extends BaseService {
} }
} }
/**
* Create a Lightning invoice for receiving payments
*/
async createInvoice(request: CreateInvoiceRequest): Promise<Invoice | null> {
this._isCreatingInvoice.value = true
this._error.value = null
try {
const invoiceKey = this.paymentService?.getPreferredWalletInvoiceKey()
if (!invoiceKey) {
throw new Error('No invoice key available')
}
// Create invoice via LNbits payments API
const response = await fetch(`${config.api.baseUrl}/api/v1/payments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': invoiceKey
},
body: JSON.stringify({
out: false, // Incoming payment (receiving)
amount: request.amount,
unit: 'sat',
memo: request.memo,
expiry: request.expiry || 3600 // Default 1 hour expiry
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || 'Failed to create invoice')
}
const rawInvoice = await response.json()
console.log('Raw invoice response:', rawInvoice)
// Process the response to fix data issues
const invoice: Invoice = {
...rawInvoice,
payment_request: rawInvoice.bolt11, // Copy bolt11 to payment_request for compatibility
amount: rawInvoice.amount / 1000, // Convert from millisats to sats
expiry: rawInvoice.expiry ? this.parseExpiryToSeconds(rawInvoice.expiry) : null
}
console.log('Processed invoice:', invoice)
return invoice
} catch (error) {
console.error('Failed to create invoice:', error)
this._error.value = error instanceof Error ? error.message : 'Failed to create invoice'
return null
} finally {
this._isCreatingInvoice.value = false
}
}
/** /**
* Send a Lightning payment * Send a Lightning payment
*/ */
@ -351,6 +427,21 @@ export default class WalletService extends BaseService {
]) ])
} }
/**
* Parse expiry date string to seconds from now
*/
private parseExpiryToSeconds(expiryStr: string): number {
try {
const expiryDate = new Date(expiryStr)
const now = new Date()
const diffMs = expiryDate.getTime() - now.getTime()
return Math.max(0, Math.floor(diffMs / 1000)) // Return seconds, minimum 0
} catch (error) {
console.error('Failed to parse expiry date:', expiryStr, error)
return 3600 // Default to 1 hour
}
}
/** /**
* Add a new transaction from WebSocket notification * Add a new transaction from WebSocket notification
*/ */
@ -390,4 +481,4 @@ export default class WalletService extends BaseService {
tag: payment.tag || null tag: payment.tag || null
} }
} }
} }

View file

@ -277,14 +277,6 @@ onMounted(async () => {
</div> </div>
</div> </div>
<Button
variant="outline"
@click="showReceiveDialog = true"
class="w-full sm:w-auto"
>
<QrCode class="h-4 w-4 mr-2" />
Manage Addresses
</Button>
</div> </div>
</div> </div>
</CardContent> </CardContent>