domain migration

This commit is contained in:
debu-2 2026-10-04 14:40:06 +05:30
parent 192b810eb4
commit c04a6ce6fe
26 changed files with 777 additions and 125 deletions

View file

@ -3,6 +3,14 @@ import { useState } from "react";
import { verifyCompanyDns } from "../api";
import Brand from "./Brand";
const CHECK_LABELS = {
ownership: "Domain ownership",
mx: "MX",
spf: "SPF",
dkim: "DKIM",
dmarc: "DMARC",
};
function DnsRecordTable({ records }) {
async function copyValue(value) {
try {
@ -99,8 +107,16 @@ export default function CompanyDnsSetup({ domain, account, onComplete, onLogout
<DnsRecordTable records={domain.dns_records} />
<ul className="dns-notes">
<li>
Add the <strong>ownership</strong> TXT first — this proves you control the domain.
Without it, verification cannot succeed even if MX/SPF look correct.
</li>
<li>Point MX to <strong>{domain.mail_host}</strong> — you do not need a separate mail A record.</li>
<li>SPF uses <code>include:_spf.getall.help</code> — that record must exist on <strong>getall.help</strong> (Host <code>_spf</code>, TXT <code>v=spf1 ip4:&lt;mail-server-ip&gt; ~all</code>).</li>
<li>
SPF uses <code>include:_spf.gentic.email</code>. The platform operator maintains
<code>_spf</code> on <strong>gentic.email</strong>; you only add SPF on your domain.
</li>
<li>DMARC must match the exact value in the table — an existing third-party DMARC record is not accepted.</li>
<li>DNS changes can take a few minutes to propagate.</li>
</ul>
@ -113,7 +129,7 @@ export default function CompanyDnsSetup({ domain, account, onComplete, onLogout
>
{check.verified ? <CheckCircle2 size={16} /> : <XCircle size={16} />}
<div>
<strong>{check.purpose.toUpperCase()}</strong>
<strong>{CHECK_LABELS[check.purpose] || check.purpose.toUpperCase()}</strong>
<span>{check.message}</span>
</div>
</div>

View file

@ -0,0 +1,229 @@
import { ArrowRight, Check } from "lucide-react";
import { useEffect, useState } from "react";
const STEPS = [
{ id: 1, title: "Add domain", hint: "Use your company domain for email" },
{ id: 2, title: "Verify domain", hint: "Prove domain ownership" },
{ id: 3, title: "Configure email DNS", hint: "Set up MX, SPF, DKIM" },
{ id: 4, title: "Company details", hint: "Tell us about your organization" },
{ id: 5, title: "Add employees", hint: "Create and invite your team" },
{ id: 6, title: "Finish setup", hint: "Start using AGMail" },
];
const ORG_SIZES = [
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"500+ employees",
];
const COUNTRIES = [
"India",
"United States",
"United Kingdom",
"Canada",
"Australia",
"Singapore",
"Other",
];
function storageKey(accountId, suffix) {
return `agmail_org_${suffix}_${accountId}`;
}
function loadStep(accountId) {
const saved = Number(localStorage.getItem(storageKey(accountId, "step")));
if (saved >= 4 && saved <= 6) return saved;
return 4;
}
function loadDetails(account, companyDomain) {
try {
const raw = localStorage.getItem(storageKey(account.id, "details"));
if (raw) return JSON.parse(raw);
} catch {
/* ignore */
}
return {
companyName: companyDomain?.domain_name?.split(".")[0] || "",
orgSize: "51-200 employees",
country: "India",
contactName: `${account.first_name} ${account.last_name}`.trim(),
workEmail: account.email,
};
}
export function isOrgSetupComplete(accountId) {
return localStorage.getItem(storageKey(accountId, "complete")) === "1";
}
export default function CompanyOverviewSetup({ account, companyDomain, onComplete }) {
const [step, setStep] = useState(() => loadStep(account.id));
const [details, setDetails] = useState(() => loadDetails(account, companyDomain));
useEffect(() => {
localStorage.setItem(storageKey(account.id, "step"), String(step));
}, [account.id, step]);
function saveDetails(event) {
event.preventDefault();
localStorage.setItem(storageKey(account.id, "details"), JSON.stringify(details));
setStep(5);
}
function finishSetup() {
localStorage.setItem(storageKey(account.id, "complete"), "1");
localStorage.removeItem(storageKey(account.id, "step"));
onComplete?.();
}
return (
<section className="org-setup">
<header className="org-setup-head">
<h1>Set up your organization</h1>
<p>Get your company email up and running in a few simple steps.</p>
</header>
<div className="org-setup-layout">
<ol className="org-setup-steps" aria-label="Setup progress">
{STEPS.map((item) => {
const complete = item.id < step || (item.id <= 3);
const active = item.id === step;
return (
<li
key={item.id}
className={`org-setup-step${complete ? " complete" : ""}${active ? " active" : ""}`}
>
<span className="org-setup-step-mark" aria-hidden="true">
{complete ? <Check size={14} strokeWidth={2.5} /> : item.id}
</span>
<span className="org-setup-step-copy">
<strong>{item.title}</strong>
<small>{item.hint}</small>
</span>
</li>
);
})}
</ol>
<div className="org-setup-panel">
{step === 4 && (
<>
<h2>Company details</h2>
<p className="org-setup-section-label">Organization information</p>
<form className="org-setup-form" onSubmit={saveDetails}>
<label>
Company name <span aria-hidden="true">*</span>
<input
required
value={details.companyName}
onChange={(event) => setDetails((current) => ({
...current,
companyName: event.target.value,
}))}
placeholder="Acme Pvt Ltd"
/>
</label>
<label>
Organization size <span aria-hidden="true">*</span>
<select
required
value={details.orgSize}
onChange={(event) => setDetails((current) => ({
...current,
orgSize: event.target.value,
}))}
>
{ORG_SIZES.map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
</label>
<label>
Country <span aria-hidden="true">*</span>
<select
required
value={details.country}
onChange={(event) => setDetails((current) => ({
...current,
country: event.target.value,
}))}
>
{COUNTRIES.map((country) => (
<option key={country} value={country}>{country}</option>
))}
</select>
</label>
<label>
Your name <span aria-hidden="true">*</span>
<input
required
value={details.contactName}
onChange={(event) => setDetails((current) => ({
...current,
contactName: event.target.value,
}))}
/>
</label>
<label>
Your work email <span aria-hidden="true">*</span>
<input
required
type="email"
value={details.workEmail}
onChange={(event) => setDetails((current) => ({
...current,
workEmail: event.target.value,
}))}
/>
</label>
<div className="org-setup-actions">
<button type="submit" className="button button-purple">
Continue <ArrowRight size={16} />
</button>
</div>
</form>
</>
)}
{step === 5 && (
<>
<h2>Add employees</h2>
<p className="org-setup-lead">
Invite teammates to <strong>{companyDomain?.domain_name}</strong>. You can add people now or skip and do this later from People.
</p>
<div className="org-setup-placeholder">
Employee invites will be available here soon.
</div>
<div className="org-setup-actions">
<button type="button" className="button button-purple" onClick={() => setStep(6)}>
Continue <ArrowRight size={16} />
</button>
</div>
</>
)}
{step === 6 && (
<>
<h2>Finish setup</h2>
<p className="org-setup-lead">
Domain <strong>{companyDomain?.domain_name}</strong> is connected. You&apos;re ready to use AGMail.
</p>
<ul className="org-setup-summary">
<li><Check size={14} /> Domain added and verified</li>
<li><Check size={14} /> Email DNS configured</li>
<li><Check size={14} /> Organization details saved</li>
</ul>
<div className="org-setup-actions">
<button type="button" className="button button-purple" onClick={finishSetup}>
Go to inbox <ArrowRight size={16} />
</button>
</div>
</>
)}
</div>
</div>
</section>
);
}

View file

@ -1,7 +1,12 @@
import {
ChevronDown,
CircleDollarSign,
CircleEllipsis,
Clock3,
Crown,
FileEdit,
Globe,
Home,
Inbox,
ListChecks,
LogOut,
@ -10,16 +15,20 @@ import {
Plug,
Send,
Settings,
Shield,
ShieldAlert,
Sparkles,
Star,
Trash2,
Users,
UsersRound,
X,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { getCurrentAccount, getMyCompanyDomain } from "../api";
import Brand from "./Brand";
import CompanyDnsSetup from "./CompanyDnsSetup";
import CompanyOverviewSetup, { isOrgSetupComplete } from "./CompanyOverviewSetup";
import ComposeModal from "./ComposeModal";
import MailboxView from "./MailboxView";
@ -49,6 +58,18 @@ const agentItems = [
[Plug, "Integrations", "integrations"],
];
const companyItems = [
[Home, "Overview"],
[Users, "People"],
[UsersRound, "Groups"],
[Globe, "Domains"],
[CircleEllipsis, "Aliases"],
[Shield, "Security"],
[Crown, "Administrators"],
[CircleDollarSign, "Billing"],
[Settings, "Settings"],
];
function clearSession() {
sessionStorage.removeItem("agmail_access_token");
sessionStorage.removeItem("agmail_account");
@ -82,6 +103,7 @@ export default function DashboardPage() {
const [lens, setLens] = useState("all");
const [agentNav, setAgentNav] = useState(null);
const [labelsExpanded, setLabelsExpanded] = useState(false);
const [companyActive, setCompanyActive] = useState(null);
const [mailStats, setMailStats] = useState({
unread: 0,
starred: 0,
@ -103,7 +125,7 @@ export default function DashboardPage() {
.then(async (currentAccount) => {
sessionStorage.setItem("agmail_account", JSON.stringify(currentAccount));
setAccount(currentAccount);
if (currentAccount.account_type === "company") {
if (currentAccount.account_type === "company" && currentAccount.is_domain_owner) {
try {
const domain = await getMyCompanyDomain(token);
setCompanyDomain(domain);
@ -120,12 +142,26 @@ export default function DashboardPage() {
});
}, []);
useEffect(() => {
if (!account?.is_domain_owner || needsDnsSetup) return;
if (!isOrgSetupComplete(account.id)) {
setCompanyActive("Overview");
}
}, [account, needsDnsSetup]);
function logout() {
clearSession();
window.location.assign("/login");
}
function selectCompanyItem(label) {
setCompanyActive(label);
setAgentNav(null);
setMenuOpen(false);
}
function selectMailItem(item) {
setCompanyActive(null);
setAgentNav(null);
if (item === "Starred") {
setActive("Inbox");
@ -141,6 +177,7 @@ export default function DashboardPage() {
}
function selectLabel(name) {
setCompanyActive(null);
setAgentNav(null);
setActive("Inbox");
setLens(name);
@ -148,6 +185,7 @@ export default function DashboardPage() {
}
function selectAgentItem(item, actionKey) {
setCompanyActive(null);
setActive("Inbox");
setLens("all");
setAgentNav(actionKey);
@ -190,6 +228,7 @@ export default function DashboardPage() {
onComplete={() => {
setCompanyDomain({ ...companyDomain, status: "active" });
setNeedsDnsSetup(false);
setCompanyActive("Overview");
}}
onLogout={logout}
/>
@ -200,6 +239,7 @@ export default function DashboardPage() {
const workspaceTitle = account.account_type === "company" && companyDomain
? companyDomain.domain_name
: "Personal workspace";
const isDomainOwner = account.account_type === "company" && account.is_domain_owner;
const mailActive = lens === "starred"
? "Starred"
: active === "Outbox"
@ -236,6 +276,24 @@ export default function DashboardPage() {
</button>
</div>
{isDomainOwner && (
<SidebarSection title="Company" className="dashboard-sidebar-company dashboard-sidebar-first">
<nav aria-label="Company">
{companyItems.map(([ItemIcon, label]) => (
<button
className={`dashboard-nav-item dashboard-company-item${companyActive === label ? " active" : ""}`}
key={label}
type="button"
onClick={() => selectCompanyItem(label)}
>
<ItemIcon size={16} strokeWidth={1.75} />
<span>{label}</span>
</button>
))}
</nav>
</SidebarSection>
)}
<SidebarSection title="Mail" className="dashboard-sidebar-mail">
<nav aria-label="Mail">
{mailItems.map(([ItemIcon, label, countKey]) => (
@ -303,13 +361,19 @@ export default function DashboardPage() {
</aside>
<main className="dashboard-content">
{!["Inbox", "Outbox", "Trash"].includes(active) && (
{companyActive && companyActive !== "Overview" && (
<div className="dashboard-content-heading">
<div><p>{workspaceTitle}</p><h1>{companyActive}</h1></div>
<span className="dashboard-account-pill">{workspaceLabel}</span>
</div>
)}
{!companyActive && !["Inbox", "Outbox", "Trash"].includes(active) && (
<div className="dashboard-content-heading">
<div><p>{workspaceTitle}</p><h1>{active}</h1></div>
<span className="dashboard-account-pill">{workspaceLabel}</span>
</div>
)}
{active === "Inbox" && (
{!companyActive && active === "Inbox" && (
<MailboxView
mailbox="inbox"
title="Inbox"
@ -322,7 +386,7 @@ export default function DashboardPage() {
onStats={handleStats}
/>
)}
{active === "Outbox" && (
{!companyActive && active === "Outbox" && (
<MailboxView
mailbox="sent"
title="Sent"
@ -332,7 +396,7 @@ export default function DashboardPage() {
onOpenSettings={() => selectMailItem("Settings")}
/>
)}
{active === "Trash" && (
{!companyActive && active === "Trash" && (
<MailboxView
mailbox="trash"
title="Trash"
@ -342,7 +406,28 @@ export default function DashboardPage() {
onOpenSettings={() => selectMailItem("Settings")}
/>
)}
{!["Inbox", "Outbox", "Trash"].includes(active) && (
{companyActive === "Overview" && isDomainOwner && (
<CompanyOverviewSetup
account={account}
companyDomain={companyDomain}
onComplete={() => {
setCompanyActive(null);
setActive("Inbox");
}}
/>
)}
{companyActive && companyActive !== "Overview" && (
<section className="dashboard-empty dashboard-company-view">
<span><Sparkles size={24} /></span>
<h2>{companyActive}</h2>
<p>
{companyActive === "Domains" && companyDomain
? `Manage DNS and delivery for ${companyDomain.domain_name}.`
: `Company ${companyActive.toLowerCase()} controls will appear here.`}
</p>
</section>
)}
{!companyActive && !["Inbox", "Outbox", "Trash"].includes(active) && (
<section className="dashboard-empty">
<span><Sparkles size={24} /></span>
<h2>{active} is ready for you</h2>

View file

@ -32,7 +32,7 @@ export default function LoginPage() {
<>
<div className="card-heading"><h2>Log in to agmail</h2><p>Enter your account details below.</p></div>
<form className="auth-form" onSubmit={handleSubmit}>
<label>Email address<div className="input-wrap"><Mail size={16} /><input name="email" type="email" placeholder="you@getall.help" required autoComplete="email" /></div></label>
<label>Email address<div className="input-wrap"><Mail size={16} /><input name="email" type="email" placeholder="you@gentic.email" required autoComplete="email" /></div></label>
<label>Password<div className="input-wrap"><LockKeyhole size={16} /><input name="password" type="password" placeholder="Your password" required autoComplete="current-password" /></div></label>
{status.error && <p className="form-error" role="alert">{status.error}</p>}
<button className="button button-coral auth-submit" disabled={status.loading}>

View file

@ -59,7 +59,7 @@ export default function SignupPage() {
<label>Company domain<div className="input-wrap"><Building2 size={16} /><input name="company_domain" placeholder="yourcompany.com" required /></div></label>
)}
<label>Choose your email name
<div className="email-builder input-wrap"><Mail size={16} /><input name="email_name" placeholder="alex" pattern="[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?" required /><span>{accountType === "individual" ? "@getall.help" : "@your domain"}</span></div>
<div className="email-builder input-wrap"><Mail size={16} /><input name="email_name" placeholder="alex" pattern="[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?" required /><span>{accountType === "individual" ? "@gentic.email" : "@your domain"}</span></div>
</label>
<div className="field-row">
<label>First name<div className="input-wrap"><UserRound size={16} /><input name="first_name" placeholder="Alex" required /></div></label>

View file

@ -210,8 +210,8 @@ export default function SmtpCredentialsPage() {
<input
name="domain"
type="text"
placeholder="getall.help"
defaultValue={result?.domain || "getall.help"}
placeholder="gentic.email"
defaultValue={result?.domain || "gentic.email"}
required
/>
</div>

View file

@ -176,6 +176,41 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.company-dns-check.ok { background:#eef8f1; color:#2f6b43; }
.company-dns-check.fail { background:#fff0eb; color:#b04a38; }
.company-dns-next { align-self:flex-start; margin-top:8px; display:inline-flex; align-items:center; gap:8px; }
.button-purple { min-height:44px; padding:0 20px; display:inline-flex; align-items:center; gap:8px; border:0; border-radius:10px; background:#7357F6; color:#fff; font-size:13px; font-weight:600; cursor:pointer; box-shadow:0 8px 20px rgba(115,87,246,0.28); }
.button-purple:hover { filter:brightness(1.03); }
.org-setup { padding:8px 0 24px; }
.org-setup-head { margin-bottom:28px; }
.org-setup-head h1 { margin:0 0 8px; font:700 clamp(28px,3vw,36px)/1.15 "DM Sans"; letter-spacing:-.6px; color:#1a1628; }
.org-setup-head p { margin:0; color:#77758A; font-size:14px; }
.org-setup-layout { display:grid; grid-template-columns:minmax(220px,280px) minmax(0,1fr); gap:28px; align-items:start; }
.org-setup-steps { margin:0; padding:0; list-style:none; display:grid; gap:0; }
.org-setup-step { padding:12px 14px; display:grid; grid-template-columns:32px 1fr; gap:12px; align-items:start; border-radius:12px; color:#6f6c7b; }
.org-setup-step-mark { width:32px; height:32px; display:grid; place-items:center; border-radius:50%; border:1px solid #E4E1E8; background:#fff; font-size:13px; font-weight:600; color:#6f6c7b; }
.org-setup-step-copy { display:grid; gap:2px; }
.org-setup-step-copy strong { font-size:13px; font-weight:600; color:#3d3948; }
.org-setup-step-copy small { font-size:11px; color:#8d8792; line-height:1.35; }
.org-setup-step.complete .org-setup-step-mark { border-color:#7357F6; background:#7357F6; color:#fff; }
.org-setup-step.complete .org-setup-step-copy strong { color:#5a4fcf; }
.org-setup-step.active { background:#F6F2FF; }
.org-setup-step.active .org-setup-step-mark { border-color:#7357F6; background:#7357F6; color:#fff; }
.org-setup-step.active .org-setup-step-copy strong { color:#5a4fcf; }
.org-setup-panel { padding:28px 32px; border:1px solid #ECE9F2; border-radius:16px; background:#fff; box-shadow:0 8px 28px rgba(20,18,30,0.04); }
.org-setup-panel h2 { margin:0 0 18px; font:700 22px/1.2 "DM Sans"; color:#1a1628; }
.org-setup-section-label { margin:0 0 16px; font-size:12px; font-weight:600; color:#8d8792; }
.org-setup-lead { margin:0 0 20px; color:#6f6c7b; font-size:14px; line-height:1.5; }
.org-setup-form { display:grid; gap:14px; }
.org-setup-form label { display:grid; gap:6px; font-size:12px; font-weight:600; color:#4a4652; }
.org-setup-form label span { color:#e15b45; }
.org-setup-form input,.org-setup-form select { height:42px; padding:0 12px; border:1px solid #E1DFE7; border-radius:10px; background:#fff; font-size:13px; color:#1a1628; }
.org-setup-form input:focus,.org-setup-form select:focus { outline:none; border-color:#C8B8FF; box-shadow:0 0 0 3px rgba(115,87,246,0.12); }
.org-setup-actions { margin-top:8px; display:flex; justify-content:flex-end; }
.org-setup-placeholder { margin-bottom:20px; padding:24px; border:1px dashed #E4E1E8; border-radius:12px; background:#FAFAFC; color:#8d8792; font-size:13px; text-align:center; }
.org-setup-summary { margin:0 0 24px; padding:0; list-style:none; display:grid; gap:10px; }
.org-setup-summary li { display:flex; align-items:center; gap:8px; color:#3d3948; font-size:13px; font-weight:500; }
.org-setup-summary svg { color:#7357F6; flex-shrink:0; }
@media(max-width:900px) {
.org-setup-layout { grid-template-columns:1fr; }
}
.dns-copy { flex-shrink:0; border:0; border-radius:7px; padding:4px; background:#f7f3f1; color:#8c8790; cursor:pointer; }
.dns-copy:hover { background:#fff0eb; color:var(--coral); }
.dashboard-scrim { display:none; }
@ -471,7 +506,9 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.label-dot-green { background:#8ed9ad; }
.dashboard-agent-item { color:#c3bed0; }
.dashboard-agent-item.active { background:linear-gradient(90deg,#8b7cff22 0%,#ffffff10 100%); box-shadow:inset 3px 0 0 #9f8cff; }
.dashboard-sidebar-mail,.dashboard-sidebar-labels,.dashboard-sidebar-agent { flex:0 0 auto; }
.dashboard-sidebar-mail,.dashboard-sidebar-labels,.dashboard-sidebar-agent,.dashboard-sidebar-company { flex:0 0 auto; }
.dashboard-sidebar-company.dashboard-sidebar-first { margin-top:var(--space-4); padding-top:0; border-top:0; }
.dashboard-company-item.active { background:rgba(255,255,255,0.09); box-shadow:inset 2px 0 0 #a78bfa; }
.dashboard-profile { margin-top:auto; padding-top:var(--space-3); border-top:1px solid #ffffff10; flex:0 0 auto; }
.dashboard-profile>span { background:linear-gradient(180deg,#ff9a82,#ff6f57); color:#2a2038; }
.dashboard-profile strong { color:#fff; }