diff --git a/app/access-check.tsx b/app/access-check.tsx index 677dae8..5658fbc 100644 --- a/app/access-check.tsx +++ b/app/access-check.tsx @@ -1,55 +1,38 @@ -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { View, Text, ScrollView, TextInput } from "react-native"; -// GuildPass Mobile: Import package module dependencies. import React, { useEffect, useState } from "react"; import { useLocalSearchParams, useRouter } from "expo-router"; -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { useWallet } from "../src/features/wallet/useWallet"; -// GuildPass Mobile: Import package module dependencies. -import { useAccessCheck } from "../src/features/access/useAccessCheck"; +import { useAccessCheckResult } from "../src/features/access/useAccessCheckResult"; import type { ParsedAccessQrPayload } from "../src/features/access/qrPayload"; import { parseAccessQrPayload } from "../src/features/access/qrPayload"; -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { AppHeader } from "../src/components/AppHeader"; -// GuildPass Mobile: Import package module dependencies. import { Card } from "../src/components/Card"; -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { Button } from "../src/components/Button"; -// GuildPass Mobile: Import package module dependencies. import { WalletInput } from "../src/components/WalletInput"; -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { AccessStatusCard } from "../src/components/AccessStatusCard"; -// GuildPass Mobile: Import package module dependencies. import { LoadingState } from "../src/components/LoadingState"; -// GuildPass Mobile: Exposed interface structure for local navigation layouts. export default function AccessCheck() { const router = useRouter(); const { qrPayload } = useLocalSearchParams<{ qrPayload?: string | string[] }>(); - // GuildPass Mobile: Variable binding and property initialization. const { walletAddress: currentWallet } = useWallet(); - // GuildPass Mobile: Local UI-scoped constant or state representation. const [address, setAddress] = useState(currentWallet || ""); - // GuildPass Mobile: Variable binding and property initialization. const [guildId, setGuildId] = useState(""); - // GuildPass Mobile: Local UI-scoped constant or state representation. const [resourceId, setResourceId] = useState(""); const [scanError, setScanError] = useState(null); const [scannedPayload, setScannedPayload] = useState(null); - // GuildPass Mobile: Variable binding and property initialization. const [checkParams, setCheckParams] = useState<{ walletAddress: string; guildId: string; resourceId: string; - // GuildPass Mobile: Exit functional execution container scope block. } | null>(null); - const checkParamsNonNull = checkParams || { walletAddress: "", guildId: "", resourceId: "" }; + const checkParamsNonNull = checkParams ?? { walletAddress: "", guildId: "", resourceId: "" }; const { data: result, isLoading, error, - } = useAccessCheck(checkParamsNonNull); + } = useAccessCheckResult(checkParamsNonNull); useEffect(() => { const rawPayload = Array.isArray(qrPayload) ? qrPayload[0] : qrPayload; @@ -73,17 +56,12 @@ export default function AccessCheck() { } }, [currentWallet, qrPayload]); - // GuildPass Mobile: Local UI-scoped constant or state representation. const handleCheck = () => { - // GuildPass Mobile: Evaluate branch condition check for UI guards. if (address && guildId && resourceId) { setCheckParams({ walletAddress: address, guildId, resourceId }); - // GuildPass Mobile: Exit functional execution container scope block. } - // GuildPass Mobile: Exit functional execution container scope block. }; - // GuildPass Mobile: Terminate block execution context and send back value. return ( @@ -92,7 +70,6 @@ export default function AccessCheck() { @@ -177,7 +154,11 @@ export default function AccessCheck() { )} {error && ( - + Error checking access Please verify your inputs and try again. @@ -187,5 +168,4 @@ export default function AccessCheck() { ); - // GuildPass Mobile: Exit functional execution container scope block. } diff --git a/app/guilds.tsx b/app/guilds.tsx index 59e2bdd..5813659 100644 --- a/app/guilds.tsx +++ b/app/guilds.tsx @@ -2,11 +2,6 @@ import { View, FlatList } from "react-native"; // GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { useRouter } from "expo-router"; -// GuildPass Mobile: Import package module dependencies. -import { useWallet } from "../src/features/wallet/useWallet"; -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. -import { useMembership } from "../src/features/membership/useMembership"; -// GuildPass Mobile: Import package module dependencies. import { AppHeader } from "../src/components/AppHeader"; // GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { GuildCard } from "../src/components/GuildCard"; @@ -23,10 +18,6 @@ import React from "react"; export default function Guilds() { // GuildPass Mobile: Variable binding and property initialization. const router = useRouter(); - // GuildPass Mobile: Local UI-scoped constant or state representation. - const { walletAddress } = useWallet(); - // GuildPass Mobile: Variable binding and property initialization. - const { getMembership } = useMembership(walletAddress); // In a real app, you would fetch all guilds. // For MVP, we'll show a few example guilds that the user can explore. diff --git a/app/guilds/[guildId].tsx b/app/guilds/[guildId].tsx index 239f293..0c4ef91 100644 --- a/app/guilds/[guildId].tsx +++ b/app/guilds/[guildId].tsx @@ -5,8 +5,10 @@ import { useLocalSearchParams } from "expo-router"; // GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { useWallet } from "../../src/features/wallet/useWallet"; // GuildPass Mobile: Import package module dependencies. -import { useGuilds } from "../../src/features/guilds/useGuilds"; +import { useGuild } from "../../src/features/guilds/useGuild"; // GuildPass Mobile: Pull in react-native, expo, or external state libraries. +import { useGuildRoles } from "../../src/features/guilds/useGuildRoles"; +// GuildPass Mobile: Import package module dependencies. import { useMembership } from "../../src/features/membership/useMembership"; // GuildPass Mobile: Import package module dependencies. import { AppHeader } from "../../src/components/AppHeader"; @@ -23,37 +25,28 @@ import React from "react"; // GuildPass Mobile: Core mobile screen or hook export definition. export default function GuildDetail() { - // GuildPass Mobile: Local UI-scoped constant or state representation. const { guildId } = useLocalSearchParams<{ guildId: string }>(); - // GuildPass Mobile: Variable binding and property initialization. const { walletAddress } = useWallet(); - // GuildPass Mobile: Local UI-scoped constant or state representation. - const { getGuild, getRoles } = useGuilds(); - // GuildPass Mobile: Variable binding and property initialization. - const { getMembership } = useMembership(walletAddress); - // GuildPass Mobile: Local UI-scoped constant or state representation. - const { data: guild, isLoading: guildLoading, error: guildError } = getGuild(guildId); - // GuildPass Mobile: Variable binding and property initialization. - const { data: membership, isLoading: memLoading } = getMembership(guildId); - // GuildPass Mobile: Local UI-scoped constant or state representation. - const { data: roles, isLoading: rolesLoading } = getRoles(guildId); + const { + data: guild, + isLoading: guildLoading, + error: guildError, + } = useGuild(guildId ?? ""); + const { data: membership, isLoading: memLoading } = useMembership( + walletAddress, + guildId ?? "", + ); + const { data: roles, isLoading: rolesLoading } = useGuildRoles(guildId ?? ""); - // GuildPass Mobile: Validate screen variables or params before routing. if (guildLoading || memLoading || rolesLoading) { - // GuildPass Mobile: Return evaluated JSX layout or callback response. return ; - // GuildPass Mobile: Exit functional execution container scope block. } - // GuildPass Mobile: Evaluate branch condition check for UI guards. if (guildError || !guild) { - // GuildPass Mobile: Terminate block execution context and send back value. return ; - // GuildPass Mobile: Exit functional execution container scope block. } - // GuildPass Mobile: Return evaluated JSX layout or callback response. return ( @@ -80,7 +73,10 @@ export default function GuildDetail() { Your Membership - + Status ); - // GuildPass Mobile: Exit functional execution container scope block. } diff --git a/src/features/access/useAccessCheck.ts b/src/features/access/useAccessCheck.ts deleted file mode 100644 index 7959a33..0000000 --- a/src/features/access/useAccessCheck.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. -import { useQuery } from "@tanstack/react-query"; -// GuildPass Mobile: Import package module dependencies. -import { guildPassClient } from "../../lib/guildpassClient"; - -// GuildPass Mobile: Exposed interface structure for local navigation layouts. -export const useAccessCheck = (params: { - walletAddress: string; - guildId: string; - resourceId: string; -}) => { - return useQuery({ - queryKey: ["access-check", params], - queryFn: () => guildPassClient.access.checkAccess(params), - enabled: !!params.walletAddress && !!params.guildId && !!params.resourceId, - }); -}; diff --git a/src/features/access/useAccessCheckResult.ts b/src/features/access/useAccessCheckResult.ts new file mode 100644 index 0000000..348fafd --- /dev/null +++ b/src/features/access/useAccessCheckResult.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { guildPassClient } from "../../lib/guildpassClient"; +import { accessCheckKeys, type AccessCheckParams } from "../../lib/queryKeys"; + +export function useAccessCheckResult(params: AccessCheckParams) { + return useQuery({ + queryKey: accessCheckKeys.detail(params), + queryFn: () => guildPassClient.access.checkAccess(params), + enabled: !!params.walletAddress && !!params.guildId && !!params.resourceId, + }); +} diff --git a/src/features/guilds/useGuild.ts b/src/features/guilds/useGuild.ts new file mode 100644 index 0000000..b8aab1a --- /dev/null +++ b/src/features/guilds/useGuild.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; +import { guildPassClient } from "../../lib/guildpassClient"; +import { guildKeys } from "../../lib/queryKeys"; + +export function createGuildQueryOptions(guildId: string) { + return { + queryKey: guildKeys.detail(guildId), + queryFn: () => guildPassClient.guilds.getGuild({ guildId }), + enabled: !!guildId, + } as const; +} + +export function useGuild(guildId: string) { + return useQuery(createGuildQueryOptions(guildId)); +} diff --git a/src/features/guilds/useGuildConfig.ts b/src/features/guilds/useGuildConfig.ts new file mode 100644 index 0000000..30de569 --- /dev/null +++ b/src/features/guilds/useGuildConfig.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { guildPassClient } from "../../lib/guildpassClient"; +import { guildKeys } from "../../lib/queryKeys"; + +export function useGuildConfig(guildId: string) { + return useQuery({ + queryKey: guildKeys.config(guildId), + queryFn: () => guildPassClient.guilds.getGuildConfig({ guildId }), + enabled: !!guildId, + }); +} diff --git a/src/features/guilds/useGuildRoles.ts b/src/features/guilds/useGuildRoles.ts new file mode 100644 index 0000000..f031c23 --- /dev/null +++ b/src/features/guilds/useGuildRoles.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { guildPassClient } from "../../lib/guildpassClient"; +import { guildKeys } from "../../lib/queryKeys"; + +export function useGuildRoles(guildId: string) { + return useQuery({ + queryKey: guildKeys.roles(guildId), + queryFn: () => guildPassClient.roles.getRoles({ guildId }), + enabled: !!guildId, + }); +} diff --git a/src/features/guilds/useGuilds.ts b/src/features/guilds/useGuilds.ts deleted file mode 100644 index 4810248..0000000 --- a/src/features/guilds/useGuilds.ts +++ /dev/null @@ -1,52 +0,0 @@ -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. -import { useQuery } from "@tanstack/react-query"; -// GuildPass Mobile: Import package module dependencies. -import { guildPassClient } from "../../lib/guildpassClient"; - -// GuildPass Mobile: Core mobile screen or hook export definition. -export const useGuilds = () => { - // GuildPass Mobile: Local UI-scoped constant or state representation. - const getGuild = (guildId: string) => { - // GuildPass Mobile: Terminate block execution context and send back value. - return useQuery({ - queryKey: ["guild", guildId], - queryFn: () => guildPassClient.guilds.getGuild({ guildId }), - enabled: !!guildId, - // GuildPass Mobile: Exit functional execution container scope block. - }); - // GuildPass Mobile: Exit functional execution container scope block. - }; - - // GuildPass Mobile: Variable binding and property initialization. - const getGuildConfig = (guildId: string) => { - // GuildPass Mobile: Return evaluated JSX layout or callback response. - return useQuery({ - queryKey: ["guild-config", guildId], - queryFn: () => guildPassClient.guilds.getGuildConfig({ guildId }), - enabled: !!guildId, - // GuildPass Mobile: Exit functional execution container scope block. - }); - // GuildPass Mobile: Exit functional execution container scope block. - }; - - // GuildPass Mobile: Local UI-scoped constant or state representation. - const getRoles = (guildId: string) => { - // GuildPass Mobile: Terminate block execution context and send back value. - return useQuery({ - queryKey: ["guild-roles", guildId], - queryFn: () => guildPassClient.roles.getRoles({ guildId }), - enabled: !!guildId, - // GuildPass Mobile: Exit functional execution container scope block. - }); - // GuildPass Mobile: Exit functional execution container scope block. - }; - - // GuildPass Mobile: Return evaluated JSX layout or callback response. - return { - getGuild, - getGuildConfig, - getRoles, - // GuildPass Mobile: Exit functional execution container scope block. - }; - // GuildPass Mobile: Exit functional execution container scope block. -}; diff --git a/src/features/membership/useMembership.ts b/src/features/membership/useMembership.ts index 9a72171..29f3ed4 100644 --- a/src/features/membership/useMembership.ts +++ b/src/features/membership/useMembership.ts @@ -1,51 +1,15 @@ -// GuildPass Mobile: Pull in react-native, expo, or external state libraries. import { useQuery } from "@tanstack/react-query"; -// GuildPass Mobile: Import package module dependencies. import { guildPassClient } from "../../lib/guildpassClient"; +import { membershipKeys } from "../../lib/queryKeys"; -// GuildPass Mobile: Exported screen, component definition, or state hooks. -export const useMembership = (walletAddress: string | null) => { - // GuildPass Mobile: Variable binding and property initialization. - const getMembership = (guildId: string) => { - // GuildPass Mobile: Terminate block execution context and send back value. - return useQuery({ - queryKey: ["membership", walletAddress, guildId], - queryFn: () => - // GuildPass Mobile: Enter functional execution container scope block. - guildPassClient.membership.getMembership({ - walletAddress: walletAddress!, - guildId, - // GuildPass Mobile: Exit functional execution container scope block. - }), - enabled: !!walletAddress && !!guildId, - // GuildPass Mobile: Exit functional execution container scope block. - }); - // GuildPass Mobile: Exit functional execution container scope block. - }; - - // GuildPass Mobile: Local UI-scoped constant or state representation. - const getUserRoles = (guildId: string) => { - // GuildPass Mobile: Return evaluated JSX layout or callback response. - return useQuery({ - queryKey: ["user-roles", walletAddress, guildId], - queryFn: () => - // GuildPass Mobile: Enter functional execution container scope block. - guildPassClient.roles.getUserRoles({ - walletAddress: walletAddress!, - guildId, - // GuildPass Mobile: Exit functional execution container scope block. - }), - enabled: !!walletAddress && !!guildId, - // GuildPass Mobile: Exit functional execution container scope block. - }); - // GuildPass Mobile: Exit functional execution container scope block. - }; - - // GuildPass Mobile: Terminate block execution context and send back value. - return { - getMembership, - getUserRoles, - // GuildPass Mobile: Exit functional execution container scope block. - }; - // GuildPass Mobile: Exit functional execution container scope block. -}; +export function useMembership(walletAddress: string | null, guildId: string) { + return useQuery({ + queryKey: membershipKeys.detail(walletAddress, guildId), + queryFn: () => + guildPassClient.membership.getMembership({ + walletAddress: walletAddress!, + guildId, + }), + enabled: !!walletAddress && !!guildId, + }); +} diff --git a/src/features/membership/useUserRoles.ts b/src/features/membership/useUserRoles.ts new file mode 100644 index 0000000..757036c --- /dev/null +++ b/src/features/membership/useUserRoles.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; +import { guildPassClient } from "../../lib/guildpassClient"; +import { membershipKeys } from "../../lib/queryKeys"; + +export function useUserRoles(walletAddress: string | null, guildId: string) { + return useQuery({ + queryKey: membershipKeys.userRoles(walletAddress, guildId), + queryFn: () => + guildPassClient.roles.getUserRoles({ + walletAddress: walletAddress!, + guildId, + }), + enabled: !!walletAddress && !!guildId, + }); +} diff --git a/src/lib/queryKeys.ts b/src/lib/queryKeys.ts new file mode 100644 index 0000000..b860fc1 --- /dev/null +++ b/src/lib/queryKeys.ts @@ -0,0 +1,22 @@ +export type AccessCheckParams = { + walletAddress: string; + guildId: string; + resourceId: string; +}; + +export const guildKeys = { + detail: (guildId: string) => ["guild", guildId] as const, + config: (guildId: string) => ["guild-config", guildId] as const, + roles: (guildId: string) => ["guild-roles", guildId] as const, +}; + +export const membershipKeys = { + detail: (walletAddress: string | null, guildId: string) => + ["membership", walletAddress, guildId] as const, + userRoles: (walletAddress: string | null, guildId: string) => + ["user-roles", walletAddress, guildId] as const, +}; + +export const accessCheckKeys = { + detail: (params: AccessCheckParams) => ["access-check", params] as const, +}; diff --git a/tests/hooks/queryKeys.test.ts b/tests/hooks/queryKeys.test.ts new file mode 100644 index 0000000..545fa5b --- /dev/null +++ b/tests/hooks/queryKeys.test.ts @@ -0,0 +1,42 @@ +/** + * queryKeys – shared query key helper tests + */ + +import { describe, it, expect } from "vitest"; +import { + accessCheckKeys, + guildKeys, + membershipKeys, +} from "../../src/lib/queryKeys"; +import { TEST_WALLET_ADDRESS } from "../fixtures/membership.fixtures"; + +describe("queryKeys", () => { + it("creates stable guild query keys", () => { + expect(guildKeys.detail("guild_abc")).toStrictEqual(["guild", "guild_abc"]); + expect(guildKeys.config("guild_abc")).toStrictEqual(["guild-config", "guild_abc"]); + expect(guildKeys.roles("guild_abc")).toStrictEqual(["guild-roles", "guild_abc"]); + }); + + it("creates wallet-scoped membership query keys", () => { + expect(membershipKeys.detail(TEST_WALLET_ADDRESS, "guild_abc")).toStrictEqual([ + "membership", + TEST_WALLET_ADDRESS, + "guild_abc", + ]); + expect(membershipKeys.userRoles(TEST_WALLET_ADDRESS, "guild_abc")).toStrictEqual([ + "user-roles", + TEST_WALLET_ADDRESS, + "guild_abc", + ]); + }); + + it("creates access-check query keys from params", () => { + const params = { + walletAddress: TEST_WALLET_ADDRESS, + guildId: "guild_abc", + resourceId: "secret-channel", + }; + + expect(accessCheckKeys.detail(params)).toStrictEqual(["access-check", params]); + }); +}); diff --git a/tests/hooks/useGuild.hook.test.ts b/tests/hooks/useGuild.hook.test.ts new file mode 100644 index 0000000..c4c2e0c --- /dev/null +++ b/tests/hooks/useGuild.hook.test.ts @@ -0,0 +1,80 @@ +/** + * useGuild hook – refactored query hook tests + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { QueryClient } from "@tanstack/react-query"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { GUILD_DETAIL_FIXTURE } from "../fixtures/guild.fixtures"; +import { guildKeys } from "../../src/lib/queryKeys"; + +const getGuild = vi.fn(); + +vi.mock("../../src/lib/guildpassClient", () => ({ + guildPassClient: { + guilds: { + getGuild: (...args: unknown[]) => getGuild(...args), + }, + }, +})); + +import { createGuildQueryOptions, useGuild } from "../../src/features/guilds/useGuild"; + +describe("useGuild", () => { + beforeEach(() => { + getGuild.mockReset(); + getGuild.mockResolvedValue(GUILD_DETAIL_FIXTURE); + }); + + it("calls useQuery directly from the custom hook module", () => { + const source = readFileSync( + resolve(__dirname, "../../src/features/guilds/useGuild.ts"), + "utf8", + ); + + expect(source).toContain("return useQuery(createGuildQueryOptions(guildId));"); + expect(source).not.toMatch(/return\s*\{\s*getGuild/); + }); + + it("builds stable query options with the shared query key helper", () => { + const options = createGuildQueryOptions("guild_abc"); + + expect(options.queryKey).toStrictEqual(guildKeys.detail("guild_abc")); + expect(options.enabled).toBe(true); + }); + + it("does not enable the query when guildId is empty", () => { + const options = createGuildQueryOptions(""); + + expect(options.enabled).toBe(false); + }); + + it("fetches guild data through the refactored query options", async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + const result = await queryClient.fetchQuery(createGuildQueryOptions("guild_abc")); + + expect(getGuild).toHaveBeenCalledWith({ guildId: "guild_abc" }); + expect(result).toStrictEqual(GUILD_DETAIL_FIXTURE); + }); + + it("surfaces SDK errors through the refactored query options", async () => { + getGuild.mockRejectedValueOnce(new Error("Network request failed")); + + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + await expect(queryClient.fetchQuery(createGuildQueryOptions("guild_abc"))).rejects.toThrow( + "Network request failed", + ); + }); + + it("exports a use* hook entry point for screens", () => { + expect(typeof useGuild).toBe("function"); + expect(useGuild.name).toBe("useGuild"); + }); +});