feat: QR-code SSO login between webmail and mobile app
This commit is contained in:
@@ -737,11 +737,16 @@ function LinkDeviceSection() {
|
|||||||
export function AccountSecuritySettings() {
|
export function AccountSecuritySettings() {
|
||||||
const t = useTranslations('settings.security');
|
const t = useTranslations('settings.security');
|
||||||
const { isStalwart, isProbing, probe, fetchAll, fetchAuthInfo } = useAccountSecurityStore();
|
const { isStalwart, isProbing, probe, fetchAll, fetchAuthInfo } = useAccountSecurityStore();
|
||||||
const { isAuthenticated, authMode } = useAuthStore();
|
const { isAuthenticated, authMode, client } = useAuthStore();
|
||||||
const isOAuth = authMode === 'oauth';
|
const isOAuth = authMode === 'oauth';
|
||||||
|
|
||||||
|
// Wait for `client` before probing. On reload the persisted `isAuthenticated`
|
||||||
|
// flips true before the async OAuth reconnect sets `client`; probing in that
|
||||||
|
// window reads a null client, decides the server isn't Stalwart, and caches
|
||||||
|
// that wrong verdict. Gating on `client` (which is set only after connect()
|
||||||
|
// populates the session capabilities) makes the probe run with real data.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated && isStalwart === null) {
|
if (isAuthenticated && client && isStalwart === null) {
|
||||||
probe().then((detected) => {
|
probe().then((detected) => {
|
||||||
if (detected) {
|
if (detected) {
|
||||||
if (isOAuth) {
|
if (isOAuth) {
|
||||||
@@ -752,7 +757,7 @@ export function AccountSecuritySettings() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, isStalwart, probe, fetchAll, fetchAuthInfo, isOAuth]);
|
}, [isAuthenticated, client, isStalwart, probe, fetchAll, fetchAuthInfo, isOAuth]);
|
||||||
|
|
||||||
if (isProbing) {
|
if (isProbing) {
|
||||||
return (
|
return (
|
||||||
@@ -766,9 +771,25 @@ export function AccountSecuritySettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isStalwart === false) {
|
if (isStalwart === false) {
|
||||||
|
// Even when Stalwart account-management isn't exposed (common for OAuth
|
||||||
|
// sessions, whose tokens may lack the management capability), the
|
||||||
|
// cross-device mobile pairing still works — it only needs the OAuth
|
||||||
|
// refresh-token cookie, not `urn:stalwart:jmap`. So surface the QR linker
|
||||||
|
// for OAuth sessions and show the "not available" note for the rest.
|
||||||
|
// Use t.raw (not t) because the message is hand-injected HTML; passing it
|
||||||
|
// through t() makes next-intl try to parse the <a> tag and throw
|
||||||
|
// INVALID_TAG.
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={t('title')} description={t('description')}>
|
<SettingsSection title={t('title')} description={t('description')}>
|
||||||
<div className="text-sm text-muted-foreground py-4" dangerouslySetInnerHTML={{ __html: sanitizeI18nHtml(t('not_available')) }} />
|
{isOAuth ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<LinkDeviceSection />
|
||||||
|
<div className="border-t border-border" />
|
||||||
|
<div className="text-sm text-muted-foreground" dangerouslySetInnerHTML={{ __html: sanitizeI18nHtml(t.raw('not_available')) }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground py-4" dangerouslySetInnerHTML={{ __html: sanitizeI18nHtml(t.raw('not_available')) }} />
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,15 @@ export const useAccountSecurityStore = create<AccountSecurityState>()((set, get)
|
|||||||
set({ isProbing: true });
|
set({ isProbing: true });
|
||||||
try {
|
try {
|
||||||
const client = useAuthStore.getState().client;
|
const client = useAuthStore.getState().client;
|
||||||
const isStalwart = !!client?.hasAccountCapability?.('urn:stalwart:jmap');
|
// No live client yet (e.g. the OAuth session is still reconnecting after
|
||||||
|
// a reload). Don't record a verdict — leave isStalwart null so the caller
|
||||||
|
// re-probes once the client is ready, instead of caching a false "not a
|
||||||
|
// Stalwart server" from a session that hasn't loaded its capabilities.
|
||||||
|
if (!client) {
|
||||||
|
set({ isProbing: false });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isStalwart = !!client.hasAccountCapability?.('urn:stalwart:jmap');
|
||||||
set({ isStalwart, isProbing: false });
|
set({ isStalwart, isProbing: false });
|
||||||
return isStalwart;
|
return isStalwart;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user