Skip to content

Commit bd9594d

Browse files
committed
[TOOL-4309] Dashbaord: Fix stripe new tab not opening on mobile (#6879)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on refactoring the billing-related functionalities and improving the loading and error handling components in the application. It removes unnecessary async functions for URL retrieval and replaces them with direct URL constructions. ### Detailed summary - Removed async functions for `getBillingPortalUrl` and `getBillingCheckoutUrl`. - Introduced `buildCheckoutUrl`, `buildCancelPlanUrl`, and `buildBillingPortalUrl` functions. - Updated components to use the new URL-building functions. - Enhanced loading and error handling components: `Loading`, `StripeRedirectErrorPage`. - Simplified storybook components by removing unnecessary props. - Updated billing-related components to reflect changes in URL handling. - Cleaned up imports and removed unused variables related to billing URLs. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 0aadcd4 commit bd9594d

File tree

26 files changed

+306
-425
lines changed

26 files changed

+306
-425
lines changed
+1-162
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,7 @@
11
"use server";
2-
import "server-only";
32

4-
import { API_SERVER_URL } from "@/constants/env";
53
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
6-
import type { ProductSKU } from "../lib/billing";
7-
8-
export type GetBillingCheckoutUrlOptions = {
9-
teamSlug: string;
10-
sku: ProductSKU;
11-
redirectUrl: string;
12-
metadata?: Record<string, string>;
13-
};
14-
15-
export async function getBillingCheckoutUrl(
16-
options: GetBillingCheckoutUrlOptions,
17-
): Promise<{ status: number; url?: string }> {
18-
if (!options.teamSlug) {
19-
return {
20-
status: 400,
21-
};
22-
}
23-
const token = await getAuthToken();
24-
25-
if (!token) {
26-
return {
27-
status: 401,
28-
};
29-
}
30-
31-
const res = await fetch(
32-
`${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-link`,
33-
{
34-
method: "POST",
35-
body: JSON.stringify({
36-
sku: options.sku,
37-
redirectTo: options.redirectUrl,
38-
metadata: options.metadata || {},
39-
}),
40-
headers: {
41-
"Content-Type": "application/json",
42-
Authorization: `Bearer ${token}`,
43-
},
44-
},
45-
);
46-
if (!res.ok) {
47-
return {
48-
status: res.status,
49-
};
50-
}
51-
52-
const json = await res.json();
53-
if (!json.result) {
54-
return {
55-
status: 500,
56-
};
57-
}
58-
59-
return {
60-
status: 200,
61-
url: json.result as string,
62-
};
63-
}
64-
65-
export type GetBillingCheckoutUrlAction = typeof getBillingCheckoutUrl;
66-
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-
}
4+
import { API_SERVER_URL } from "../constants/env";
1115

1126
export async function reSubscribePlan(options: {
1137
teamId: string;
@@ -141,58 +35,3 @@ export async function reSubscribePlan(options: {
14135
status: 200,
14236
};
14337
}
144-
export type GetBillingPortalUrlOptions = {
145-
teamSlug: string | undefined;
146-
redirectUrl: string;
147-
};
148-
149-
export async function getBillingPortalUrl(
150-
options: GetBillingPortalUrlOptions,
151-
): Promise<{ status: number; url?: string }> {
152-
if (!options.teamSlug) {
153-
return {
154-
status: 400,
155-
};
156-
}
157-
const token = await getAuthToken();
158-
if (!token) {
159-
return {
160-
status: 401,
161-
};
162-
}
163-
164-
const res = await fetch(
165-
`${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-session-link`,
166-
{
167-
method: "POST",
168-
body: JSON.stringify({
169-
redirectTo: options.redirectUrl,
170-
}),
171-
headers: {
172-
"Content-Type": "application/json",
173-
Authorization: `Bearer ${token}`,
174-
},
175-
},
176-
);
177-
178-
if (!res.ok) {
179-
return {
180-
status: res.status,
181-
};
182-
}
183-
184-
const json = await res.json();
185-
186-
if (!json.result) {
187-
return {
188-
status: 500,
189-
};
190-
}
191-
192-
return {
193-
status: 200,
194-
url: json.result as string,
195-
};
196-
}
197-
198-
export type GetBillingPortalUrlAction = typeof getBillingPortalUrl;
+40-117
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,51 @@
11
"use client";
2-
3-
import { useMutation } from "@tanstack/react-query";
42
import { AlertTriangleIcon } from "lucide-react";
53
import Link from "next/link";
6-
import { toast } from "sonner";
7-
import type {
8-
GetBillingCheckoutUrlAction,
9-
GetBillingCheckoutUrlOptions,
10-
GetBillingPortalUrlAction,
11-
GetBillingPortalUrlOptions,
12-
} from "../actions/billing";
4+
import {
5+
buildBillingPortalUrl,
6+
buildCheckoutUrl,
7+
} from "../../app/(app)/(stripe)/utils/build-url";
138
import type { Team } from "../api/team";
9+
import type { ProductSKU } from "../lib/billing";
1410
import { cn } from "../lib/utils";
15-
import { Spinner } from "./ui/Spinner/Spinner";
1611
import { Button, type ButtonProps } from "./ui/button";
1712

18-
type CheckoutButtonProps = Omit<GetBillingCheckoutUrlOptions, "redirectUrl"> & {
19-
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
13+
export function CheckoutButton(props: {
2014
buttonProps?: Omit<ButtonProps, "children">;
2115
children: React.ReactNode;
2216
billingStatus: Team["billingStatus"];
23-
};
24-
25-
export function CheckoutButton({
26-
teamSlug,
27-
sku,
28-
metadata,
29-
getBillingCheckoutUrl,
30-
children,
31-
buttonProps,
32-
billingStatus,
33-
}: CheckoutButtonProps) {
34-
const getUrlMutation = useMutation({
35-
mutationFn: async () => {
36-
return getBillingCheckoutUrl({
37-
teamSlug,
38-
sku,
39-
metadata,
40-
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
41-
});
42-
},
43-
});
44-
45-
const errorMessage = "Failed to open checkout page";
46-
17+
teamSlug: string;
18+
sku: Exclude<ProductSKU, null>;
19+
}) {
4720
return (
4821
<div className="flex w-full flex-col items-center gap-2">
4922
{/* show warning if the team has an invalid payment method */}
50-
{billingStatus === "invalidPayment" && (
51-
<BillingWarning teamSlug={teamSlug} />
23+
{props.billingStatus === "invalidPayment" && (
24+
<BillingWarning teamSlug={props.teamSlug} />
5225
)}
5326
<Button
54-
{...buttonProps}
55-
className={cn(buttonProps?.className, "w-full gap-2")}
27+
{...props.buttonProps}
28+
asChild
29+
className={cn(props.buttonProps?.className, "w-full gap-2")}
5630
disabled={
5731
// disable button if the team has an invalid payment method
5832
// api will return 402 error if the team has an invalid payment method
59-
billingStatus === "invalidPayment" ||
60-
getUrlMutation.isPending ||
61-
buttonProps?.disabled
33+
props.billingStatus === "invalidPayment" ||
34+
props.buttonProps?.disabled
6235
}
6336
onClick={async (e) => {
64-
buttonProps?.onClick?.(e);
65-
getUrlMutation.mutate(undefined, {
66-
onSuccess: (res) => {
67-
if (!res.url) {
68-
toast.error(errorMessage);
69-
return;
70-
}
71-
72-
const tab = window.open(res.url, "_blank");
73-
74-
if (!tab) {
75-
toast.error(errorMessage);
76-
return;
77-
}
78-
},
79-
onError: () => {
80-
toast.error(errorMessage);
81-
},
82-
});
37+
props.buttonProps?.onClick?.(e);
8338
}}
8439
>
85-
{getUrlMutation.isPending && <Spinner className="size-4" />}
86-
{children}
40+
<Link
41+
target="_blank"
42+
href={buildCheckoutUrl({
43+
teamSlug: props.teamSlug,
44+
sku: props.sku,
45+
})}
46+
>
47+
{props.children}
48+
</Link>
8749
</Button>
8850
</div>
8951
);
@@ -107,66 +69,27 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {
10769
);
10870
}
10971

110-
type BillingPortalButtonProps = Omit<
111-
GetBillingPortalUrlOptions,
112-
"redirectUrl"
113-
> & {
114-
getBillingPortalUrl: GetBillingPortalUrlAction;
72+
export function BillingPortalButton(props: {
73+
teamSlug: string;
11574
buttonProps?: Omit<ButtonProps, "children">;
11675
children: React.ReactNode;
117-
};
118-
119-
export function BillingPortalButton({
120-
teamSlug,
121-
children,
122-
getBillingPortalUrl,
123-
buttonProps,
124-
}: BillingPortalButtonProps) {
125-
const getUrlMutation = useMutation({
126-
mutationFn: async () => {
127-
return getBillingPortalUrl({
128-
teamSlug,
129-
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
130-
});
131-
},
132-
});
133-
134-
const errorMessage = "Failed to open billing portal";
135-
76+
}) {
13677
return (
13778
<Button
138-
{...buttonProps}
139-
className={cn(buttonProps?.className, "gap-2")}
140-
disabled={getUrlMutation.isPending || buttonProps?.disabled}
79+
{...props.buttonProps}
80+
className={cn(props.buttonProps?.className, "gap-2")}
81+
disabled={props.buttonProps?.disabled}
82+
asChild
14183
onClick={async (e) => {
142-
buttonProps?.onClick?.(e);
143-
getUrlMutation.mutate(undefined, {
144-
onSuccess(res) {
145-
if (!res.url) {
146-
toast.error(errorMessage);
147-
return;
148-
}
149-
150-
const tab = window.open(res.url, "_blank");
151-
if (!tab) {
152-
toast.error(errorMessage);
153-
return;
154-
}
155-
},
156-
onError: () => {
157-
toast.error(errorMessage);
158-
},
159-
});
84+
props.buttonProps?.onClick?.(e);
16085
}}
16186
>
162-
{getUrlMutation.isPending && <Spinner className="size-4" />}
163-
{children}
87+
<Link
88+
href={buildBillingPortalUrl({ teamSlug: props.teamSlug })}
89+
target="_blank"
90+
>
91+
{props.children}
92+
</Link>
16493
</Button>
16594
);
16695
}
167-
168-
function getAbsoluteUrl(path: string) {
169-
const url = new URL(window.location.origin);
170-
url.pathname = path;
171-
return url.toString();
172-
}

0 commit comments

Comments
 (0)