From bcf74e1fd173d2a8b08151a8e1840e1e0ce1e02c Mon Sep 17 00:00:00 2001 From: Unchained Date: Sat, 28 Mar 2026 18:03:12 +0200 Subject: [PATCH] feat: implement one-page checkout with dynamic shipping --- src/app/[locale]/checkout/page.tsx | 206 +++++++++++++++-------------- src/i18n/messages/de.json | 5 + src/i18n/messages/en.json | 3 + src/i18n/messages/fr.json | 5 + src/i18n/messages/sr.json | 5 + 5 files changed, 122 insertions(+), 102 deletions(-) diff --git a/src/app/[locale]/checkout/page.tsx b/src/app/[locale]/checkout/page.tsx index 71ec887..dbb1c8d 100644 --- a/src/app/[locale]/checkout/page.tsx +++ b/src/app/[locale]/checkout/page.tsx @@ -129,11 +129,78 @@ export default function CheckoutPage() { const [shippingMethods, setShippingMethods] = useState([]); const [selectedShippingMethod, setSelectedShippingMethod] = useState(""); - const [showShippingMethods, setShowShippingMethods] = useState(false); + const [isLoadingShipping, setIsLoadingShipping] = useState(false); const lines = getLines(); const total = getTotal(); + // Debounced shipping method fetching + useEffect(() => { + if (!checkout) return; + + // Check if address is complete enough to fetch shipping methods + const isAddressComplete = + shippingAddress.firstName && + shippingAddress.lastName && + shippingAddress.streetAddress1 && + shippingAddress.city && + shippingAddress.postalCode && + shippingAddress.country; + + if (!isAddressComplete) { + setShippingMethods([]); + return; + } + + const timer = setTimeout(async () => { + setIsLoadingShipping(true); + try { + console.log("Fetching shipping methods..."); + + // First update the shipping address + await saleorClient.mutate({ + mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE, + variables: { + checkoutId: checkout.id, + shippingAddress: { + firstName: shippingAddress.firstName, + lastName: shippingAddress.lastName, + streetAddress1: shippingAddress.streetAddress1, + streetAddress2: shippingAddress.streetAddress2, + city: shippingAddress.city, + postalCode: shippingAddress.postalCode, + country: shippingAddress.country, + phone: shippingAddress.phone, + }, + }, + }); + + // Then query for shipping methods + const checkoutQueryResult = await saleorClient.query({ + query: GET_CHECKOUT_BY_ID, + variables: { id: checkout.id }, + fetchPolicy: "network-only", + }); + + const availableMethods = checkoutQueryResult.data?.checkout?.shippingMethods || []; + console.log("Available shipping methods:", availableMethods); + + setShippingMethods(availableMethods); + + // Auto-select first method if none selected + if (availableMethods.length > 0 && !selectedShippingMethod) { + setSelectedShippingMethod(availableMethods[0].id); + } + } catch (err) { + console.error("Error fetching shipping methods:", err); + } finally { + setIsLoadingShipping(false); + } + }, 500); // 500ms debounce + + return () => clearTimeout(timer); + }, [checkout, shippingAddress]); + useEffect(() => { if (!checkout) { refreshCheckout(); @@ -189,23 +256,32 @@ export default function CheckoutPage() { return; } + // Validate all required fields if (!shippingAddress.email || !shippingAddress.email.includes("@")) { setError(t("errorEmailRequired")); return; } - if (!shippingAddress.firstName || !shippingAddress.lastName || !shippingAddress.streetAddress1 || !shippingAddress.city || !shippingAddress.postalCode || !shippingAddress.phone) { + if (!shippingAddress.phone || shippingAddress.phone.length < 8) { + setError(t("errorPhoneRequired")); + return; + } + + if (!shippingAddress.firstName || !shippingAddress.lastName || !shippingAddress.streetAddress1 || !shippingAddress.city || !shippingAddress.postalCode) { setError(t("errorFieldsRequired")); return; } + if (!selectedShippingMethod) { + setError(t("errorSelectShipping")); + return; + } + setIsLoading(true); setError(null); try { - // If we're showing shipping methods and one is selected, complete the order - if (showShippingMethods && selectedShippingMethod) { - console.log("Phase 2: Completing order with shipping method..."); + console.log("Completing order..."); console.log("Step 1: Updating billing address..."); const billingResult = await saleorClient.mutate({ @@ -304,93 +380,6 @@ export default function CheckoutPage() { } else { throw new Error(t("errorCreatingOrder")); } - } else { - // Phase 1: Update email and address, then fetch shipping methods - console.log("Phase 1: Updating email and address..."); - - console.log("Step 1: Updating email..."); - const emailResult = await saleorClient.mutate({ - mutation: CHECKOUT_EMAIL_UPDATE, - variables: { - checkoutId: checkout.id, - email: shippingAddress.email, - }, - }); - - if (emailResult.data?.checkoutEmailUpdate?.errors && emailResult.data.checkoutEmailUpdate.errors.length > 0) { - const errorMessage = emailResult.data.checkoutEmailUpdate.errors[0].message; - // Check if checkout no longer exists - if (errorMessage.includes("Couldn't resolve to a node")) { - console.error("Checkout not found, clearing cart..."); - localStorage.removeItem('cart'); - localStorage.removeItem('checkoutId'); - window.location.href = `/${locale}/products`; - return; - } - throw new Error(`Email update failed: ${errorMessage}`); - } - console.log("Step 1: Email updated successfully"); - - console.log("Step 2: Updating shipping address..."); - console.log("Shipping address data:", { - firstName: shippingAddress.firstName, - lastName: shippingAddress.lastName, - streetAddress1: shippingAddress.streetAddress1, - city: shippingAddress.city, - postalCode: shippingAddress.postalCode, - country: shippingAddress.country, - phone: shippingAddress.phone, - }); - const shippingResult = await saleorClient.mutate({ - mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE, - variables: { - checkoutId: checkout.id, - shippingAddress: { - firstName: shippingAddress.firstName, - lastName: shippingAddress.lastName, - streetAddress1: shippingAddress.streetAddress1, - streetAddress2: shippingAddress.streetAddress2, - city: shippingAddress.city, - postalCode: shippingAddress.postalCode, - country: shippingAddress.country, - phone: shippingAddress.phone, - }, - }, - }); - - if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) { - throw new Error(`Shipping address update failed: ${shippingResult.data.checkoutShippingAddressUpdate.errors[0].message}`); - } - console.log("Step 2: Shipping address updated successfully"); - - // Query for checkout to get available shipping methods - console.log("Step 3: Fetching shipping methods..."); - const checkoutQueryResult = await saleorClient.query({ - query: GET_CHECKOUT_BY_ID, - variables: { - id: checkout.id, - }, - fetchPolicy: "network-only", - }); - - const availableMethods = checkoutQueryResult.data?.checkout?.shippingMethods || []; - console.log("Available shipping methods:", availableMethods); - - if (availableMethods.length === 0) { - throw new Error(t("errorNoShippingMethods")); - } - - setShippingMethods(availableMethods); - setShowShippingMethods(true); - - // Track shipping step - trackCheckoutStep("shipping_method_selection", { - available_methods_count: availableMethods.length, - }); - - // Don't complete yet - show shipping method selection - console.log("Phase 1 complete. Waiting for shipping method selection..."); - } } catch (err: unknown) { console.error("Checkout error:", err); @@ -606,9 +595,17 @@ export default function CheckoutPage() { {/* Shipping Method Selection */} - {showShippingMethods && shippingMethods.length > 0 && ( -
-

{t("shippingMethod")}

+
+

{t("shippingMethod")}

+ {isLoadingShipping ? ( +
+ + + + + {t("loadingShippingMethods")} +
+ ) : shippingMethods.length > 0 ? (
{shippingMethods.map((method) => (
- {!selectedShippingMethod && ( -

{t("errorSelectShipping")}

- )} -
- )} + ) : ( +

{t("enterAddressForShipping")}

+ )} +
@@ -691,6 +687,12 @@ export default function CheckoutPage() { {t("subtotal")} {formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)} + {selectedShippingMethod && ( +
+ {t("shipping")} + {formatPrice(shippingMethods.find(m => m.id === selectedShippingMethod)?.price.amount || 0)} +
+ )}
{t("total")} {formatPrice(total)} diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index cdc1791..4e3026e 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -372,6 +372,11 @@ "errorNoCheckout": "Keine aktive Kasse. Bitte versuchen Sie es erneut.", "errorEmailRequired": "Bitte geben Sie eine gültige E-Mail-Adresse ein.", "errorFieldsRequired": "Bitte füllen Sie alle erforderlichen Felder aus.", + "errorNoShippingMethods": "Keine Versandmethoden für diese Adresse verfügbar. Bitte überprüfen Sie Ihre Adresse oder kontaktieren Sie den Support.", + "errorSelectShipping": "Bitte wählen Sie eine Versandmethode.", + "errorPhoneRequired": "Bitte geben Sie eine gültige Telefonnummer ein.", + "loadingShippingMethods": "Versandoptionen werden geladen...", + "enterAddressForShipping": "Geben Sie Ihre Adresse ein, um Versandoptionen zu sehen.", "errorOccurred": "Ein Fehler ist during des Checkouts aufgetreten.", "errorCreatingOrder": "Bestellung konnte nicht erstellt werden.", "orderConfirmed": "Bestellung bestätigt!", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 1237f89..9c7d482 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -420,6 +420,9 @@ "errorFieldsRequired": "Please fill in all required fields.", "errorNoShippingMethods": "No shipping methods available for this address. Please check your address or contact support.", "errorSelectShipping": "Please select a shipping method.", + "errorPhoneRequired": "Please enter a valid phone number.", + "loadingShippingMethods": "Loading shipping options...", + "enterAddressForShipping": "Enter your address to see shipping options.", "errorOccurred": "An error occurred during checkout.", "errorCreatingOrder": "Failed to create order.", "continueToShipping": "Continue to Shipping", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index a03ea26..2d93a15 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -372,6 +372,11 @@ "errorNoCheckout": "Pas de paiement actif. Veuillez réessayer.", "errorEmailRequired": "Veuillez entrer une adresse e-mail valide.", "errorFieldsRequired": "Veuillez remplir tous les champs obligatoires.", + "errorNoShippingMethods": "Aucune méthode de livraison disponible pour cette adresse. Veuillez vérifier votre adresse ou contacter le support.", + "errorSelectShipping": "Veuillez sélectionner une méthode de livraison.", + "errorPhoneRequired": "Veuillez entrer un numéro de téléphone valide.", + "loadingShippingMethods": "Chargement des options de livraison...", + "enterAddressForShipping": "Entrez votre adresse pour voir les options de livraison.", "errorOccurred": "Une erreur s'est produite lors du paiement.", "errorCreatingOrder": "Échec de la création de la commande.", "orderConfirmed": "Commande Confirmée!", diff --git a/src/i18n/messages/sr.json b/src/i18n/messages/sr.json index a84fb1f..23fc54e 100644 --- a/src/i18n/messages/sr.json +++ b/src/i18n/messages/sr.json @@ -418,6 +418,11 @@ "errorNoCheckout": "Nema aktivne korpe. Molimo pokušajte ponovo.", "errorEmailRequired": "Molimo unesite validnu email adresu.", "errorFieldsRequired": "Molimo popunite sva obavezna polja.", + "errorNoShippingMethods": "Nema dostupnih načina dostave za ovu adresu. Molimo proverite adresu ili kontaktirajte podršku.", + "errorSelectShipping": "Molimo izaberite način dostave.", + "errorPhoneRequired": "Molimo unesite validan broj telefona.", + "loadingShippingMethods": "Učitavanje opcija dostave...", + "enterAddressForShipping": "Unesite adresu da vidite opcije dostave.", "errorOccurred": "Došlo je do greške prilikom kupovine.", "errorCreatingOrder": "Neuspešno kreiranje narudžbine.", "orderConfirmed": "Narudžbina potvrđena!",