feat(checkout): add CheckoutSuccessView, CheckoutErrorView and getOrder
This commit is contained in:
parent
5233a5b7f2
commit
508e082bcc
10 changed files with 204 additions and 0 deletions
|
|
@ -34,5 +34,6 @@ export {
|
|||
LazySellerDashboard,
|
||||
LazyWishlist,
|
||||
LazyPurchases,
|
||||
LazyCheckoutComplete,
|
||||
} from './lazy-component';
|
||||
export type { LazyComponentProps, LazyErrorFallbackProps, LazyErrorBoundaryProps } from './lazy-component';
|
||||
|
|
|
|||
|
|
@ -37,4 +37,5 @@ export {
|
|||
LazySellerDashboard,
|
||||
LazyWishlist,
|
||||
LazyPurchases,
|
||||
LazyCheckoutComplete,
|
||||
} from './lazyExports';
|
||||
|
|
|
|||
|
|
@ -227,3 +227,11 @@ export const LazyPurchases = createLazyComponent(
|
|||
undefined,
|
||||
'Purchases',
|
||||
);
|
||||
export const LazyCheckoutComplete = createLazyComponent(
|
||||
() =>
|
||||
import('@/features/checkout/CheckoutCompletePage').then((m) => ({
|
||||
default: m.CheckoutCompletePage,
|
||||
})),
|
||||
undefined,
|
||||
'Checkout Complete',
|
||||
);
|
||||
|
|
|
|||
55
apps/web/src/features/checkout/CheckoutCompletePage.tsx
Normal file
55
apps/web/src/features/checkout/CheckoutCompletePage.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* CheckoutCompletePage — Page après redirect Hyperswitch
|
||||
* v0.402 P1.2: Lit order_id depuis l'URL, charge la commande, affiche success ou error
|
||||
*/
|
||||
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { marketplaceService } from '@/services/marketplaceService';
|
||||
import { CheckoutSuccessView } from './CheckoutSuccessView';
|
||||
import { CheckoutErrorView } from './CheckoutErrorView';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export function CheckoutCompletePage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const orderId = searchParams.get('order_id');
|
||||
|
||||
const { data: order, isLoading, error } = useQuery({
|
||||
queryKey: ['order', orderId],
|
||||
queryFn: () => marketplaceService.getOrder(orderId!),
|
||||
enabled: !!orderId,
|
||||
});
|
||||
|
||||
if (!orderId) {
|
||||
return (
|
||||
<CheckoutErrorView />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-layout-page px-4 py-12">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<Skeleton className="h-24 w-24 rounded-full mx-auto" />
|
||||
<div className="space-y-2 text-center">
|
||||
<Skeleton className="h-8 w-48 mx-auto" />
|
||||
<Skeleton className="h-4 w-64 mx-auto" />
|
||||
</div>
|
||||
<Skeleton className="h-32 w-full rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !order) {
|
||||
return <CheckoutErrorView orderId={orderId} />;
|
||||
}
|
||||
|
||||
if (order.status === 'completed') {
|
||||
return <CheckoutSuccessView order={order} />;
|
||||
}
|
||||
|
||||
return <CheckoutErrorView orderId={orderId} status={order.status} />;
|
||||
}
|
||||
|
||||
export default CheckoutCompletePage;
|
||||
53
apps/web/src/features/checkout/CheckoutErrorView.tsx
Normal file
53
apps/web/src/features/checkout/CheckoutErrorView.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* CheckoutErrorView — Échec ou annulation du paiement
|
||||
* v0.402 P1.2: Page affichée après redirect Hyperswitch (order failed/cancelled)
|
||||
*/
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AlertCircle, ShoppingCart } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface CheckoutErrorViewProps {
|
||||
orderId?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export function CheckoutErrorView({ orderId, status }: CheckoutErrorViewProps) {
|
||||
const isCancelled = status === 'cancelled' || status === 'canceled';
|
||||
const title = isCancelled ? 'Paiement annulé' : 'Échec du paiement';
|
||||
const message = isCancelled
|
||||
? 'Vous avez annulé le paiement. Votre panier n\'a pas été modifié.'
|
||||
: 'Le paiement n\'a pas pu être traité. Veuillez réessayer ou utiliser une autre méthode.';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-layout-page px-4 py-12">
|
||||
<div className="max-w-md w-full space-y-8 text-center">
|
||||
<div className="flex justify-center">
|
||||
<div className="rounded-full bg-destructive/10 p-4">
|
||||
<AlertCircle className="h-16 w-16 text-destructive" aria-hidden />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
||||
<p className="text-muted-foreground">{message}</p>
|
||||
{orderId && (
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
Référence : {orderId.slice(0, 8)}…
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Button variant="primary" asChild>
|
||||
<Link to="/marketplace" className="inline-flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
Retour au panier
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/marketplace">Continuer mes achats</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
apps/web/src/features/checkout/CheckoutSuccessView.tsx
Normal file
68
apps/web/src/features/checkout/CheckoutSuccessView.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* CheckoutSuccessView — Confirmation de paiement réussi
|
||||
* v0.402 P1.2: Page affichée après redirect Hyperswitch (order completed)
|
||||
*/
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { CheckCircle2, ShoppingBag } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
status: string;
|
||||
total_amount: number;
|
||||
currency: string;
|
||||
items?: Array<{ product_id: string; price: number }>;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
interface CheckoutSuccessViewProps {
|
||||
order: Order;
|
||||
}
|
||||
|
||||
const formatPrice = (amount: number, currency: string) =>
|
||||
new Intl.NumberFormat('fr-FR', { style: 'currency', currency: currency || 'EUR' }).format(amount);
|
||||
|
||||
export function CheckoutSuccessView({ order }: CheckoutSuccessViewProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-layout-page px-4 py-12">
|
||||
<div className="max-w-md w-full space-y-8 text-center">
|
||||
<div className="flex justify-center">
|
||||
<div className="rounded-full bg-success/10 p-4">
|
||||
<CheckCircle2 className="h-16 w-16 text-success" aria-hidden />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-foreground">Paiement réussi</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Votre commande <span className="font-mono text-foreground">{order.id.slice(0, 8)}…</span> a été confirmée.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6 text-left space-y-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Total</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{formatPrice(order.total_amount, order.currency)}
|
||||
</span>
|
||||
</div>
|
||||
{order.items && order.items.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{order.items.length} produit{order.items.length > 1 ? 's' : ''} dans cette commande
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Button variant="primary" asChild>
|
||||
<Link to="/purchases" className="inline-flex items-center gap-2">
|
||||
<ShoppingBag className="h-4 w-4" />
|
||||
Mes achats
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/marketplace">Continuer mes achats</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
apps/web/src/features/checkout/index.ts
Normal file
3
apps/web/src/features/checkout/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { CheckoutCompletePage } from './CheckoutCompletePage';
|
||||
export { CheckoutSuccessView } from './CheckoutSuccessView';
|
||||
export { CheckoutErrorView } from './CheckoutErrorView';
|
||||
|
|
@ -74,6 +74,7 @@ vi.mock('@/components/ui/LazyComponent', () => ({
|
|||
LazySellerDashboard: () => <div>Seller Page</div>,
|
||||
LazyWishlist: () => <div>Wishlist Page</div>,
|
||||
LazyPurchases: () => <div>Purchases Page</div>,
|
||||
LazyCheckoutComplete: () => <div>Checkout Complete Page</div>,
|
||||
}));
|
||||
|
||||
// Mock DashboardLayout (used by ProtectedLayoutRoute)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
LazySellerDashboard,
|
||||
LazyWishlist,
|
||||
LazyPurchases,
|
||||
LazyCheckoutComplete,
|
||||
LazyQueue,
|
||||
LazyDeveloper,
|
||||
LazyGear,
|
||||
|
|
@ -81,6 +82,7 @@ export function getProtectedRoutes(): RouteEntry[] {
|
|||
{ path: '/sell', element: wrapProtected(<LazySellerDashboard onCreateProduct={() => {}} />) },
|
||||
{ path: '/wishlist', element: wrapProtected(<LazyWishlist />) },
|
||||
{ path: '/purchases', element: wrapProtected(<LazyPurchases />) },
|
||||
{ path: '/checkout/complete', element: wrapProtected(<LazyCheckoutComplete />) },
|
||||
{ path: '/chat', element: wrapProtected(<LazyChat />) },
|
||||
{ path: '/library', element: wrapProtected(<LazyLibrary />) },
|
||||
{ path: '/profile', element: wrapProtected(<LazyProfile />) },
|
||||
|
|
|
|||
|
|
@ -137,6 +137,18 @@ export const marketplaceService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
getOrder: async (orderId: string) => {
|
||||
const response = await apiClient.get<{
|
||||
id: string;
|
||||
status: string;
|
||||
total_amount: number;
|
||||
currency: string;
|
||||
items?: Array<{ product_id: string; price: number }>;
|
||||
created_at?: string;
|
||||
}>(`/marketplace/orders/${orderId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Wishlist (requires auth)
|
||||
getWishlist: async (): Promise<Product[]> => {
|
||||
const response = await apiClient.get<{
|
||||
|
|
|
|||
Loading…
Reference in a new issue