mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import i18n from '@app/app/I18n';
|
|
import {showGenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModalCommands';
|
|
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
|
|
import {Endpoints} from '@app/features/app/constants/Endpoints';
|
|
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
|
import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands';
|
|
import {getAccountAvatarUrl, getAccountDisplayName} from '@app/features/auth/components/accounts/AccountListItem';
|
|
import {showBrowserLoginHandoffModal} from '@app/features/auth/flow/BrowserLoginHandoffModal';
|
|
import AccountManager from '@app/features/auth/state/AccountManager';
|
|
import type {LoginSuccessPayload} from '@app/features/auth/state/AuthFlow';
|
|
import {SOMETHING_WENT_WRONG_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
|
import {type Account, SessionExpiredError} from '@app/features/platform/state/AuthSession';
|
|
import {http} from '@app/features/platform/transport/RestTransport';
|
|
import {Logger} from '@app/features/platform/utils/AppLogger';
|
|
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
|
|
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
|
|
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
|
|
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
|
import {modal} from '@app/features/ui/commands/ModalCommands';
|
|
import {formatUserTag} from '@app/features/user/utils/UserTagUtils';
|
|
import type {MessageDescriptor} from '@lingui/core';
|
|
import {msg} from '@lingui/core/macro';
|
|
import {Trans} from '@lingui/react/macro';
|
|
import {SignOutIcon} from '@phosphor-icons/react';
|
|
import type React from 'react';
|
|
|
|
const WE_COULDN_T_SWITCH_ACCOUNTS_PLEASE_TRY_AGAIN_DESCRIPTOR = msg({
|
|
message: "Couldn't switch accounts. Try again.",
|
|
comment: 'Toast error shown when switching to a different saved account fails.',
|
|
});
|
|
const SIGNING_OUT_FAILED_TRY_AGAIN_IN_A_MOMENT_DESCRIPTOR = msg({
|
|
message: 'Signing out failed. Try again in a moment.',
|
|
comment: 'Toast error shown when signing out from the account switcher fails.',
|
|
});
|
|
const WE_COULDN_T_REMOVE_THIS_ACCOUNT_PLEASE_TRY_DESCRIPTOR = msg({
|
|
message: "Couldn't remove this account. Try again.",
|
|
comment: 'Toast error shown when removing the current account from the device fails.',
|
|
});
|
|
const logger = new Logger('AccountSwitcherModalUtils');
|
|
|
|
function showAccountSwitcherErrorModal(message: MessageDescriptor): void {
|
|
showGenericErrorModal({
|
|
title: () => i18n._(SOMETHING_WENT_WRONG_DESCRIPTOR),
|
|
message: () => i18n._(message),
|
|
dataFlx: 'auth.account-switcher-modal-utils.error-modal',
|
|
defer: true,
|
|
});
|
|
}
|
|
|
|
export interface AccountSwitcherLogic {
|
|
currentAccount: Account | null;
|
|
accounts: Array<Account>;
|
|
isBusy: boolean;
|
|
handleSwitchAccount: (userId: string) => Promise<void>;
|
|
handleLogout: () => Promise<void>;
|
|
handleLogoutStoredAccount: (account: Account) => Promise<void>;
|
|
handleAddAccount: () => void;
|
|
handleReLogin: (_userId: string) => void;
|
|
handleRemoveAccount: (userId: string) => Promise<void>;
|
|
getAvatarUrl: (account: Account) => string | undefined;
|
|
}
|
|
|
|
export interface AccountSwitcherLogicOptions {
|
|
redirectAfterSwitch?: string | null;
|
|
redirectAfterLogin?: string | null;
|
|
switchAccount?: (userId: string) => Promise<void>;
|
|
}
|
|
|
|
export function useAccountSwitcherLogic(options: AccountSwitcherLogicOptions = {}): AccountSwitcherLogic {
|
|
const {redirectAfterSwitch = undefined, redirectAfterLogin = undefined, switchAccount} = options;
|
|
const currentAccount = AccountManager.currentAccount;
|
|
const accounts = AccountManager.getAllAccounts();
|
|
const isBusy = AccountManager.isSwitching || AccountManager.isLoading;
|
|
const handleLoginSuccess = async (payload: LoginSuccessPayload): Promise<void> => {
|
|
await AuthenticationCommands.completeLogin(payload, {redirectPath: redirectAfterLogin});
|
|
ModalCommands.popAll();
|
|
};
|
|
const handleReLogin = (userId: string): void => {
|
|
const account = AccountManager.accounts.get(userId);
|
|
if (!account) {
|
|
showAccountSwitcherErrorModal(WE_COULDN_T_SWITCH_ACCOUNTS_PLEASE_TRY_AGAIN_DESCRIPTOR);
|
|
return;
|
|
}
|
|
const prefillLogin = RuntimeConfig.usesUsernameSignIn
|
|
? account.userData && formatUserTag(account.userData)
|
|
: (account.userData?.email ?? undefined);
|
|
showBrowserLoginHandoffModal(handleLoginSuccess, prefillLogin);
|
|
};
|
|
const handleSwitchAccount = async (userId: string): Promise<void> => {
|
|
if (isBusy) {
|
|
return;
|
|
}
|
|
try {
|
|
if (switchAccount) {
|
|
await switchAccount(userId);
|
|
} else {
|
|
await AccountManager.switchToAccount(userId, redirectAfterSwitch);
|
|
}
|
|
ModalCommands.pop();
|
|
} catch (error) {
|
|
if (error instanceof SessionExpiredError) {
|
|
handleReLogin(userId);
|
|
} else {
|
|
logger.error('Failed to switch account', error);
|
|
showAccountSwitcherErrorModal(WE_COULDN_T_SWITCH_ACCOUNTS_PLEASE_TRY_AGAIN_DESCRIPTOR);
|
|
}
|
|
}
|
|
};
|
|
const handleLogout = async (): Promise<void> => {
|
|
if (isBusy) {
|
|
return;
|
|
}
|
|
try {
|
|
await AccountManager.logout();
|
|
ModalCommands.pop();
|
|
} catch (error) {
|
|
logger.error('Logout failed', error);
|
|
showAccountSwitcherErrorModal(SIGNING_OUT_FAILED_TRY_AGAIN_IN_A_MOMENT_DESCRIPTOR);
|
|
}
|
|
};
|
|
const handleLogoutStoredAccount = async (account: Account): Promise<void> => {
|
|
if (isBusy) {
|
|
return;
|
|
}
|
|
try {
|
|
await http.post(Endpoints.AUTH_LOGOUT, {
|
|
headers: {Authorization: account.token},
|
|
timeoutMs: 5000,
|
|
retries: 0,
|
|
auth: 'none',
|
|
});
|
|
} catch (error) {
|
|
logger.warn('Failed to log out stored account', error);
|
|
}
|
|
await handleRemoveAccount(account.userId);
|
|
};
|
|
const handleAddAccount = (): void => {
|
|
showBrowserLoginHandoffModal(handleLoginSuccess);
|
|
};
|
|
const handleRemoveAccount = async (userId: string): Promise<void> => {
|
|
if (isBusy) {
|
|
return;
|
|
}
|
|
try {
|
|
await AccountManager.removeStoredAccount(userId);
|
|
} catch (error) {
|
|
logger.error('Failed to remove account', error);
|
|
showAccountSwitcherErrorModal(WE_COULDN_T_REMOVE_THIS_ACCOUNT_PLEASE_TRY_DESCRIPTOR);
|
|
}
|
|
};
|
|
return {
|
|
currentAccount,
|
|
accounts,
|
|
isBusy,
|
|
handleSwitchAccount,
|
|
handleLogout,
|
|
handleLogoutStoredAccount,
|
|
handleAddAccount,
|
|
handleReLogin,
|
|
handleRemoveAccount,
|
|
getAvatarUrl: getAccountAvatarUrl,
|
|
};
|
|
}
|
|
|
|
export interface OpenSignOutConfirmOptions {
|
|
account: Account;
|
|
currentAccountId: string | null;
|
|
hasMultipleAccounts: boolean;
|
|
onLogout: () => Promise<void>;
|
|
onLogoutStoredAccount: (account: Account) => Promise<void>;
|
|
}
|
|
|
|
export function openSignOutConfirm({
|
|
account,
|
|
currentAccountId,
|
|
hasMultipleAccounts,
|
|
onLogout,
|
|
onLogoutStoredAccount,
|
|
}: OpenSignOutConfirmOptions): void {
|
|
const displayName = getAccountDisplayName(account, account.userId);
|
|
const isCurrentAccount = account.userId === currentAccountId;
|
|
ModalCommands.push(
|
|
modal(() => (
|
|
<ConfirmModal
|
|
title={<Trans>Sign out of {displayName}</Trans>}
|
|
description={
|
|
isCurrentAccount ? (
|
|
hasMultipleAccounts ? (
|
|
<Trans>Signing out will bring you to the sign-in screen so you can pick another account.</Trans>
|
|
) : (
|
|
<Trans>Signing out will bring you to the sign-in screen.</Trans>
|
|
)
|
|
) : (
|
|
<Trans>Signing out will remove this account from the device.</Trans>
|
|
)
|
|
}
|
|
primaryText={<Trans>Sign out</Trans>}
|
|
primaryVariant="danger"
|
|
onPrimary={async () => {
|
|
if (isCurrentAccount) {
|
|
await onLogout();
|
|
} else {
|
|
await onLogoutStoredAccount(account);
|
|
}
|
|
}}
|
|
data-flx="auth.account-switcher-modal-utils.open-sign-out-confirm.confirm-modal"
|
|
/>
|
|
)),
|
|
);
|
|
}
|
|
|
|
export interface OpenAccountContextMenuOptions {
|
|
account: Account;
|
|
currentAccountId: string | null;
|
|
hasMultipleAccounts: boolean;
|
|
onSwitch: (userId: string) => void;
|
|
onReLogin: (userId: string) => void;
|
|
onLogout: () => Promise<void>;
|
|
onLogoutStoredAccount: (account: Account) => Promise<void>;
|
|
}
|
|
|
|
export function openAccountContextMenu(
|
|
event: React.MouseEvent<HTMLButtonElement>,
|
|
{
|
|
account,
|
|
currentAccountId,
|
|
hasMultipleAccounts,
|
|
onSwitch,
|
|
onReLogin,
|
|
onLogout,
|
|
onLogoutStoredAccount,
|
|
}: OpenAccountContextMenuOptions,
|
|
): void {
|
|
const isCurrent = account.userId === currentAccountId;
|
|
ContextMenuCommands.openFromEvent(event, (props) => (
|
|
<MenuGroup data-flx="auth.account-switcher-modal-utils.open-account-context-menu.menu-group">
|
|
{isCurrent ? (
|
|
<MenuItem
|
|
danger
|
|
icon={
|
|
<SignOutIcon
|
|
size={18}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.sign-out-icon"
|
|
/>
|
|
}
|
|
onClick={() => {
|
|
props.onClose();
|
|
openSignOutConfirm({
|
|
account,
|
|
currentAccountId,
|
|
hasMultipleAccounts,
|
|
onLogout,
|
|
onLogoutStoredAccount,
|
|
});
|
|
}}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.menu-item.close"
|
|
>
|
|
<Trans>Sign out</Trans>
|
|
</MenuItem>
|
|
) : (
|
|
<>
|
|
{account.isValid === false ? (
|
|
<MenuItem
|
|
icon={
|
|
<SignOutIcon
|
|
size={18}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.sign-out-icon--2"
|
|
/>
|
|
}
|
|
onClick={() => {
|
|
props.onClose();
|
|
onReLogin(account.userId);
|
|
}}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.menu-item.close--2"
|
|
>
|
|
<Trans>Sign in again</Trans>
|
|
</MenuItem>
|
|
) : (
|
|
<MenuItem
|
|
icon={
|
|
<SignOutIcon
|
|
size={18}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.sign-out-icon--3"
|
|
/>
|
|
}
|
|
onClick={() => {
|
|
props.onClose();
|
|
onSwitch(account.userId);
|
|
}}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.menu-item.close--3"
|
|
>
|
|
<Trans>Switch to this account</Trans>
|
|
</MenuItem>
|
|
)}
|
|
<MenuItem
|
|
danger
|
|
icon={
|
|
<SignOutIcon
|
|
size={18}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.sign-out-icon--4"
|
|
/>
|
|
}
|
|
onClick={() => {
|
|
props.onClose();
|
|
openSignOutConfirm({
|
|
account,
|
|
currentAccountId,
|
|
hasMultipleAccounts,
|
|
onLogout,
|
|
onLogoutStoredAccount,
|
|
});
|
|
}}
|
|
data-flx="auth.account-switcher-modal-utils.open-account-context-menu.menu-item.close--4"
|
|
>
|
|
<Trans>Sign out</Trans>
|
|
</MenuItem>
|
|
</>
|
|
)}
|
|
</MenuGroup>
|
|
));
|
|
}
|