Skip to content
Merged
8 changes: 8 additions & 0 deletions .changeset/shaky-sloths-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
'@clerk/nextjs': minor
'@clerk/clerk-react': minor
---

[Billing Beta] Update `PlanDetailsProps` to reflect that either `planId` or `plan` is allowed.
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
ClerkOptions,
CreateOrganizationProps,
EnvironmentResource,
FlattenUnionType,
GoogleOneTapProps,
OrganizationProfileProps,
SignInProps,
Expand Down Expand Up @@ -112,7 +113,7 @@ export type ComponentControls = {
props: T extends 'checkout'
? __internal_CheckoutProps
: T extends 'planDetails'
? __internal_PlanDetailsProps
? FlattenUnionType<__internal_PlanDetailsProps>
: T extends 'subscriptionDetails'
? __internal_SubscriptionDetailsProps
: never,
Expand Down Expand Up @@ -161,7 +162,7 @@ interface ComponentsState {
};
planDetailsDrawer: {
open: false;
props: null | __internal_PlanDetailsProps;
props: null | FlattenUnionType<__internal_PlanDetailsProps>;
};
subscriptionDetailsDrawer: {
open: false;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ClerkAPIResponseError,
CommercePlanResource,
CommerceSubscriptionPlanPeriod,
FlattenUnionType,
} from '@clerk/types';
import * as React from 'react';
import { useMemo, useState } from 'react';
Expand All @@ -28,7 +29,7 @@ import {
useLocalizations,
} from '../../customizables';

export const PlanDetails = (props: __internal_PlanDetailsProps) => {
export const PlanDetails = (props: FlattenUnionType<__internal_PlanDetailsProps>) => {
return (
<Drawer.Content>
<PlanDetailsInternal {...props} />
Expand Down Expand Up @@ -73,7 +74,7 @@ const PlanDetailsInternal = ({
planId,
plan: initialPlan,
initialPlanPeriod = 'month',
}: __internal_PlanDetailsProps) => {
}: FlattenUnionType<__internal_PlanDetailsProps>) => {
const clerk = useClerk();
const [planPeriod, setPlanPeriod] = useState<CommerceSubscriptionPlanPeriod>(initialPlanPeriod);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useUser } from '@clerk/shared/react';
import type { __internal_PlanDetailsProps, Appearance } from '@clerk/types';
import type { __internal_PlanDetailsProps, Appearance, FlattenUnionType } from '@clerk/types';

import { PlanDetails } from './components';
import { LazyDrawerRenderer } from './providers';
Expand All @@ -13,7 +13,7 @@ export function MountedPlanDetailDrawer({
onOpenChange: (open: boolean) => void;
planDetailsDrawer: {
open: false;
props: null | __internal_PlanDetailsProps;
props: null | FlattenUnionType<__internal_PlanDetailsProps>;
};
}) {
const { user } = useUser();
Expand Down
6 changes: 4 additions & 2 deletions packages/react/src/components/PlanDetailsButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { __experimental_PlanDetailsButtonProps } from '@clerk/types';
import type { __experimental_PlanDetailsButtonProps, FlattenUnionType } from '@clerk/types';
import React from 'react';

import type { WithClerkProp } from '../types';
Expand Down Expand Up @@ -35,7 +35,8 @@ import { withClerk } from './withClerk';
*/
export const PlanDetailsButton = withClerk(
({ clerk, children, ...props }: WithClerkProp<React.PropsWithChildren<__experimental_PlanDetailsButtonProps>>) => {
const { plan, planId, initialPlanPeriod, planDetailsProps, ...rest } = props;
const { plan, planId, initialPlanPeriod, planDetailsProps, ...rest } =
props as FlattenUnionType<__experimental_PlanDetailsButtonProps>;
children = normalizeWithDefaultValue(children, 'Plan details');
const child = assertSingleChild(children)('PlanDetailsButton');

Expand All @@ -45,6 +46,7 @@ export const PlanDetailsButton = withClerk(
}

return clerk.__internal_openPlanDetails({
// @ts-expect-error - plan is not required
plan,
Comment thread
panteliselef marked this conversation as resolved.
Outdated
planId,
initialPlanPeriod,
Expand Down
28 changes: 22 additions & 6 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@
*/
windowNavigate: (to: URL | string) => void;
},
) => Promise<unknown> | unknown;

Check warning on line 1141 in packages/types/src/clerk.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'unknown' overrides all other types in this union type

export type WithoutRouting<T> = Omit<T, 'path' | 'routing'>;

Expand Down Expand Up @@ -1886,10 +1886,18 @@
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
export type __internal_PlanDetailsProps = {
export type __internal_PlanDetailsProps = (
| {
planId: string;
}
| {
/**
* The plan object will be used as initial data until the plan is fetched from the server.
*/
plan: CommercePlanResource;
}
) & {
appearance?: PlanDetailTheme;
plan?: CommercePlanResource;
planId?: string;
initialPlanPeriod?: CommerceSubscriptionPlanPeriod;
portalId?: string;
portalRoot?: PortalRoot;
Expand All @@ -1905,9 +1913,17 @@
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
export type __experimental_PlanDetailsButtonProps = {
plan?: CommercePlanResource;
planId?: string;
export type __experimental_PlanDetailsButtonProps = (
Comment thread
panteliselef marked this conversation as resolved.
| {
planId: string;
}
| {
/**
* The plan object will be used as initial data until the plan is fetched from the server.
*/
plan: CommercePlanResource;
}
) & {
initialPlanPeriod?: CommerceSubscriptionPlanPeriod;
planDetailsProps?: {
appearance?: PlanDetailTheme;
Expand Down
32 changes: 32 additions & 0 deletions packages/types/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,35 @@ export type Without<T, W> = {
* Value contains: { a:string, b: string }
*/
export type Override<T, U> = Omit<T, keyof U> & U;

// Converts a union of two types into an intersection
// i.e. A | B -> A & B
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

type FlattenUnionTypeInternal<T> = {
[K in keyof UnionToIntersection<T>]: K extends keyof T
? T[K] extends any[]
? T[K]
: T[K] extends object
? FlattenUnionType<T[K]>
: T[K]
: UnionToIntersection<T>[K] | undefined;
};

// Convert properties with `| undefined` to optional properties
type UndefinedToOptional<T> = {
[K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
} & {
[K in keyof T as undefined extends T[K] ? never : K]: T[K];
};

/**
* Flattens a union type into a single type with all properties, making union properties optional.
*
* @example
* type A = { a: number; c: number };
* type B = { b: string; c: number };
* type Result = FlattenUnionType<A | B>;
* // Result: { a?: number; b?: string; c: number }
*/
export type FlattenUnionType<T> = UndefinedToOptional<FlattenUnionTypeInternal<T>>;
Loading