feat(marketplace): add invoice download link to PurchasesView and LicensesView

This commit is contained in:
senke 2026-02-22 16:15:55 +01:00
parent 166acc6069
commit 9f4c84c025
5 changed files with 71 additions and 11 deletions

View file

@ -1,11 +1,12 @@
/**
* LicensesView list of user's purchased licenses with download links (v0.401 M2)
* v0.403 F1: Invoice download link
*/
import { useState, useEffect } from 'react';
import { marketplaceService } from '@/services/marketplaceService';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Download, FileAudio } from 'lucide-react';
import { Download, FileAudio, FileText } from 'lucide-react';
import { EmptyState } from '@/components/ui/empty-state';
import { Skeleton } from '@/components/ui/skeleton';
@ -72,16 +73,32 @@ export function LicensesView() {
Purchased {item.order?.created_at ? new Date(item.order.created_at).toLocaleDateString() : '—'}
</div>
</div>
<Button
variant="outline"
size="sm"
disabled={!item.download_url}
onClick={() => item.download_url && window.open(item.download_url, '_blank')}
className="shrink-0"
>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
<div className="flex gap-2 shrink-0">
<Button
variant="outline"
size="sm"
disabled={!item.download_url}
onClick={() => item.download_url && window.open(item.download_url, '_blank')}
>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
<Button
variant="outline"
size="sm"
onClick={async () => {
try {
const orderId = item.order?.id ?? item.license?.order_id;
if (orderId) await marketplaceService.downloadInvoice(orderId);
} catch {
// ignore
}
}}
title="Download Invoice"
>
<FileText className="w-4 h-4" />
</Button>
</div>
</Card>
))}
</div>

View file

@ -1,5 +1,6 @@
import { RefundRequestModal } from '@/components/commerce/modals/RefundRequestModal';
import toast from '@/utils/toast';
import { marketplaceService } from '@/services/marketplaceService';
import { usePurchasesView } from './usePurchasesView';
import { PurchasesViewHeader } from './PurchasesViewHeader';
import { PurchasesViewList } from './PurchasesViewList';
@ -19,6 +20,15 @@ export function PurchasesView({ initialPurchases }: PurchasesViewProps = {}) {
handleDownload,
} = usePurchasesView(initialPurchases ?? undefined);
const handleInvoiceDownload = async (orderId: string) => {
try {
await marketplaceService.downloadInvoice(orderId);
toast.success('Invoice downloaded');
} catch {
toast.error('Failed to download invoice');
}
};
if (loading) {
return <PurchasesViewSkeleton />;
}
@ -34,6 +44,7 @@ export function PurchasesView({ initialPurchases }: PurchasesViewProps = {}) {
setActiveDownloadId={setActiveDownloadId}
onDownloadFormat={handleDownload}
onLicense={() => toast('License document opened')}
onInvoiceDownload={handleInvoiceDownload}
onRefund={setRefundOrderId}
/>

View file

@ -9,6 +9,7 @@ interface PurchasesViewItemProps {
onToggleDownload: () => void;
onDownloadFormat: (format: string) => void;
onLicense: () => void;
onInvoiceDownload: (orderId: string) => void;
onRefund: () => void;
}
@ -20,6 +21,7 @@ export function PurchasesViewItem({
onToggleDownload,
onDownloadFormat,
onLicense,
onInvoiceDownload,
onRefund,
}: PurchasesViewItemProps) {
const product = purchase.product as { title: string; coverUrl?: string; type?: string };
@ -86,6 +88,17 @@ export function PurchasesViewItem({
License
</Button>
<Button
variant="ghost"
size="sm"
className="border border-border"
icon={<FileText className="w-4 h-4" />}
onClick={() => onInvoiceDownload(purchase.orderId)}
title="Download Invoice"
>
Invoice
</Button>
<Button
variant="ghost"
size="sm"

View file

@ -23,6 +23,7 @@ interface PurchasesViewListProps {
setActiveDownloadId: (id: string | null) => void;
onDownloadFormat: (format: string) => void;
onLicense: () => void;
onInvoiceDownload: (orderId: string) => void;
onRefund: (orderId: string) => void;
}
@ -33,6 +34,7 @@ export function PurchasesViewList({
setActiveDownloadId,
onDownloadFormat,
onLicense,
onInvoiceDownload,
onRefund,
}: PurchasesViewListProps) {
if (loading) return null;
@ -64,6 +66,7 @@ export function PurchasesViewList({
}
onDownloadFormat={onDownloadFormat}
onLicense={onLicense}
onInvoiceDownload={onInvoiceDownload}
onRefund={() => onRefund(purchase.orderId)}
/>
</motion.div>

View file

@ -149,6 +149,22 @@ export const marketplaceService = {
return response.data;
},
// v0.403 F1: Download invoice PDF
downloadInvoice: async (orderId: string) => {
const response = await apiClient.get(`/marketplace/orders/${orderId}/invoice`, {
responseType: 'blob',
});
const blob = new Blob([response.data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `invoice-${orderId.slice(0, 8)}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
// Wishlist (requires auth)
getWishlist: async (): Promise<Product[]> => {
const response = await apiClient.get<{