"use client";
// ============================================================
// Profile — header card + 6 tabs:
// Personal Info · KYC · Security · Notifications · Privacy · Devices
// ============================================================
import { useEffect, useMemo, useState } from "react";
import {
user, kycDocTypes, kyc as kycSeed, contactPrefs, contactDestinations,
contactTimezones, contactTimes, loginActivity, consentGroups,
passwordStrength, maskEmail, maskPhone,
type KycSlot, type NotifCategory, type ConsentGroup,
} from "./account-data";
import {
Avatar, Btn, Field, Icon, Modal, OtpField, PageHead, Pill, SegTabs,
StatusDot, Toggle, useToast,
} from "./ui";
const TABS = [
{ value: "personal", label: "Personal Info", icon: "user" },
{ value: "kyc", label: "KYC", icon: "shield-check" },
{ value: "security", label: "Security", icon: "lock" },
{ value: "notifications", label: "Notifications", icon: "bell" },
{ value: "privacy", label: "Privacy & Consent", icon: "privacy" },
{ value: "devices", label: "Devices", icon: "devices" },
];
export function Profile() {
const [tab, setTab] = useState("personal");
return (
Account active}
/>
{tab === "personal" &&
}
{tab === "kyc" &&
}
{tab === "security" &&
}
{tab === "notifications" &&
}
{tab === "privacy" &&
}
{tab === "devices" &&
}
);
}
/* ============================================================ */
/* Header card */
/* ============================================================ */
function ProfileHeaderCard() {
const { push } = useToast();
return (
{user.name}
{user.role} · Member since {user.memberSince}
{user.sector} · {user.pocket}
{user.category}
House {user.plot}
);
}
function HeroStat({ value, label, accent }: { value: string; label: string; accent?: string }) {
return (
);
}
/* ============================================================ */
/* Tab: Personal Info — verify-to-reveal + change email/mobile */
/* ============================================================ */
type ContactKind = "email" | "mobile" | "whatsapp";
function PersonalInfoTab() {
const { push } = useToast();
const [verified, setVerified] = useState(false);
const [plot, setPlot] = useState("");
const [err, setErr] = useState("");
const [change, setChange] = useState(null);
// editable contact values (live-updating after OTP confirmation)
const [contacts, setContacts] = useState({ email: user.email.full, mobile: user.mobile.full, whatsapp: user.whatsapp.full });
function verify() {
if (plot.trim() === user.plot) { setVerified(true); setErr(""); push({ tone: "success", title: "Verified", desc: "Your full details are now visible and editable." }); }
else setErr(`That doesn't match the house number on file (hint: it's ${user.plot}).`);
}
function mask(kind: ContactKind, full: string) {
return kind === "email" ? maskEmail(full) : maskPhone(full);
}
const contactRows: { key: ContactKind; label: string; full: string; masked: string }[] = [
{ key: "email", label: "Email", full: contacts.email, masked: mask("email", contacts.email) },
{ key: "mobile", label: "Mobile", full: contacts.mobile, masked: mask("mobile", contacts.mobile) },
{ key: "whatsapp", label: "WhatsApp", full: contacts.whatsapp, masked: mask("whatsapp", contacts.whatsapp) },
];
return (
Contact details
{verified
?
Revealed
:
Masked}
{!verified && (
Verify to reveal & edit
Sensitive details are masked. Enter your house number to unmask and edit them.
{ setPlot(e.target.value.replace(/\D/g, "")); setErr(""); }}
onKeyDown={(e) => e.key === "Enter" && verify()} />
Reveal
{err &&
{err}
}
)}
{contactRows.map((f) => (
- {f.label}
-
{verified ? f.full : f.masked}
{verified && (
)}
))}
setChange("email")} disabled={!verified}>Change email
setChange("mobile")} disabled={!verified}>Change mobile
{!verified &&
Reveal your details first to edit contact information.
}
Account & identity
{verified
?
Revealed
:
Protected}
- Full name
- {user.name}
- Role
- {user.role}
- Member since
- {user.memberSince}
- PAN
- {verified ? user.pan.full : user.pan.masked}{verified && via KYC}
- Aadhaar
- {verified ? user.aadhaar.full : user.aadhaar.masked}{verified && via KYC}
PAN & Aadhaar are managed under the KYC tab and can't be edited here.
setChange(null)}
onSave={(kind, value) => setContacts((c) => ({ ...c, [kind]: value }))}
/>
);
}
const CONTACT_LABEL: Record = { email: "email", mobile: "mobile", whatsapp: "WhatsApp number" };
// OTP-to-registered-mobile flow for changing a contact field. On confirm the
// new value is saved back so the Personal Info card updates live.
function ChangeContactModal({ kind, current, onClose, onSave }: {
kind: null | ContactKind; current: string; onClose: () => void; onSave: (kind: ContactKind, value: string) => void;
}) {
const { push } = useToast();
const [step, setStep] = useState<"enter" | "otp">("enter");
const [val, setVal] = useState("");
const [otp, setOtp] = useState("");
// prefill with the current value whenever a new field is opened
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (kind) { setStep("enter"); setVal(current); setOtp(""); }
}, [kind, current]);
function close() { setStep("enter"); setVal(""); setOtp(""); onClose(); }
const isEmail = kind === "email";
const label = kind ? CONTACT_LABEL[kind] : "";
return (
Cancel { setStep("otp"); push({ tone: "info", title: "Code sent", desc: `OTP sent to your registered mobile ${user.mobile.masked}.` }); }}>Send code>
: <> setStep("enter")}>Back { if (kind) onSave(kind, val.trim()); push({ tone: "success", title: `${label[0].toUpperCase()}${label.slice(1)} updated`, desc: "Your change has been confirmed." }); close(); }}>Confirm>
}
>
{step === "enter" ? (
setVal(e.target.value)} />
) : (
)}
);
}
/* ============================================================ */
/* Tab: KYC — inline upload, ID≠address, photo mandatory, % */
/* ============================================================ */
function KycTab() {
const { push } = useToast();
const [slots, setSlots] = useState(kycSeed.slots);
const idDoc = slots.find((s) => s.key === "id")?.docId ?? null;
const addrDoc = slots.find((s) => s.key === "address")?.docId ?? null;
const completion = useMemo(() => {
const done = slots.filter((s) => s.status === "verified" || s.status === "uploaded").length;
return Math.round((done / slots.length) * 100);
}, [slots]);
function options(key: KycSlot["key"]) {
const want = key === "id" ? ["id", "any"] : key === "address" ? ["address", "any"] : [];
return kycDocTypes.filter((d) => want.includes(d.use));
}
function pickDoc(key: KycSlot["key"], docId: string) {
// enforce two-different-documents rule between ID and address
if (key === "id" && docId === addrDoc) { push({ tone: "error", title: "Pick a different document", desc: "Identity and address proofs must be two different documents." }); return; }
if (key === "address" && docId === idDoc) { push({ tone: "error", title: "Pick a different document", desc: "Address and identity proofs must be two different documents." }); return; }
setSlots((s) => s.map((x) => x.key === key ? { ...x, docId } : x));
}
function upload(key: KycSlot["key"]) {
const slot = slots.find((s) => s.key === key)!;
if (key !== "photo" && !slot.docId) { push({ tone: "error", title: "Select a document type first" }); return; }
const name = key === "photo" ? "selfie.jpg" : `${slot.docId}_upload.pdf`;
setSlots((s) => s.map((x) => x.key === key ? { ...x, fileName: name, status: "uploaded", note: "Under review" } : x));
push({ tone: "success", title: "Uploaded", desc: "Your document is queued for review." });
}
function remove(key: KycSlot["key"]) {
setSlots((s) => s.map((x) => x.key === key ? { ...x, fileName: null, docId: key === "photo" ? null : x.docId, status: "missing", note: undefined } : x));
}
const photoDone = slots.find((s) => s.key === "photo")!.status !== "missing";
return (
Verification documents
{completion}% complete
{slots.map((s) => (
{s.title}
))}
{!photoDone &&
Live photo is mandatory.KYC cannot be completed without a verified photo.
}
{slots.map((slot) => (
pickDoc(slot.key, d)} onUpload={() => upload(slot.key)} onRemove={() => remove(slot.key)} />
))}
Rules
- A live photo / selfie is mandatory.
- Provide one ID and one address proof.
- ID and address must be two different documents.
- Accepted: PDF, JPG, PNG · size limits per document.
- Review completes within ~4 business hours.
Current selection
Identity{kycDocTypes.find((d) => d.id === idDoc)?.label ?? "Not chosen"}
Address{kycDocTypes.find((d) => d.id === addrDoc)?.label ?? "Not chosen"}
);
}
function KycSlotRow({ slot, options, onPick, onUpload, onRemove }: {
slot: KycSlot; options: typeof kycDocTypes; onPick: (d: string) => void; onUpload: () => void; onRemove: () => void;
}) {
const tone = slot.status === "verified" ? "green" : slot.status === "uploaded" ? "blue" : slot.status === "rejected" ? "red" : "muted";
const docMeta = kycDocTypes.find((d) => d.id === slot.docId);
return (
{slot.title}{slot.required && *}
{slot.status === "missing" ? "Missing" : slot.status === "uploaded" ? "Under review" : slot.status === "verified" ? "Verified" : "Rejected"}
{slot.key !== "photo" && (
)}
{docMeta &&
{docMeta.hint} · {docMeta.formats} · ≤ {docMeta.maxMb} MB
}
{slot.fileName ? (
{slot.fileName}
{slot.note && {slot.note}}
) : (
)}
);
}
/* ============================================================ */
/* Tab: Security */
/* ============================================================ */
function SecurityTab() {
const { push } = useToast();
const [pw, setPw] = useState("");
const [twoFA, setTwoFA] = useState(false);
const [authApp, setAuthApp] = useState(false);
const [alerts, setAlerts] = useState(true);
const [twoFaModal, setTwoFaModal] = useState(false);
const strength = passwordStrength(pw);
return (
Password
Updated 5 days ago
setPw(e.target.value)} />
{pw && (
{[0, 1, 2, 3].map((i) => )}
{strength.label}
)}
{strength.checks.map((c) => - {c.label}
)}
{ push({ tone: "success", title: "Password updated" }); setPw(""); }}>Update password
Two-factor & sign-in
{ if (v) setTwoFaModal(true); else { setTwoFA(false); setAuthApp(false); push({ tone: "info", title: "Two-factor disabled" }); } }} label="2FA" />
{ setAuthApp(v); push({ tone: v ? "success" : "info", title: v ? "Authenticator linked" : "Authenticator removed" }); }} label="Authenticator" />
{ setAlerts(v); push({ tone: "info", title: v ? "Login alerts on" : "Login alerts off" }); }} label="Login alerts" />
{!twoFA &&
Enable two-factor authentication to link an authenticator app.
}
setTwoFaModal(false)} onDone={() => { setTwoFA(true); setTwoFaModal(false); push({ tone: "success", title: "Two-factor enabled", desc: "Your account is now protected with 2FA." }); }} />
);
}
function TwoFaModal({ open, onClose, onDone }: { open: boolean; onClose: () => void; onDone: () => void }) {
const { push } = useToast();
const [step, setStep] = useState<"password" | "otp">("password");
const [pw, setPw] = useState("");
const [otp, setOtp] = useState("");
function close() { setStep("password"); setPw(""); setOtp(""); onClose(); }
return (
Cancel { setStep("otp"); push({ tone: "info", title: "Code sent", desc: "OTP sent to your registered mobile." }); }}>Continue>
: <> setStep("password")}>BackEnable 2FA>}
>
{step === "password" ? (
setPw(e.target.value)} />
) : (
Both password and OTP are required to turn 2FA on.
)}
);
}
function SettingRow({ icon, title, desc, children }: { icon: string; title: string; desc: string; children: React.ReactNode }) {
return (
);
}
/* ============================================================ */
/* Tab: Notifications */
/* ============================================================ */
function NotificationsTab() {
const { push } = useToast();
const [prefs, setPrefs] = useState(contactPrefs);
const [tz, setTz] = useState(contactTimezones[0]);
const [times, setTimes] = useState(contactTimes);
const channels: { key: keyof NotifCategory; label: string; icon: string }[] = [
{ key: "email", label: "Email", icon: "mail" },
{ key: "sms", label: "SMS", icon: "phone" },
{ key: "whatsapp", label: "WhatsApp", icon: "chat" },
{ key: "push", label: "Push", icon: "bell" },
];
function toggle(catId: string, ch: keyof NotifCategory) {
setPrefs((p) => p.map((c) => {
if (c.id !== catId) return c;
if (c.locked) { push({ tone: "info", title: "Required notifications", desc: "Security alerts can't be turned off." }); return c; }
return { ...c, [ch]: !c[ch] } as NotifCategory;
}));
}
return (
Per-channel matrix
Choose how each category reaches you
Category
{channels.map((c) => {c.label})}
{prefs.map((cat) => (
{cat.label}{cat.locked && }
{cat.desc}
{channels.map((c) => (
toggle(cat.id, c.key)} label={`${cat.label} ${c.label}`} />
))}
))}
Contact-time window
{times.map((t) => (
))}
Callbacks & proactive messages only arrive inside enabled windows, in your timezone.
Destinations
{contactDestinations.map((d) => (
{d.label}{d.primary &&
Primary}
{d.value}
{d.verified ?
Verified :
push({ tone: "info", title: "Verification sent" })}>Verify}
))}
push({ tone: "info", title: "Add destination" })}>Add destination
);
}
/* ============================================================ */
/* Tab: Privacy & Consent */
/* ============================================================ */
function PrivacyTab() {
const { push } = useToast();
const [groups, setGroups] = useState(consentGroups);
function toggle(gid: string, iid: string) {
setGroups((gs) => gs.map((g) => g.id !== gid ? g : {
...g,
items: g.items.map((it) => {
if (it.id !== iid) return it;
if (it.locked) { push({ tone: "info", title: "Required consent", desc: "Statutory consents can't be withdrawn while active." }); return it; }
return { ...it, granted: !it.granted };
}),
}));
}
return (
{groups.map((g) => (
{g.title}
{g.id === "marketing" &&
Optional}
{g.id === "essential" &&
Required}
{g.items.map((it) => (
{it.label}{it.locked && }
{it.desc}
toggle(g.id, it.id)} label={it.label} />
))}
{g.id === "marketing" && groups.find((x) => x.id === "marketing")!.items.find((i) => i.id === "promos")!.granted && (
Promotional sub-preferences
{["Product offers", "Surveys & feedback", "Partner news"].map((s, i) => (
))}
)}
))}
);
}
/* ============================================================ */
/* Tab: Devices */
/* ============================================================ */
function DevicesTab() {
const { push } = useToast();
const [devices, setDevices] = useState(loginActivity);
function signOut(id: string) {
setDevices((d) => d.filter((x) => x.id !== id));
push({ tone: "success", title: "Signed out", desc: "That device no longer has access." });
}
return (
Connected devices
{ setDevices((d) => d.filter((x) => x.current)); push({ tone: "success", title: "Other devices signed out" }); }}>Sign out all others
{devices.filter((d) => d.event !== "password-change").map((d) => (
{d.device}{d.current &&
This device}{!d.trusted &&
New}
{d.browser} · {d.os}
{d.location} · {d.ip} · {d.lastActive}
{!d.current &&
}
))}
Recent activity
{loginActivity.map((a) => (
{labelForEvent(a.event)}
{a.device} · {a.location}
{a.lastActive}
))}
);
}
function labelForEvent(ev: string) {
switch (ev) {
case "password-change": return "Password changed";
case "new-device": return "New device signed in";
case "2fa-enabled": return "Two-factor enabled";
case "sign-out": return "Signed out";
default: return "Successful sign-in";
}
}