From 34034c479f63152ee4e8b54c8605ea25c21bd6e3 Mon Sep 17 00:00:00 2001 From: Pavel Beloborodov <73575789+boocmp@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:25:22 +0700 Subject: [PATCH] [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. --- .../email_aliases/email_aliases_controller.cc | 2 +- .../content/email_alias_limit_reached.tsx | 90 ++++++++++++ .../resources/content/email_aliases_list.tsx | 2 +- .../resources/content/email_aliases_modal.tsx | 137 ++++++++++-------- .../resources/email_aliases_panel.html | 3 +- .../resources/email_aliases_panel.tsx | 4 +- .../stories/email_aliases_modal.stories.tsx | 41 +++++- .../tests/email_aliases_modal.test.tsx | 31 ++-- .../resources/email_aliases_strings.grdp | 10 +- 9 files changed, 235 insertions(+), 85 deletions(-) create mode 100644 components/email_aliases/resources/content/email_alias_limit_reached.tsx diff --git a/browser/ui/email_aliases/email_aliases_controller.cc b/browser/ui/email_aliases/email_aliases_controller.cc index b5071d9a514..49cf0eb6502 100644 --- a/browser/ui/email_aliases/email_aliases_controller.cc +++ b/browser/ui/email_aliases/email_aliases_controller.cc @@ -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); diff --git a/components/email_aliases/resources/content/email_alias_limit_reached.tsx b/components/email_aliases/resources/content/email_alias_limit_reached.tsx new file mode 100644 index 00000000000..abef395a085 --- /dev/null +++ b/components/email_aliases/resources/content/email_alias_limit_reached.tsx @@ -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 ( + + + + {getLocale(S.SETTINGS_EMAIL_ALIASES_LIMIT_REACHED_ALERT_TITLE)} + + + {formatLocale(S.SETTINGS_EMAIL_ALIASES_LIMIT_REACHED_ALERT_BODY, { + $1: aliasLimit, + })} + + +
+ + {formatLocale(S.SETTINGS_EMAIL_ALIASES_YOUR_ALIASES_COUNT_LABEL, { + $1: count, + $2: aliasLimit, + })} + + + {aliases.map((alias, index) => ( + + {alias.email} + + ))} + +
+
+ ) +} diff --git a/components/email_aliases/resources/content/email_aliases_list.tsx b/components/email_aliases/resources/content/email_aliases_list.tsx index 670ff4ef38b..c77c69b53a2 100644 --- a/components/email_aliases/resources/content/email_aliases_list.tsx +++ b/components/email_aliases/resources/content/email_aliases_list.tsx @@ -119,7 +119,7 @@ export const AliasList = ({ editing={editState.mode === 'Edit'} editAlias={editState.alias} mainEmail={authEmail} - aliasCount={aliases.length} + aliases={aliases} emailAliasesService={emailAliasesService} /> )} diff --git a/components/email_aliases/resources/content/email_aliases_modal.tsx b/components/email_aliases/resources/content/email_aliases_modal.tsx index 4174885e61e..f7daba93655 100644 --- a/components/email_aliases/resources/content/email_aliases_modal.tsx +++ b/components/email_aliases/resources/content/email_aliases_modal.tsx @@ -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)} - - - - + + {deleteErrorMessage && {deleteErrorMessage}} @@ -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(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 ( @@ -314,10 +328,11 @@ export const EmailAliasModal = ({ {getLocale(S.SETTINGS_EMAIL_ALIASES_BUBBLE_DESCRIPTION)} )} - {bubble && limitReached ? ( - - {getLocale(S.SETTINGS_EMAIL_ALIASES_BUBBLE_LIMIT_REACHED)} - + {limitReached ? ( + ) : ( @@ -372,39 +387,41 @@ export const EmailAliasModal = ({ )} - - - {bubble && ( - - )} - + {getLocale(S.SETTINGS_EMAIL_ALIASES_MANAGE_BUTTON)} + + )} + + {!limitReached && ( - + )} {updateErrorMessage && {updateErrorMessage}} diff --git a/components/email_aliases/resources/email_aliases_panel.html b/components/email_aliases/resources/email_aliases_panel.html index 2c6f5d54790..fee86d1da30 100644 --- a/components/email_aliases/resources/email_aliases_panel.html +++ b/components/email_aliases/resources/email_aliases_panel.html @@ -16,8 +16,7 @@ } :host, body, #mountPoint { display: block; - width: 420px; - max-width: 420px; + width: 100vw; height: auto; } #mountPoint { diff --git a/components/email_aliases/resources/email_aliases_panel.tsx b/components/email_aliases/resources/email_aliases_panel.tsx index dfeff3e4b31..0997b767e43 100644 --- a/components/email_aliases/resources/email_aliases_panel.tsx +++ b/components/email_aliases/resources/email_aliases_panel.tsx @@ -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 ( { switch (action.type) { case EmailAliasModalResultType.Cancelled: @@ -74,7 +77,6 @@ export const EmailAliasesPanelConnected = ({ }} editing={false} mainEmail={authState.email} - aliasCount={aliasesState.length} emailAliasesService={emailAliasesService} bubble /> diff --git a/components/email_aliases/resources/stories/email_aliases_modal.stories.tsx b/components/email_aliases/resources/stories/email_aliases_modal.stories.tsx index 5099a63cbe6..ad52ad25d28 100644 --- a/components/email_aliases/resources/stories/email_aliases_modal.stories.tsx +++ b/components/email_aliases/resources/stories/email_aliases_modal.stories.tsx @@ -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 ( {}} editing={false} mainEmail={demoData.email} @@ -35,7 +40,8 @@ export const EditAliasDialog = () => { return ( {}} editing mainEmail={demoData.email} @@ -59,7 +65,32 @@ export const DeleteAliasDialog = () => { export const Panel = () => { return ( {}} + 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 ( + {}} editing={false} mainEmail={demoData.email} @@ -75,7 +106,7 @@ export default { decorators: [ (Story: any) => { return ( -
+
) diff --git a/components/email_aliases/resources/tests/email_aliases_modal.test.tsx b/components/email_aliases/resources/tests/email_aliases_modal.test.tsx index 4c4a288d4a9..919a918d424 100644 --- a/components/email_aliases/resources/tests/email_aliases_modal.test.tsx +++ b/components/email_aliases/resources/tests/email_aliases_modal.test.tsx @@ -59,7 +59,6 @@ describe('EmailAliasModal', () => { , @@ -88,7 +87,6 @@ describe('EmailAliasModal', () => { editing={true} editAlias={mockEditAlias} mainEmail={mockEmail} - aliasCount={0} onReturnToMain={mockOnReturnToMain} emailAliasesService={mockEmailAliasesService} />, @@ -158,7 +156,6 @@ describe('EmailAliasModal', () => { , @@ -202,7 +199,6 @@ describe('EmailAliasModal', () => { , @@ -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( , ) - // 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', () => { , @@ -299,7 +308,6 @@ describe('EmailAliasModal', () => { , @@ -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', () => { Create a random email address that forwards to your inbox while keeping your personal email private. - - You have reached the limit of 5 free email aliases. Click "Manage" to re-use or delete an alias. + + Alias limit reached + + + You've used all $15 of your email aliases. Delete an existing alias to create a new one. + + + Your aliases ($15 / $25) New alias