diff --git a/.changeset/orange-tigers-act.md b/.changeset/orange-tigers-act.md
new file mode 100644
index 0000000000..4352bfc2e7
--- /dev/null
+++ b/.changeset/orange-tigers-act.md
@@ -0,0 +1,11 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/shared': minor
+'@clerk/clerk-react': minor
+'@clerk/types': minor
+---
+
+Introduces userInvitations from `useOrganizationList`
+
+`userInvitations` is a paginated list of data. It can be used to create Paginated tables or Infinite lists.
+
diff --git a/package-lock.json b/package-lock.json
index e473d9fcc4..ad35540a8d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37884,9 +37884,12 @@
}
},
"node_modules/swr": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz",
- "integrity": "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
+ "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.0"
+ },
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
@@ -39901,7 +39904,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
- "peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
@@ -41402,10 +41404,10 @@
},
"packages/backend": {
"name": "@clerk/backend",
- "version": "0.26.0",
+ "version": "0.27.0",
"license": "MIT",
"dependencies": {
- "@clerk/types": "^3.48.1",
+ "@clerk/types": "^3.49.0",
"@peculiar/webcrypto": "1.4.1",
"@types/node": "16.18.6",
"cookie": "0.5.0",
@@ -41448,11 +41450,11 @@
},
"packages/chrome-extension": {
"name": "@clerk/chrome-extension",
- "version": "0.3.25",
+ "version": "0.3.26",
"license": "MIT",
"dependencies": {
- "@clerk/clerk-js": "^4.54.2",
- "@clerk/clerk-react": "^4.23.1"
+ "@clerk/clerk-js": "^4.55.0",
+ "@clerk/clerk-react": "^4.23.2"
},
"devDependencies": {
"@types/chrome": "*",
@@ -41468,12 +41470,12 @@
},
"packages/clerk-js": {
"name": "@clerk/clerk-js",
- "version": "4.54.2",
+ "version": "4.55.0",
"license": "MIT",
"dependencies": {
- "@clerk/localizations": "^1.24.0",
- "@clerk/shared": "^0.20.0",
- "@clerk/types": "^3.48.1",
+ "@clerk/localizations": "^1.24.1",
+ "@clerk/shared": "^0.21.0",
+ "@clerk/types": "^3.49.0",
"@emotion/cache": "11.10.5",
"@emotion/react": "11.10.5",
"@floating-ui/react": "0.19.0",
@@ -41890,16 +41892,16 @@
},
"packages/expo": {
"name": "@clerk/clerk-expo",
- "version": "0.18.16",
+ "version": "0.18.17",
"license": "MIT",
"dependencies": {
- "@clerk/clerk-js": "^4.54.2",
- "@clerk/clerk-react": "^4.23.1",
+ "@clerk/clerk-js": "^4.55.0",
+ "@clerk/clerk-react": "^4.23.2",
"base-64": "1.0.0",
"react-native-url-polyfill": "1.3.0"
},
"devDependencies": {
- "@clerk/types": "^3.48.1",
+ "@clerk/types": "^3.49.0",
"@types/base-64": "^1.0.0",
"@types/node": "^16.11.55",
"@types/react": "*",
@@ -41918,11 +41920,11 @@
},
"packages/fastify": {
"name": "@clerk/fastify",
- "version": "0.6.2",
+ "version": "0.6.3",
"license": "MIT",
"dependencies": {
- "@clerk/backend": "^0.26.0",
- "@clerk/types": "^3.48.1",
+ "@clerk/backend": "^0.27.0",
+ "@clerk/types": "^3.49.0",
"cookies": "0.8.0"
},
"devDependencies": {
@@ -41938,13 +41940,13 @@
}
},
"packages/gatsby-plugin-clerk": {
- "version": "4.4.3",
+ "version": "4.4.4",
"license": "MIT",
"dependencies": {
- "@clerk/backend": "^0.26.0",
- "@clerk/clerk-react": "^4.23.1",
- "@clerk/clerk-sdk-node": "^4.12.1",
- "@clerk/types": "^3.48.1",
+ "@clerk/backend": "^0.27.0",
+ "@clerk/clerk-react": "^4.23.2",
+ "@clerk/clerk-sdk-node": "^4.12.2",
+ "@clerk/types": "^3.49.0",
"cookie": "0.5.0",
"tslib": "2.4.1"
},
@@ -41967,10 +41969,10 @@
},
"packages/localizations": {
"name": "@clerk/localizations",
- "version": "1.24.0",
+ "version": "1.24.1",
"license": "MIT",
"dependencies": {
- "@clerk/types": "^3.48.1"
+ "@clerk/types": "^3.49.0"
},
"devDependencies": {
"tsup": "*",
@@ -41985,13 +41987,13 @@
},
"packages/nextjs": {
"name": "@clerk/nextjs",
- "version": "4.23.1",
+ "version": "4.23.2",
"license": "MIT",
"dependencies": {
- "@clerk/backend": "^0.26.0",
- "@clerk/clerk-react": "^4.23.1",
- "@clerk/clerk-sdk-node": "^4.12.1",
- "@clerk/types": "^3.48.1",
+ "@clerk/backend": "^0.27.0",
+ "@clerk/clerk-react": "^4.23.2",
+ "@clerk/clerk-sdk-node": "^4.12.2",
+ "@clerk/types": "^3.49.0",
"path-to-regexp": "6.2.1",
"tslib": "2.4.1"
},
@@ -42024,11 +42026,11 @@
},
"packages/react": {
"name": "@clerk/clerk-react",
- "version": "4.23.1",
+ "version": "4.23.2",
"license": "MIT",
"dependencies": {
- "@clerk/shared": "^0.20.0",
- "@clerk/types": "^3.48.1",
+ "@clerk/shared": "^0.21.0",
+ "@clerk/types": "^3.49.0",
"tslib": "2.4.1"
},
"devDependencies": {
@@ -42051,13 +42053,13 @@
},
"packages/remix": {
"name": "@clerk/remix",
- "version": "2.9.0",
+ "version": "2.9.1",
"license": "MIT",
"dependencies": {
- "@clerk/backend": "^0.26.0",
- "@clerk/clerk-react": "^4.23.1",
- "@clerk/shared": "^0.20.0",
- "@clerk/types": "^3.48.1",
+ "@clerk/backend": "^0.27.0",
+ "@clerk/clerk-react": "^4.23.2",
+ "@clerk/shared": "^0.21.0",
+ "@clerk/types": "^3.49.0",
"cookie": "0.5.0",
"tslib": "2.4.1"
},
@@ -42085,11 +42087,11 @@
},
"packages/sdk-node": {
"name": "@clerk/clerk-sdk-node",
- "version": "4.12.1",
+ "version": "4.12.2",
"license": "MIT",
"dependencies": {
- "@clerk/backend": "^0.26.0",
- "@clerk/types": "^3.48.1",
+ "@clerk/backend": "^0.27.0",
+ "@clerk/types": "^3.49.0",
"@types/cookies": "0.7.7",
"@types/express": "4.17.14",
"@types/node-fetch": "2.6.2",
@@ -42127,15 +42129,15 @@
},
"packages/shared": {
"name": "@clerk/shared",
- "version": "0.20.0",
+ "version": "0.21.0",
"license": "ISC",
"dependencies": {
"glob-to-regexp": "0.4.1",
"js-cookie": "3.0.1",
- "swr": "1.3.0"
+ "swr": "2.2.0"
},
"devDependencies": {
- "@clerk/types": "^3.48.1",
+ "@clerk/types": "^3.49.0",
"@types/glob-to-regexp": "0.4.1",
"@types/js-cookie": "3.0.2",
"tsup": "*",
@@ -42150,7 +42152,7 @@
"version": "1.7.5",
"license": "MIT",
"devDependencies": {
- "@clerk/types": "^3.48.1",
+ "@clerk/types": "^3.49.0",
"typescript": "*"
},
"engines": {
@@ -42162,7 +42164,7 @@
},
"packages/types": {
"name": "@clerk/types",
- "version": "3.48.1",
+ "version": "3.49.0",
"license": "MIT",
"dependencies": {
"csstype": "3.1.1"
diff --git a/packages/clerk-js/src/core/resources/UserOrganizationInvitation.ts b/packages/clerk-js/src/core/resources/UserOrganizationInvitation.ts
index 378f5f6350..52e7f42df0 100644
--- a/packages/clerk-js/src/core/resources/UserOrganizationInvitation.ts
+++ b/packages/clerk-js/src/core/resources/UserOrganizationInvitation.ts
@@ -8,6 +8,7 @@ import type {
} from '@clerk/types';
import { unixEpochToDate } from '../../utils/date';
+import { convertPageToOffset } from '../../utils/pagesToOffset';
import { BaseResource } from './internal';
export class UserOrganizationInvitation extends BaseResource implements UserOrganizationInvitationResource {
@@ -32,7 +33,7 @@ export class UserOrganizationInvitation extends BaseResource implements UserOrga
return await BaseResource._fetch({
path: '/me/organization_invitations',
method: 'GET',
- search: params as any,
+ search: convertPageToOffset(params) as any,
})
.then(res => {
const { data: invites, total_count } =
diff --git a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx
index 8709f987f2..0d427f39ae 100644
--- a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx
+++ b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { CoreClerkContext } from './CoreClerkContext';
import { CoreClientContext } from './CoreClientContext';
-import { CoreOrganizationContext } from './CoreOrganizationContext';
+import { CoreOrganizationProvider } from './CoreOrganizationContext';
import { CoreSessionContext } from './CoreSessionContext';
import { CoreUserContext } from './CoreUserContext';
import { assertClerkSingletonExists } from './utils';
@@ -11,6 +11,7 @@ import { assertClerkSingletonExists } from './utils';
type CoreClerkContextWrapperProps = {
clerk: Clerk;
children: React.ReactNode;
+ swrConfig?: any;
};
type CoreClerkContextProviderState = Resources;
@@ -51,9 +52,12 @@ export function CoreClerkContextWrapper(props: CoreClerkContextWrapperProps): JS
-
+
{props.children}
-
+
diff --git a/packages/clerk-js/src/ui/contexts/CoreOrganizationContext.tsx b/packages/clerk-js/src/ui/contexts/CoreOrganizationContext.tsx
index 93041c60a3..5ece45ac17 100644
--- a/packages/clerk-js/src/ui/contexts/CoreOrganizationContext.tsx
+++ b/packages/clerk-js/src/ui/contexts/CoreOrganizationContext.tsx
@@ -1,6 +1,6 @@
-import { OrganizationContext, useOrganization, useOrganizationList, useOrganizations } from '@clerk/shared';
+import { OrganizationProvider, useOrganization, useOrganizationList, useOrganizations } from '@clerk/shared';
-export const CoreOrganizationContext = OrganizationContext;
+export const CoreOrganizationProvider = OrganizationProvider;
export const useCoreOrganization = useOrganization;
export const useCoreOrganizationList = useOrganizationList;
export const useCoreOrganizations = useOrganizations;
diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx
new file mode 100644
index 0000000000..6a0d62489b
--- /dev/null
+++ b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx
@@ -0,0 +1,267 @@
+import type { MembershipRole, OrganizationInvitationStatus, UserOrganizationInvitationResource } from '@clerk/types';
+import { describe, jest } from '@jest/globals';
+import React from 'react';
+
+import { act, bindCreateFixtures, renderHook, waitFor } from '../../../testUtils';
+import { useCoreOrganizationList } from '../../contexts';
+
+const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');
+
+const defaultRenderer = () =>
+ useCoreOrganizationList({
+ userInvitations: {
+ pageSize: 2,
+ },
+ });
+
+type FakeOrganizationParams = {
+ id: string;
+ createdAt?: Date;
+ emailAddress: string;
+ role?: MembershipRole;
+ status?: OrganizationInvitationStatus;
+};
+
+const createFakeUserOrganizationInvitations = (params: FakeOrganizationParams): UserOrganizationInvitationResource => {
+ return {
+ pathRoot: '',
+ emailAddress: params.emailAddress,
+ publicOrganizationData: { hasImage: false, id: '', imageUrl: '', name: '', slug: '' },
+ role: params.role || 'basic_member',
+ status: params.status || 'pending',
+ id: params.id,
+ createdAt: params?.createdAt || new Date(),
+ updatedAt: new Date(),
+ publicMetadata: {},
+ accept: jest.fn() as any,
+ reload: jest.fn() as any,
+ };
+};
+
+describe('useOrganizationList', () => {
+ it('opens organization profile when "Manage Organization" is clicked', async () => {
+ const { wrapper } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.dev'],
+ organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
+ });
+ });
+
+ const { result } = renderHook(useCoreOrganizationList, { wrapper });
+
+ expect(result.current.isLoaded).toBe(true);
+ expect(result.current.setActive).toBeDefined();
+ expect(result.current.createOrganization).toBeDefined();
+ expect(result.current.organizationList).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ membership: expect.objectContaining({
+ role: 'basic_member',
+ }),
+ }),
+ ]),
+ );
+
+ expect(result.current.userInvitations).toEqual(
+ expect.objectContaining({
+ data: [],
+ count: 0,
+ isLoading: false,
+ isFetching: false,
+ isError: false,
+ page: 1,
+ pageCount: 0,
+ hasNextPage: false,
+ hasPreviousPage: false,
+ }),
+ );
+ });
+
+ it.only('opens organization profile when "Manage Organization" is clicked', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.dev'],
+ organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
+ });
+ });
+
+ fixtures.clerk.user?.getOrganizationInvitations.mockReturnValue(
+ Promise.resolve({
+ data: [
+ createFakeUserOrganizationInvitations({
+ id: '1',
+ emailAddress: 'one@clerk.com',
+ }),
+ createFakeUserOrganizationInvitations({
+ id: '2',
+ emailAddress: 'two@clerk.com',
+ }),
+ ],
+ total_count: 4,
+ }),
+ );
+ const { result } = renderHook(defaultRenderer, { wrapper });
+ expect(result.current.userInvitations.isLoading).toBe(true);
+ expect(result.current.userInvitations.count).toBe(0);
+
+ await waitFor(() => {
+ expect(result.current.userInvitations.isLoading).toBe(false);
+ expect(result.current.userInvitations.count).toBe(4);
+ expect(result.current.userInvitations.page).toBe(1);
+ expect(result.current.userInvitations.pageCount).toBe(2);
+ expect(result.current.userInvitations.hasNextPage).toBe(true);
+ });
+
+ fixtures.clerk.user?.getOrganizationInvitations.mockReturnValue(
+ Promise.resolve({
+ data: [
+ createFakeUserOrganizationInvitations({
+ id: '3',
+ emailAddress: 'three@clerk.com',
+ }),
+ createFakeUserOrganizationInvitations({
+ id: '4',
+ emailAddress: 'four@clerk.com',
+ }),
+ ],
+ total_count: 4,
+ }),
+ );
+
+ act(() => {
+ result.current.userInvitations.fetchNext?.();
+ });
+
+ await waitFor(() => {
+ expect(result.current.userInvitations.isLoading).toBe(true);
+ });
+
+ await waitFor(() => {
+ expect(result.current.userInvitations.isLoading).toBe(false);
+ expect(result.current.userInvitations.page).toBe(2);
+ expect(result.current.userInvitations.hasNextPage).toBe(false);
+ expect(result.current.userInvitations.data).toEqual(
+ expect.arrayContaining([
+ expect.not.objectContaining({
+ id: '1',
+ }),
+ expect.not.objectContaining({
+ id: '2',
+ }),
+ expect.objectContaining({
+ id: '3',
+ }),
+ expect.objectContaining({
+ id: '4',
+ }),
+ ]),
+ );
+ });
+ });
+
+ it.only('infinite', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.dev'],
+ organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
+ });
+ });
+
+ fixtures.clerk.user?.getOrganizationInvitations.mockReturnValue(
+ Promise.resolve({
+ data: [
+ createFakeUserOrganizationInvitations({
+ id: '1',
+ emailAddress: 'one@clerk.com',
+ }),
+ createFakeUserOrganizationInvitations({
+ id: '2',
+ emailAddress: 'two@clerk.com',
+ }),
+ ],
+ total_count: 4,
+ }),
+ );
+ const { result } = renderHook(
+ () =>
+ useCoreOrganizationList({
+ userInvitations: {
+ pageSize: 2,
+ infinite: true,
+ },
+ }),
+ { wrapper },
+ );
+ expect(result.current.userInvitations.isLoading).toBe(true);
+ expect(result.current.userInvitations.isFetching).toBe(true);
+
+ await waitFor(() => {
+ expect(result.current.userInvitations.isLoading).toBe(false);
+ expect(result.current.userInvitations.isFetching).toBe(false);
+ });
+
+ fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
+ Promise.resolve({
+ data: [
+ createFakeUserOrganizationInvitations({
+ id: '1',
+ emailAddress: 'one@clerk.com',
+ }),
+ createFakeUserOrganizationInvitations({
+ id: '2',
+ emailAddress: 'two@clerk.com',
+ }),
+ ],
+ total_count: 4,
+ }),
+ );
+
+ fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
+ Promise.resolve({
+ data: [
+ createFakeUserOrganizationInvitations({
+ id: '3',
+ emailAddress: 'three@clerk.com',
+ }),
+ createFakeUserOrganizationInvitations({
+ id: '4',
+ emailAddress: 'four@clerk.com',
+ }),
+ ],
+ total_count: 4,
+ }),
+ );
+
+ act(() => {
+ result.current.userInvitations.fetchNext?.();
+ });
+
+ await waitFor(() => {
+ expect(result.current.userInvitations.isLoading).toBe(false);
+ expect(result.current.userInvitations.isFetching).toBe(true);
+ });
+
+ await waitFor(() => {
+ expect(result.current.userInvitations.isFetching).toBe(false);
+ expect(result.current.userInvitations.data).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: '1',
+ }),
+ expect.objectContaining({
+ id: '2',
+ }),
+ expect.objectContaining({
+ id: '3',
+ }),
+ expect.objectContaining({
+ id: '4',
+ }),
+ ]),
+ );
+ });
+ });
+});
diff --git a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx
index 7284b2fabc..40418434aa 100644
--- a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx
+++ b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx
@@ -1,7 +1,6 @@
import type { ClerkOptions, ClientJSON, EnvironmentJSON, LoadedClerk } from '@clerk/types';
import { jest } from '@jest/globals';
import React from 'react';
-import { SWRConfig } from 'swr';
import { default as ClerkCtor } from '../../../core/clerk';
import { Client, Environment } from '../../../core/resources';
@@ -85,25 +84,27 @@ const unboundCreateFixtures = [
const MockClerkProvider = (props: any) => {
const { children } = props;
return (
- new Map(), dedupingInterval: 0 }}>
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+ new Map() }}
+ >
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/clerk-js/src/utils/pagesToOffset.ts b/packages/clerk-js/src/utils/pagesToOffset.ts
new file mode 100644
index 0000000000..131c27b342
--- /dev/null
+++ b/packages/clerk-js/src/utils/pagesToOffset.ts
@@ -0,0 +1,18 @@
+import type { ClerkPaginationParams } from '@clerk/types';
+
+type Pages = {
+ initialPage?: number;
+ pageSize?: number;
+};
+
+export function convertPageToOffset(pageParams: T): ClerkPaginationParams {
+ const { pageSize, initialPage, ...restParams } = pageParams || {};
+ const _pageSize = pageSize ?? 10;
+ const _initialPage = initialPage ?? 1;
+
+ return {
+ ...restParams,
+ limit: _pageSize,
+ offset: (_initialPage - 1) * _pageSize,
+ };
+}
diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx
index fd6a576345..5973c95576 100644
--- a/packages/react/src/contexts/ClerkContextProvider.tsx
+++ b/packages/react/src/contexts/ClerkContextProvider.tsx
@@ -7,7 +7,7 @@ import { deriveState } from '../utils/deriveState';
import { AuthContext } from './AuthContext';
import { ClientContext } from './ClientContext';
import { IsomorphicClerkContext } from './IsomorphicClerkContext';
-import { OrganizationContext } from './OrganizationContext';
+import { OrganizationProvider } from './OrganizationContext';
import { SessionContext } from './SessionContext';
import { UserContext } from './UserContext';
@@ -74,11 +74,11 @@ export function ClerkContextProvider(props: ClerkContextProvider): JSX.Element |
-
+
{children}
-
+
diff --git a/packages/react/src/contexts/OrganizationContext.tsx b/packages/react/src/contexts/OrganizationContext.tsx
index 4dfad56e2d..13ed6bf2e2 100644
--- a/packages/react/src/contexts/OrganizationContext.tsx
+++ b/packages/react/src/contexts/OrganizationContext.tsx
@@ -1 +1 @@
-export { OrganizationContext, useOrganizationContext } from '@clerk/shared';
+export { OrganizationProvider, OrganizationContext, useOrganizationContext } from '@clerk/shared';
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 1c8c589b8e..697f38b608 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -52,6 +52,6 @@
"dependencies": {
"glob-to-regexp": "0.4.1",
"js-cookie": "3.0.1",
- "swr": "1.3.0"
+ "swr": "2.2.0"
}
}
diff --git a/packages/shared/src/hooks/contexts.tsx b/packages/shared/src/hooks/contexts.tsx
index b9d08790c7..bd5c2e08da 100644
--- a/packages/shared/src/hooks/contexts.tsx
+++ b/packages/shared/src/hooks/contexts.tsx
@@ -7,6 +7,9 @@ import type {
OrganizationResource,
UserResource,
} from '@clerk/types';
+import type { PropsWithChildren } from 'react';
+import React from 'react';
+import { SWRConfig } from 'swr';
import { createContextAndHook } from './createContextAndHook';
@@ -16,16 +19,56 @@ const [ClientContext, useClientContext] = createContextAndHook(
'SessionContext',
);
-const [OrganizationContext, useOrganizationContext] = createContextAndHook<{
+
+type OrganizationContextProps = {
+ organization: OrganizationResource | null | undefined;
+ lastOrganizationInvitation: OrganizationInvitationResource | null | undefined;
+ lastOrganizationMember: OrganizationMembershipResource | null | undefined;
+};
+const [OrganizationContextInternal, useOrganizationContext] = createContextAndHook<{
organization: OrganizationResource | null | undefined;
lastOrganizationInvitation: OrganizationInvitationResource | null | undefined;
lastOrganizationMember: OrganizationMembershipResource | null | undefined;
}>('OrganizationContext');
+const OrganizationProvider = ({
+ children,
+ organization,
+ lastOrganizationMember,
+ lastOrganizationInvitation,
+ swrConfig,
+}: PropsWithChildren<
+ OrganizationContextProps & {
+ // Exporting inferred types directly from SWR will result in error while building declarations
+ swrConfig?: any;
+ }
+>) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+/**
+ * @deprecated use OrganizationProvider instead
+ */
+export const OrganizationContext = OrganizationProvider;
+
export {
ClientContext,
useClientContext,
- OrganizationContext,
+ OrganizationProvider,
useOrganizationContext,
UserContext,
useUserContext,
diff --git a/packages/shared/src/hooks/types.ts b/packages/shared/src/hooks/types.ts
new file mode 100644
index 0000000000..a2614cb214
--- /dev/null
+++ b/packages/shared/src/hooks/types.ts
@@ -0,0 +1,22 @@
+export type ValueOrSetter = (size: T | ((_size: T) => T)) => void;
+export type PaginatedResources = {
+ data: T[];
+ count: number;
+ isLoading: boolean;
+ isFetching: boolean;
+ isError: boolean;
+ page: number;
+ pageCount: number;
+ fetchPage: ValueOrSetter;
+ fetchPrevious: () => void;
+ fetchNext: () => void;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+};
+
+// Utility type to convert PaginatedDataAPI to properties as undefined, except booleans set to false
+export type PaginatedResourcesWithDefault = {
+ [K in keyof PaginatedResources]: PaginatedResources[K] extends boolean
+ ? false
+ : PaginatedResources[K] | undefined;
+};
diff --git a/packages/shared/src/hooks/useOrganizationList.tsx b/packages/shared/src/hooks/useOrganizationList.tsx
index 23bbcbab0a..990e3c2643 100644
--- a/packages/shared/src/hooks/useOrganizationList.tsx
+++ b/packages/shared/src/hooks/useOrganizationList.tsx
@@ -1,39 +1,221 @@
import type {
+ ClerkPaginatedResponse,
CreateOrganizationParams,
+ GetUserOrganizationInvitationsParams,
OrganizationMembershipResource,
OrganizationResource,
SetActive,
+ UserOrganizationInvitationResource,
+ UserResource,
} from '@clerk/types';
+import { useCallback, useMemo, useRef, useState } from 'react';
+import useSWR from 'swr';
+import useSWRInfinite from 'swr/infinite';
import { useClerkInstanceContext, useUserContext } from './contexts';
+import type { PaginatedResources, PaginatedResourcesWithDefault, ValueOrSetter } from './types';
+
+type UseOrganizationListParams = {
+ userInvitations?:
+ | true
+ | (GetUserOrganizationInvitationsParams & {
+ infinite?: boolean;
+ keepPreviousData?: boolean;
+ });
+};
type OrganizationList = ReturnType;
type UseOrganizationListReturn =
- | { isLoaded: false; organizationList: undefined; createOrganization: undefined; setActive: undefined }
| {
- isLoaded: true;
+ isLoaded: false;
+ organizationList: undefined;
+ createOrganization: undefined;
+ setActive: undefined;
+ userInvitations: PaginatedResourcesWithDefault;
+ }
+ | {
+ isLoaded: boolean;
organizationList: OrganizationList;
createOrganization: (params: CreateOrganizationParams) => Promise;
setActive: SetActive;
+ userInvitations: PaginatedResources;
};
-type UseOrganizationList = () => UseOrganizationListReturn;
+type UseOrganizationList = (params?: UseOrganizationListParams) => UseOrganizationListReturn;
+
+export const useOrganizationList: UseOrganizationList = params => {
+ const { userInvitations } = params || {};
+
+ const shouldUseDefaultOptions = userInvitations === true;
+ const [paginatedPage, setPaginatedPage] = useState(shouldUseDefaultOptions ? 1 : userInvitations?.initialPage ?? 1);
+
+ // Cache initialPage and initialPageSize until unmount
+ const initialPageRef = useRef(shouldUseDefaultOptions ? 1 : userInvitations?.initialPage ?? 1);
+ const pageSizeRef = useRef(shouldUseDefaultOptions ? 10 : userInvitations?.pageSize ?? 10);
+
+ const triggerInfinite = shouldUseDefaultOptions ? false : !!userInvitations?.infinite;
+ const internalKeepPreviousData = shouldUseDefaultOptions ? false : !!userInvitations?.keepPreviousData;
+ const internalStatus = shouldUseDefaultOptions ? 'pending' : userInvitations?.status ?? 'pending';
-export const useOrganizationList: UseOrganizationList = () => {
const clerk = useClerkInstanceContext();
const user = useUserContext();
+ const paginatedParams =
+ typeof userInvitations === 'undefined'
+ ? undefined
+ : {
+ initialPage: paginatedPage,
+ pageSize: pageSizeRef.current,
+ status: internalStatus,
+ };
+
+ const canFetch = !!(clerk.loaded && user);
+
+ const fetchInvitations = () => user!.getOrganizationInvitations(paginatedParams);
+
+ const {
+ data: userInvitationsData,
+ isValidating: userInvitationsValidating,
+ isLoading: userInvitationsLoading,
+ error: userInvitationsError,
+ mutate: userInvitationsMutate,
+ } = useSWR(
+ !triggerInfinite && canFetch && paginatedParams ? cacheKey('userInvitations', user, paginatedParams) : null,
+ fetchInvitations,
+ { keepPreviousData: internalKeepPreviousData },
+ );
+
+ const getInfiniteKey = (
+ pageIndex: number,
+ previousPageData: ClerkPaginatedResponse | null,
+ ) => {
+ if (!canFetch || !paginatedParams || !triggerInfinite) {
+ return null;
+ }
+
+ return cacheKey('userInvitations', user, {
+ initialPage: initialPageRef.current + pageIndex,
+ pageSize: pageSizeRef.current,
+ status: internalStatus,
+ });
+ };
+
+ const {
+ data: userInvitationsDataInfinite,
+ isLoading: userInvitationsLoadingInfinite,
+ isValidating: userInvitationsInfiniteValidating,
+ error: userInvitationsInfiniteError,
+ size,
+ setSize,
+ mutate: userInvitationsInfiniteMutate,
+ } = useSWRInfinite(getInfiniteKey, ({ initialPage, pageSize, status }) => {
+ return user!.getOrganizationInvitations({
+ initialPage,
+ pageSize,
+ status,
+ });
+ });
+
+ const isomorphicPage = useMemo(() => {
+ if (triggerInfinite) {
+ return size;
+ }
+ return paginatedPage;
+ }, [triggerInfinite, size, paginatedPage]);
+
+ const isomorphicSetPage: ValueOrSetter = useCallback(
+ numberOrgFn => {
+ if (triggerInfinite) {
+ void setSize(numberOrgFn);
+ return;
+ }
+ return setPaginatedPage(numberOrgFn);
+ },
+ [setSize],
+ );
+
+ const isomorphicData = useMemo(() => {
+ if (triggerInfinite) {
+ return userInvitationsDataInfinite?.map(a => a?.data).flat() ?? [];
+ }
+ return userInvitationsData?.data ?? [];
+ }, [triggerInfinite, userInvitationsDataInfinite, userInvitationsData]);
+
+ const isomorphicCount = useMemo(() => {
+ if (triggerInfinite) {
+ return userInvitationsDataInfinite?.[userInvitationsDataInfinite?.length - 1]?.total_count || 0;
+ }
+ return userInvitationsData?.total_count ?? 0;
+ }, [triggerInfinite, userInvitationsDataInfinite, userInvitationsData]);
+
+ const isomorphicIsLoading = triggerInfinite ? userInvitationsLoadingInfinite : userInvitationsLoading;
+ const isomorphicIsFetching = triggerInfinite ? userInvitationsInfiniteValidating : userInvitationsValidating;
+ const isomorphicIsError = !!(triggerInfinite ? userInvitationsInfiniteError : userInvitationsError);
+ /**
+ * Helpers
+ */
+ const fetchNext = useCallback(() => {
+ isomorphicSetPage(n => Math.max(0, n + 1));
+ }, [isomorphicSetPage]);
+
+ const fetchPrevious = useCallback(() => {
+ isomorphicSetPage(n => Math.max(0, n - 1));
+ }, [isomorphicSetPage]);
+
+ const offsetCount = (initialPageRef.current - 1) * pageSizeRef.current;
+
+ const pageCount = Math.ceil((isomorphicCount - offsetCount) / pageSizeRef.current);
+ const hasNextPage = isomorphicCount - offsetCount * pageSizeRef.current > isomorphicPage * pageSizeRef.current;
+ const hasPreviousPage = (isomorphicPage - 1) * pageSizeRef.current > offsetCount * pageSizeRef.current;
+
+ const unstable__mutate = triggerInfinite ? userInvitationsInfiniteMutate : userInvitationsMutate;
+
// TODO: Properly check for SSR user values
if (!clerk.loaded || !user) {
- return { isLoaded: false, organizationList: undefined, createOrganization: undefined, setActive: undefined };
+ return {
+ isLoaded: false,
+ organizationList: undefined,
+ createOrganization: undefined,
+ setActive: undefined,
+ userInvitations: {
+ data: undefined,
+ count: undefined,
+ isLoading: false,
+ isFetching: false,
+ isError: false,
+ page: undefined,
+ pageCount: undefined,
+ fetchPage: undefined,
+ fetchNext: undefined,
+ fetchPrevious: undefined,
+ hasNextPage: false,
+ hasPreviousPage: false,
+ unstable__mutate: undefined,
+ },
+ };
}
return {
- isLoaded: true,
+ isLoaded: canFetch,
organizationList: createOrganizationList(user.organizationMemberships),
setActive: clerk.setActive,
createOrganization: clerk.createOrganization,
+ userInvitations: {
+ data: isomorphicData,
+ count: isomorphicCount,
+ isLoading: isomorphicIsLoading,
+ isFetching: isomorphicIsFetching,
+ isError: isomorphicIsError,
+ page: isomorphicPage,
+ pageCount,
+ fetchPage: isomorphicSetPage,
+ fetchNext,
+ fetchPrevious,
+ hasNextPage,
+ hasPreviousPage,
+ unstable__mutate,
+ },
};
};
@@ -43,3 +225,13 @@ function createOrganizationList(organizationMemberships: OrganizationMembershipR
organization: organizationMembership.organization,
}));
}
+
+function cacheKey(type: 'userInvitations', user: UserResource, pagination: GetUserOrganizationInvitationsParams) {
+ return {
+ type,
+ userId: user.id,
+ initialPage: pagination.initialPage,
+ pageSize: pagination.pageSize,
+ status: pagination.status,
+ };
+}
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index cbd4bd2c26..e2fbe82050 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -23,6 +23,7 @@ export {
ClerkInstanceContext,
ClientContext,
OrganizationContext,
+ OrganizationProvider,
SessionContext,
useClerkInstanceContext,
useClientContext,
diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts
index 242531a77c..4e0c540ae4 100644
--- a/packages/types/src/user.ts
+++ b/packages/types/src/user.ts
@@ -1,4 +1,4 @@
-import type { ClerkPaginatedResponse, ClerkPaginationParams } from './api';
+import type { ClerkPaginatedResponse } from './api';
import type { BackupCodeResource } from './backupCode';
import type { DeletedObjectResource } from './deletedObject';
import type { EmailAddressResource } from './emailAddress';
@@ -6,6 +6,7 @@ import type { ExternalAccountResource } from './externalAccount';
import type { ImageResource } from './image';
import type { UserJSON } from './json';
import type { OAuthScope } from './oauth';
+import type { OrganizationInvitationStatus } from './organizationInvitation';
import type { OrganizationMembershipResource } from './organizationMembership';
import type { PhoneNumberResource } from './phoneNumber';
import type { ClerkResource } from './resource';
@@ -160,4 +161,15 @@ export type UpdateUserPasswordParams = {
export type RemoveUserPasswordParams = Pick;
-export type GetUserOrganizationInvitationsParams = ClerkPaginationParams;
+export type GetUserOrganizationInvitationsParams = {
+ /**
+ * This the starting point for your fetched results. The initial value persists between re-renders
+ */
+ initialPage?: number;
+ /**
+ * Maximum number of items returned per request. The initial value persists between re-renders
+ */
+ pageSize?: number;
+
+ status?: OrganizationInvitationStatus;
+};