Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smart-pandas-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Use hooks exported from `@clerk/shared` to query commerce data.
10 changes: 10 additions & 0 deletions .changeset/violet-terms-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@clerk/shared': minor
'@clerk/types': minor
---

Introduce experimental paginated hooks for commerce data.
- `useStatements`
- `usePaymentAttempts`
- `usePaymentMethods`
Prefixed with `__experimental_`
5 changes: 2 additions & 3 deletions packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ import { Select, SelectButton, SelectOptionList } from '@/ui/elements/Select';
import { Tooltip } from '@/ui/elements/Tooltip';

import { DevOnly } from '../../common/DevOnly';
import { useCheckoutContext, usePaymentSources } from '../../contexts';
import { useCheckoutContext, usePaymentMethods } from '../../contexts';
import { Box, Button, Col, descriptors, Flex, Form, localizationKeys, Text } from '../../customizables';
import { ChevronUpDown, InformationCircle } from '../../icons';
import { handleError } from '../../utils';
@@ -191,8 +191,7 @@ const useCheckoutMutations = () => {
};

const CheckoutFormElements = ({ checkout }: { checkout: CommerceCheckoutResource }) => {
const { data } = usePaymentSources();
const { data: paymentSources } = data || { data: [] };
const { data: paymentSources } = usePaymentMethods();

const [paymentMethodSource, setPaymentMethodSource] = useState<PaymentMethodSource>(() =>
paymentSources.length > 0 ? 'existing' : 'new',
Original file line number Diff line number Diff line change
@@ -12,14 +12,14 @@ import { formatDate, truncateWithEndVisible } from '../../utils';
* -----------------------------------------------------------------------------------------------*/

export const PaymentAttemptsList = () => {
const { data: paymentAttempts, isLoading } = usePaymentAttempts();
const { data: paymentAttempts, isLoading, count } = usePaymentAttempts();
const localizationRoot = useSubscriberTypeLocalizationRoot();

return (
<DataTable
page={1}
onPageChange={_ => {}}
itemCount={paymentAttempts?.total_count || 0}
itemCount={count}
pageCount={1}
itemsPerPage={10}
isLoading={isLoading}
@@ -29,7 +29,7 @@ export const PaymentAttemptsList = () => {
localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.tableHeader__amount`),
localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.tableHeader__status`),
]}
rows={(paymentAttempts?.data || []).map(i => (
rows={paymentAttempts.map(i => (
<PaymentAttemptsListRow
key={i.id}
paymentAttempt={i}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useClerk, useOrganization } from '@clerk/shared/react';
import type { CommercePaymentSourceResource } from '@clerk/types';
import type { SetupIntent } from '@stripe/stripe-js';
import { Fragment, useCallback, useMemo, useRef } from 'react';
import { Fragment, useMemo, useRef } from 'react';

import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { FullHeightLoader } from '@/ui/elements/FullHeightLoader';
@@ -10,7 +10,7 @@ import { ThreeDotsMenu } from '@/ui/elements/ThreeDotsMenu';

import { RemoveResourceForm } from '../../common';
import { DevOnly } from '../../common/DevOnly';
import { usePaymentSources, useSubscriberTypeContext, useSubscriberTypeLocalizationRoot } from '../../contexts';
import { usePaymentMethods, useSubscriberTypeContext, useSubscriberTypeLocalizationRoot } from '../../contexts';
import { localizationKeys } from '../../customizables';
import { Action } from '../../elements/Action';
import { useActionContext } from '../../elements/Action/ActionRoot';
@@ -114,17 +114,13 @@ export const PaymentSources = withCardStateProvider(() => {
const localizationRoot = useSubscriberTypeLocalizationRoot();
const resource = subscriberType === 'org' ? clerk?.organization : clerk.user;

const { data, isLoading, mutate: mutatePaymentSources } = usePaymentSources();

const { data: paymentSources = [] } = data || {};
const { data: paymentMethods, isLoading, revalidate: revalidatePaymentMethods } = usePaymentMethods();

const sortedPaymentSources = useMemo(
() => paymentSources.sort((a, b) => (a.isDefault && !b.isDefault ? -1 : 1)),
[paymentSources],
() => paymentMethods.sort((a, b) => (a.isDefault && !b.isDefault ? -1 : 1)),
[paymentMethods],
);

const revalidatePaymentSources = useCallback(() => void mutatePaymentSources(), [mutatePaymentSources]);

if (!resource) {
return null;
}
@@ -156,15 +152,15 @@ export const PaymentSources = withCardStateProvider(() => {
<PaymentSourceRow paymentSource={paymentSource} />
<PaymentSourceMenu
paymentSource={paymentSource}
revalidate={revalidatePaymentSources}
revalidate={revalidatePaymentMethods}
/>
</ProfileSection.Item>

<Action.Open value={`remove-${paymentSource.id}`}>
<Action.Card variant='destructive'>
<RemoveScreen
paymentSource={paymentSource}
revalidate={revalidatePaymentSources}
revalidate={revalidatePaymentMethods}
/>
</Action.Card>
</Action.Open>
@@ -178,7 +174,7 @@ export const PaymentSources = withCardStateProvider(() => {
</Action.Trigger>
<Action.Open value='add'>
<Action.Card>
<AddScreen onSuccess={revalidatePaymentSources} />
<AddScreen onSuccess={revalidatePaymentMethods} />
</Action.Card>
</Action.Open>
</>
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { useClerk } from '@clerk/shared/react';
import type { CommercePlanResource, CommerceSubscriptionPlanPeriod, PricingTableProps } from '@clerk/types';
import { useEffect, useMemo, useState } from 'react';

import { usePaymentSources, usePlans, usePlansContext, usePricingTableContext, useSubscriptions } from '../../contexts';
import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscriptions } from '../../contexts';
import { Flow } from '../../customizables';
import { PricingTableDefault } from './PricingTableDefault';
import { PricingTableMatrix } from './PricingTableMatrix';
@@ -60,7 +60,7 @@ const PricingTableRoot = (props: PricingTableProps) => {
};

// Pre-fetch payment sources
usePaymentSources();
usePaymentMethods();

return (
<Flow.Root
Original file line number Diff line number Diff line change
@@ -13,14 +13,14 @@ import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible';
* -----------------------------------------------------------------------------------------------*/

export const StatementsList = () => {
const { data: statements, isLoading } = useStatements();
const { data: statements, isLoading, count } = useStatements();
const localizationRoot = useSubscriberTypeLocalizationRoot();

return (
<DataTable
page={1}
onPageChange={_ => {}}
itemCount={statements?.total_count || 0}
itemCount={count}
pageCount={1}
itemsPerPage={10}
isLoading={isLoading}
@@ -29,7 +29,7 @@ export const StatementsList = () => {
localizationKeys(`${localizationRoot}.billingPage.statementsSection.tableHeader__date`),
localizationKeys(`${localizationRoot}.billingPage.statementsSection.tableHeader__amount`),
]}
rows={(statements?.data || []).map(i => (
rows={statements.map(i => (
<StatementsListRow
key={i.id}
statement={i}
Original file line number Diff line number Diff line change
@@ -6,9 +6,9 @@ export const usePaymentAttemptsContext = () => {
const { data: payments } = usePaymentAttempts();
const getPaymentAttemptById = useCallback(
(paymentAttemptId: string) => {
return payments?.data.find(payment => payment.id === paymentAttemptId);
return payments.find(payment => payment.id === paymentAttemptId);
},
[payments?.data],
[payments],
);

return {
85 changes: 39 additions & 46 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { useClerk, useOrganization, useSession, useUser } from '@clerk/shared/react';
import {
__experimental_usePaymentAttempts,
__experimental_usePaymentMethods,
__experimental_useStatements,
useClerk,
useOrganization,
useSession,
useUser,
} from '@clerk/shared/react';
import type {
Appearance,
CommercePlanResource,
@@ -8,7 +16,8 @@ import type {
import { useCallback, useMemo } from 'react';
import useSWR from 'swr';

import { CommerceSubscription } from '../../../core/resources/internal';
import { CommerceSubscription } from '@/core/resources/CommerceSubscription';

import type { LocalizationKey } from '../../localization';
import { localizationKeys } from '../../localization';
import { getClosestProfileScrollBox } from '../../utils';
@@ -30,51 +39,36 @@ export const usePaymentSourcesCacheKey = () => {
};
};

export const usePaymentSources = () => {
const { organization } = useOrganization();
const { user } = useUser();
const subscriberType = useSubscriberTypeContext();
const cacheKey = usePaymentSourcesCacheKey();

return useSWR(cacheKey, () => (subscriberType === 'org' ? organization : user)?.getPaymentSources({}), dedupeOptions);
};

export const usePaymentAttemptsCacheKey = () => {
const { organization } = useOrganization();
const { user } = useUser();
// TODO(@COMMERCE): Rename payment sources to payment methods at the API level
export const usePaymentMethods = () => {
const subscriberType = useSubscriberTypeContext();

return {
key: `commerce-payment-history`,
userId: user?.id,
args: { orgId: subscriberType === 'org' ? organization?.id : undefined },
};
return __experimental_usePaymentMethods({
for: subscriberType === 'org' ? 'organization' : 'user',
initialPage: 1,
pageSize: 10,
keepPreviousData: true,
});
};

export const usePaymentAttempts = () => {
const { billing } = useClerk();
const cacheKey = usePaymentAttemptsCacheKey();

return useSWR(cacheKey, ({ args, userId }) => (userId ? billing.getPaymentAttempts(args) : undefined), dedupeOptions);
};

export const useStatementsCacheKey = () => {
const { organization } = useOrganization();
const { user } = useUser();
const subscriberType = useSubscriberTypeContext();

return {
key: `commerce-statements`,
userId: user?.id,
args: { orgId: subscriberType === 'org' ? organization?.id : undefined },
};
return __experimental_usePaymentAttempts({
for: subscriberType === 'org' ? 'organization' : 'user',
initialPage: 1,
pageSize: 10,
keepPreviousData: true,
});
};

export const useStatements = () => {
const { billing } = useClerk();
const cacheKey = useStatementsCacheKey();

return useSWR(cacheKey, ({ args, userId }) => (userId ? billing.getStatements(args) : undefined), dedupeOptions);
export const useStatements = (params?: { mode: 'cache' }) => {
const subscriberType = useSubscriberTypeContext();
return __experimental_useStatements({
for: subscriberType === 'org' ? 'organization' : 'user',
initialPage: 1,
pageSize: 10,
keepPreviousData: true,
__experimental_mode: params?.mode,
});
};

export const useSubscriptions = () => {
@@ -181,18 +175,17 @@ export const usePlansContext = () => {
});

// Invalidates cache but does not fetch immediately
const { mutate: mutateStatements } =
useSWR<Awaited<ReturnType<typeof clerk.billing.getStatements>>>(useStatementsCacheKey());
const { revalidate: revalidateStatements } = useStatements({ mode: 'cache' });

const { mutate: mutatePaymentSources } = usePaymentSources();
const { revalidate: revalidatePaymentSources } = usePaymentMethods();

const revalidateAll = useCallback(() => {
// Revalidate the plans and subscriptions
void mutateSubscriptions();
void mutatePlans();
void mutateStatements();
void mutatePaymentSources();
}, [mutateSubscriptions, mutatePlans, mutateStatements, mutatePaymentSources]);
void revalidateStatements();
void revalidatePaymentSources();
}, [mutateSubscriptions, mutatePlans, revalidateStatements, revalidatePaymentSources]);

// should the default plan be shown as active
const isDefaultPlanImplicitlyActiveOrUpcoming = useMemo(() => {
4 changes: 2 additions & 2 deletions packages/clerk-js/src/ui/contexts/components/Statements.tsx
Original file line number Diff line number Diff line change
@@ -6,9 +6,9 @@ export const useStatementsContext = () => {
const { data: statements } = useStatements();
const getStatementById = useCallback(
(statementId: string) => {
return statements?.data.find(statement => statement.id === statementId);
return statements.find(statement => statement.id === statementId);
},
[statements?.data],
[statements],
);

return {
101 changes: 101 additions & 0 deletions packages/shared/src/react/hooks/createCommerceHook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { ClerkPaginatedResponse, ClerkResource } from '@clerk/types';

import { eventMethodCalled } from '../../telemetry/events/method-called';
import {
useAssertWrappedByClerkProvider,
useClerkInstanceContext,
useOrganizationContext,
useUserContext,
} from '../contexts';
import type { PagesOrInfiniteOptions, PaginatedHookConfig, PaginatedResources } from '../types';
import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite';

/**
* @internal
*/
type CommerceHookConfig<TResource extends ClerkResource, TParams extends PagesOrInfiniteOptions> = {
hookName: string;
resourceType: string;
useFetcher: (
param: 'organization' | 'user',
) => ((params: TParams) => Promise<ClerkPaginatedResponse<TResource>>) | undefined;
};

/**
* A hook factory that creates paginated data fetching hooks for commerce-related resources.
* It provides a standardized way to create hooks that can fetch either user or organization resources
* with built-in pagination support.
*
* The generated hooks handle:
* - Clerk authentication context
* - Resource-specific data fetching
* - Pagination (both traditional and infinite scroll)
* - Telemetry tracking
* - Type safety for the specific resource.
*
* @internal
*/
export function createCommerceHook<TResource extends ClerkResource, TParams extends PagesOrInfiniteOptions>({
hookName,
resourceType,
useFetcher,
}: CommerceHookConfig<TResource, TParams>) {
type HookParams = PaginatedHookConfig<PagesOrInfiniteOptions> & {
for: 'organization' | 'user';
};

return function useCommerceHook<T extends HookParams>(
params: T,
): PaginatedResources<TResource, T extends { infinite: true } ? true : false> {
const { for: _for, ...paginationParams } = params;

useAssertWrappedByClerkProvider(hookName);

const fetchFn = useFetcher(_for);

const safeValues = useWithSafeValues(paginationParams, {
initialPage: 1,
pageSize: 10,
keepPreviousData: false,
infinite: false,
__experimental_mode: undefined,
} as unknown as T);

const clerk = useClerkInstanceContext();
const user = useUserContext();
const { organization } = useOrganizationContext();

clerk.telemetry?.record(eventMethodCalled(hookName));

const hookParams =
typeof paginationParams === 'undefined'
? undefined
: ({
initialPage: safeValues.initialPage,
pageSize: safeValues.pageSize,
...(_for === 'organization' ? { orgId: organization?.id } : {}),
} as TParams);

const isClerkLoaded = !!(clerk.loaded && user);

const isEnabled = !!hookParams && isClerkLoaded;

const result = usePagesOrInfinite<TParams, ClerkPaginatedResponse<TResource>>(
(hookParams || {}) as TParams,
fetchFn,
{
keepPreviousData: safeValues.keepPreviousData,
infinite: safeValues.infinite,
enabled: isEnabled,
__experimental_mode: safeValues.__experimental_mode,
},
{
type: resourceType,
userId: user?.id,
...(_for === 'organization' ? { orgId: organization?.id } : {}),
},
);

return result;
};
}
4 changes: 4 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -8,3 +8,7 @@ export { useUser } from './useUser';
export { useClerk } from './useClerk';
export { useDeepEqualMemo, isDeeplyEqual } from './useDeepEqualMemo';
export { useReverification } from './useReverification';
export { useStatements as __experimental_useStatements } from './useStatements';
export { usePaymentAttempts as __experimental_usePaymentAttempts } from './usePaymentAttempts';
export { usePaymentMethods as __experimental_usePaymentMethods } from './usePaymentMethods';
export { useSubscriptionItems as __experimental_useSubscriptionItems } from './useSubscriptionItems';
96 changes: 81 additions & 15 deletions packages/shared/src/react/hooks/usePagesOrInfinite.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,25 @@ import type {
ValueOrSetter,
} from '../types';

/**
* Returns an object containing only the keys from the first object that are not present in the second object.
* Useful for extracting unique parameters that should be passed to a request while excluding common cache keys.
*
* @internal
*
* @example
* ```typescript
* // Example 1: Basic usage
* const obj1 = { name: 'John', age: 30, city: 'NY' };
* const obj2 = { name: 'John', age: 30 };
* getDifferentKeys(obj1, obj2); // Returns { city: 'NY' }
*
* // Example 2: With cache keys
* const requestParams = { page: 1, limit: 10, userId: '123' };
* const cacheKeys = { userId: '123' };
* getDifferentKeys(requestParams, cacheKeys); // Returns { page: 1, limit: 10 }
* ```
*/
function getDifferentKeys(obj1: Record<string, unknown>, obj2: Record<string, unknown>): Record<string, unknown> {
const keysSet = new Set(Object.keys(obj2));
const differentKeysObject: Record<string, unknown> = {};
@@ -24,6 +43,33 @@ function getDifferentKeys(obj1: Record<string, unknown>, obj2: Record<string, un
return differentKeysObject;
}

/**
* A hook that safely merges user-provided pagination options with default values.
* It caches initial pagination values (page and size) until component unmount to prevent unwanted rerenders.
*
* @internal
*
* @example
* ```typescript
* // Example 1: With user-provided options
* const userOptions = { initialPage: 2, pageSize: 20, infinite: true };
* const defaults = { initialPage: 1, pageSize: 10, infinite: false };
* useWithSafeValues(userOptions, defaults);
* // Returns { initialPage: 2, pageSize: 20, infinite: true }
*
* // Example 2: With boolean true (use defaults)
* const params = true;
* const defaults = { initialPage: 1, pageSize: 10, infinite: false };
* useWithSafeValues(params, defaults);
* // Returns { initialPage: 1, pageSize: 10, infinite: false }
*
* // Example 3: With undefined options (fallback to defaults)
* const params = undefined;
* const defaults = { initialPage: 1, pageSize: 10, infinite: false };
* useWithSafeValues(params, defaults);
* // Returns { initialPage: 1, pageSize: 10, infinite: false }
* ```
*/
export const useWithSafeValues = <T extends PagesOrInfiniteOptions>(params: T | true | undefined, defaultValues: T) => {
const shouldUseDefaults = typeof params === 'boolean' && params;

@@ -57,24 +103,40 @@ type ExtractData<Type> = Type extends { data: infer Data } ? ArrayType<Data> : T
type UsePagesOrInfinite = <
Params extends PagesOrInfiniteOptions,
FetcherReturnData extends Record<string, any>,
CacheKeys = Record<string, unknown>,
CacheKeys extends Record<string, unknown> = Record<string, unknown>,
TConfig extends PagesOrInfiniteConfig = PagesOrInfiniteConfig,
>(
/**
* The parameters will be passed to the fetcher
* The parameters will be passed to the fetcher.
*/
params: Params,
/**
* A Promise returning function to fetch your data
* A Promise returning function to fetch your data.
*/
fetcher: ((p: Params) => FetcherReturnData | Promise<FetcherReturnData>) | undefined,
/**
* Internal configuration of the hook
* Internal configuration of the hook.
*/
config: TConfig,
cacheKeys: CacheKeys,
) => PaginatedResources<ExtractData<FetcherReturnData>, TConfig['infinite']>;

/**
* A flexible pagination hook that supports both traditional pagination and infinite loading.
* It provides a unified API for handling paginated data fetching, with built-in caching through SWR.
* The hook can operate in two modes:
* - Traditional pagination: Fetches one page at a time with page navigation
* - Infinite loading: Accumulates data as more pages are loaded.
*
* Features:
* - Cache management with SWR
* - Loading and error states
* - Page navigation helpers
* - Data revalidation and updates
* - Support for keeping previous data while loading.
*
* @internal
*/
export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config, cacheKeys) => {
const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1);

@@ -83,6 +145,7 @@ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config,
const pageSizeRef = useRef(params.pageSize ?? 10);

const enabled = config.enabled ?? true;
const cacheMode = config.__experimental_mode === 'cache';
const triggerInfinite = config.infinite ?? false;
const keepPreviousData = config.keepPreviousData ?? false;

@@ -93,22 +156,25 @@ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config,
pageSize: pageSizeRef.current,
};

// cacheMode being `true` indicates that the cache key is defined, but the fetcher is not.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 This comment feels like it should be reflected in the __experimental_mode jsdoc description.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consumers of the exported hooks, do not have control over the fetcher, thus should not be mentioned

// This allows to ready the cache instead of firing a request.
const shouldFetch = !triggerInfinite && enabled && (!cacheMode ? !!fetcher : true);
const swrKey = shouldFetch ? pagesCacheKey : null;
const swrFetcher =
!cacheMode && !!fetcher
? (cacheKeyParams: Record<string, unknown>) => {
const requestParams = getDifferentKeys(cacheKeyParams, cacheKeys);
return fetcher({ ...params, ...requestParams });
}
: null;

const {
data: swrData,
isValidating: swrIsValidating,
isLoading: swrIsLoading,
error: swrError,
mutate: swrMutate,
} = useSWR(
!triggerInfinite && !!fetcher && enabled ? pagesCacheKey : null,
cacheKeyParams => {
// @ts-ignore
const requestParams = getDifferentKeys(cacheKeyParams, cacheKeys);
// @ts-ignore
return fetcher?.(requestParams);
},
{ keepPreviousData, ...cachingSWROptions },
);
} = useSWR(swrKey, swrFetcher, { keepPreviousData, ...cachingSWROptions });

const {
data: swrInfiniteData,
@@ -177,7 +243,7 @@ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config,
const error = (triggerInfinite ? swrInfiniteError : swrError) ?? null;
const isError = !!error;
/**
* Helpers
* Helpers.
*/
const fetchNext = useCallback(() => {
fetchPage(n => Math.max(0, n + 1));
16 changes: 16 additions & 0 deletions packages/shared/src/react/hooks/usePaymentAttempts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { CommercePaymentResource, GetPaymentAttemptsParams } from '@clerk/types';

import { useClerkInstanceContext } from '../contexts';
import { createCommerceHook } from './createCommerceHook';

/**
* @internal
*/
export const usePaymentAttempts = createCommerceHook<CommercePaymentResource, GetPaymentAttemptsParams>({
hookName: 'usePaymentAttempts',
resourceType: 'commerce-payment-attempts',
useFetcher: () => {
const clerk = useClerkInstanceContext();
return clerk.billing.getPaymentAttempts;
},
});
21 changes: 21 additions & 0 deletions packages/shared/src/react/hooks/usePaymentMethods.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { CommercePaymentSourceResource, GetPaymentSourcesParams } from '@clerk/types';

import { useOrganizationContext, useUserContext } from '../contexts';
import { createCommerceHook } from './createCommerceHook';

/**
* @internal
*/
export const usePaymentMethods = createCommerceHook<CommercePaymentSourceResource, GetPaymentSourcesParams>({
hookName: 'usePaymentMethods',
resourceType: 'commerce-payment-methods',
useFetcher: resource => {
const { organization } = useOrganizationContext();
const user = useUserContext();

if (resource === 'organization') {
return organization?.getPaymentSources;
}
return user?.getPaymentSources;
},
});
16 changes: 16 additions & 0 deletions packages/shared/src/react/hooks/useStatements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { CommerceStatementResource, GetStatementsParams } from '@clerk/types';

import { useClerkInstanceContext } from '../contexts';
import { createCommerceHook } from './createCommerceHook';

/**
* @internal
*/
export const useStatements = createCommerceHook<CommerceStatementResource, GetStatementsParams>({
hookName: 'useStatements',
resourceType: 'commerce-statements',
useFetcher: () => {
const clerk = useClerkInstanceContext();
return clerk.billing.getStatements;
},
});
16 changes: 16 additions & 0 deletions packages/shared/src/react/hooks/useSubscriptionItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { CommerceSubscriptionResource, GetSubscriptionsParams } from '@clerk/types';

import { useClerkInstanceContext } from '../contexts';
import { createCommerceHook } from './createCommerceHook';

/**
* @internal
*/
export const useSubscriptionItems = createCommerceHook<CommerceSubscriptionResource, GetSubscriptionsParams>({
hookName: 'useSubscriptionItems',
resourceType: 'commerce-subscription-items',
useFetcher: () => {
const clerk = useClerkInstanceContext();
return clerk.billing.getSubscriptions;
},
});
21 changes: 20 additions & 1 deletion packages/shared/src/react/types.ts
Original file line number Diff line number Diff line change
@@ -90,22 +90,32 @@ export type PaginatedResourcesWithDefault<T> = {
export type PaginatedHookConfig<T> = T & {
/**
* If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality.
*
* @default false
*/
infinite?: boolean;
/**
* If `true`, the previous data will be kept in the cache until new data is fetched.
*
* @default false
*/
keepPreviousData?: boolean;
};

export type PagesOrInfiniteConfig = PaginatedHookConfig<{
/**
* If `true`, a request will be triggered.
* If `true`, a request will be triggered when the hook is mounted.
*
* @default true
*/
enabled?: boolean;
/**
* @experimental
* On `cache` mode, no request will be triggered when the hook is mounted and the data will be fetched from the cache.
*
* @default undefined
*/
__experimental_mode?: 'cache';
}>;

/**
@@ -114,12 +124,21 @@ export type PagesOrInfiniteConfig = PaginatedHookConfig<{
export type PagesOrInfiniteOptions = {
/**
* A number that specifies which page to fetch. For example, if `initialPage` is set to 10, it will skip the first 9 pages and fetch the 10th page.
*
* @default 1
*/
initialPage?: number;
/**
* A number that specifies the maximum number of results to return per page.
*
* @default 10
*/
pageSize?: number;
/**
* @experimental
* On `cache` mode, no request will be triggered when the hook is mounted and the data will be fetched from the cache.
*
* @default undefined
*/
__experimental_mode?: 'cache';
};