From 508e082bcce22dd8cc545ca6fa972d5e0764ec91 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 22 Feb 2026 14:42:15 +0100 Subject: [PATCH] feat(checkout): add CheckoutSuccessView, CheckoutErrorView and getOrder --- apps/web/src/components/ui/LazyComponent.tsx | 1 + .../src/components/ui/lazy-component/index.ts | 1 + .../ui/lazy-component/lazyExports.ts | 8 +++ .../checkout/CheckoutCompletePage.tsx | 55 +++++++++++++++ .../features/checkout/CheckoutErrorView.tsx | 53 +++++++++++++++ .../features/checkout/CheckoutSuccessView.tsx | 68 +++++++++++++++++++ apps/web/src/features/checkout/index.ts | 3 + apps/web/src/router/index.test.tsx | 1 + apps/web/src/router/routeConfig.tsx | 2 + apps/web/src/services/marketplaceService.ts | 12 ++++ 10 files changed, 204 insertions(+) create mode 100644 apps/web/src/features/checkout/CheckoutCompletePage.tsx create mode 100644 apps/web/src/features/checkout/CheckoutErrorView.tsx create mode 100644 apps/web/src/features/checkout/CheckoutSuccessView.tsx create mode 100644 apps/web/src/features/checkout/index.ts diff --git a/apps/web/src/components/ui/LazyComponent.tsx b/apps/web/src/components/ui/LazyComponent.tsx index 627538593..140c407e8 100644 --- a/apps/web/src/components/ui/LazyComponent.tsx +++ b/apps/web/src/components/ui/LazyComponent.tsx @@ -34,5 +34,6 @@ export { LazySellerDashboard, LazyWishlist, LazyPurchases, + LazyCheckoutComplete, } from './lazy-component'; export type { LazyComponentProps, LazyErrorFallbackProps, LazyErrorBoundaryProps } from './lazy-component'; diff --git a/apps/web/src/components/ui/lazy-component/index.ts b/apps/web/src/components/ui/lazy-component/index.ts index 7c655a210..218649591 100644 --- a/apps/web/src/components/ui/lazy-component/index.ts +++ b/apps/web/src/components/ui/lazy-component/index.ts @@ -37,4 +37,5 @@ export { LazySellerDashboard, LazyWishlist, LazyPurchases, + LazyCheckoutComplete, } from './lazyExports'; diff --git a/apps/web/src/components/ui/lazy-component/lazyExports.ts b/apps/web/src/components/ui/lazy-component/lazyExports.ts index aac8f8a1e..4667b0a42 100644 --- a/apps/web/src/components/ui/lazy-component/lazyExports.ts +++ b/apps/web/src/components/ui/lazy-component/lazyExports.ts @@ -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', +); diff --git a/apps/web/src/features/checkout/CheckoutCompletePage.tsx b/apps/web/src/features/checkout/CheckoutCompletePage.tsx new file mode 100644 index 000000000..0c3fa00fd --- /dev/null +++ b/apps/web/src/features/checkout/CheckoutCompletePage.tsx @@ -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 ( + + ); + } + + if (isLoading) { + return ( +
+
+ +
+ + +
+ +
+
+ ); + } + + if (error || !order) { + return ; + } + + if (order.status === 'completed') { + return ; + } + + return ; +} + +export default CheckoutCompletePage; diff --git a/apps/web/src/features/checkout/CheckoutErrorView.tsx b/apps/web/src/features/checkout/CheckoutErrorView.tsx new file mode 100644 index 000000000..0667f813b --- /dev/null +++ b/apps/web/src/features/checkout/CheckoutErrorView.tsx @@ -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 ( +
+
+
+
+ +
+
+
+

{title}

+

{message}

+ {orderId && ( +

+ Référence : {orderId.slice(0, 8)}… +

+ )} +
+
+ + +
+
+
+ ); +} diff --git a/apps/web/src/features/checkout/CheckoutSuccessView.tsx b/apps/web/src/features/checkout/CheckoutSuccessView.tsx new file mode 100644 index 000000000..773a41f85 --- /dev/null +++ b/apps/web/src/features/checkout/CheckoutSuccessView.tsx @@ -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 ( +
+
+
+
+ +
+
+
+

Paiement réussi

+

+ Votre commande {order.id.slice(0, 8)}… a été confirmée. +

+
+
+
+ Total + + {formatPrice(order.total_amount, order.currency)} + +
+ {order.items && order.items.length > 0 && ( +

+ {order.items.length} produit{order.items.length > 1 ? 's' : ''} dans cette commande +

+ )} +
+
+ + +
+
+
+ ); +} diff --git a/apps/web/src/features/checkout/index.ts b/apps/web/src/features/checkout/index.ts new file mode 100644 index 000000000..6a3664edc --- /dev/null +++ b/apps/web/src/features/checkout/index.ts @@ -0,0 +1,3 @@ +export { CheckoutCompletePage } from './CheckoutCompletePage'; +export { CheckoutSuccessView } from './CheckoutSuccessView'; +export { CheckoutErrorView } from './CheckoutErrorView'; diff --git a/apps/web/src/router/index.test.tsx b/apps/web/src/router/index.test.tsx index 01a0b60c2..797ec9a87 100644 --- a/apps/web/src/router/index.test.tsx +++ b/apps/web/src/router/index.test.tsx @@ -74,6 +74,7 @@ vi.mock('@/components/ui/LazyComponent', () => ({ LazySellerDashboard: () =>
Seller Page
, LazyWishlist: () =>
Wishlist Page
, LazyPurchases: () =>
Purchases Page
, + LazyCheckoutComplete: () =>
Checkout Complete Page
, })); // Mock DashboardLayout (used by ProtectedLayoutRoute) diff --git a/apps/web/src/router/routeConfig.tsx b/apps/web/src/router/routeConfig.tsx index 3802595f3..243d0c5f7 100644 --- a/apps/web/src/router/routeConfig.tsx +++ b/apps/web/src/router/routeConfig.tsx @@ -30,6 +30,7 @@ import { LazySellerDashboard, LazyWishlist, LazyPurchases, + LazyCheckoutComplete, LazyQueue, LazyDeveloper, LazyGear, @@ -81,6 +82,7 @@ export function getProtectedRoutes(): RouteEntry[] { { path: '/sell', element: wrapProtected( {}} />) }, { path: '/wishlist', element: wrapProtected() }, { path: '/purchases', element: wrapProtected() }, + { path: '/checkout/complete', element: wrapProtected() }, { path: '/chat', element: wrapProtected() }, { path: '/library', element: wrapProtected() }, { path: '/profile', element: wrapProtected() }, diff --git a/apps/web/src/services/marketplaceService.ts b/apps/web/src/services/marketplaceService.ts index 39728eca5..ee79e8752 100644 --- a/apps/web/src/services/marketplaceService.ts +++ b/apps/web/src/services/marketplaceService.ts @@ -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 => { const response = await apiClient.get<{