# yatra/3.0.13/resources/js/pages/account/Profile.tsx

Yatra – Travel Booking &amp; Tour Operator Software, version 3.0.13. 648 lines.

- Page: https://pluginprobe.com/plugins/yatra/3.0.13/code/resources/js/pages/account/Profile.tsx
- Raw: https://pluginprobe.com/plugins/yatra/3.0.13/raw/resources/js/pages/account/Profile.tsx
- Modified: 2026-07-20T12:05:54+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/yatra/3.0.13/code/resources/js/pages/account/Profile.tsx#L10-L20`.

```tsx
import React, { useState, useEffect } from "react";
import {
  User,
  PenSquare,
  ShieldCheck,
  Heart,
  CheckCircle,
  XCircle,
} from "lucide-react";
import { useQueryClient } from "@tanstack/react-query";
import { __, sprintf } from "../../lib/i18n";
import { currency } from "./utils";
import { getCountryName, getCountrySelectOptions } from "../../lib/countries";
import { SearchableSelect } from "../../components/ui/searchable-select";
import { apiClient } from "../../lib/api-client";
import { API_ENDPOINTS } from "../../lib/api-endpoints";
import { useToast } from "../../components/ui/toast";
import type { CustomerProfile } from "./types";

interface ProfileProps {
  profile: CustomerProfile | null;
  savedTrips: any[];
  wishlistEnabled?: boolean;
}

const Profile: React.FC<ProfileProps> = ({
  profile,
  savedTrips,
  wishlistEnabled = false,
}) => {
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    phone: "",
    address: "",
    city: "",
    country: "",
  });

  const [isChangingPassword, setIsChangingPassword] = useState(false);
  const [passwordData, setPasswordData] = useState({
    currentPassword: "",
    newPassword: "",
    confirmPassword: "",
  });

  // Update formData when profile changes
  useEffect(() => {
    if (profile && !isEditing) {
      setFormData({
        name: profile.name || "",
        email: profile.email || "",
        phone: profile.phone || "",
        address: profile.address || "",
        city: profile.city || "",
        country: profile.country || "",
      });
    }
  }, [profile, isEditing]);

  const queryClient = useQueryClient();
  const { showToast } = useToast();
  const [isSaving, setIsSaving] = useState(false);
  const [isSavingPassword, setIsSavingPassword] = useState(false);
  const [isResendingEmail, setIsResendingEmail] = useState(false);
  const [isCancellingEmail, setIsCancellingEmail] = useState(false);

  const handleInputChange = (field: string, value: string) => {
    setFormData((prev) => ({ ...prev, [field]: value }));
  };

  const handleSave = async () => {
    setIsSaving(true);
    try {
      // The form has a single "Name" field; the backend stores first/last
      // separately. Split on whitespace (first token = first name, rest = last).
      const parts = formData.name.trim().split(/\s+/).filter(Boolean);
      const firstName = parts.shift() || "";
      const lastName = parts.join(" ");

      // Email is NOT sent to the profile endpoint — it is the account login and
      // the backend strips it defensively. Changing it follows WordPress core's
      // pending-change pattern (handled separately below).
      const payload: Record<string, string> = {
        first_name: firstName,
        phone: formData.phone,
        address: formData.address,
        city: formData.city,
        country: formData.country,
      };
      // Only send last_name when present (backend rejects an empty last_name).
      if (lastName) {
        payload.last_name = lastName;
      }

      await apiClient.put(API_ENDPOINTS.CUSTOMER_ME, payload);

      // Refetch the parent's profile query so the saved values are reflected.
      await queryClient.invalidateQueries({ queryKey: ["account-profile"] });

      // If the email was edited, request the change separately. Like WordPress
      // core, this does NOT change the email now — it emails a confirmation link
      // to the new address and the change only applies once it is clicked.
      const currentEmail = (profile?.email || "").trim().toLowerCase();
      const nextEmail = formData.email.trim();
      if (nextEmail && nextEmail.toLowerCase() !== currentEmail) {
        try {
          const resp: any = await apiClient.post(
            API_ENDPOINTS.CUSTOMER_CHANGE_EMAIL,
            { email: nextEmail },
          );
          await queryClient.invalidateQueries({ queryKey: ["account-profile"] });
          showToast(
            resp?.message ||
              __(
                "A confirmation link has been sent to your new email address. The change takes effect once you confirm it.",
                "yatra",
              ),
            "success",
          );
        } catch (emailError: any) {
          showToast(
            emailError?.message ||
              __("Could not request the email change.", "yatra"),
            "error",
          );
        }
      } else {
        showToast(__("Profile updated successfully.", "yatra"), "success");
      }
      setIsEditing(false);
    } catch (error: any) {
      showToast(
        error?.message || __("Failed to update profile.", "yatra"),
        "error",
      );
    } finally {
      setIsSaving(false);
    }
  };

  const handleChangePassword = async () => {
    if (passwordData.newPassword !== passwordData.confirmPassword) {
      showToast(__("Passwords do not match.", "yatra"), "error");
      return;
    }

    setIsSavingPassword(true);
    try {
      await apiClient.post(API_ENDPOINTS.CUSTOMER_CHANGE_PASSWORD, {
        current_password: passwordData.currentPassword,
        new_password: passwordData.newPassword,
      });

      showToast(__("Password changed successfully.", "yatra"), "success");
      setIsChangingPassword(false);
      setPasswordData({
        currentPassword: "",
        newPassword: "",
        confirmPassword: "",
      });
      // Changing the password rotates the WP auth cookie + REST nonce, so the
      // current page's nonce is now stale. Reload to pick up a fresh one and
      // avoid 403s on subsequent actions.
      setTimeout(() => window.location.reload(), 1200);
    } catch (error: any) {
      showToast(
        error?.message || __("Failed to change password.", "yatra"),
        "error",
      );
    } finally {
      setIsSavingPassword(false);
    }
  };

  const handleResendEmailChange = async () => {
    setIsResendingEmail(true);
    try {
      const resp: any = await apiClient.post(
        API_ENDPOINTS.CUSTOMER_RESEND_EMAIL_CHANGE,
      );
      showToast(
        resp?.message ||
          __("Confirmation link re-sent to your new email address.", "yatra"),
        "success",
      );
    } catch (error: any) {
      showToast(
        error?.message ||
          __("Could not re-send the confirmation email.", "yatra"),
        "error",
      );
    } finally {
      setIsResendingEmail(false);
    }
  };

  const handleCancelEmailChange = async () => {
    setIsCancellingEmail(true);
    try {
      const resp: any = await apiClient.delete(
        API_ENDPOINTS.CUSTOMER_CHANGE_EMAIL,
      );
      await queryClient.invalidateQueries({ queryKey: ["account-profile"] });
      showToast(
        resp?.message ||
          __("The pending email change has been cancelled.", "yatra"),
        "success",
      );
    } catch (error: any) {
      showToast(
        error?.message ||
          __("Could not cancel the pending email change.", "yatra"),
        "error",
      );
    } finally {
      setIsCancellingEmail(false);
    }
  };

  const handleCancel = () => {
    setIsEditing(false);
    if (profile) {
      setFormData({
        name: profile.name || "",
        email: profile.email || "",
        phone: profile.phone || "",
        address: profile.address || "",
        city: profile.city || "",
        country: profile.country || "",
      });
    }
  };

  return (
    <div className="yatra-profile-page space-y-6">
      {/* Header */}
      <div className="yatra-profile-header bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
        <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
          <div>
            <h2 className="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-3">
              <div className="p-2 bg-yatra-soft dark:bg-yatra-surface-dark-muted rounded-lg">
                <User className="w-6 h-6 text-yatra-primary dark:text-yatra-on-dark" />
              </div>
              {__("Profile", "yatra")}
            </h2>
            <p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
              {__(
                "Update your contact details, address, and communication preferences.",
                "yatra",
              )}
            </p>
          </div>
          {!isEditing && (
            <div
              role="button"
              tabIndex={0}
              onClick={() => setIsEditing(true)}
              className="yatra-profile-edit-btn inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium cursor-pointer"
            >
              <PenSquare className="w-4 h-4" /> {__("Edit Profile", "yatra")}
            </div>
          )}
        </div>
      </div>

      {/* Profile Form */}
      <div className="yatra-profile-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
        <div className="yatra-profile-fields space-y-6">
          <div className="grid gap-6 md:grid-cols-2">
            <div className="yatra-profile-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("Full Name", "yatra")}{" "}
                <span className="text-red-500">*</span>
              </label>
              {isEditing ? (
                <input
                  type="text"
                  value={formData.name}
                  onChange={(e) => handleInputChange("name", e.target.value)}
                  className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                  placeholder={__("Enter your full name", "yatra")}
                />
              ) : (
                <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
                  {formData.name || __("Not set", "yatra")}
                </p>
              )}
            </div>

            <div className="yatra-profile-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("Email Address", "yatra")}
              </label>
              {isEditing ? (
                <>
                  <input
                    type="email"
                    value={formData.email}
                    onChange={(e) => handleInputChange("email", e.target.value)}
                    className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                    placeholder={__("you@example.com", "yatra")}
                  />
                  {/* Mirrors WordPress core: the email is not changed here — a
                      confirmation link is sent to the new address first. */}
                  <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
                    {__(
                      "For security, we'll email a confirmation link to the new address. Your email changes only after you click that link.",
                      "yatra",
                    )}
                  </p>
                </>
              ) : (
                <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
                  {formData.email || __("Not set", "yatra")}
                </p>
              )}
              {profile?.pending_email ? (
                <div className="mt-2 text-xs text-amber-600 dark:text-amber-400">
                  <p>
                    {sprintf(
                      /* translators: %s: the pending new email address. */
                      __(
                        "Pending confirmation for %s — check that inbox for the confirmation link.",
                        "yatra",
                      ),
                      profile.pending_email,
                    )}
                  </p>
                  <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
                    <button
                      type="button"
                      onClick={handleResendEmailChange}
                      disabled={isResendingEmail || isCancellingEmail}
                      className="font-medium text-yatra-primary underline hover:no-underline disabled:opacity-60"
                    >
                      {isResendingEmail
                        ? __("Sending…", "yatra")
                        : __("Resend confirmation email", "yatra")}
                    </button>
                    <button
                      type="button"
                      onClick={handleCancelEmailChange}
                      disabled={isResendingEmail || isCancellingEmail}
                      className="font-medium text-gray-500 dark:text-gray-400 underline hover:no-underline disabled:opacity-60"
                    >
                      {isCancellingEmail
                        ? __("Cancelling…", "yatra")
                        : __("Cancel change", "yatra")}
                    </button>
                  </div>
                </div>
              ) : null}
            </div>

            <div className="yatra-profile-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("Phone Number", "yatra")}
              </label>
              {isEditing ? (
                <input
                  type="tel"
                  value={formData.phone}
                  onChange={(e) => handleInputChange("phone", e.target.value)}
                  className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                  placeholder={__("Enter your phone number", "yatra")}
                />
              ) : (
                <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
                  {formData.phone || __("Not set", "yatra")}
                </p>
              )}
            </div>

            {/* Logical order: Country → City → Address (broad to specific). */}
            <div className="yatra-profile-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("Country", "yatra")}
              </label>
              {isEditing ? (
                <SearchableSelect
                  value={formData.country}
                  onChange={(v) => handleInputChange("country", v)}
                  options={getCountrySelectOptions()}
                  placeholder={__("Select your country", "yatra")}
                  searchPlaceholder={__("Search country", "yatra")}
                />
              ) : (
                <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
                  {getCountryName(formData.country) || __("Not set", "yatra")}
                </p>
              )}
            </div>

            <div className="yatra-profile-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("City", "yatra")}
              </label>
              {isEditing ? (
                <input
                  type="text"
                  value={formData.city}
                  onChange={(e) => handleInputChange("city", e.target.value)}
                  className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                  placeholder={__("Enter your city", "yatra")}
                />
              ) : (
                <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
                  {formData.city || __("Not set", "yatra")}
                </p>
              )}
            </div>

            <div className="yatra-profile-field md:col-span-2">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("Address", "yatra")}
              </label>
              {isEditing ? (
                <textarea
                  value={formData.address}
                  onChange={(e) => handleInputChange("address", e.target.value)}
                  rows={3}
                  className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                  placeholder={__("Enter your address", "yatra")}
                />
              ) : (
                <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
                  {formData.address || __("Not set", "yatra")}
                </p>
              )}
            </div>
          </div>

          {isEditing && (
            <div className="yatra-profile-actions flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
              <button
                type="button"
                onClick={handleSave}
                disabled={isSaving}
                className="inline-flex items-center gap-2 px-6 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
              >
                <CheckCircle className="w-4 h-4" />
                {isSaving
                  ? __("Saving…", "yatra")
                  : __("Save Changes", "yatra")}
              </button>
              <div
                role="button"
                tabIndex={0}
                onClick={handleCancel}
                className="inline-flex items-center gap-2 px-6 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium cursor-pointer"
              >
                <XCircle className="w-4 h-4" />
                {__("Cancel", "yatra")}
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Password Change Section */}
      <div className="yatra-profile-password bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
        <div className="flex items-center justify-between mb-6">
          <div>
            <h3 className="text-base font-semibold text-gray-900 dark:text-white flex items-center gap-2">
              <ShieldCheck className="w-5 h-5 text-yatra-primary dark:text-yatra-on-dark" />
              {__("Change Password", "yatra")}
            </h3>
            <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
              {__("Update your password to keep your account secure.", "yatra")}
            </p>
          </div>
          {!isChangingPassword && (
            <div
              role="button"
              tabIndex={0}
              onClick={() => setIsChangingPassword(true)}
              className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium cursor-pointer"
            >
              <ShieldCheck className="w-4 h-4" />
              {__("Change Password", "yatra")}
            </div>
          )}
        </div>

        {isChangingPassword && (
          <div className="space-y-4">
            <div className="yatra-password-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("Current Password", "yatra")}{" "}
                <span className="text-red-500">*</span>
              </label>
              <input
                type="password"
                value={passwordData.currentPassword}
                onChange={(e) =>
                  setPasswordData((prev) => ({
                    ...prev,
                    currentPassword: e.target.value,
                  }))
                }
                className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                placeholder={__("Enter your current password", "yatra")}
              />
            </div>

            <div className="yatra-password-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("New Password", "yatra")}{" "}
                <span className="text-red-500">*</span>
              </label>
              <input
                type="password"
                value={passwordData.newPassword}
                onChange={(e) =>
                  setPasswordData((prev) => ({
                    ...prev,
                    newPassword: e.target.value,
                  }))
                }
                className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                placeholder={__("Enter new password", "yatra")}
              />
            </div>

            <div className="yatra-password-field">
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                {__("Confirm New Password", "yatra")}{" "}
                <span className="text-red-500">*</span>
              </label>
              <input
                type="password"
                value={passwordData.confirmPassword}
                onChange={(e) =>
                  setPasswordData((prev) => ({
                    ...prev,
                    confirmPassword: e.target.value,
                  }))
                }
                className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
                placeholder={__("Confirm your new password", "yatra")}
              />
              {passwordData.newPassword &&
                passwordData.confirmPassword &&
                passwordData.newPassword !== passwordData.confirmPassword && (
                  <p className="text-xs text-red-500 mt-1">
                    {__("Passwords do not match", "yatra")}
                  </p>
                )}
            </div>

            <div className="yatra-password-actions flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
              <button
                type="button"
                onClick={handleChangePassword}
                disabled={
                  isSavingPassword ||
                  !passwordData.currentPassword ||
                  !passwordData.newPassword ||
                  !passwordData.confirmPassword ||
                  passwordData.newPassword !== passwordData.confirmPassword
                }
                className="inline-flex items-center gap-2 px-6 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
              >
                <CheckCircle className="w-4 h-4" />
                {isSavingPassword
                  ? __("Updating…", "yatra")
                  : __("Update Password", "yatra")}
              </button>
              <div
                role="button"
                tabIndex={0}
                onClick={() => {
                  setIsChangingPassword(false);
                  setPasswordData({
                    currentPassword: "",
                    newPassword: "",
                    confirmPassword: "",
                  });
                }}
                className="inline-flex items-center gap-2 px-6 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium cursor-pointer"
              >
                <XCircle className="w-4 h-4" />
                {__("Cancel", "yatra")}
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Additional Sections.
          Communication Preferences card was removed: it shipped as four
          checkboxes with `defaultChecked` and no onChange / no save
          handler / no API persistence — purely decorative. Showing
          fake toggles is worse than not shipping the feature because
          customers expect a "Save" round-trip and refresh-revert.
          Re-introduce only when there's a real preferences endpoint
          + service-side honouring (suppress emails when toggled off). */}
      <div className="yatra-profile-sections grid gap-6 lg:grid-cols-1">
        {wishlistEnabled && (
          <div className="yatra-profile-saved-trips bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
            <h3 className="text-base font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
              <Heart className="w-5 h-5 text-red-600 dark:text-red-400" />
              {__("Saved Trips", "yatra")}
            </h3>
            <div className="yatra-saved-trips-list space-y-3">
              {savedTrips.length > 0 ? (
                savedTrips.map((trip: any) => (
                  <div
                    key={trip.id || trip.trip_id}
                    className="yatra-saved-trip-item flex items-center justify-between border border-gray-100 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-900/40 transition-colors"
                  >
                    <div className="flex-1 min-w-0">
                      <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
                        {trip.trip_title || trip.title}
                      </p>
                      {(trip.location || trip.duration) && (
                        <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
                          {trip.location || trip.duration}
                        </p>
                      )}
                    </div>
                    <div className="text-right ml-4 flex-shrink-0">
                      <p className="text-xs text-gray-500 dark:text-gray-400">
                        {__("From", "yatra")}
                      </p>
                      <p className="text-sm font-semibold text-gray-900 dark:text-white">
                        {currency(trip.price_from || trip.price || 0)}
                      </p>
                    </div>
                  </div>
                ))
              ) : (
                <p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
                  {__("No saved trips yet", "yatra")}
                </p>
              )}
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default Profile;

```
