refactor: simplify account standing and verification levels (#3130)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-02 18:17:41 +02:00
1 parent 1abde06824
commit 1ec8f31253
488 files changed
+10390 -47117

No files matched your search

@@ -72,6 +72,7 @@ export function resolveDmActionErrorContent(
): {title: string; message: string} {
switch (code) {
case APIErrorCodes.NEW_CONVERSATIONS_LIMITED:
case APIErrorCodes.ACCOUNT_LIMITED:
return {
title: i18nGlobal._(GENERIC_TITLE_DESCRIPTOR),
message: apiMessage || i18nGlobal._(GENERIC_ERROR_BODY_DESCRIPTOR),
@@ -2,7 +2,6 @@
import styles from '@app/features/app/components/layout/AppLayout.module.css';
import {useAppLayoutState} from '@app/features/app/components/layout/app_layout/AppLayoutHooks';
import RequiredActionGate from '@app/features/app/components/layout/RequiredActionGate';
import Initialization from '@app/features/app/state/Initialization';
import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands';
import AccountManager from '@app/features/auth/state/AccountManager';
@@ -40,7 +39,6 @@ export const AppLayout = observer(({children}: {children: React.ReactNode}) => {
<MediaDeviceStartupPreloadManager data-flx="app.app-layout.media-device-startup-preload-manager" />
)}
{isAuthenticated && <NewDeviceMonitoringManager data-flx="app.app-layout.new-device-monitoring-manager" />}
{isAuthenticated && <RequiredActionGate data-flx="app.app-layout.required-action-gate" />}
<div
className={clsx(styles.appLayout, appState.isStandalone && styles.appLayoutStandalone)}
data-flx="app.app-layout.app-layout"
@@ -1,42 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {resolveRequiredActionFlow} from '@app/features/auth/components/modals/RequiredActionFlow';
import RequiredActionModal from '@app/features/auth/components/modals/RequiredActionModal';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import LayerManager from '@app/features/ui/state/LayerManager';
import Modal from '@app/features/ui/state/Modal';
import Users from '@app/features/user/state/Users';
import {observer} from 'mobx-react-lite';
import {useEffect} from 'react';
export const REQUIRED_ACTION_MODAL_KEY = 'required-actions';
const RequiredActionGate = observer(() => {
const requiredActionKey = resolveRequiredActionFlow(Users.currentUser?.requiredActions)?.key ?? null;
const isModalOpen = Modal.hasModal(REQUIRED_ACTION_MODAL_KEY);
useEffect(() => {
if (!requiredActionKey) {
if (isModalOpen) {
ModalCommands.popWithKey(REQUIRED_ACTION_MODAL_KEY);
}
return;
}
if (!isModalOpen) {
LayerManager.closeAll();
}
ModalCommands.pushWithKey(
ModalCommands.modal(() => (
<RequiredActionModal mock={false} data-flx="app.required-action-gate.required-action-modal" />
)),
REQUIRED_ACTION_MODAL_KEY,
);
}, [isModalOpen, requiredActionKey]);
useEffect(
() => () => {
ModalCommands.popWithKey(REQUIRED_ACTION_MODAL_KEY);
},
[],
);
return null;
});
export default RequiredActionGate;
@@ -7,6 +7,7 @@ import {NAGBAR_TONES, NagbarToneKind} from '@app/features/app/components/layout/
import {OPEN_SETTINGS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {BouncedEmailChangeModal} from '@app/features/user/components/modals/BouncedEmailChangeModal';
import {UserSettingsModal} from '@app/features/user/components/modals/UserSettingsModal';
import Users from '@app/features/user/state/Users';
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
@@ -19,12 +20,29 @@ const EMAIL_VERIFICATION_NAGBAR_MESSAGE_DESCRIPTOR = msg({
comment:
'Nagbar warning shown when the current user still needs to verify email. displayName is the user display name.',
});
const EMAIL_BOUNCED_NAGBAR_MESSAGE_DESCRIPTOR = msg({
message: "Hey {displayName}, we can't deliver email to your address. Add a working email to verify your account.",
comment:
'Nagbar warning shown when emails to the current user address bounce, so the address cannot be verified. displayName is the user display name.',
});
const ADD_A_WORKING_EMAIL_BUTTON_DESCRIPTOR = msg({
message: 'Add a working email',
comment: 'Nagbar button that opens the flow to replace an email address that bounced. Keep it concise.',
});
export const EmailVerificationNagbar = observer(({isMobile}: {isMobile: boolean}) => {
const {i18n} = useLingui();
const user = Users.currentUser;
if (!user) {
return null;
}
const isEmailBounced = user.emailBounced === true;
const openBouncedEmailChange = () => {
ModalCommands.push(
modal(() => (
<BouncedEmailChangeModal data-flx="app.app-layout.nagbars.email-verification-nagbar.open-bounced-email-change.bounced-email-change-modal" />
)),
);
};
const openUserSettings = () => {
ModalCommands.push(
modal(() => (
@@ -44,17 +62,28 @@ export const EmailVerificationNagbar = observer(({isMobile}: {isMobile: boolean}
>
<NagbarContent
isMobile={isMobile}
message={i18n._(EMAIL_VERIFICATION_NAGBAR_MESSAGE_DESCRIPTOR, {
displayName: NicknameUtils.getDisplayName(user),
})}
message={i18n._(
isEmailBounced ? EMAIL_BOUNCED_NAGBAR_MESSAGE_DESCRIPTOR : EMAIL_VERIFICATION_NAGBAR_MESSAGE_DESCRIPTOR,
{displayName: NicknameUtils.getDisplayName(user)},
)}
actions={
<NagbarButton
isMobile={isMobile}
onClick={openUserSettings}
data-flx="app.app-layout.nagbars.email-verification-nagbar.nagbar-button.open-user-settings"
>
{i18n._(OPEN_SETTINGS_DESCRIPTOR)}
</NagbarButton>
isEmailBounced ? (
<NagbarButton
isMobile={isMobile}
onClick={openBouncedEmailChange}
data-flx="app.app-layout.nagbars.email-verification-nagbar.nagbar-button.open-bounced-email-change"
>
{i18n._(ADD_A_WORKING_EMAIL_BUTTON_DESCRIPTOR)}
</NagbarButton>
) : (
<NagbarButton
isMobile={isMobile}
onClick={openUserSettings}
data-flx="app.app-layout.nagbars.email-verification-nagbar.nagbar-button.open-user-settings"
>
{i18n._(OPEN_SETTINGS_DESCRIPTOR)}
</NagbarButton>
)
}
data-flx="app.app-layout.nagbars.email-verification-nagbar.nagbar-content"
/>
@@ -188,17 +188,12 @@ export const Endpoints = {
USER_MFA_WEBAUTHN_MIGRATION_REGISTRATION_OPTIONS: '/users/@me/mfa/webauthn/migration/registration-options',
USER_PASSKEY_BRIDGE: '/users/@me/passkey-bridge',
USER_PASSKEY_BRIDGE_REDEEM: (ceremonyId: string) => `/users/@me/passkey-bridge/${ceremonyId}/redeem`,
USER_PHONE_SEND_VERIFICATION: '/users/@me/phone/send-verification',
USER_PHONE_INBOUND_CHALLENGE: '/users/@me/phone/inbound-challenge',
USER_PHONE_VERIFY: '/users/@me/phone/verify',
USER_REQUIRED_ACTION_PHONE_GATE_ESCAPE: '/users/@me/required-actions/phone-gate-escape',
USER_EMAIL_CHANGE_START: '/users/@me/email-change/start',
USER_EMAIL_CHANGE_RESEND_ORIGINAL: '/users/@me/email-change/resend-original',
USER_EMAIL_CHANGE_VERIFY_ORIGINAL: '/users/@me/email-change/verify-original',
USER_EMAIL_CHANGE_REQUEST_NEW: '/users/@me/email-change/request-new',
USER_EMAIL_CHANGE_RESEND_NEW: '/users/@me/email-change/resend-new',
USER_EMAIL_CHANGE_VERIFY_NEW: '/users/@me/email-change/verify-new',
USER_EMAIL_CHANGE_APPLY: '/users/@me/email-change/apply',
USER_EMAIL_CHANGE_BOUNCED_REQUEST_NEW: '/users/@me/email-change/bounced/request-new',
USER_EMAIL_CHANGE_BOUNCED_RESEND_NEW: '/users/@me/email-change/bounced/resend-new',
USER_EMAIL_CHANGE_BOUNCED_VERIFY_NEW: '/users/@me/email-change/bounced/verify-new',
@@ -100,7 +100,7 @@ const DEFAULT_INSTANCE_FEATURES: InstanceFeatures = {
self_hosted: false,
presigned_attachment_uploads: false,
emails_enabled: false,
phone_verification_enabled: true,
phone_verification_enabled: false,
};
export const DEFAULT_INSTANCE_REGISTRATION: InstanceRegistration = {
@@ -517,10 +517,6 @@ class RuntimeConfig {
return this.features.emails_enabled;
}
get phoneVerificationEnabled(): boolean {
return this.features.phone_verification_enabled;
}
get productName(): string {
return this.appPublic.branding.product_name.trim() || DEFAULT_APP_PUBLIC_CONFIG.branding.product_name;
}