Improve showing errors from main window

This commit is contained in:
Samuel Elliott
2025-07-25 21:04:57 +01:00
parent e9137a41c6
commit 706e41133d
8 changed files with 95 additions and 25 deletions

View File

@@ -9,7 +9,7 @@ import Section from './section.js';
import { Button } from '../components/index.js';
export default function Event(props: {
user: User;
user: User<true>;
event: ActiveEvent;
loading?: boolean;
error?: Error;
@@ -21,7 +21,9 @@ export default function Event(props: {
const event_members = props.event.members.filter(m => m.isPlaying).length;
const voip_members = props.event.members.filter(m => m.isJoinedVoip).length;
return <Section title={t('title')} loading={props.loading} error={props.error}>
return <Section title={t('title')} loading={props.loading} error={props.error}
errorKey={[props.user.nsotoken, 'activeevent']}
>
<View style={styles.content}>
<Image source={{uri: props.event.imageUri, width: 100, height: 100}} style={styles.image} />

View File

@@ -10,7 +10,7 @@ import AddOutline from '../components/icons/add-outline.js';
import { FriendCode } from '../components/index.js';
export default function Friends(props: {
user: User;
user: User<true>;
friends: Friend_4[];
loading?: boolean;
error?: Error;
@@ -27,7 +27,10 @@ export default function Friends(props: {
<Text style={[styles.icon, {color: '#' + accent_colour}]}><AddOutline title={t('add')!} /></Text>
</TouchableOpacity>;
return <Section title={t('title')} loading={props.loading} error={props.error} headerButtons={header_buttons}>
return <Section title={t('title')} loading={props.loading} error={props.error}
errorKey={[props.user.nsotoken, 'friends']}
headerButtons={header_buttons}
>
{props.friends.length ? <ScrollView horizontal>
<View style={styles.content}>
{props.friends.map(f => <Friend key={f.nsaId} friend={f} user={props.user} />)}

View File

@@ -56,9 +56,7 @@ export default function Main(props: {
useEventListener(events, 'window:refresh', refresh, []);
const showErrorDetails = useCallback(() => {
if (friends_error) alert(friends_error.stack ?? friends_error.message);
if (webservices_error) alert(webservices_error.stack ?? webservices_error.message);
if (active_event_error) alert(active_event_error.stack ?? active_event_error.message);
ipc.showCoralErrors(props.user.nsotoken!, ['friends', 'webservices', 'activeevent']);
}, [friends_error, webservices_error, active_event_error]);
if (!friends || !webservices || !active_event) {

View File

@@ -3,6 +3,7 @@ import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'rea
import { useAccentColour, useColourScheme } from '../util.js';
import { useTranslation } from 'react-i18next';
import { BORDER_COLOUR_LIGHT, BORDER_COLOUR_SECONDARY_DARK, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import type { CachedErrorKey } from '../../main/ipc.js';
import ipc from '../ipc.js';
import Warning from '../components/icons/warning.js';
@@ -10,6 +11,7 @@ export default function Section(props: React.PropsWithChildren<{
title: string;
loading?: boolean;
error?: Error;
errorKey?: [string, CachedErrorKey];
headerButtons?: React.ReactNode;
}>) {
const theme = useColourScheme() === 'light' ? light : dark;
@@ -17,7 +19,7 @@ export default function Section(props: React.PropsWithChildren<{
const { t, i18n } = useTranslation('main_window', { keyPrefix: 'main_section' });
const showErrorDetails = useCallback(() => {
alert(props.error);
props.errorKey ? ipc.showCoralErrors(...props.errorKey) : alert(props.error);
}, [props.error]);
return <View style={[styles.container, theme.container]}>

View File

@@ -8,7 +8,7 @@ import { TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import Section from './section.js';
export default function WebServices(props: {
user: User;
user: User<true>;
webservices: WebService[];
loading?: boolean;
error?: Error;
@@ -17,7 +17,9 @@ export default function WebServices(props: {
if (!props.webservices.length) return null;
return <Section title={t('title')} loading={props.loading} error={props.error}>
return <Section title={t('title')} loading={props.loading} error={props.error}
errorKey={[props.user.nsotoken, 'webservices']}
>
<ScrollView horizontal>
<View style={styles.content}>
{props.webservices.map(g => <WebService key={g.id} webservice={g} token={props.user.nsotoken} />)}

View File

@@ -250,9 +250,12 @@ export interface User<IsCoral extends boolean = boolean> {
NintendoAccountUserCoral | NintendoAccountUserMoon;
nso:
IsCoral extends true ? SavedToken :
IsCoral extends true ? null :
IsCoral extends false ? null :
SavedToken | null;
nsotoken: string | undefined;
nsotoken:
IsCoral extends true ? string :
IsCoral extends false ? null :
string | null;
moon: SavedMoonToken | null;
moontoken: string | undefined;
}
@@ -263,7 +266,7 @@ export async function getAccounts() {
const accounts: User[] = [];
for (const id of ids ?? []) {
const nsotoken = await ipc.getNintendoAccountCoralToken(id);
const nsotoken = await ipc.getNintendoAccountCoralToken(id) ?? null;
const moontoken = await ipc.getNintendoAccountMoonToken(id);
const nso = nsotoken ? await ipc.getSavedCoralToken(nsotoken) ?? null : null;

View File

@@ -1,5 +1,8 @@
import { BrowserWindow, clipboard, IpcMain, IpcMainInvokeEvent, KeyboardEvent, Menu, MenuItem, ShareMenu, SharingItem, shell, systemPreferences } from 'electron';
import { User } from 'discord-rpc';
import createDebug from '../../util/debug.js';
import { ErrorDescription, ErrorDescriptionSymbol, HasErrorDescription } from '../../util/errors.js';
import { Jwt } from '../../util/jwt.js';
import openWebService, { handleOpenWebServiceError, QrCodeReaderOptions, WebServiceIpc, WebServiceValidationError } from './webservices.js';
import { createModalWindow, getWindowConfiguration, setWindowHeight } from './windows.js';
import { askAddNsoAccount, askAddPctlAccount } from './na-auth.js';
@@ -7,19 +10,42 @@ import { App } from './index.js';
import { EmbeddedPresenceMonitor } from './monitor.js';
import { DiscordPresenceConfiguration, DiscordPresenceSource, DiscordStatus, LoginItemOptions, WindowType } from '../common/types.js';
import { CurrentUser, Friend, Game, PresenceState, WebService } from '../../api/coral-types.js';
import { NintendoAccountUser } from '../../api/na.js';
import createDebug from '../../util/debug.js';
import { NintendoAccountSessionTokenJwtPayload, NintendoAccountUser } from '../../api/na.js';
import { DiscordPresence } from '../../discord/types.js';
import { getDiscordRpcClients } from '../../discord/rpc.js';
import { defaultTitle } from '../../discord/titles.js';
import type { FriendProps } from '../browser/friend/index.js';
import type { DiscordSetupProps } from '../browser/discord/index.js';
import type { AddFriendProps } from '../browser/add-friend/index.js';
import { CoralUser } from '../../common/users.js';
import { MembershipRequiredError } from '../../common/auth/util.js';
import { ErrorDescription, ErrorDescriptionSymbol, HasErrorDescription } from '../../util/errors.js';
import { showErrorDialog } from './util.js';
const debug = createDebug('app:main:ipc');
export type CachedErrorKey = 'user' | 'announcements' | 'friends' | 'webservices' | 'activeevent' | 'friendcodeurl' | 'friendrequests-received' | 'friendrequests-sent';
const cached_errors = new Map<string, Map<CachedErrorKey, Error>>();
function createErrorHandler(user: CoralUser<any>, key: CachedErrorKey): (err: Error) => void
function createErrorHandler(na_session_token: string, key: CachedErrorKey): (err: Error) => void
function createErrorHandler(user: CoralUser<any> | string, key: CachedErrorKey) {
if (typeof user === 'string') {
const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(user);
user = jwt.payload.sub;
} else {
user = user.data.user.id;
}
return (err: Error) => {
let errors = cached_errors.get(user);
if (!errors) cached_errors.set(user, errors = new Map());
errors.set(key, err);
throw err;
};
}
export function setupIpc(appinstance: App, ipcMain: IpcMain) {
const store = appinstance.store;
const storage = appinstance.store.storage;
@@ -81,20 +107,52 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
handle('coral:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken.' + id));
handle('coral:getcachedtoken', (e, token: string) => storage.getItem('NsoToken.' + token));
handle('coral:announcements', (e, token: string) => store.users.get(token).then(u => u.announcements.result));
handle('coral:friends', (e, token: string) => store.users.get(token).then(u => u.getFriends()));
handle('coral:webservices', (e, token: string) => store.users.get(token).then(u => u.getWebServices()));
handle('coral:friends', (e, token: string) => store.users.get(token).then(u => u.getFriends())
.catch(createErrorHandler(token, 'friends')));
handle('coral:webservices', (e, token: string) => store.users.get(token).then(u => u.getWebServices())
.catch(createErrorHandler(token, 'webservices')));
handle('coral:openwebservice', (e, webservice: WebService, token: string, qs?: string) =>
store.users.get(token).then(u => openWebService(store, token, u.nso, u.data, webservice, qs)
.catch(err => err instanceof WebServiceValidationError || err instanceof MembershipRequiredError ?
handleOpenWebServiceError(err, webservice, qs, u.data, BrowserWindow.fromWebContents(e.sender)!) :
null)));
handle('coral:activeevent', (e, token: string) => store.users.get(token).then(u => u.getActiveEvent()));
handle('coral:friendcodeurl', (e, token: string) => store.users.get(token).then(u => u.nso.getFriendCodeUrl()));
handle('coral:friendrequests:received', (e, token: string) => store.users.get(token).then(u => u.nso.getReceivedFriendRequests()));
handle('coral:friendrequests:sent', (e, token: string) => store.users.get(token).then(u => u.nso.getSentFriendRequests()));
handle('coral:activeevent', (e, token: string) => store.users.get(token).then(u => u.getActiveEvent()
.then(e => e ?? {}))
.catch(createErrorHandler(token, 'activeevent')));
handle('coral:friendcodeurl', (e, token: string) => store.users.get(token).then(u => u.nso.getFriendCodeUrl())
.catch(createErrorHandler(token, 'friendcodeurl')));
handle('coral:friendrequests:received', (e, token: string) => store.users.get(token).then(u => u.getReceivedFriendRequests())
.catch(createErrorHandler(token, 'friendrequests-received')));
handle('coral:friendrequests:sent', (e, token: string) => store.users.get(token).then(u => u.getSentFriendRequests())
.catch(createErrorHandler(token, 'friendrequests-sent')));
handle('coral:friendcode', (e, token: string, friendcode: string, hash?: string) => store.users.get(token).then(u => u.nso.getUserByFriendCode(friendcode, hash)));
handle('coral:addfriend', (e, token: string, nsaid: string) => store.users.get(token).then(u => u.addFriend(nsaid)));
handle('coral:showlasterrors', async (e, token: string, keys: CachedErrorKey | CachedErrorKey[]) => {
const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(token);
const user = jwt.payload.sub;
const errors = cached_errors.get(user);
if (!errors?.size) return;
if (typeof keys === 'string') keys = [keys];
const show = new Set<Error>();
for (const key of keys) {
if (!errors.has(key)) continue;
show.add(errors.get(key)!);
}
for (const error of show) {
showErrorDialog({
message: 'Error loading data',
error,
app: appinstance,
window: BrowserWindow.fromWebContents(e.sender) ?? undefined,
});
}
});
handle('window:showpreferences', () => appinstance.showPreferencesWindow().id);
handle('window:showfriend', (e, props: FriendProps) =>
createModalWindow(WindowType.FRIEND, props, e.sender).id);

View File

@@ -7,9 +7,9 @@ import type { SavedToken } from '../../common/auth/coral.js';
import type { SavedMoonToken } from '../../common/auth/moon.js';
import type { UpdateCacheData } from '../../common/update.js';
import type { StatusUpdate } from '../../common/status.js';
import type { Announcements, Announcements_4, CoralSuccessResponse, CurrentUser, Friend, Friend_4, FriendCodeUrl, FriendCodeUser, GetActiveEventResult, ReceivedFriendRequests, SentFriendRequests, WebService, WebServices, WebServices_4 } from '../../api/coral-types.js';
import type { Announcements_4, CoralSuccessResponse, CurrentUser, Friend, Friend_4, FriendCodeUrl, FriendCodeUser, GetActiveEventResult, ReceivedFriendRequests, SentFriendRequests, WebService, WebServices_4 } from '../../api/coral-types.js';
import type { DiscordPresence } from '../../discord/types.js';
import type { NintendoAccountUser } from '../../api/na.js';
import type { CachedErrorKey } from '../main/ipc.js';
import type { DiscordSetupProps } from '../browser/discord/index.js';
import type { FriendProps } from '../browser/friend/index.js';
import type { AddFriendProps } from '../browser/add-friend/index.js';
@@ -72,6 +72,8 @@ const ipc = {
getNsoUserByFriendCode: (token: string, friendcode: string, hash?: string) => inv<FriendCodeUser>('coral:friendcode', token, friendcode, hash),
addNsoFriend: (token: string, nsa_id: string) => inv<{result: CoralSuccessResponse<{}>; friend: Friend | null}>('coral:addfriend', token, nsa_id),
showCoralErrors: (token: string, keys: CachedErrorKey | CachedErrorKey[]) => inv('coral:showlasterrors', token, keys),
getDiscordPresenceConfig: () => inv<DiscordPresenceConfiguration | null>('discord:config'),
setDiscordPresenceConfig: (config: DiscordPresenceConfiguration | null) => inv<void>('discord:setconfig', config),
getDiscordPresenceOptions: () => inv<Omit<DiscordPresenceConfiguration, 'source'> | null>('discord:options'),
@@ -100,7 +102,7 @@ const ipc = {
showUserMenu: (user: NintendoAccountUserCoral | NintendoAccountUserMoon, nso?: CurrentUser, moon?: boolean) => inv('menu:user', user, nso, moon),
showAddUserMenu: () => inv('menu:add-user'),
showFriendCodeMenu: (fc: CurrentUser['links']['friendCode']) => inv('menu:friend-code', fc),
showFriendMenu: (user: NintendoAccountUserCoral, nso: CurrentUser, friend: Friend) => inv('menu:friend', user, nso, friend),
showFriendMenu: (user: NintendoAccountUserCoral, nso: CurrentUser, friend: Friend_4) => inv('menu:friend', user, nso, friend),
registerEventListener: (event: string, listener: (args: any[]) => void) => events.on(event, listener),
removeEventListener: (event: string, listener: (args: any[]) => void) => events.removeListener(event, listener),