@@ -19,6 +19,7 @@ import {
1919 ScrollAreaNative ,
2020 Select ,
2121 Tooltip ,
22+ PanelBanner ,
2223 css ,
2324 theme ,
2425} from "@webstudio-is/design-system" ;
@@ -254,18 +255,11 @@ const computeAvailableSeats = (
254255 { success : true }
255256 > [ "data" ]
256257 | undefined ,
257- optimisticPending : OptimisticPendingInvite [ ] ,
258- // Optimistic upper bound for maxSeats set when extra seats are confirmed,
259- // to avoid flickering while the Stripe webhook has not yet updated TransactionLog.
260- confirmedMaxSeats ?: number
258+ optimisticPending : OptimisticPendingInvite [ ]
261259) : number | undefined => {
262260 if ( membersData === undefined ) {
263261 return ;
264262 }
265- const effectiveMaxSeats =
266- confirmedMaxSeats !== undefined
267- ? Math . max ( membersData . maxSeats , confirmedMaxSeats )
268- : membersData . maxSeats ;
269263 const knownEmails = new Set ( [
270264 membersData . owner . email ,
271265 ...membersData . members . map ( ( m ) => m . email ?? "" ) ,
@@ -275,7 +269,7 @@ const computeAvailableSeats = (
275269 ( o ) => ! knownEmails . has ( o . email )
276270 ) . length ;
277271 return (
278- effectiveMaxSeats -
272+ membersData . maxSeats -
279273 membersData . members . length -
280274 membersData . pendingInvites . length -
281275 extraCount
@@ -435,10 +429,6 @@ export const ManageMembersDialog = ({
435429 relation : Role ;
436430 extraSeats : number ;
437431 } > ( ) ;
438- // Optimistic maxSeats ceiling: set when the user confirms extra-seat charges so
439- // the footer and confirmation gate stay correct while the Stripe webhook is in
440- // flight (TransactionLog not yet updated).
441- const [ confirmedMaxSeats , setConfirmedMaxSeats ] = useState < number > ( ) ;
442432 const formRef = useRef < HTMLFormElement > ( null ) ;
443433
444434 const { load, data } = trpcClient . workspace . listMembers . useQuery ( ) ;
@@ -453,22 +443,23 @@ export const ManageMembersDialog = ({
453443 }
454444 } , [ isOpen , isOwner , load , workspace . id ] ) ;
455445
456- // Clear the optimistic override once the server reflects the paid seats.
457- useEffect ( ( ) => {
458- if (
459- membersData !== undefined &&
460- confirmedMaxSeats !== undefined &&
461- membersData . maxSeats >= confirmedMaxSeats
462- ) {
463- setConfirmedMaxSeats ( undefined ) ;
464- }
465- } , [ membersData , confirmedMaxSeats ] ) ;
446+ const availableSeats = computeAvailableSeats ( membersData , optimisticPending ) ;
466447
467- const availableSeats = computeAvailableSeats (
468- membersData ,
469- optimisticPending ,
470- confirmedMaxSeats
471- ) ;
448+ const syncSeatsMutation = trpcClient . workspace . syncSeats . useMutation ( ) ;
449+
450+ const overCapacity =
451+ availableSeats !== undefined && availableSeats < 0 ? - availableSeats : 0 ;
452+
453+ const handleSyncSeats = ( ) => {
454+ syncSeatsMutation . send ( { workspaceId : workspace . id } , ( result ) => {
455+ if ( result && "error" in result ) {
456+ setErrors ( [ result . error ] ) ;
457+ return ;
458+ }
459+ handleRefresh ( ) ;
460+ revalidator . revalidate ( ) ;
461+ } ) ;
462+ } ;
472463
473464 const performInvite = async ( emails : string [ ] , relation : Role ) => {
474465 setErrors ( undefined ) ;
@@ -536,14 +527,6 @@ export const ManageMembersDialog = ({
536527 onConfirm = { async ( ) => {
537528 const confirm = pendingConfirm ;
538529 setPendingConfirm ( undefined ) ;
539- // Optimistically raise confirmedMaxSeats so the footer and the
540- // confirmation gate reflect the payment before the webhook fires.
541- setConfirmedMaxSeats ( ( prev ) =>
542- Math . max (
543- prev ?? 0 ,
544- ( membersData ?. maxSeats ?? 0 ) + confirm . extraSeats
545- )
546- ) ;
547530 await performInvite ( confirm . emails , confirm . relation ) ;
548531 } }
549532 />
@@ -554,7 +537,6 @@ export const ManageMembersDialog = ({
554537 onOpenChange ( open ) ;
555538 if ( open === false ) {
556539 setErrors ( undefined ) ;
557- setConfirmedMaxSeats ( undefined ) ;
558540 }
559541 } }
560542 >
@@ -599,6 +581,31 @@ export const ManageMembersDialog = ({
599581 </ Flex >
600582 </ Flex >
601583 ) }
584+ { isOwner && overCapacity > 0 && (
585+ < PanelBanner variant = "warning" >
586+ < Flex direction = "column" gap = "2" >
587+ < Text >
588+ { `Your workspace has ${ overCapacity } more member${ overCapacity === 1 ? "" : "s" } than your plan covers. Non-owner members won't be able to access the workspace until this is resolved.` }
589+ </ Text >
590+ < Flex gap = "2" >
591+ < Button
592+ color = "dark"
593+ onClick = { handleSyncSeats }
594+ state = {
595+ syncSeatsMutation . state !== "idle"
596+ ? "pending"
597+ : undefined
598+ }
599+ >
600+ { `Buy ${ overCapacity } extra seat${ overCapacity === 1 ? "" : "s" } ` }
601+ </ Button >
602+ < Text color = "subtle" css = { { alignSelf : "center" } } >
603+ { `or remove ${ overCapacity } member${ overCapacity === 1 ? "" : "s" } ` }
604+ </ Text >
605+ </ Flex >
606+ </ Flex >
607+ </ PanelBanner >
608+ ) }
602609 < ScrollAreaNative
603610 css = { {
604611 maxHeight : 300 ,
0 commit comments