diff --git a/apps/dashboard/src/@/actions/billing.ts b/apps/dashboard/src/@/actions/billing.ts index c10fa6d61f7..3b93a76570c 100644 --- a/apps/dashboard/src/@/actions/billing.ts +++ b/apps/dashboard/src/@/actions/billing.ts @@ -1,113 +1,7 @@ "use server"; -import "server-only"; -import { API_SERVER_URL } from "@/constants/env"; import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken"; -import type { ProductSKU } from "../lib/billing"; - -export type GetBillingCheckoutUrlOptions = { - teamSlug: string; - sku: ProductSKU; - redirectUrl: string; - metadata?: Record; -}; - -export async function getBillingCheckoutUrl( - options: GetBillingCheckoutUrlOptions, -): Promise<{ status: number; url?: string }> { - if (!options.teamSlug) { - return { - status: 400, - }; - } - const token = await getAuthToken(); - - if (!token) { - return { - status: 401, - }; - } - - const res = await fetch( - `${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-link`, - { - method: "POST", - body: JSON.stringify({ - sku: options.sku, - redirectTo: options.redirectUrl, - metadata: options.metadata || {}, - }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }, - ); - if (!res.ok) { - return { - status: res.status, - }; - } - - const json = await res.json(); - if (!json.result) { - return { - status: 500, - }; - } - - return { - status: 200, - url: json.result as string, - }; -} - -export type GetBillingCheckoutUrlAction = typeof getBillingCheckoutUrl; - -export async function getPlanCancelUrl(options: { - teamId: string; - redirectUrl: string; -}): Promise<{ status: number; url?: string }> { - const token = await getAuthToken(); - if (!token) { - return { - status: 401, - }; - } - - const res = await fetch( - `${API_SERVER_URL}/v1/teams/${options.teamId}/checkout/cancel-plan-link`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - redirectTo: options.redirectUrl, - }), - }, - ); - - if (!res.ok) { - return { - status: res.status, - }; - } - - const json = await res.json(); - - if (!json.result) { - return { - status: 500, - }; - } - - return { - status: 200, - url: json.result as string, - }; -} +import { API_SERVER_URL } from "../constants/env"; export async function reSubscribePlan(options: { teamId: string; @@ -141,58 +35,3 @@ export async function reSubscribePlan(options: { status: 200, }; } -export type GetBillingPortalUrlOptions = { - teamSlug: string | undefined; - redirectUrl: string; -}; - -export async function getBillingPortalUrl( - options: GetBillingPortalUrlOptions, -): Promise<{ status: number; url?: string }> { - if (!options.teamSlug) { - return { - status: 400, - }; - } - const token = await getAuthToken(); - if (!token) { - return { - status: 401, - }; - } - - const res = await fetch( - `${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-session-link`, - { - method: "POST", - body: JSON.stringify({ - redirectTo: options.redirectUrl, - }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }, - ); - - if (!res.ok) { - return { - status: res.status, - }; - } - - const json = await res.json(); - - if (!json.result) { - return { - status: 500, - }; - } - - return { - status: 200, - url: json.result as string, - }; -} - -export type GetBillingPortalUrlAction = typeof getBillingPortalUrl; diff --git a/apps/dashboard/src/@/components/billing.tsx b/apps/dashboard/src/@/components/billing.tsx index d0137111d78..e2dead0c85f 100644 --- a/apps/dashboard/src/@/components/billing.tsx +++ b/apps/dashboard/src/@/components/billing.tsx @@ -1,89 +1,51 @@ "use client"; - -import { useMutation } from "@tanstack/react-query"; import { AlertTriangleIcon } from "lucide-react"; import Link from "next/link"; -import { toast } from "sonner"; -import type { - GetBillingCheckoutUrlAction, - GetBillingCheckoutUrlOptions, - GetBillingPortalUrlAction, - GetBillingPortalUrlOptions, -} from "../actions/billing"; +import { + buildBillingPortalUrl, + buildCheckoutUrl, +} from "../../app/(app)/(stripe)/utils/build-url"; import type { Team } from "../api/team"; +import type { ProductSKU } from "../lib/billing"; import { cn } from "../lib/utils"; -import { Spinner } from "./ui/Spinner/Spinner"; import { Button, type ButtonProps } from "./ui/button"; -type CheckoutButtonProps = Omit & { - getBillingCheckoutUrl: GetBillingCheckoutUrlAction; +export function CheckoutButton(props: { buttonProps?: Omit; children: React.ReactNode; billingStatus: Team["billingStatus"]; -}; - -export function CheckoutButton({ - teamSlug, - sku, - metadata, - getBillingCheckoutUrl, - children, - buttonProps, - billingStatus, -}: CheckoutButtonProps) { - const getUrlMutation = useMutation({ - mutationFn: async () => { - return getBillingCheckoutUrl({ - teamSlug, - sku, - metadata, - redirectUrl: getAbsoluteUrl("/stripe-redirect"), - }); - }, - }); - - const errorMessage = "Failed to open checkout page"; - + teamSlug: string; + sku: Exclude; +}) { return (
{/* show warning if the team has an invalid payment method */} - {billingStatus === "invalidPayment" && ( - + {props.billingStatus === "invalidPayment" && ( + )}
); @@ -107,66 +69,27 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) { ); } -type BillingPortalButtonProps = Omit< - GetBillingPortalUrlOptions, - "redirectUrl" -> & { - getBillingPortalUrl: GetBillingPortalUrlAction; +export function BillingPortalButton(props: { + teamSlug: string; buttonProps?: Omit; children: React.ReactNode; -}; - -export function BillingPortalButton({ - teamSlug, - children, - getBillingPortalUrl, - buttonProps, -}: BillingPortalButtonProps) { - const getUrlMutation = useMutation({ - mutationFn: async () => { - return getBillingPortalUrl({ - teamSlug, - redirectUrl: getAbsoluteUrl("/stripe-redirect"), - }); - }, - }); - - const errorMessage = "Failed to open billing portal"; - +}) { return ( ); } - -function getAbsoluteUrl(path: string) { - const url = new URL(window.location.origin); - url.pathname = path; - return url.toString(); -} diff --git a/apps/dashboard/src/@/components/blocks/pricing-card.tsx b/apps/dashboard/src/@/components/blocks/pricing-card.tsx index a10f6fa901a..c71909de405 100644 --- a/apps/dashboard/src/@/components/blocks/pricing-card.tsx +++ b/apps/dashboard/src/@/components/blocks/pricing-card.tsx @@ -11,7 +11,6 @@ import { TEAM_PLANS } from "utils/pricing"; import { RenewSubscriptionButton } from "../../../components/settings/Account/Billing/renew-subscription/renew-subscription-button"; import { useTrack } from "../../../hooks/analytics/useTrack"; import { remainingDays } from "../../../utils/date-utils"; -import type { GetBillingCheckoutUrlAction } from "../../actions/billing"; import type { ProductSKU } from "../../lib/billing"; import { CheckoutButton } from "../billing"; @@ -45,7 +44,6 @@ type PricingCardProps = { highlighted?: boolean; current?: boolean; activeTrialEndsAt?: string; - getBillingCheckoutUrl: GetBillingCheckoutUrlAction; }; export const PricingCard: React.FC = ({ @@ -58,7 +56,6 @@ export const PricingCard: React.FC = ({ highlighted = false, current = false, activeTrialEndsAt, - getBillingCheckoutUrl, }) => { const plan = TEAM_PLANS[billingPlan]; const isCustomPrice = typeof plan.price === "string"; @@ -154,7 +151,6 @@ export const PricingCard: React.FC = ({ }} teamSlug={teamSlug} sku={billingPlanToSkuMap[billingPlan]} - getBillingCheckoutUrl={getBillingCheckoutUrl} > {cta.label} diff --git a/apps/dashboard/src/app/(app)/(stripe)/_components/StripeRedirectErrorPage.tsx b/apps/dashboard/src/app/(app)/(stripe)/_components/StripeRedirectErrorPage.tsx new file mode 100644 index 00000000000..57b43a46e71 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(stripe)/_components/StripeRedirectErrorPage.tsx @@ -0,0 +1,16 @@ +import { AlertTriangleIcon } from "lucide-react"; + +export function StripeRedirectErrorPage(props: { + errorMessage: string; +}) { + return ( +
+
+
+ +
+

{props.errorMessage}

+
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/(stripe)/cancel-plan/[team_id]/page.tsx b/apps/dashboard/src/app/(app)/(stripe)/cancel-plan/[team_id]/page.tsx new file mode 100644 index 00000000000..7fc765d025c --- /dev/null +++ b/apps/dashboard/src/app/(app)/(stripe)/cancel-plan/[team_id]/page.tsx @@ -0,0 +1,23 @@ +import { redirect } from "next/navigation"; +import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage"; +import { getPlanCancelUrl } from "../../utils/billing"; + +export default async function CancelPlanPage(props: { + params: Promise<{ + team_id: string; + }>; +}) { + const params = await props.params; + + const billingUrl = await getPlanCancelUrl({ + teamId: params.team_id, + }); + + if (!billingUrl) { + return ; + } + + redirect(billingUrl); + + return null; +} diff --git a/apps/dashboard/src/app/(app)/(stripe)/checkout/[team_slug]/[sku]/page.tsx b/apps/dashboard/src/app/(app)/(stripe)/checkout/[team_slug]/[sku]/page.tsx new file mode 100644 index 00000000000..d170aab563d --- /dev/null +++ b/apps/dashboard/src/app/(app)/(stripe)/checkout/[team_slug]/[sku]/page.tsx @@ -0,0 +1,28 @@ +import type { ProductSKU } from "@/lib/billing"; +import { redirect } from "next/navigation"; +import { StripeRedirectErrorPage } from "../../../_components/StripeRedirectErrorPage"; +import { getBillingCheckoutUrl } from "../../../utils/billing"; + +export default async function CheckoutPage(props: { + params: Promise<{ + team_slug: string; + sku: string; + }>; +}) { + const params = await props.params; + + const billingUrl = await getBillingCheckoutUrl({ + teamSlug: params.team_slug, + sku: decodeURIComponent(params.sku) as Exclude, + }); + + if (!billingUrl) { + return ( + + ); + } + + redirect(billingUrl); + + return null; +} diff --git a/apps/dashboard/src/app/(app)/(stripe)/loading.tsx b/apps/dashboard/src/app/(app)/(stripe)/loading.tsx new file mode 100644 index 00000000000..b1cc1d09823 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(stripe)/loading.tsx @@ -0,0 +1,11 @@ +import { Spinner } from "@/components/ui/Spinner/Spinner"; + +export default function Loading() { + return ( +
+
+ +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/(stripe)/manage-billing/[team_slug]/page.tsx b/apps/dashboard/src/app/(app)/(stripe)/manage-billing/[team_slug]/page.tsx new file mode 100644 index 00000000000..6960353c290 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(stripe)/manage-billing/[team_slug]/page.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation"; +import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage"; +import { getBillingPortalUrl } from "../../utils/billing"; + +export default async function ManageBillingPage(props: { + params: Promise<{ + team_slug: string; + }>; +}) { + const params = await props.params; + + const billingUrl = await getBillingPortalUrl({ + teamSlug: params.team_slug, + }); + + if (!billingUrl) { + return ( + + ); + } + + redirect(billingUrl); + + return null; +} diff --git a/apps/dashboard/src/app/(app)/stripe-redirect/page.tsx b/apps/dashboard/src/app/(app)/(stripe)/stripe-redirect/page.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/stripe-redirect/page.tsx rename to apps/dashboard/src/app/(app)/(stripe)/stripe-redirect/page.tsx diff --git a/apps/dashboard/src/app/(app)/stripe-redirect/stripeRedirectChannel.ts b/apps/dashboard/src/app/(app)/(stripe)/stripe-redirect/stripeRedirectChannel.ts similarity index 100% rename from apps/dashboard/src/app/(app)/stripe-redirect/stripeRedirectChannel.ts rename to apps/dashboard/src/app/(app)/(stripe)/stripe-redirect/stripeRedirectChannel.ts diff --git a/apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts b/apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts new file mode 100644 index 00000000000..aa1c56474d3 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts @@ -0,0 +1,122 @@ +import "server-only"; +import { API_SERVER_URL } from "@/constants/env"; +import type { ProductSKU } from "@/lib/billing"; +import { getAbsoluteUrl } from "../../../../lib/vercel-utils"; +import { getAuthToken } from "../../api/lib/getAuthToken"; + +export async function getBillingCheckoutUrl(options: { + teamSlug: string; + sku: Exclude; +}): Promise { + const token = await getAuthToken(); + + if (!token) { + return undefined; + } + + const res = await fetch( + `${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-link`, + { + method: "POST", + body: JSON.stringify({ + sku: options.sku, + redirectTo: getAbsoluteStripeRedirectUrl(), + }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }, + ); + if (!res.ok) { + console.error("Failed to create checkout link", await res.json()); + return undefined; + } + + const json = await res.json(); + if (!json.result) { + return undefined; + } + + return json.result as string; +} + +export async function getPlanCancelUrl(options: { + teamId: string; +}): Promise { + const token = await getAuthToken(); + if (!token) { + return undefined; + } + + const res = await fetch( + `${API_SERVER_URL}/v1/teams/${options.teamId}/checkout/cancel-plan-link`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + redirectTo: getAbsoluteStripeRedirectUrl(), + }), + }, + ); + + if (!res.ok) { + return undefined; + } + + const json = await res.json(); + + if (!json.result) { + return undefined; + } + + return json.result as string; +} + +export async function getBillingPortalUrl(options: { + teamSlug: string; +}): Promise { + if (!options.teamSlug) { + return undefined; + } + const token = await getAuthToken(); + if (!token) { + return undefined; + } + + const res = await fetch( + `${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-session-link`, + { + method: "POST", + body: JSON.stringify({ + redirectTo: getAbsoluteStripeRedirectUrl(), + }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (!res.ok) { + return undefined; + } + + const json = await res.json(); + + if (!json.result) { + return undefined; + } + + return json.result as string; +} + +function getAbsoluteStripeRedirectUrl() { + const baseUrl = getAbsoluteUrl(); + const url = new URL(baseUrl); + url.pathname = "/stripe-redirect"; + return url.toString(); +} diff --git a/apps/dashboard/src/app/(app)/(stripe)/utils/build-url.ts b/apps/dashboard/src/app/(app)/(stripe)/utils/build-url.ts new file mode 100644 index 00000000000..e831e8d8e07 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(stripe)/utils/build-url.ts @@ -0,0 +1,20 @@ +import type { ProductSKU } from "@/lib/billing"; + +export function buildCheckoutUrl(options: { + sku: Exclude; + teamSlug: string; +}) { + return `/checkout/${options.teamSlug}/${options.sku}`; +} + +export function buildCancelPlanUrl(options: { + teamId: string; +}) { + return `/cancel-plan/${options.teamId}`; +} + +export function buildBillingPortalUrl(options: { + teamSlug: string; +}) { + return `/manage-billing/${options.teamSlug}`; +} diff --git a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx index 902ec46e12e..6cd50b51f00 100644 --- a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx +++ b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx @@ -1,6 +1,4 @@ "use client"; - -import { getBillingPortalUrl } from "@/actions/billing"; import { confirmEmailWithOTP } from "@/actions/confirmEmail"; import { apiServerProxy } from "@/actions/proxies"; import { updateAccount } from "@/actions/updateAccount"; @@ -68,7 +66,6 @@ export function AccountSettingsPage(props: { return { status: 500 }; } }} - getBillingPortalUrl={getBillingPortalUrl} updateAccountAvatar={async (file) => { let uri: string | undefined = undefined; diff --git a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.stories.tsx b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.stories.tsx index e4c8da561c2..9d7ed19c325 100644 --- a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.stories.tsx +++ b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.stories.tsx @@ -105,10 +105,6 @@ function Variants() { { - await new Promise((resolve) => setTimeout(resolve, 1000)); - return { status: 200 }; - }} account={{ name: "John Doe", email: "johndoe@gmail.com", diff --git a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx index 8dc457a47a5..1b1056880e9 100644 --- a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx +++ b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx @@ -1,6 +1,5 @@ "use client"; -import type { GetBillingPortalUrlAction } from "@/actions/billing"; import { BillingPortalButton } from "@/components/billing"; import { DangerSettingCard } from "@/components/blocks/DangerSettingCard"; import { SettingsCard } from "@/components/blocks/SettingsCard"; @@ -63,7 +62,6 @@ export function AccountSettingsPageUI(props: { onAccountDeleted: () => void; defaultTeamSlug: string; defaultTeamName: string; - getBillingPortalUrl: GetBillingPortalUrlAction; cancelSubscriptions: () => Promise; }) { return ( @@ -89,7 +87,6 @@ export function AccountSettingsPageUI(props: { onAccountDeleted={props.onAccountDeleted} defaultTeamSlug={props.defaultTeamSlug} defaultTeamName={props.defaultTeamName} - getBillingPortalUrl={props.getBillingPortalUrl} cancelSubscriptions={props.cancelSubscriptions} /> @@ -208,7 +205,6 @@ function DeleteAccountCard(props: { onAccountDeleted: () => void; defaultTeamSlug: string; defaultTeamName: string; - getBillingPortalUrl: GetBillingPortalUrlAction; cancelSubscriptions: () => Promise; }) { const title = "Delete Account"; @@ -333,7 +329,6 @@ function DeleteAccountCard(props: { variant: "default", }} teamSlug={props.defaultTeamSlug} - getBillingPortalUrl={props.getBillingPortalUrl} > Manage Billing diff --git a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.stories.tsx b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.stories.tsx index 03bca736e98..dfb9de7da6a 100644 --- a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.stories.tsx +++ b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.stories.tsx @@ -72,9 +72,6 @@ function Story(props: { inviteTeamMembers={async (params) => { return { results: params.map(() => "fulfilled") }; }} - getBillingCheckoutUrl={async () => { - return { status: 200 }; - }} onComplete={() => { storybookLog("onComplete"); }} diff --git a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.tsx b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.tsx index c82a1017c53..626e824c039 100644 --- a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.tsx +++ b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/InviteTeamMembers.tsx @@ -1,6 +1,5 @@ "use client"; -import type { GetBillingCheckoutUrlAction } from "@/actions/billing"; import type { Team } from "@/api/team"; import { PricingCard } from "@/components/blocks/pricing-card"; import { Spinner } from "@/components/ui/Spinner/Spinner"; @@ -20,7 +19,7 @@ import { ArrowRightIcon, CircleArrowUpIcon } from "lucide-react"; import { useState, useTransition } from "react"; import type { ThirdwebClient } from "thirdweb"; import { pollWithTimeout } from "utils/pollWithTimeout"; -import { useStripeRedirectEvent } from "../../../stripe-redirect/stripeRedirectChannel"; +import { useStripeRedirectEvent } from "../../../(stripe)/stripe-redirect/stripeRedirectChannel"; import { InviteSection, type InviteTeamMembersFn, @@ -28,7 +27,6 @@ import { export function InviteTeamMembersUI(props: { team: Team; - getBillingCheckoutUrl: GetBillingCheckoutUrlAction; inviteTeamMembers: InviteTeamMembersFn; onComplete: () => void; getTeam: () => Promise; @@ -81,7 +79,6 @@ export function InviteTeamMembersUI(props: { void; getTeam: () => Promise; teamId: string; @@ -179,7 +175,6 @@ function InviteModalContent(props: { }); }, }} - getBillingCheckoutUrl={props.getBillingCheckoutUrl} getTeam={props.getTeam} teamId={props.teamId} /> @@ -203,7 +198,6 @@ function InviteModalContent(props: { }, }} highlighted - getBillingCheckoutUrl={props.getBillingCheckoutUrl} getTeam={props.getTeam} teamId={props.teamId} /> @@ -226,7 +220,6 @@ function InviteModalContent(props: { }); }, }} - getBillingCheckoutUrl={props.getBillingCheckoutUrl} getTeam={props.getTeam} teamId={props.teamId} /> @@ -249,7 +242,6 @@ function InviteModalContent(props: { }); }, }} - getBillingCheckoutUrl={props.getBillingCheckoutUrl} getTeam={props.getTeam} teamId={props.teamId} /> diff --git a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx index 3f63f93ad97..ada5027196e 100644 --- a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx +++ b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx @@ -1,6 +1,4 @@ "use client"; - -import { getBillingCheckoutUrl } from "@/actions/billing"; import { apiServerProxy } from "@/actions/proxies"; import { sendTeamInvites } from "@/actions/sendTeamInvite"; import type { Team } from "@/api/team"; @@ -123,7 +121,6 @@ export function InviteTeamMembers(props: { return res.data.result; }} team={props.team} - getBillingCheckoutUrl={getBillingCheckoutUrl} inviteTeamMembers={async (params) => { const res = await sendTeamInvites({ teamId: props.team.id, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanner.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanner.stories.tsx index abfca98639a..72f103ecfe6 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanner.stories.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanner.stories.tsx @@ -20,15 +20,9 @@ type Story = StoryObj; export const PaymentAlerts: Story = { render: () => (
- Promise.resolve({ status: 200 })} - /> + - Promise.resolve({ status: 200 })} - /> +
), }; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanners.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanners.tsx index 856f87313d4..4048681062b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanners.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBanners.tsx @@ -1,25 +1,13 @@ "use client"; - -import { getBillingPortalUrl } from "@/actions/billing"; import { PastDueBannerUI, ServiceCutOffBannerUI, } from "./BillingAlertBannersUI"; export function PastDueBanner(props: { teamSlug: string }) { - return ( - - ); + return ; } export function ServiceCutOffBanner(props: { teamSlug: string }) { - return ( - - ); + return ; } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx index 0146d147498..148de2c96a1 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx @@ -1,13 +1,12 @@ "use client"; -import type { GetBillingPortalUrlAction } from "@/actions/billing"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; import Link from "next/link"; import { useTransition } from "react"; -import { useStripeRedirectEvent } from "../../../../stripe-redirect/stripeRedirectChannel"; +import { useStripeRedirectEvent } from "../../../../(stripe)/stripe-redirect/stripeRedirectChannel"; function BillingAlertBanner(props: { title: string; @@ -65,7 +64,6 @@ function BillingAlertBanner(props: { export function PastDueBannerUI(props: { teamSlug: string; - getBillingPortalUrl: GetBillingPortalUrlAction; }) { return ( { const res = await apiServerProxy<{ result: Team; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.stories.tsx index f56b9bf8bb2..fc04bf27041 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.stories.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.stories.tsx @@ -105,16 +105,6 @@ function Story(props: { }, }); - const getBillingPortalUrlStub = async () => ({ - status: 200, - url: "https://example.com", - }); - - const getBillingCheckoutUrlStub = async () => ({ - status: 200, - url: "https://example.com", - }); - const teamTeamStub = async () => ({ ...team, @@ -127,8 +117,6 @@ function Story(props: { @@ -140,8 +128,6 @@ function Story(props: { planCancellationDate: addDays(new Date(), 10).toISOString(), }} subscriptions={zeroUsageOnDemandSubs} - getBillingPortalUrl={getBillingPortalUrlStub} - getBillingCheckoutUrl={getBillingCheckoutUrlStub} getTeam={teamTeamStub} /> @@ -150,8 +136,6 @@ function Story(props: { @@ -160,8 +144,6 @@ function Story(props: { @@ -170,8 +152,6 @@ function Story(props: { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx index 5c63bdcc8aa..519b67b9022 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx @@ -1,9 +1,5 @@ "use client"; -import type { - GetBillingCheckoutUrlAction, - GetBillingPortalUrlAction, -} from "@/actions/billing"; import type { Team } from "@/api/team"; import type { TeamSubscription } from "@/api/team-subscription"; import { BillingPortalButton } from "@/components/billing"; @@ -31,8 +27,6 @@ import { getValidTeamPlan } from "../../../../../../components/TeamHeader/getVal export function PlanInfoCardUI(props: { subscriptions: TeamSubscription[]; team: Team; - getBillingPortalUrl: GetBillingPortalUrlAction; - getBillingCheckoutUrl: GetBillingCheckoutUrlAction; getTeam: () => Promise; }) { const { subscriptions, team } = props; @@ -57,7 +51,6 @@ export function PlanInfoCardUI(props: { Manage Billing @@ -303,7 +295,6 @@ function formatCurrencyAmount(centsAmount: number, currency: string) { function ViewPlansSheet(props: { team: Team; trialPeriodEndedAt: string | undefined; - getBillingCheckoutUrl: GetBillingCheckoutUrlAction; isOpen: boolean; onOpenChange: (open: boolean) => void; getTeam: () => Promise; @@ -317,7 +308,6 @@ function ViewPlansSheet(props: { diff --git a/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx b/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx index 578951ea3d5..eb1452fe2e5 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx @@ -1,6 +1,4 @@ "use client"; - -import { getPlanCancelUrl } from "@/actions/billing"; import type { Team } from "@/api/team"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; @@ -12,12 +10,12 @@ import { SheetTrigger, } from "@/components/ui/sheet"; import { useDashboardRouter } from "@/lib/DashboardRouter"; -import { useMutation } from "@tanstack/react-query"; import { CircleXIcon, ExternalLinkIcon } from "lucide-react"; import Link from "next/link"; import { useState, useTransition } from "react"; import { toast } from "sonner"; -import { useStripeRedirectEvent } from "../../../../../app/(app)/stripe-redirect/stripeRedirectChannel"; +import { useStripeRedirectEvent } from "../../../../../app/(app)/(stripe)/stripe-redirect/stripeRedirectChannel"; +import { buildCancelPlanUrl } from "../../../../../app/(app)/(stripe)/utils/build-url"; import { PRO_CONTACT_US_URL } from "../../../../../constants/pro"; import { pollWithTimeout } from "../../../../../utils/pollWithTimeout"; import { tryCatch } from "../../../../../utils/try-catch"; @@ -139,36 +137,7 @@ function ImmediateCancelPlanButton(props: { }); }); - const cancelPlan = useMutation({ - mutationFn: async (opts: { teamId: string }) => { - const { url, status } = await getPlanCancelUrl({ - teamId: opts.teamId, - redirectUrl: getAbsoluteUrl("/stripe-redirect"), - }); - - if (!url) { - throw new Error("Failed to get cancel plan url"); - } - - if (status !== 200) { - throw new Error("Failed to get cancel plan url"); - } - - const tab = window.open(url, "_blank"); - if (!tab) { - throw new Error("Failed to open cancel plan url"); - } - }, - }); - - async function handleCancelPlan() { - cancelPlan.mutate({ - teamId: props.teamId, - }); - } - - const showPlanSpinner = - isPollingTeam || isRoutePending || cancelPlan.isPending; + const showPlanSpinner = isPollingTeam || isRoutePending; return ( ); } - -function getAbsoluteUrl(path: string) { - const url = new URL(window.location.origin); - url.pathname = path; - return url.toString(); -} diff --git a/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx b/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx index 82bf3f3f920..cb640b4ee71 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx @@ -1,6 +1,5 @@ "use client"; -import type { GetBillingCheckoutUrlAction } from "@/actions/billing"; import type { Team } from "@/api/team"; import { PricingCard } from "@/components/blocks/pricing-card"; import { Spinner } from "@/components/ui/Spinner/Spinner"; @@ -9,7 +8,8 @@ import { useDashboardRouter } from "@/lib/DashboardRouter"; import { CheckIcon } from "lucide-react"; import Link from "next/link"; import { useTransition } from "react"; -import { useStripeRedirectEvent } from "../../../../app/(app)/stripe-redirect/stripeRedirectChannel"; + +import { useStripeRedirectEvent } from "../../../../app/(app)/(stripe)/stripe-redirect/stripeRedirectChannel"; import { getValidTeamPlan } from "../../../../app/(app)/team/components/TeamHeader/getValidTeamPlan"; import { PRO_CONTACT_US_URL } from "../../../../constants/pro"; @@ -28,7 +28,6 @@ const planToTierRecord: Record = { interface BillingPricingProps { team: Team; trialPeriodEndedAt: string | undefined; - getBillingCheckoutUrl: GetBillingCheckoutUrlAction; getTeam: () => Promise; } @@ -49,7 +48,6 @@ type CtaLink = export const BillingPricing: React.FC = ({ team, trialPeriodEndedAt, - getBillingCheckoutUrl, getTeam, }) => { const validTeamPlan = getValidTeamPlan(team); @@ -106,7 +104,6 @@ export const BillingPricing: React.FC = ({ teamSlug={team.slug} teamId={team.id} highlighted={highlightStarterPlan} - getBillingCheckoutUrl={getBillingCheckoutUrl} getTeam={getTeam} /> @@ -126,7 +123,6 @@ export const BillingPricing: React.FC = ({ highlighted={highlightGrowthPlan} teamSlug={team.slug} teamId={team.id} - getBillingCheckoutUrl={getBillingCheckoutUrl} getTeam={getTeam} /> @@ -143,7 +139,6 @@ export const BillingPricing: React.FC = ({ isCurrentPlanScheduledToCancel, )} highlighted={highlightAcceleratePlan} - getBillingCheckoutUrl={getBillingCheckoutUrl} getTeam={getTeam} /> @@ -160,7 +155,6 @@ export const BillingPricing: React.FC = ({ isCurrentPlanScheduledToCancel, )} highlighted={highlightScalePlan} - getBillingCheckoutUrl={getBillingCheckoutUrl} getTeam={getTeam} />