feat(marketplace): add invoice download link to PurchasesView and LicensesView
This commit is contained in:
parent
166acc6069
commit
9f4c84c025
5 changed files with 71 additions and 11 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
Loading…
Reference in a new issue