diff --git a/apps/dashboard/src/@/components/billing.tsx b/apps/dashboard/src/@/components/billing.tsx
index e2dead0c85f..c36fa083453 100644
--- a/apps/dashboard/src/@/components/billing.tsx
+++ b/apps/dashboard/src/@/components/billing.tsx
@@ -84,12 +84,13 @@ export function BillingPortalButton(props: {
props.buttonProps?.onClick?.(e);
}}
>
-
{props.children}
-
+
);
}
diff --git a/apps/dashboard/src/@/components/ui/button.tsx b/apps/dashboard/src/@/components/ui/button.tsx
index ea25f0e8b27..81b78577aab 100644
--- a/apps/dashboard/src/@/components/ui/button.tsx
+++ b/apps/dashboard/src/@/components/ui/button.tsx
@@ -45,17 +45,35 @@ export interface ButtonProps
}
const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
+ ({ className, variant, size, asChild = false, disabled, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
+
+ // "button" elements automatically handle the `disabled` attribute.
+ // For non-button elements rendered via `asChild` (e.g. ), we still want
+ // to visually convey the disabled state and prevent user interaction.
+ // We do that by conditionally adding the same utility classes that the
+ // `disabled:` pseudo-variant would normally apply and by setting
+ // `aria-disabled` for accessibility.
+ const disabledClass = disabled ? "pointer-events-none opacity-50" : "";
+
const btnOnlyProps =
Comp === "button"
- ? { type: props.type || ("button" as const) }
+ ? {
+ type:
+ (props as React.ButtonHTMLAttributes).type ||
+ ("button" as const),
+ }
: undefined;
return (
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.client.tsx
index 033a16eafd0..ea38deb8a61 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.client.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.client.tsx
@@ -9,12 +9,14 @@ export function PlanInfoCardClient(props: {
team: Team;
openPlanSheetButtonByDefault: boolean;
highlightPlan: Team["billingPlan"] | undefined;
+ isOwnerAccount: boolean;
}) {
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 70e140ef374..5eccd417f6f 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
@@ -120,6 +120,7 @@ function Story(props: {
getTeam={teamTeamStub}
highlightPlan={undefined}
openPlanSheetButtonByDefault={false}
+ isOwnerAccount={true}
/>
@@ -133,6 +134,7 @@ function Story(props: {
getTeam={teamTeamStub}
highlightPlan={undefined}
openPlanSheetButtonByDefault={false}
+ isOwnerAccount={true}
/>
@@ -143,6 +145,7 @@ function Story(props: {
getTeam={teamTeamStub}
highlightPlan={undefined}
openPlanSheetButtonByDefault={false}
+ isOwnerAccount={true}
/>
@@ -153,6 +156,7 @@ function Story(props: {
getTeam={teamTeamStub}
highlightPlan={undefined}
openPlanSheetButtonByDefault={false}
+ isOwnerAccount={true}
/>
@@ -163,6 +167,7 @@ function Story(props: {
getTeam={teamTeamStub}
highlightPlan={undefined}
openPlanSheetButtonByDefault={false}
+ isOwnerAccount={true}
/>
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 f8ac4b413a5..2c3cfee7107 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
@@ -13,6 +13,7 @@ import {
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
+import { ToolTipLabel } from "@/components/ui/tooltip";
import { CancelPlanButton } from "components/settings/Account/Billing/CancelPlanModal/CancelPlanModal";
import { BillingPricing } from "components/settings/Account/Billing/Pricing";
import { differenceInDays, isAfter } from "date-fns";
@@ -30,6 +31,7 @@ export function PlanInfoCardUI(props: {
getTeam: () => Promise;
openPlanSheetButtonByDefault: boolean;
highlightPlan: Team["billingPlan"] | undefined;
+ isOwnerAccount: boolean;
}) {
const { subscriptions, team, openPlanSheetButtonByDefault } = props;
const validPlan = getValidTeamPlan(team);
@@ -112,32 +114,57 @@ export function PlanInfoCardUI(props: {
{props.team.billingPlan !== "free" && (
-
+
+
+
+
- {props.team.planCancellationDate ? (
-
- ) : (
-
- )}
+
+
+ {props.team.planCancellationDate ? (
+
+ ) : (
+
+ )}
+
+
)}
@@ -153,16 +180,28 @@ export function PlanInfoCardUI(props: {
To unlock additional usage, upgrade your plan to Starter or
Growth.
+
-
+
+
+
+
) : (
@@ -203,17 +242,28 @@ export function PlanInfoCardUI(props: {
{/* manage team billing */}
-
-
- Manage Billing
-
+
+
+
+ Manage Billing
+
+
+
)}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx
index bdd4b9d0328..a969d3ba98a 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx
@@ -12,8 +12,8 @@ import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
+import { ToolTipLabel } from "@/components/ui/tooltip";
import { ArrowRightIcon, DollarSignIcon } from "lucide-react";
-import Link from "next/link";
import { Suspense, use, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
@@ -28,11 +28,13 @@ const predefinedAmounts = [
interface CreditBalanceSectionProps {
balancePromise: Promise;
teamSlug: string;
+ isOwnerAccount: boolean;
}
export function CreditBalanceSection({
balancePromise,
teamSlug,
+ isOwnerAccount,
}: CreditBalanceSectionProps) {
const [selectedAmount, setSelectedAmount] = useState(
predefinedAmounts[0].value,
@@ -114,17 +116,30 @@ export function CreditBalanceSection({
-
+
+
+
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx
index 148a361eda2..b3771da3b2c 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx
@@ -1,5 +1,6 @@
import { getStripeBalance } from "@/actions/stripe-actions";
import { type Team, getTeamBySlug } from "@/api/team";
+import { getMemberById } from "@/api/team-members";
import { getTeamSubscriptions } from "@/api/team-subscription";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { redirect } from "next/navigation";
@@ -25,16 +26,20 @@ export default async function Page(props: {
]);
const pagePath = `/team/${params.team_slug}/settings/billing`;
- const [account, team, authToken] = await Promise.all([
- getValidAccount(pagePath),
+ const account = await getValidAccount(pagePath);
+
+ const [team, authToken, teamMember] = await Promise.all([
getTeamBySlug(params.team_slug),
getAuthToken(),
+ getMemberById(params.team_slug, account.id),
]);
- if (!team) {
+ if (!team || !teamMember) {
redirect("/team");
}
+ const isOwnerAccount = teamMember.role === "OWNER";
+
const subscriptions = await getTeamSubscriptions(team.slug);
if (!subscriptions) {
@@ -66,6 +71,7 @@ export default async function Page(props: {
subscriptions={subscriptions}
openPlanSheetButtonByDefault={searchParams.showPlans === "true"}
highlightPlan={highlightPlan}
+ isOwnerAccount={isOwnerAccount}
/>
@@ -74,6 +80,7 @@ export default async function Page(props: {
)}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx
index a129ed110f1..c4dd2eec107 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx
@@ -11,6 +11,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
+import { ToolTipLabel } from "@/components/ui/tooltip";
import {
ChevronLeftIcon,
ChevronRightIcon,
@@ -18,7 +19,6 @@ import {
DownloadIcon,
ReceiptIcon,
} from "lucide-react";
-import Link from "next/link";
import { useQueryState } from "nuqs";
import { useTransition } from "react";
import type Stripe from "stripe";
@@ -30,6 +30,7 @@ export function BillingHistory(props: {
invoices: Stripe.Invoice[];
status: "all" | "past_due" | "open";
hasMore: boolean;
+ isOwnerAccount: boolean;
}) {
const [isLoading, startTransition] = useTransition();
const [cursor, setCursor] = useQueryState(
@@ -128,27 +129,58 @@ export function BillingHistory(props: {
{invoice.status === "open" && (
<>
{/* always show the crypto payment button */}
-
+
+
+
{/* if we have a hosted invoice url, show that */}
{invoice.hosted_invoice_url && (
-
+
+
+
)}
>
)}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx
index d3ade359e52..e4f9ecb86e5 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx
@@ -1,5 +1,6 @@
import { getTeamInvoices } from "@/actions/stripe-actions";
import { getTeamBySlug } from "@/api/team";
+import { getMemberById } from "@/api/team-members";
import { redirect } from "next/navigation";
import type { SearchParams } from "nuqs/server";
import { getValidAccount } from "../../../../../../account/settings/getAccount";
@@ -20,16 +21,19 @@ export default async function Page(props: {
const pagePath = `/team/${params.team_slug}/settings/invoices`;
- const [, team] = await Promise.all([
- // only called to verify login status etc
- getValidAccount(pagePath),
+ const account = await getValidAccount(pagePath);
+
+ const [team, teamMember] = await Promise.all([
getTeamBySlug(params.team_slug),
+ getMemberById(params.team_slug, account.id),
]);
if (!team) {
redirect("/team");
}
+ const isOwnerAccount = teamMember?.role === "OWNER";
+
const invoices = await getTeamInvoices(team, {
cursor: searchParams.cursor ?? undefined,
status: searchParams.status ?? undefined,
@@ -54,6 +58,7 @@ export default async function Page(props: {
hasMore={invoices.has_more}
// fall back to "all" if the status is not set
status={searchParams.status ?? "all"}
+ isOwnerAccount={isOwnerAccount}
/>
);
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx
index 51a1099dec2..8629445b7b4 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx
@@ -1,10 +1,10 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
+import { TabPathLinks } from "@/components/ui/tabs";
import { DatabaseIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { EngineIcon } from "../../../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon";
-import { TabPathLinks } from "../../../../../../../../../@/components/ui/tabs";
import { ImportEngineLink } from "./_components";
export default async function Layout(props: {
diff --git a/apps/dashboard/src/components/analytics/empty-chart-state.tsx b/apps/dashboard/src/components/analytics/empty-chart-state.tsx
index 319af5deeff..4c42f7ad954 100644
--- a/apps/dashboard/src/components/analytics/empty-chart-state.tsx
+++ b/apps/dashboard/src/components/analytics/empty-chart-state.tsx
@@ -1,9 +1,9 @@
"use client";
import { LoadingDots } from "@/components/ui/LoadingDots";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
+import { cn } from "@/lib/utils";
import { useMemo } from "react";
import { Area, AreaChart, Bar, BarChart } from "recharts";
-import { cn } from "../../@/lib/utils";
type FakeCartData = {
value: number;
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 eb1452fe2e5..9c8f2eee03e 100644
--- a/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx
+++ b/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx
@@ -26,6 +26,7 @@ export function CancelPlanButton(props: {
currentPlan: Team["billingPlan"];
billingStatus: Team["billingStatus"];
getTeam: () => Promise;
+ disabled?: boolean;
}) {
// shortcut the sheet in case the user is in the default state
if (props.billingStatus !== "invalidPayment" && props.currentPlan !== "pro") {
@@ -33,6 +34,7 @@ export function CancelPlanButton(props: {
);
}
@@ -40,7 +42,12 @@ export function CancelPlanButton(props: {
return (
-