feat(auth): add username sign-in mode and recovery kits (#3215)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-05 14:10:07 +02:00
1 parent bf3d73a5f7
commit e1eecc3b6c
355 files changed
+52138 -24097

No files matched your search

@@ -1,7 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {StatusSlate} from '@app/features/app/components/dialogs/shared/StatusSlate';
import {EXAMPLE_FLUXER_TAG_FULL} from '@app/features/app/config/I18nDisplayConstants';
import {EXAMPLE_FLUXER_TAG_FULL, EXAMPLE_USERNAME} from '@app/features/app/config/I18nDisplayConstants';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
import styles from '@app/features/channel/components/direct_message/AddFriendForm.module.css';
import {CLAIM_ACCOUNT_DESCRIPTOR, VERIFY_EMAIL_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
@@ -34,6 +35,10 @@ const PLEASE_ENTER_A_VALID_USERNAME_DESCRIPTOR = msg({
comment:
'Description text in the channel and chat add friend form. Preserve {exampleFluxerTagFull}; it is inserted by code.',
});
const ENTER_A_VALID_USERNAME_DESCRIPTOR = msg({
message: 'Enter a valid username.',
comment: 'Error text in the add friend form on instances where people sign in with a username and have no tag.',
});
const SEND_REQUEST_DESCRIPTOR = msg({
message: 'Send request',
comment: 'Button or menu action label in the channel and chat add friend form. Keep it concise.',
@@ -55,6 +60,7 @@ export const AddFriendForm: React.FC<AddFriendFormProps> = observer(({onSuccess}
const [errorCode, setErrorCode] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const currentUser = Users.currentUser;
const usernameOnly = RuntimeConfig.usesUniqueUsernames;
const isClaimed = currentUser?.isClaimed() ?? true;
if (!isClaimed) {
return (
@@ -101,7 +107,8 @@ export const AddFriendForm: React.FC<AddFriendFormProps> = observer(({onSuccess}
const parseInput = (input: string): [string, string] => {
const parts = input['split']('#');
if (parts.length > 1) {
return [parts[0], parts.slice(1).join('#')];
const discriminator = parts.slice(1).join('#');
return [parts[0], usernameOnly && /^0{1,4}$/.test(discriminator) ? '0000' : discriminator];
}
return [input, '0000'];
};
@@ -121,6 +128,7 @@ export const AddFriendForm: React.FC<AddFriendFormProps> = observer(({onSuccess}
return i18n._(NO_USER_FOUND_WITH_THAT_USERNAME_DESCRIPTOR);
}
if (errorCode === APIErrorCodes.DISCRIMINATOR_REQUIRED) {
if (usernameOnly) return i18n._(ENTER_A_VALID_USERNAME_DESCRIPTOR);
return i18n._(PLEASE_ENTER_A_VALID_USERNAME_DESCRIPTOR, {exampleFluxerTagFull: EXAMPLE_FLUXER_TAG_FULL});
}
if (errorCode === APIErrorCodes.NEW_CONVERSATIONS_LIMITED) {
@@ -176,7 +184,7 @@ export const AddFriendForm: React.FC<AddFriendFormProps> = observer(({onSuccess}
type="text"
value={input}
onChange={handleInputChange}
placeholder={EXAMPLE_FLUXER_TAG_FULL}
placeholder={usernameOnly ? EXAMPLE_USERNAME : EXAMPLE_FLUXER_TAG_FULL}
className={clsx(
styles.input,
!isMobile && styles.inputDesktop,
@@ -14,6 +14,7 @@ import {http} from '@app/features/platform/transport/RestTransport';
import {Logger} from '@app/features/platform/utils/AppLogger';
import Users from '@app/features/user/state/Users';
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
import {formatUserTag} from '@app/features/user/utils/UserTagUtils';
import {extractTimestampFromSnowflakeAsDate} from '@fluxer/snowflake/src/SnowflakeUtils';
export const logger = new Logger('GuildMembersPage');
@@ -30,7 +31,13 @@ export function toMemberDisplayData(searchMember: SearchableGuildMember, guildId
const member = GuildMembers.getMember(guildId, searchMember.user_id);
const resolvedName = member?.nick || searchMember.nickname || searchMember.global_name || searchMember.username;
const displayName = NicknameUtils.formatNicknameForStreamerMode(resolvedName);
const tag = user ? user.tag : `${searchMember.username}#${searchMember.discriminator}`;
const tag = user
? user.tag
: formatUserTag({
username: searchMember.username,
discriminator: searchMember.discriminator,
bot: searchMember.is_bot,
});
return {
userId: searchMember.user_id,
displayName,