fix: add required email and country fields to checkout

- Add email field (required) for order confirmation
- Add phone field in contact info section
- Add country dropdown with regional options
- Add validation for email format and required fields
- Add checkoutEmailUpdate mutation call before completing
- Use selected country instead of hardcoded RS
- Add translations for new fields (EN, SR, DE, FR)
This commit is contained in:
Unchained
2026-03-25 10:50:42 +02:00
parent 4fcd4b3ba8
commit ef83538d0b
5 changed files with 144 additions and 15 deletions

View File

@@ -14,6 +14,7 @@ import {
CHECKOUT_SHIPPING_ADDRESS_UPDATE, CHECKOUT_SHIPPING_ADDRESS_UPDATE,
CHECKOUT_BILLING_ADDRESS_UPDATE, CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE, CHECKOUT_COMPLETE,
CHECKOUT_EMAIL_UPDATE,
} from "@/lib/saleor/mutations/Checkout"; } from "@/lib/saleor/mutations/Checkout";
import type { Checkout } from "@/types/saleor"; import type { Checkout } from "@/types/saleor";
@@ -38,6 +39,13 @@ interface CheckoutCompleteResponse {
}; };
} }
interface EmailUpdateResponse {
checkoutEmailUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface AddressForm { interface AddressForm {
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -45,7 +53,9 @@ interface AddressForm {
streetAddress2: string; streetAddress2: string;
city: string; city: string;
postalCode: string; postalCode: string;
country: string;
phone: string; phone: string;
email: string;
} }
export default function CheckoutPage() { export default function CheckoutPage() {
@@ -66,7 +76,9 @@ export default function CheckoutPage() {
streetAddress2: "", streetAddress2: "",
city: "", city: "",
postalCode: "", postalCode: "",
country: "RS",
phone: "", phone: "",
email: "",
}); });
const [billingAddress, setBillingAddress] = useState<AddressForm>({ const [billingAddress, setBillingAddress] = useState<AddressForm>({
firstName: "", firstName: "",
@@ -75,7 +87,9 @@ export default function CheckoutPage() {
streetAddress2: "", streetAddress2: "",
city: "", city: "",
postalCode: "", postalCode: "",
country: "RS",
phone: "", phone: "",
email: "",
}); });
const lines = getLines(); const lines = getLines();
@@ -89,7 +103,7 @@ export default function CheckoutPage() {
const handleShippingChange = (field: keyof AddressForm, value: string) => { const handleShippingChange = (field: keyof AddressForm, value: string) => {
setShippingAddress((prev) => ({ ...prev, [field]: value })); setShippingAddress((prev) => ({ ...prev, [field]: value }));
if (sameAsShipping) { if (sameAsShipping && field !== "email") {
setBillingAddress((prev) => ({ ...prev, [field]: value })); setBillingAddress((prev) => ({ ...prev, [field]: value }));
} }
}; };
@@ -98,6 +112,10 @@ export default function CheckoutPage() {
setBillingAddress((prev) => ({ ...prev, [field]: value })); setBillingAddress((prev) => ({ ...prev, [field]: value }));
}; };
const handleEmailChange = (value: string) => {
setShippingAddress((prev) => ({ ...prev, email: value }));
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -106,17 +124,45 @@ export default function CheckoutPage() {
return; return;
} }
if (!shippingAddress.email || !shippingAddress.email.includes("@")) {
setError(t("errorEmailRequired"));
return;
}
if (!shippingAddress.firstName || !shippingAddress.lastName || !shippingAddress.streetAddress1 || !shippingAddress.city || !shippingAddress.postalCode || !shippingAddress.phone) {
setError(t("errorFieldsRequired"));
return;
}
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
try { try {
const emailResult = await saleorClient.mutate<EmailUpdateResponse>({
mutation: CHECKOUT_EMAIL_UPDATE,
variables: {
checkoutId: checkout.id,
email: shippingAddress.email,
},
});
if (emailResult.data?.checkoutEmailUpdate?.errors && emailResult.data.checkoutEmailUpdate.errors.length > 0) {
throw new Error(emailResult.data.checkoutEmailUpdate.errors[0].message);
}
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({ const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE, mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
shippingAddress: { shippingAddress: {
...shippingAddress, firstName: shippingAddress.firstName,
country: "RS", lastName: shippingAddress.lastName,
streetAddress1: shippingAddress.streetAddress1,
streetAddress2: shippingAddress.streetAddress2,
city: shippingAddress.city,
postalCode: shippingAddress.postalCode,
country: shippingAddress.country,
phone: shippingAddress.phone,
}, },
}, },
}); });
@@ -130,8 +176,14 @@ export default function CheckoutPage() {
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
billingAddress: { billingAddress: {
...billingAddress, firstName: billingAddress.firstName,
country: "RS", lastName: billingAddress.lastName,
streetAddress1: billingAddress.streetAddress1,
streetAddress2: billingAddress.streetAddress2,
city: billingAddress.city,
postalCode: billingAddress.postalCode,
country: billingAddress.country,
phone: billingAddress.phone,
}, },
}, },
}); });
@@ -227,6 +279,36 @@ export default function CheckoutPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div> <div>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">{t("contactInfo")}</h2>
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-sm font-medium mb-1">{t("email")}</label>
<input
type="email"
required
value={shippingAddress.email}
onChange={(e) => handleEmailChange(e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
placeholder="email@example.com"
/>
<p className="text-xs text-foreground-muted mt-1">{t("emailRequired")}</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">{t("phone")}</label>
<input
type="tel"
required
value={shippingAddress.phone}
onChange={(e) => handleShippingChange("phone", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
placeholder="+381..."
/>
<p className="text-xs text-foreground-muted mt-1">{t("phoneRequired")}</p>
</div>
</div>
</div>
<div className="border-b border-border pb-6"> <div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">{t("shippingAddress")}</h2> <h2 className="text-xl font-serif mb-4">{t("shippingAddress")}</h2>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -250,6 +332,35 @@ export default function CheckoutPage() {
className="w-full border border-border px-4 py-2 rounded" className="w-full border border-border px-4 py-2 rounded"
/> />
</div> </div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">{t("country")}</label>
<select
required
value={shippingAddress.country}
onChange={(e) => handleShippingChange("country", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
>
<option value="RS">Serbia (Srbija)</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="ME">Montenegro</option>
<option value="HR">Croatia</option>
<option value="SI">Slovenia</option>
<option value="MK">North Macedonia</option>
<option value="AL">Albania</option>
<option value="XK">Kosovo</option>
<option value="BG">Bulgaria</option>
<option value="RO">Romania</option>
<option value="HU">Hungary</option>
<option value="DE">Germany</option>
<option value="AT">Austria</option>
<option value="CH">Switzerland</option>
<option value="FR">France</option>
<option value="GB">United Kingdom</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="AU">Australia</option>
</select>
</div>
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-sm font-medium mb-1">{t("streetAddress")}</label> <label className="block text-sm font-medium mb-1">{t("streetAddress")}</label>
<input <input
@@ -289,16 +400,6 @@ export default function CheckoutPage() {
className="w-full border border-border px-4 py-2 rounded" className="w-full border border-border px-4 py-2 rounded"
/> />
</div> </div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">{t("phone")}</label>
<input
type="tel"
required
value={shippingAddress.phone}
onChange={(e) => handleShippingChange("phone", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
</div> </div>
</div> </div>

View File

@@ -340,7 +340,12 @@
}, },
"Checkout": { "Checkout": {
"checkout": "Kasse", "checkout": "Kasse",
"contactInfo": "Kontaktinformationen",
"email": "E-Mail",
"emailRequired": "Erforderlich für Bestellbestätigung",
"phoneRequired": "Erforderlich für Lieferkoordination",
"shippingAddress": "Lieferadresse", "shippingAddress": "Lieferadresse",
"country": "Land",
"firstName": "Vorname", "firstName": "Vorname",
"lastName": "Nachname", "lastName": "Nachname",
"streetAddress": "Straße und Nummer", "streetAddress": "Straße und Nummer",
@@ -364,6 +369,8 @@
"yourCartEmpty": "Ihr Warenkorb ist leer", "yourCartEmpty": "Ihr Warenkorb ist leer",
"continueShopping": "Weiter einkaufen", "continueShopping": "Weiter einkaufen",
"errorNoCheckout": "Keine aktive Kasse. Bitte versuchen Sie es erneut.", "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.",
"errorOccurred": "Ein Fehler ist during des Checkouts aufgetreten.", "errorOccurred": "Ein Fehler ist during des Checkouts aufgetreten.",
"errorCreatingOrder": "Bestellung konnte nicht erstellt werden.", "errorCreatingOrder": "Bestellung konnte nicht erstellt werden.",
"orderConfirmed": "Bestellung bestätigt!", "orderConfirmed": "Bestellung bestätigt!",

View File

@@ -386,7 +386,12 @@
}, },
"Checkout": { "Checkout": {
"checkout": "Checkout", "checkout": "Checkout",
"contactInfo": "Contact Information",
"email": "Email",
"emailRequired": "Required for order confirmation",
"phoneRequired": "Required for delivery coordination",
"shippingAddress": "Shipping Address", "shippingAddress": "Shipping Address",
"country": "Country",
"firstName": "First Name", "firstName": "First Name",
"lastName": "Last Name", "lastName": "Last Name",
"streetAddress": "Street Address", "streetAddress": "Street Address",
@@ -410,6 +415,8 @@
"yourCartEmpty": "Your cart is empty", "yourCartEmpty": "Your cart is empty",
"continueShopping": "Continue Shopping", "continueShopping": "Continue Shopping",
"errorNoCheckout": "No active checkout. Please try again.", "errorNoCheckout": "No active checkout. Please try again.",
"errorEmailRequired": "Please enter a valid email address.",
"errorFieldsRequired": "Please fill in all required fields.",
"errorOccurred": "An error occurred during checkout.", "errorOccurred": "An error occurred during checkout.",
"errorCreatingOrder": "Failed to create order.", "errorCreatingOrder": "Failed to create order.",
"orderConfirmed": "Order Confirmed!", "orderConfirmed": "Order Confirmed!",

View File

@@ -340,7 +340,12 @@
}, },
"Checkout": { "Checkout": {
"checkout": "Commande", "checkout": "Commande",
"contactInfo": "Coordonnées",
"email": "E-mail",
"emailRequired": "Requis pour la confirmation de commande",
"phoneRequired": "Requis pour la coordination de livraison",
"shippingAddress": "Adresse de Livraison", "shippingAddress": "Adresse de Livraison",
"country": "Pays",
"firstName": "Prénom", "firstName": "Prénom",
"lastName": "Nom", "lastName": "Nom",
"streetAddress": "Rue et Numéro", "streetAddress": "Rue et Numéro",
@@ -364,6 +369,8 @@
"yourCartEmpty": "Votre panier est vide", "yourCartEmpty": "Votre panier est vide",
"continueShopping": "Continuer les Achats", "continueShopping": "Continuer les Achats",
"errorNoCheckout": "Pas de paiement actif. Veuillez réessayer.", "errorNoCheckout": "Pas de paiement actif. Veuillez réessayer.",
"errorEmailRequired": "Veuillez entrer une adresse e-mail valide.",
"errorFieldsRequired": "Veuillez remplir tous les champs obligatoires.",
"errorOccurred": "Une erreur s'est produite lors du paiement.", "errorOccurred": "Une erreur s'est produite lors du paiement.",
"errorCreatingOrder": "Échec de la création de la commande.", "errorCreatingOrder": "Échec de la création de la commande.",
"orderConfirmed": "Commande Confirmée!", "orderConfirmed": "Commande Confirmée!",

View File

@@ -386,7 +386,12 @@
}, },
"Checkout": { "Checkout": {
"checkout": "Kupovina", "checkout": "Kupovina",
"contactInfo": "Kontakt informacije",
"email": "Email",
"emailRequired": "Potrebno za potvrdu narudžbine",
"phoneRequired": "Potrebno za koordinaciju dostave",
"shippingAddress": "Adresa za dostavu", "shippingAddress": "Adresa za dostavu",
"country": "Država",
"firstName": "Ime", "firstName": "Ime",
"lastName": "Prezime", "lastName": "Prezime",
"streetAddress": "Ulica i broj", "streetAddress": "Ulica i broj",
@@ -410,6 +415,8 @@
"yourCartEmpty": "Vaša korpa je prazna", "yourCartEmpty": "Vaša korpa je prazna",
"continueShopping": "Nastavi kupovinu", "continueShopping": "Nastavi kupovinu",
"errorNoCheckout": "Nema aktivne korpe. Molimo pokušajte ponovo.", "errorNoCheckout": "Nema aktivne korpe. Molimo pokušajte ponovo.",
"errorEmailRequired": "Molimo unesite validnu email adresu.",
"errorFieldsRequired": "Molimo popunite sva obavezna polja.",
"errorOccurred": "Došlo je do greške prilikom kupovine.", "errorOccurred": "Došlo je do greške prilikom kupovine.",
"errorCreatingOrder": "Neuspešno kreiranje narudžbine.", "errorCreatingOrder": "Neuspešno kreiranje narudžbine.",
"orderConfirmed": "Narudžbina potvrđena!", "orderConfirmed": "Narudžbina potvrđena!",