Handle 409/410 when confirming username
This commit is contained in:
parent
10885e5d3f
commit
486ada8b6c
8 changed files with 106 additions and 15 deletions
|
@ -5223,6 +5223,10 @@
|
||||||
"message": "Your username couldn’t be saved. Check your connection and try again.",
|
"message": "Your username couldn’t 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 couldn’t be removed. Check your connection and try again.",
|
"message": "Your username couldn’t 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."
|
||||||
|
|
|
@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,6 +428,8 @@ export function reducer(
|
||||||
'Must be reserving before resolving confirmation'
|
'Must be reserving before resolving confirmation'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { payload } = action;
|
||||||
|
if (payload === ConfirmUsernameResult.Ok) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
usernameReservation: {
|
usernameReservation: {
|
||||||
|
@ -432,6 +437,17 @@ export function reducer(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
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') {
|
||||||
assertDev(
|
assertDev(
|
||||||
|
|
|
@ -29,4 +29,5 @@ export enum UsernameReservationError {
|
||||||
CheckCharacters = 'CheckCharacters',
|
CheckCharacters = 'CheckCharacters',
|
||||||
UsernameNotAvailable = 'UsernameNotAvailable',
|
UsernameNotAvailable = 'UsernameNotAvailable',
|
||||||
General = 'General',
|
General = 'General',
|
||||||
|
ConflictOrGone = 'ConflictOrGone',
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue