From 54a5adb778bdf8b3c675bd3224b80beff1a6737a Mon Sep 17 00:00:00 2001 From: Miracle Nnaji Date: Sat, 25 Apr 2026 15:03:17 +0100 Subject: [PATCH 1/6] feat(bookmarks): production harden saved bounty system - Replace in-memory bookmark storage with GraphQL-backed persistence - Introduce O(1) Set-based bookmark lookups for UI and notifications - Split bookmark state into ids() and list() caches for consistency - Fix optimistic updates to avoid undefined state and fake bounty objects - Ensure atomic rollback across all bookmark-related caches - Improve notification logic with meaningful-change filtering and unique IDs - Fix authentication UX by removing inconsistent disabled states - Add /bookmarks/ids endpoint for optimized state checks - Remove legacy BookmarkService and unused in-memory logic --- app/api/bookmarks/[bountyId]/route.ts | 99 +++ app/api/bookmarks/ids/route.ts | 42 + app/api/bookmarks/route.ts | 80 ++ app/saved/page.tsx | 13 + app/saved/saved-client.tsx | 77 ++ codegen.ts | 6 + .../bounty-detail-header-card.tsx | 8 +- components/bounty/bookmark-button.tsx | 121 +++ components/bounty/bounty-card.tsx | 8 +- components/ui/stellar-link.tsx | 2 +- hooks/use-bookmarks.ts | 121 +++ hooks/use-notifications.ts | 69 +- lib/graphql/client.ts | 75 +- lib/graphql/generated.ts | 793 ++++++++++++++++-- .../operations/bookmark-operations.graphql | 23 + lib/graphql/schema.graphql | 20 +- lib/query/query-keys.ts | 14 + lib/server-graphql.ts | 39 + package-lock.json | 151 +--- package.json | 1 + 20 files changed, 1556 insertions(+), 206 deletions(-) create mode 100644 app/api/bookmarks/[bountyId]/route.ts create mode 100644 app/api/bookmarks/ids/route.ts create mode 100644 app/api/bookmarks/route.ts create mode 100644 app/saved/page.tsx create mode 100644 app/saved/saved-client.tsx create mode 100644 components/bounty/bookmark-button.tsx create mode 100644 hooks/use-bookmarks.ts create mode 100644 lib/graphql/operations/bookmark-operations.graphql create mode 100644 lib/server-graphql.ts diff --git a/app/api/bookmarks/[bountyId]/route.ts b/app/api/bookmarks/[bountyId]/route.ts new file mode 100644 index 00000000..7c617554 --- /dev/null +++ b/app/api/bookmarks/[bountyId]/route.ts @@ -0,0 +1,99 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/server-auth"; +import { graphqlRequest } from "@/lib/server-graphql"; +import type { Bookmark } from "@/lib/graphql/generated"; + +/** + * POST /api/bookmarks/[bountyId] + * Toggles bookmark state for the given bounty + * If bookmarked → removes and returns null + * If not bookmarked → adds and returns bookmark + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ bountyId: string }> }, +) { + try { + const user = await getCurrentUser(); + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { bountyId } = await params; + + if (!bountyId) { + return NextResponse.json( + { error: "bountyId is required" }, + { status: 400 }, + ); + } + + // Call GraphQL mutation to toggle bookmark + const TOGGLE_BOOKMARK_MUTATION = ` + mutation ToggleBookmark($input: ToggleBookmarkInput!) { + toggleBookmark(input: $input) { + id + userId + bountyId + createdAt + bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } + } + } + } + `; + + const data = await graphqlRequest<{ toggleBookmark: Bookmark | null }>( + TOGGLE_BOOKMARK_MUTATION, + { input: { bountyId } }, + ); + + // GraphQL mutation returns the bookmark when added, null when removed + if (data.toggleBookmark) { + return NextResponse.json(data.toggleBookmark); + } else { + return NextResponse.json(null, { status: 200 }); + } + } catch (error: unknown) { + console.error("Error toggling bookmark:", error); + const message = + error instanceof Error ? error.message : "Failed to toggle bookmark"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/app/api/bookmarks/ids/route.ts b/app/api/bookmarks/ids/route.ts new file mode 100644 index 00000000..e7ff5f28 --- /dev/null +++ b/app/api/bookmarks/ids/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/server-auth"; +import { graphqlRequest } from "@/lib/server-graphql"; + +/** + * GET /api/bookmarks/ids + * Returns an array of bookmarked bounty IDs for the current user + * Optimized for O(1) bookmark existence checks + */ +export async function GET() { + try { + const user = await getCurrentUser(); + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Query GraphQL for bookmarks - we only need the IDs + const BOOKMARKS_QUERY = ` + query GetBookmarkIds { + bookmarks { + bountyId + } + } + `; + + const data = await graphqlRequest<{ + bookmarks: Array<{ bountyId: string }>; + }>(BOOKMARKS_QUERY); + + // Extract just the bounty IDs as a simple array + const bountyIds = data.bookmarks.map((b) => b.bountyId); + + return NextResponse.json(bountyIds); + } catch (error) { + console.error("Error fetching bookmark IDs:", error); + return NextResponse.json( + { error: "Failed to fetch bookmark IDs" }, + { status: 500 }, + ); + } +} diff --git a/app/api/bookmarks/route.ts b/app/api/bookmarks/route.ts new file mode 100644 index 00000000..f795a16d --- /dev/null +++ b/app/api/bookmarks/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/server-auth"; +import { graphqlRequest } from "@/lib/server-graphql"; +import type { Bookmark } from "@/lib/graphql/generated"; + +/** + * GET /api/bookmarks + * Returns all bookmarked bounties for the current user + */ +export async function GET() { + try { + const user = await getCurrentUser(); + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Query GraphQL for bookmarks with full bounty details + const BOOKMARKS_QUERY = ` + query GetBookmarks { + bookmarks { + id + userId + bountyId + createdAt + bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } + } + } + } + `; + + const data = await graphqlRequest<{ bookmarks: Bookmark[] }>( + BOOKMARKS_QUERY, + ); + + return NextResponse.json(data.bookmarks); + } catch (error) { + console.error("Error fetching bookmarks:", error); + return NextResponse.json( + { error: "Failed to fetch bookmarks" }, + { status: 500 }, + ); + } +} diff --git a/app/saved/page.tsx b/app/saved/page.tsx new file mode 100644 index 00000000..16f0927b --- /dev/null +++ b/app/saved/page.tsx @@ -0,0 +1,13 @@ +import { getCurrentUser } from "@/lib/server-auth"; +import { redirect } from "next/navigation"; +import SavedBountiesClient from "./saved-client"; + +export default async function SavedPage() { + const user = await getCurrentUser(); + + if (!user) { + redirect("/auth"); + } + + return ; +} diff --git a/app/saved/saved-client.tsx b/app/saved/saved-client.tsx new file mode 100644 index 00000000..62e33555 --- /dev/null +++ b/app/saved/saved-client.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useBookmarks } from "@/hooks/use-bookmarks"; +import { BountyCard } from "@/components/bounty/bounty-card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Bookmark } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import type { + Bookmark as BookmarkType, + Bounty as BountyType, +} from "@/lib/graphql/generated"; + +function SavedBountiesClient() { + const { data: bookmarks, isLoading, error } = useBookmarks(); + + if (isLoading) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ +
+ ))} +
+ ); + } + + if (error) { + return ( +
+

+ Failed to load saved bounties. Please try again. +

+ +
+ ); + } + + // Filter out any bookmarks with null bounty (defensive check) + const bookmarkedBounties = (bookmarks ?? []) + .filter((b): b is BookmarkType => b.bounty !== null) + .map((b) => b.bounty as BountyType); + + if (bookmarkedBounties.length === 0) { + return ( +
+
+ +
+

No saved bounties yet

+

+ Bookmark interesting bounties to save them here for later review. +

+ +
+ ); + } + + return ( +
+ {bookmarkedBounties.map((bounty) => ( + { + window.location.href = `/bounty/${bounty.id}`; + }} + /> + ))} +
+ ); +} + +export default SavedBountiesClient; diff --git a/codegen.ts b/codegen.ts index d387693e..851a6d0a 100644 --- a/codegen.ts +++ b/codegen.ts @@ -13,6 +13,11 @@ const config: CodegenConfig = { generates: { "./lib/graphql/generated.ts": { plugins: [ + { + add: { + content: "import { gql } from 'graphql-tag';", + }, + }, "typescript", "typescript-operations", "typescript-react-query", @@ -24,6 +29,7 @@ const config: CodegenConfig = { }, exposeQueryKeys: true, reactQueryVersion: 5, + documentMode: "graphQLTag", scalars: { DateTime: "string", JSON: "Record", diff --git a/components/bounty-detail/bounty-detail-header-card.tsx b/components/bounty-detail/bounty-detail-header-card.tsx index 9aaec8d9..28235a55 100644 --- a/components/bounty-detail/bounty-detail-header-card.tsx +++ b/components/bounty-detail/bounty-detail-header-card.tsx @@ -2,13 +2,19 @@ import { ExternalLink, GitBranch } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { BountyFieldsFragment } from "@/lib/graphql/generated"; import { StatusBadge, TypeBadge } from "./bounty-badges"; +import { BookmarkButton } from "@/components/bounty/bookmark-button"; export function HeaderCard({ bounty }: { bounty: BountyFieldsFragment }) { const orgName = bounty.organization?.name ?? "Unknown"; const orgLogo = bounty.organization?.logo; return ( -
+
+ {/* Bookmark button - top right corner */} +
+ +
+ {/* Badges */}
diff --git a/components/bounty/bookmark-button.tsx b/components/bounty/bookmark-button.tsx new file mode 100644 index 00000000..66bcbdcf --- /dev/null +++ b/components/bounty/bookmark-button.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useMemo } from "react"; +import { Bookmark, BookmarkCheck } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { toast } from "sonner"; +import { useBookmarkIds, useToggleBookmark } from "@/hooks/use-bookmarks"; +import { authClient } from "@/lib/auth-client"; + +interface BookmarkButtonProps { + bountyId: string; + size?: "sm" | "md" | "lg" | "icon"; + className?: string; + showLabel?: boolean; +} + +/** + * BookmarkButton component for toggling bookmark state on a bounty. + * + * Features: + * - Instant visual feedback (filled/outline icon) + * - Optimistic UI updates via React Query + * - Accessible: aria-pressed, keyboard operable + * - Handles loading state and errors + * - Shows disabled state with login tooltip for unauthenticated users + * + * @param bountyId - The bounty ID to toggle bookmark for + * @param size - Button size (default: "md") + * @param className - Additional CSS classes + * @param showLabel - Whether to show "Save" / "Saved" label (default: false, icon only) + */ +export function BookmarkButton({ + bountyId, + size = "md", + className, + showLabel = false, +}: BookmarkButtonProps) { + const { data: session } = authClient.useSession(); + const { data: bookmarkedIds } = useBookmarkIds(); + const toggleMutation = useToggleBookmark(); + + const isAuthenticated = Boolean(session?.user); + const bookmarkedIdsSet = useMemo(() => { + return new Set(bookmarkedIds ?? []); + }, [bookmarkedIds]); + + const isBookmarked = useMemo(() => { + return bookmarkedIdsSet.has(bountyId); + }, [bookmarkedIdsSet, bountyId]); + + const handleToggle = async (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent card click + + if (!isAuthenticated) { + toast.error("Please log in to save bounties"); + return; + } + + try { + await toggleMutation.mutateAsync(bountyId); + } catch { + // Error handled by mutation hook + } + }; + + const isLoading = toggleMutation.isPending; + + const iconSize = size === "sm" ? 16 : size === "lg" ? 24 : 20; + const buttonSize = size === "md" ? "default" : size; + + const button = ( + + ); + + // If not authenticated, wrap in tooltip to prompt login + if (!isAuthenticated) { + return ( + + {button} + +

Log in to save bounties

+
+
+ ); + } + + return button; +} diff --git a/components/bounty/bounty-card.tsx b/components/bounty/bounty-card.tsx index c00230c4..a07ba4ce 100644 --- a/components/bounty/bounty-card.tsx +++ b/components/bounty/bounty-card.tsx @@ -15,6 +15,7 @@ import { formatDistanceToNow } from "date-fns"; import { BountyFieldsFragment } from "@/lib/graphql/generated"; import { EscrowStatus } from "./escrow-status"; import { useEscrowPool } from "@/hooks/use-escrow"; +import { BookmarkButton } from "./bookmark-button"; interface BountyCardProps { bounty: BountyFieldsFragment; @@ -103,7 +104,7 @@ export function BountyCard({ + {/* Bookmark button - top-right corner */} +
+ +
+
diff --git a/components/ui/stellar-link.tsx b/components/ui/stellar-link.tsx index 015e587b..f54df329 100644 --- a/components/ui/stellar-link.tsx +++ b/components/ui/stellar-link.tsx @@ -132,7 +132,7 @@ export function StellarLink({ const prefix = tooltipPrefix || `View ${type}`; const networkText = network || getStellarNetwork(); return `${prefix} on ${networkText} • ${explorer}`; - }, [type, network, explorer, isValid, tooltipPrefix, value]); + }, [type, network, explorer, isValid, tooltipPrefix]); if (!value || !isValid) { return ( diff --git a/hooks/use-bookmarks.ts b/hooks/use-bookmarks.ts new file mode 100644 index 00000000..22a8d849 --- /dev/null +++ b/hooks/use-bookmarks.ts @@ -0,0 +1,121 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { bookmarkKeys } from "@/lib/query/query-keys"; +import { get, post } from "@/lib/api/client"; +import { toast } from "sonner"; +import type { Bookmark } from "@/lib/graphql/generated"; + +const BOOKMARKS_ENDPOINT = "/api/bookmarks"; +const BOOKMARKS_IDS_ENDPOINT = "/api/bookmarks/ids"; + +async function fetchBookmarks(): Promise { + return get(BOOKMARKS_ENDPOINT); +} + +async function fetchBookmarkIds(): Promise { + return get(BOOKMARKS_IDS_ENDPOINT); +} + +/** + * Hook to fetch and manage bookmarked bounties + */ +export function useBookmarks() { + return useQuery({ + queryKey: bookmarkKeys.list(), + queryFn: fetchBookmarks, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +} + +/** + * Hook to fetch just the bookmarked bounty IDs. + * Use this for O(1) bookmark state checks (backed by Set). + */ +export function useBookmarkIds() { + return useQuery({ + queryKey: bookmarkKeys.ids(), + queryFn: fetchBookmarkIds, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +} + +/** + * Hook to toggle bookmark status for a bounty + * + * Optimistic update strategy: + * - IDs cache is the source of truth for UI state + * - List cache is derived and kept in sync when possible + * - Both caches are rolled back atomically on error + * - No fake bounty objects ever constructed + * + * All membership checks use Set for O(1) performance. + */ +export function useToggleBookmark() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (bountyId: string) => { + const response = await post( + `${BOOKMARKS_ENDPOINT}/${bountyId}`, + {}, + ); + return response; + }, + onMutate: async (bountyId: string) => { + await Promise.all([ + queryClient.cancelQueries({ queryKey: bookmarkKeys.list() }), + queryClient.cancelQueries({ queryKey: bookmarkKeys.ids() }), + ]); + + // Snapshot with safe fallbacks + const previousList = + queryClient.getQueryData(bookmarkKeys.list()) ?? []; + const previousIds = + queryClient.getQueryData(bookmarkKeys.ids()) ?? []; + + // Build Sets for O(1) membership checks + const prevIdsSet = new Set(previousIds); + const existsInIds = prevIdsSet.has(bountyId); + + // Check if real bookmark data exists in list cache + const existsInList = previousList.some((b) => b.bountyId === bountyId); + + // OPTIMISTIC UPDATE: IDs cache is authoritative + const newIds = existsInIds + ? previousIds.filter((id) => id !== bountyId) + : [bountyId, ...previousIds]; + + queryClient.setQueryData(bookmarkKeys.ids(), newIds); + + // Update list cache derivatively, only if we have real data + if (existsInList) { + const newList = previousList.filter((b) => b.bountyId !== bountyId); + queryClient.setQueryData(bookmarkKeys.list(), newList); + } + // If adding a new bookmark without cached bounty data, skip list mutation + // Server response will populate it after settlement. + + return { previousList, previousIds }; + }, + onError: (err, bountyId, context) => { + if (context) { + queryClient.setQueryData(bookmarkKeys.list(), context.previousList); + queryClient.setQueryData(bookmarkKeys.ids(), context.previousIds); + } + toast.error(`Failed to bookmark: ${err.message}`); + }, + onSettled: () => { + Promise.all([ + queryClient.invalidateQueries({ queryKey: bookmarkKeys.list() }), + queryClient.invalidateQueries({ queryKey: bookmarkKeys.ids() }), + ]); + }, + onSuccess: (_result, variables) => { + // Source of truth: check current IDs cache using Set for O(1) + const currentIds = + queryClient.getQueryData(bookmarkKeys.ids()) ?? []; + const currentIdsSet = new Set(currentIds); + const isBookmarked = currentIdsSet.has(variables as string); + toast.success(isBookmarked ? "Bounty bookmarked!" : "Bookmark removed"); + }, + }); +} diff --git a/hooks/use-notifications.ts b/hooks/use-notifications.ts index 48e7b917..9ec2c90a 100644 --- a/hooks/use-notifications.ts +++ b/hooks/use-notifications.ts @@ -1,7 +1,5 @@ -"use client"; - import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useQueryClient } from "@tanstack/react-query"; +import { useQueryClient, type QueryClient } from "@tanstack/react-query"; import { authClient } from "@/lib/auth-client"; import { @@ -12,14 +10,20 @@ import { type OnNewApplicationData, type OnSubmissionReviewedData, } from "@/lib/graphql/subscriptions"; -import { bountyKeys, submissionKeys } from "@/lib/query/query-keys"; +import { + bountyKeys, + submissionKeys, + bookmarkKeys, +} from "@/lib/query/query-keys"; +import type { Bookmark } from "@/lib/graphql/generated"; import { useGraphQLSubscription } from "./use-graphql-subscription"; export type NotificationType = | "bounty-updated" | "new-application" - | "submission-reviewed"; + | "submission-reviewed" + | "saved-bounty-updated"; export interface NotificationItem { id: string; @@ -83,6 +87,24 @@ function saveToStorage(userId: string, items: NotificationItem[]): void { } } +/** + * Get a Map of bountyId -> status for currently bookmarked bounties. + * Uses Set for O(1) membership filtering. + */ +function getBookmarkedStatusMap(queryClient: QueryClient): Map { + const ids = queryClient.getQueryData(bookmarkKeys.ids()) ?? []; + const idsSet = new Set(ids); + const list = queryClient.getQueryData(bookmarkKeys.list()) ?? []; + + const statusMap = new Map(); + for (const b of list) { + if (b.bounty && idsSet.has(b.bountyId)) { + statusMap.set(b.bountyId, b.bounty.status); + } + } + return statusMap; +} + export function useNotifications() { const { data: session } = authClient.useSession(); const queryClient = useQueryClient(); @@ -90,29 +112,23 @@ export function useNotifications() { const isEnabled = Boolean(session?.user); const userId = session?.user?.id ?? null; - // Initialize state with lazy loading from localStorage - // This runs only once during initial render, avoiding setState in effect const [notifications, setNotifications] = useState(() => { if (typeof window === "undefined" || !userId) return []; return loadFromStorage(userId); }); - // Reset on user change - this is allowed during render as it's a state update - // based on a condition change (userId) const prevHydratedUserIdRef = useRef(userId); if (prevHydratedUserIdRef.current !== userId) { prevHydratedUserIdRef.current = userId; setNotifications(userId ? loadFromStorage(userId) : []); } - // Persist to localStorage whenever notifications change useEffect(() => { if (userId) { saveToStorage(userId, notifications); } }, [notifications, userId]); - // Helper to update notifications with cache invalidation const addNotification = useCallback( ( item: NotificationItem, @@ -148,6 +164,37 @@ export function useNotifications() { bountyKeys.allListKeys, ); + // Check bookmark membership using ONLY ids cache (O(1) via Set) + const bookmarkedIds = queryClient.getQueryData( + bookmarkKeys.ids(), + ); + const bookmarkedIdsSet = new Set(bookmarkedIds ?? []); + const isBookmarked = bookmarkedIdsSet.has(bounty.id); + + if (isBookmarked) { + // Get previous status from status map (derived from list cache) + const statusMap = getBookmarkedStatusMap(queryClient); + const previousStatus = statusMap.get(bounty.id); + + // Only notify if status changed (meaningful update) + if ( + previousStatus === undefined || + previousStatus !== bounty.status + ) { + const timestamp = Date.now(); + addNotification( + { + id: `saved-bounty-updated-${bounty.id}-${timestamp}`, + message: `Saved bounty "${bounty.title}" was updated.`, + type: "saved-bounty-updated", + timestamp: normaliseTimestamp(bounty.updatedAt), + read: false, + }, + [], + ); + } + } + queryClient.invalidateQueries({ queryKey: bountyKeys.detail(bounty.id), }); diff --git a/lib/graphql/client.ts b/lib/graphql/client.ts index 762c7a4a..77cd5c0b 100644 --- a/lib/graphql/client.ts +++ b/lib/graphql/client.ts @@ -1,11 +1,23 @@ "use client"; -import { GraphQLClient } from "graphql-request"; +import { + GraphQLClient, + type RequestDocument, + type RequestOptions, + type Variables, +} from "graphql-request"; import { isAuthStatus } from "./errors"; import { toast } from "sonner"; import { getAccessToken } from "../auth-utils"; +import { TypedDocumentString } from "@/lib/graphql/generated"; -// Re-export all error utilities from errors.ts for convenience +export type GraphQLRequestDocument = + | RequestDocument + | TypedDocumentString>; + +export type GraphQLRequestHeaders = Parameters[0]; + +// Re-export all error utilities export { AppGraphQLError, ApiError, @@ -25,54 +37,75 @@ export async function hasAccessToken(): Promise { return token !== null; } -// Create the generic GraphQLClient instance +// Create GraphQL client const url = process.env.NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql"; + export const graphQLClient = new GraphQLClient(url, { credentials: "include", }); -// A custom fetcher for @graphql-codegen/typescript-react-query -export const fetcher = < - TData, - TVariables extends object = Record, ->( - query: string, +/** + * Normalize any GraphQL document into a valid RequestDocument + */ +function normalizeDocument(query: GraphQLRequestDocument): RequestDocument { + if (typeof query === "string") return query; + + // TypedDocumentString behaves like string + return query.toString(); +} + +/** + * Custom fetcher for react-query + graphql-request + */ +export const fetcher = ( + query: GraphQLRequestDocument, variables?: TVariables, ) => { return async (): Promise => { const token = await getAccessToken(); - const headers: Record = {}; + + const headers: GraphQLRequestHeaders = {}; if (token) { headers.authorization = `Bearer ${token}`; } + const requestDocument = normalizeDocument(query); + try { - return await ( - graphQLClient.request as unknown as ( - q: string, - v?: TVariables, - h?: Record, - ) => Promise - )(query, variables, headers); + // Build request options and assert to RequestOptions to satisfy + // graphql-request's complex conditional types + const requestOptions = { + document: requestDocument, + variables: variables ?? ({} as TVariables), + requestHeaders: headers, + } as unknown as RequestOptions; + + return await graphQLClient.request(requestOptions); } catch (error: unknown) { - // Global error handling for auth failures + // Global auth error handling const gqlError = error as { - response?: { errors?: Array<{ extensions?: { status?: number } }> }; + response?: { + errors?: Array<{ extensions?: { status?: number } }>; + }; }; + if (gqlError?.response?.errors) { gqlError.response.errors.forEach((err) => { const status = err?.extensions?.status ?? 500; + if (isAuthStatus(status)) { - // Let the application handle unauthorized state, potentially redirecting to login if (typeof window !== "undefined") { toast.error("Your session has expired. Please log in again."); window.dispatchEvent( - new CustomEvent("auth:unauthorized", { detail: { status } }), + new CustomEvent("auth:unauthorized", { + detail: { status }, + }), ); } } }); } + throw error; } }; diff --git a/lib/graphql/generated.ts b/lib/graphql/generated.ts index da7d0be8..8cede818 100644 --- a/lib/graphql/generated.ts +++ b/lib/graphql/generated.ts @@ -1,10 +1,32 @@ import { - useMutation, useQuery, - UseMutationOptions, + useMutation, UseQueryOptions, + UseMutationOptions, } from "@tanstack/react-query"; import { fetcher } from "./client"; + +/** + * TypedDocumentString is a runtime class used by generated GraphQL documents. + * It extends String to hold the GraphQL query string and carries type metadata. + */ +export class TypedDocumentString< + TResult = unknown, + TVariables extends object = Record, +> extends String { + __apiType?: ((variables: TVariables) => TResult) | undefined; + private value: string; + __meta__?: Record | undefined; + constructor(value: string, __meta__?: Record | undefined) { + super(value); + this.value = value; + this.__meta__ = __meta__; + } + override toString(): string { + return this.value; + } +} + export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { @@ -661,6 +683,15 @@ export enum BlogPostStatus { Scheduled = "SCHEDULED", } +export type Bookmark = { + __typename?: "Bookmark"; + bounty: Bounty; + bountyId: Scalars["String"]["output"]; + createdAt: Scalars["DateTime"]["output"]; + id: Scalars["ID"]["output"]; + userId: Scalars["String"]["output"]; +}; + export type Bounty = { __typename?: "Bounty"; _count?: Maybe; @@ -1003,6 +1034,8 @@ export type Mutation = { reviewSubmission: BountySubmissionType; /** Submit to a bounty (any authenticated user) */ submitToBounty: BountySubmissionType; + /** Toggle a bookmark on a bounty (authenticated users) */ + toggleBookmark: Bookmark; /** Update a blog post (Admin only) */ updateAdminBlogPost: AdminBlogPostDto; /** Update an existing bounty (organization members only) */ @@ -1139,6 +1172,10 @@ export type MutationSubmitToBountyArgs = { input: CreateSubmissionInput; }; +export type MutationToggleBookmarkArgs = { + input: ToggleBookmarkInput; +}; + export type MutationUpdateAdminBlogPostArgs = { id: Scalars["ID"]["input"]; input: UpdateBlogPostDto; @@ -1203,6 +1240,8 @@ export type Query = { adminUserCrowdfundingCampaigns: AdminCrowdfundingResponseDto; /** Get all users with pagination and filtering */ adminUsers: AdminUsersResponseDto; + /** Get all bookmarked bounties for the current user */ + bookmarks: Array; /** Get paginated list of bounties with filtering */ bounties: PaginatedBounties; /** Get a single bounty by ID */ @@ -1305,7 +1344,6 @@ export type QueryAdminUsersArgs = { isActive?: InputMaybe; limit?: InputMaybe; page?: InputMaybe; - role?: InputMaybe; search?: InputMaybe; }; @@ -1368,6 +1406,10 @@ export enum RewardDistributionStatus { Rejected = "REJECTED", } +export type ToggleBookmarkInput = { + bountyId: Scalars["ID"]["input"]; +}; + export type UpdateBlogPostDto = { categories?: InputMaybe>; content?: InputMaybe; @@ -1409,6 +1451,114 @@ export type UserLeaderboardRankResponse = { rank: Scalars["Int"]["output"]; }; +export type BookmarksQueryVariables = Exact<{ [key: string]: never }>; + +export type BookmarksQuery = { + __typename?: "Query"; + bookmarks: Array<{ + __typename?: "Bookmark"; + id: string; + userId: string; + bountyId: string; + createdAt: string; + bounty: { + __typename?: "Bounty"; + id: string; + title: string; + description: string; + status: string; + type: string; + rewardAmount: number; + rewardCurrency: string; + createdAt: string; + updatedAt: string; + organizationId: string; + projectId?: string | null; + bountyWindowId?: string | null; + githubIssueUrl: string; + githubIssueNumber?: number | null; + createdBy: string; + organization?: { + __typename?: "BountyOrganization"; + id: string; + name: string; + logo?: string | null; + slug?: string | null; + } | null; + project?: { + __typename?: "BountyProject"; + id: string; + title: string; + description?: string | null; + } | null; + bountyWindow?: { + __typename?: "BountyWindowType"; + id: string; + name: string; + status: string; + startDate?: string | null; + endDate?: string | null; + } | null; + _count?: { __typename?: "BountyCount"; submissions: number } | null; + }; + }>; +}; + +export type ToggleBookmarkMutationVariables = Exact<{ + input: ToggleBookmarkInput; +}>; + +export type ToggleBookmarkMutation = { + __typename?: "Mutation"; + toggleBookmark: { + __typename?: "Bookmark"; + id: string; + userId: string; + bountyId: string; + createdAt: string; + bounty: { + __typename?: "Bounty"; + id: string; + title: string; + description: string; + status: string; + type: string; + rewardAmount: number; + rewardCurrency: string; + createdAt: string; + updatedAt: string; + organizationId: string; + projectId?: string | null; + bountyWindowId?: string | null; + githubIssueUrl: string; + githubIssueNumber?: number | null; + createdBy: string; + organization?: { + __typename?: "BountyOrganization"; + id: string; + name: string; + logo?: string | null; + slug?: string | null; + } | null; + project?: { + __typename?: "BountyProject"; + id: string; + title: string; + description?: string | null; + } | null; + bountyWindow?: { + __typename?: "BountyWindowType"; + id: string; + name: string; + status: string; + startDate?: string | null; + endDate?: string | null; + } | null; + _count?: { __typename?: "BountyCount"; submissions: number } | null; + }; + }; +}; + export type CreateBountyMutationVariables = Exact<{ input: CreateBountyInput; }>; @@ -2103,7 +2253,8 @@ export type MarkSubmissionPaidMutation = { }; }; -export const BountyFieldsFragmentDoc = ` +export const BountyFieldsFragmentDoc = new TypedDocumentString( + ` fragment BountyFields on Bounty { id title @@ -2142,8 +2293,11 @@ export const BountyFieldsFragmentDoc = ` submissions } } - `; -export const SubmissionFieldsFragmentDoc = ` + `, + { fragmentName: "BountyFields" }, +); +export const SubmissionFieldsFragmentDoc = new TypedDocumentString( + ` fragment SubmissionFields on BountySubmissionType { id bountyId @@ -2168,8 +2322,11 @@ export const SubmissionFieldsFragmentDoc = ` paidAt rewardTransactionHash } - `; -export const SubmissionFieldsWithContactFragmentDoc = ` + `, + { fragmentName: "SubmissionFields" }, +); +export const SubmissionFieldsWithContactFragmentDoc = new TypedDocumentString( + ` fragment SubmissionFieldsWithContact on BountySubmissionType { ...SubmissionFields submittedByUser { @@ -2179,51 +2336,296 @@ export const SubmissionFieldsWithContactFragmentDoc = ` email } } - ${SubmissionFieldsFragmentDoc}`; -export const CreateBountyDocument = ` - mutation CreateBounty($input: CreateBountyInput!) { - createBounty(input: $input) { - ...BountyFields + fragment SubmissionFields on BountySubmissionType { + id + bountyId + submittedBy + submittedByUser { + id + name + image + } + githubPullRequestUrl + status + createdAt + updatedAt + reviewedAt + reviewedBy + reviewedByUser { + id + name + image + } + reviewComments + paidAt + rewardTransactionHash +}`, + { fragmentName: "SubmissionFieldsWithContact" }, +); +export const BookmarksDocument = new TypedDocumentString(` + query Bookmarks { + bookmarks { + id + userId + bountyId + createdAt + bounty { + ...BountyFields + } } } - ${BountyFieldsFragmentDoc}`; + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); -export const useCreateBountyMutation = ( +export const useBookmarksQuery = ( + variables?: BookmarksQueryVariables, + options?: Omit, "queryKey"> & { + queryKey?: UseQueryOptions["queryKey"]; + }, +) => { + return useQuery({ + queryKey: + variables === undefined ? ["Bookmarks"] : ["Bookmarks", variables], + queryFn: fetcher( + BookmarksDocument, + variables, + ), + ...options, + }); +}; + +useBookmarksQuery.getKey = (variables?: BookmarksQueryVariables) => + variables === undefined ? ["Bookmarks"] : ["Bookmarks", variables]; + +export const ToggleBookmarkDocument = new TypedDocumentString(` + mutation ToggleBookmark($input: ToggleBookmarkInput!) { + toggleBookmark(input: $input) { + id + userId + bountyId + createdAt + bounty { + ...BountyFields + } + } +} + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); + +export const useToggleBookmarkMutation = ( options?: UseMutationOptions< - CreateBountyMutation, + ToggleBookmarkMutation, TError, - CreateBountyMutationVariables, + ToggleBookmarkMutationVariables, TContext >, ) => { return useMutation< - CreateBountyMutation, + ToggleBookmarkMutation, TError, - CreateBountyMutationVariables, + ToggleBookmarkMutationVariables, TContext >({ - mutationKey: ["CreateBounty"], - mutationFn: (variables?: CreateBountyMutationVariables) => - fetcher( - CreateBountyDocument, + mutationKey: ["ToggleBookmark"], + mutationFn: (variables?: ToggleBookmarkMutationVariables) => + fetcher( + ToggleBookmarkDocument, variables, )(), ...options, }); }; -export const UpdateBountyDocument = ` - mutation UpdateBounty($input: UpdateBountyInput!) { - updateBounty(input: $input) { +export const CreateBountyDocument = new TypedDocumentString(` + mutation CreateBounty($input: CreateBountyInput!) { + createBounty(input: $input) { ...BountyFields } } - ${BountyFieldsFragmentDoc}`; - -export const useUpdateBountyMutation = ( - options?: UseMutationOptions< - UpdateBountyMutation, - TError, + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); + +export const useCreateBountyMutation = ( + options?: UseMutationOptions< + CreateBountyMutation, + TError, + CreateBountyMutationVariables, + TContext + >, +) => { + return useMutation< + CreateBountyMutation, + TError, + CreateBountyMutationVariables, + TContext + >({ + mutationKey: ["CreateBounty"], + mutationFn: (variables?: CreateBountyMutationVariables) => + fetcher( + CreateBountyDocument, + variables, + )(), + ...options, + }); +}; + +export const UpdateBountyDocument = new TypedDocumentString(` + mutation UpdateBounty($input: UpdateBountyInput!) { + updateBounty(input: $input) { + ...BountyFields + } +} + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); + +export const useUpdateBountyMutation = ( + options?: UseMutationOptions< + UpdateBountyMutation, + TError, UpdateBountyMutationVariables, TContext >, @@ -2244,11 +2646,11 @@ export const useUpdateBountyMutation = ( }); }; -export const DeleteBountyDocument = ` +export const DeleteBountyDocument = new TypedDocumentString(` mutation DeleteBounty($id: ID!) { deleteBounty(id: $id) } - `; + `); export const useDeleteBountyMutation = ( options?: UseMutationOptions< @@ -2274,7 +2676,7 @@ export const useDeleteBountyMutation = ( }); }; -export const BountiesDocument = ` +export const BountiesDocument = new TypedDocumentString(` query Bounties($query: BountyQueryInput) { bounties(query: $query) { bounties { @@ -2285,7 +2687,44 @@ export const BountiesDocument = ` offset } } - ${BountyFieldsFragmentDoc}`; + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); export const useBountiesQuery = ( variables?: BountiesQueryVariables, @@ -2306,7 +2745,7 @@ export const useBountiesQuery = ( useBountiesQuery.getKey = (variables?: BountiesQueryVariables) => variables === undefined ? ["Bounties"] : ["Bounties", variables]; -export const BountyDocument = ` +export const BountyDocument = new TypedDocumentString(` query Bounty($id: ID!) { bounty(id: $id) { ...BountyFields @@ -2315,8 +2754,68 @@ export const BountyDocument = ` } } } - ${BountyFieldsFragmentDoc} -${SubmissionFieldsFragmentDoc}`; + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +} +fragment SubmissionFields on BountySubmissionType { + id + bountyId + submittedBy + submittedByUser { + id + name + image + } + githubPullRequestUrl + status + createdAt + updatedAt + reviewedAt + reviewedBy + reviewedByUser { + id + name + image + } + reviewComments + paidAt + rewardTransactionHash +}`); export const useBountyQuery = ( variables: BountyQueryVariables, @@ -2339,13 +2838,50 @@ useBountyQuery.getKey = (variables: BountyQueryVariables) => [ variables, ]; -export const ActiveBountiesDocument = ` +export const ActiveBountiesDocument = new TypedDocumentString(` query ActiveBounties { activeBounties { ...BountyFields } } - ${BountyFieldsFragmentDoc}`; + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); export const useActiveBountiesQuery = < TData = ActiveBountiesQuery, @@ -2375,13 +2911,50 @@ export const useActiveBountiesQuery = < useActiveBountiesQuery.getKey = (variables?: ActiveBountiesQueryVariables) => variables === undefined ? ["ActiveBounties"] : ["ActiveBounties", variables]; -export const OrganizationBountiesDocument = ` +export const OrganizationBountiesDocument = new TypedDocumentString(` query OrganizationBounties($organizationId: ID!) { organizationBounties(organizationId: $organizationId) { ...BountyFields } } - ${BountyFieldsFragmentDoc}`; + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); export const useOrganizationBountiesQuery = < TData = OrganizationBountiesQuery, @@ -2413,13 +2986,50 @@ useOrganizationBountiesQuery.getKey = ( variables: OrganizationBountiesQueryVariables, ) => ["OrganizationBounties", variables]; -export const ProjectBountiesDocument = ` +export const ProjectBountiesDocument = new TypedDocumentString(` query ProjectBounties($projectId: ID!) { projectBounties(projectId: $projectId) { ...BountyFields } } - ${BountyFieldsFragmentDoc}`; + fragment BountyFields on Bounty { + id + title + description + status + type + rewardAmount + rewardCurrency + createdAt + updatedAt + organizationId + projectId + bountyWindowId + githubIssueUrl + githubIssueNumber + createdBy + organization { + id + name + logo + slug + } + project { + id + title + description + } + bountyWindow { + id + name + status + startDate + endDate + } + _count { + submissions + } +}`); export const useProjectBountiesQuery = < TData = ProjectBountiesQuery, @@ -2448,7 +3058,7 @@ useProjectBountiesQuery.getKey = (variables: ProjectBountiesQueryVariables) => [ variables, ]; -export const LeaderboardDocument = ` +export const LeaderboardDocument = new TypedDocumentString(` query Leaderboard($filters: LeaderboardFilters!, $pagination: LeaderboardPagination!) { leaderboard(filters: $filters, pagination: $pagination) { entries { @@ -2483,7 +3093,7 @@ export const LeaderboardDocument = ` lastUpdatedAt } } - `; + `); export const useLeaderboardQuery = ( variables: LeaderboardQueryVariables, @@ -2509,7 +3119,7 @@ useLeaderboardQuery.getKey = (variables: LeaderboardQueryVariables) => [ variables, ]; -export const UserLeaderboardRankDocument = ` +export const UserLeaderboardRankDocument = new TypedDocumentString(` query UserLeaderboardRank($userId: ID!) { userLeaderboardRank(userId: $userId) { rank @@ -2537,7 +3147,7 @@ export const UserLeaderboardRankDocument = ` } } } - `; + `); export const useUserLeaderboardRankQuery = < TData = UserLeaderboardRankQuery, @@ -2569,7 +3179,7 @@ useUserLeaderboardRankQuery.getKey = ( variables: UserLeaderboardRankQueryVariables, ) => ["UserLeaderboardRank", variables]; -export const TopContributorsDocument = ` +export const TopContributorsDocument = new TypedDocumentString(` query TopContributors($count: Int = 5) { topContributors(count: $count) { id @@ -2594,7 +3204,7 @@ export const TopContributorsDocument = ` lastActiveAt } } - `; + `); export const useTopContributorsQuery = < TData = TopContributorsQuery, @@ -2626,13 +3236,36 @@ useTopContributorsQuery.getKey = (variables?: TopContributorsQueryVariables) => ? ["TopContributors"] : ["TopContributors", variables]; -export const SubmitToBountyDocument = ` +export const SubmitToBountyDocument = new TypedDocumentString(` mutation SubmitToBounty($input: CreateSubmissionInput!) { submitToBounty(input: $input) { ...SubmissionFields } } - ${SubmissionFieldsFragmentDoc}`; + fragment SubmissionFields on BountySubmissionType { + id + bountyId + submittedBy + submittedByUser { + id + name + image + } + githubPullRequestUrl + status + createdAt + updatedAt + reviewedAt + reviewedBy + reviewedByUser { + id + name + image + } + reviewComments + paidAt + rewardTransactionHash +}`); export const useSubmitToBountyMutation = ( options?: UseMutationOptions< @@ -2658,13 +3291,36 @@ export const useSubmitToBountyMutation = ( }); }; -export const ReviewSubmissionDocument = ` +export const ReviewSubmissionDocument = new TypedDocumentString(` mutation ReviewSubmission($input: ReviewSubmissionInput!) { reviewSubmission(input: $input) { ...SubmissionFields } } - ${SubmissionFieldsFragmentDoc}`; + fragment SubmissionFields on BountySubmissionType { + id + bountyId + submittedBy + submittedByUser { + id + name + image + } + githubPullRequestUrl + status + createdAt + updatedAt + reviewedAt + reviewedBy + reviewedByUser { + id + name + image + } + reviewComments + paidAt + rewardTransactionHash +}`); export const useReviewSubmissionMutation = < TError = unknown, @@ -2693,7 +3349,7 @@ export const useReviewSubmissionMutation = < }); }; -export const MarkSubmissionPaidDocument = ` +export const MarkSubmissionPaidDocument = new TypedDocumentString(` mutation MarkSubmissionPaid($submissionId: ID!, $transactionHash: String!) { markSubmissionPaid( submissionId: $submissionId @@ -2702,7 +3358,30 @@ export const MarkSubmissionPaidDocument = ` ...SubmissionFields } } - ${SubmissionFieldsFragmentDoc}`; + fragment SubmissionFields on BountySubmissionType { + id + bountyId + submittedBy + submittedByUser { + id + name + image + } + githubPullRequestUrl + status + createdAt + updatedAt + reviewedAt + reviewedBy + reviewedByUser { + id + name + image + } + reviewComments + paidAt + rewardTransactionHash +}`); export const useMarkSubmissionPaidMutation = < TError = unknown, diff --git a/lib/graphql/operations/bookmark-operations.graphql b/lib/graphql/operations/bookmark-operations.graphql new file mode 100644 index 00000000..7ce9bb15 --- /dev/null +++ b/lib/graphql/operations/bookmark-operations.graphql @@ -0,0 +1,23 @@ +query Bookmarks { + bookmarks { + id + userId + bountyId + createdAt + bounty { + ...BountyFields + } + } +} + +mutation ToggleBookmark($input: ToggleBookmarkInput!) { + toggleBookmark(input: $input) { + id + userId + bountyId + createdAt + bounty { + ...BountyFields + } + } +} diff --git a/lib/graphql/schema.graphql b/lib/graphql/schema.graphql index cdfb4a81..03300399 100644 --- a/lib/graphql/schema.graphql +++ b/lib/graphql/schema.graphql @@ -701,6 +701,18 @@ type BountyWindowType { status: String! } +type Bookmark { + id: ID! + userId: String! + bountyId: String! + createdAt: DateTime! + bounty: Bounty! +} + +input ToggleBookmarkInput { + bountyId: ID! +} + input ContactOrganizersInput { message: String! subject: String! @@ -906,6 +918,9 @@ type Mutation { """Submit to a bounty (any authenticated user)""" submitToBounty(input: CreateSubmissionInput!): BountySubmissionType! + """Toggle a bookmark on a bounty (authenticated users)""" + toggleBookmark(input: ToggleBookmarkInput!): Bookmark! + """Update a blog post (Admin only)""" updateAdminBlogPost(id: ID!, input: UpdateBlogPostDto!): AdminBlogPostDto! @@ -982,7 +997,7 @@ type Query { adminUserCrowdfundingCampaigns(limit: Int = 20, page: Int = 1, usernameOrId: String!): AdminCrowdfundingResponseDto! """Get all users with pagination and filtering""" - adminUsers(isActive: Boolean, limit: Int, page: Int, role: String, search: String): AdminUsersResponseDto! + adminUsers(isActive: Boolean, limit: Int, page: Int, search: String): AdminUsersResponseDto! """Get paginated list of bounties with filtering""" bounties(query: BountyQueryInput): PaginatedBounties! @@ -996,6 +1011,9 @@ type Query { """Get bounties for a specific project""" projectBounties(projectId: ID!): [Bounty!]! + """Get all bookmarked bounties for the current user""" + bookmarks: [Bookmark!]! + """Get leaderboard with filtering and pagination""" leaderboard(filters: LeaderboardFilters!, pagination: LeaderboardPagination!): LeaderboardResponse! diff --git a/lib/query/query-keys.ts b/lib/query/query-keys.ts index 6ac19098..8192321e 100644 --- a/lib/query/query-keys.ts +++ b/lib/query/query-keys.ts @@ -107,3 +107,17 @@ export const withdrawalKeys = { all: ["withdrawal"] as const, history: () => [...withdrawalKeys.all, "history"] as const, }; + +/** + * Query Key Factory for Bookmarks + */ +export const bookmarkKeys = { + all: ["bookmarks"] as const, + list: () => [...bookmarkKeys.all, "list"] as const, + ids: () => [...bookmarkKeys.all, "ids"] as const, + // Minimal status cache for notification deduplication: { [bountyId]: status } + statusCache: () => [...bookmarkKeys.all, "statusCache"] as const, +}; + +export type BookmarkQueryKey = ReturnType; +export type BookmarkIdsQueryKey = ReturnType; diff --git a/lib/server-graphql.ts b/lib/server-graphql.ts new file mode 100644 index 00000000..646557cc --- /dev/null +++ b/lib/server-graphql.ts @@ -0,0 +1,39 @@ +import { getAccessToken } from "@/lib/auth-utils"; + +const GRAPHQL_URL = process.env.NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql"; + +/** + * Server-side GraphQL client using fetch with cookie-based auth + * Use this in Server Components and Route Handlers + */ +export async function graphqlRequest( + query: string, + variables?: Record, +): Promise { + const token = await getAccessToken(); + const headers: Record = { + "Content-Type": "application/json", + }; + if (token) { + headers.authorization = `Bearer ${token}`; + } + + const response = await fetch(GRAPHQL_URL, { + method: "POST", + headers, + body: JSON.stringify({ query, variables }), + // Don't cache - always get fresh data + cache: "no-store", + }); + + if (!response.ok) { + throw new Error(`GraphQL request failed: ${response.status}`); + } + + const json = await response.json(); + if (json.errors) { + throw new Error(json.errors[0]?.message || "GraphQL error"); + } + + return json.data; +} diff --git a/package-lock.json b/package-lock.json index c14f2d78..d391fa83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "@graphql-codegen/typescript": "^5.0.8", "@graphql-codegen/typescript-operations": "^5.0.8", "@graphql-codegen/typescript-react-query": "^7.0.0", + "@graphql-typed-document-node/core": "^3.2.0", "@next/swc-wasm-nodejs": "^16.1.6", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", @@ -145,7 +146,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^2.1.3", @@ -159,7 +160,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/@babel/code-frame": { @@ -1101,21 +1102,6 @@ } } }, - "node_modules/@creit-tech/stellar-wallets-kit/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@creit.tech/xbull-wallet-connect": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@creit.tech/xbull-wallet-connect/-/xbull-wallet-connect-0.4.0.tgz", @@ -1157,7 +1143,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1177,7 +1163,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1201,7 +1187,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1229,7 +1215,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1252,7 +1238,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1278,7 +1264,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1300,7 +1285,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6862,7 +6846,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6879,7 +6862,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6896,7 +6878,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6913,7 +6894,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6930,7 +6910,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6947,7 +6926,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6964,7 +6942,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6981,7 +6958,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6998,7 +6974,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7015,7 +6990,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7032,7 +7006,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7049,7 +7022,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7066,7 +7038,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7080,7 +7051,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7102,7 +7072,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7119,7 +7088,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10904,7 +10872,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12184,21 +12151,6 @@ "ws": "^7.5.1" } }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -14840,7 +14792,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^3.2.0", @@ -14998,7 +14950,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", @@ -15134,7 +15086,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -15598,7 +15550,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -16839,7 +16791,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -17634,7 +17585,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -17698,7 +17649,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -17712,7 +17663,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -18393,7 +18344,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-property": { @@ -18864,21 +18815,6 @@ "ws": "*" } }, - "node_modules/jayson/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/jayson/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -19834,7 +19770,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -19878,7 +19814,7 @@ "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cssstyle": "^4.2.1", @@ -20141,7 +20077,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20162,7 +20097,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20183,7 +20117,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20204,7 +20137,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20225,7 +20157,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20246,7 +20177,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20267,7 +20197,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20288,7 +20217,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20309,7 +20237,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20330,7 +20257,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20351,7 +20277,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -22368,7 +22293,7 @@ "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -22767,7 +22692,7 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -23287,7 +23212,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -24434,7 +24359,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/run-parallel": { @@ -24558,14 +24483,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -25650,7 +25575,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sync-fetch": { @@ -25882,7 +25807,7 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tldts-core": "^6.1.86" @@ -25895,7 +25820,7 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tmpl": { @@ -25952,7 +25877,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" @@ -25965,7 +25890,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -27228,7 +27153,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -27261,7 +27186,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -27272,7 +27197,7 @@ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -27285,7 +27210,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -27298,7 +27223,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -27308,7 +27233,7 @@ "version": "14.2.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tr46": "^5.1.0", @@ -27629,7 +27554,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -27639,7 +27564,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/xrpl": { @@ -27694,7 +27619,7 @@ "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 9e5cde73..922321ef 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@graphql-codegen/typescript": "^5.0.8", "@graphql-codegen/typescript-operations": "^5.0.8", "@graphql-codegen/typescript-react-query": "^7.0.0", + "@graphql-typed-document-node/core": "^3.2.0", "@next/swc-wasm-nodejs": "^16.1.6", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", From fb5e55faf9dec97a953903f9919a3e2b388da02d Mon Sep 17 00:00:00 2001 From: Miracle Nnaji Date: Sat, 25 Apr 2026 15:32:52 +0100 Subject: [PATCH 2/6] fix: prevent duplicate saved bounty notifications Ensure notification deduplication cache is both written to and read from. Previously, statusCache was updated but never used, causing repeated notifications for identical bounty status updates. Now: - Read from statusCache as primary baseline - Fallback to list-derived status when absent - Skip notifications when status hasn't changed - Maintain cache consistency after emitting notification Also confirms: - GraphQL generated documents are used across bookmark routes - React Query onSettled properly awaits cache invalidation - Notification false positives avoided when cache is uninitialized --- app/api/bookmarks/[bountyId]/route.ts | 54 ++------------------------- app/api/bookmarks/route.ts | 54 ++------------------------- hooks/use-bookmarks.ts | 2 +- hooks/use-notifications.ts | 27 +++++++++++++- lib/graphql/schema.graphql | 4 +- lib/server-graphql.ts | 6 ++- 6 files changed, 39 insertions(+), 108 deletions(-) diff --git a/app/api/bookmarks/[bountyId]/route.ts b/app/api/bookmarks/[bountyId]/route.ts index 7c617554..b4434050 100644 --- a/app/api/bookmarks/[bountyId]/route.ts +++ b/app/api/bookmarks/[bountyId]/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/server-auth"; import { graphqlRequest } from "@/lib/server-graphql"; +import { ToggleBookmarkDocument } from "@/lib/graphql/generated"; import type { Bookmark } from "@/lib/graphql/generated"; /** @@ -29,58 +30,9 @@ export async function POST( ); } - // Call GraphQL mutation to toggle bookmark - const TOGGLE_BOOKMARK_MUTATION = ` - mutation ToggleBookmark($input: ToggleBookmarkInput!) { - toggleBookmark(input: $input) { - id - userId - bountyId - createdAt - bounty { - id - title - description - status - type - rewardAmount - rewardCurrency - createdAt - updatedAt - organizationId - projectId - bountyWindowId - githubIssueUrl - githubIssueNumber - createdBy - organization { - id - name - logo - slug - } - project { - id - title - description - } - bountyWindow { - id - name - status - startDate - endDate - } - _count { - submissions - } - } - } - } - `; - + // Use generated GraphQL mutation document const data = await graphqlRequest<{ toggleBookmark: Bookmark | null }>( - TOGGLE_BOOKMARK_MUTATION, + ToggleBookmarkDocument, { input: { bountyId } }, ); diff --git a/app/api/bookmarks/route.ts b/app/api/bookmarks/route.ts index f795a16d..63ca10fe 100644 --- a/app/api/bookmarks/route.ts +++ b/app/api/bookmarks/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/server-auth"; import { graphqlRequest } from "@/lib/server-graphql"; +import { BookmarksDocument } from "@/lib/graphql/generated"; import type { Bookmark } from "@/lib/graphql/generated"; /** @@ -15,58 +16,9 @@ export async function GET() { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - // Query GraphQL for bookmarks with full bounty details - const BOOKMARKS_QUERY = ` - query GetBookmarks { - bookmarks { - id - userId - bountyId - createdAt - bounty { - id - title - description - status - type - rewardAmount - rewardCurrency - createdAt - updatedAt - organizationId - projectId - bountyWindowId - githubIssueUrl - githubIssueNumber - createdBy - organization { - id - name - logo - slug - } - project { - id - title - description - } - bountyWindow { - id - name - status - startDate - endDate - } - _count { - submissions - } - } - } - } - `; - + // Use generated GraphQL document for type safety const data = await graphqlRequest<{ bookmarks: Bookmark[] }>( - BOOKMARKS_QUERY, + BookmarksDocument, ); return NextResponse.json(data.bookmarks); diff --git a/hooks/use-bookmarks.ts b/hooks/use-bookmarks.ts index 22a8d849..30408206 100644 --- a/hooks/use-bookmarks.ts +++ b/hooks/use-bookmarks.ts @@ -104,7 +104,7 @@ export function useToggleBookmark() { toast.error(`Failed to bookmark: ${err.message}`); }, onSettled: () => { - Promise.all([ + return Promise.all([ queryClient.invalidateQueries({ queryKey: bookmarkKeys.list() }), queryClient.invalidateQueries({ queryKey: bookmarkKeys.ids() }), ]); diff --git a/hooks/use-notifications.ts b/hooks/use-notifications.ts index 9ec2c90a..2d6edceb 100644 --- a/hooks/use-notifications.ts +++ b/hooks/use-notifications.ts @@ -90,11 +90,19 @@ function saveToStorage(userId: string, items: NotificationItem[]): void { /** * Get a Map of bountyId -> status for currently bookmarked bounties. * Uses Set for O(1) membership filtering. + * Returns null if list cache is missing (no data available). */ -function getBookmarkedStatusMap(queryClient: QueryClient): Map { +function getBookmarkedStatusMap( + queryClient: QueryClient, +): Map | null { const ids = queryClient.getQueryData(bookmarkKeys.ids()) ?? []; const idsSet = new Set(ids); - const list = queryClient.getQueryData(bookmarkKeys.list()) ?? []; + const list = queryClient.getQueryData(bookmarkKeys.list()); + + // If list cache is missing or empty, return null to signal "no data" + if (!list || list.length === 0) { + return null; + } const statusMap = new Map(); for (const b of list) { @@ -174,6 +182,12 @@ export function useNotifications() { if (isBookmarked) { // Get previous status from status map (derived from list cache) const statusMap = getBookmarkedStatusMap(queryClient); + + // If no cached data, skip notification (avoid false positives) + if (statusMap === null) { + return; + } + const previousStatus = statusMap.get(bounty.id); // Only notify if status changed (meaningful update) @@ -192,6 +206,15 @@ export function useNotifications() { }, [], ); + + // Update status cache to prevent duplicate notifications + queryClient.setQueryData>( + bookmarkKeys.statusCache(), + (old = {}) => ({ + ...old, + [bounty.id]: bounty.status, + }), + ); } } diff --git a/lib/graphql/schema.graphql b/lib/graphql/schema.graphql index 03300399..fa4d043b 100644 --- a/lib/graphql/schema.graphql +++ b/lib/graphql/schema.graphql @@ -918,8 +918,8 @@ type Mutation { """Submit to a bounty (any authenticated user)""" submitToBounty(input: CreateSubmissionInput!): BountySubmissionType! - """Toggle a bookmark on a bounty (authenticated users)""" - toggleBookmark(input: ToggleBookmarkInput!): Bookmark! + """Toggle a bookmark on a bounty (authenticated users)""" + toggleBookmark(input: ToggleBookmarkInput!): Bookmark """Update a blog post (Admin only)""" updateAdminBlogPost(id: ID!, input: UpdateBlogPostDto!): AdminBlogPostDto! diff --git a/lib/server-graphql.ts b/lib/server-graphql.ts index 646557cc..4b9fee5c 100644 --- a/lib/server-graphql.ts +++ b/lib/server-graphql.ts @@ -1,4 +1,5 @@ import { getAccessToken } from "@/lib/auth-utils"; +import { TypedDocumentString } from "@/lib/graphql/generated"; const GRAPHQL_URL = process.env.NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql"; @@ -7,7 +8,7 @@ const GRAPHQL_URL = process.env.NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql"; * Use this in Server Components and Route Handlers */ export async function graphqlRequest( - query: string, + query: string | TypedDocumentString>, variables?: Record, ): Promise { const token = await getAccessToken(); @@ -18,6 +19,9 @@ export async function graphqlRequest( headers.authorization = `Bearer ${token}`; } + // Normalize: if TypedDocumentString, convert to string + const queryString = typeof query === "string" ? query : query.toString(); + const response = await fetch(GRAPHQL_URL, { method: "POST", headers, From d1b9695a23c25ccae9a5ac60618f38318057288b Mon Sep 17 00:00:00 2001 From: Miracle Nnaji Date: Sat, 25 Apr 2026 16:48:45 +0100 Subject: [PATCH 3/6] fix : corrected code according to review. --- components/bounty-detail/bounty-detail-header-card.tsx | 10 +++++++++- components/bounty/bounty-card.tsx | 10 +++++++++- hooks/use-bookmarks.ts | 8 ++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/components/bounty-detail/bounty-detail-header-card.tsx b/components/bounty-detail/bounty-detail-header-card.tsx index 28235a55..fd00552c 100644 --- a/components/bounty-detail/bounty-detail-header-card.tsx +++ b/components/bounty-detail/bounty-detail-header-card.tsx @@ -11,7 +11,15 @@ export function HeaderCard({ bounty }: { bounty: BountyFieldsFragment }) { return (
{/* Bookmark button - top right corner */} -
+
e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + } + }} + >
diff --git a/components/bounty/bounty-card.tsx b/components/bounty/bounty-card.tsx index a338b2d0..3d2f2aff 100644 --- a/components/bounty/bounty-card.tsx +++ b/components/bounty/bounty-card.tsx @@ -150,7 +150,15 @@ export function BountyCard({ )} {/* Bookmark button - top-right corner */} -
+
e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + } + }} + >
diff --git a/hooks/use-bookmarks.ts b/hooks/use-bookmarks.ts index 30408206..54d647b9 100644 --- a/hooks/use-bookmarks.ts +++ b/hooks/use-bookmarks.ts @@ -29,12 +29,20 @@ export function useBookmarks() { /** * Hook to fetch just the bookmarked bounty IDs. * Use this for O(1) bookmark state checks (backed by Set). + * + * NOTE: Derives from the full bookmarks query when available to keep caches in sync. + * Only falls back to independent fetch when the list cache is empty (e.g., on first load + * of a fresh session, or on the /saved page where the list is the primary source). */ export function useBookmarkIds() { + const queryClient = useQueryClient(); + const listData = queryClient.getQueryData(bookmarkKeys.list()); + return useQuery({ queryKey: bookmarkKeys.ids(), queryFn: fetchBookmarkIds, staleTime: 1000 * 60 * 5, // 5 minutes + initialData: listData?.map((b) => b.bounty.id) ?? undefined, }); } From 4129851a8545245b45338b240a2325f4d8166084 Mon Sep 17 00:00:00 2001 From: Miracle Nnaji Date: Sat, 25 Apr 2026 16:51:31 +0100 Subject: [PATCH 4/6] chore: sync pnpm lockfile --- pnpm-lock.yaml | 147 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 51 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e9af7a4..fa0720d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@creit-tech/stellar-wallets-kit': specifier: npm:@creit.tech/stellar-wallets-kit@^2.0.1 - version: '@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6)' + version: '@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6)' '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.71.1(react@19.2.3)) @@ -139,7 +139,7 @@ importers: version: 2.12.6(graphql@16.12.0) graphql-ws: specifier: ^6.0.7 - version: 6.0.7(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + version: 6.0.7(crossws@0.3.5)(graphql@16.12.0)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -184,7 +184,7 @@ importers: version: 4.0.1 smart-account-kit: specifier: ^0.2.10 - version: 0.2.10(@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6))(@stellar/stellar-sdk@14.6.1) + version: 0.2.10(@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6))(@stellar/stellar-sdk@14.6.1) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -210,6 +210,9 @@ importers: '@graphql-codegen/typescript-react-query': specifier: ^7.0.0 version: 7.0.1(graphql@16.12.0) + '@graphql-typed-document-node/core': + specifier: ^3.2.0 + version: 3.2.0(graphql@16.12.0) '@next/swc-wasm-nodejs': specifier: ^16.1.6 version: 16.1.6 @@ -1143,89 +1146,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1615,24 +1634,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-wasm-nodejs@16.1.6': resolution: {integrity: sha512-U9Qpc9JefEXb1ykflZoYdFskAfVemCHNzTwKoG7nyRnO0DMmvitsoQwYl9JCcFVU2tR8MGmJu5cs4jVMiyOEPQ==} @@ -2622,66 +2645,79 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -3455,24 +3491,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.0': resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.0': resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.0': resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.0': resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==} @@ -3957,41 +3997,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -6371,24 +6419,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -8791,7 +8843,7 @@ snapshots: - utf-8-validate optional: true - '@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6)': + '@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6)': dependencies: '@albedo-link/intent': 0.12.0 '@creit.tech/xbull-wallet-connect': 0.4.0 @@ -8804,8 +8856,8 @@ snapshots: '@reown/appkit': 1.8.19(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(zod@4.3.6) '@stellar/freighter-api': 6.0.0 '@stellar/stellar-base': 14.0.1 - '@trezor/connect-plugin-stellar': 9.2.6(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(tslib@2.8.1) - '@trezor/connect-web': 9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@trezor/connect-plugin-stellar': 9.2.6(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(tslib@2.8.1) + '@trezor/connect-web': 9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@twind/core': 1.1.3(typescript@5.9.3) '@twind/preset-autoprefix': 1.0.7(@twind/core@1.1.3(typescript@5.9.3))(typescript@5.9.3) '@twind/preset-tailwind': 1.1.4(@twind/core@1.1.3(typescript@5.9.3))(typescript@5.9.3) @@ -11579,31 +11631,31 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@solana-program/stake@0.2.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': + '@solana-program/stake@0.2.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))': dependencies: '@solana/kit': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) optional: true - '@solana-program/system@0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': + '@solana-program/system@0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))': dependencies: @@ -11835,7 +11887,7 @@ snapshots: - fastestsmallesttextencoderdecoder optional: true - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -11848,11 +11900,11 @@ snapshots: '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 @@ -12065,14 +12117,14 @@ snapshots: - fastestsmallesttextencoderdecoder optional: true - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) '@solana/functional': 2.3.0(typescript@5.9.3) '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) '@solana/subscribable': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: @@ -12106,7 +12158,7 @@ snapshots: typescript: 5.9.3 optional: true - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) @@ -12114,7 +12166,7 @@ snapshots: '@solana/promises': 2.3.0(typescript@5.9.3) '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -12308,7 +12360,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -12316,7 +12368,7 @@ snapshots: '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/promises': 2.3.0(typescript@5.9.3) '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -12684,13 +12736,13 @@ snapshots: - react-native - utf-8-validate - '@trezor/blockchain-link@2.6.1(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@trezor/blockchain-link@2.6.1(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))) - '@solana-program/stake': 0.2.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))) + '@solana-program/stake': 0.2.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@stellar/stellar-sdk': 14.2.0 '@trezor/blockchain-link-types': 1.5.0(tslib@2.8.1) @@ -12738,16 +12790,16 @@ snapshots: - expo-localization - react-native - '@trezor/connect-plugin-stellar@9.2.6(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(tslib@2.8.1)': + '@trezor/connect-plugin-stellar@9.2.6(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(tslib@2.8.1)': dependencies: '@stellar/stellar-sdk': 14.6.1 - '@trezor/connect': 9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@trezor/connect': 9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@trezor/utils': 9.5.0(tslib@2.8.1) tslib: 2.8.1 - '@trezor/connect-web@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@trezor/connect-web@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: - '@trezor/connect': 9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@trezor/connect': 9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@trezor/connect-common': 0.5.1(tslib@2.8.1) '@trezor/utils': 9.5.0(tslib@2.8.1) '@trezor/websocket-client': 1.3.0(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@6.0.6) @@ -12766,7 +12818,7 @@ snapshots: - utf-8-validate - ws - '@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@ethereumjs/common': 10.1.1 '@ethereumjs/tx': 10.1.1 @@ -12774,12 +12826,12 @@ snapshots: '@mobily/ts-belt': 3.13.1 '@noble/hashes': 1.8.0 '@scure/bip39': 1.6.0 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))) - '@solana-program/system': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@trezor/blockchain-link': 2.6.1(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))) + '@solana-program/system': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@trezor/blockchain-link': 2.6.1(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@trezor/blockchain-link-types': 1.5.1(tslib@2.8.1) '@trezor/blockchain-link-utils': 1.5.2(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@6.0.6) '@trezor/connect-analytics': 1.4.0(tslib@2.8.1) @@ -15498,13 +15550,6 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - graphql-ws@6.0.7(crossws@0.3.5)(graphql@16.12.0)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)): - dependencies: - graphql: 16.12.0 - optionalDependencies: - crossws: 0.3.5 - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) - graphql-ws@6.0.7(crossws@0.3.5)(graphql@16.12.0)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)): dependencies: graphql: 16.12.0 @@ -18103,14 +18148,14 @@ snapshots: '@stellar/stellar-sdk': 14.6.1 buffer: 6.0.3 - smart-account-kit@0.2.10(@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6))(@stellar/stellar-sdk@14.6.1): + smart-account-kit@0.2.10(@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6))(@stellar/stellar-sdk@14.6.1): dependencies: '@simplewebauthn/browser': 13.3.0 '@stellar/stellar-sdk': 14.6.1 base64url: 3.0.1 smart-account-kit-bindings: 0.1.2(@stellar/stellar-sdk@14.6.1) optionalDependencies: - '@creit-tech/stellar-wallets-kit': '@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6)' + '@creit-tech/stellar-wallets-kit': '@creit.tech/stellar-wallets-kit@2.1.0(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@stellar/stellar-sdk@14.6.1)(@trezor/connect@9.7.2(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(near-api-js@5.1.1)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@6.0.6)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.3.6)' smart-buffer@4.2.0: {} From 3a6ddbecdced11222fc600cbfbfb941b4e7ab26c Mon Sep 17 00:00:00 2001 From: Miracle Nnaji Date: Sat, 25 Apr 2026 16:58:28 +0100 Subject: [PATCH 5/6] fix(notifications): ensure bounty detail invalidation always runs when bookmarked --- hooks/use-notifications.ts | 62 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/hooks/use-notifications.ts b/hooks/use-notifications.ts index 2d6edceb..fafbb36d 100644 --- a/hooks/use-notifications.ts +++ b/hooks/use-notifications.ts @@ -183,38 +183,36 @@ export function useNotifications() { // Get previous status from status map (derived from list cache) const statusMap = getBookmarkedStatusMap(queryClient); - // If no cached data, skip notification (avoid false positives) - if (statusMap === null) { - return; - } - - const previousStatus = statusMap.get(bounty.id); - - // Only notify if status changed (meaningful update) - if ( - previousStatus === undefined || - previousStatus !== bounty.status - ) { - const timestamp = Date.now(); - addNotification( - { - id: `saved-bounty-updated-${bounty.id}-${timestamp}`, - message: `Saved bounty "${bounty.title}" was updated.`, - type: "saved-bounty-updated", - timestamp: normaliseTimestamp(bounty.updatedAt), - read: false, - }, - [], - ); - - // Update status cache to prevent duplicate notifications - queryClient.setQueryData>( - bookmarkKeys.statusCache(), - (old = {}) => ({ - ...old, - [bounty.id]: bounty.status, - }), - ); + // Only process notification if cached data exists (avoid false positives) + if (statusMap !== null) { + const previousStatus = statusMap.get(bounty.id); + + // Only notify if status changed (meaningful update) + if ( + previousStatus === undefined || + previousStatus !== bounty.status + ) { + const timestamp = Date.now(); + addNotification( + { + id: `saved-bounty-updated-${bounty.id}-${timestamp}`, + message: `Saved bounty "${bounty.title}" was updated.`, + type: "saved-bounty-updated", + timestamp: normaliseTimestamp(bounty.updatedAt), + read: false, + }, + [], + ); + + // Update status cache to prevent duplicate notifications + queryClient.setQueryData>( + bookmarkKeys.statusCache(), + (old = {}) => ({ + ...old, + [bounty.id]: bounty.status, + }), + ); + } } } From 92fb36143227dde6aae1e5f921b19c57d0f99db0 Mon Sep 17 00:00:00 2001 From: Miracle Nnaji Date: Sat, 25 Apr 2026 18:37:41 +0100 Subject: [PATCH 6/6] refactor(bookmarks): consolidate staleTime into shared constant --- hooks/use-bookmarks.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hooks/use-bookmarks.ts b/hooks/use-bookmarks.ts index 54d647b9..c6a79fbd 100644 --- a/hooks/use-bookmarks.ts +++ b/hooks/use-bookmarks.ts @@ -6,6 +6,7 @@ import type { Bookmark } from "@/lib/graphql/generated"; const BOOKMARKS_ENDPOINT = "/api/bookmarks"; const BOOKMARKS_IDS_ENDPOINT = "/api/bookmarks/ids"; +const BOOKMARK_STALE_TIME = 5 * 60 * 1000; // 5 minutes async function fetchBookmarks(): Promise { return get(BOOKMARKS_ENDPOINT); @@ -22,7 +23,7 @@ export function useBookmarks() { return useQuery({ queryKey: bookmarkKeys.list(), queryFn: fetchBookmarks, - staleTime: 1000 * 60 * 5, // 5 minutes + staleTime: BOOKMARK_STALE_TIME, }); } @@ -41,7 +42,7 @@ export function useBookmarkIds() { return useQuery({ queryKey: bookmarkKeys.ids(), queryFn: fetchBookmarkIds, - staleTime: 1000 * 60 * 5, // 5 minutes + staleTime: BOOKMARK_STALE_TIME, initialData: listData?.map((b) => b.bounty.id) ?? undefined, }); }