web-app/src/modules/wallet/components/SendDialog.vue
padreug f94dc1d03c Enhance SendDialog and WalletPage with QR code scanning integration
- Added initialDestination prop to SendDialog for pre-filling the destination field.
- Implemented a watcher to update the destination field when initialDestination changes.
- Integrated QRScanner component into WalletPage, allowing users to scan QR codes for payment destinations.
- Updated SendDialog to accept scanned destination and improved user feedback with toast notifications.

These changes streamline the payment process by enabling QR code scanning directly within the wallet interface.
2025-09-18 23:02:56 +02:00

238 lines
No EOL
7.2 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Send, AlertCircle, Loader2, ScanLine } from 'lucide-vue-next'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import QRScanner from '@/components/ui/qr-scanner.vue'
interface Props {
open: boolean
initialDestination?: string
}
interface Emits {
(e: 'update:open', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// Services
const walletService = injectService(SERVICE_TOKENS.WALLET_SERVICE) as any
const toastService = injectService(SERVICE_TOKENS.TOAST_SERVICE) as any
// Form validation schema
const formSchema = toTypedSchema(z.object({
destination: z.string().min(1, "Destination is required"),
amount: z.number().min(1, "Amount must be at least 1 sat").max(1000000, "Amount too large"),
comment: z.string().optional()
}))
// Form setup
const form = useForm({
validationSchema: formSchema,
initialValues: {
destination: props.initialDestination || '',
amount: 100,
comment: ''
}
})
const { resetForm, values, meta, setFieldValue } = form
const isFormValid = computed(() => meta.value.valid)
// Watch for prop changes
watch(() => props.initialDestination, (newDestination) => {
if (newDestination) {
setFieldValue('destination', newDestination)
}
}, { immediate: true })
// State
const isSending = computed(() => walletService?.isSendingPayment?.value || false)
const showScanner = ref(false)
const error = computed(() => walletService?.error?.value)
// Methods
const onSubmit = form.handleSubmit(async (formValues) => {
try {
const success = await walletService.sendPayment({
destination: formValues.destination,
amount: formValues.amount,
comment: formValues.comment || undefined
})
if (success) {
toastService?.success('Payment sent successfully!')
closeDialog()
}
} catch (error) {
console.error('Failed to send payment:', error)
}
})
function closeDialog() {
emit('update:open', false)
resetForm()
showScanner.value = false
}
// QR Scanner functions
function openScanner() {
showScanner.value = true
}
function closeScanner() {
showScanner.value = false
}
function handleScanResult(result: string) {
// Clean up the scanned result by removing lightning: prefix if present
let cleanedResult = result
if (result.toLowerCase().startsWith('lightning:')) {
cleanedResult = result.substring(10) // Remove "lightning:" prefix
}
// Set the cleaned result in the destination field
setFieldValue('destination', cleanedResult)
closeScanner()
toastService?.success('QR code scanned successfully!')
}
// Determine destination type helper text
const destinationType = computed(() => {
const dest = values.destination?.toLowerCase() || ''
if (dest.startsWith('lnbc') || dest.startsWith('lntb')) {
return 'Lightning Invoice'
} else if (dest.includes('@')) {
return 'Lightning Address'
} else if (dest.startsWith('lnurl')) {
return 'LNURL'
}
return ''
})
</script>
<template>
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Send class="h-5 w-5" />
Send Bitcoin
</DialogTitle>
<DialogDescription>
Send Bitcoin via Lightning Network
</DialogDescription>
</DialogHeader>
<form @submit="onSubmit" class="space-y-4">
<FormField v-slot="{ componentField }" name="destination">
<FormItem>
<div class="flex items-center justify-between">
<FormLabel>Destination *</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
@click="openScanner"
class="h-7 px-2 text-xs"
>
<ScanLine class="w-3 h-3 mr-1" />
Scan QR
</Button>
</div>
<FormControl>
<Textarea
placeholder="Lightning invoice, LNURL, or Lightning address (user@domain.com)"
v-bind="componentField"
class="min-h-[80px] font-mono text-xs"
/>
</FormControl>
<FormDescription v-if="destinationType">
Detected: {{ destinationType }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="amount">
<FormItem>
<FormLabel>Amount (sats) *</FormLabel>
<FormControl>
<Input
type="number"
min="1"
placeholder="100"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Amount to send in satoshis</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="comment">
<FormItem>
<FormLabel>Comment (Optional)</FormLabel>
<FormControl>
<Input
placeholder="Optional message with payment"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Add a note to your payment (if supported)</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<div v-if="error" class="flex items-start gap-2 p-3 bg-destructive/15 text-destructive rounded-lg">
<AlertCircle class="h-4 w-4 mt-0.5" />
<span class="text-sm">{{ error }}</span>
</div>
<div class="flex gap-2 pt-4">
<Button
type="submit"
:disabled="!isFormValid || isSending"
class="flex-1"
>
<Loader2 v-if="isSending" class="w-4 h-4 mr-2 animate-spin" />
<Send v-else class="w-4 h-4 mr-2" />
{{ isSending ? 'Sending...' : 'Send Payment' }}
</Button>
<Button type="button" variant="outline" @click="closeDialog" :disabled="isSending">
Cancel
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<!-- QR Scanner Dialog -->
<Dialog :open="showScanner" @update:open="showScanner = $event">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<ScanLine class="h-5 w-5" />
Scan QR Code
</DialogTitle>
<DialogDescription>
Point your camera at a Lightning invoice QR code
</DialogDescription>
</DialogHeader>
<QRScanner
@result="handleScanResult"
@close="closeScanner"
/>
</DialogContent>
</Dialog>
</template>