From 706e41133d305f709e1baf48df846d078a8afd68 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Fri, 25 Jul 2025 21:04:57 +0100 Subject: [PATCH] Improve showing errors from main window --- src/app/browser/main/event.tsx | 6 ++- src/app/browser/main/friends.tsx | 7 ++- src/app/browser/main/main.tsx | 4 +- src/app/browser/main/section.tsx | 4 +- src/app/browser/main/webservices.tsx | 6 ++- src/app/browser/util.tsx | 9 ++-- src/app/main/ipc.ts | 76 ++++++++++++++++++++++++---- src/app/preload/index.ts | 8 +-- 8 files changed, 95 insertions(+), 25 deletions(-) diff --git a/src/app/browser/main/event.tsx b/src/app/browser/main/event.tsx index 91e4380..751c192 100644 --- a/src/app/browser/main/event.tsx +++ b/src/app/browser/main/event.tsx @@ -9,7 +9,7 @@ import Section from './section.js'; import { Button } from '../components/index.js'; export default function Event(props: { - user: User; + user: User; 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
+ return
diff --git a/src/app/browser/main/friends.tsx b/src/app/browser/main/friends.tsx index ecca42a..3c3f61d 100644 --- a/src/app/browser/main/friends.tsx +++ b/src/app/browser/main/friends.tsx @@ -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; friends: Friend_4[]; loading?: boolean; error?: Error; @@ -27,7 +27,10 @@ export default function Friends(props: { ; - return
+ return
{props.friends.length ? {props.friends.map(f => )} diff --git a/src/app/browser/main/main.tsx b/src/app/browser/main/main.tsx index 1de2bd8..675f9dd 100644 --- a/src/app/browser/main/main.tsx +++ b/src/app/browser/main/main.tsx @@ -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) { diff --git a/src/app/browser/main/section.tsx b/src/app/browser/main/section.tsx index ac043c0..40b981f 100644 --- a/src/app/browser/main/section.tsx +++ b/src/app/browser/main/section.tsx @@ -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 diff --git a/src/app/browser/main/webservices.tsx b/src/app/browser/main/webservices.tsx index 569e464..a3c9b25 100644 --- a/src/app/browser/main/webservices.tsx +++ b/src/app/browser/main/webservices.tsx @@ -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; webservices: WebService[]; loading?: boolean; error?: Error; @@ -17,7 +17,9 @@ export default function WebServices(props: { if (!props.webservices.length) return null; - return
+ return
{props.webservices.map(g => )} diff --git a/src/app/browser/util.tsx b/src/app/browser/util.tsx index d8681dc..c0dd421 100644 --- a/src/app/browser/util.tsx +++ b/src/app/browser/util.tsx @@ -250,9 +250,12 @@ export interface User { 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; diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts index 9236b06..8473699 100644 --- a/src/app/main/ipc.ts +++ b/src/app/main/ipc.ts @@ -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>(); + +function createErrorHandler(user: CoralUser, key: CachedErrorKey): (err: Error) => void +function createErrorHandler(na_session_token: string, key: CachedErrorKey): (err: Error) => void +function createErrorHandler(user: CoralUser | string, key: CachedErrorKey) { + if (typeof user === 'string') { + const [jwt, sig] = Jwt.decode(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(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(); + + 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); diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts index 0636c61..f94520e 100644 --- a/src/app/preload/index.ts +++ b/src/app/preload/index.ts @@ -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('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('discord:config'), setDiscordPresenceConfig: (config: DiscordPresenceConfiguration | null) => inv('discord:setconfig', config), getDiscordPresenceOptions: () => inv | 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),