diff --git a/packages/better-auth/src/plugins/organization/client.ts b/packages/better-auth/src/plugins/organization/client.ts index 20daa91d56..4aa1ae7946 100644 --- a/packages/better-auth/src/plugins/organization/client.ts +++ b/packages/better-auth/src/plugins/organization/client.ts @@ -201,7 +201,7 @@ export const organizationClient = ( ); const activeMember = useAuthQuery( - [$activeMemberSignal], + [$activeOrgSignal, $activeMemberSignal], "/organization/get-active-member", $fetch, { @@ -210,7 +210,7 @@ export const organizationClient = ( ); const activeMemberRole = useAuthQuery<{ role: string }>( - [$activeMemberRoleSignal], + [$activeOrgSignal, $activeMemberRoleSignal], "/organization/get-active-member-role", $fetch, { @@ -252,22 +252,17 @@ export const organizationClient = ( }, { matcher(path) { - return path.startsWith("/organization/set-active"); + return ( + path.startsWith("/organization/set-active") || + path === "/organization/create" || + path === "/organization/delete" || + path === "/organization/remove-member" || + path === "/organization/leave" || + path === "/organization/accept-invitation" + ); }, signal: "$sessionSignal", }, - { - matcher(path) { - return path.includes("/organization/update-member-role"); - }, - signal: "$activeMemberSignal", - }, - { - matcher(path) { - return path.includes("/organization/update-member-role"); - }, - signal: "$activeMemberRoleSignal", - }, ], $ERROR_CODES: ORGANIZATION_ERROR_CODES, } satisfies BetterAuthClientPlugin; diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts index 7e22e04b43..7eb3d5ef81 100644 --- a/packages/better-auth/src/plugins/organization/organization.test.ts +++ b/packages/better-auth/src/plugins/organization/organization.test.ts @@ -1,7 +1,7 @@ import type { APIError } from "@better-auth/core/error"; import { memoryAdapter } from "@better-auth/memory-adapter"; import type { Prettify } from "better-call"; -import { describe, expect, expectTypeOf, it } from "vitest"; +import { describe, expect, expectTypeOf, it, onTestFinished } from "vitest"; import type { BetterFetchError, PreinitializedWritableAtom, @@ -2421,6 +2421,188 @@ describe("Additional Fields", async () => { expect(data.someHiddenField).toBeUndefined(); }); + /** + * @see https://github.com/better-auth/better-auth/issues/7981 + */ + describe("active organization hook refresh", () => { + type QueryState = { + data: T | null; + isPending: boolean; + isRefetching: boolean; + }; + + type QueryAtomLike = { + get: () => QueryState; + subscribe: (listener: (state: QueryState) => void) => () => void; + }; + + const startClientSideQuery = () => { + const previousWindow = global.window as + | (Window & typeof globalThis) + | undefined; + global.window = {} as unknown as Window & typeof globalThis; + return () => { + global.window = previousWindow as unknown as Window & typeof globalThis; + }; + }; + + const registerActiveOrganizationCleanup = (restoreWindow: () => void) => { + onTestFinished(async () => { + restoreWindow(); + await client.organization + .setActive({ + organizationId: org.id, + fetchOptions: { + headers, + }, + }) + .catch(() => undefined); + }); + }; + + const waitForQueryData = async ( + query: QueryAtomLike, + matches: (data: T) => boolean, + triggerFetch = false, + ) => { + return new Promise((resolve, reject) => { + let unsubscribe = () => {}; + const timeoutId = setTimeout(() => { + unsubscribe(); + reject(new Error("Timed out waiting for query data")); + }, 1000); + + unsubscribe = query.subscribe((state) => { + if (state.isPending || state.isRefetching || state.data === null) { + return; + } + if (!matches(state.data)) { + return; + } + clearTimeout(timeoutId); + unsubscribe(); + resolve(state.data); + }); + + if (triggerFetch) { + query.get(); + } + }); + }; + + it("updates active member when setActive changes organization", async () => { + await client.organization.setActive({ + organizationId: org.id, + fetchOptions: { + headers, + }, + }); + + const secondOrganization = await auth.api.createOrganization({ + body: { + name: "test-issue-7981", + slug: "test-issue-7981", + someRequiredField: "issue-7981-required", + keepCurrentActiveOrganization: true, + }, + headers, + }); + + if (!secondOrganization) { + throw new Error("Second organization is null"); + } + + const restoreWindow = startClientSideQuery(); + registerActiveOrganizationCleanup(restoreWindow); + + const activeMemberQuery = orgClientPlugin.getAtoms( + client.$fetch, + ).activeMember; + await waitForQueryData( + activeMemberQuery, + (member) => member.organizationId === org.id, + true, + ); + const switchedMember = waitForQueryData( + activeMemberQuery, + (member) => member.organizationId === secondOrganization.id, + ); + + await client.organization.setActive({ + organizationId: secondOrganization.id, + fetchOptions: { + headers, + }, + }); + + const updatedMember = await switchedMember; + expect(updatedMember.organizationId).toBe(secondOrganization.id); + }); + + it("updates session and active member when create switches active organization", async () => { + await client.organization.setActive({ + organizationId: org.id, + fetchOptions: { + headers, + }, + }); + + const restoreWindow = startClientSideQuery(); + registerActiveOrganizationCleanup(restoreWindow); + + const activeMemberQuery = orgClientPlugin.getAtoms( + client.$fetch, + ).activeMember; + const sessionQuery = client.useSession; + + await Promise.all([ + waitForQueryData( + activeMemberQuery, + (member) => member.organizationId === org.id, + true, + ), + waitForQueryData( + sessionQuery, + (session) => session.session.activeOrganizationId === org.id, + true, + ), + ]); + await new Promise((resolve) => setTimeout(resolve, 30)); + + const switchedMember = waitForQueryData( + activeMemberQuery, + (member) => member.organizationId !== org.id, + ); + const switchedSession = waitForQueryData( + sessionQuery, + (session) => + Boolean(session.session.activeOrganizationId) && + session.session.activeOrganizationId !== org.id, + ); + + const createdOrganization = await client.organization.create({ + name: "test-issue-7981-create", + slug: "test-issue-7981-create", + someRequiredField: "issue-7981-create-required", + fetchOptions: { + headers, + }, + }); + if (!createdOrganization.data) { + throw createdOrganization.error || new Error("Create failed"); + } + + const [updatedMember, updatedSession] = await Promise.all([ + switchedMember, + switchedSession, + ]); + expect(updatedMember.organizationId).toBe(createdOrganization.data.id); + expect(updatedSession.session.activeOrganizationId).toBe( + createdOrganization.data.id, + ); + }); + }); + it("getFullOrganization", async () => { const res = await auth.api.getFullOrganization({ query: {