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>