mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): refetch oauth bot destinations after account switch (#3460)
This commit is contained in:
1 parent
c0f825ae2a
commit
3c8fe74660
9 files changed
+62
-40
No files matched your search
@@ -769,7 +769,7 @@ export async function fetchDesktopHandoffInfo({
|
||||
return response.body;
|
||||
}
|
||||
|
||||
export interface CompleteLoginOptions {
|
||||
interface CompleteLoginOptions {
|
||||
redirectPath?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ const LoginPage = observer(function LoginPage({onBackActionChange}: LoginPagePro
|
||||
title={null}
|
||||
onLoginComplete={runtimeTarget.reset}
|
||||
onBackActionChange={onBackActionChange}
|
||||
completeLoginRedirectPath={null}
|
||||
completeLoginRedirectPath={Routes.ME}
|
||||
forceCredentials={false}
|
||||
startWithAddAccount={false}
|
||||
runtimeTarget={runtimeTarget}
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ export const OAuthCommunityStep: React.FC<OAuthCommunityStepProps> = ({
|
||||
{destinationsError}
|
||||
</div>
|
||||
)}
|
||||
{!destinationsLoading && destinationOptions.length === 0 && (
|
||||
{!destinationsLoading && !destinationsError && destinationOptions.length === 0 && (
|
||||
<div className={styles.emptyState} data-flx="auth.o-auth-authorize-page.community-step.empty-state">
|
||||
{guildSelectorEmptyState}
|
||||
</div>
|
||||
|
||||
+30
-5
@@ -10,6 +10,8 @@ import {
|
||||
} from '@app/features/auth/components/pages/oauth_authorize_page/OAuthAuthorizePageShared';
|
||||
import Accounts from '@app/features/auth/state/Accounts';
|
||||
import {formatList} from '@app/features/i18n/utils/IntlCache';
|
||||
import {AccountScopedWork} from '@app/features/platform/state/AccountScopedWork';
|
||||
import {isAccountTransitionAbortError} from '@app/features/platform/state/AccountTransitionAbort';
|
||||
import {http} from '@app/features/platform/transport/RestTransport';
|
||||
import {failureMessage} from '@app/features/platform/utils/ResponseInspection';
|
||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||
@@ -106,9 +108,14 @@ function getGroupDmLabel(channel: ChannelResponse, unknownGroupDmLabel: string,
|
||||
return unknownGroupDmLabel;
|
||||
}
|
||||
|
||||
export function useBotInviteDestinations(enabled: boolean, rawPermissions: string | null): BotInviteDestinationsResult {
|
||||
export function useBotInviteDestinations(
|
||||
enabled: boolean,
|
||||
rawPermissions: string | null,
|
||||
revalidate: boolean,
|
||||
): BotInviteDestinationsResult {
|
||||
const {i18n} = useLingui();
|
||||
const currentAccountKey = Accounts.currentAccountKey;
|
||||
const accountWorkAdmitted = !AccountScopedWork.isSuspended;
|
||||
const requestedPermissions = useMemo(() => parseRequestedBotPermissions(rawPermissions, false), [rawPermissions]);
|
||||
const i18nRef = useRef(i18n);
|
||||
i18nRef.current = i18n;
|
||||
@@ -116,17 +123,32 @@ export function useBotInviteDestinations(enabled: boolean, rawPermissions: strin
|
||||
const [rawGuilds, setRawGuilds] = useState<ReadonlyArray<GuildSummary>>([]);
|
||||
const [rawChannels, setRawChannels] = useState<ReadonlyArray<ChannelResponse>>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [revision, setRevision] = useState(0);
|
||||
const loadedAccountKeyRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!revalidate) return;
|
||||
const bumpRevision = () => setRevision((value) => value + 1);
|
||||
bumpRevision();
|
||||
window.addEventListener('focus', bumpRevision);
|
||||
return () => {
|
||||
window.removeEventListener('focus', bumpRevision);
|
||||
};
|
||||
}, [revalidate]);
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
loadedAccountKeyRef.current = null;
|
||||
setStatus('idle');
|
||||
setRawGuilds([]);
|
||||
setRawChannels([]);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
if (loadedAccountKeyRef.current !== currentAccountKey) {
|
||||
setStatus('loading');
|
||||
setError(null);
|
||||
}
|
||||
if (!accountWorkAdmitted) return;
|
||||
let cancelled = false;
|
||||
setStatus('loading');
|
||||
setError(null);
|
||||
(async () => {
|
||||
try {
|
||||
const [guildsResp, channelsResp] = await Promise.all([
|
||||
@@ -136,10 +158,13 @@ export function useBotInviteDestinations(enabled: boolean, rawPermissions: strin
|
||||
if (cancelled) return;
|
||||
setRawGuilds(guildsResp.body);
|
||||
setRawChannels(channelsResp.body.filter((channel) => channel.type === ChannelTypes.GROUP_DM));
|
||||
loadedAccountKeyRef.current = currentAccountKey;
|
||||
setError(null);
|
||||
setStatus('ready');
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
if (cancelled || isAccountTransitionAbortError(err)) return;
|
||||
logger.error('Failed to fetch bot invite destinations', err);
|
||||
loadedAccountKeyRef.current = null;
|
||||
setError(failureMessage(err) ?? i18nRef.current._(FAILED_TO_LOAD_INVITE_DESTINATIONS_DESCRIPTOR));
|
||||
setRawGuilds([]);
|
||||
setRawChannels([]);
|
||||
@@ -149,7 +174,7 @@ export function useBotInviteDestinations(enabled: boolean, rawPermissions: strin
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [enabled, currentAccountKey]);
|
||||
}, [enabled, currentAccountKey, accountWorkAdmitted, revision]);
|
||||
const guilds = useMemo<ReadonlyArray<GuildWithPermissions>>(
|
||||
() => mapBotGuilds(rawGuilds, requestedPermissions, i18n._(UNKNOWN_COMMUNITY_DESCRIPTOR)),
|
||||
[rawGuilds, requestedPermissions, i18n.locale],
|
||||
|
||||
+7
-3
@@ -8,6 +8,8 @@ import {
|
||||
} from '@app/features/auth/components/pages/oauth_authorize_page/OAuthAuthorizePageShared';
|
||||
import accountStorage from '@app/features/auth/state/AccountStorage';
|
||||
import Accounts from '@app/features/auth/state/Accounts';
|
||||
import {AccountScopedWork} from '@app/features/platform/state/AccountScopedWork';
|
||||
import {isAccountTransitionAbortError} from '@app/features/platform/state/AccountTransitionAbort';
|
||||
import {http} from '@app/features/platform/transport/RestTransport';
|
||||
import {HttpError} from '@app/features/platform/types/EndpointError';
|
||||
import {useEffect, useState} from 'react';
|
||||
@@ -23,6 +25,7 @@ export interface PublicAppState {
|
||||
export function useOAuthPublicApp(clientId: string | null): PublicAppState {
|
||||
const currentUserId = Accounts.currentUserId;
|
||||
const currentAccountKey = Accounts.currentAccountKey;
|
||||
const accountWorkAdmitted = !AccountScopedWork.isSuspended;
|
||||
const [state, setState] = useState<PublicAppState>(() => ({
|
||||
status: clientId ? 'loading' : 'idle',
|
||||
data: null,
|
||||
@@ -33,8 +36,9 @@ export function useOAuthPublicApp(clientId: string | null): PublicAppState {
|
||||
setState({status: 'idle', data: null, error: null});
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setState({status: 'loading', data: null, error: null});
|
||||
if (!accountWorkAdmitted) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await http.get<PublicAppData>(Endpoints.OAUTH_PUBLIC_APPLICATION(clientId));
|
||||
@@ -56,7 +60,7 @@ export function useOAuthPublicApp(clientId: string | null): PublicAppState {
|
||||
}
|
||||
setState({status: 'ready', data: resp.body, error: null});
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
if (cancelled || isAccountTransitionAbortError(err)) return;
|
||||
if (err instanceof HttpError && err.status === 401) {
|
||||
logger.warn('OAuth public app fetch returned 401', err);
|
||||
setState({status: 'session_expired', data: null, error: err});
|
||||
@@ -69,6 +73,6 @@ export function useOAuthPublicApp(clientId: string | null): PublicAppState {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [clientId, currentAccountKey, currentUserId]);
|
||||
}, [clientId, currentAccountKey, currentUserId, accountWorkAdmitted]);
|
||||
return state;
|
||||
}
|
||||
+15
-14
@@ -185,7 +185,11 @@ export function useAuthorizeFlow(options: UseAuthorizeFlowOptions = {}): Authori
|
||||
const initialMissingClientId = !params;
|
||||
const publicAppState = useOAuthPublicApp(params?.clientId ?? null);
|
||||
const scopeSelection = useScopeSelection(scopes);
|
||||
const destinations = useBotInviteDestinations(hasBotScope, params?.permissions ?? null);
|
||||
const destinations = useBotInviteDestinations(
|
||||
hasBotScope,
|
||||
params?.permissions ?? null,
|
||||
phase.kind === 'review' && phase.step === 'community',
|
||||
);
|
||||
const selectedDestinationTarget = parseBotInviteDestinationKey(selectedDestinationKey);
|
||||
const selectedGuildId = selectedDestinationTarget?.kind === 'guild' ? selectedDestinationTarget.id : null;
|
||||
const permissionSelection = usePermissionSelection(
|
||||
@@ -280,18 +284,15 @@ export function useAuthorizeFlow(options: UseAuthorizeFlowOptions = {}): Authori
|
||||
});
|
||||
}, [phase.kind]);
|
||||
const currentAccountKey = Accounts.currentAccountKey;
|
||||
const destinationInitRef = useRef(false);
|
||||
useEffect(() => {
|
||||
destinationInitRef.current = false;
|
||||
setSelectedDestinationKey(null);
|
||||
}, [currentAccountKey]);
|
||||
useEffect(() => {
|
||||
if (!hasBotScope) {
|
||||
destinationInitRef.current = false;
|
||||
setSelectedDestinationKey(null);
|
||||
return;
|
||||
}
|
||||
if (destinationInitRef.current) return;
|
||||
if (destinations.options.some((option) => option.value === selectedDestinationKey)) return;
|
||||
const requestedDestinationKey = params?.channelId
|
||||
? createBotInviteDestinationKey('group_dm', params.channelId)
|
||||
: params?.guildId
|
||||
@@ -304,23 +305,23 @@ export function useAuthorizeFlow(options: UseAuthorizeFlowOptions = {}): Authori
|
||||
? requestedDestinationKey
|
||||
: null,
|
||||
);
|
||||
destinationInitRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (destinations.status !== 'ready') return;
|
||||
const preferredDestinationKey = preferredDestinationKeyRef.current;
|
||||
if (preferredDestinationKey && destinations.options.some((option) => option.value === preferredDestinationKey)) {
|
||||
setSelectedDestinationKey(preferredDestinationKey);
|
||||
destinationInitRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (destinations.options.length > 0) {
|
||||
setSelectedDestinationKey(destinations.options[0].value);
|
||||
destinationInitRef.current = true;
|
||||
return;
|
||||
}
|
||||
destinationInitRef.current = true;
|
||||
}, [hasBotScope, params?.guildId, params?.channelId, destinations.options, destinations.status]);
|
||||
setSelectedDestinationKey(destinations.options[0]?.value ?? null);
|
||||
}, [
|
||||
hasBotScope,
|
||||
params?.guildId,
|
||||
params?.channelId,
|
||||
destinations.options,
|
||||
destinations.status,
|
||||
selectedDestinationKey,
|
||||
]);
|
||||
const selectedDestination = useMemo(
|
||||
() => destinations.options.find((option) => option.value === selectedDestinationKey) ?? null,
|
||||
[destinations.options, selectedDestinationKey],
|
||||
|
||||
@@ -121,7 +121,7 @@ export interface AuthLoginLayoutProps {
|
||||
readonly registerLink: ReactElement<Record<string, unknown>>;
|
||||
readonly onLoginComplete?: ((payload: LoginSuccessPayload) => Promise<void> | void) | null;
|
||||
readonly onBackActionChange?: ((action: (() => void) | null) => void) | null;
|
||||
readonly completeLoginRedirectPath?: string | null;
|
||||
readonly completeLoginRedirectPath: string | null;
|
||||
readonly forceCredentials?: boolean;
|
||||
readonly startWithAddAccount?: boolean;
|
||||
readonly runtimeTarget: AuthRuntimeTarget;
|
||||
@@ -343,15 +343,6 @@ function resolveMethodSsoAction({
|
||||
return {isStarting, label, onStart};
|
||||
}
|
||||
|
||||
function resolveCompleteLoginOptions(
|
||||
completeLoginRedirectPath: string | null,
|
||||
): AuthenticationCommands.CompleteLoginOptions {
|
||||
if (completeLoginRedirectPath == null) {
|
||||
return {};
|
||||
}
|
||||
return {redirectPath: completeLoginRedirectPath};
|
||||
}
|
||||
|
||||
function resolveAuthBackAction({
|
||||
accountListBackAction,
|
||||
authLoginStep,
|
||||
@@ -400,7 +391,7 @@ export const AuthLoginLayout = observer(function AuthLoginLayout({
|
||||
registerLink,
|
||||
onLoginComplete = null,
|
||||
onBackActionChange = null,
|
||||
completeLoginRedirectPath = null,
|
||||
completeLoginRedirectPath,
|
||||
forceCredentials = false,
|
||||
startWithAddAccount = false,
|
||||
runtimeTarget,
|
||||
@@ -509,7 +500,7 @@ export const AuthLoginLayout = observer(function AuthLoginLayout({
|
||||
}
|
||||
await AuthenticationCommands.completeLogin(
|
||||
{...payload, runtimeSnapshot: requireAuthInstanceSnapshot(resolvedAuthRuntimeSnapshot)},
|
||||
resolveCompleteLoginOptions(completeLoginRedirectPath),
|
||||
{redirectPath: completeLoginRedirectPath},
|
||||
);
|
||||
if (onLoginComplete != null) {
|
||||
await onLoginComplete(payload);
|
||||
@@ -572,7 +563,7 @@ export const AuthLoginLayout = observer(function AuthLoginLayout({
|
||||
);
|
||||
const handleStoredAccountLogin = useCallback(
|
||||
async (payload: StoredAccountLoginPayload) => {
|
||||
await AuthenticationCommands.completeLogin(payload, resolveCompleteLoginOptions(completeLoginRedirectPath));
|
||||
await AuthenticationCommands.completeLogin(payload, {redirectPath: completeLoginRedirectPath});
|
||||
if (onLoginComplete != null) {
|
||||
await onLoginComplete(payload);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Routes} from '@app/app/Routes';
|
||||
import type {RuntimeConfigSnapshot} from '@app/features/app/state/InstanceSnapshotStore';
|
||||
import {usesUsernameSignIn} from '@app/features/app/utils/AccountIdentityFeatures';
|
||||
import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands';
|
||||
@@ -141,7 +142,7 @@ const ResourceAuthContent = observer(function ResourceAuthContent<T>({
|
||||
showTitle={false}
|
||||
title={null}
|
||||
onBackActionChange={null}
|
||||
completeLoginRedirectPath={null}
|
||||
completeLoginRedirectPath={Routes.ME}
|
||||
forceCredentials={false}
|
||||
startWithAddAccount={false}
|
||||
runtimeTarget={runtimeTarget}
|
||||
|
||||
@@ -63,7 +63,7 @@ const InviteLoginPage = observer(function InviteLoginPage({code, invite}: Invite
|
||||
title={null}
|
||||
onLoginComplete={runtimeTarget.reset}
|
||||
onBackActionChange={null}
|
||||
completeLoginRedirectPath={null}
|
||||
completeLoginRedirectPath={Routes.ME}
|
||||
forceCredentials={false}
|
||||
startWithAddAccount={false}
|
||||
runtimeTarget={runtimeTarget}
|
||||
|
||||
Reference in new issue
Block a user