From 111292d481d0b3cfa6a82514db029a2a151f5ed4 Mon Sep 17 00:00:00 2001 From: Zeus <32263615+ZeusJunior@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:29:23 +0100 Subject: [PATCH 1/6] Fix release note generation --- .github/workflows/version.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index c1cf4df..78b58c0 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -92,7 +92,7 @@ jobs: echo "Generating changelog from $PREV_TAG to ${{ steps.version.outputs.new_version }}" # Features (feat:, add:, new:) - FEATURES=$(git log --pretty=format:"- %s" $PREV_TAG..HEAD | grep -E "^- (feat|add|new):" | sed 's/^- (feat|add|new): /- /' || true) + FEATURES=$(git log --pretty=format:"%s" $PREV_TAG..HEAD | grep -E "^(\[skip ci\] )?(feat|add|new):" | sed -E 's/^(\[skip ci\] )?(feat|add|new): /- /' || true) if [ -n "$FEATURES" ]; then echo "### New Features" >> release_notes.md echo "$FEATURES" >> release_notes.md @@ -100,7 +100,7 @@ jobs: fi # Bug fixes (fix:, bug:) - FIXES=$(git log --pretty=format:"- %s" $PREV_TAG..HEAD | grep -E "^- (fix|bug):" | sed 's/^- (fix|bug): /- /' || true) + FIXES=$(git log --pretty=format:"%s" $PREV_TAG..HEAD | grep -E "^(\[skip ci\] )?(fix|bug):" | sed -E 's/^(\[skip ci\] )?(fix|bug): /- /' || true) if [ -n "$FIXES" ]; then echo "### Bug Fixes" >> release_notes.md echo "$FIXES" >> release_notes.md @@ -108,7 +108,7 @@ jobs: fi # Other commits - OTHER=$(git log --pretty=format:"- %s" $PREV_TAG..HEAD | grep -vE "^- (feat|add|new|fix|bug|chore):" || true) + OTHER=$(git log --pretty=format:"%s" $PREV_TAG..HEAD | grep -vE "^(\[skip ci\] )?(feat|add|new|fix|bug|chore):" || true) if [ -n "$OTHER" ]; then echo "### Other Changes" >> release_notes.md echo "$OTHER" >> release_notes.md From 84a3cd1dab8d000e1bffbc9087aff425bc4a62c0 Mon Sep 17 00:00:00 2001 From: Zeus <32263615+ZeusJunior@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:58:04 +0100 Subject: [PATCH 2/6] fix: don't save refreshToken when importing from SDA, it's not made for web use --- main/background.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/main/background.ts b/main/background.ts index fbae6a5..00745ab 100644 --- a/main/background.ts +++ b/main/background.ts @@ -254,10 +254,6 @@ handleIpc('import-mafile', async (event, filePath) => { } }; - if ('RefreshToken' in maFileData.Session && maFileData.Session.RefreshToken) { - accountData.refreshToken = maFileData.Session.RefreshToken; - } - // Add the account const success = addAccount(maFileData.Session.SteamID, accountData); From d84e6f3e9da90a5ce680c0d36f1b959b9f2a18b8 Mon Sep 17 00:00:00 2001 From: Zeus <32263615+ZeusJunior@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:17:26 +0100 Subject: [PATCH 3/6] feat: accept/decline confirmations --- main/background.ts | 75 ++++++++++- main/preload.ts | 10 ++ main/types.ts | 12 +- renderer/components/Icons/DocumentCheck.tsx | 7 ++ renderer/components/Sidebar.tsx | 26 +++- renderer/pages/confirmations.tsx | 132 ++++++++++++++++++++ 6 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 renderer/components/Icons/DocumentCheck.tsx create mode 100644 renderer/pages/confirmations.tsx diff --git a/main/background.ts b/main/background.ts index 00745ab..11f381b 100644 --- a/main/background.ts +++ b/main/background.ts @@ -5,8 +5,9 @@ import { createWindow, getCurrentAccount, getDebugInfo, configFileExists, getAll import SteamCommunity from 'steamcommunity'; import { addAuthenticator, finalizeAuthenticator, getAuthCode, loginAgain, refreshProfile } from './helpers/steam'; import { createEncryptedStore, initializeStore } from './store'; -import { Account, IpcHandlers, MaFileData } from './types'; +import { Account, Confirmation, IpcHandlers, MaFileData } from './types'; import { readFile } from 'fs/promises'; +import { getConfirmationKey, time } from 'steam-totp'; // Type-safe IPC handler helper function handleIpc( @@ -262,4 +263,76 @@ handleIpc('import-mafile', async (event, filePath) => { } return maFileData.Session.SteamID; +}); + +handleIpc('get-confirmations', async (event) => { + const account = getCurrentAccount(false); + if (!account) { + throw new Error('No current account set'); + } + + const community = new SteamCommunity(); + community.setCookies(account.cookies || []); + + return new Promise((resolve, reject) => { + community.loggedIn(async (err, loggedIn) => { + if (err) { + event.sender.send('login-required'); + return resolve([]); + } + + const proceed = async () => { + community.getConfirmations(time(), getConfirmationKey(account.identitySecret, time(), 'conf'), async (err, confirmations) => { + if (err) { + return reject(err); + } + + return resolve(confirmations as unknown as Confirmation[]); + }); + }; + + if (loggedIn) { + return proceed(); + } + + // If we're not logged in, check if we have a refresh token to re-authenticate + if (!account?.refreshToken) { + event.sender.send('login-required'); + return resolve([]); + } + + loginAgain({ + refreshToken: account.refreshToken, + }) + .then(() => { + return proceed(); + }) + .catch(() => { + event.sender.send('login-required'); + return resolve([]); + }); + }); + }); +}); + +handleIpc('respond-to-confirmation', async (event, id: number, key: string, accept: boolean) => { + const account = getCurrentAccount(false); + if (!account) { + throw new Error('No current account set'); + } + + const community = new SteamCommunity(); + community.setCookies(account.cookies || []); + + return new Promise((resolve, reject) => { + const confTime = time(); + const confKey = getConfirmationKey(account.identitySecret, confTime, accept ? 'allow' : 'cancel'); + community.respondToConfirmation(id, key, confTime, confKey, accept, (err) => { + if (err) { + return reject(err); + } + + resolve(); + }); + }); }); \ No newline at end of file diff --git a/main/preload.ts b/main/preload.ts index e4e6644..2a5deda 100644 --- a/main/preload.ts +++ b/main/preload.ts @@ -51,12 +51,22 @@ const handler = { return invoke('import-mafile', filePath); }, + getConfirmations: () => { + return invoke('get-confirmations'); + }, + respondToConfirmation: (id: number, key: string, accept: boolean) => { + return invoke('respond-to-confirmation', id, key, accept); + }, + events: { onLoginRequired: (callback: () => void) => { ipcRenderer.on('login-required', () => { callback(); }); }, + removeOnLoginRequired: () => { + ipcRenderer.removeAllListeners('login-required'); + } } }; diff --git a/main/types.ts b/main/types.ts index 76a914a..750d5b2 100644 --- a/main/types.ts +++ b/main/types.ts @@ -1,5 +1,6 @@ import SteamUser from 'steam-user'; import SteamCommunity from 'steamcommunity'; +import CConfirmation from 'steamcommunity/classes/CConfirmation'; export interface ThunderConfig { initialized: boolean; @@ -12,9 +13,9 @@ export interface Account { id64: string; personaName: string; accountName: string; - sharedSecret?: string; - identitySecret?: string; - recoveryCode?: string; + sharedSecret: string; + identitySecret: string; + recoveryCode: string; avatarUrl: string; // Steam login stuff @@ -79,6 +80,9 @@ interface AddAuthenticatorSuccess { recoveryCode: string; } +export type Confirmation = Omit & { + sending: string; +}; export interface IpcHandlers { 'debug-info': () => Promise; @@ -95,4 +99,6 @@ export interface IpcHandlers { 'get-auth-code': () => Promise; 'show-mafile-dialog': () => Promise; 'import-mafile': (filePath: string) => Promise; + 'get-confirmations': () => Promise; + 'respond-to-confirmation': (id: number, key: string, accept: boolean) => Promise; } diff --git a/renderer/components/Icons/DocumentCheck.tsx b/renderer/components/Icons/DocumentCheck.tsx new file mode 100644 index 0000000..f9dea4e --- /dev/null +++ b/renderer/components/Icons/DocumentCheck.tsx @@ -0,0 +1,7 @@ +export default function DocumentCheckIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/renderer/components/Sidebar.tsx b/renderer/components/Sidebar.tsx index 0cfd58f..cca9a55 100644 --- a/renderer/components/Sidebar.tsx +++ b/renderer/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import Link from 'next/link'; import HomeIcon from './Icons/Home'; import GithubIcon from './Icons/Github'; @@ -7,6 +7,7 @@ import Image from 'next/image'; import ArrowLRIcon from './Icons/ArrowsLR'; import Popup from './Popup/Popup'; import ExternalIcon from './Icons/External'; +import DocumentCheckIcon from './Icons/DocumentCheck'; export default function Sidebar() { const { currentAccount } = useAccount(); @@ -17,11 +18,18 @@ export default function Sidebar() { const handleOpenSteam = (url: string) => { window.electron.openSteamWindow(url); + }; + + useEffect(() => { // Perhaps our session expired, listen for event window.electron.events.onLoginRequired(() => { setIsPopupOpen(true); }); - }; + + return () => { + window.electron.events.removeOnLoginRequired(); + }; + }, []); const handlePasswordSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -66,7 +74,15 @@ export default function Sidebar() { Home - {/* Button to open https://steamcommunity.com in a new thunder tab with the cookies of the current account */} +
  • + + + Confirmations + +
  • setIsPopupOpen(false)} > @@ -186,7 +202,7 @@ export default function Sidebar() { - Logging... + Logging in... ) : ( 'Login' diff --git a/renderer/pages/confirmations.tsx b/renderer/pages/confirmations.tsx new file mode 100644 index 0000000..9d00c7c --- /dev/null +++ b/renderer/pages/confirmations.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import { Confirmation } from '../../main/types'; + +interface LoadingConfirmations extends Confirmation { + isLoading?: boolean; +} + +export default function ConfirmationsPage() { + const [confirmations, setConfirmations] = useState([]); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(true); + + const getConfirmations = () => { + setIsLoading(true); + window.electron.getConfirmations() + .then((confs) => { + setConfirmations(confs); + }) + .catch((err: Error) => { + setError(err.message || 'An unknown error occurred'); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + useEffect(() => { + getConfirmations(); + }, []); + + const handleConfirmationAction = (id: number, key: string, accept: boolean) => { + // Find the confirmation and set it to loading + setConfirmations((prev) => + prev.map((conf) => + conf.id === id ? { ...conf, isLoading: true } : conf + ) + ); + + window.electron.respondToConfirmation(id, key, accept) + .then(() => { + setConfirmations((prev) => prev.filter((conf) => conf.id !== id)); + }) + .catch((err: Error) => { + // On error, unset loading state + setConfirmations((prev) => + prev.map((conf) => + conf.id === id ? { ...conf, isLoading: false } : conf + ) + ); + setError(err.message || 'An unknown error occurred'); + }); + }; + + return ( + <> + + Confirmations - Thunder + +
    +

    + Confirmations +

    + + {error && ( +
    + {error} +
    + )} + + {isLoading && ( +
    +
    +
    +

    Loading confirmations...

    +
    +
    + )} + + {!isLoading && confirmations.length === 0 && ( +

    No pending confirmations.

    + )} + {!isLoading && confirmations.length > 0 && ( +
      + {confirmations.map((conf) => ( +
    • +
      +
      + {conf.title} +
      +

      {conf.title}

      +

      {conf.sending}

      + {conf.receiving &&

      {conf.receiving}

      } +
      +
      +
      +

      {new Date(conf.time).toLocaleString()}

      +
      + + +
      +
      +
      +
    • + ))} +
    + )} +
    + + ); +} From 85d5e7a3d8c09f7ffe0f8e6f7ff2329b16746419 Mon Sep 17 00:00:00 2001 From: Zeus <32263615+ZeusJunior@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:38:13 +0100 Subject: [PATCH 4/6] feat: accept all confirmations at once --- main/background.ts | 23 +++++ main/preload.ts | 3 + main/types.ts | 1 + renderer/pages/confirmations.tsx | 161 ++++++++++++++++++++++--------- 4 files changed, 144 insertions(+), 44 deletions(-) diff --git a/main/background.ts b/main/background.ts index 11f381b..dd56d96 100644 --- a/main/background.ts +++ b/main/background.ts @@ -332,6 +332,29 @@ handleIpc('respond-to-confirmation', async (event, id: number, key: string, acce return reject(err); } + resolve(); + }); + }); +}); + +handleIpc('accept-all-confirmations', async () => { + const account = getCurrentAccount(false); + if (!account) { + throw new Error('No current account set'); + } + + const community = new SteamCommunity(); + community.setCookies(account.cookies || []); + + return new Promise((resolve, reject) => { + const confTime = time(); + const confKey = getConfirmationKey(account.identitySecret, confTime, 'conf'); + const allowKey = getConfirmationKey(account.identitySecret, confTime, 'allow'); + community.acceptAllConfirmations(confTime, confKey, allowKey, (err) => { + if (err) { + return reject(err); + } + resolve(); }); }); diff --git a/main/preload.ts b/main/preload.ts index 2a5deda..cbcef22 100644 --- a/main/preload.ts +++ b/main/preload.ts @@ -57,6 +57,9 @@ const handler = { respondToConfirmation: (id: number, key: string, accept: boolean) => { return invoke('respond-to-confirmation', id, key, accept); }, + acceptAllConfirmations: () => { + return invoke('accept-all-confirmations'); + }, events: { onLoginRequired: (callback: () => void) => { diff --git a/main/types.ts b/main/types.ts index 750d5b2..434666d 100644 --- a/main/types.ts +++ b/main/types.ts @@ -101,4 +101,5 @@ export interface IpcHandlers { 'import-mafile': (filePath: string) => Promise; 'get-confirmations': () => Promise; 'respond-to-confirmation': (id: number, key: string, accept: boolean) => Promise; + 'accept-all-confirmations': () => Promise; } diff --git a/renderer/pages/confirmations.tsx b/renderer/pages/confirmations.tsx index 9d00c7c..0792de4 100644 --- a/renderer/pages/confirmations.tsx +++ b/renderer/pages/confirmations.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import { Confirmation } from '../../main/types'; +import Popup from '../components/Popup/Popup'; interface LoadingConfirmations extends Confirmation { isLoading?: boolean; @@ -11,6 +12,8 @@ export default function ConfirmationsPage() { const [confirmations, setConfirmations] = useState([]); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(true); + const [isAcceptingAll, setIsAcceptingAll] = useState(false); + const [showAcceptAllModal, setShowAcceptAllModal] = useState(false); const getConfirmations = () => { setIsLoading(true); @@ -53,16 +56,55 @@ export default function ConfirmationsPage() { }); }; + const handleAcceptAll = async () => { + setShowAcceptAllModal(false); + setIsAcceptingAll(true); + setError(''); + + try { + await window.electron.acceptAllConfirmations(); + // Refresh confirmations after accepting all + getConfirmations(); + } catch (err: Error | unknown) { + setError(err instanceof Error ? err.message : 'An unknown error occurred'); + } finally { + setIsAcceptingAll(false); + } + }; + return ( <> Confirmations - Thunder
    -

    - Confirmations -

    +
    +

    + Confirmations +

    + {!isLoading && confirmations.length > 0 && ( + + )} +
    + {/* TODO: Make error alert reusable */} {error && (
    {error} @@ -82,49 +124,80 @@ export default function ConfirmationsPage() {

    No pending confirmations.

    )} {!isLoading && confirmations.length > 0 && ( -
      - {confirmations.map((conf) => ( -
    • -
      -
      - {conf.title} -
      -

      {conf.title}

      -

      {conf.sending}

      - {conf.receiving &&

      {conf.receiving}

      } -
      -
      -
      -

      {new Date(conf.time).toLocaleString()}

      -
      - - -
      +
      + {isAcceptingAll ? ( + <> +
      +
      +
      +

      Accepting all confirmations...

      -
    • - ))} -
    + + ) : ( +
      + {confirmations.map((conf) => ( +
    • +
      +
      + {conf.title} +
      +

      {conf.title}

      +

      {conf.sending}

      + {conf.receiving &&

      {conf.receiving}

      } +
      +
      +
      +

      {new Date(conf.time).toLocaleString()}

      +
      + + +
      +
      +
      +
    • + ))} +
    + )} + + +
    + )} + + {showAcceptAllModal && ( + setShowAcceptAllModal(false)}> +

    + Are you sure you want to accept all pending confirmations? This action cannot be undone. +

    +
    + +
    +
    )}
    From 3d4690dfe7b5cbae49f513834895974a5e2e19fd Mon Sep 17 00:00:00 2001 From: Zeus <32263615+ZeusJunior@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:43:13 +0100 Subject: [PATCH 5/6] feat: add nav item directly to steam trade offers --- renderer/components/Sidebar.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/renderer/components/Sidebar.tsx b/renderer/components/Sidebar.tsx index cca9a55..e669979 100644 --- a/renderer/components/Sidebar.tsx +++ b/renderer/components/Sidebar.tsx @@ -83,7 +83,7 @@ export default function Sidebar() { Confirmations
  • -
  • +
  • handleOpenSteam('https://steamcommunity.com')} @@ -93,6 +93,16 @@ export default function Sidebar() { Steam
  • +
  • + handleOpenSteam('https://steamcommunity.com/my/tradeoffers')} + className="flex items-center px-3 py-2 rounded-lg hover:bg-gray-800 transition-colors duration-200" + > + + Trade offers + +
  • From 68109cca0573abe5160f6ef8c9ba96ff7083727b Mon Sep 17 00:00:00 2001 From: Zeus <32263615+ZeusJunior@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:46:22 +0100 Subject: [PATCH 6/6] don't upload artefacts on PR, only build --- .github/workflows/build.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e5de2e..3b9f1e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,11 +33,4 @@ jobs: - name: Build application env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: yarn run build - - - name: Upload build artifacts - uses: actions/upload-artifact@v5 - with: - name: build-${{ matrix.os }} - path: dist/ - retention-days: 7 \ No newline at end of file + run: yarn run build \ No newline at end of file