fix: thank you page 401 error
- Add public /checkout/order/{id} endpoint with order_key validation
- Update checkout redirect to include order_key parameter
- Update ThankYou page to use new public endpoint with key
- Support both guest (via key) and logged-in (via customer_id) access
This commit is contained in:
@@ -243,7 +243,7 @@ export default function Checkout() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Order placed successfully!');
|
toast.success('Order placed successfully!');
|
||||||
navigate(`/order-received/${data.order_id}`);
|
navigate(`/order-received/${data.order_id}?key=${data.order_key}`);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.error || 'Failed to create order');
|
throw new Error(data.error || 'Failed to create order');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 { useThankYouSettings } from '@/hooks/useAppearanceSettings';
|
||||||
import Container from '@/components/Layout/Container';
|
import Container from '@/components/Layout/Container';
|
||||||
import { CheckCircle, ShoppingBag, Package, Truck } from 'lucide-react';
|
import { CheckCircle, ShoppingBag, Package, Truck } from 'lucide-react';
|
||||||
@@ -9,17 +9,22 @@ import { apiClient } from '@/lib/api/client';
|
|||||||
|
|
||||||
export default function ThankYou() {
|
export default function ThankYou() {
|
||||||
const { orderId } = useParams<{ orderId: string }>();
|
const { orderId } = useParams<{ orderId: string }>();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const orderKey = searchParams.get('key');
|
||||||
const { template, headerVisibility, footerVisibility, backgroundColor, customMessage, elements, isLoading: settingsLoading } = useThankYouSettings();
|
const { template, headerVisibility, footerVisibility, backgroundColor, customMessage, elements, isLoading: settingsLoading } = useThankYouSettings();
|
||||||
const [order, setOrder] = useState<any>(null);
|
const [order, setOrder] = useState<any>(null);
|
||||||
const [relatedProducts, setRelatedProducts] = useState<any[]>([]);
|
const [relatedProducts, setRelatedProducts] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOrderData = async () => {
|
const fetchOrderData = async () => {
|
||||||
if (!orderId) return;
|
if (!orderId) return;
|
||||||
|
|
||||||
try {
|
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);
|
setOrder(orderData);
|
||||||
|
|
||||||
// Fetch related products from first order item
|
// Fetch related products from first order item
|
||||||
@@ -30,15 +35,16 @@ export default function ThankYou() {
|
|||||||
setRelatedProducts(productData.related_products.slice(0, 4));
|
setRelatedProducts(productData.related_products.slice(0, 4));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch order data:', error);
|
console.error('Failed to fetch order data:', err);
|
||||||
|
setError(err.message || 'Failed to load order');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchOrderData();
|
fetchOrderData();
|
||||||
}, [orderId]);
|
}, [orderId, orderKey]);
|
||||||
|
|
||||||
if (loading || settingsLoading || !order) {
|
if (loading || settingsLoading || !order) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ class CheckoutController {
|
|||||||
'callback' => [ new self(), 'get_fields' ],
|
'callback' => [ new self(), 'get_fields' ],
|
||||||
'permission_callback' => [ \WooNooW\Api\Permissions::class, 'anon_or_wp_nonce' ],
|
'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<id>\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:
|
* Submit an order:
|
||||||
* {
|
* {
|
||||||
|
|||||||
Reference in New Issue
Block a user