diff --git a/.changeset/unlucky-carrots-exist.md b/.changeset/unlucky-carrots-exist.md new file mode 100644 index 0000000000..58d02fbde9 --- /dev/null +++ b/.changeset/unlucky-carrots-exist.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/clerk-react': patch +'@clerk/types': patch +--- + +Fix methods in clerk-js that consumede paginated endpoints in order to retrieve single resources. diff --git a/packages/clerk-js/src/core/clerk.test.ts b/packages/clerk-js/src/core/clerk.test.ts index 1018b2ce3c..9e74fad62d 100644 --- a/packages/clerk-js/src/core/clerk.test.ts +++ b/packages/clerk-js/src/core/clerk.test.ts @@ -5,7 +5,15 @@ import { mockNativeRuntime } from '../testUtils'; import Clerk from './clerk'; import { eventBus, events } from './events'; import type { AuthConfig, DisplayConfig, Organization } from './resources/internal'; -import { Client, EmailLinkErrorCode, Environment, MagicLinkErrorCode, SignIn, SignUp } from './resources/internal'; +import { + BaseResource, + Client, + EmailLinkErrorCode, + Environment, + MagicLinkErrorCode, + SignIn, + SignUp, +} from './resources/internal'; import { SessionCookieService } from './services'; import { mockJwt } from './test/fixtures'; @@ -2012,4 +2020,20 @@ describe('Clerk singleton', () => { expect(url).toBe('https://rested-anemone-14.accounts.dev/?__dev_session=deadbeef'); }); }); + + describe('Organizations', () => { + it('getOrganization', async () => { + // @ts-ignore + BaseResource._fetch = jest.fn().mockResolvedValue({}); + const sut = new Clerk(devFrontendApi); + + await sut.getOrganization('some-org-id'); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'GET', + path: '/organizations/some-org-id', + }); + }); + }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index dbde35c213..0668c314d6 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1152,10 +1152,8 @@ export default class Clerk implements ClerkInterface { return await OrganizationMembership.retrieve(); }; - public getOrganization = async (organizationId: string): Promise => { - return (await OrganizationMembership.retrieve()).find(orgMem => orgMem.organization.id === organizationId) - ?.organization; - }; + public getOrganization = async (organizationId: string): Promise => + Organization.get(organizationId); public updateEnvironment(environment: EnvironmentResource) { this.#environment = environment; diff --git a/packages/clerk-js/src/core/resources/Organization.ts b/packages/clerk-js/src/core/resources/Organization.ts index 5f327a1ee8..e2414a2c06 100644 --- a/packages/clerk-js/src/core/resources/Organization.ts +++ b/packages/clerk-js/src/core/resources/Organization.ts @@ -342,18 +342,19 @@ export class Organization extends BaseResource implements OrganizationResource { public async reload(params?: ClerkResourceReloadParams): Promise { const { rotatingTokenNonce } = params || {}; - const json = await BaseResource._fetch( - { - method: 'GET', - path: `/me/organization_memberships`, - rotatingTokenNonce, - }, - { forceUpdateClient: true }, - ); - const currentOrganization = (json?.response as unknown as OrganizationMembershipJSON[]).find( - orgMem => orgMem.organization.id === this.id, - ); - return this.fromJSON(currentOrganization?.organization as OrganizationJSON); + + const json = ( + await BaseResource._fetch( + { + path: `/organizations/${this.id}`, + method: 'GET', + rotatingTokenNonce, + }, + { forceUpdateClient: true }, + ) + )?.response as unknown as OrganizationJSON; + + return this.fromJSON(json); } } diff --git a/packages/clerk-js/src/ui/components/OrganizationList/UserInvitationList.tsx b/packages/clerk-js/src/ui/components/OrganizationList/UserInvitationList.tsx index 6754ee517a..c32931a39a 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/UserInvitationList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/UserInvitationList.tsx @@ -1,8 +1,7 @@ import type { OrganizationResource, UserOrganizationInvitationResource } from '@clerk/types'; import { useState } from 'react'; -import { Organization } from '../../../core/resources/Organization'; -import { useCoreOrganizationList } from '../../contexts'; +import { useCoreClerk, useCoreOrganizationList } from '../../contexts'; import { localizationKeys } from '../../customizables'; import { useCardState, withCardStateProvider } from '../../elements'; import { handleError } from '../../utils'; @@ -25,6 +24,7 @@ export const AcceptRejectInvitationButtons = (props: { onAccept: () => void }) = export const InvitationPreview = withCardStateProvider((props: UserOrganizationInvitationResource) => { const card = useCardState(); + const { getOrganization } = useCoreClerk(); const [acceptedOrganization, setAcceptedOrganization] = useState(null); const { userInvitations } = useCoreOrganizationList({ userInvitations: organizationListParams.userInvitations, @@ -34,7 +34,7 @@ export const InvitationPreview = withCardStateProvider((props: UserOrganizationI return card .runAsync(async () => { const updatedItem = await props.accept(); - const organization = await Organization.get(props.publicOrganizationData.id); + const organization = await getOrganization(props.publicOrganizationData.id); return [updatedItem, organization] as const; }) .then(([updatedItem, organization]) => { diff --git a/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx b/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx index 9c7bf470b3..4d032d560e 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx @@ -2,7 +2,11 @@ import { describe } from '@jest/globals'; import { render, waitFor } from '../../../../testUtils'; import { bindCreateFixtures } from '../../../utils/test/createFixtures'; -import { createFakeUserOrganizationMembership } from '../../OrganizationSwitcher/__tests__/utlis'; +import { createFakeOrganization } from '../../CreateOrganization/__tests__/CreateOrganization.test'; +import { + createFakeUserOrganizationInvitation, + createFakeUserOrganizationMembership, +} from '../../OrganizationSwitcher/__tests__/utlis'; import { OrganizationList } from '../OrganizationList'; const { createFixtures } = bindCreateFixtures('OrganizationList'); @@ -109,6 +113,85 @@ describe('OrganizationList', () => { expect(queryByRole('button', { name: 'Create organization' })).toBeInTheDocument(); }); }); + + it('lists invitations', async () => { + const { wrapper, props, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Org1', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const invitation = createFakeUserOrganizationInvitation({ + id: '1', + emailAddress: 'one@clerk.com', + publicOrganizationData: { + name: 'OrgOne', + }, + }); + + invitation.accept = jest.fn().mockResolvedValue( + createFakeUserOrganizationInvitation({ + id: '1', + emailAddress: 'one@clerk.com', + publicOrganizationData: { + name: 'OrgOne', + }, + status: 'accepted', + }), + ); + + fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce( + Promise.resolve({ + data: [invitation], + total_count: 1, + }), + ); + + fixtures.clerk.getOrganization.mockResolvedValue( + createFakeOrganization({ + adminDeleteEnabled: false, + id: '2', + maxAllowedMemberships: 0, + membersCount: 1, + name: 'OrgOne', + pendingInvitationsCount: 0, + slug: '', + }), + ); + + props.setProps({ hidePersonal: true }); + const { queryByText, userEvent, getByRole, queryByRole } = render(, { wrapper }); + + await waitFor(async () => { + // Display membership + expect(queryByText('Org1')).toBeInTheDocument(); + // Display invitation + expect(queryByText('OrgOne')).toBeInTheDocument(); + await userEvent.click(getByRole('button', { name: 'Join' })); + expect(queryByRole('button', { name: 'Join' })).not.toBeInTheDocument(); + }); + }); }); describe('CreateOrganization', () => { diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 16b89c6465..d51e2a93d1 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -719,10 +719,10 @@ export default class IsomorphicClerk { } }; - getOrganization = async (organizationId: string): Promise => { + getOrganization = async (organizationId: string): Promise => { const callback = () => this.clerkjs?.getOrganization(organizationId); if (this.clerkjs && this.#loaded) { - return callback() as Promise; + return callback() as Promise; } else { this.premountMethodCalls.set('getOrganization', callback); } diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index f8a52101fc..28c1653b64 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -442,7 +442,7 @@ export interface Clerk { /** * Retrieves a single organization by id. */ - getOrganization: (organizationId: string) => Promise; + getOrganization: (organizationId: string) => Promise; /** * Handles a 401 response from Frontend API by refreshing the client and session object accordingly