From a25f3ee07e1a2f951d8eafc5553ab0b2d43a4682 Mon Sep 17 00:00:00 2001 From: lshw54 Date: Tue, 21 Apr 2026 15:19:08 +0800 Subject: [PATCH 1/2] fix: auto-logout on OTP retrieval failure when session is expired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When OTP retrieval fails due to session expiry or being kicked (e.g. '閒置過久,請嘗試重新登入'), the active session is now automatically removed and the user is redirected to the login page with a 'Session expired' error toast. Previously, the error was silently swallowed with no UI feedback. Also adds launcher.otp_error i18n key for generic OTP failure messages. --- src/features/launcher/OtpPanel.tsx | 33 +++++++++++++++++++++++++++++- src/locales/en-US.json | 1 + src/locales/zh-CN.json | 1 + src/locales/zh-TW.json | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/features/launcher/OtpPanel.tsx b/src/features/launcher/OtpPanel.tsx index b960dd1..5344044 100644 --- a/src/features/launcher/OtpPanel.tsx +++ b/src/features/launcher/OtpPanel.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useTranslation } from "../../lib/i18n"; import { useGameCredentials } from "../../lib/hooks/use-accounts"; import { useAuthStore } from "../../lib/stores/auth-store"; +import { useErrorToastStore } from "../../lib/stores/error-toast-store"; import { commands } from "../../lib/tauri"; import type { GameCredentialsDto } from "../../lib/types"; @@ -11,12 +12,39 @@ interface OtpPanelProps { } export function OtpPanel({ selectedAccountId, onOtpFetched }: OtpPanelProps) { - const { t } = useTranslation(); const credentialsMutation = useGameCredentials(); const [credentials, setCredentials] = useState(null); const [copied, setCopied] = useState(false); const [autoInput, setAutoInput] = useState(true); const [pasting, setPasting] = useState(false); + const { t } = useTranslation(); + const addToast = useErrorToastStore((s) => s.addToast); + + function handleOtpError(error: Error) { + const msg = error.message || t("launcher.otp_error"); + const isSessionGone = + msg.includes("Not authenticated") || + msg.includes("expired") || + msg.includes("閒置過久") || + msg.includes("重新登入") || + msg.includes("Invalid credentials"); + + if (isSessionGone) { + // Session is dead — remove it and redirect to login + const sessionId = useAuthStore.getState().activeSessionId; + if (sessionId) { + commands.logout(sessionId).catch(() => {}); + useAuthStore.getState().removeSession(sessionId); + } + addToast({ + message: t("errors.AUTH_SESSION_EXPIRED"), + category: "authentication", + critical: true, + }); + } else { + addToast({ message: msg, category: "authentication", critical: false }); + } + } async function handleGetOtp() { if (!selectedAccountId) return; @@ -42,6 +70,7 @@ export function OtpPanel({ selectedAccountId, onOtpFetched }: OtpPanelProps) { setTimeout(() => setCopied(false), 2000); } }, + onError: handleOtpError, }); } catch { // Error — fall back to regular OTP @@ -51,6 +80,7 @@ export function OtpPanel({ selectedAccountId, onOtpFetched }: OtpPanelProps) { onOtpFetched?.(selectedAccountId, data.otp); setCopied(false); }, + onError: handleOtpError, }); } finally { setPasting(false); @@ -63,6 +93,7 @@ export function OtpPanel({ selectedAccountId, onOtpFetched }: OtpPanelProps) { onOtpFetched?.(selectedAccountId, data.otp); setCopied(false); }, + onError: handleOtpError, }); } } diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 23137b3..012f8f9 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -173,6 +173,7 @@ "launcher.otp": "One-Time Password", "launcher.otp_placeholder": "Select an account to get OTP", "launcher.get_otp": "Get OTP", + "launcher.otp_error": "Failed to retrieve OTP", "launcher.fetching_otp": "Fetching...", "launcher.auto_input": "Auto Input", "launcher.game_info": "MapleStory", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 0853f86..974247f 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -173,6 +173,7 @@ "launcher.otp": "一次性密码", "launcher.otp_placeholder": "选择账号以获取 OTP", "launcher.get_otp": "获取 OTP", + "launcher.otp_error": "获取密码失败", "launcher.fetching_otp": "获取中...", "launcher.auto_input": "自动输入", "launcher.game_info": "MapleStory", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 24b0c6b..e29de75 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -179,6 +179,7 @@ "launcher.otp": "一次性密碼", "launcher.otp_placeholder": "選擇帳號以取得 OTP", "launcher.get_otp": "取得 OTP", + "launcher.otp_error": "取得密碼失敗", "launcher.fetching_otp": "取得中...", "launcher.auto_input": "自動輸入", "launcher.game_info": "MapleStory", From 998c51d557f92d2e3bd60327284ee82af7cb7f82 Mon Sep 17 00:00:00 2001 From: lshw54 Date: Tue, 21 Apr 2026 15:19:21 +0800 Subject: [PATCH 2/2] feat: add copy account button on account cards Adds a small copy icon (top-right corner, visible on hover) to each account card. Clicking it copies the game account ID to clipboard and shows a brief checkmark confirmation. Replaces the previous double-click-to-copy approach which used error toasts and disrupted the layout. --- src/features/launcher/AccountGrid.tsx | 52 ++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/features/launcher/AccountGrid.tsx b/src/features/launcher/AccountGrid.tsx index dc77813..2764b00 100644 --- a/src/features/launcher/AccountGrid.tsx +++ b/src/features/launcher/AccountGrid.tsx @@ -19,6 +19,7 @@ export function AccountGrid({ selectedAccountId, onSelectAccount }: AccountGridP const { data: accounts, isLoading } = useGameAccounts(); const refreshAccounts = useRefreshAccounts(); const [contextMenu, setContextMenu] = useState(null); + const [copiedId, setCopiedId] = useState(null); const handleContextMenu = useCallback((e: React.MouseEvent, accountId: string) => { e.preventDefault(); @@ -27,6 +28,13 @@ export function AccountGrid({ selectedAccountId, onSelectAccount }: AccountGridP const closeContextMenu = useCallback(() => setContextMenu(null), []); + function handleCopyAccount(e: React.MouseEvent, accountId: string) { + e.stopPropagation(); + navigator.clipboard.writeText(accountId); + setCopiedId(accountId); + setTimeout(() => setCopiedId(null), 1500); + } + return (
@@ -46,17 +54,59 @@ export function AccountGrid({ selectedAccountId, onSelectAccount }: AccountGridP {accounts.map((account) => { const isSelected = selectedAccountId === account.id; const initial = account.displayName.charAt(0).toUpperCase(); + const isCopied = copiedId === account.id; return (