Skip to content

chore(clerk-js,shared): Expose experimental useSubscription #6317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
18 changes: 15 additions & 3 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import type {
CommerceProductJSON,
CommerceStatementJSON,
CommerceStatementResource,
CommerceSubscriptionItemJSON,
CommerceSubscriptionItemResource,
CommerceSubscriptionJSON,
CommerceSubscriptionResource,
CreateCheckoutParams,
GetPaymentAttemptsParams,
GetPlansParams,
GetStatementsParams,
GetSubscriptionParams,
GetSubscriptionsParams,
} from '@clerk/types';

Expand All @@ -26,6 +29,7 @@ import {
CommercePlan,
CommerceStatement,
CommerceSubscription,
CommerceSubscriptionItem,
} from '../../resources/internal';

export class CommerceBilling implements CommerceBillingNamespace {
Expand All @@ -40,6 +44,7 @@ export class CommerceBilling implements CommerceBillingNamespace {
return defaultProduct?.plans.map(plan => new CommercePlan(plan)) || [];
};

// Inconsistent API
getPlan = async (params: { id: string }): Promise<CommercePlanResource> => {
const plan = (await BaseResource._fetch({
path: `/commerce/plans/${params.id}`,
Expand All @@ -48,9 +53,16 @@ export class CommerceBilling implements CommerceBillingNamespace {
return new CommercePlan(plan);
};

getSubscription = async (params: GetSubscriptionParams): Promise<CommerceSubscriptionResource> => {
return await BaseResource._fetch({
path: params.orgId ? `/organizations/${params.orgId}/commerce/subscription` : `/me/commerce/subscription`,
method: 'GET',
}).then(res => new CommerceSubscription(res?.response as CommerceSubscriptionJSON));
};

getSubscriptions = async (
params: GetSubscriptionsParams,
): Promise<ClerkPaginatedResponse<CommerceSubscriptionResource>> => {
): Promise<ClerkPaginatedResponse<CommerceSubscriptionItemResource>> => {
const { orgId, ...rest } = params;

return await BaseResource._fetch({
Expand All @@ -59,11 +71,11 @@ export class CommerceBilling implements CommerceBillingNamespace {
search: convertPageToOffsetSearchParams(rest),
}).then(res => {
const { data: subscriptions, total_count } =
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionJSON>;
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionItemJSON>;

return {
total_count,
data: subscriptions.map(subscription => new CommerceSubscription(subscription)),
data: subscriptions.map(subscription => new CommerceSubscriptionItem(subscription)),
};
});
};
Expand Down
17 changes: 11 additions & 6 deletions packages/clerk-js/src/core/resources/CommercePayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@ import type {
CommercePaymentChargeType,
CommercePaymentJSON,
CommercePaymentResource,
CommercePaymentSourceResource,
CommercePaymentStatus,
CommerceSubscriptionItemResource,
} from '@clerk/types';

import { commerceMoneyFromJSON } from '../../utils';
import { unixEpochToDate } from '../../utils/date';
import { BaseResource, CommercePaymentSource, CommerceSubscription } from './internal';
import { BaseResource, CommercePaymentSource, CommerceSubscriptionItem } from './internal';

export class CommercePayment extends BaseResource implements CommercePaymentResource {
id!: string;
amount!: CommerceMoney;
failedAt?: Date;
paidAt?: Date;
updatedAt!: Date;
paymentSource!: CommercePaymentSource;
subscription!: CommerceSubscription;
subscriptionItem!: CommerceSubscription;
paymentSource!: CommercePaymentSourceResource;
/**
* @deprecated
*/
subscription!: CommerceSubscriptionItemResource;
subscriptionItem!: CommerceSubscriptionItemResource;
chargeType!: CommercePaymentChargeType;
status!: CommercePaymentStatus;

Expand All @@ -38,8 +43,8 @@ export class CommercePayment extends BaseResource implements CommercePaymentReso
this.failedAt = data.failed_at ? unixEpochToDate(data.failed_at) : undefined;
this.updatedAt = unixEpochToDate(data.updated_at);
this.paymentSource = new CommercePaymentSource(data.payment_source);
this.subscription = new CommerceSubscription(data.subscription);
this.subscriptionItem = new CommerceSubscription(data.subscription_item);
this.subscription = new CommerceSubscriptionItem(data.subscription);
this.subscriptionItem = new CommerceSubscriptionItem(data.subscription_item);
this.chargeType = data.charge_type;
this.status = data.status;
return this;
Expand Down
52 changes: 50 additions & 2 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {
CancelSubscriptionParams,
CommerceMoney,
CommerceSubscriptionItemJSON,
CommerceSubscriptionItemResource,
CommerceSubscriptionJSON,
CommerceSubscriptionPlanPeriod,
CommerceSubscriptionResource,
Expand All @@ -14,6 +16,50 @@ import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

export class CommerceSubscription extends BaseResource implements CommerceSubscriptionResource {
id!: string;
status!: CommerceSubscriptionStatus;
activeAt!: Date;
createdAt!: Date;
pastDueAt!: Date | null;
updatedAt!: Date | null;
//TODO(@COMMERCE): Why can this be undefined ?
nextPayment: {
//TODO(@COMMERCE): Why can this be undefined ?
amount: CommerceMoney;
//TODO(@COMMERCE): This need to change to `date` probably
//TODO(@COMMERCE): Why can this be undefined ?
time: Date;
} | null = null;
subscriptionItems!: CommerceSubscriptionItemResource[];

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: CommerceSubscriptionJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.status = data.status;
this.createdAt = unixEpochToDate(data.created_at);
this.updatedAt = data.update_at ? unixEpochToDate(data.update_at) : null;
this.activeAt = unixEpochToDate(data.active_at);
this.pastDueAt = data.past_due_at ? unixEpochToDate(data.past_due_at) : null;
this.nextPayment = data.next_payment
? {
amount: commerceMoneyFromJSON(data.next_payment.amount),
time: unixEpochToDate(data.next_payment.time),
}
: null;
this.subscriptionItems = data.subscription_items.map(item => new CommerceSubscriptionItem(item));
return this;
}
}

export class CommerceSubscriptionItem extends BaseResource implements CommerceSubscriptionItemResource {
id!: string;
paymentSourceId!: string;
plan!: CommercePlan;
Expand All @@ -27,17 +73,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
//TODO(@COMMERCE): Why can this be undefined ?
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
constructor(data: CommerceSubscriptionItemJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: CommerceSubscriptionJSON | null): this {
protected fromJSON(data: CommerceSubscriptionItemJSON | null): this {
if (!data) {
return this;
}
Expand All @@ -63,6 +110,7 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
return this;
}

//TODO(@COMMERCE): shouldn't this change to `subscriptions_items` ?
public async cancel(params: CancelSubscriptionParams) {
const { orgId } = params;
const json = (
Expand Down
12 changes: 6 additions & 6 deletions packages/clerk-js/src/core/resources/Organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type {
AddMemberParams,
ClerkPaginatedResponse,
ClerkResourceReloadParams,
CommerceSubscriptionJSON,
CommerceSubscriptionResource,
CommerceSubscriptionItemJSON,
CommerceSubscriptionItemResource,
CreateOrganizationParams,
GetDomainsParams,
GetInvitationsParams,
Expand Down Expand Up @@ -32,7 +32,7 @@ import type {
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
import { unixEpochToDate } from '../../utils/date';
import { addPaymentSource, getPaymentSources, initializePaymentSource } from '../modules/commerce';
import { BaseResource, CommerceSubscription, OrganizationInvitation, OrganizationMembership } from './internal';
import { BaseResource, CommerceSubscriptionItem, OrganizationInvitation, OrganizationMembership } from './internal';
import { OrganizationDomain } from './OrganizationDomain';
import { OrganizationMembershipRequest } from './OrganizationMembershipRequest';
import { Role } from './Role';
Expand Down Expand Up @@ -235,18 +235,18 @@ export class Organization extends BaseResource implements OrganizationResource {

getSubscriptions = async (
getSubscriptionsParams?: GetSubscriptionsParams,
): Promise<ClerkPaginatedResponse<CommerceSubscriptionResource>> => {
): Promise<ClerkPaginatedResponse<CommerceSubscriptionItemResource>> => {
return await BaseResource._fetch({
path: `/organizations/${this.id}/commerce/subscriptions`,
method: 'GET',
search: convertPageToOffsetSearchParams(getSubscriptionsParams),
}).then(res => {
const { data: subscriptions, total_count } =
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionJSON>;
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionItemJSON>;

return {
total_count,
data: subscriptions.map(subscription => new CommerceSubscription(subscription)),
data: subscriptions.map(subscription => new CommerceSubscriptionItem(subscription)),
};
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@ import { useClerk } from '@clerk/shared/react';
import type { CommercePlanResource, CommerceSubscriptionPlanPeriod, PricingTableProps } from '@clerk/types';
import { useEffect, useMemo, useState } from 'react';

import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscriptions } from '../../contexts';
import { Flow } from '../../customizables';
import { Flow } from '@/ui/customizables/Flow';

import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscription } from '../../contexts';
import { PricingTableDefault } from './PricingTableDefault';
import { PricingTableMatrix } from './PricingTableMatrix';

const PricingTableRoot = (props: PricingTableProps) => {
const clerk = useClerk();
const { mode = 'mounted', signInMode = 'redirect' } = usePricingTableContext();
const isCompact = mode === 'modal';
const { data: subscriptions } = useSubscriptions();
const { subscriptionItems } = useSubscription();
const { data: plans } = usePlans();
const { handleSelectPlan } = usePlansContext();

const defaultPlanPeriod = useMemo(() => {
if (isCompact) {
const upcomingSubscription = subscriptions?.find(sub => sub.status === 'upcoming');
const upcomingSubscription = subscriptionItems?.find(sub => sub.status === 'upcoming');
if (upcomingSubscription) {
return upcomingSubscription.planPeriod;
}

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
const activeSubscription = subscriptionItems?.find(
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
Expand All @@ -32,7 +33,7 @@ const PricingTableRoot = (props: PricingTableProps) => {
}

return 'annual';
}, [isCompact, subscriptions]);
}, [isCompact, subscriptionItems]);

const [planPeriod, setPlanPeriod] = useState<CommerceSubscriptionPlanPeriod>(defaultPlanPeriod);

Expand Down
Loading