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:
Dwindi Ramadhana
2025-12-31 21:42:40 +07:00
parent d7505252ac
commit a87357d890
3 changed files with 620 additions and 539 deletions

View File

@@ -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<SavedAddress[]>([]);
const [selectedBillingAddressId, setSelectedBillingAddressId] = useState<number | null>(null);
@@ -92,15 +92,15 @@ export default function Checkout() {
setLoadingAddresses(false);
return;
}
try {
const addresses = await api.get<SavedAddress[]>('/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
</Button>
</div>
{selectedBillingAddressId ? (
(() => {
const selected = savedAddresses.find(a => a.id === selectedBillingAddressId);
@@ -344,7 +344,7 @@ export default function Checkout() {
)}
</div>
)}
{/* Billing Address Modal */}
<AddressSelector
isOpen={showBillingModal}
@@ -354,204 +354,19 @@ export default function Checkout() {
onSelectAddress={handleSelectBillingAddress}
type="billing"
/>
{/* Billing Details Form - Only show if no saved address selected or user wants to enter manually */}
{(savedAddresses.length === 0 || !selectedBillingAddressId || showBillingForm) && (
<div className="bg-white border rounded-lg p-6">
<h2 className="text-xl font-bold mb-4">Billing Details</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">First Name *</label>
<input
type="text"
required
value={billingData.firstName}
onChange={(e) => setBillingData({ ...billingData, firstName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Last Name *</label>
<input
type="text"
required
value={billingData.lastName}
onChange={(e) => setBillingData({ ...billingData, lastName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Email Address *</label>
<input
type="email"
required
value={billingData.email}
onChange={(e) => setBillingData({ ...billingData, email: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Phone *</label>
<input
type="tel"
required
value={billingData.phone}
onChange={(e) => setBillingData({ ...billingData, phone: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
{/* Address fields - only for physical products */}
{!isVirtualOnly && (
<>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Street Address *</label>
<input
type="text"
required
value={billingData.address}
onChange={(e) => setBillingData({ ...billingData, address: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">City *</label>
<input
type="text"
required
value={billingData.city}
onChange={(e) => setBillingData({ ...billingData, city: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">State / Province *</label>
<input
type="text"
required
value={billingData.state}
onChange={(e) => setBillingData({ ...billingData, state: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>
<input
type="text"
required
value={billingData.postcode}
onChange={(e) => setBillingData({ ...billingData, postcode: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Country *</label>
<input
type="text"
required
value={billingData.country}
onChange={(e) => setBillingData({ ...billingData, country: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
</>
)}
</div>
</div>
)}
{/* Ship to Different Address - only for physical products */}
{!isVirtualOnly && (
<div className="bg-white border rounded-lg p-6">
<label className="flex items-center gap-2 mb-4">
<input
type="checkbox"
checked={shipToDifferentAddress}
onChange={(e) => setShipToDifferentAddress(e.target.checked)}
className="w-4 h-4"
/>
<span className="font-medium">Ship to a different address?</span>
</label>
{shipToDifferentAddress && (
<>
{/* Selected Shipping Address Summary */}
{!loadingAddresses && savedAddresses.length > 0 && savedAddresses.some(a => a.type === 'shipping' || a.type === 'both') && (
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<MapPin className="w-4 h-4" />
Shipping Address
</h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowShippingModal(true)}
className="flex items-center gap-2"
>
<Edit2 className="w-4 h-4" />
Change Address
</Button>
</div>
{selectedShippingAddressId ? (
(() => {
const selected = savedAddresses.find(a => a.id === selectedShippingAddressId);
return selected ? (
<div>
<div className="bg-primary/5 border-2 border-primary rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<p className="font-semibold">{selected.label}</p>
{selected.is_default && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Default</span>
)}
</div>
<p className="text-sm font-medium text-gray-900">{selected.first_name} {selected.last_name}</p>
{selected.phone && <p className="text-sm text-gray-600">{selected.phone}</p>}
<p className="text-sm text-gray-600 mt-2">{selected.address_1}</p>
{selected.address_2 && <p className="text-sm text-gray-600">{selected.address_2}</p>}
<p className="text-sm text-gray-600">{selected.city}, {selected.state} {selected.postcode}</p>
<p className="text-sm text-gray-600">{selected.country}</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowShippingForm(true)}
className="mt-3 text-primary hover:text-primary"
>
Use a different address
</Button>
</div>
) : null;
})()
) : (
<p className="text-gray-500 text-sm">No address selected</p>
)}
</div>
)}
{/* Shipping Address Modal */}
<AddressSelector
isOpen={showShippingModal}
onClose={() => 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) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white border rounded-lg p-6">
<h2 className="text-xl font-bold mb-4">Billing Details</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">First Name *</label>
<input
type="text"
required
value={shippingData.firstName}
onChange={(e) => 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"
/>
</div>
@@ -560,66 +375,251 @@ export default function Checkout() {
<input
type="text"
required
value={shippingData.lastName}
onChange={(e) => 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Street Address *</label>
<label className="block text-sm font-medium mb-2">Email Address *</label>
<input
type="text"
type="email"
required
value={shippingData.address}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">City *</label>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Phone *</label>
<input
type="text"
type="tel"
required
value={shippingData.city}
onChange={(e) => setShippingData({ ...shippingData, city: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">State / Province *</label>
<input
type="text"
required
value={shippingData.state}
onChange={(e) => setShippingData({ ...shippingData, state: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>
<input
type="text"
required
value={shippingData.postcode}
onChange={(e) => setShippingData({ ...shippingData, postcode: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Country *</label>
<input
type="text"
required
value={shippingData.country}
onChange={(e) => 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"
/>
</div>
{/* Address fields - only for physical products */}
{!isVirtualOnly && (
<>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Street Address *</label>
<input
type="text"
required
value={billingData.address}
onChange={(e) => setBillingData({ ...billingData, address: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">City *</label>
<input
type="text"
required
value={billingData.city}
onChange={(e) => setBillingData({ ...billingData, city: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">State / Province *</label>
<input
type="text"
required
value={billingData.state}
onChange={(e) => setBillingData({ ...billingData, state: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>
<input
type="text"
required
value={billingData.postcode}
onChange={(e) => setBillingData({ ...billingData, postcode: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Country *</label>
<input
type="text"
required
value={billingData.country}
onChange={(e) => setBillingData({ ...billingData, country: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
</>
)}
</div>
</div>
)}
{/* Ship to Different Address - only for physical products */}
{!isVirtualOnly && (
<div className="bg-white border rounded-lg p-6">
<label className="flex items-center gap-2 mb-4">
<input
type="checkbox"
checked={shipToDifferentAddress}
onChange={(e) => setShipToDifferentAddress(e.target.checked)}
className="w-4 h-4"
/>
<span className="font-medium">Ship to a different address?</span>
</label>
{shipToDifferentAddress && (
<>
{/* Selected Shipping Address Summary */}
{!loadingAddresses && savedAddresses.length > 0 && savedAddresses.some(a => a.type === 'shipping' || a.type === 'both') && (
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<MapPin className="w-4 h-4" />
Shipping Address
</h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowShippingModal(true)}
className="flex items-center gap-2"
>
<Edit2 className="w-4 h-4" />
Change Address
</Button>
</div>
{selectedShippingAddressId ? (
(() => {
const selected = savedAddresses.find(a => a.id === selectedShippingAddressId);
return selected ? (
<div>
<div className="bg-primary/5 border-2 border-primary rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<p className="font-semibold">{selected.label}</p>
{selected.is_default && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Default</span>
)}
</div>
<p className="text-sm font-medium text-gray-900">{selected.first_name} {selected.last_name}</p>
{selected.phone && <p className="text-sm text-gray-600">{selected.phone}</p>}
<p className="text-sm text-gray-600 mt-2">{selected.address_1}</p>
{selected.address_2 && <p className="text-sm text-gray-600">{selected.address_2}</p>}
<p className="text-sm text-gray-600">{selected.city}, {selected.state} {selected.postcode}</p>
<p className="text-sm text-gray-600">{selected.country}</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowShippingForm(true)}
className="mt-3 text-primary hover:text-primary"
>
Use a different address
</Button>
</div>
) : null;
})()
) : (
<p className="text-gray-500 text-sm">No address selected</p>
)}
</div>
)}
{/* Shipping Address Modal */}
<AddressSelector
isOpen={showShippingModal}
onClose={() => 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) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">First Name *</label>
<input
type="text"
required
value={shippingData.firstName}
onChange={(e) => setShippingData({ ...shippingData, firstName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Last Name *</label>
<input
type="text"
required
value={shippingData.lastName}
onChange={(e) => setShippingData({ ...shippingData, lastName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Street Address *</label>
<input
type="text"
required
value={shippingData.address}
onChange={(e) => setShippingData({ ...shippingData, address: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">City *</label>
<input
type="text"
required
value={shippingData.city}
onChange={(e) => setShippingData({ ...shippingData, city: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">State / Province *</label>
<input
type="text"
required
value={shippingData.state}
onChange={(e) => setShippingData({ ...shippingData, state: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>
<input
type="text"
required
value={shippingData.postcode}
onChange={(e) => setShippingData({ ...shippingData, postcode: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Country *</label>
<input
type="text"
required
value={shippingData.country}
onChange={(e) => setShippingData({ ...shippingData, country: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
</div>
)}
</>
)}
</>
)}
</div>
</div>
)}
{/* Order Notes */}
@@ -722,30 +722,30 @@ export default function Checkout() {
{/* Hide COD for virtual-only products */}
{!isVirtualOnly && (
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="payment"
value="cod"
<input
type="radio"
name="payment"
value="cod"
checked={paymentMethod === 'cod'}
onChange={(e) => setPaymentMethod(e.target.value)}
className="w-4 h-4"
className="w-4 h-4"
/>
<span>Cash on Delivery</span>
</label>
)}
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="payment"
value="bacs"
<input
type="radio"
name="payment"
value="bacs"
checked={paymentMethod === 'bacs'}
onChange={(e) => setPaymentMethod(e.target.value)}
className="w-4 h-4"
className="w-4 h-4"
/>
<span>Bank Transfer</span>
</label>
</div>
{/* Payment Icons */}
{elements.payment_icons && (
<div className="flex items-center gap-2 mt-3 pt-3 border-t">