Skip to content

Commit a759921

Browse files
committed
Add plan cancellation and re-subscription functionality
1 parent ec5362b commit a759921

File tree

9 files changed

+307
-284
lines changed

9 files changed

+307
-284
lines changed

apps/dashboard/src/@/actions/billing.ts

+79
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,85 @@ export async function getBillingCheckoutUrl(
6464

6565
export type GetBillingCheckoutUrlAction = typeof getBillingCheckoutUrl;
6666

67+
export async function getPlanCancelUrl(options: {
68+
teamId: string;
69+
redirectUrl: string;
70+
}): Promise<{ status: number; url?: string }> {
71+
const token = await getAuthToken();
72+
if (!token) {
73+
return {
74+
status: 401,
75+
};
76+
}
77+
78+
const res = await fetch(
79+
`${API_SERVER_URL}/v1/teams/${options.teamId}/checkout/cancel-plan-link`,
80+
{
81+
method: "POST",
82+
headers: {
83+
"Content-Type": "application/json",
84+
Authorization: `Bearer ${token}`,
85+
},
86+
body: JSON.stringify({
87+
redirectTo: options.redirectUrl,
88+
}),
89+
},
90+
);
91+
92+
if (!res.ok) {
93+
return {
94+
status: res.status,
95+
};
96+
}
97+
98+
const json = await res.json();
99+
100+
if (!json.result) {
101+
return {
102+
status: 500,
103+
};
104+
}
105+
106+
return {
107+
status: 200,
108+
url: json.result as string,
109+
};
110+
}
111+
112+
export async function reSubscribePlan(options: {
113+
teamId: string;
114+
}): Promise<{ status: number }> {
115+
const token = await getAuthToken();
116+
if (!token) {
117+
return {
118+
status: 401,
119+
};
120+
}
121+
122+
const res = await fetch(
123+
`${API_SERVER_URL}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
124+
{
125+
method: "PUT",
126+
headers: {
127+
"Content-Type": "application/json",
128+
Authorization: `Bearer ${token}`,
129+
},
130+
body: JSON.stringify({}),
131+
},
132+
);
133+
134+
console.log(res);
135+
136+
if (!res.ok) {
137+
return {
138+
status: res.status,
139+
};
140+
}
141+
142+
return {
143+
status: 200,
144+
};
145+
}
67146
export type GetBillingPortalUrlOptions = {
68147
teamSlug: string | undefined;
69148
redirectUrl: string;

apps/dashboard/src/@/components/blocks/pricing-card.tsx

+16-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
88
import Link from "next/link";
99
import type React from "react";
1010
import { TEAM_PLANS } from "utils/pricing";
11+
import { RenewSubscriptionButton } from "../../../components/settings/Account/Billing/re-subscribe-button/re-subscribe-button";
1112
import { useTrack } from "../../../hooks/analytics/useTrack";
1213
import { remainingDays } from "../../../utils/date-utils";
1314
import type { GetBillingCheckoutUrlAction } from "../../actions/billing";
@@ -16,20 +17,27 @@ import { CheckoutButton } from "../billing";
1617

1718
type PricingCardCta = {
1819
hint?: string;
19-
title: string;
20+
2021
onClick?: () => void;
2122
} & (
2223
| {
2324
type: "link";
2425
href: string;
26+
label: string;
2527
}
2628
| {
2729
type: "checkout";
30+
label: string;
31+
}
32+
| {
33+
type: "re-subscribe";
2834
}
2935
);
3036

3137
type PricingCardProps = {
38+
getTeam: () => Promise<Team>;
3239
teamSlug: string;
40+
teamId: string;
3341
billingStatus: Team["billingStatus"];
3442
billingPlan: keyof typeof TEAM_PLANS;
3543
cta?: PricingCardCta;
@@ -41,7 +49,9 @@ type PricingCardProps = {
4149
};
4250

4351
export const PricingCard: React.FC<PricingCardProps> = ({
52+
getTeam,
4453
teamSlug,
54+
teamId,
4555
billingStatus,
4656
billingPlan,
4757
cta,
@@ -131,6 +141,9 @@ export const PricingCard: React.FC<PricingCardProps> = ({
131141

132142
{cta && (
133143
<div className="flex flex-col gap-3">
144+
{cta.type === "re-subscribe" && (
145+
<RenewSubscriptionButton teamId={teamId} getTeam={getTeam} />
146+
)}
134147
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
135148
<CheckoutButton
136149
billingStatus={billingStatus}
@@ -143,7 +156,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
143156
sku={billingPlanToSkuMap[billingPlan]}
144157
getBillingCheckoutUrl={getBillingCheckoutUrl}
145158
>
146-
{cta.title}
159+
{cta.label}
147160
</CheckoutButton>
148161
)}
149162

@@ -154,7 +167,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
154167
asChild
155168
>
156169
<Link href={cta.href} target="_blank" onClick={handleCTAClick}>
157-
{cta.title}
170+
{cta.label}
158171
</Link>
159172
</Button>
160173
)}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.client.tsx

-19
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,6 @@ export function PlanInfoCardClient(props: {
3030

3131
return res.data.result;
3232
}}
33-
cancelPlan={async (params) => {
34-
const res = await apiServerProxy<{
35-
data: {
36-
result: "success";
37-
};
38-
}>({
39-
pathname: `/v1/teams/${props.team.id}/checkout/cancel-plan`,
40-
headers: {
41-
"Content-Type": "application/json",
42-
},
43-
method: "PUT",
44-
body: JSON.stringify(params),
45-
});
46-
47-
if (!res.ok) {
48-
console.error(res.error);
49-
throw new Error(res.error);
50-
}
51-
}}
5233
/>
5334
);
5435
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx

+30-12
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,22 @@ import {
1717
SheetHeader,
1818
SheetTitle,
1919
} from "@/components/ui/sheet";
20-
import {
21-
type CancelPlan,
22-
CancelPlanButton,
23-
} from "components/settings/Account/Billing/CancelPlanModal/CancelPlanModal";
20+
import { CancelPlanButton } from "components/settings/Account/Billing/CancelPlanModal/CancelPlanModal";
2421
import { BillingPricing } from "components/settings/Account/Billing/Pricing";
2522
import { differenceInDays, isAfter } from "date-fns";
2623
import { format } from "date-fns/format";
2724
import { CreditCardIcon, FileTextIcon, SquarePenIcon } from "lucide-react";
2825
import { CircleAlertIcon } from "lucide-react";
2926
import Link from "next/link";
3027
import { useState } from "react";
28+
import { RenewSubscriptionButton } from "../../../../../../../../../components/settings/Account/Billing/re-subscribe-button/re-subscribe-button";
3129
import { getValidTeamPlan } from "../../../../../../components/TeamHeader/getValidTeamPlan";
3230

3331
export function PlanInfoCardUI(props: {
3432
subscriptions: TeamSubscription[];
3533
team: Team;
3634
getBillingPortalUrl: GetBillingPortalUrlAction;
3735
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
38-
cancelPlan: CancelPlan;
3936
getTeam: () => Promise<Team>;
4037
}) {
4138
const { subscriptions, team } = props;
@@ -63,6 +60,7 @@ export function PlanInfoCardUI(props: {
6360
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
6461
isOpen={isPlanSheetOpen}
6562
onOpenChange={setIsPlanSheetOpen}
63+
getTeam={props.getTeam}
6664
/>
6765

6866
<div className="flex flex-col gap-4 p-4 lg:flex-row lg:items-center lg:justify-between lg:p-6">
@@ -102,6 +100,16 @@ export function PlanInfoCardUI(props: {
102100
Your trial ends in {trialEndsAfterDays} days
103101
</p>
104102
)}
103+
{props.team.planCancellationDate && (
104+
<Badge variant="destructive">
105+
Scheduled to cancel in{" "}
106+
{differenceInDays(
107+
new Date(props.team.planCancellationDate),
108+
new Date(),
109+
)}{" "}
110+
days
111+
</Badge>
112+
)}
105113
</div>
106114

107115
{props.team.billingPlan !== "free" && (
@@ -118,13 +126,21 @@ export function PlanInfoCardUI(props: {
118126
Change Plan
119127
</Button>
120128

121-
<CancelPlanButton
122-
teamSlug={props.team.slug}
123-
billingStatus={props.team.billingStatus}
124-
cancelPlan={props.cancelPlan}
125-
currentPlan={props.team.billingPlan}
126-
getTeam={props.getTeam}
127-
/>
129+
{props.team.planCancellationDate ? (
130+
// TODO: add re-subscribe button
131+
<RenewSubscriptionButton
132+
teamId={props.team.id}
133+
getTeam={props.getTeam}
134+
/>
135+
) : (
136+
<CancelPlanButton
137+
teamId={props.team.id}
138+
teamSlug={props.team.slug}
139+
billingStatus={props.team.billingStatus}
140+
currentPlan={props.team.billingPlan}
141+
getTeam={props.getTeam}
142+
/>
143+
)}
128144
</div>
129145
)}
130146
</div>
@@ -291,6 +307,7 @@ function ViewPlansSheet(props: {
291307
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
292308
isOpen: boolean;
293309
onOpenChange: (open: boolean) => void;
310+
getTeam: () => Promise<Team>;
294311
}) {
295312
return (
296313
<Sheet open={props.isOpen} onOpenChange={props.onOpenChange}>
@@ -302,6 +319,7 @@ function ViewPlansSheet(props: {
302319
team={props.team}
303320
trialPeriodEndedAt={props.trialPeriodEndedAt}
304321
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
322+
getTeam={props.getTeam}
305323
/>
306324
</SheetContent>
307325
</Sheet>

0 commit comments

Comments
 (0)