- Replaced the Alert component with a custom error display using a div for better styling and user experience. - Updated the error message presentation to enhance visibility and maintain consistency with the overall UI design. These changes improve the user feedback mechanism in the SendDialog, ensuring errors are communicated effectively.
174 lines
No EOL
5.4 KiB
Vue
174 lines
No EOL
5.4 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed } 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 { Label } from '@/components/ui/label'
|
|
import { Send, AlertCircle, Loader2 } from 'lucide-vue-next'
|
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
|
|
interface Props {
|
|
open: boolean
|
|
}
|
|
|
|
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: '',
|
|
amount: 100,
|
|
comment: ''
|
|
}
|
|
})
|
|
|
|
const { resetForm, values, meta } = form
|
|
const isFormValid = computed(() => meta.value.valid)
|
|
|
|
// State
|
|
const isSending = computed(() => walletService?.isSendingPayment?.value || 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()
|
|
}
|
|
|
|
// 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>
|
|
<FormLabel>Destination *</FormLabel>
|
|
<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>
|
|
</template> |