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.",
"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": {
"message": "Your username couldnt be removed. Check your connection and try again.",
"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');
}
// Displayed through confirmation modal below
if (error === UsernameReservationError.General) {
if (
error === UsernameReservationError.General ||
error === UsernameReservationError.ConflictOrGone
) {
return;
}
throw missingCaseError(error);
@ -264,6 +267,24 @@ export function EditUsernameModalBody({
{i18n('ProfileEditor--username--general-error')}
</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 { getMinNickname, getMaxNickname } from '../util/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 log from '../logging/log';
import MessageSender from '../textsecure/SendMessage';
@ -129,7 +129,7 @@ async function updateUsernameAndSyncProfile(
export async function confirmUsername(
reservation: UsernameReservationType,
abortSignal?: AbortSignal
): Promise<void> {
): Promise<ConfirmUsernameResult> {
const { server } = window.textsecure;
if (!server) {
throw new Error('server interface is not available!');
@ -162,9 +162,15 @@ export async function confirmUsername(
return confirmUsername(reservation, abortSignal);
}
if (error.code === 409 || error.code === 410) {
return ConfirmUsernameResult.ConflictOrGone;
}
}
throw error;
}
return ConfirmUsernameResult.Ok;
}
export async function deleteUsername(

View file

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

View file

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

View file

@ -20,7 +20,10 @@ import { actions } from '../../../state/ducks/username';
import { ToastType } from '../../../types/Toast';
import { noopAction } from '../../../state/ducks/noop';
import { reducer } from '../../../state/reducer';
import { ReserveUsernameError } from '../../../types/Username';
import {
ReserveUsernameError,
ConfirmUsernameResult,
} from '../../../types/Username';
const DEFAULT_RESERVATION = {
username: 'abc.12',
@ -312,7 +315,7 @@ describe('electron/state/ducks/username', () => {
describe('confirmUsername', () => {
it('should dispatch promise when reservation is present', () => {
const doConfirmUsername = sinon.stub().resolves();
const doConfirmUsername = sinon.stub().resolves(ConfirmUsernameResult.Ok);
const dispatch = sinon.spy();
actions.confirmUsername({
@ -344,7 +347,7 @@ describe('electron/state/ducks/username', () => {
state = reducer(state, {
type: 'username/CONFIRM_USERNAME_FULFILLED',
payload: undefined,
payload: ConfirmUsernameResult.Ok,
meta: undefined,
});
@ -389,6 +392,39 @@ describe('electron/state/ducks/username', () => {
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', () => {

View file

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

View file

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