diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index f9319f97ba3..f940f0d033c 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -1242,7 +1242,7 @@ "modalAccountLeaveGuestRoomAction": "Leave", "modalAccountLeaveGuestRoomHeadline": "Leave the guest room?", "modalAccountLeaveGuestRoomMessage": "Conversation history will be deleted. To keep it, create an account next time.", - "modalAccountLogoutAction": "Log out", + "modalAccountLogoutAction": "Log Out", "modalAccountLogoutHeadline": "Clear Data?", "modalAccountLogoutOption": "Delete all your personal information and conversations on this device.", "modalAccountNewDevicesFrom": "From:", @@ -1259,16 +1259,18 @@ "modalAccountRemoveDevicePlaceholder": "Password", "modalAcknowledgeAction": "Ok", "modalAcknowledgeHeadline": "Something went wrong", - "modalAppLockForgotGoBackButton": "Go back", - "modalAppLockForgotMessage": "The data stored on this device can only be accessed with your app lock passcode. If you have forgotten your passcode, you can reset this client.", - "modalAppLockForgotTitle": "Forgot your app lock passcode?", - "modalAppLockForgotWipeCTA": "Reset this client", + "modalAppLockForgotGoBackButton": "Back", + "modalAppLockForgotMessage": "The data stored on this device can only be accessed with your app lock passcode.", + "modalAppLockForgotSecondMessage":"If you have forgotten your passcode, you can log out of this account and set a new passcode the next time you log in.", + "modalAppLockForgotTitle": "Forgot passcode?", "modalAppLockLockedError": "Wrong passcode", - "modalAppLockLockedForgotCTA": "Access as new device", + "modalAppLockLockedForgotCTA": "Forgot passcode?", + "modalAppLockLogoutCancelButton": "Cancel", + "modalAppLockInputPlaceholder": "Enter passcode", "modalAppLockLockedTitle": "Enter passcode to unlock {brandName}", "modalAppLockLockedUnlockButton": "Unlock", "modalAppLockPasscode": "Passcode", - "modalAppLockSetupAcceptButton": "Set passcode", + "modalAppLockSetupAcceptButton": "Create Passcode", "modalAppLockSetupChangeMessage": "Your organization needs to lock your app when {brandName} is not in use to keep the team safe.[br]Create a passlock to unlock {brandName}. Please, remember it, as it can not be recovered.", "modalAppLockSetupChangeTitle": "There was a change at {brandName}", "modalAppLockSetupCloseBtn": "Close window, Set app lock passcode?", @@ -1278,17 +1280,8 @@ "modalAppLockSetupMessage": "The app will lock itself after a certain time of inactivity.[br]To unlock the app you need to enter this passcode.[br]Make sure to remember this passcode as there is no way to recover it.", "modalAppLockSetupSecondPlaceholder": "Repeat passcode", "modalAppLockSetupSpecial": "A special character", - "modalAppLockSetupTitle": "Set app lock passcode", + "modalAppLockSetupTitle": "Create passcode", "modalAppLockSetupUppercase": "An uppercase letter", - "modalAppLockWipeConfirmConfirmButton": "Reset this client", - "modalAppLockWipeConfirmGoBackButton": "Go back", - "modalAppLockWipeConfirmMessage": "All your conversation history will be permanently deleted from this client. You may then log in again.", - "modalAppLockWipeConfirmTitle": "Do you really want to reset this client?", - "modalAppLockWipePasswordConfirmButton": "Reset this client", - "modalAppLockWipePasswordError": "Wrong password", - "modalAppLockWipePasswordGoBackButton": "Go back", - "modalAppLockWipePasswordPlaceholder": "Password", - "modalAppLockWipePasswordTitle": "Enter your {brandName} account password to reset this client", "modalAssetFileTypeRestrictionHeadline": "Restricted filetype", "modalAssetFileTypeRestrictionMessage": "The filetype of \"{fileName}\" is not allowed.", "modalAssetParallelUploadsHeadline": "Too many files at once", diff --git a/apps/webapp/src/script/page/AppLock/AppLock.test.tsx b/apps/webapp/src/script/page/AppLock/AppLock.test.tsx index ced8b79d4c6..922b13269d7 100644 --- a/apps/webapp/src/script/page/AppLock/AppLock.test.tsx +++ b/apps/webapp/src/script/page/AppLock/AppLock.test.tsx @@ -32,6 +32,7 @@ import {UserState} from 'Repositories/user/UserState'; import {createUuid} from 'Util/uuid'; import {AppLock, APPLOCK_STATE} from './AppLock'; +import {withTheme} from 'src/script/auth/util/test/TestUtil'; // https://github.com/jedisct1/libsodium.js/issues/235 jest.mock('libsodium-wrappers', () => ({ @@ -91,7 +92,7 @@ describe('AppLock', () => { clientRepository, }; - const {queryByTestId} = render(); + const {queryByTestId} = render(withTheme()); const appLockModal = queryByTestId('applock-modal'); expect(appLockModal).toBe(null); @@ -112,7 +113,7 @@ describe('AppLock', () => { clientRepository, }; - const {getByTestId} = render(); + const {getByTestId} = render(withTheme()); const appLockModalBody = getByTestId('applock-modal-body'); expect(appLockModalBody.getAttribute('data-uie-value')).toEqual(APPLOCK_STATE.LOCKED); @@ -131,7 +132,7 @@ describe('AppLock', () => { clientRepository, }; - const {getByTestId} = render(); + const {getByTestId} = render(withTheme()); act(() => { amplify.publish(WebAppEvents.PREFERENCES.CHANGE_APP_LOCK_PASSPHRASE); @@ -153,7 +154,7 @@ describe('AppLock', () => { clientRepository, }; - const {getByTestId} = render(); + const {getByTestId} = render(withTheme()); act(() => { amplify.publish(WebAppEvents.PREFERENCES.CHANGE_APP_LOCK_PASSPHRASE); @@ -177,7 +178,7 @@ describe('AppLock', () => { clientRepository, }; - const {getByTestId} = render(); + const {getByTestId} = render(withTheme()); const appLockModal = getByTestId('applock-modal'); expect(window.getComputedStyle(appLockModal).getPropertyValue('display')).toBe('flex'); diff --git a/apps/webapp/src/script/page/AppLock/AppLock.tsx b/apps/webapp/src/script/page/AppLock/AppLock.tsx index e2dacf03cba..145734437ef 100644 --- a/apps/webapp/src/script/page/AppLock/AppLock.tsx +++ b/apps/webapp/src/script/page/AppLock/AppLock.tsx @@ -21,10 +21,10 @@ import {useCallback, useEffect, useRef, useState, Fragment, FormEvent} from 'rea import {amplify} from 'amplify'; import cx from 'classnames'; -import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import {container} from 'tsyringe'; import {ValidationUtil} from '@wireapp/commons'; +import {Button, ButtonVariant, Checkbox, CheckboxLabel, Input, Link, LinkVariant} from '@wireapp/react-ui-kit'; import {WebAppEvents} from '@wireapp/webapp-events'; import * as Icon from 'Components/Icon'; @@ -38,14 +38,15 @@ import {Config} from 'src/script/Config'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; +import {applockStyles} from './Applock.styles'; + export enum APPLOCK_STATE { FORGOT = 'applock.forgot', LOCKED = 'applock.locked', + LOGOUT = 'applock.logout', NONE = 'applock.none', SETUP = 'applock.setup', SETUP_CHANGE = 'applock.setup_change', - WIPE_CONFIRM = 'applock.wipe-confirm', - WIPE_PASSWORD = 'applock.wipe-password', } const DEFAULT_INACTIVITY_APP_LOCK_TIMEOUT_IN_SEC = 60; @@ -71,11 +72,10 @@ const AppLock = ({ appLockRepository = container.resolve(AppLockRepository), }: AppLockProps) => { const [state, setState] = useState(APPLOCK_STATE.NONE); - const [wipeError, setWipeError] = useState(''); const [unlockError, setUnlockError] = useState(''); const [isVisible, setIsVisible] = useState(false); - const [isLoading, setIsLoading] = useState(false); const [setupPassphrase, setSetupPassphrase] = useState(''); + const [clearData, setClearData] = useState(false); const [inactivityTimeoutId, setInactivityTimeoutId] = useState(); const [scheduledTimeoutId, setScheduledTimeoutId] = useState(); const {isAppLockActivated, isAppLockEnabled, isAppLockEnforced} = useKoSubscribableChildren(appLockState, [ @@ -84,6 +84,8 @@ const AppLock = ({ 'isAppLockEnforced', ]); + const isTemporaryClient = clientState.currentClient?.isTemporary(); + // We log the user out if there is a style change on the app element // i.e. if there is an attempt to remove the blur effect const {current: appObserver} = useRef( @@ -207,21 +209,12 @@ const AppLock = ({ startScheduledTimeout(); }; - const onWipeDatabase = async (event: FormEvent) => { - const target = event.target as HTMLFormElement & {password: HTMLInputElement}; - try { - setIsLoading(true); - const currentClientId = clientState.currentClient.id; - await clientRepository.clientService.deleteClient(currentClientId, target.password.value); - appLockRepository.removeCode(); - amplify.publish(WebAppEvents.LIFECYCLE.SIGN_OUT, SIGN_OUT_REASON.USER_REQUESTED, true); - } catch ({code, message}) { - setIsLoading(false); - if ([HTTP_STATUS.BAD_REQUEST, HTTP_STATUS.UNAUTHORIZED, HTTP_STATUS.FORBIDDEN].includes(code)) { - return setWipeError(t('modalAppLockWipePasswordError')); - } - setWipeError(message); + const onLogout = (clearData: boolean) => { + if (!isAppLockEnforced) { + appLockRepository.disableFeature(); } + appLockRepository.removeCode(); + amplify.publish(WebAppEvents.LIFECYCLE.SIGN_OUT, SIGN_OUT_REASON.USER_REQUESTED, clearData); }; const changePassphrase = () => { @@ -236,12 +229,16 @@ const AppLock = ({ const isSetupPassphraseLength = passwordRegexLength.test(setupPassphrase); const isSetupPassphraseSpecial = passwordRegexSpecial.test(setupPassphrase); - const clearWipeError = () => setWipeError(''); const clearUnlockError = () => setUnlockError(''); const onGoBack = () => setState(APPLOCK_STATE.LOCKED); const onClickForgot = () => setState(APPLOCK_STATE.FORGOT); - const onClickWipe = () => setState(APPLOCK_STATE.WIPE_CONFIRM); - const onClickWipeConfirm = () => setState(APPLOCK_STATE.WIPE_PASSWORD); + const onClickLogout = async () => { + if (isTemporaryClient) { + await clientRepository.logoutClient(); + } else { + setState(APPLOCK_STATE.LOGOUT); + } + }; const onClosed = () => { setState(APPLOCK_STATE.NONE); setSetupPassphrase(''); @@ -259,20 +256,24 @@ const AppLock = ({ return t('modalAppLockSetupTitle'); case APPLOCK_STATE.LOCKED: return t('modalAppLockLockedTitle', {brandName: Config.getConfig().BRAND_NAME}); + case APPLOCK_STATE.LOGOUT: + return t('modalAccountLogoutHeadline'); case APPLOCK_STATE.FORGOT: return t('modalAppLockForgotTitle'); - case APPLOCK_STATE.WIPE_CONFIRM: - return t('modalAppLockWipeConfirmTitle'); - case APPLOCK_STATE.WIPE_PASSWORD: - return t('modalAppLockWipePasswordTitle', {brandName: Config.getConfig().BRAND_NAME}); default: return ''; } }; + const ErrorMessage = () => ( +

+ {unlockError} +

+ ); + return ( - -
+ +
{!isAppLockEnforced && !isAppLockActivated && ( + )} - +
)} @@ -403,17 +400,15 @@ const AppLock = ({ data-uie-name="label-applock-set-text" /> -
- {t('modalAppLockPasscode')} -
- - setSetupPassphrase(event.target.value)} + onChange={(event: React.ChangeEvent) => setSetupPassphrase(event.target.value)} data-uie-status={isSetupPassphraseValid ? 'valid' : 'invalid'} data-uie-name="input-applock-set-a" autoComplete="new-password" @@ -442,147 +437,98 @@ const AppLock = ({

- +
)} {state === APPLOCK_STATE.LOCKED && (
-
- {t('modalAppLockPasscode')} -
- - -

- {unlockError} -

+ - - -
- -
+
)} {state === APPLOCK_STATE.FORGOT && ( -
- {t('modalAppLockForgotMessage')} -
- - - -
- + +
)} - {state === APPLOCK_STATE.WIPE_CONFIRM && ( + {state === APPLOCK_STATE.LOGOUT && ( -
- {t('modalAppLockWipeConfirmMessage')} -
- -
- - - -
-
- )} - - {state === APPLOCK_STATE.WIPE_PASSWORD && ( -
- - -

- {wipeError} -

- -
- + {t('modalAppLockLogoutCancelButton')} + - +
-
+ )}
diff --git a/apps/webapp/src/script/page/AppLock/Applock.styles.ts b/apps/webapp/src/script/page/AppLock/Applock.styles.ts new file mode 100644 index 00000000000..b1e81c96f71 --- /dev/null +++ b/apps/webapp/src/script/page/AppLock/Applock.styles.ts @@ -0,0 +1,56 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +const buttonGroupStyle: CSSObject = { + margin: '16px 0', + display: 'flex', + justifyContent: 'space-around', +}; + +const buttonStyle: CSSObject = { + width: '160px', +}; + +const headerStyle: CSSObject = { + marginBottom: '32px', + fontSize: 'var(--font-size-large)', + textTransform: 'initial', +}; + +const linkStyle: CSSObject = { + margin: '32px 0', + fontSize: 'var(--font-size-base)', + fontWeight: 'var(--font-weight-bold)', + display: 'flex', + justifyContent: 'center', +}; + +const unlockButtonStyle: CSSObject = { + margin: '16px 0', +}; + +export const applockStyles = { + buttonGroupStyle, + buttonStyle, + headerStyle, + linkStyle, + unlockButtonStyle, +}; diff --git a/apps/webapp/src/script/repositories/user/AppLockRepository.ts b/apps/webapp/src/script/repositories/user/AppLockRepository.ts index 952b37821d1..24ab208d051 100644 --- a/apps/webapp/src/script/repositories/user/AppLockRepository.ts +++ b/apps/webapp/src/script/repositories/user/AppLockRepository.ts @@ -75,11 +75,12 @@ export class AppLockRepository { window.removeEventListener('storage', this.handlePassphraseStorageEvent); }; + disableFeature = () => { + this.appLockState.isActivatedInPreferences(false); + window.localStorage.removeItem(this.getEnabledStorageKey()); + }; + setEnabled = (enabled: boolean) => { - const disableFeature = () => { - this.appLockState.isActivatedInPreferences(false); - window.localStorage.removeItem(this.getEnabledStorageKey()); - }; if (enabled) { window.localStorage.setItem(this.getEnabledStorageKey(), 'true'); this.appLockState.isActivatedInPreferences(true); @@ -87,7 +88,7 @@ export class AppLockRepository { // If the user has set a passphrase we want to ask confirmation before disabling the feature PrimaryModal.show(PrimaryModal.type.CONFIRM, { primaryAction: { - action: disableFeature, + action: this.disableFeature, text: t('AppLockDisableTurnOff'), }, secondaryAction: { @@ -99,7 +100,7 @@ export class AppLockRepository { }, }); } else { - disableFeature(); + this.disableFeature(); } }; diff --git a/apps/webapp/src/types/i18n.d.ts b/apps/webapp/src/types/i18n.d.ts index 25a858c12dd..43054609a3c 100644 --- a/apps/webapp/src/types/i18n.d.ts +++ b/apps/webapp/src/types/i18n.d.ts @@ -1253,7 +1253,7 @@ declare module 'I18n/en-US.json' { 'modalAccountLeaveGuestRoomAction': `Leave`; 'modalAccountLeaveGuestRoomHeadline': `Leave the guest room?`; 'modalAccountLeaveGuestRoomMessage': `Conversation history will be deleted. To keep it, create an account next time.`; - 'modalAccountLogoutAction': `Log out`; + 'modalAccountLogoutAction': `Log Out`; 'modalAccountLogoutHeadline': `Clear Data?`; 'modalAccountLogoutOption': `Delete all your personal information and conversations on this device.`; 'modalAccountNewDevicesFrom': `From:`; @@ -1270,16 +1270,18 @@ declare module 'I18n/en-US.json' { 'modalAccountRemoveDevicePlaceholder': `Password`; 'modalAcknowledgeAction': `Ok`; 'modalAcknowledgeHeadline': `Something went wrong`; - 'modalAppLockForgotGoBackButton': `Go back`; - 'modalAppLockForgotMessage': `The data stored on this device can only be accessed with your app lock passcode. If you have forgotten your passcode, you can reset this client.`; - 'modalAppLockForgotTitle': `Forgot your app lock passcode?`; - 'modalAppLockForgotWipeCTA': `Reset this client`; + 'modalAppLockForgotGoBackButton': `Back`; + 'modalAppLockForgotMessage': `The data stored on this device can only be accessed with your app lock passcode.`; + 'modalAppLockForgotSecondMessage': `If you have forgotten your passcode, you can log out of this account and set a new passcode the next time you log in.`; + 'modalAppLockForgotTitle': `Forgot passcode?`; 'modalAppLockLockedError': `Wrong passcode`; - 'modalAppLockLockedForgotCTA': `Access as new device`; + 'modalAppLockLockedForgotCTA': `Forgot passcode?`; + 'modalAppLockLogoutCancelButton': `Cancel`; + 'modalAppLockInputPlaceholder': `Enter passcode`; 'modalAppLockLockedTitle': `Enter passcode to unlock {brandName}`; 'modalAppLockLockedUnlockButton': `Unlock`; 'modalAppLockPasscode': `Passcode`; - 'modalAppLockSetupAcceptButton': `Set passcode`; + 'modalAppLockSetupAcceptButton': `Create Passcode`; 'modalAppLockSetupChangeMessage': `Your organization needs to lock your app when {brandName} is not in use to keep the team safe.[br]Create a passlock to unlock {brandName}. Please, remember it, as it can not be recovered.`; 'modalAppLockSetupChangeTitle': `There was a change at {brandName}`; 'modalAppLockSetupCloseBtn': `Close window, Set app lock passcode?`; @@ -1289,17 +1291,8 @@ declare module 'I18n/en-US.json' { 'modalAppLockSetupMessage': `The app will lock itself after a certain time of inactivity.[br]To unlock the app you need to enter this passcode.[br]Make sure to remember this passcode as there is no way to recover it.`; 'modalAppLockSetupSecondPlaceholder': `Repeat passcode`; 'modalAppLockSetupSpecial': `A special character`; - 'modalAppLockSetupTitle': `Set app lock passcode`; + 'modalAppLockSetupTitle': `Create passcode`; 'modalAppLockSetupUppercase': `An uppercase letter`; - 'modalAppLockWipeConfirmConfirmButton': `Reset this client`; - 'modalAppLockWipeConfirmGoBackButton': `Go back`; - 'modalAppLockWipeConfirmMessage': `All your conversation history will be permanently deleted from this client. You may then log in again.`; - 'modalAppLockWipeConfirmTitle': `Do you really want to reset this client?`; - 'modalAppLockWipePasswordConfirmButton': `Reset this client`; - 'modalAppLockWipePasswordError': `Wrong password`; - 'modalAppLockWipePasswordGoBackButton': `Go back`; - 'modalAppLockWipePasswordPlaceholder': `Password`; - 'modalAppLockWipePasswordTitle': `Enter your {brandName} account password to reset this client`; 'modalAssetFileTypeRestrictionHeadline': `Restricted filetype`; 'modalAssetFileTypeRestrictionMessage': `The filetype of "{fileName}" is not allowed.`; 'modalAssetParallelUploadsHeadline': `Too many files at once`; @@ -1798,6 +1791,7 @@ declare module 'I18n/en-US.json' { 'searchCreateGroup': `Create group`; 'searchCreateGuestRoom': `Create guest room`; 'searchDirectConversations': `Search 1:1 conversations`; + 'searchDraftsConversations': `Search in drafts`; 'searchFavoriteConversations': `Search favorites`; 'searchFederatedDomainNotAvailable': `The federated domain is currently not available.`; 'searchFederatedDomainNotAvailableLearnMore': `Learn more`; @@ -1816,6 +1810,7 @@ declare module 'I18n/en-US.json' { 'searchManageServices': `Manage Apps`; 'searchManageServicesNoResults': `Manage apps`; 'searchMemberInvite': `Invite people to join the team`; + 'searchMentionsConversations': `Search in mentions`; 'searchNoContactsOnWire': `You have no contacts on {brandName}.\nTry finding people by\nname or username.`; 'searchNoMatchesPartner': `No results`; 'searchNoServicesManager': `Apps are helpers that can improve your workflow.`; @@ -1826,6 +1821,8 @@ declare module 'I18n/en-US.json' { 'searchPeople': `People`; 'searchPeopleOnlyPlaceholder': `Search people`; 'searchPeoplePlaceholder': `Search for people and conversations`; + 'searchPingsConversations': `Search in pings`; + 'searchRepliesConversations': `Search in replies`; 'searchServiceConfirmButton': `Open Conversation`; 'searchServicePlaceholder': `Search by name`; 'searchServices': `Apps`; @@ -1835,6 +1832,7 @@ declare module 'I18n/en-US.json' { 'searchTrySearch': `Find people by\nname or username`; 'searchTrySearchFederation': `Find people in Wire by name or\n@username\n\nFind people from another domain\nby @username@domainname`; 'searchTrySearchLearnMore': `Learn more`; + 'searchUnreadConversations': `Search in unread`; 'selectAccountTypeHeading': `How will you use Wire?`; 'selectPersonalAccountTypeOptionButtonText': `Create Personal Account`; 'selectPersonalAccountTypeOptionDescription': `Chat with friends and family.`; @@ -1885,6 +1883,8 @@ declare module 'I18n/en-US.json' { 'success.openWebAppText': `Open Wire for web`; 'success.subheader': `What do you want to do next?`; 'systemMessageLearnMore': `Learn more`; + 'tabsFilterHeader': `Show filters`; + 'tabsFilterTooltip': `Customize visible tabs`; 'takeoverButtonChoose': `Choose your own`; 'takeoverButtonKeep': `Keep this one`; 'takeoverLink': `Learn more`; diff --git a/apps/webapp/test/e2e_tests/specs/CriticalFlow/accountManagement-TC-8639.spec.ts b/apps/webapp/test/e2e_tests/specs/CriticalFlow/accountManagement-TC-8639.spec.ts index 3643d4cac09..dfd7ed17ff7 100644 --- a/apps/webapp/test/e2e_tests/specs/CriticalFlow/accountManagement-TC-8639.spec.ts +++ b/apps/webapp/test/e2e_tests/specs/CriticalFlow/accountManagement-TC-8639.spec.ts @@ -47,7 +47,6 @@ test('Account Management', {tag: ['@TC-8639', '@crit-flow-web']}, async ({create await test.step('Member verifies if applock is working', async () => { await pageManager.refreshPage(); await expect(modals.appLock().appLockModalHeader).toContainText('Enter passcode to unlock'); - await expect(modals.appLock().appLockModalText).toContainText('Passcode'); await modals.appLock().unlockAppWithPasscode(appLockPassphrase); });