Skip to content

Commit 259f17a

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

File tree

12 files changed

+325
-298
lines changed

12 files changed

+325
-298
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/renew-subscription/renew-subscription-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: "renew";
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 === "renew" && (
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)/login/onboarding/team-onboarding/InviteTeamMembers.tsx

+16-4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export function InviteTeamMembersUI(props: {
8181
teamSlug={props.team.slug}
8282
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
8383
trackEvent={props.trackEvent}
84+
getTeam={props.getTeam}
85+
teamId={props.team.id}
8486
/>
8587
</SheetContent>
8688
</Sheet>
@@ -148,6 +150,8 @@ function InviteModalContent(props: {
148150
billingStatus: Team["billingStatus"];
149151
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
150152
trackEvent: (params: TrackingParams) => void;
153+
getTeam: () => Promise<Team>;
154+
teamId: string;
151155
}) {
152156
const [planToShow, setPlanToShow] = useState<
153157
"starter" | "growth" | "accelerate" | "scale"
@@ -159,7 +163,7 @@ function InviteModalContent(props: {
159163
billingStatus={props.billingStatus}
160164
teamSlug={props.teamSlug}
161165
cta={{
162-
title: "Get Started",
166+
label: "Get Started",
163167
type: "checkout",
164168
onClick() {
165169
props.trackEvent({
@@ -171,6 +175,8 @@ function InviteModalContent(props: {
171175
},
172176
}}
173177
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
178+
getTeam={props.getTeam}
179+
teamId={props.teamId}
174180
/>
175181
);
176182

@@ -180,7 +186,7 @@ function InviteModalContent(props: {
180186
billingStatus={props.billingStatus}
181187
teamSlug={props.teamSlug}
182188
cta={{
183-
title: "Get Started",
189+
label: "Get Started",
184190
type: "checkout",
185191
onClick() {
186192
props.trackEvent({
@@ -193,6 +199,8 @@ function InviteModalContent(props: {
193199
}}
194200
highlighted
195201
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
202+
getTeam={props.getTeam}
203+
teamId={props.teamId}
196204
/>
197205
);
198206

@@ -202,7 +210,7 @@ function InviteModalContent(props: {
202210
billingStatus={props.billingStatus}
203211
teamSlug={props.teamSlug}
204212
cta={{
205-
title: "Get started",
213+
label: "Get started",
206214
type: "checkout",
207215
onClick() {
208216
props.trackEvent({
@@ -214,6 +222,8 @@ function InviteModalContent(props: {
214222
},
215223
}}
216224
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
225+
getTeam={props.getTeam}
226+
teamId={props.teamId}
217227
/>
218228
);
219229

@@ -223,7 +233,7 @@ function InviteModalContent(props: {
223233
billingStatus={props.billingStatus}
224234
teamSlug={props.teamSlug}
225235
cta={{
226-
title: "Get started",
236+
label: "Get started",
227237
type: "checkout",
228238
onClick() {
229239
props.trackEvent({
@@ -235,6 +245,8 @@ function InviteModalContent(props: {
235245
},
236246
}}
237247
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
248+
getTeam={props.getTeam}
249+
teamId={props.teamId}
238250
/>
239251
);
240252

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.stories.tsx

-9
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,6 @@ function Story(props: {
115115
url: "https://example.com",
116116
});
117117

118-
const cancelPlanStub = async () => {
119-
await new Promise((resolve) => setTimeout(resolve, 1000));
120-
return;
121-
};
122-
123118
const teamTeamStub = async () =>
124119
({
125120
...team,
@@ -134,7 +129,6 @@ function Story(props: {
134129
subscriptions={zeroUsageOnDemandSubs}
135130
getBillingPortalUrl={getBillingPortalUrlStub}
136131
getBillingCheckoutUrl={getBillingCheckoutUrlStub}
137-
cancelPlan={cancelPlanStub}
138132
getTeam={teamTeamStub}
139133
/>
140134
</BadgeContainer>
@@ -145,7 +139,6 @@ function Story(props: {
145139
subscriptions={trialPlanZeroUsageOnDemandSubs}
146140
getBillingPortalUrl={getBillingPortalUrlStub}
147141
getBillingCheckoutUrl={getBillingCheckoutUrlStub}
148-
cancelPlan={cancelPlanStub}
149142
getTeam={teamTeamStub}
150143
/>
151144
</BadgeContainer>
@@ -156,7 +149,6 @@ function Story(props: {
156149
subscriptions={subsWith1Usage}
157150
getBillingPortalUrl={getBillingPortalUrlStub}
158151
getBillingCheckoutUrl={getBillingCheckoutUrlStub}
159-
cancelPlan={cancelPlanStub}
160152
getTeam={teamTeamStub}
161153
/>
162154
</BadgeContainer>
@@ -167,7 +159,6 @@ function Story(props: {
167159
subscriptions={subsWith4Usage}
168160
getBillingPortalUrl={getBillingPortalUrlStub}
169161
getBillingCheckoutUrl={getBillingCheckoutUrlStub}
170-
cancelPlan={cancelPlanStub}
171162
getTeam={teamTeamStub}
172163
/>
173164
</BadgeContainer>

0 commit comments

Comments
 (0)