@@ -11,7 +11,7 @@ import {
1111} from "@remix-run/react" ;
1212import { json } from "@remix-run/server-runtime" ;
1313import { tryCatch } from "@trigger.dev/core/utils" ;
14- import { useEffect , useRef , useState } from "react" ;
14+ import { cloneElement , useEffect , useRef , useState } from "react" ;
1515import { type UseDataFunctionReturn , typedjson , useTypedLoaderData } from "remix-typedjson" ;
1616import invariant from "tiny-invariant" ;
1717import { z } from "zod" ;
@@ -30,6 +30,7 @@ import {
3030 AlertTrigger ,
3131} from "~/components/primitives/Alert" ;
3232import { Button , ButtonContent , LinkButton } from "~/components/primitives/Buttons" ;
33+ import { PermissionButton } from "~/components/primitives/PermissionButton" ;
3334import { DateTime } from "~/components/primitives/DateTime" ;
3435import { Dialog , DialogContent , DialogHeader , DialogTrigger } from "~/components/primitives/Dialog" ;
3536import { Fieldset } from "~/components/primitives/Fieldset" ;
@@ -119,10 +120,12 @@ export const loader = dashboardLoader(
119120 }
120121
121122 // Pre-compute manage authority server-side so the UI gating matches
122- // the action gating (the action enforces it independently).
123+ // the action gating (the action enforces it independently). Seat
124+ // purchases are a billing operation, so they gate on manage:billing.
123125 const canManageMembers = ability . can ( "manage" , { type : "members" } ) ;
126+ const canManageBilling = ability . can ( "manage" , { type : "billing" } ) ;
124127
125- return typedjson ( { ...result , canManageMembers } ) ;
128+ return typedjson ( { ...result , canManageMembers, canManageBilling } ) ;
126129 }
127130) ;
128131
@@ -318,6 +321,7 @@ export default function Page() {
318321 assignableRoleIds,
319322 memberRoles,
320323 canManageMembers,
324+ canManageBilling,
321325 } = useTypedLoaderData < typeof loader > ( ) ;
322326 // Build a userId → roleId map so the dropdown's defaultValue matches
323327 // each member's current assignment without re-querying.
@@ -532,6 +536,7 @@ export default function Page() {
532536 usedSeats = { limits . used }
533537 maxQuota = { maxSeatQuota }
534538 planSeatLimit = { planSeatLimit }
539+ canManageBilling = { canManageBilling }
535540 />
536541 ) : canUpgrade ? (
537542 showSelfServe ? (
@@ -864,6 +869,7 @@ export function PurchaseSeatsModal({
864869 maxQuota,
865870 planSeatLimit,
866871 triggerButton,
872+ canManageBilling = true ,
867873} : {
868874 seatPricing : {
869875 stepSize : number ;
@@ -874,6 +880,7 @@ export function PurchaseSeatsModal({
874880 maxQuota : number ;
875881 planSeatLimit : number ;
876882 triggerButton ?: React . ReactElement ;
883+ canManageBilling ?: boolean ;
877884} ) {
878885 const showSelfServe = useShowSelfServe ( ) ;
879886 const fetcher = useFetcher ( ) ;
@@ -933,15 +940,30 @@ export function PurchaseSeatsModal({
933940 ) ;
934941 }
935942
943+ // Buying seats is a billing action — disable the trigger (and explain why)
944+ // when the role can't manage billing. The action enforces it independently.
945+ const noBillingTooltip = "You don't have permission to manage billing" ;
946+ const trigger = canManageBilling ? (
947+ triggerButton ?? (
948+ < Button variant = "primary/small" onClick = { ( ) => setOpen ( true ) } >
949+ { title }
950+ </ Button >
951+ )
952+ ) : triggerButton ? (
953+ cloneElement ( triggerButton , { disabled : true , tooltip : noBillingTooltip } )
954+ ) : (
955+ < PermissionButton
956+ variant = "primary/small"
957+ hasPermission = { false }
958+ noPermissionTooltip = { noBillingTooltip }
959+ >
960+ { title }
961+ </ PermissionButton >
962+ ) ;
963+
936964 return (
937965 < Dialog open = { open } onOpenChange = { setOpen } >
938- < DialogTrigger asChild >
939- { triggerButton ?? (
940- < Button variant = "primary/small" onClick = { ( ) => setOpen ( true ) } >
941- { title }
942- </ Button >
943- ) }
944- </ DialogTrigger >
966+ < DialogTrigger asChild > { trigger } </ DialogTrigger >
945967 < DialogContent >
946968 < DialogHeader > { title } </ DialogHeader >
947969 < fetcher . Form method = "post" action = { organizationTeamPath ( organization ) } { ...form . props } >
0 commit comments