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
304
src/modules/wallet/views/WalletPage.vue
Normal file
304
src/modules/wallet/views/WalletPage.vue
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { RefreshCw, Send, QrCode, ArrowUpRight, ArrowDownLeft, Clock, Wallet } from 'lucide-vue-next'
|
||||
import CurrencyDisplay from '@/components/ui/CurrencyDisplay.vue'
|
||||
import ReceiveDialog from '../components/ReceiveDialog.vue'
|
||||
import SendDialog from '../components/SendDialog.vue'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
// Services
|
||||
const walletService = injectService(SERVICE_TOKENS.WALLET_SERVICE) as any
|
||||
const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) as any
|
||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||
|
||||
// State
|
||||
const showReceiveDialog = ref(false)
|
||||
const showSendDialog = ref(false)
|
||||
const selectedTab = ref('transactions')
|
||||
|
||||
// Computed
|
||||
const transactions = computed(() => walletService?.transactions?.value || [])
|
||||
const isLoading = computed(() => walletService?.isLoading?.value || false)
|
||||
const error = computed(() => walletService?.error?.value)
|
||||
const currentUser = computed(() => authService?.currentUser?.value)
|
||||
const totalBalance = computed(() => {
|
||||
if (!currentUser.value?.wallets) return 0
|
||||
return currentUser.value.wallets.reduce((total: number, wallet: any) => {
|
||||
return total + (wallet.balance_msat || 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// Get transactions grouped by date
|
||||
const groupedTransactions = computed(() => {
|
||||
const groups: Record<string, typeof transactions.value> = {}
|
||||
|
||||
transactions.value.forEach((tx: any) => {
|
||||
const dateKey = format(tx.timestamp, 'MMMM d, yyyy')
|
||||
if (!groups[dateKey]) {
|
||||
groups[dateKey] = []
|
||||
}
|
||||
groups[dateKey].push(tx)
|
||||
})
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
// Methods
|
||||
async function refresh() {
|
||||
await walletService?.refresh()
|
||||
// Also refresh auth data to update balance
|
||||
await authService?.refreshUserData()
|
||||
}
|
||||
|
||||
function getTransactionIcon(type: string) {
|
||||
return type === 'received' ? ArrowDownLeft : ArrowUpRight
|
||||
}
|
||||
|
||||
function getTransactionColor(type: string) {
|
||||
return type === 'received' ? 'text-green-600' : 'text-orange-600'
|
||||
}
|
||||
|
||||
function getStatusColor(status: string) {
|
||||
switch (status) {
|
||||
case 'confirmed':
|
||||
return 'default'
|
||||
case 'pending':
|
||||
return 'secondary'
|
||||
case 'failed':
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'outline'
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on mount
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto py-8 px-4 max-w-6xl">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-2">
|
||||
<Wallet class="h-8 w-8" />
|
||||
Wallet
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-1">Manage your Bitcoin transactions</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="refresh"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': isLoading }" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Balance Card -->
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Total Balance</CardTitle>
|
||||
<CardDescription>Available across all your wallets</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-3xl font-bold">
|
||||
<CurrencyDisplay :balance-msat="totalBalance" />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
@click="showReceiveDialog = true"
|
||||
class="gap-2"
|
||||
>
|
||||
<QrCode class="h-4 w-4" />
|
||||
Receive
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="showSendDialog = true"
|
||||
class="gap-2"
|
||||
>
|
||||
<Send class="h-4 w-4" />
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Tabs -->
|
||||
<Tabs v-model="selectedTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="transactions">Transaction History</TabsTrigger>
|
||||
<TabsTrigger value="addresses">Receive Addresses</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- Transactions Tab -->
|
||||
<TabsContent value="transactions" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Transactions</CardTitle>
|
||||
<CardDescription>Your payment history</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="error" class="text-center py-8 text-destructive">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="transactions.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
<Clock class="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No transactions yet</p>
|
||||
<p class="text-sm mt-2">Your transaction history will appear here</p>
|
||||
</div>
|
||||
|
||||
<ScrollArea v-else class="h-[500px] pr-4">
|
||||
<div v-for="(txs, date) in groupedTransactions" :key="date" class="mb-6">
|
||||
<div class="text-sm font-medium text-muted-foreground mb-3">{{ date }}</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="tx in txs"
|
||||
:key="tx.id"
|
||||
class="flex items-center justify-between p-4 rounded-lg border hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="p-2 rounded-full bg-background border"
|
||||
:class="getTransactionColor(tx.type)"
|
||||
>
|
||||
<component
|
||||
:is="getTransactionIcon(tx.type)"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium">{{ tx.description }}</p>
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{{ format(tx.timestamp, 'HH:mm') }}</span>
|
||||
<Badge :variant="getStatusColor(tx.status)" class="text-xs">
|
||||
{{ tx.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<p
|
||||
class="font-semibold"
|
||||
:class="getTransactionColor(tx.type)"
|
||||
>
|
||||
{{ tx.type === 'received' ? '+' : '-' }}
|
||||
{{ tx.amount.toLocaleString() }} sats
|
||||
</p>
|
||||
<p v-if="tx.fee" class="text-xs text-muted-foreground">
|
||||
Fee: {{ tx.fee }} sats
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- Addresses Tab -->
|
||||
<TabsContent value="addresses" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Receive Addresses</CardTitle>
|
||||
<CardDescription>Your LNURL and Lightning addresses for receiving payments</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
@click="showReceiveDialog = true"
|
||||
>
|
||||
<QrCode class="h-4 w-4 mr-2" />
|
||||
New Address
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="walletService?.payLinks?.value?.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
<QrCode class="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No receive addresses created</p>
|
||||
<p class="text-sm mt-2">Create an address to start receiving payments</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="mt-4"
|
||||
@click="showReceiveDialog = true"
|
||||
>
|
||||
Create Your First Address
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="link in walletService?.payLinks?.value"
|
||||
:key="link.id"
|
||||
class="p-4 border rounded-lg hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">{{ link.description }}</p>
|
||||
<div class="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
|
||||
<span>{{ link.min }}-{{ link.max }} sats</span>
|
||||
<Badge v-if="link.username" variant="secondary">
|
||||
{{ link.username }}@{{ new URL(paymentService?.config?.baseUrl || 'http://localhost').hostname }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-2">
|
||||
<div v-if="link.lnaddress" class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">Lightning Address:</span>
|
||||
<code class="text-xs bg-muted px-2 py-1 rounded">{{ link.lnaddress }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="showReceiveDialog = true"
|
||||
>
|
||||
<QrCode class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<ReceiveDialog
|
||||
:open="showReceiveDialog"
|
||||
@update:open="showReceiveDialog = $event"
|
||||
/>
|
||||
|
||||
<SendDialog
|
||||
v-if="showSendDialog"
|
||||
:open="showSendDialog"
|
||||
@update:open="showSendDialog = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue