2025-09-17 18:43:43 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-12-23 15:19:20 +09:00
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
2025-10-22 10:58:16 +09:00
|
|
|
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
2025-09-25 15:54:54 +09:00
|
|
|
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
2025-09-25 17:42:36 +09:00
|
|
|
import {
|
|
|
|
|
MapPinIcon,
|
|
|
|
|
PencilIcon,
|
|
|
|
|
CheckIcon,
|
|
|
|
|
XMarkIcon,
|
|
|
|
|
UserIcon,
|
2025-12-23 15:19:20 +09:00
|
|
|
ShieldCheckIcon,
|
2025-09-25 17:42:36 +09:00
|
|
|
} from "@heroicons/react/24/outline";
|
2025-12-29 18:19:27 +09:00
|
|
|
import { useAuthStore } from "@/features/auth/stores/auth.store";
|
|
|
|
|
import { accountService } from "@/features/account/api/account.api";
|
2025-09-17 18:43:43 +09:00
|
|
|
import { useProfileEdit } from "@/features/account/hooks/useProfileEdit";
|
2025-12-25 13:20:45 +09:00
|
|
|
import { AddressForm } from "@/features/services/components/base/AddressForm";
|
2025-09-20 11:35:40 +09:00
|
|
|
import { Button } from "@/components/atoms/button";
|
2025-12-23 15:19:20 +09:00
|
|
|
import { StatusPill } from "@/components/atoms/status-pill";
|
2025-09-17 18:43:43 +09:00
|
|
|
import { useAddressEdit } from "@/features/account/hooks/useAddressEdit";
|
2025-12-23 15:19:20 +09:00
|
|
|
import {
|
|
|
|
|
useResidenceCardVerification,
|
|
|
|
|
useSubmitResidenceCard,
|
|
|
|
|
} from "@/features/verification/hooks/useResidenceCardVerification";
|
2025-12-16 13:54:31 +09:00
|
|
|
import { PageLayout } from "@/components/templates";
|
2025-12-29 18:19:27 +09:00
|
|
|
import { formatIsoDate } from "@/shared/utils";
|
2025-09-17 18:43:43 +09:00
|
|
|
|
|
|
|
|
export default function ProfileContainer() {
|
|
|
|
|
const { user } = useAuthStore();
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [editingProfile, setEditingProfile] = useState(false);
|
|
|
|
|
const [editingAddress, setEditingAddress] = useState(false);
|
2025-12-16 13:54:31 +09:00
|
|
|
const [reloadKey, setReloadKey] = useState(0);
|
2025-09-17 18:43:43 +09:00
|
|
|
|
|
|
|
|
const profile = useProfileEdit({
|
2025-12-15 17:29:28 +09:00
|
|
|
email: user?.email || "",
|
2025-10-09 10:49:03 +09:00
|
|
|
phonenumber: user?.phonenumber || "",
|
2025-09-17 18:43:43 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const address = useAddressEdit({
|
2025-10-07 17:38:39 +09:00
|
|
|
address1: "",
|
|
|
|
|
address2: "",
|
2025-09-17 18:43:43 +09:00
|
|
|
city: "",
|
|
|
|
|
state: "",
|
2025-10-07 17:38:39 +09:00
|
|
|
postcode: "",
|
2025-09-17 18:43:43 +09:00
|
|
|
country: "",
|
2025-10-07 17:38:39 +09:00
|
|
|
countryCode: "",
|
|
|
|
|
phoneNumber: "",
|
|
|
|
|
phoneCountryCode: "",
|
2025-09-17 18:43:43 +09:00
|
|
|
});
|
|
|
|
|
|
2025-12-23 15:19:20 +09:00
|
|
|
// ID Verification status from Salesforce
|
|
|
|
|
const verificationQuery = useResidenceCardVerification();
|
|
|
|
|
const submitResidenceCard = useSubmitResidenceCard();
|
|
|
|
|
const verificationStatus = verificationQuery.data?.status;
|
|
|
|
|
const [verificationFile, setVerificationFile] = useState<File | null>(null);
|
|
|
|
|
const verificationFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
|
const canUploadVerification = verificationStatus !== "verified";
|
|
|
|
|
|
2026-01-15 11:28:25 +09:00
|
|
|
// Helper to render verification status pill
|
|
|
|
|
const renderVerificationStatusPill = () => {
|
|
|
|
|
if (verificationQuery.isLoading) {
|
|
|
|
|
return <Skeleton className="h-6 w-20" />;
|
|
|
|
|
}
|
|
|
|
|
switch (verificationStatus) {
|
|
|
|
|
case "verified":
|
|
|
|
|
return <StatusPill label="Verified" variant="success" />;
|
|
|
|
|
case "pending":
|
|
|
|
|
return <StatusPill label="Under Review" variant="info" />;
|
|
|
|
|
case "rejected":
|
|
|
|
|
return <StatusPill label="Action Needed" variant="warning" />;
|
|
|
|
|
default:
|
|
|
|
|
return <StatusPill label="Required for SIM" variant="warning" />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper to render verification content based on status
|
|
|
|
|
const renderVerificationContent = () => {
|
|
|
|
|
if (verificationQuery.isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<Skeleton className="h-4 w-48" />
|
|
|
|
|
<Skeleton className="h-4 w-32" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (verificationStatus === "verified") {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Your identity has been verified. No further action is needed.
|
|
|
|
|
</p>
|
|
|
|
|
{verificationQuery.data?.reviewedAt && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Verified on{" "}
|
|
|
|
|
{formatIsoDate(verificationQuery.data.reviewedAt, { dateStyle: "medium" })}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (verificationStatus === "pending") {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<AlertBanner variant="info" title="Under review" size="sm" elevated>
|
|
|
|
|
Your residence card has been submitted. We'll verify it before activating SIM
|
|
|
|
|
service.
|
|
|
|
|
</AlertBanner>
|
|
|
|
|
{verificationQuery.data?.submittedAt && (
|
|
|
|
|
<div className="rounded-lg border border-border bg-muted/30 px-4 py-3">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Submission status
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
Submitted on{" "}
|
|
|
|
|
{formatIsoDate(verificationQuery.data.submittedAt, { dateStyle: "medium" })}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default: rejected or not submitted
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
// Extract stable setValue functions to avoid infinite re-render loop.
|
|
|
|
|
// The hook objects (address, profile) are recreated every render, but
|
|
|
|
|
// the setValue callbacks inside them are stable (memoized with useCallback).
|
|
|
|
|
const setAddressValue = address.setValue;
|
|
|
|
|
const setProfileValue = profile.setValue;
|
2025-10-29 13:29:28 +09:00
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
const loadProfile = useCallback(() => {
|
2025-09-17 18:43:43 +09:00
|
|
|
void (async () => {
|
|
|
|
|
try {
|
2025-12-16 13:54:31 +09:00
|
|
|
setError(null);
|
2025-09-17 18:43:43 +09:00
|
|
|
setLoading(true);
|
|
|
|
|
const [addr, prof] = await Promise.all([
|
|
|
|
|
accountService.getAddress().catch(() => null),
|
|
|
|
|
accountService.getProfile().catch(() => null),
|
|
|
|
|
]);
|
|
|
|
|
if (addr) {
|
2025-12-16 13:54:31 +09:00
|
|
|
setAddressValue("address1", addr.address1 ?? "");
|
|
|
|
|
setAddressValue("address2", addr.address2 ?? "");
|
|
|
|
|
setAddressValue("city", addr.city ?? "");
|
|
|
|
|
setAddressValue("state", addr.state ?? "");
|
|
|
|
|
setAddressValue("postcode", addr.postcode ?? "");
|
|
|
|
|
setAddressValue("country", addr.country ?? "");
|
|
|
|
|
setAddressValue("countryCode", addr.countryCode ?? "");
|
|
|
|
|
setAddressValue("phoneNumber", addr.phoneNumber ?? "");
|
|
|
|
|
setAddressValue("phoneCountryCode", addr.phoneCountryCode ?? "");
|
2025-09-17 18:43:43 +09:00
|
|
|
}
|
|
|
|
|
if (prof) {
|
2025-12-16 13:54:31 +09:00
|
|
|
setProfileValue("email", prof.email || "");
|
|
|
|
|
setProfileValue("phonenumber", prof.phonenumber || "");
|
2025-12-23 15:19:20 +09:00
|
|
|
// Spread all profile fields into the user state, including sfNumber, dateOfBirth, gender
|
2025-09-17 18:43:43 +09:00
|
|
|
useAuthStore.setState(state => ({
|
|
|
|
|
...state,
|
|
|
|
|
user: state.user
|
|
|
|
|
? {
|
|
|
|
|
...state.user,
|
2025-12-23 15:19:20 +09:00
|
|
|
...prof,
|
2025-09-17 18:43:43 +09:00
|
|
|
}
|
2025-12-23 15:19:20 +09:00
|
|
|
: prof,
|
2025-09-17 18:43:43 +09:00
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2025-12-16 13:54:31 +09:00
|
|
|
// Keep message customer-safe (no internal details)
|
2025-09-17 18:43:43 +09:00
|
|
|
setError(e instanceof Error ? e.message : "Failed to load profile data");
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
})();
|
2025-12-16 13:54:31 +09:00
|
|
|
}, [setAddressValue, setProfileValue]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadProfile();
|
|
|
|
|
}, [loadProfile, reloadKey]);
|
2025-09-17 18:43:43 +09:00
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
2025-12-16 13:54:31 +09:00
|
|
|
<PageLayout
|
|
|
|
|
icon={<UserIcon />}
|
|
|
|
|
title="Profile"
|
|
|
|
|
description="Manage your account information"
|
|
|
|
|
loading
|
|
|
|
|
>
|
|
|
|
|
<div className="space-y-8">
|
|
|
|
|
<div className="bg-card border border-border rounded-xl shadow-[var(--cp-shadow-1)]">
|
|
|
|
|
<div className="px-6 py-5 border-b border-border">
|
2025-09-17 18:43:43 +09:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-3">
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="h-6 w-6 bg-muted rounded" />
|
|
|
|
|
<div className="h-6 w-40 bg-muted rounded" />
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="h-8 w-20 bg-muted rounded" />
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
|
|
|
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
|
|
|
<div key={i} className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-24" />
|
|
|
|
|
<Skeleton className="h-10 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<div className="sm:col-span-2">
|
|
|
|
|
<Skeleton className="h-4 w-28 mb-3" />
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-09-17 18:43:43 +09:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Skeleton className="h-5 w-48" />
|
|
|
|
|
<Skeleton className="h-5 w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
<Skeleton className="h-3 w-64 mt-2" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-border mt-6">
|
2025-09-17 18:43:43 +09:00
|
|
|
<Skeleton className="h-9 w-24" />
|
|
|
|
|
<Skeleton className="h-9 w-28" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="bg-card border border-border rounded-xl shadow-[var(--cp-shadow-1)]">
|
|
|
|
|
<div className="px-6 py-5 border-b border-border">
|
2025-09-17 18:43:43 +09:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-3">
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="h-6 w-6 bg-muted rounded" />
|
|
|
|
|
<div className="h-6 w-48 bg-muted rounded" />
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="h-8 w-20 bg-muted rounded" />
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-6">
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-09-17 18:43:43 +09:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-60" />
|
|
|
|
|
<Skeleton className="h-4 w-48" />
|
|
|
|
|
<Skeleton className="h-4 w-52" />
|
|
|
|
|
<Skeleton className="h-4 w-32" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-end space-x-3 pt-6">
|
|
|
|
|
<Skeleton className="h-9 w-24" />
|
|
|
|
|
<Skeleton className="h-9 w-28" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</PageLayout>
|
2025-09-17 18:43:43 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-16 13:54:31 +09:00
|
|
|
<PageLayout
|
|
|
|
|
icon={<UserIcon />}
|
|
|
|
|
title="Profile"
|
|
|
|
|
description="Manage your account information"
|
|
|
|
|
error={error}
|
|
|
|
|
onRetry={() => setReloadKey(k => k + 1)}
|
|
|
|
|
>
|
|
|
|
|
{error && (
|
|
|
|
|
<AlertBanner variant="error" title="Unable to load profile" className="mb-6" elevated>
|
|
|
|
|
{error}
|
|
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
2025-09-17 18:43:43 +09:00
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="bg-card text-card-foreground rounded-xl border border-border shadow-[var(--cp-shadow-1)]">
|
|
|
|
|
<div className="px-6 py-5 border-b border-border">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
<UserIcon className="h-6 w-6 text-primary" />
|
|
|
|
|
<h2 className="text-xl font-semibold text-foreground">Personal Information</h2>
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
{!editingProfile && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setEditingProfile(true)}
|
|
|
|
|
leftIcon={<PencilIcon className="h-4 w-4" />}
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
2025-09-17 18:43:43 +09:00
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="p-6">
|
|
|
|
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-muted-foreground mb-2">
|
|
|
|
|
First Name
|
|
|
|
|
</label>
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-12-16 13:54:31 +09:00
|
|
|
<p className="text-base text-foreground font-medium">
|
|
|
|
|
{user?.firstname || (
|
|
|
|
|
<span className="text-muted-foreground italic">Not provided</span>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
|
|
|
Name cannot be changed from the portal.
|
|
|
|
|
</p>
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-muted-foreground mb-2">
|
|
|
|
|
Last Name
|
|
|
|
|
</label>
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-12-16 13:54:31 +09:00
|
|
|
<p className="text-base text-foreground font-medium">
|
|
|
|
|
{user?.lastname || (
|
|
|
|
|
<span className="text-muted-foreground italic">Not provided</span>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
|
|
|
Name cannot be changed from the portal.
|
|
|
|
|
</p>
|
2025-12-15 17:29:28 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
|
|
|
|
<div className="sm:col-span-2">
|
|
|
|
|
<label className="block text-sm font-medium text-muted-foreground mb-2">
|
|
|
|
|
Email Address
|
|
|
|
|
</label>
|
|
|
|
|
{editingProfile ? (
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
value={profile.values.email}
|
|
|
|
|
onChange={e => profile.setValue("email", e.target.value)}
|
|
|
|
|
className="block w-full px-4 py-2.5 border border-input rounded-lg bg-background text-foreground shadow-[var(--cp-shadow-1)] focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<p className="text-base text-foreground font-medium">{user?.email}</p>
|
2025-12-15 17:29:28 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
|
|
|
Email can be updated from the portal.
|
2025-12-15 17:29:28 +09:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-15 17:29:28 +09:00
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-muted-foreground mb-2">
|
|
|
|
|
Customer Number
|
|
|
|
|
</label>
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-12-16 13:54:31 +09:00
|
|
|
<p className="text-base text-foreground font-medium">
|
|
|
|
|
{user?.sfNumber || (
|
|
|
|
|
<span className="text-muted-foreground italic">Not available</span>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">Customer number is read-only.</p>
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-muted-foreground mb-2">
|
|
|
|
|
Date of Birth
|
|
|
|
|
</label>
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-12-16 13:54:31 +09:00
|
|
|
<p className="text-base text-foreground font-medium">
|
|
|
|
|
{user?.dateOfBirth || (
|
|
|
|
|
<span className="text-muted-foreground italic">Not provided</span>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
|
|
|
Date of birth is stored in billing profile.
|
|
|
|
|
</p>
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-muted-foreground mb-2">
|
|
|
|
|
Phone Number
|
|
|
|
|
</label>
|
|
|
|
|
{editingProfile ? (
|
|
|
|
|
<input
|
|
|
|
|
type="tel"
|
|
|
|
|
value={profile.values.phonenumber}
|
|
|
|
|
onChange={e => profile.setValue("phonenumber", e.target.value)}
|
|
|
|
|
placeholder="+81 XX-XXXX-XXXX"
|
|
|
|
|
className="block w-full px-4 py-2.5 border border-input rounded-lg bg-background text-foreground shadow-[var(--cp-shadow-1)] focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-base text-foreground py-2">
|
|
|
|
|
{user?.phonenumber || (
|
|
|
|
|
<span className="text-muted-foreground italic">Not provided</span>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-15 17:29:28 +09:00
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-muted-foreground mb-2">Gender</label>
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
2025-12-16 13:54:31 +09:00
|
|
|
<p className="text-base text-foreground font-medium">
|
|
|
|
|
{user?.gender || (
|
|
|
|
|
<span className="text-muted-foreground italic">Not provided</span>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
|
|
|
Gender is stored in billing profile.
|
|
|
|
|
</p>
|
2025-12-15 17:29:28 +09:00
|
|
|
</div>
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{editingProfile && (
|
|
|
|
|
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-border mt-6">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setEditingProfile(false)}
|
|
|
|
|
disabled={profile.isSubmitting}
|
|
|
|
|
leftIcon={<XMarkIcon className="h-4 w-4" />}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
void profile
|
|
|
|
|
.handleSubmit()
|
|
|
|
|
.then(() => {
|
|
|
|
|
setEditingProfile(false);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
// Error is handled by useZodForm
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
isLoading={profile.isSubmitting}
|
2026-01-15 11:28:25 +09:00
|
|
|
leftIcon={profile.isSubmitting ? undefined : <CheckIcon className="h-4 w-4" />}
|
2025-12-16 13:54:31 +09:00
|
|
|
>
|
|
|
|
|
{profile.isSubmitting ? "Saving..." : "Save Changes"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="bg-card text-card-foreground rounded-xl border border-border shadow-[var(--cp-shadow-1)]">
|
|
|
|
|
<div className="px-6 py-5 border-b border-border">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
<MapPinIcon className="h-6 w-6 text-primary" />
|
|
|
|
|
<h2 className="text-xl font-semibold text-foreground">Address Information</h2>
|
|
|
|
|
</div>
|
|
|
|
|
{!editingAddress && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setEditingAddress(true)}
|
|
|
|
|
leftIcon={<PencilIcon className="h-4 w-4" />}
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-09-17 18:43:43 +09:00
|
|
|
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="p-6">
|
|
|
|
|
{editingAddress ? (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<AddressForm
|
|
|
|
|
initialAddress={{
|
|
|
|
|
address1: address.values.address1,
|
|
|
|
|
address2: address.values.address2,
|
|
|
|
|
city: address.values.city,
|
|
|
|
|
state: address.values.state,
|
|
|
|
|
postcode: address.values.postcode,
|
|
|
|
|
country: address.values.country,
|
|
|
|
|
countryCode: address.values.countryCode,
|
|
|
|
|
phoneNumber: address.values.phoneNumber,
|
|
|
|
|
phoneCountryCode: address.values.phoneCountryCode,
|
|
|
|
|
}}
|
|
|
|
|
onChange={a => {
|
|
|
|
|
address.setValue("address1", a.address1 ?? "");
|
|
|
|
|
address.setValue("address2", a.address2 ?? "");
|
|
|
|
|
address.setValue("city", a.city ?? "");
|
|
|
|
|
address.setValue("state", a.state ?? "");
|
|
|
|
|
address.setValue("postcode", a.postcode ?? "");
|
|
|
|
|
address.setValue("country", a.country ?? "");
|
|
|
|
|
address.setValue("countryCode", a.countryCode ?? "");
|
|
|
|
|
address.setValue("phoneNumber", a.phoneNumber ?? "");
|
|
|
|
|
address.setValue("phoneCountryCode", a.phoneCountryCode ?? "");
|
|
|
|
|
}}
|
|
|
|
|
title="Mailing Address"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex items-center justify-end space-x-3 pt-2">
|
2025-09-17 18:43:43 +09:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2025-12-16 13:54:31 +09:00
|
|
|
onClick={() => setEditingAddress(false)}
|
|
|
|
|
disabled={address.isSubmitting}
|
2025-10-29 13:29:28 +09:00
|
|
|
leftIcon={<XMarkIcon className="h-4 w-4" />}
|
2025-09-17 18:43:43 +09:00
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
2025-12-16 13:54:31 +09:00
|
|
|
void address
|
2025-09-25 17:42:36 +09:00
|
|
|
.handleSubmit()
|
|
|
|
|
.then(() => {
|
2025-12-16 13:54:31 +09:00
|
|
|
setEditingAddress(false);
|
2025-09-25 17:42:36 +09:00
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
// Error is handled by useZodForm
|
|
|
|
|
});
|
2025-09-17 18:43:43 +09:00
|
|
|
}}
|
2025-12-16 13:54:31 +09:00
|
|
|
isLoading={address.isSubmitting}
|
2026-01-15 11:28:25 +09:00
|
|
|
leftIcon={address.isSubmitting ? undefined : <CheckIcon className="h-4 w-4" />}
|
2025-09-17 18:43:43 +09:00
|
|
|
>
|
2025-12-16 13:54:31 +09:00
|
|
|
{address.isSubmitting ? "Saving..." : "Save Address"}
|
2025-09-17 18:43:43 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
{address.submitError && (
|
|
|
|
|
<AlertBanner variant="error" title="Address Error">
|
|
|
|
|
{address.submitError}
|
|
|
|
|
</AlertBanner>
|
2025-09-17 18:43:43 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
) : (
|
|
|
|
|
<div>
|
|
|
|
|
{address.values.address1 || address.values.city ? (
|
2025-12-16 16:08:17 +09:00
|
|
|
<div className="bg-card rounded-lg p-5 border border-border shadow-sm">
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="text-foreground space-y-1.5">
|
2026-01-06 16:13:59 +09:00
|
|
|
{(address.values.address2 || address.values.address1) && (
|
|
|
|
|
<p className="font-medium text-base">
|
|
|
|
|
{address.values.address2 || address.values.address1}
|
|
|
|
|
</p>
|
2025-12-16 13:54:31 +09:00
|
|
|
)}
|
2026-01-06 16:13:59 +09:00
|
|
|
{address.values.address2 && address.values.address1 && (
|
|
|
|
|
<p className="text-muted-foreground">{address.values.address1}</p>
|
2025-12-16 13:54:31 +09:00
|
|
|
)}
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
{[address.values.city, address.values.state, address.values.postcode]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(", ")}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-muted-foreground">{address.values.country}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<MapPinIcon className="h-12 w-12 text-muted-foreground/60 mx-auto mb-4" />
|
|
|
|
|
<p className="text-muted-foreground mb-4">No address on file</p>
|
2025-09-17 18:43:43 +09:00
|
|
|
<Button
|
2025-12-16 13:54:31 +09:00
|
|
|
onClick={() => setEditingAddress(true)}
|
|
|
|
|
leftIcon={<PencilIcon className="h-4 w-4" />}
|
2025-09-17 18:43:43 +09:00
|
|
|
>
|
2025-12-16 13:54:31 +09:00
|
|
|
Add Address
|
2025-09-17 18:43:43 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-17 18:43:43 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-23 15:19:20 +09:00
|
|
|
|
|
|
|
|
{/* ID Verification Card - Integrated Upload */}
|
|
|
|
|
<div className="bg-card text-card-foreground rounded-xl border border-border shadow-[var(--cp-shadow-1)]">
|
|
|
|
|
<div className="px-6 py-5 border-b border-border">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
<ShieldCheckIcon className="h-6 w-6 text-primary" />
|
|
|
|
|
<h2 className="text-xl font-semibold text-foreground">Identity Verification</h2>
|
|
|
|
|
</div>
|
2026-01-15 11:28:25 +09:00
|
|
|
{renderVerificationStatusPill()}
|
2025-12-23 15:19:20 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="p-6">
|
2026-01-15 11:28:25 +09:00
|
|
|
{renderVerificationContent()}
|
2025-12-23 15:19:20 +09:00
|
|
|
|
2026-01-15 11:28:25 +09:00
|
|
|
{/* Upload section for rejected or not submitted status */}
|
|
|
|
|
{!verificationQuery.isLoading &&
|
|
|
|
|
verificationStatus !== "verified" &&
|
|
|
|
|
verificationStatus !== "pending" && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{verificationStatus === "rejected" ? (
|
|
|
|
|
<AlertBanner variant="warning" title="Verification rejected" size="sm" elevated>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{verificationQuery.data?.reviewerNotes && (
|
|
|
|
|
<p>{verificationQuery.data.reviewerNotes}</p>
|
|
|
|
|
)}
|
|
|
|
|
<p>Please upload a new, clear photo or scan of your residence card.</p>
|
|
|
|
|
<ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
|
|
|
|
|
<li>Make sure all text is readable and the full card is visible.</li>
|
|
|
|
|
<li>Avoid glare/reflections and blurry photos.</li>
|
|
|
|
|
<li>Maximum file size: 5MB.</li>
|
|
|
|
|
</ul>
|
2025-12-23 17:53:08 +09:00
|
|
|
</div>
|
2026-01-15 11:28:25 +09:00
|
|
|
</AlertBanner>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Upload your residence card to activate SIM services. This is required for SIM
|
|
|
|
|
orders.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(verificationQuery.data?.submittedAt || verificationQuery.data?.reviewedAt) && (
|
|
|
|
|
<div className="rounded-lg border border-border bg-muted/30 px-4 py-3">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Latest submission
|
2025-12-23 17:53:08 +09:00
|
|
|
</div>
|
2026-01-15 11:28:25 +09:00
|
|
|
{verificationQuery.data?.submittedAt && (
|
|
|
|
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
Submitted on{" "}
|
|
|
|
|
{formatIsoDate(verificationQuery.data.submittedAt, { dateStyle: "medium" })}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{verificationQuery.data?.reviewedAt && (
|
|
|
|
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
Reviewed on{" "}
|
|
|
|
|
{formatIsoDate(verificationQuery.data.reviewedAt, { dateStyle: "medium" })}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-23 17:53:08 +09:00
|
|
|
|
2026-01-15 11:28:25 +09:00
|
|
|
{canUploadVerification && (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<input
|
|
|
|
|
ref={verificationFileInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/*,application/pdf"
|
|
|
|
|
onChange={e => setVerificationFile(e.target.files?.[0] ?? null)}
|
|
|
|
|
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{verificationFile && (
|
|
|
|
|
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 px-3 py-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
Selected file
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm font-medium text-foreground truncate">
|
|
|
|
|
{verificationFile.name}
|
|
|
|
|
</div>
|
2025-12-23 15:19:20 +09:00
|
|
|
</div>
|
2026-01-15 11:28:25 +09:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setVerificationFile(null);
|
|
|
|
|
if (verificationFileInputRef.current) {
|
|
|
|
|
verificationFileInputRef.current.value = "";
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Change
|
|
|
|
|
</Button>
|
2025-12-23 15:19:20 +09:00
|
|
|
</div>
|
2026-01-15 11:28:25 +09:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-end">
|
2025-12-23 15:19:20 +09:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
2026-01-15 11:28:25 +09:00
|
|
|
disabled={!verificationFile || submitResidenceCard.isPending}
|
|
|
|
|
isLoading={submitResidenceCard.isPending}
|
|
|
|
|
loadingText="Uploading…"
|
2025-12-23 15:19:20 +09:00
|
|
|
onClick={() => {
|
2026-01-15 11:28:25 +09:00
|
|
|
if (!verificationFile) return;
|
|
|
|
|
submitResidenceCard.mutate(verificationFile, {
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setVerificationFile(null);
|
|
|
|
|
if (verificationFileInputRef.current) {
|
|
|
|
|
verificationFileInputRef.current.value = "";
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-12-23 15:19:20 +09:00
|
|
|
}}
|
|
|
|
|
>
|
2026-01-15 11:28:25 +09:00
|
|
|
Submit Document
|
2025-12-23 15:19:20 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-15 11:28:25 +09:00
|
|
|
{submitResidenceCard.isError && (
|
|
|
|
|
<p className="text-sm text-destructive">
|
|
|
|
|
{submitResidenceCard.error instanceof Error
|
|
|
|
|
? submitResidenceCard.error.message
|
|
|
|
|
: "Failed to submit residence card."}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2025-12-23 15:19:20 +09:00
|
|
|
|
2026-01-15 11:28:25 +09:00
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Accepted formats: JPG, PNG, or PDF (max 5MB). Make sure all text is readable.
|
2025-12-23 15:19:20 +09:00
|
|
|
</p>
|
2026-01-15 11:28:25 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-23 15:19:20 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
</PageLayout>
|
2025-09-17 18:43:43 +09:00
|
|
|
);
|
|
|
|
|
}
|