- 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.
238 lines
No EOL
7.2 KiB
Vue
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> |