fix: shipping cost calculation and performance optimization

- Fix shipping cost not included in checkout total
- Add useShippingMethodSelector hook for proper abstraction
- Remove blocking initCheckout from Header for better performance
- Checkout now initializes lazily when cart opens or item added
This commit is contained in:
Unchained
2026-03-30 06:31:52 +02:00
parent adb28c2a91
commit 25e60457cc
3 changed files with 98 additions and 8 deletions

View File

@@ -19,6 +19,7 @@ import { DEFAULT_PAYMENT_METHOD } from "@/lib/config/paymentMethods";
import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout"; import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout";
import type { Checkout } from "@/types/saleor"; import type { Checkout } from "@/types/saleor";
import { createCheckoutService, type Address } from "@/lib/services/checkoutService"; import { createCheckoutService, type Address } from "@/lib/services/checkoutService";
import { useShippingMethodSelector } from "@/lib/hooks/useShippingMethodSelector";
interface ShippingAddressUpdateResponse { interface ShippingAddressUpdateResponse {
checkoutShippingAddressUpdate?: { checkoutShippingAddressUpdate?: {
@@ -31,6 +32,8 @@ interface CheckoutQueryResponse {
checkout?: Checkout; checkout?: Checkout;
} }
interface ShippingMethod { interface ShippingMethod {
id: string; id: string;
name: string; name: string;
@@ -92,8 +95,16 @@ export default function CheckoutPage() {
const [selectedShippingMethod, setSelectedShippingMethod] = useState<string>(""); const [selectedShippingMethod, setSelectedShippingMethod] = useState<string>("");
const [isLoadingShipping, setIsLoadingShipping] = useState(false); const [isLoadingShipping, setIsLoadingShipping] = useState(false);
// Hook to manage shipping method selection (both manual and auto)
const { selectShippingMethodWithApi } = useShippingMethodSelector({
checkoutId: checkout?.id ?? null,
onSelect: setSelectedShippingMethod,
onRefresh: refreshCheckout,
});
const lines = getLines(); const lines = getLines();
const total = getTotal(); // Use checkout.totalPrice directly for reactive updates when shipping method changes
const total = checkout?.totalPrice?.gross?.amount || getTotal();
// Debounced shipping method fetching // Debounced shipping method fetching
useEffect(() => { useEffect(() => {
@@ -147,10 +158,12 @@ export default function CheckoutPage() {
console.log("Available shipping methods:", availableMethods); console.log("Available shipping methods:", availableMethods);
setShippingMethods(availableMethods); setShippingMethods(availableMethods);
// Auto-select first method if none selected // Auto-select first method if none selected
if (availableMethods.length > 0 && !selectedShippingMethod) { if (availableMethods.length > 0 && !selectedShippingMethod) {
setSelectedShippingMethod(availableMethods[0].id); const firstMethodId = availableMethods[0].id;
// Use the hook to both update UI and call API
await selectShippingMethodWithApi(firstMethodId);
} }
} catch (err) { } catch (err) {
console.error("Error fetching shipping methods:", err); console.error("Error fetching shipping methods:", err);
@@ -210,6 +223,10 @@ export default function CheckoutPage() {
setShippingAddress((prev) => ({ ...prev, email: value })); setShippingAddress((prev) => ({ ...prev, email: value }));
}; };
const handleShippingMethodSelect = async (methodId: string) => {
await selectShippingMethodWithApi(methodId);
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -576,7 +593,7 @@ export default function CheckoutPage() {
name="shippingMethod" name="shippingMethod"
value={method.id} value={method.id}
checked={selectedShippingMethod === method.id} checked={selectedShippingMethod === method.id}
onChange={(e) => setSelectedShippingMethod(e.target.value)} onChange={(e) => handleShippingMethodSelect(e.target.value)}
className="w-4 h-4" className="w-4 h-4"
/> />
<span className="font-medium">{method.name}</span> <span className="font-medium">{method.name}</span>

View File

@@ -55,14 +55,14 @@ export default function Header({ locale: propLocale = "sr" }: HeaderProps) {
setLangDropdownOpen(false); setLangDropdownOpen(false);
}; };
// Set language code first, then initialize checkout // Set language code - checkout initializes lazily when cart is opened
useEffect(() => { useEffect(() => {
if (locale) { if (locale) {
setLanguageCode(locale); setLanguageCode(locale);
// Initialize checkout after language code is set // Checkout will initialize lazily when user adds to cart or opens cart drawer
initCheckout(); // This prevents blocking page render with unnecessary API calls
} }
}, [locale, setLanguageCode, initCheckout]); }, [locale, setLanguageCode]);
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {

View File

@@ -0,0 +1,73 @@
"use client";
import { useCallback } from "react";
import { createCheckoutService } from "@/lib/services/checkoutService";
interface UseShippingMethodSelectorOptions {
checkoutId: string | null;
onSelect: (methodId: string) => void;
onRefresh: () => Promise<void>;
}
interface UseShippingMethodSelectorResult {
selectShippingMethod: (methodId: string) => Promise<void>;
selectShippingMethodWithApi: (methodId: string) => Promise<void>;
}
/**
* Hook to manage shipping method selection
* Encapsulates both UI state update and API communication
* Used for both manual selection (user click) and auto-selection (default method)
*/
export function useShippingMethodSelector(
options: UseShippingMethodSelectorOptions
): UseShippingMethodSelectorResult {
const { checkoutId, onSelect, onRefresh } = options;
/**
* Updates UI state only (for initial/pre-selection)
*/
const selectShippingMethod = useCallback(
async (methodId: string) => {
onSelect(methodId);
},
[onSelect]
);
/**
* Updates UI state AND calls Saleor API
* Use this when user manually selects OR when auto-selecting the default
*/
const selectShippingMethodWithApi = useCallback(
async (methodId: string) => {
if (!checkoutId) {
console.warn("[selectShippingMethodWithApi] No checkoutId provided");
return;
}
// Update UI immediately for responsiveness
onSelect(methodId);
// Call API through CheckoutService
const checkoutService = createCheckoutService(checkoutId);
const result = await checkoutService.updateShippingMethod(methodId);
if (result.success) {
// Refresh checkout to get updated totals including shipping
await onRefresh();
} else {
console.error(
"[selectShippingMethodWithApi] Failed to update shipping method:",
result.error
);
// Could add error handling/rollback here
}
},
[checkoutId, onSelect, onRefresh]
);
return {
selectShippingMethod,
selectShippingMethodWithApi,
};
}