1. ThankYou page - Go to Account button: - Added for logged-in users (next to Continue Shopping) - Shows in both receipt and basic templates - Uses outline variant with User icon 2. Wishlist merge on login: - Reads guest wishlist from localStorage (woonoow_guest_wishlist) - POSTs each product to /account/wishlist API - Handles duplicates gracefully (skips on error) - Clears localStorage after successful merge
498 lines
22 KiB
TypeScript
498 lines
22 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, Link, useSearchParams } from 'react-router-dom';
|
|
import { useThankYouSettings } from '@/hooks/useAppearanceSettings';
|
|
import Container from '@/components/Layout/Container';
|
|
import { CheckCircle, ShoppingBag, Package, Truck, User } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { formatPrice } from '@/lib/currency';
|
|
import { apiClient } from '@/lib/api/client';
|
|
|
|
export default function ThankYou() {
|
|
const { orderId } = useParams<{ orderId: string }>();
|
|
const [searchParams] = useSearchParams();
|
|
const orderKey = searchParams.get('key');
|
|
const { template, headerVisibility, footerVisibility, backgroundColor, customMessage, elements, isLoading: settingsLoading } = useThankYouSettings();
|
|
const [order, setOrder] = useState<any>(null);
|
|
const [relatedProducts, setRelatedProducts] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const isLoggedIn = (window as any).woonoowCustomer?.user?.isLoggedIn;
|
|
|
|
useEffect(() => {
|
|
const fetchOrderData = async () => {
|
|
if (!orderId) return;
|
|
|
|
try {
|
|
// Use public order endpoint with key validation
|
|
const keyParam = orderKey ? `?key=${orderKey}` : '';
|
|
const orderData = await apiClient.get(`/checkout/order/${orderId}${keyParam}`) as any;
|
|
setOrder(orderData);
|
|
|
|
// Fetch related products from first order item
|
|
if (orderData.items && orderData.items.length > 0) {
|
|
const firstProductId = orderData.items[0].product_id;
|
|
const productData = await apiClient.get(`/shop/products/${firstProductId}`) as any;
|
|
if (productData.related_products && productData.related_products.length > 0) {
|
|
setRelatedProducts(productData.related_products.slice(0, 4));
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Failed to fetch order data:', err);
|
|
setError(err.message || 'Failed to load order');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchOrderData();
|
|
}, [orderId, orderKey]);
|
|
|
|
if (loading || settingsLoading || !order) {
|
|
return (
|
|
<Container>
|
|
<div className="py-20 text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mx-auto"></div>
|
|
</div>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
const getStatusLabel = (status: string) => {
|
|
const statusMap: Record<string, string> = {
|
|
'pending': 'Pending Payment',
|
|
'processing': 'Processing',
|
|
'on-hold': 'On Hold',
|
|
'completed': 'Completed',
|
|
'cancelled': 'Cancelled',
|
|
'refunded': 'Refunded',
|
|
'failed': 'Failed',
|
|
};
|
|
return statusMap[status] || status.charAt(0).toUpperCase() + status.slice(1);
|
|
};
|
|
|
|
// Render receipt style template
|
|
if (template === 'receipt') {
|
|
return (
|
|
<div style={{ backgroundColor }}>
|
|
<Container>
|
|
<div className="py-12 max-w-2xl mx-auto">
|
|
{/* Receipt Container */}
|
|
<div className="bg-white shadow-lg" style={{ fontFamily: 'monospace' }}>
|
|
{/* Receipt Header */}
|
|
<div className="border-b-2 border-dashed border-gray-400 p-8 text-center">
|
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-4">
|
|
<CheckCircle className="w-10 h-10 text-green-600" />
|
|
</div>
|
|
<h1 className="text-2xl font-bold mb-2">PAYMENT RECEIPT</h1>
|
|
<p className="text-gray-600">Order #{order.number}</p>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
{new Date().toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Custom Message */}
|
|
<div className="px-8 py-4 bg-gray-50 border-b border-dashed border-gray-300">
|
|
<p className="text-sm text-center text-gray-700">{customMessage}</p>
|
|
</div>
|
|
|
|
{/* Order Items */}
|
|
{elements.order_details && (
|
|
<div className="p-8">
|
|
<div className="border-b-2 border-gray-900 pb-2 mb-4">
|
|
<div className="flex justify-between text-sm font-bold">
|
|
<span>ITEM</span>
|
|
<span>AMOUNT</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{order.items.map((item: any) => (
|
|
<div key={item.id}>
|
|
<div className="flex justify-between">
|
|
<div className="flex-1">
|
|
<div className="font-medium">{item.name}</div>
|
|
<div className="text-sm text-gray-600">Qty: {item.qty}</div>
|
|
</div>
|
|
<div className="text-right font-mono">
|
|
{formatPrice(item.total)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Totals */}
|
|
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span>SUBTOTAL:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.subtotal || 0))}</span>
|
|
</div>
|
|
{parseFloat(order.shipping_total || 0) > 0 && (
|
|
<div className="flex justify-between text-sm">
|
|
<span>SHIPPING:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.shipping_total))}</span>
|
|
</div>
|
|
)}
|
|
{parseFloat(order.tax_total || 0) > 0 && (
|
|
<div className="flex justify-between text-sm">
|
|
<span>TAX:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.tax_total))}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between text-lg font-bold border-t-2 border-gray-900 pt-2 mt-2">
|
|
<span>TOTAL:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.total || 0))}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Payment & Status Info */}
|
|
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Payment Method:</span>
|
|
<span className="font-medium uppercase">{order.payment_method || 'N/A'}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Status:</span>
|
|
<span className="font-medium uppercase">{getStatusLabel(order.status)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customer Info */}
|
|
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4">
|
|
<div className="text-xs text-gray-600 uppercase mb-2">Bill To:</div>
|
|
<div className="text-sm">
|
|
<div className="font-medium">
|
|
{order.billing?.first_name} {order.billing?.last_name}
|
|
</div>
|
|
<div className="text-gray-600">{order.billing?.email}</div>
|
|
{order.billing?.phone && (
|
|
<div className="text-gray-600">{order.billing.phone}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Receipt Footer */}
|
|
<div className="border-t-2 border-dashed border-gray-400 p-8 text-center bg-gray-50">
|
|
<p className="text-sm text-gray-600 mb-4">
|
|
{order.status === 'pending'
|
|
? 'Awaiting payment confirmation'
|
|
: 'Thank you for your business!'}
|
|
</p>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
|
{elements.continue_shopping_button && (
|
|
<Link to="/shop">
|
|
<Button size="lg" className="gap-2">
|
|
<ShoppingBag className="w-5 h-5" />
|
|
Continue Shopping
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
{isLoggedIn && (
|
|
<Link to="/my-account">
|
|
<Button size="lg" variant="outline" className="gap-2">
|
|
<User className="w-5 h-5" />
|
|
Go to Account
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Related Products */}
|
|
{elements.related_products && relatedProducts.length > 0 && (
|
|
<div className="mt-12">
|
|
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{relatedProducts.map((product: any) => (
|
|
<Link key={product.id} to={`/product/${product.slug}`} className="group no-underline">
|
|
<div className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
|
|
<div className="aspect-square bg-gray-100 flex items-center justify-center">
|
|
{product.image ? (
|
|
<img src={product.image} alt={product.name} className="w-full !h-full object-cover" />
|
|
) : (
|
|
<Package className="w-12 h-12 text-gray-400" />
|
|
)}
|
|
</div>
|
|
<div className="p-3">
|
|
<h3 className="font-medium text-sm text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2">
|
|
{product.name}
|
|
</h3>
|
|
<p className="text-sm font-bold text-gray-900 mt-1">
|
|
{formatPrice(parseFloat(product.price || 0))}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* Totals */}
|
|
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span>SUBTOTAL:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.subtotal || 0))}</span>
|
|
</div>
|
|
{parseFloat(order.shipping_total || 0) > 0 && (
|
|
<div className="flex justify-between text-sm">
|
|
<span>SHIPPING:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.shipping_total))}</span>
|
|
</div>
|
|
)}
|
|
{parseFloat(order.tax_total || 0) > 0 && (
|
|
<div className="flex justify-between text-sm">
|
|
<span>TAX:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.tax_total))}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between text-lg font-bold border-t-2 border-gray-900 pt-2 mt-2">
|
|
<span>TOTAL:</span>
|
|
<span className="font-mono">{formatPrice(parseFloat(order.total || 0))}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Payment & Status Info */}
|
|
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4 space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Payment Method:</span>
|
|
<span className="font-medium uppercase">{order.payment_method || 'N/A'}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Status:</span>
|
|
<span className="font-medium uppercase">{getStatusLabel(order.status)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customer Info */}
|
|
<div className="border-t-2 border-dashed border-gray-400 mt-6 pt-4">
|
|
<div className="text-xs text-gray-600 uppercase mb-2">Bill To:</div>
|
|
<div className="text-sm">
|
|
<div className="font-medium">
|
|
{order.billing?.first_name} {order.billing?.last_name}
|
|
</div>
|
|
<div className="text-gray-600">{order.billing?.email}</div>
|
|
{order.billing?.phone && (
|
|
<div className="text-gray-600">{order.billing.phone}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Receipt Footer */}
|
|
<div className="border-t-2 border-dashed border-gray-400 p-8 text-center bg-gray-50">
|
|
<p className="text-sm text-gray-600 mb-4">
|
|
{order.status === 'pending'
|
|
? 'Awaiting payment confirmation'
|
|
: 'Thank you for your business!'}
|
|
</p>
|
|
|
|
{elements.continue_shopping_button && (
|
|
<Link to="/shop">
|
|
<Button size="lg" className="gap-2">
|
|
<ShoppingBag className="w-5 h-5" />
|
|
Continue Shopping
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Related Products */}
|
|
{elements.related_products && relatedProducts.length > 0 && (
|
|
<div className="mt-12">
|
|
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{relatedProducts.map((product: any) => (
|
|
<Link key={product.id} to={`/product/${product.slug}`} className="group no-underline">
|
|
<div className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
|
|
<div className="aspect-square bg-gray-100 flex items-center justify-center">
|
|
{product.image ? (
|
|
<img src={product.image} alt={product.name} className="w-full !h-full object-cover" />
|
|
) : (
|
|
<Package className="w-12 h-12 text-gray-400" />
|
|
)}
|
|
</div>
|
|
<div className="p-3">
|
|
<h3 className="font-medium text-sm text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2">
|
|
{product.name}
|
|
</h3>
|
|
<p className="text-sm font-bold text-gray-900 mt-1">
|
|
{formatPrice(parseFloat(product.price || 0))}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Container>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Render basic style template (default)
|
|
return (
|
|
<div style={{ backgroundColor }}>
|
|
<Container>
|
|
<div className="py-12 max-w-3xl mx-auto">
|
|
{/* Success Header */}
|
|
<div className="text-center mb-8">
|
|
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4">
|
|
<CheckCircle className="w-8 h-8 text-green-600" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Order Confirmed!</h1>
|
|
<p className="text-gray-600">Order #{order.number}</p>
|
|
</div>
|
|
|
|
{/* Custom Message */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
|
|
<p className="text-gray-800 text-center">{customMessage}</p>
|
|
</div>
|
|
|
|
{/* Order Details */}
|
|
{elements.order_details && (
|
|
<div className="bg-white border rounded-lg p-6 mb-6">
|
|
<h2 className="text-xl font-bold mb-4">Order Details</h2>
|
|
|
|
{/* Order Items */}
|
|
<div className="space-y-4 mb-6">
|
|
{order.items.map((item: any) => (
|
|
<div key={item.id} className="flex items-center gap-4 pb-4 border-b last:border-0">
|
|
<div className="w-16 h-16 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
{item.image && typeof item.image === 'string' ? (
|
|
<img src={item.image} alt={item.name} className="w-full !h-full object-cover rounded-lg" />
|
|
) : (
|
|
<Package className="w-8 h-8 text-gray-400" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<h3 className="font-medium text-gray-900">{item.name}</h3>
|
|
<p className="text-sm text-gray-500">Quantity: {item.qty}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-medium text-gray-900">{formatPrice(item.total)}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Order Summary */}
|
|
<div className="border-t pt-4 space-y-2">
|
|
<div className="flex justify-between text-gray-600">
|
|
<span>Subtotal</span>
|
|
<span>{formatPrice(parseFloat(order.subtotal || 0))}</span>
|
|
</div>
|
|
{parseFloat(order.shipping_total || 0) > 0 && (
|
|
<div className="flex justify-between text-gray-600">
|
|
<span>Shipping</span>
|
|
<span>{formatPrice(parseFloat(order.shipping_total))}</span>
|
|
</div>
|
|
)}
|
|
{parseFloat(order.tax_total || 0) > 0 && (
|
|
<div className="flex justify-between text-gray-600">
|
|
<span>Tax</span>
|
|
<span>{formatPrice(parseFloat(order.tax_total))}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between font-bold text-lg text-gray-900 pt-2 border-t">
|
|
<span>Total</span>
|
|
<span>{formatPrice(parseFloat(order.total || 0))}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customer Info */}
|
|
<div className="mt-6 pt-6 border-t">
|
|
<h3 className="font-medium text-gray-900 mb-3">Customer Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-gray-500 mb-1">Email</p>
|
|
<p className="text-gray-900">{order.billing?.email || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 mb-1">Phone</p>
|
|
<p className="text-gray-900">{order.billing?.phone || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order Status */}
|
|
<div className="mt-6 pt-6 border-t">
|
|
<div className="flex items-center gap-3">
|
|
<Truck className="w-5 h-5 text-blue-600" />
|
|
<div>
|
|
<p className="font-medium text-gray-900">Order Status: {getStatusLabel(order.status)}</p>
|
|
<p className="text-sm text-gray-500">
|
|
{order.status === 'pending' ? 'Awaiting payment confirmation' : "We'll send you shipping updates via email"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="text-center flex flex-col sm:flex-row gap-3 justify-center">
|
|
{elements.continue_shopping_button && (
|
|
<Link to="/shop">
|
|
<Button size="lg" className="gap-2">
|
|
<ShoppingBag className="w-5 h-5" />
|
|
Continue Shopping
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
{isLoggedIn && (
|
|
<Link to="/my-account">
|
|
<Button size="lg" variant="outline" className="gap-2">
|
|
<User className="w-5 h-5" />
|
|
Go to Account
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{/* Related Products */}
|
|
{elements.related_products && relatedProducts.length > 0 && (
|
|
<div className="mt-12">
|
|
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{relatedProducts.map((product: any) => (
|
|
<Link key={product.id} to={`/product/${product.slug}`} className="group no-underline">
|
|
<div className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
|
|
<div className="aspect-square bg-gray-100 flex items-center justify-center">
|
|
{product.image ? (
|
|
<img src={product.image} alt={product.name} className="w-full !h-full object-cover" />
|
|
) : (
|
|
<Package className="w-12 h-12 text-gray-400" />
|
|
)}
|
|
</div>
|
|
<div className="p-3">
|
|
<h3 className="font-medium text-sm text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2">
|
|
{product.name}
|
|
</h3>
|
|
<p className="text-sm font-bold text-gray-900 mt-1">
|
|
{formatPrice(parseFloat(product.price || 0))}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Container>
|
|
</div>
|
|
);
|
|
}
|