diff --git a/app/views/ProfileView/index.test.tsx b/app/views/ProfileView/index.test.tsx index 0fe9bc0cb4..b1b456993c 100644 --- a/app/views/ProfileView/index.test.tsx +++ b/app/views/ProfileView/index.test.tsx @@ -6,6 +6,7 @@ import ProfileView from './index'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; import { saveUserProfile } from '../../lib/services/restApi'; import { twoFactor } from '../../lib/services/twoFactor'; +import { TwoFactorCancelledError } from '../../lib/services/twoFactorCancelled'; import handleSaveUserProfileError from '../../lib/methods/helpers/handleSaveUserProfileError'; import EventEmitter from '../../lib/methods/helpers/events'; import { setUser } from '../../actions/login'; @@ -28,6 +29,7 @@ jest.mock('../../lib/services/restApi', () => ({ })); jest.mock('../../lib/services/twoFactor', () => ({ + ...jest.requireActual('../../lib/services/twoFactor'), twoFactor: jest.fn() })); @@ -134,6 +136,28 @@ describe('ProfileView submit', () => { expect(handleSaveUserProfileError).not.toHaveBeenCalled(); }); + it('stays silent when the user cancels the 2FA challenge', async () => { + (saveUserProfile as jest.Mock).mockRejectedValue({ error: 'totp-invalid', details: { method: 'totp' } }); + (twoFactor as jest.Mock).mockRejectedValue(new TwoFactorCancelledError()); + + const { getByTestId } = renderProfile(); + changeNameAndSubmit(getByTestId); + + await waitFor(() => expect(twoFactor).toHaveBeenCalled()); + expect(handleSaveUserProfileError).not.toHaveBeenCalled(); + }); + + it('reports the 2FA error itself when the challenge fails for a reason other than cancelling', async () => { + const twoFactorError = { error: 'totp-required' }; + (saveUserProfile as jest.Mock).mockRejectedValue({ error: 'totp-invalid', details: { method: 'totp' } }); + (twoFactor as jest.Mock).mockRejectedValue(twoFactorError); + + const { getByTestId } = renderProfile(); + changeNameAndSubmit(getByTestId); + + await waitFor(() => expect(handleSaveUserProfileError).toHaveBeenCalledWith(twoFactorError, 'saving_profile')); + }); + it('handles the save error after a cancelled/non-2FA failure', async () => { const error = { error: 'some-other-error' }; (saveUserProfile as jest.Mock).mockRejectedValue(error); diff --git a/app/views/ProfileView/index.tsx b/app/views/ProfileView/index.tsx index 87a4e2d5e2..7dff44bbbd 100644 --- a/app/views/ProfileView/index.tsx +++ b/app/views/ProfileView/index.tsx @@ -50,6 +50,8 @@ const MAX_NICKNAME_LENGTH = 120; interface IProfileViewProps { navigation: NativeStackNavigationProp; } +type TwoFactorChallengeOutcome = { status: 'retried' } | { status: 'cancelled' } | { status: 'failed'; error: unknown }; + const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { const validationSchema = yup.object().shape({ name: yup.string().required(I18n.t('Name_required')), @@ -205,21 +207,20 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { } }; - const handleTwoFactorChallenge = async (e: any): Promise => { + const handleTwoFactorChallenge = async (e: any): Promise => { if (e?.error !== 'totp-invalid' || e?.details.method === TwoFactorMethods.PASSWORD) { - return false; + return { status: 'failed', error: e }; } try { const code = await twoFactor({ method: e.details.method, invalid: e?.error === 'totp-invalid' && !!twoFactorCode }); setTwoFactorCode(code as any); await submit(); - return true; + return { status: 'retried' }; } catch (twoFactorError) { if (isTwoFactorCancelled(twoFactorError)) { - resetSavingState(); - return true; + return { status: 'cancelled' }; } - return false; + return { status: 'failed', error: twoFactorError }; } }; @@ -251,12 +252,14 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { const { email } = getValues(); setFieldErrorsFromResponse(e, email); - const handled = await handleTwoFactorChallenge(e); - if (handled) return; + const twoFactorOutcome = await handleTwoFactorChallenge(e); + if (twoFactorOutcome.status === 'retried') return; - logEvent(events.PROFILE_SAVE_CHANGES_F); resetSavingState(); - handleSaveUserProfileError(e, 'saving_profile'); + if (twoFactorOutcome.status === 'cancelled') return; + + logEvent(events.PROFILE_SAVE_CHANGES_F); + handleSaveUserProfileError(twoFactorOutcome.error, 'saving_profile'); } };