domain migration
This commit is contained in:
parent
192b810eb4
commit
c04a6ce6fe
26 changed files with 777 additions and 125 deletions
|
|
@ -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:<mail-server-ip> ~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>
|
||||
|
|
|
|||
229
frontend/src/components/CompanyOverviewSetup.jsx
Normal file
229
frontend/src/components/CompanyOverviewSetup.jsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue