Handle 409/410 when confirming username

This commit is contained in:
Fedor Indutny 2023-02-14 09:39:47 -08:00 committed by GitHub
parent 10885e5d3f
commit 486ada8b6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 15 deletions

View file

@ -5223,6 +5223,10 @@
"message": "Your username couldnt be saved. Check your connection and try again.", "message": "Your username couldnt be saved. Check your connection and try again.",
"description": "Shown if something unknown has gone wrong with username save." "description": "Shown if something unknown has gone wrong with username save."
}, },
"icu:ProfileEditor--username--reservation-gone": {
"messageformat": "{username} is no longer available. A new set of digits will be paired with your username, please try saving it again.",
"description": "Shown if username reservation has expired and new one needs to be generated."
},
"ProfileEditor--username--delete-general-error": { "ProfileEditor--username--delete-general-error": {
"message": "Your username couldnt be removed. Check your connection and try again.", "message": "Your username couldnt be removed. Check your connection and try again.",
"description": "Shown if something unknown has gone wrong with username delete." "description": "Shown if something unknown has gone wrong with username delete."

View file

@ -120,7 +120,10 @@ export function EditUsernameModalBody({
return i18n('ProfileEditor--username--unavailable'); return i18n('ProfileEditor--username--unavailable');
} }
// Displayed through confirmation modal below // Displayed through confirmation modal below
if (error === UsernameReservationError.General) { if (
error === UsernameReservationError.General ||
error === UsernameReservationError.ConflictOrGone
) {
return; return;
} }
throw missingCaseError(error); throw missingCaseError(error);
@ -264,6 +267,24 @@ export function EditUsernameModalBody({
{i18n('ProfileEditor--username--general-error')} {i18n('ProfileEditor--username--general-error')}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
{error === UsernameReservationError.ConflictOrGone && (
<ConfirmationDialog
dialogName="EditUsernameModalBody.conflictOrGone"
cancelText={i18n('ok')}
cancelButtonVariant={ButtonVariant.Secondary}
i18n={i18n}
onClose={() => {
if (nickname) {
reserveUsername(nickname);
}
}}
>
{i18n('icu:ProfileEditor--username--reservation-gone', {
username: currentUsername,
})}
</ConfirmationDialog>
)}
</> </>
); );
} }

View file

@ -8,7 +8,7 @@ import { strictAssert } from '../util/assert';
import { sleep } from '../util/sleep'; import { sleep } from '../util/sleep';
import { getMinNickname, getMaxNickname } from '../util/Username'; import { getMinNickname, getMaxNickname } from '../util/Username';
import type { UsernameReservationType } from '../types/Username'; import type { UsernameReservationType } from '../types/Username';
import { ReserveUsernameError } from '../types/Username'; import { ReserveUsernameError, ConfirmUsernameResult } from '../types/Username';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import * as log from '../logging/log'; import * as log from '../logging/log';
import MessageSender from '../textsecure/SendMessage'; import MessageSender from '../textsecure/SendMessage';
@ -129,7 +129,7 @@ async function updateUsernameAndSyncProfile(
export async function confirmUsername( export async function confirmUsername(
reservation: UsernameReservationType, reservation: UsernameReservationType,
abortSignal?: AbortSignal abortSignal?: AbortSignal
): Promise<void> { ): Promise<ConfirmUsernameResult> {
const { server } = window.textsecure; const { server } = window.textsecure;
if (!server) { if (!server) {
throw new Error('server interface is not available!'); throw new Error('server interface is not available!');
@ -162,9 +162,15 @@ export async function confirmUsername(
return confirmUsername(reservation, abortSignal); return confirmUsername(reservation, abortSignal);
} }
if (error.code === 409 || error.code === 410) {
return ConfirmUsernameResult.ConflictOrGone;
}
} }
throw error; throw error;
} }
return ConfirmUsernameResult.Ok;
} }
export async function deleteUsername( export async function deleteUsername(

View file

@ -5,7 +5,10 @@ import type { ThunkAction } from 'redux-thunk';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import type { UsernameReservationType } from '../../types/Username'; import type { UsernameReservationType } from '../../types/Username';
import { ReserveUsernameError } from '../../types/Username'; import {
ReserveUsernameError,
ConfirmUsernameResult,
} from '../../types/Username';
import * as usernameServices from '../../services/username'; import * as usernameServices from '../../services/username';
import type { ReserveUsernameResultType } from '../../services/username'; import type { ReserveUsernameResultType } from '../../services/username';
import { import {
@ -83,7 +86,7 @@ type ReserveUsernameActionType = ReadonlyDeep<
> >
>; >;
type ConfirmUsernameActionType = ReadonlyDeep< type ConfirmUsernameActionType = ReadonlyDeep<
PromiseAction<typeof CONFIRM_USERNAME, void> PromiseAction<typeof CONFIRM_USERNAME, ConfirmUsernameResult>
>; >;
type DeleteUsernameActionType = ReadonlyDeep< type DeleteUsernameActionType = ReadonlyDeep<
PromiseAction<typeof DELETE_USERNAME, void> PromiseAction<typeof DELETE_USERNAME, void>
@ -425,12 +428,25 @@ export function reducer(
'Must be reserving before resolving confirmation' 'Must be reserving before resolving confirmation'
); );
return { const { payload } = action;
...state, if (payload === ConfirmUsernameResult.Ok) {
usernameReservation: { return {
state: UsernameReservationState.Closed, ...state,
}, usernameReservation: {
}; state: UsernameReservationState.Closed,
},
};
}
if (payload === ConfirmUsernameResult.ConflictOrGone) {
return {
...state,
usernameReservation: {
state: UsernameReservationState.Open,
error: UsernameReservationError.ConflictOrGone,
},
};
}
throw missingCaseError(payload);
} }
if (action.type === 'username/CONFIRM_USERNAME_REJECTED') { if (action.type === 'username/CONFIRM_USERNAME_REJECTED') {

View file

@ -29,4 +29,5 @@ export enum UsernameReservationError {
CheckCharacters = 'CheckCharacters', CheckCharacters = 'CheckCharacters',
UsernameNotAvailable = 'UsernameNotAvailable', UsernameNotAvailable = 'UsernameNotAvailable',
General = 'General', General = 'General',
ConflictOrGone = 'ConflictOrGone',
} }

View file

@ -20,7 +20,10 @@ import { actions } from '../../../state/ducks/username';
import { ToastType } from '../../../types/Toast'; import { ToastType } from '../../../types/Toast';
import { noopAction } from '../../../state/ducks/noop'; import { noopAction } from '../../../state/ducks/noop';
import { reducer } from '../../../state/reducer'; import { reducer } from '../../../state/reducer';
import { ReserveUsernameError } from '../../../types/Username'; import {
ReserveUsernameError,
ConfirmUsernameResult,
} from '../../../types/Username';
const DEFAULT_RESERVATION = { const DEFAULT_RESERVATION = {
username: 'abc.12', username: 'abc.12',
@ -312,7 +315,7 @@ describe('electron/state/ducks/username', () => {
describe('confirmUsername', () => { describe('confirmUsername', () => {
it('should dispatch promise when reservation is present', () => { it('should dispatch promise when reservation is present', () => {
const doConfirmUsername = sinon.stub().resolves(); const doConfirmUsername = sinon.stub().resolves(ConfirmUsernameResult.Ok);
const dispatch = sinon.spy(); const dispatch = sinon.spy();
actions.confirmUsername({ actions.confirmUsername({
@ -344,7 +347,7 @@ describe('electron/state/ducks/username', () => {
state = reducer(state, { state = reducer(state, {
type: 'username/CONFIRM_USERNAME_FULFILLED', type: 'username/CONFIRM_USERNAME_FULFILLED',
payload: undefined, payload: ConfirmUsernameResult.Ok,
meta: undefined, meta: undefined,
}); });
@ -389,6 +392,39 @@ describe('electron/state/ducks/username', () => {
UsernameReservationError.General UsernameReservationError.General
); );
}); });
it('should not close modal on "conflict or gone"', () => {
let state = stateWithReservation;
state = reducer(state, {
type: 'username/CONFIRM_USERNAME_PENDING',
meta: undefined,
});
assert.strictEqual(
getUsernameReservationState(state),
UsernameReservationState.Confirming
);
assert.strictEqual(
getUsernameReservationObject(state),
DEFAULT_RESERVATION
);
state = reducer(state, {
type: 'username/CONFIRM_USERNAME_FULFILLED',
payload: ConfirmUsernameResult.ConflictOrGone,
meta: undefined,
});
assert.strictEqual(
getUsernameReservationState(state),
UsernameReservationState.Open
);
assert.strictEqual(getUsernameReservationObject(state), undefined);
assert.strictEqual(
getUsernameReservationError(state),
UsernameReservationError.ConflictOrGone
);
});
}); });
describe('deleteUsername', () => { describe('deleteUsername', () => {

View file

@ -25,7 +25,9 @@ describe('pnp/send gv2 invite', function needsName() {
let pniContact: PrimaryDevice; let pniContact: PrimaryDevice;
beforeEach(async () => { beforeEach(async () => {
bootstrap = new Bootstrap(); bootstrap = new Bootstrap({
contactCount: 0,
});
await bootstrap.init(); await bootstrap.init();
const { phone, server } = bootstrap; const { phone, server } = bootstrap;

View file

@ -12,6 +12,11 @@ export enum ReserveUsernameError {
Conflict = 'Conflict', Conflict = 'Conflict',
} }
export enum ConfirmUsernameResult {
Ok = 'Ok',
ConflictOrGone = 'ConflictOrGone',
}
export function getUsernameFromSearch(searchTerm: string): string | undefined { export function getUsernameFromSearch(searchTerm: string): string | undefined {
// Search term contains username if it: // Search term contains username if it:
// - Is a valid username with or without a discriminator // - Is a valid username with or without a discriminator