diff options
| author | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-11 09:35:06 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-11 09:35:06 +0200 |
| commit | e7de4693adc78878d2686f228b5c91c6bb9bc319 (patch) | |
| tree | 256c461cbb139d8d21061b4bd4632458d1c097f1 | |
| parent | 8e7b6dcdf884f5f2b3876fd4d88f82797a50d850 (diff) | |
| parent | cf026ee0306eee2a9d6c3afe3fde0735565b383c (diff) | |
| download | mullvadvpn-e7de4693adc78878d2686f228b5c91c6bb9bc319.tar.xz mullvadvpn-e7de4693adc78878d2686f228b5c91c6bb9bc319.zip | |
Merge branch 'add-login-button-des-2396'
17 files changed, 234 insertions, 161 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot index bdc7ffe220..3eafee0b0e 100644 --- a/desktop/packages/mullvad-vpn/locales/messages.pot +++ b/desktop/packages/mullvad-vpn/locales/messages.pot @@ -1484,8 +1484,9 @@ msgctxt "login-view" msgid "Checking account number" msgstr "" +#. Text in button that allows user to create a new account. msgctxt "login-view" -msgid "Create account" +msgid "Create a new account" msgstr "" #. Button which confirms the action to create a new account. @@ -1508,10 +1509,6 @@ msgid "Do you want to remove the saved account number?" msgstr "" msgctxt "login-view" -msgid "Don’t have an account number?" -msgstr "" - -msgctxt "login-view" msgid "Enter your account number" msgstr "" @@ -1547,6 +1544,7 @@ msgctxt "login-view" msgid "Logging in..." msgstr "" +#. Label for the login button. msgctxt "login-view" msgid "Login" msgstr "" @@ -1555,6 +1553,13 @@ msgctxt "login-view" msgid "Login failed" msgstr "" +#. Text shown between two horizontal lines above the "create account" button. +#. In this context it is used to separate the users alternative of logging in +#. or creating a new account, "Login or Create a new account". +msgctxt "login-view" +msgid "Or" +msgstr "" + #. This is a warning message shown when the app is blocking the users #. internet connection while logged out. msgctxt "login-view" @@ -2965,6 +2970,9 @@ msgstr "" msgid "Create" msgstr "" +msgid "Create account" +msgstr "" + msgid "Create new list" msgstr "" @@ -3019,6 +3027,9 @@ msgstr "" msgid "Dismiss" msgstr "" +msgid "Don’t have an account number?" +msgstr "" + msgid "Edit custom lists" msgstr "" diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx index 16fa8bcbda..eb40349089 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx @@ -3,7 +3,6 @@ import { Route, Switch } from 'react-router'; import { RoutePath } from '../../shared/routes'; import SelectLocation from '../components/select-location/SelectLocationContainer'; -import LoginPage from '../components/views/login/Login'; import { useViewTransitions } from '../lib/transition-hooks'; import Account from './Account'; import ApiAccessMethods from './ApiAccessMethods'; @@ -34,7 +33,14 @@ import Support from './Support'; import TooManyDevices from './TooManyDevices'; import UdpOverTcp from './UdpOverTcp'; import UserInterfaceSettings from './UserInterfaceSettings'; -import { AppInfoView, AppUpgradeView, ChangelogView, LaunchView, SettingsView } from './views'; +import { + AppInfoView, + AppUpgradeView, + ChangelogView, + LaunchView, + LoginView, + SettingsView, +} from './views'; import VpnSettings from './VpnSettings'; import WireguardSettings from './WireguardSettings'; @@ -50,7 +56,7 @@ export default function AppRouter() { <Focus ref={focusRef}> <Switch key={currentLocation.key} location={currentLocation}> <Route exact path={RoutePath.launch} component={LaunchView} /> - <Route exact path={RoutePath.login} component={LoginPage} /> + <Route exact path={RoutePath.login} component={LoginView} /> <Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} /> <Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} /> <Route exact path={RoutePath.main} component={MainView} /> diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx index 204da6fabc..837b0701cf 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx @@ -32,11 +32,7 @@ const formatSubtitle = (subtitle: InAppNotificationSubtitle) => { ); case 'run-function': return ( - <StyledLink - forwardedAs="button" - color="white" - variant="labelTiny" - {...subtitle.action.button}> + <StyledLink forwardedAs="button" variant="labelTiny" {...subtitle.action.button}> <StyledLink.Text>{content}</StyledLink.Text> </StyledLink> ); diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts index 2ce9d17013..c307b7eb71 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts @@ -1,5 +1,6 @@ export * from './app-info'; export * from './app-upgrade'; export * from './launch'; +export * from './login'; export * from './changelog'; export * from './settings'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/login/LoginStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/login/LoginStyles.tsx index 54bdeab8b4..f7aa67d824 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/login/LoginStyles.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/login/LoginStyles.tsx @@ -1,10 +1,9 @@ import styled from 'styled-components'; -import { Icon } from '../../../lib/components'; +import { Icon, Layout } from '../../../lib/components'; import { colors, spacings } from '../../../lib/foundations'; -import { hugeText, largeText, measurements, smallText, tinyText } from '../../common-styles'; +import { hugeText, largeText, smallText, tinyText } from '../../common-styles'; import FormattableTextInput from '../../FormattableTextInput'; -import { Footer } from '../../Layout'; export const StyledAccountDropdownContainer = styled.ul({ display: 'flex', @@ -46,46 +45,23 @@ export const StyledAccountDropdownItemIconButton = styled.button({ justifyContent: 'center', }); -export const StyledTopInfo = styled.div({ - display: 'flex', - justifyContent: 'center', - flex: 1, -}); - -export const StyledFooter = styled(Footer)<{ $show: boolean }>((props) => ({ - position: 'relative', - width: '100%', - bottom: 0, - transform: `translateY(${props.$show ? 0 : 100}%)`, - backgroundColor: colors.darkBlue, - transition: 'transform 250ms ease-in-out', -})); - export const StyledStatusIcon = styled.div({ display: 'flex', alignSelf: 'end', flex: 0, - marginBottom: '30px', justifyContent: 'center', + marginTop: spacings.large, height: '48px', minHeight: '48px', }); -export const StyledLoginForm = styled.div({ - display: 'flex', - flex: '0 1 225px', - flexDirection: 'column', - overflow: 'visible', - padding: `0 ${measurements.horizontalViewMargin}`, -}); - interface IStyledAccountInputGroupProps { $editable: boolean; $active: boolean; $error: boolean; } -export const StyledAccountInputGroup = styled.form<IStyledAccountInputGroupProps>((props) => ({ +export const StyledAccountInputGroup = styled.div<IStyledAccountInputGroupProps>((props) => ({ borderWidth: '2px', borderStyle: 'solid', borderRadius: '8px', @@ -100,17 +76,6 @@ export const StyledAccountInputBackdrop = styled.div({ borderColor: colors.darkBlue, }); -export const StyledInputButton = styled.button<{ $visible: boolean }>((props) => ({ - display: 'flex', - borderWidth: 0, - width: '48px', - alignItems: 'center', - justifyContent: 'center', - opacity: props.$visible ? 1 : 0, - transition: 'opacity 250ms ease-in-out', - backgroundColor: colors.green, -})); - export const StyledDropdownSpacer = styled.div({ height: 1, backgroundColor: colors.darkBlue, @@ -142,7 +107,6 @@ export const StyledBlockMessageContainer = styled.div({ alignSelf: 'start', backgroundColor: colors.darkBlue, borderRadius: '8px', - margin: '5px 16px 10px', padding: '16px', }); @@ -156,3 +120,9 @@ export const StyledBlockMessage = styled.div(tinyText, { color: colors.white, marginBottom: '10px', }); + +export const StyledLine = styled(Layout)` + height: 1px; + width: 100%; + background-color: ${colors.whiteAlpha20}; +`; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/login/Login.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/login/LoginView.tsx index d085ea2b54..87de6e1a61 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/login/Login.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/login/LoginView.tsx @@ -14,10 +14,13 @@ import { Flex, Icon, Label, - LabelTiny, + Link, Spinner, + Text, TitleMedium, } from '../../../lib/components'; +import { FlexColumn } from '../../../lib/components/flex-column'; +import { View } from '../../../lib/components/view'; import { colors } from '../../../lib/foundations'; import { formatHtml } from '../../../lib/html-formatter'; import { IconBadge } from '../../../lib/icon-badge'; @@ -26,7 +29,6 @@ import { LoginState } from '../../../redux/account/reducers'; import { useSelector } from '../../../redux/store'; import Accordion from '../../Accordion'; import { AppMainHeader } from '../../app-main-header'; -import { Container, Layout } from '../../Layout'; import ClearAccountHistoryDialog from './ClearAccountHistoryDialog'; import CreateAccountDialog from './CreateAccountDialog'; import { @@ -40,17 +42,12 @@ import { StyledBlockMessageContainer, StyledBlockTitle, StyledDropdownSpacer, - StyledFooter, StyledInput, - StyledInputButton, - StyledInputSubmitIcon, - StyledLoginForm, + StyledLine, StyledStatusIcon, - StyledTitle, - StyledTopInfo, } from './LoginStyles'; -export default function LoginContainer() { +export function LoginView() { const { openUrl, login, clearAccountHistory, createNewAccount } = useAppContext(); const { resetLoginError, updateAccountNumber } = useActions(accountActions); @@ -136,26 +133,46 @@ class Login extends React.Component<IProps, IState> { public render() { const allowInteraction = this.allowInteraction(); - return ( - <Layout> + <View> <AppMainHeader> <AppMainHeader.SettingsButton disabled={!allowInteraction} /> </AppMainHeader> - <Container> - <StyledTopInfo> - {this.props.showBlockMessage ? <BlockMessage /> : this.getStatusIcon()} - </StyledTopInfo> - - <StyledLoginForm> - <StyledTitle aria-live="polite">{this.formTitle()}</StyledTitle> + <View.Container size="4" $justifyContent="center" $padding={{ bottom: 'large' }}> + <FlexColumn $gap="medium"> + <Flex $flex={1} $justifyContent="center"> + {this.props.showBlockMessage ? <BlockMessage /> : this.getStatusIcon()} + </Flex> - {this.createLoginForm()} - </StyledLoginForm> + <FlexColumn + $gap="large" + $margin={{ horizontal: 'small' }} + $justifyContent="center" + $flexGrow={1}> + <FlexColumn $gap="small"> + <Text as="h1" variant="titleBig" aria-live="polite"> + {this.formTitle()} + </Text> - <StyledFooter $show={allowInteraction}>{this.createFooter()}</StyledFooter> - </Container> - </Layout> + {this.createLoginForm()} + </FlexColumn> + <Flex $justifyContent="center"> + <StyledLine $margin={{ vertical: 'small', right: 'small' }} /> + <Text variant="labelTiny"> + { + // TRANSLATORS: Text shown between two horizontal lines above the "create account" button. + // TRANSLATORS: In this context it is used to separate the users alternative of logging in + // TRANSLATORS: or creating a new account, "Login or Create a new account". + messages.pgettext('login-view', 'Or') + } + </Text> + <StyledLine $margin={{ vertical: 'small', left: 'small' }} /> + </Flex> + </FlexColumn> + {this.createFooter()} + </FlexColumn> + </View.Container> + </View> ); } @@ -354,59 +371,60 @@ class Login extends React.Component<IProps, IState> { return ( <> - <Flex $flexDirection="column" $gap="small"> - <Label htmlFor={inputId} data-testid="subtitle"> + <Flex $flexDirection="column" $gap="tiny"> + <Label htmlFor={inputId} variant="labelTiny" color="whiteAlpha60" data-testid="subtitle"> {this.formSubtitle()} </Label> - <StyledAccountInputGroup - $active={allowInteraction && this.state.isActive} - $editable={allowInteraction} - $error={hasError} - onSubmit={this.onSubmit}> - <StyledAccountInputBackdrop> - <StyledInput - id={inputId} - allowedCharacters="[0-9]" - separator=" " - groupLength={4} - placeholder="0000 0000 0000 0000" - value={this.props.accountNumber || ''} - disabled={!allowInteraction} - onFocus={this.onFocus} - onBlur={this.onBlur} - handleChange={this.onInputChange} - autoFocus={true} - ref={this.accountInput} - aria-autocomplete="list" - /> - <StyledInputButton + <form onSubmit={this.onSubmit}> + <FlexColumn $gap="large"> + <StyledAccountInputGroup + $active={allowInteraction && this.state.isActive} + $editable={allowInteraction} + $error={hasError}> + <StyledAccountInputBackdrop> + <StyledInput + id={inputId} + allowedCharacters="[0-9]" + separator=" " + groupLength={4} + placeholder="0000 0000 0000 0000" + value={this.props.accountNumber || ''} + disabled={!allowInteraction} + onFocus={this.onFocus} + onBlur={this.onBlur} + handleChange={this.onInputChange} + autoFocus={true} + ref={this.accountInput} + aria-autocomplete="list" + /> + </StyledAccountInputBackdrop> + <Accordion expanded={this.shouldShowAccountHistory()}> + <StyledAccountDropdownContainer> + <AccountDropdown + item={this.props.accountHistory} + onSelect={this.onSelectAccountFromHistory} + onRemove={this.onClearAccountHistory} + /> + </StyledAccountDropdownContainer> + </Accordion> + </StyledAccountInputGroup> + <Button type="submit" - $visible={allowLogin} + variant="success" disabled={!allowLogin} aria-label={ // TRANSLATORS: This is used by screenreaders to communicate the login button. messages.pgettext('accessibility', 'Login') }> - <StyledInputSubmitIcon - $visible={ - this.props.loginState.type !== 'logging in' && - !this.props.isPerformingPostUpgrade + <Button.Text> + { + // TRANSLATORS: Label for the login button. + messages.pgettext('login-view', 'Login') } - icon="chevron-right" - size="large" - /> - </StyledInputButton> - </StyledAccountInputBackdrop> - <Accordion expanded={this.shouldShowAccountHistory()}> - <StyledAccountDropdownContainer> - <AccountDropdown - item={this.props.accountHistory} - onSelect={this.onSelectAccountFromHistory} - onRemove={this.onClearAccountHistory} - /> - </StyledAccountDropdownContainer> - </Accordion> - </StyledAccountInputGroup> + </Button.Text> + </Button> + </FlexColumn> + </form> </Flex> <ClearAccountHistoryDialog @@ -421,13 +439,15 @@ class Login extends React.Component<IProps, IState> { private createFooter() { return ( <> - <Flex $flexDirection="column" $gap="small"> - <LabelTiny color="whiteAlpha60"> - {messages.pgettext('login-view', 'Don’t have an account number?')} - </LabelTiny> - <Button onClick={this.onCreateNewAccount} disabled={!this.allowCreateAccount()}> - <Button.Text>{messages.pgettext('login-view', 'Create account')}</Button.Text> - </Button> + <Flex $flexDirection="column" $gap="small" $alignItems="center"> + <Link as="button" onClick={this.onCreateNewAccount} disabled={!this.allowCreateAccount()}> + <Link.Text> + { + // TRANSLATORS: Text in button that allows user to create a new account. + messages.pgettext('login-view', 'Create a new account') + } + </Link.Text> + </Link> </Flex> <CreateAccountDialog visible={this.state.createAccountDialogVisible} @@ -511,6 +531,7 @@ function AccountDropdownItem({ label, onRemove, onSelect, value }: AccountDropdo <Box $height="48px" $width="48px" center> <StyledAccountDropdownItemIconButton onClick={handleRemove} + type="button" aria-controls={itemId} aria-label={sprintf( // TRANSLATORS: This is used by screenreaders to communicate the "x" button next to a saved account number. diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/login/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/login/index.ts new file mode 100644 index 0000000000..0ded5f7ba6 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/login/index.ts @@ -0,0 +1 @@ +export * from './LoginView'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx index 5e1bef8d5f..747eff06ed 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx @@ -1,26 +1,25 @@ import React from 'react'; import styled, { css } from 'styled-components'; -import { Colors, colors, Radius, Typography } from '../../foundations'; -import { TransientProps } from '../../types'; +import { Colors, colors, Typography } from '../../foundations'; +import { PolymorphicProps } from '../../types'; import { LinkIcon, LinkText, StyledIcon as StyledLinkIcon, StyledLinkText } from './components'; -import { useHoverColor } from './hooks'; +import { useStateColors } from './hooks'; import { LinkProvider } from './LinkContext'; type LinkBaseProps = { variant?: Typography; color?: Colors; - onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void; }; -export type LinkProps = LinkBaseProps & - Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkBaseProps>; +export type LinkProps<T extends React.ElementType = 'a'> = PolymorphicProps<T, LinkBaseProps>; -const StyledLink = styled.a< - TransientProps<LinkProps> & { - $hoverColor?: Colors; - } ->(({ $hoverColor }) => { +const StyledLink = styled.a<{ + $hoverColor: Colors; + $activeColor: Colors; +}>(({ $hoverColor, $activeColor }) => { + const hoverColor = colors[$hoverColor]; + const activeColor = colors[$activeColor]; return css` cursor: default; text-decoration: none; @@ -28,17 +27,22 @@ const StyledLink = styled.a< width: fit-content; &&:hover > ${StyledLinkText} { - text-decoration-line: underline; - text-underline-offset: 2px; - color: ${$hoverColor}; + color: ${hoverColor}; + } + + &&:active > ${StyledLinkText} { + color: ${activeColor}; } &&:focus-visible > ${StyledLinkText} { - border-radius: ${Radius.radius4}; outline: 2px solid ${colors.white}; outline-offset: 2px; } + &&:disabled > ${StyledLinkText} { + color: ${colors.whiteAlpha40}; + } + > ${StyledLinkIcon}:first-child:not(:only-child) { margin-right: 2px; } @@ -48,11 +52,16 @@ const StyledLink = styled.a< `; }); -function Link({ color, variant, children, ...props }: LinkProps) { - const hoverColor = useHoverColor(color); +function Link<T extends React.ElementType = 'a'>({ + color = 'chalk', + variant, + children, + ...props +}: LinkProps<T>) { + const { hover, active } = useStateColors(color); return ( <LinkProvider variant={variant} color={color}> - <StyledLink $hoverColor={hoverColor} {...props}> + <StyledLink $hoverColor={hover} $activeColor={active} draggable={false} {...props}> {children} </StyledLink> </LinkProvider> diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx index 3a82f11a4d..6b4ac8f4f0 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx @@ -11,5 +11,5 @@ export const StyledIcon = styled(Icon)` `; export function LinkIcon({ ...props }: LinkIconProps) { - return <StyledIcon size="small" {...props} />; + return <StyledIcon size="small" color="chalk" {...props} />; } diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkText.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkText.tsx index c8c1ff8baa..26acb87e15 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkText.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkText.tsx @@ -5,7 +5,10 @@ import { useLinkContext } from '../LinkContext'; export type LinkTextProps = TextProps; -export const StyledLinkText = styled(Text)``; +export const StyledLinkText = styled(Text)` + text-decoration-line: underline; + text-underline-offset: 2px; +`; export function LinkText(props: LinkTextProps) { const { variant, color } = useLinkContext(); diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/index.ts index 4f17d940a4..700c211e5c 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/index.ts @@ -1 +1 @@ -export * from './use-hover-color'; +export * from './use-state-colors'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-hover-color.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-hover-color.ts deleted file mode 100644 index f62f26a6fe..0000000000 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-hover-color.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Colors } from '../../../foundations'; - -export const useHoverColor = (color: Colors | undefined) => { - switch (color) { - case 'whiteAlpha60': - return 'white'; - default: - return undefined; - } -}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-state-colors.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-state-colors.ts new file mode 100644 index 0000000000..b1a7426e14 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-state-colors.ts @@ -0,0 +1,24 @@ +import { Colors } from '../../../foundations'; + +const colorMap: Record< + Extract<Colors, 'chalk'> | 'default', + { + hover: Colors; + active: Colors; + } +> = { + chalk: { hover: 'whiteAlpha60', active: 'whiteAlpha20' }, + default: { hover: 'whiteAlpha60', active: 'whiteAlpha20' }, +} as const; + +export const useStateColors = ( + color: Colors | undefined, +): { + hover: Colors; + active: Colors; +} => { + if (color === 'chalk') { + return colorMap[color]; + } + return colorMap.default; +}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx index 69d2f5291b..3425a70397 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx @@ -16,7 +16,7 @@ const sizes: Record<'3' | '4', string> = { const StyledFlex = styled(Flex)<{ $size: string }>((props) => ({ width: props.$size, - margin: 'auto', + margin: '0 auto', })); export function Container({ size = '4', ...props }: ContainerProps) { diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/login.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/login.spec.ts index fb868a639e..b7e0801ceb 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/mocked/login.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/login.spec.ts @@ -10,7 +10,7 @@ let routes: RoutesObjectModel; test.describe.configure({ mode: 'parallel' }); -test.describe('Clear account history warnings', () => { +test.describe('Login view', () => { const startup = async () => { ({ page, util } = await startMockedApp()); routes = new RoutesObjectModel(page, util); @@ -42,6 +42,32 @@ test.describe('Clear account history warnings', () => { await util.ipc.accountHistory[''].notify('1234123412341234'); }; + test('Should try to login when clicking login button', async () => { + await routes.login.fillAccountNumber('1234 1234 1234 1234'); + + await Promise.all([util.ipc.account.login.expect(), routes.login.loginByPressingEnter()]); + const header = routes.login.selectors.header(); + await expect(header).toHaveText('Logging in...'); + await expect(routes.login.selectors.loginButton()).toBeDisabled(); + }); + + test('Should try to login when pressing enter', async () => { + await routes.login.fillAccountNumber('1234 1234 1234 1234'); + + await Promise.all([util.ipc.account.login.expect(), routes.login.loginByPressingEnter()]); + const header = routes.login.selectors.header(); + await expect(header).toHaveText('Logging in...'); + await expect(routes.login.selectors.loginButton()).toBeDisabled(); + }); + + test('Should disable login button when input is invalid', async () => { + const loginButton = routes.login.selectors.loginButton(); + await expect(loginButton).toBeDisabled(); + + await routes.login.fillAccountNumber('1234 1234'); + await expect(loginButton).toBeDisabled(); + }); + test('Should not warn about creating an account', async () => { const accountHistoryItemButton = routes.login.getAccountHistoryItemButton(); await expect(accountHistoryItemButton).not.toBeVisible(); diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/login-route-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/login-route-object-model.ts index 4dfe73a2a8..a866052551 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/login-route-object-model.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/login-route-object-model.ts @@ -18,6 +18,18 @@ export class LoginRouteObjectModel { await this.utils.waitForRoute(RoutePath.login); } + fillAccountNumber(accountNumber: string) { + return this.selectors.loginInput().fill(accountNumber); + } + + async loginByPressingEnter() { + await this.selectors.loginInput().press('Enter'); + } + + async loginByClickingLoginButton() { + await this.selectors.loginButton().click(); + } + async createNewAccount() { await this.selectors.createNewAccountButton().click(); } diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/selectors.ts index c49df8ce87..12ed0720db 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/selectors.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/login/selectors.ts @@ -1,7 +1,7 @@ import { Page } from 'playwright'; export const createSelectors = (page: Page) => ({ - createNewAccountButton: () => page.getByRole('button', { name: 'Create account' }), + createNewAccountButton: () => page.getByRole('button', { name: 'Create a new account' }), createNewAccountMessage: () => page.getByText('Do you want to create a new account?'), confirmCreateNewAccountButton: () => page.getByRole('button', { name: 'Create new account' }), @@ -12,4 +12,7 @@ export const createSelectors = (page: Page) => ({ confirmClearAccountHistoryButton: () => page.getByRole('button', { name: 'Remove' }), cancelDialogButton: () => page.getByRole('button', { name: 'Cancel' }), + loginInput: () => page.getByLabel('Enter your account number'), + loginButton: () => page.getByRole('button', { name: 'Login', exact: true }), + header: () => page.getByRole('heading', { level: 1 }), }); |
