Files
qwen-code/packages/web-shell/client/hooks/useSubscriptionLogout.ts
T
1853b1505f fix(subscription): isolate activation and restore sign-in recovery (#16453)
* fix(web-shell): wait for session before checking subscription commands

* fix(cli): keep signed-out subscription startup in the composer

* fix(subscription): preserve model selection across sign-in

* fix(subscription): align sign-in with model selection scope

* fix(subscription): apply reasoning defaults across models

* fix(subscription): tag Qwen web links by client

* fix(subscription): prefix Qwen web client tags

* fix(subscription): attribute Qwen web links by paywall scene

* fix(subscription): recover historical sessions with saved API models

* fix(cli): remove the signed-out subscription startup notice

* fix(subscription): support standalone desktop activation and expose errors

* fix(subscription): isolate activation and session model selection

* fix(subscription): enforce auth policy and restore home login

* test(cli): preserve the default initializer mock on restore

---------

Co-authored-by: luoyun <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-11-06 20:11:52 +08:00

213 lines
6.8 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import {
useConnection,
useDaemonSessionOwnerGuard,
useWorkspaceActions,
useActions,
} from '@qwen-code/web-shell/daemon-react-sdk';
import type { useI18n } from '../i18n';
import { isDesktopShell, openExternalUrl } from '../utils/externalOpen';
interface Options {
t: ReturnType<typeof useI18n>['t'];
workspaceCwd?: string;
isBlocked: () => boolean;
onBlocked: () => boolean;
onNotice: (kind: 'info' | 'success' | 'warning', message: string) => void;
onError: (error: unknown, message: string) => void;
onCommand?: (text: string) => boolean;
}
export function useSubscriptionLogout(options: Options) {
const connection = useConnection();
const connectionRef = useRef(connection);
connectionRef.current = connection;
const actions = useWorkspaceActions();
const sessionActions = useActions();
const ownerGuard = useDaemonSessionOwnerGuard();
const callbacks = useRef(options);
callbacks.current = options;
const pending = useRef(false);
const [available, setAvailable] = useState(false);
const { workspaceCwd, t } = options;
const sessionAuthType = connection.sessionId
? connection.context?.state._meta?.['authType']
: undefined;
const canCheckAvailability =
!connection.sessionId || connection.status === 'connected';
const loadAuthType = useCallback(
async (opts?: { silent?: boolean }) => {
if (connectionRef.current.sessionId) {
const context = await sessionActions.getContext(opts);
return context.state._meta?.['authType'];
}
if (!workspaceCwd) return undefined;
return (await actions.loadProviders(workspaceCwd)).current?.authType;
},
[actions, sessionActions, workspaceCwd],
);
useEffect(() => {
const abort = new AbortController();
const owner = ownerGuard.capture();
setAvailable(false);
if (workspaceCwd && connection.providers && canCheckAvailability) {
void actions
.getSubscriptionConfig({ signal: abort.signal })
.then(async (config) => {
if (
abort.signal.aborted ||
!owner.isCurrent() ||
!config.enabled ||
!config.signedIn
)
return;
const authType = await loadAuthType({ silent: true });
const latest = await actions.getSubscriptionConfig({
signal: abort.signal,
});
if (!abort.signal.aborted && owner.isCurrent())
setAvailable(
latest.enabled &&
latest.signedIn === true &&
authType === 'qwen-subscription',
);
})
.catch(() => undefined);
}
return () => abort.abort();
}, [
actions,
ownerGuard,
workspaceCwd,
connection.sessionId,
connection.currentModel,
connection.providers,
canCheckAvailability,
sessionAuthType,
loadAuthType,
]);
const handleCommand = useCallback(
(text: string): boolean | undefined => {
const command = /^\/(logout|upgrade)(?:\s|$)/i
.exec(text.trim())?.[1]
?.toLowerCase();
if (
!command ||
connection.commands?.some(
(entry) =>
entry.name === command &&
entry.source &&
entry.source !== 'builtin-command',
)
)
return undefined;
if (!workspaceCwd) return false;
if (callbacks.current.isBlocked()) return callbacks.current.onBlocked();
if (pending.current) return true;
const owner = ownerGuard.capture();
const model = connectionRef.current.currentModel;
const authType = connectionRef.current.context?.state._meta?.['authType'];
const isCurrent = () =>
owner.isCurrent() &&
connectionRef.current.currentModel === model &&
connectionRef.current.context?.state._meta?.['authType'] === authType;
pending.current = true;
void actions
.getSubscriptionConfig()
.then(async (config) => {
if (!isCurrent()) return;
if (!config.enabled) {
callbacks.current.onNotice('info', t('subscription.unavailable'));
return;
}
const authType = await loadAuthType();
if (!isCurrent()) return;
const latest = await actions.getSubscriptionConfig();
if (!isCurrent()) return;
if (
!latest.enabled ||
!latest.signedIn ||
authType !== 'qwen-subscription'
) {
setAvailable(false);
callbacks.current.onNotice(
'info',
t(
command === 'logout'
? 'subscription.logoutNotRequired'
: 'subscription.commandUnavailable',
),
);
return;
}
if (callbacks.current.isBlocked()) {
callbacks.current.onBlocked();
return;
}
if (callbacks.current.onCommand?.(text)) return;
if (command === 'upgrade') {
if (latest.plansUrl) {
if (isDesktopShell()) await openExternalUrl(latest.plansUrl);
else
window.open(latest.plansUrl, '_blank', 'noopener,noreferrer');
}
return;
}
await actions.signOutSubscription();
setAvailable(false);
if (!owner.isCurrent()) return;
await sessionActions.refreshModels(undefined, {
reloadProviders: false,
});
if (owner.isCurrent()) setAvailable(false);
})
.catch((error: unknown) => {
if (!owner.isCurrent()) return;
const failure = error as { body?: { errorStage?: string } };
if (failure?.body?.errorStage === 'revoke') {
setAvailable(false);
void sessionActions
.refreshModels(undefined, { reloadProviders: false })
.catch((refreshError: unknown) => {
if (owner.isCurrent())
callbacks.current.onError(
refreshError,
t('subscription.signOutFailed'),
);
});
callbacks.current.onNotice(
'warning',
t('subscription.signedOutLocally'),
);
} else {
callbacks.current.onError(
error,
t(
command === 'logout'
? 'subscription.signOutFailed'
: 'subscription.upgradeFailed',
),
);
}
})
.finally(() => {
pending.current = false;
});
return true;
},
[
actions,
sessionActions,
connection.commands,
ownerGuard,
t,
workspaceCwd,
loadAuthType,
],
);
return { available, handleCommand };
}