Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/docs/providers/communications/agentmail.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ channel enabled:
channel was enabled) connect their address the first time they email
Roomote: the refusal email carries a link that ties the address to their
account.
- **Personal settings > Linked Accounts** shows the Email row's verification
status separately from sender addresses connected through that refusal link.
When the login email is not verified and AgentMail is ready to deliver, use
**Resend** to request another verification message. A delivery failure is
shown instead of reporting success. To connect another sender address,
send from that address to the deployment inbox and use the link in Roomote's
reply; this does not verify or change the login email.
- Sign-ins through Slack, Microsoft, GitHub, and the other OAuth providers
are unaffected; those providers assert a verified email.
- Password reset links are emailed to the user as well as shown to the
Expand Down
123 changes: 123 additions & 0 deletions apps/web/src/components/settings/LinkedAccounts.test.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 147 additions & 2 deletions apps/web/src/components/settings/LinkedAccounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ import {
useBitbucketLinkedAccount,
useGiteaLinkedAccount,
useGitHubLinkedAccount,
useLinkedEmailAccounts,
useLinearLinkedAccount,
useMicrosoftTeamsLinkedAccount,
useSlackLinkedAccount,
useTelegramLinkedAccount,
useResendEmailVerification,
useUnlinkAdoLinkedAccount,
useUnlinkGitLabLinkedAccount,
useUnlinkBitbucketLinkedAccount,
Expand All @@ -61,6 +63,7 @@ import { useAuthorizedUser } from '@/hooks/useUser';

import {
BrandIcon,
Badge,
Button,
Dialog,
DialogContent,
Expand All @@ -70,6 +73,8 @@ import {
Github,
LinearLogo,
LucideLink,
Mail,
RefreshCw,
Skeleton,
Slack,
Spinner,
Expand Down Expand Up @@ -402,6 +407,60 @@ function LinkedAccountRowSkeleton() {
);
}

function EmailAccountDetails({
emailAddress,
status,
variant,
}: {
emailAddress: string;
status: 'Linked' | 'Not verified' | 'Verified';
variant: 'success' | 'warning';
}) {
return (
<span className="flex min-w-0 items-center gap-2">
<span className="truncate ph-no-capture">{emailAddress}</span>
<Badge variant={variant}>{status}</Badge>
</span>
);
}

function EmailLinkingGuidance({
canViewInboxAddress,
emailEnabled,
inboxEmail,
}: {
canViewInboxAddress: boolean;
emailEnabled: boolean;
inboxEmail: string | null;
}) {
if (!emailEnabled) {
return (
<p className="text-sm text-muted-foreground">
Email is disabled for this deployment. Verification messages and
sender-address linking are unavailable until an admin enables it.
</p>
);
}

if (inboxEmail) {
return (
<p className="text-sm text-muted-foreground">
To link another sender address, email{' '}
<span className="font-mono ph-no-capture">{inboxEmail}</span> from that
address, then use the link in Roomote&apos;s reply.
</p>
);
}

return (
<p className="text-sm text-muted-foreground">
{canViewInboxAddress
? 'Email is enabled, but no AgentMail inbox is configured. Configure it in Communications before linking sender addresses.'
: "To link another sender address, email your deployment's Roomote inbox, then use the link in Roomote's reply. Ask an admin for the inbox address."}
</p>
);
}

export function LinkedAccounts() {
const pathname = usePathname();
const searchParams = useSearchParams();
Expand All @@ -411,6 +470,8 @@ export function LinkedAccounts() {
const userConnections = useUserMcpConnections();
const connectMcp = useConnectMcp();
const disconnectMcp = useDisconnectMcp();
const emailAccounts = useLinkedEmailAccounts();
const resendEmailVerification = useResendEmailVerification();

const githubInstallations = useGitHubInstallations();
const githubAccount = useGitHubLinkedAccount();
Expand Down Expand Up @@ -810,8 +871,14 @@ export function LinkedAccounts() {
}),
].filter(isLinkedAccountDescriptor);

const hasVisibleRows = linkedAccountDescriptors.length > 0;
const primaryEmail = emailAccounts.data?.primaryEmail;
const hasVisibleEmailRows = Boolean(
primaryEmail || emailAccounts.data?.senderAddresses.length,
);
const hasVisibleRows =
hasVisibleEmailRows || linkedAccountDescriptors.length > 0;
const isLoadingVisibleRows =
emailAccounts.isPending ||
githubInstallations.isPending ||
gitlabAccount.isPending ||
giteaAccount.isPending ||
Expand All @@ -837,10 +904,88 @@ export function LinkedAccounts() {
</div>
) : null}

{!showLoadingState && !hasVisibleRows ? (
{!showLoadingState && !hasVisibleRows && !emailAccounts.isError ? (
<p className="text-sm text-muted-foreground">{emptyStateMessage}</p>
) : null}

{emailAccounts.isError ? (
<p className="text-sm text-destructive">
Unable to load email account status.
</p>
) : null}

{primaryEmail ? (
<LinkedAccountRow
icon={<Mail className="size-4" />}
name="Email"
details={
<EmailAccountDetails
emailAddress={primaryEmail.emailAddress}
status={primaryEmail.verified ? 'Verified' : 'Not verified'}
variant={primaryEmail.verified ? 'success' : 'warning'}
/>
}
actions={
!primaryEmail.verified &&
emailAccounts.data?.verificationDeliveryAvailable ? (
<Button
type="button"
size="sm"
variant="outline"
onClick={() => {
resendEmailVerification.mutate(undefined, {
onSuccess: () => {
toast.success(
'Verification requested. Check your inbox for the link.',
);
},
onError: (error) => {
toast.error(
error instanceof Error
? error.message
: 'Unable to send a verification email.',
);
},
});
}}
disabled={resendEmailVerification.isPending}
aria-label="Resend verification email"
>
{resendEmailVerification.isPending ? (
<Spinner size="sm" />
) : (
<RefreshCw />
)}
Resend
</Button>
) : null
}
/>
) : null}

{emailAccounts.data?.senderAddresses.map((emailAddress) => (
<LinkedAccountRow
key={`email-sender:${emailAddress}`}
icon={<Mail className="size-4" />}
name="Email sender"
details={
<EmailAccountDetails
emailAddress={emailAddress}
status="Linked"
variant="success"
/>
}
/>
))}

{emailAccounts.data ? (
<EmailLinkingGuidance
canViewInboxAddress={emailAccounts.data.canViewInboxAddress}
emailEnabled={emailAccounts.data.emailEnabled}
inboxEmail={emailAccounts.data.inboxEmail}
/>
) : null}

{[...linkedAccountDescriptors]
.sort(sortLinkedAccountDescriptors)
.map((descriptor) => (
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/hooks/linked-accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export * from './useCreateDiscordLinkCode';

export * from './useEmailLinkPreview';
export * from './useLinkEmailAddress';
export * from './useLinkedEmailAccounts';
export * from './useResendEmailVerification';
Loading
Loading