Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@
"firstName": "Vorname",
"lastName": "Nachname",
"emailAddress": "E-Mail-Adresse",
"currentPassword": "Aktuelles Passwort",
"currentPasswordHelp": "Bestätigen Sie Ihr aktuelles Passwort, um Ihre E-Mail-Adresse zu ändern.",
"saveChanges": "Änderungen speichern",
"saving": "Speichern...",
"profileUpdated": "Profil erfolgreich aktualisiert!",
Expand Down
2 changes: 2 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@
"firstName": "First Name",
"lastName": "Last Name",
"emailAddress": "Email Address",
"currentPassword": "Current Password",
"currentPasswordHelp": "Confirm your current password to change your email address.",
"saveChanges": "Save Changes",
"saving": "Saving...",
"profileUpdated": "Profile updated successfully!",
Expand Down
2 changes: 2 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@
"firstName": "Nombre",
"lastName": "Apellido",
"emailAddress": "Correo electronico",
"currentPassword": "Contrasena actual",
"currentPasswordHelp": "Confirma tu contrasena actual para cambiar tu correo electronico.",
"saveChanges": "Guardar cambios",
"saving": "Guardando...",
"profileUpdated": "\u00a1Perfil actualizado con exito!",
Expand Down
2 changes: 2 additions & 0 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@
"firstName": "Prenom",
"lastName": "Nom",
"emailAddress": "Adresse e-mail",
"currentPassword": "Mot de passe actuel",
"currentPasswordHelp": "Confirmez votre mot de passe actuel pour modifier votre adresse e-mail.",
"saveChanges": "Enregistrer les modifications",
"saving": "Enregistrement...",
"profileUpdated": "Profil mis a jour avec succes !",
Expand Down
2 changes: 2 additions & 0 deletions messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@
"firstName": "Imię",
"lastName": "Nazwisko",
"emailAddress": "Adres e-mail",
"currentPassword": "Obecne hasło",
"currentPasswordHelp": "Potwierdź swoje obecne hasło, aby zmienić adres e-mail.",
"saveChanges": "Zapisz zmiany",
"saving": "Zapisywanie...",
"profileUpdated": "Profil został zaktualizowany!",
Expand Down
77 changes: 74 additions & 3 deletions src/app/[country]/[locale]/(storefront)/account/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { CircleAlert } from "lucide-react";
import { CircleAlert, Eye, EyeOff } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { toast } from "sonner";
Expand All @@ -25,27 +25,44 @@ function ProfileForm({
refreshUser: () => Promise<void>;
}) {
const t = useTranslations("profile");
const ta = useTranslations("account");
// Initialize form data from user props - no useEffect needed
const [formData, setFormData] = useState({
first_name: user.first_name || "",
last_name: user.last_name || "",
email: user.email || "",
});
const [currentPassword, setCurrentPassword] = useState("");
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [passwordError, setPasswordError] = useState<string | null>(null);

const emailChanged = formData.email.trim() !== user.email;

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setPasswordError(null);
setSaving(true);

const result = await updateCustomer(formData);
const result = await updateCustomer({
...formData,
...(emailChanged && { current_password: currentPassword }),
});

if (result.success) {
toast.success(t("profileUpdated"));
setCurrentPassword("");
setShowCurrentPassword(false);
await refreshUser();
} else {
setError(result.error || t("failedToUpdate"));
const message = result.error || t("failedToUpdate");
if (emailChanged && /current password/i.test(message)) {
setPasswordError(message);
} else {
setError(message);
}
}

setSaving(false);
Expand Down Expand Up @@ -102,6 +119,60 @@ function ProfileForm({
}
/>
</Field>

{emailChanged && (
<Field>
<FieldLabel htmlFor="current_password">
{t("currentPassword")}
</FieldLabel>
<div className="relative">
<Input
type={showCurrentPassword ? "text" : "password"}
id="current_password"
autoComplete="current-password"
required
value={currentPassword}
onChange={(e) => {
setCurrentPassword(e.target.value);
if (passwordError) setPasswordError(null);
}}
placeholder="••••••••"
className="pr-10"
aria-invalid={passwordError ? true : undefined}
aria-describedby="current_password_help"
/>
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={() =>
setShowCurrentPassword(!showCurrentPassword)
}
aria-label={
showCurrentPassword
? ta("hidePassword")
: ta("showPassword")
}
>
{showCurrentPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</Button>
</div>
</div>
<p
id="current_password_help"
className={`text-sm ${
passwordError ? "text-red-600" : "text-gray-500"
}`}
>
{passwordError || t("currentPasswordHelp")}
</p>
</Field>
)}
</div>

<div className="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-end">
Expand Down
31 changes: 31 additions & 0 deletions src/lib/data/__tests__/customer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,21 @@ describe("customer server actions", () => {
expect(result).toEqual({ success: true, customer: mockUser });
});

it("forwards current_password when changing email", async () => {
mockClient.customer.update.mockResolvedValue(mockUser);

const result = await updateCustomer({
email: "new@example.com",
current_password: "secret",
});

expect(mockClient.customer.update).toHaveBeenCalledWith(
{ email: "new@example.com", current_password: "secret" },
{ token: "jwt-token" },
);
expect(result).toEqual({ success: true, customer: mockUser });
});

it("returns error on failure", async () => {
mockClient.customer.update.mockRejectedValue(new Error("Email taken"));

Expand All @@ -204,6 +219,22 @@ describe("customer server actions", () => {
});
});

it("surfaces invalid current password error", async () => {
mockClient.customer.update.mockRejectedValue(
new Error("Current password is invalid or missing"),
);

const result = await updateCustomer({
email: "new@example.com",
current_password: "wrong",
});

expect(result).toEqual({
success: false,
error: "Current password is invalid or missing",
});
});

it("returns fallback message for non-Error throws", async () => {
mockClient.customer.update.mockRejectedValue("unexpected");

Expand Down
1 change: 1 addition & 0 deletions src/lib/data/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export async function updateCustomer(data: {
first_name?: string;
last_name?: string;
email?: string;
current_password?: string;
}) {
return actionResult(async () => {
let customer;
Expand Down
Loading