Add wallet module with receive and send functionality
- Introduced a new wallet module that includes components for sending and receiving Bitcoin payments. - Implemented WalletService to manage payment links and transactions, including methods for creating LNURL pay links and sending payments. - Added dialogs for receiving and sending payments, enhancing user interaction with the wallet. - Updated app configuration to enable the wallet module and integrated it into the main application flow. These changes provide users with a comprehensive wallet experience, allowing for seamless Bitcoin transactions.
This commit is contained in:
parent
c74945874c
commit
f75aae6be6
12 changed files with 1294 additions and 3 deletions
177
src/modules/wallet/components/SendDialog.vue
Normal file
177
src/modules/wallet/components/SendDialog.vue
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<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 { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
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>
|
||||
|
||||
<Alert v-if="error" variant="destructive">
|
||||
<AlertCircle class="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{{ error }}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue