diff --git a/flake.lock b/flake.lock index 5e956a7..ee3b660 100644 --- a/flake.lock +++ b/flake.lock @@ -17,11 +17,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769470494, - "narHash": "sha256-kX/2faovWWNPV7PLq4yTdxOFh9fcJ+TlXAK/HE54WP4=", + "lastModified": 1781568795, + "narHash": "sha256-xDR1Sy/hEf6sPio7ywWCmdgWmY2zbxcGAcsnvv/aR/M=", "owner": "nixos", "repo": "nixpkgs", - "rev": "de6cba5efd5dfdb6e955f2da9ce2fe89ee6325dc", + "rev": "efe01379b2fa1c8f71ba31a9b6710303bfcd9f1c", "type": "github" }, "original": { @@ -32,11 +32,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1765674936, - "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", + "lastModified": 1777168982, + "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", + "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", "type": "github" }, "original": { @@ -50,11 +50,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1768135262, - "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { diff --git a/src/components/PostDialog.tsx b/src/components/PostDialog.tsx index fd3a5e4..fbf5b07 100644 --- a/src/components/PostDialog.tsx +++ b/src/components/PostDialog.tsx @@ -9,7 +9,8 @@ import { Link } from "~/components/ui/link"; import { parseCanonicalResourceUri } from "@atcute/lexicons/syntax"; import { css } from "styled-system/css"; -import { sendPost, UploadStatus } from "~/lib/at"; +import { sendPost, UploadStatus, isSessionExpired } from "~/lib/at"; +import { showSessionExpiredToast } from "~/lib/sessionExpired"; import { toaster } from "~/components/Toaster"; import { Dialog } from "~/components/ui/dialog"; import { Textarea } from "~/components/ui/textarea"; @@ -200,11 +201,15 @@ const PostDialog = (props: { setOpen(false); }) .catch((error) => { - toaster.create({ - title: "send post failed", - description: error, - type: "error", - }); + if (isSessionExpired(error) && props.account?.did) { + showSessionExpiredToast(props.account.did); + } else { + toaster.create({ + title: "send post failed", + description: error, + type: "error", + }); + } }) .finally(() => { setPosting(false); diff --git a/src/lib/at.ts b/src/lib/at.ts index 5074bdb..c4d9afd 100644 --- a/src/lib/at.ts +++ b/src/lib/at.ts @@ -1,10 +1,24 @@ import { XrpcHandleResolver } from "@atcute/identity-resolver"; -import { OAuthUserAgent } from "@atcute/oauth-browser-client"; +import { OAuthUserAgent, TokenRefreshError } from "@atcute/oauth-browser-client"; import { Client as AtcuteClient, simpleFetchHandler } from "@atcute/client"; import { getSessionClient } from "./oauth"; import { AppBskyFeedPost } from "@atcute/bluesky"; import { AtprotoDid } from "@atcute/lexicons/syntax"; +/** Returns true when the error is caused by an expired or revoked session. */ +export const isSessionExpired = (err: unknown): boolean => { + if (err instanceof TokenRefreshError) return true; + if (err instanceof Error) { + const msg = err.message.toLowerCase(); + if (msg.includes("invalid_grant") || msg.includes("invalid_token")) return true; + } + if (typeof err === "string") { + const msg = err.toLowerCase(); + if (msg.includes("invalid_grant") || msg.includes("invalid_token")) return true; + } + return false; +}; + export const slingshotUrl = "https://slingshot.microcosm.blue"; export const handleResolver = new XrpcHandleResolver({ diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index 3f570fd..80c5d16 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -111,3 +111,8 @@ export const getSessionClient = async (did: AtprotoDid) => { if (!session) throw `no session found for ${did}`; return await login(session); }; + +export const relogin = async (did: AtprotoDid): Promise => { + loggingIn.set(did); + await flow.start(did); +}; diff --git a/src/lib/sessionExpired.tsx b/src/lib/sessionExpired.tsx new file mode 100644 index 0000000..1d12aef --- /dev/null +++ b/src/lib/sessionExpired.tsx @@ -0,0 +1,30 @@ +import { HStack } from "styled-system/jsx"; +import { Text } from "~/components/ui/text"; +import { Link } from "~/components/ui/link"; +import { toaster } from "~/components/Toaster"; +import { relogin } from "./oauth"; +import type { AtprotoDid } from "@atcute/lexicons/syntax"; + +/** + * Shows a toast telling the user their session expired, with a clickable + * "re-login" link that re-triggers the OAuth flow for the given account. + */ +export const showSessionExpiredToast = (did: AtprotoDid) => { + toaster.create({ + title: "session expired", + description: ( + + your login session has expired, + relogin(did)} + > + click here to re-login + + + ), + type: "error", + duration: 10_000, + }); +}; diff --git a/src/lib/task.ts b/src/lib/task.ts index 8dce3bb..c0d0432 100644 --- a/src/lib/task.ts +++ b/src/lib/task.ts @@ -17,6 +17,8 @@ import { FastAverageColor } from "fast-average-color"; import { toaster } from "~/components/Toaster"; import { parseColor } from "@ark-ui/solid"; import { transcribe } from "./transcribe"; +import { isSessionExpired } from "./at"; +import { showSessionExpiredToast } from "./sessionExpired"; export type TaskState = { file: File } & ( | { status: "processing" } @@ -102,10 +104,19 @@ export const addTask = async ( }); } catch (error) { console.error(error); - tasks.set(id, { - file, - status: "error", - error: `failed to process audio: ${error}`, - }); + if (isSessionExpired(error) && did) { + showSessionExpiredToast(did); + tasks.set(id, { + file, + status: "error", + error: "session expired, please re-login in settings.", + }); + } else { + tasks.set(id, { + file, + status: "error", + error: `failed to process audio: ${error}`, + }); + } } };