diff --git a/customer-spa/src/pages/Checkout/index.tsx b/customer-spa/src/pages/Checkout/index.tsx index 7a24f9a..cd7431a 100644 --- a/customer-spa/src/pages/Checkout/index.tsx +++ b/customer-spa/src/pages/Checkout/index.tsx @@ -74,7 +74,7 @@ export default function Checkout() { const [shipToDifferentAddress, setShipToDifferentAddress] = useState(false); const [orderNotes, setOrderNotes] = useState(''); const [paymentMethod, setPaymentMethod] = useState(isVirtualOnly ? 'bacs' : 'cod'); - + // Saved addresses const [savedAddresses, setSavedAddresses] = useState([]); const [selectedBillingAddressId, setSelectedBillingAddressId] = useState(null); @@ -92,15 +92,15 @@ export default function Checkout() { setLoadingAddresses(false); return; } - + try { const addresses = await api.get('/account/addresses'); setSavedAddresses(addresses); - + // Auto-select default addresses const defaultBilling = addresses.find(a => a.is_default && (a.type === 'billing' || a.type === 'both')); const defaultShipping = addresses.find(a => a.is_default && (a.type === 'shipping' || a.type === 'both')); - + if (defaultBilling) { setSelectedBillingAddressId(defaultBilling.id); fillBillingFromAddress(defaultBilling); @@ -117,10 +117,10 @@ export default function Checkout() { setLoadingAddresses(false); } }; - + loadAddresses(); }, [user, isVirtualOnly]); - + // Helper functions to fill forms from saved addresses const fillBillingFromAddress = (address: SavedAddress) => { setBillingData({ @@ -135,7 +135,7 @@ export default function Checkout() { country: address.country, }); }; - + const fillShippingFromAddress = (address: SavedAddress) => { setShippingData({ firstName: address.first_name, @@ -147,13 +147,13 @@ export default function Checkout() { country: address.country, }); }; - + const handleSelectBillingAddress = (address: SavedAddress) => { setSelectedBillingAddressId(address.id); fillBillingFromAddress(address); setShowBillingForm(false); // Hide form when address is selected }; - + const handleSelectShippingAddress = (address: SavedAddress) => { setSelectedShippingAddressId(address.id); fillShippingFromAddress(address); @@ -235,15 +235,15 @@ export default function Checkout() { // Submit order const response = await apiClient.post('/checkout/submit', orderData); const data = (response as any).data || response; - + if (data.ok && data.order_id) { // Clear cart cart.items.forEach(item => { useCartStore.getState().removeItem(item.key); }); - + toast.success('Order placed successfully!'); - navigate(`/order-received/${data.order_id}`); + navigate(`/order-received/${data.order_id}?key=${data.order_key}`); } else { throw new Error(data.error || 'Failed to create order'); } @@ -307,7 +307,7 @@ export default function Checkout() { Change Address - + {selectedBillingAddressId ? ( (() => { const selected = savedAddresses.find(a => a.id === selectedBillingAddressId); @@ -344,7 +344,7 @@ export default function Checkout() { )} )} - + {/* Billing Address Modal */} - + {/* Billing Details Form - Only show if no saved address selected or user wants to enter manually */} {(savedAddresses.length === 0 || !selectedBillingAddressId || showBillingForm) && ( -
-

Billing Details

-
-
- - setBillingData({ ...billingData, firstName: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, lastName: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, email: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, phone: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
- - {/* Address fields - only for physical products */} - {!isVirtualOnly && ( - <> -
- - setBillingData({ ...billingData, address: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, city: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, state: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, postcode: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, country: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
- - )} -
-
- )} - - {/* Ship to Different Address - only for physical products */} - {!isVirtualOnly && ( -
- - - {shipToDifferentAddress && ( - <> - {/* Selected Shipping Address Summary */} - {!loadingAddresses && savedAddresses.length > 0 && savedAddresses.some(a => a.type === 'shipping' || a.type === 'both') && ( -
-
-

- - Shipping Address -

- -
- - {selectedShippingAddressId ? ( - (() => { - const selected = savedAddresses.find(a => a.id === selectedShippingAddressId); - return selected ? ( -
-
-
-

{selected.label}

- {selected.is_default && ( - Default - )} -
-

{selected.first_name} {selected.last_name}

- {selected.phone &&

{selected.phone}

} -

{selected.address_1}

- {selected.address_2 &&

{selected.address_2}

} -

{selected.city}, {selected.state} {selected.postcode}

-

{selected.country}

-
- -
- ) : null; - })() - ) : ( -

No address selected

- )} -
- )} - - {/* Shipping Address Modal */} - setShowShippingModal(false)} - addresses={savedAddresses} - selectedAddressId={selectedShippingAddressId} - onSelectAddress={handleSelectShippingAddress} - type="shipping" - /> - - {/* Shipping Form - Only show if no saved address selected or user wants to enter manually */} - {(!selectedShippingAddressId || showShippingForm) && ( -
+
+

Billing Details

+
setShippingData({ ...shippingData, firstName: e.target.value })} + value={billingData.firstName} + onChange={(e) => setBillingData({ ...billingData, firstName: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
@@ -560,66 +375,251 @@ export default function Checkout() { setShippingData({ ...shippingData, lastName: e.target.value })} + value={billingData.lastName} + onChange={(e) => setBillingData({ ...billingData, lastName: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
- + setShippingData({ ...shippingData, address: e.target.value })} + value={billingData.email} + onChange={(e) => setBillingData({ ...billingData, email: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
-
- +
+ setShippingData({ ...shippingData, city: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setShippingData({ ...shippingData, state: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setShippingData({ ...shippingData, postcode: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setShippingData({ ...shippingData, country: e.target.value })} + value={billingData.phone} + onChange={(e) => setBillingData({ ...billingData, phone: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
+ + {/* Address fields - only for physical products */} + {!isVirtualOnly && ( + <> +
+ + setBillingData({ ...billingData, address: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setBillingData({ ...billingData, city: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setBillingData({ ...billingData, state: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setBillingData({ ...billingData, postcode: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setBillingData({ ...billingData, country: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+ + )}
+
+ )} + + {/* Ship to Different Address - only for physical products */} + {!isVirtualOnly && ( +
+ + + {shipToDifferentAddress && ( + <> + {/* Selected Shipping Address Summary */} + {!loadingAddresses && savedAddresses.length > 0 && savedAddresses.some(a => a.type === 'shipping' || a.type === 'both') && ( +
+
+

+ + Shipping Address +

+ +
+ + {selectedShippingAddressId ? ( + (() => { + const selected = savedAddresses.find(a => a.id === selectedShippingAddressId); + return selected ? ( +
+
+
+

{selected.label}

+ {selected.is_default && ( + Default + )} +
+

{selected.first_name} {selected.last_name}

+ {selected.phone &&

{selected.phone}

} +

{selected.address_1}

+ {selected.address_2 &&

{selected.address_2}

} +

{selected.city}, {selected.state} {selected.postcode}

+

{selected.country}

+
+ +
+ ) : null; + })() + ) : ( +

No address selected

+ )} +
+ )} + + {/* Shipping Address Modal */} + setShowShippingModal(false)} + addresses={savedAddresses} + selectedAddressId={selectedShippingAddressId} + onSelectAddress={handleSelectShippingAddress} + type="shipping" + /> + + {/* Shipping Form - Only show if no saved address selected or user wants to enter manually */} + {(!selectedShippingAddressId || showShippingForm) && ( +
+
+ + setShippingData({ ...shippingData, firstName: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setShippingData({ ...shippingData, lastName: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setShippingData({ ...shippingData, address: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setShippingData({ ...shippingData, city: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setShippingData({ ...shippingData, state: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setShippingData({ ...shippingData, postcode: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ + setShippingData({ ...shippingData, country: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+
+ )} + )} - - )} -
+
)} {/* Order Notes */} @@ -722,30 +722,30 @@ export default function Checkout() { {/* Hide COD for virtual-only products */} {!isVirtualOnly && ( )}
- + {/* Payment Icons */} {elements.payment_icons && (
diff --git a/customer-spa/src/pages/ThankYou/index.tsx b/customer-spa/src/pages/ThankYou/index.tsx index e0cd83b..529f8e8 100644 --- a/customer-spa/src/pages/ThankYou/index.tsx +++ b/customer-spa/src/pages/ThankYou/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useSearchParams } from 'react-router-dom'; import { useThankYouSettings } from '@/hooks/useAppearanceSettings'; import Container from '@/components/Layout/Container'; import { CheckCircle, ShoppingBag, Package, Truck } from 'lucide-react'; @@ -9,17 +9,22 @@ 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(null); const [relatedProducts, setRelatedProducts] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { const fetchOrderData = async () => { if (!orderId) return; - + try { - const orderData = await apiClient.get(`/orders/${orderId}`) as any; + // 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 @@ -30,15 +35,16 @@ export default function ThankYou() { setRelatedProducts(productData.related_products.slice(0, 4)); } } - } catch (error) { - console.error('Failed to fetch order data:', error); + } catch (err: any) { + console.error('Failed to fetch order data:', err); + setError(err.message || 'Failed to load order'); } finally { setLoading(false); } }; fetchOrderData(); - }, [orderId]); + }, [orderId, orderKey]); if (loading || settingsLoading || !order) { return ( @@ -68,55 +74,154 @@ export default function ThankYou() { return (
-
- {/* Receipt Container */} -
- {/* Receipt Header */} -
-
- -
-

PAYMENT RECEIPT

-

Order #{order.number}

-

- {new Date().toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} -

-
- - {/* Custom Message */} -
-

{customMessage}

-
- - {/* Order Items */} - {elements.order_details && ( -
-
-
- ITEM - AMOUNT -
+
+ {/* Receipt Container */} +
+ {/* Receipt Header */} +
+
+
+

PAYMENT RECEIPT

+

Order #{order.number}

+

+ {new Date().toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +

+
-
- {order.items.map((item: any) => ( -
-
-
-
{item.name}
-
Qty: {item.qty}
-
-
- {formatPrice(item.total)} + {/* Custom Message */} +
+

{customMessage}

+
+ + {/* Order Items */} + {elements.order_details && ( +
+
+
+ ITEM + AMOUNT +
+
+ +
+ {order.items.map((item: any) => ( +
+
+
+
{item.name}
+
Qty: {item.qty}
+
+
+ {formatPrice(item.total)} +
+ ))} +
+ + {/* Totals */} +
+
+ SUBTOTAL: + {formatPrice(parseFloat(order.subtotal || 0))}
+ {parseFloat(order.shipping_total || 0) > 0 && ( +
+ SHIPPING: + {formatPrice(parseFloat(order.shipping_total))} +
+ )} + {parseFloat(order.tax_total || 0) > 0 && ( +
+ TAX: + {formatPrice(parseFloat(order.tax_total))} +
+ )} +
+ TOTAL: + {formatPrice(parseFloat(order.total || 0))} +
+
+ + {/* Payment & Status Info */} +
+
+ Payment Method: + {order.payment_method || 'N/A'} +
+
+ Status: + {getStatusLabel(order.status)} +
+
+ + {/* Customer Info */} +
+
Bill To:
+
+
+ {order.billing?.first_name} {order.billing?.last_name} +
+
{order.billing?.email}
+ {order.billing?.phone && ( +
{order.billing.phone}
+ )} +
+
+
+ )} + + {/* Receipt Footer */} +
+

+ {order.status === 'pending' + ? 'Awaiting payment confirmation' + : 'Thank you for your business!'} +

+ + {elements.continue_shopping_button && ( + + + + )} +
+
+ + {/* Related Products */} + {elements.related_products && relatedProducts.length > 0 && ( +
+

You May Also Like

+
+ {relatedProducts.map((product: any) => ( + +
+
+ {product.image ? ( + {product.name} + ) : ( + + )} +
+
+

+ {product.name} +

+

+ {formatPrice(parseFloat(product.price || 0))} +

+
+
+ ))}
@@ -175,11 +280,11 @@ export default function ThankYou() { {/* Receipt Footer */}

- {order.status === 'pending' - ? 'Awaiting payment confirmation' + {order.status === 'pending' + ? 'Awaiting payment confirmation' : 'Thank you for your business!'}

- + {elements.continue_shopping_button && (
- {/* Related Products */} - {elements.related_products && relatedProducts.length > 0 && ( -
-

You May Also Like

-
- {relatedProducts.map((product: any) => ( - -
-
- {product.image ? ( - {product.name} - ) : ( - - )} -
-
-

- {product.name} -

-

- {formatPrice(parseFloat(product.price || 0))} -

-
-
- - ))} -
- - {/* Totals */} -
-
- SUBTOTAL: - {formatPrice(parseFloat(order.subtotal || 0))} -
- {parseFloat(order.shipping_total || 0) > 0 && ( -
- SHIPPING: - {formatPrice(parseFloat(order.shipping_total))} -
- )} - {parseFloat(order.tax_total || 0) > 0 && ( -
- TAX: - {formatPrice(parseFloat(order.tax_total))} -
- )} -
- TOTAL: - {formatPrice(parseFloat(order.total || 0))} -
-
- - {/* Payment & Status Info */} -
-
- Payment Method: - {order.payment_method || 'N/A'} -
-
- Status: - {getStatusLabel(order.status)} -
-
- - {/* Customer Info */} -
-
Bill To:
-
-
- {order.billing?.first_name} {order.billing?.last_name} -
-
{order.billing?.email}
- {order.billing?.phone && ( -
{order.billing.phone}
- )} -
-
-
- )} - - {/* Receipt Footer */} -
-

- {order.status === 'pending' - ? 'Awaiting payment confirmation' - : 'Thank you for your business!'} -

- - {elements.continue_shopping_button && ( - - - - )} -
-
- {/* Related Products */} {elements.related_products && relatedProducts.length > 0 && (
@@ -328,144 +334,144 @@ export default function ThankYou() { return (
-
- {/* Success Header */} -
-
- +
+ {/* Success Header */} +
+
+ +
+

Order Confirmed!

+

Order #{order.number}

-

Order Confirmed!

-

Order #{order.number}

-
- {/* Custom Message */} -
-

{customMessage}

-
- - {/* Order Details */} - {elements.order_details && ( -
-

Order Details

- - {/* Order Items */} -
- {order.items.map((item: any) => ( -
-
- {item.image && typeof item.image === 'string' ? ( - {item.name} - ) : ( - - )} -
-
-

{item.name}

-

Quantity: {item.qty}

-
-
-

{formatPrice(item.total)}

-
-
- ))} -
- - {/* Order Summary */} -
-
- Subtotal - {formatPrice(parseFloat(order.subtotal || 0))} -
- {parseFloat(order.shipping_total || 0) > 0 && ( -
- Shipping - {formatPrice(parseFloat(order.shipping_total))} -
- )} - {parseFloat(order.tax_total || 0) > 0 && ( -
- Tax - {formatPrice(parseFloat(order.tax_total))} -
- )} -
- Total - {formatPrice(parseFloat(order.total || 0))} -
-
- - {/* Customer Info */} -
-

Customer Information

-
-
-

Email

-

{order.billing?.email || 'N/A'}

-
-
-

Phone

-

{order.billing?.phone || 'N/A'}

-
-
-
- - {/* Order Status */} -
-
- -
-

Order Status: {getStatusLabel(order.status)}

-

- {order.status === 'pending' ? 'Awaiting payment confirmation' : "We'll send you shipping updates via email"} -

-
-
-
+ {/* Custom Message */} +
+

{customMessage}

- )} - {/* Continue Shopping Button */} - {elements.continue_shopping_button && ( -
- - - -
- )} + {/* Order Details */} + {elements.order_details && ( +
+

Order Details

- {/* Related Products */} - {elements.related_products && relatedProducts.length > 0 && ( -
-

You May Also Like

-
- {relatedProducts.map((product: any) => ( - -
-
- {product.image ? ( - {product.name} + {/* Order Items */} +
+ {order.items.map((item: any) => ( +
+
+ {item.image && typeof item.image === 'string' ? ( + {item.name} ) : ( - + )}
-
-

- {product.name} -

-

- {formatPrice(parseFloat(product.price || 0))} -

+
+

{item.name}

+

Quantity: {item.qty}

+
+
+

{formatPrice(item.total)}

- - ))} + ))} +
+ + {/* Order Summary */} +
+
+ Subtotal + {formatPrice(parseFloat(order.subtotal || 0))} +
+ {parseFloat(order.shipping_total || 0) > 0 && ( +
+ Shipping + {formatPrice(parseFloat(order.shipping_total))} +
+ )} + {parseFloat(order.tax_total || 0) > 0 && ( +
+ Tax + {formatPrice(parseFloat(order.tax_total))} +
+ )} +
+ Total + {formatPrice(parseFloat(order.total || 0))} +
+
+ + {/* Customer Info */} +
+

Customer Information

+
+
+

Email

+

{order.billing?.email || 'N/A'}

+
+
+

Phone

+

{order.billing?.phone || 'N/A'}

+
+
+
+ + {/* Order Status */} +
+
+ +
+

Order Status: {getStatusLabel(order.status)}

+

+ {order.status === 'pending' ? 'Awaiting payment confirmation' : "We'll send you shipping updates via email"} +

+
+
+
-
- )} -
+ )} + + {/* Continue Shopping Button */} + {elements.continue_shopping_button && ( +
+ + + +
+ )} + + {/* Related Products */} + {elements.related_products && relatedProducts.length > 0 && ( +
+

You May Also Like

+
+ {relatedProducts.map((product: any) => ( + +
+
+ {product.image ? ( + {product.name} + ) : ( + + )} +
+
+

+ {product.name} +

+

+ {formatPrice(parseFloat(product.price || 0))} +

+
+
+ + ))} +
+
+ )} +
); diff --git a/includes/Api/CheckoutController.php b/includes/Api/CheckoutController.php index 40d413e..96e4df1 100644 --- a/includes/Api/CheckoutController.php +++ b/includes/Api/CheckoutController.php @@ -32,6 +32,18 @@ class CheckoutController { 'callback' => [ new self(), 'get_fields' ], 'permission_callback' => [ \WooNooW\Api\Permissions::class, 'anon_or_wp_nonce' ], ]); + // Public order view endpoint for thank you page + register_rest_route($namespace, '/checkout/order/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [ new self(), 'get_order' ], + 'permission_callback' => '__return_true', // Public, validated via order_key + 'args' => [ + 'key' => [ + 'type' => 'string', + 'required' => false, + ], + ], + ]); } /** @@ -133,6 +145,69 @@ class CheckoutController { ]; } + /** + * Public order view endpoint for thank you page + * Validates access via order_key (for guests) or logged-in customer ID + * GET /checkout/order/{id}?key=wc_order_xxx + */ + public function get_order(WP_REST_Request $r): array { + $order_id = absint($r['id']); + $order_key = sanitize_text_field($r->get_param('key') ?? ''); + + if (!$order_id) { + return ['error' => __('Invalid order ID', 'woonoow')]; + } + + $order = wc_get_order($order_id); + if (!$order) { + return ['error' => __('Order not found', 'woonoow')]; + } + + // Validate access: order_key must match OR user must be logged in and own the order + $valid_key = $order_key && hash_equals($order->get_order_key(), $order_key); + $valid_owner = is_user_logged_in() && get_current_user_id() === $order->get_customer_id(); + + if (!$valid_key && !$valid_owner) { + return ['error' => __('Unauthorized access to order', 'woonoow')]; + } + + // Build order items + $items = []; + foreach ($order->get_items() as $item) { + $product = $item->get_product(); + $items[] = [ + 'id' => $item->get_id(), + 'product_id' => $product ? $product->get_id() : 0, + 'name' => $item->get_name(), + 'qty' => (int) $item->get_quantity(), + 'price' => (float) $item->get_total() / max(1, $item->get_quantity()), + 'total' => (float) $item->get_total(), + 'image' => $product ? wp_get_attachment_image_url($product->get_image_id(), 'thumbnail') : null, + ]; + } + + return [ + 'ok' => true, + 'id' => $order->get_id(), + 'number' => $order->get_order_number(), + 'status' => $order->get_status(), + 'subtotal' => (float) $order->get_subtotal(), + 'shipping_total' => (float) $order->get_shipping_total(), + 'tax_total' => (float) $order->get_total_tax(), + 'total' => (float) $order->get_total(), + 'currency' => $order->get_currency(), + 'currency_symbol' => get_woocommerce_currency_symbol($order->get_currency()), + 'payment_method' => $order->get_payment_method_title(), + 'billing' => [ + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ], + 'items' => $items, + ]; + } + /** * Submit an order: * {