[Email Aliases] Limit Reached dialog (#35208)

* Updated dialogs width (as defined in Figma).
* Added strings for Limit Reached dialog.
* Added Limit Reached content.
* Limit Reached dialog integrated.
* Updated storybook.
This commit is contained in:
Pavel Beloborodov
2026-04-13 09:25:22 +07:00
committed by GitHub
parent 9b419a919d
commit 34034c479f
9 changed files with 235 additions and 85 deletions
@@ -25,7 +25,7 @@ namespace {
inline constexpr char kEmailAliasesPanelURL[] = "chrome://email-aliases.panel/";
constexpr char kEmailAliasesSettingsURL[] = "brave://settings/email-aliases";
constexpr int kDialogWidth = 420;
constexpr int kDialogWidth = 512;
constexpr gfx::Size kDialogMinSize(kDialogWidth, 336);
constexpr gfx::Size kDialogMaxSize(kDialogWidth, 794);
@@ -0,0 +1,90 @@
// Copyright (c) 2026 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import { color, font, radius, spacing } from '@brave/leo/tokens/css/variables'
import * as React from 'react'
import { formatLocale, getLocale } from '$web-common/locale'
import Alert from '@brave/leo/react/alert'
import Col from './styles/Col'
import styled from 'styled-components'
import { Alias } from 'gen/brave/components/email_aliases/email_aliases.mojom.m'
import './strings'
const SectionCol = styled(Col)`
row-gap: ${spacing['2Xl']};
`
const LimitAlertTitle = styled.div`
font: ${font.heading.h4};
`
const LimitDescription = styled.div`
font: ${font.default.regular};
`
const ListLabel = styled.div`
font: ${font.small.semibold};
margin: 0;
padding: 0 ${spacing.s};
`
const AliasListBox = styled.div`
border: 1px solid ${color.divider.subtle};
border-radius: ${radius.xl};
overflow: hidden auto;
max-height: 208px;
`
const AliasRow = styled.div<{ showDivider: boolean }>`
font: ${font.default.semibold};
padding: ${spacing.m} ${spacing.l};
gap: 8px;
border-bottom: ${(p) =>
p.showDivider
? `1px solid ${color.divider.subtle}`
: '1px solid transparent'};
`
export const EmailAliasLimitReached = ({
aliases,
aliasLimit,
}: {
aliases: Alias[]
aliasLimit: number
}) => {
const count = aliases.length
return (
<SectionCol>
<Alert type='warning'>
<LimitAlertTitle>
{getLocale(S.SETTINGS_EMAIL_ALIASES_LIMIT_REACHED_ALERT_TITLE)}
</LimitAlertTitle>
<LimitDescription>
{formatLocale(S.SETTINGS_EMAIL_ALIASES_LIMIT_REACHED_ALERT_BODY, {
$1: aliasLimit,
})}
</LimitDescription>
</Alert>
<div>
<ListLabel>
{formatLocale(S.SETTINGS_EMAIL_ALIASES_YOUR_ALIASES_COUNT_LABEL, {
$1: count,
$2: aliasLimit,
})}
</ListLabel>
<AliasListBox>
{aliases.map((alias, index) => (
<AliasRow
key={alias.email}
showDivider={index < aliases.length - 1}
>
{alias.email}
</AliasRow>
))}
</AliasListBox>
</div>
</SectionCol>
)
}
@@ -119,7 +119,7 @@ export const AliasList = ({
editing={editState.mode === 'Edit'}
editAlias={editState.alias}
mainEmail={authEmail}
aliasCount={aliases.length}
aliases={aliases}
emailAliasesService={emailAliasesService}
/>
)}
@@ -26,6 +26,7 @@ import {
EmailAliasesServiceInterface,
MAX_ALIASES,
} from 'gen/brave/components/email_aliases/email_aliases.mojom.m'
import { EmailAliasLimitReached } from './email_alias_limit_reached'
import './strings'
const ModalCol = styled(Col)`
@@ -103,12 +104,9 @@ const NoteInput = styled(Input)`
margin 0;
`
const WarningText = styled.div`
font: ${font.default.semibold};
`
const ButtonRow = styled(Row)<{ bubble?: boolean }>`
justify-content: ${(props) => (props.bubble ? 'space-between' : 'end')};
const ButtonRow = styled(Row)<{ bubble?: boolean; limitReached?: boolean }>`
justify-content: ${(props) =>
!props.bubble || props.limitReached ? 'end' : 'space-between'};
& leo-button {
flex-grow: 0;
}
@@ -119,6 +117,20 @@ const ButtonRow = styled(Row)<{ bubble?: boolean }>`
}
`
const ManageButton = styled(Button)<{ limitReached?: boolean }>`
${(props) =>
props.limitReached
&& `
order: 1; // Move Manage button to the right.
`}
${(props) =>
!props.limitReached
&& `
margin-right: auto; // Move Manage button to the left.
`}
`
const LoadingIcon = styled(ProgressRing)`
--leo-progressring-color: ${color.icon.default};
--leo-progressring-size: 24px;
@@ -188,21 +200,19 @@ export const DeleteAliasModal = ({
{getLocale(S.SETTINGS_EMAIL_ALIASES_DELETE_WARNING)}
</Alert>
<ButtonRow>
<span>
<Button
onClick={onReturnToMain}
kind='plain-faint'
>
{getLocale(S.SETTINGS_EMAIL_ALIASES_CANCEL_BUTTON)}
</Button>
<Button
onClick={onDeleteAlias}
kind='filled'
isDisabled={deleting}
>
{getLocale(S.SETTINGS_EMAIL_ALIASES_DELETE_ALIAS_BUTTON)}
</Button>
</span>
<Button
onClick={onReturnToMain}
kind='plain-faint'
>
{getLocale(S.SETTINGS_EMAIL_ALIASES_CANCEL_BUTTON)}
</Button>
<Button
onClick={onDeleteAlias}
kind='filled'
isDisabled={deleting}
>
{getLocale(S.SETTINGS_EMAIL_ALIASES_DELETE_ALIAS_BUTTON)}
</Button>
</ButtonRow>
{deleteErrorMessage && <Alert>{deleteErrorMessage}</Alert>}
</ModalCol>
@@ -225,7 +235,8 @@ export const EmailAliasModal = ({
editing,
editAlias,
mainEmail,
aliasCount,
aliases,
aliasLimit = MAX_ALIASES,
emailAliasesService,
bubble,
}: {
@@ -234,7 +245,8 @@ export const EmailAliasModal = ({
editAlias?: Alias
bubble?: boolean
mainEmail: string
aliasCount: number
aliases?: Alias[]
aliasLimit?: number
emailAliasesService: EmailAliasesServiceInterface
}) => {
const [limitReached, setLimitReached] = React.useState<boolean>(false)
@@ -295,13 +307,15 @@ export const EmailAliasModal = ({
setAwaitingProposedAlias(false)
}
React.useEffect(() => {
if (bubble) {
setLimitReached(aliasCount >= MAX_ALIASES)
setLimitReached((aliases?.length ?? 0) >= aliasLimit)
}, [aliases, aliasLimit])
React.useEffect(() => {
if (editing || (aliases?.length ?? 0) >= aliasLimit) {
return
}
if (!editing) {
regenerateAlias()
}
}, [editing])
regenerateAlias()
}, [editing, aliases, aliasLimit])
return (
<ModalCol>
<ModalTitle>
@@ -314,10 +328,11 @@ export const EmailAliasModal = ({
{getLocale(S.SETTINGS_EMAIL_ALIASES_BUBBLE_DESCRIPTION)}
</ModalDescription>
)}
{bubble && limitReached ? (
<WarningText>
{getLocale(S.SETTINGS_EMAIL_ALIASES_BUBBLE_LIMIT_REACHED)}
</WarningText>
{limitReached ? (
<EmailAliasLimitReached
aliases={aliases!}
aliasLimit={aliasLimit}
/>
) : (
<ModalCol>
<ModalSectionCol>
@@ -372,39 +387,41 @@ export const EmailAliasModal = ({
</ModalSectionCol>
</ModalCol>
)}
<ButtonRow bubble={bubble}>
<span>
{bubble && (
<Button
id='manage-button'
onClick={() => {
onReturnToMain({
type: EmailAliasModalResultType.ShouldManageAliases,
})
}}
kind='plain'
>
{getLocale(S.SETTINGS_EMAIL_ALIASES_MANAGE_BUTTON)}
</Button>
)}
<Button
id='cancel-button'
onClick={() =>
onReturnToMain({ type: EmailAliasModalResultType.Cancelled })
}
kind='plain-faint'
<ButtonRow
bubble={bubble}
limitReached={limitReached}
>
{bubble && (
<ManageButton
limitReached={limitReached}
id='manage-button'
onClick={() => {
onReturnToMain({
type: EmailAliasModalResultType.ShouldManageAliases,
})
}}
kind={limitReached ? 'filled' : 'plain'}
>
{getLocale(S.SETTINGS_EMAIL_ALIASES_CANCEL_BUTTON)}
</Button>
{getLocale(S.SETTINGS_EMAIL_ALIASES_MANAGE_BUTTON)}
</ManageButton>
)}
<Button
id='cancel-button'
onClick={() =>
onReturnToMain({ type: EmailAliasModalResultType.Cancelled })
}
kind='plain-faint'
>
{getLocale(S.SETTINGS_EMAIL_ALIASES_CANCEL_BUTTON)}
</Button>
{!limitReached && (
<Button
id='create-alias-button'
kind='filled'
isDisabled={
awaitingUpdate
|| (!editing
&& (limitReached
|| awaitingProposedAlias
|| !generateAliasResult?.aliasEmail))
&& (awaitingProposedAlias || !generateAliasResult?.aliasEmail))
}
onClick={createOrSave}
>
@@ -412,7 +429,7 @@ export const EmailAliasModal = ({
? getLocale(S.SETTINGS_EMAIL_ALIASES_CREATE_ALIAS_BUTTON)
: getLocale(S.SETTINGS_EMAIL_ALIASES_SAVE_ALIAS_BUTTON)}
</Button>
</span>
)}
</ButtonRow>
{updateErrorMessage && <Alert>{updateErrorMessage}</Alert>}
</ModalCol>
@@ -16,8 +16,7 @@
}
:host, body, #mountPoint {
display: block;
width: 420px;
max-width: 420px;
width: 100vw;
height: auto;
}
#mountPoint {
@@ -22,6 +22,7 @@ import {
EmailAliasesService,
EmailAliasesPanelHandlerInterface,
EmailAliasesPanelHandler,
MAX_ALIASES,
} from 'gen/brave/components/email_aliases/email_aliases.mojom.m'
export const EmailAliasesPanelConnected = ({
@@ -59,6 +60,8 @@ export const EmailAliasesPanelConnected = ({
}, [])
return (
<EmailAliasModal
aliases={aliasesState}
aliasLimit={MAX_ALIASES}
onReturnToMain={(action: EmailAliasModalResult) => {
switch (action.type) {
case EmailAliasModalResultType.Cancelled:
@@ -74,7 +77,6 @@ export const EmailAliasesPanelConnected = ({
}}
editing={false}
mainEmail={authState.email}
aliasCount={aliasesState.length}
emailAliasesService={emailAliasesService}
bubble
/>
@@ -9,7 +9,11 @@ import {
EmailAliasModal,
} from '../content/email_aliases_modal'
import { StubEmailAliasesService, demoData } from './utils/stubs'
import { AuthenticationStatus } from 'gen/brave/components/email_aliases/email_aliases.mojom.m'
import {
Alias,
AuthenticationStatus,
MAX_ALIASES,
} from 'gen/brave/components/email_aliases/email_aliases.mojom.m'
const stubEmailAliasesServiceAccountReadyInstance = new StubEmailAliasesService(
{
@@ -21,7 +25,8 @@ const stubEmailAliasesServiceAccountReadyInstance = new StubEmailAliasesService(
export const NewAliasDialog = () => {
return (
<EmailAliasModal
aliasCount={demoData.aliases.length}
aliases={demoData.aliases}
aliasLimit={MAX_ALIASES}
onReturnToMain={() => {}}
editing={false}
mainEmail={demoData.email}
@@ -35,7 +40,8 @@ export const EditAliasDialog = () => {
return (
<EmailAliasModal
editAlias={demoData.aliases[0]}
aliasCount={demoData.aliases.length}
aliases={demoData.aliases}
aliasLimit={MAX_ALIASES}
onReturnToMain={() => {}}
editing
mainEmail={demoData.email}
@@ -59,7 +65,32 @@ export const DeleteAliasDialog = () => {
export const Panel = () => {
return (
<EmailAliasModal
aliasCount={demoData.aliases.length}
aliases={demoData.aliases}
aliasLimit={MAX_ALIASES}
onReturnToMain={() => {}}
editing={false}
mainEmail={demoData.email}
bubble={true}
// @ts-expect-error https://github.com/brave/brave-browser/issues/48960
emailAliasesService={stubEmailAliasesServiceAccountReadyInstance}
/>
)
}
const atLimitAliases: Alias[] = Array.from(
{ length: MAX_ALIASES + Math.round(Math.random() * MAX_ALIASES) },
(_, i) => ({
email: `isolating-cubicle${i}@bravealias.com`,
note: undefined,
domains: undefined,
}),
)
export const LimitReachedBubble = () => {
return (
<EmailAliasModal
aliases={atLimitAliases}
aliasLimit={MAX_ALIASES}
onReturnToMain={() => {}}
editing={false}
mainEmail={demoData.email}
@@ -75,7 +106,7 @@ export default {
decorators: [
(Story: any) => {
return (
<div style={{ width: '420px', margin: '0 auto' }}>
<div style={{ width: '464px', margin: '0 auto' }}>
<Story />
</div>
)
@@ -59,7 +59,6 @@ describe('EmailAliasModal', () => {
<EmailAliasModal
editing={false}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -88,7 +87,6 @@ describe('EmailAliasModal', () => {
editing={true}
editAlias={mockEditAlias}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -158,7 +156,6 @@ describe('EmailAliasModal', () => {
<EmailAliasModal
editing={false}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -202,7 +199,6 @@ describe('EmailAliasModal', () => {
<EmailAliasModal
editing={false}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -228,24 +224,38 @@ describe('EmailAliasModal', () => {
})
})
it('shows limit reached message in bubble mode', async () => {
it('shows limit reached dialog in bubble mode without generating an alias', async () => {
const atLimitAliases: Alias[] = Array.from({ length: 5 }, (_, i) => ({
email: `alias-${i}@bravealias.com`,
note: undefined,
domains: undefined,
}))
render(
<EmailAliasModal
editing={false}
mainEmail={mockEmail}
aliasCount={5}
aliases={atLimitAliases}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
bubble={true}
/>,
)
// Wait for limit check
await waitFor(() => {
expect(
screen.getByText(S.SETTINGS_EMAIL_ALIASES_BUBBLE_LIMIT_REACHED),
screen.getByText(S.SETTINGS_EMAIL_ALIASES_LIMIT_REACHED_ALERT_TITLE),
).toBeInTheDocument()
})
expect(mockEmailAliasesService.generateAlias).not.toHaveBeenCalled()
expect(screen.getByText('alias-0@bravealias.com')).toBeInTheDocument()
expect(
screen.queryByText(S.SETTINGS_EMAIL_ALIASES_CREATE_ALIAS_BUTTON),
).not.toBeInTheDocument()
expect(
screen.getByText(S.SETTINGS_EMAIL_ALIASES_MANAGE_BUTTON),
).toBeInTheDocument()
})
it('shows loading state while generating alias', async () => {
@@ -258,7 +268,6 @@ describe('EmailAliasModal', () => {
<EmailAliasModal
editing={false}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -299,7 +308,6 @@ describe('EmailAliasModal', () => {
<EmailAliasModal
editing={false}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -337,7 +345,6 @@ describe('EmailAliasModal', () => {
editing={true}
editAlias={mockEditAlias}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -400,7 +407,6 @@ describe('EmailAliasModal', () => {
editing={isEditing}
editAlias={alias}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
/>,
@@ -463,7 +469,6 @@ describe('EmailAliasModal', () => {
<EmailAliasModal
editing={false}
mainEmail={mockEmail}
aliasCount={0}
onReturnToMain={mockOnReturnToMain}
emailAliasesService={mockEmailAliasesService}
bubble={true}
@@ -39,8 +39,14 @@
<message name="IDS_SETTINGS_EMAIL_ALIASES_BUBBLE_DESCRIPTION" desc="The description for the email alias bubble" formatter_data="webui=EmailAliases">
Create a random email address that forwards to your inbox while keeping your personal email private.
</message>
<message name="IDS_SETTINGS_EMAIL_ALIASES_BUBBLE_LIMIT_REACHED" desc="The description for the email alias bubble when the limit is reached" formatter_data="webui=EmailAliases">
You have reached the limit of 5 free email aliases. Click "Manage" to re-use or delete an alias.
<message name="IDS_SETTINGS_EMAIL_ALIASES_LIMIT_REACHED_ALERT_TITLE" desc="Title for the email alias limit reached banner in the new-alias dialog" formatter_data="webui=EmailAliases">
Alias limit reached
</message>
<message name="IDS_SETTINGS_EMAIL_ALIASES_LIMIT_REACHED_ALERT_BODY" desc="Body for the email alias limit reached banner; placeholder is the max aliases allowed for the user's plan" formatter_data="webui=EmailAliases">
You've used all <ph name="MAX_ALIASES">$1<ex>5</ex></ph> of your email aliases. Delete an existing alias to create a new one.
</message>
<message name="IDS_SETTINGS_EMAIL_ALIASES_YOUR_ALIASES_COUNT_LABEL" desc="Label above the alias list when showing usage versus plan limit" formatter_data="webui=EmailAliases">
Your aliases (<ph name="CURRENT">$1<ex>5</ex></ph> / <ph name="MAX">$2<ex>5</ex></ph>)
</message>
<message name="IDS_SETTINGS_EMAIL_ALIASES_CREATE_ALIAS_LABEL" desc="The label for the create alias" formatter_data="webui=EmailAliases">
New alias