feat: Enhance protocol status handling and repair functionality in About and SSOLogin components

This commit is contained in:
2026-05-25 01:01:05 +10:00
parent a4a0fa6758
commit d1626e3585
3 changed files with 73 additions and 58 deletions

View File

@@ -19,41 +19,31 @@ export function About({ requestClose }: AboutProps) {
const [protocolBusy, setProtocolBusy] = useState<boolean>(false); const [protocolBusy, setProtocolBusy] = useState<boolean>(false);
const formatProtocolStatus = useCallback((data: { const formatProtocolStatus = useCallback((data: {
scheme: string;
platform: string; platform: string;
primaryScheme: string; isDefault: boolean;
schemes: Record<string, { registerCall: boolean;
isDefault: boolean; error: string | null;
registerCall: boolean; windows: null | {
error: string | null;
}>;
windows: null | Record<string, {
userChoiceProgId: string | null; userChoiceProgId: string | null;
userChoiceCommand: string | null; userChoiceCommand: string | null;
hkcuCommand: string | null; hkcuCommand: string | null;
hklmCommand: string | null; hklmCommand: string | null;
}>; };
}): string => { }): string => {
const { element } = data.schemes; const state = data.isDefault ? 'registered' : 'not registered';
const primary = data.schemes[data.primaryScheme]; const startupState = data.registerCall ? 'ok' : 'no-change';
const primaryState = primary?.isDefault ? 'registered' : 'not registered'; const owner =
const elementState = element?.isDefault ? 'registered' : 'not registered'; data.platform === 'win32' && data.windows?.userChoiceProgId
const primaryCall = primary?.registerCall ? 'ok' : 'no-change'; ? ` Current default ProgId=${data.windows.userChoiceProgId}.`
const elementCall = element?.registerCall ? 'ok' : 'no-change';
const primaryError = primary?.error ? ` ${data.primaryScheme} error: ${primary.error}` : '';
const elementError = element?.error ? ` element error: ${element.error}` : '';
const primaryWindows = data.windows?.[data.primaryScheme];
const primaryOwner =
data.platform === 'win32' && primaryWindows?.userChoiceProgId
? ` Current ${data.primaryScheme} default ProgId=${primaryWindows.userChoiceProgId}.`
: ''; : '';
const primaryMissingHint = const missingHint =
data.platform === 'win32' && !primaryWindows?.hkcuCommand && !primaryWindows?.hklmCommand data.platform === 'win32' && !data.windows?.hkcuCommand && !data.windows?.hklmCommand
? ` ${data.primaryScheme} link type is missing from Windows registry. Use Repair to create it.` ? ` ${data.scheme} link type is missing from Windows registry. Use Repair to create it.`
: ''; : '';
const errorPart = data.error ? ` Error: ${data.error}` : '';
return `Primary ${data.primaryScheme} is ${primaryState} (startup=${primaryCall}) on ${data.platform}. Legacy element is ${elementState} (startup=${elementCall}).${primaryOwner}${primaryMissingHint}${primaryError}${elementError}`; return `${data.scheme} is ${state} (startup=${startupState}) on ${data.platform}.${owner}${missingHint}${errorPart}`;
}, []); }, []);
const refreshProtocolStatus = useCallback(() => { const refreshProtocolStatus = useCallback(() => {
@@ -92,11 +82,7 @@ export function About({ requestClose }: AboutProps) {
return; return;
} }
const settingsHint = setProtocolStatus(formatProtocolStatus(result.data));
result.data.platform === 'win32' && result.data.openedSettings
? ' Windows Default Apps was opened; set PAARROT to Paarrot and press Refresh.'
: '';
setProtocolStatus(`${formatProtocolStatus(result.data)}${settingsHint}`);
setProtocolBusy(false); setProtocolBusy(false);
}) })
.catch((error) => { .catch((error) => {
@@ -189,7 +175,7 @@ export function About({ requestClose }: AboutProps) {
} }
/> />
<SettingTile <SettingTile
title="Protocol Handlers (paarrot:// + element://)" title="Protocol Handler (paarrot://)"
description={protocolStatus} description={protocolStatus}
after={ after={
<Box gap="100"> <Box gap="100">

View File

@@ -1,7 +1,8 @@
import { Avatar, AvatarImage, Box, Button, Text } from 'folds'; import { Avatar, AvatarImage, Box, Button, Text } from 'folds';
import { IIdentityProvider, SSOAction, createClient } from 'matrix-js-sdk'; import { IIdentityProvider, SSOAction, createClient } from 'matrix-js-sdk';
import React, { useMemo } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
import { openExternalUrl } from '../../utils/tauri';
type SSOLoginProps = { type SSOLoginProps = {
providers?: IIdentityProvider[]; providers?: IIdentityProvider[];
@@ -13,12 +14,39 @@ export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SS
const discovery = useAutoDiscoveryInfo(); const discovery = useAutoDiscoveryInfo();
const baseUrl = discovery['m.homeserver'].base_url; const baseUrl = discovery['m.homeserver'].base_url;
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
const [launchingProviderId, setLaunchingProviderId] = useState<string | null>(null);
const getSSOIdUrl = (ssoId?: string): string => { const getSSOIdUrl = (ssoId?: string): string => {
const existingDeviceId = localStorage.getItem('cinny_device_id') ?? undefined; const existingDeviceId = localStorage.getItem('cinny_device_id') ?? undefined;
return mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId, action, existingDeviceId); return mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId, action, existingDeviceId);
}; };
const handleContinue = useCallback(
async (providerId?: string) => {
if (launchingProviderId) {
return;
}
const currentProviderId = providerId ?? '__default__';
setLaunchingProviderId(currentProviderId);
try {
if (window.electron?.protocol?.repair) {
await window.electron.protocol.repair();
}
} catch (error) {
console.warn('[SSOLogin] Protocol repair failed before SSO launch:', error);
}
try {
await openExternalUrl(getSSOIdUrl(providerId));
} finally {
setLaunchingProviderId(null);
}
},
[getSSOIdUrl, launchingProviderId]
);
const withoutIcon = providers const withoutIcon = providers
? providers.find( ? providers.find(
(provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false) (provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false)
@@ -41,11 +69,13 @@ export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SS
<Avatar <Avatar
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
key={id} key={id}
as="a" as="button"
href={getSSOIdUrl(id)}
aria-label={buttonTitle} aria-label={buttonTitle}
size="300" size="300"
radii="300" radii="300"
onClick={() => {
handleContinue(id);
}}
> >
<AvatarImage src={iconUrl!} alt={name} title={buttonTitle} /> <AvatarImage src={iconUrl!} alt={name} title={buttonTitle} />
</Avatar> </Avatar>
@@ -56,12 +86,14 @@ export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SS
<Button <Button
style={{ width: '100%' }} style={{ width: '100%' }}
key={id} key={id}
as="a"
href={getSSOIdUrl(id)}
size="500" size="500"
variant="Secondary" variant="Secondary"
fill="Soft" fill="Soft"
outlined outlined
disabled={Boolean(launchingProviderId)}
onClick={() => {
handleContinue(id);
}}
before={ before={
iconUrl && ( iconUrl && (
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
@@ -79,12 +111,14 @@ export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SS
) : ( ) : (
<Button <Button
style={{ width: '100%' }} style={{ width: '100%' }}
as="a"
href={getSSOIdUrl()}
size="500" size="500"
variant="Secondary" variant="Secondary"
fill="Soft" fill="Soft"
outlined outlined
disabled={Boolean(launchingProviderId)}
onClick={() => {
handleContinue();
}}
> >
<Text align="Center" size="B500" truncate> <Text align="Center" size="B500" truncate>
Continue with SSO Continue with SSO

33
src/ext.d.ts vendored
View File

@@ -61,41 +61,36 @@ interface ElectronAPI {
getStatus: () => Promise<{ getStatus: () => Promise<{
success: boolean; success: boolean;
data?: { data?: {
scheme: string;
platform: string; platform: string;
primaryScheme: string; isDefault: boolean;
schemes: Record<string, { before: boolean;
isDefault: boolean; registerCall: boolean;
before: boolean; error: string | null;
registerCall: boolean; windows: null | {
error: string | null;
}>;
windows: null | Record<string, {
userChoiceProgId: string | null; userChoiceProgId: string | null;
userChoiceCommand: string | null; userChoiceCommand: string | null;
hkcuCommand: string | null; hkcuCommand: string | null;
hklmCommand: string | null; hklmCommand: string | null;
}>; };
}; };
error?: string; error?: string;
}>; }>;
repair: () => Promise<{ repair: () => Promise<{
success: boolean; success: boolean;
data?: { data?: {
openedSettings: boolean; scheme: string;
platform: string; platform: string;
primaryScheme: string; isDefault: boolean;
schemes: Record<string, { before: boolean;
isDefault: boolean; registerCall: boolean;
before: boolean; error: string | null;
registerCall: boolean; windows: null | {
error: string | null;
}>;
windows: null | Record<string, {
userChoiceProgId: string | null; userChoiceProgId: string | null;
userChoiceCommand: string | null; userChoiceCommand: string | null;
hkcuCommand: string | null; hkcuCommand: string | null;
hklmCommand: string | null; hklmCommand: string | null;
}>; };
}; };
error?: string; error?: string;
}>; }>;