11'use client' ;
22
3- import { useState , useMemo } from 'react' ;
3+ import { useState , useMemo , useEffect } from 'react' ;
44import { BoundlessButton } from '@/components/buttons' ;
55import { useOrganization } from '@/lib/providers/OrganizationProvider' ;
6+ import {
7+ removeBetterAuthMember ,
8+ listBetterAuthInvitations ,
9+ cancelBetterAuthInvitation ,
10+ } from '@/lib/api/better-auth-organization' ;
611import EmailInviteSection from './MembersTab/EmailInviteSection' ;
712import PermissionsTable from './MembersTab/PermissionsTable' ;
813import TeamManagementSection from './MembersTab/TeamManagementSection' ;
914import { toast } from 'sonner' ;
15+ import { X , Mail , Clock } from 'lucide-react' ;
1016
1117interface Member {
1218 id : string ;
@@ -18,6 +24,17 @@ interface Member {
1824 status : 'active' | 'pending' | 'suspended' ;
1925}
2026
27+ interface Invitation {
28+ id : string ;
29+ email : string ;
30+ role : string ;
31+ status : string ;
32+ expiresAt : Date ;
33+ createdAt ?: Date ;
34+ organizationId : string ;
35+ inviterId : string ;
36+ }
37+
2138interface MembersTabProps {
2239 onSave ?: ( members : Member [ ] ) => void ;
2340}
@@ -28,11 +45,22 @@ export default function MembersTab({ onSave }: MembersTabProps) {
2845 activeOrgId,
2946 updateOrganizationMembers,
3047 inviteMember,
31- removeMember,
3248 assignRole,
3349 isLoading,
50+ isOwner,
51+ refreshOrganization,
3452 } = useOrganization ( ) ;
3553
54+ const [ userIsOwner , setUserIsOwner ] = useState ( false ) ;
55+ useEffect ( ( ) => {
56+ const checkUserIsOwner = async ( ) => {
57+ const checkIsOwner = await isOwner ( activeOrgId || undefined ) ;
58+ setUserIsOwner ( checkIsOwner ) ;
59+ console . log ( 'userIsOwner' , checkIsOwner ) ;
60+ } ;
61+ checkUserIsOwner ( ) ;
62+ } , [ activeOrgId ] ) ;
63+
3664 const members : Member [ ] = useMemo ( ( ) => {
3765 const emails = activeOrg ?. members ?? [ ] ;
3866 return emails . map ( ( email , idx ) => ( {
@@ -44,15 +72,53 @@ export default function MembersTab({ onSave }: MembersTabProps) {
4472 status : 'active' ,
4573 } ) ) ;
4674 } , [ activeOrg ?. members ] ) ;
75+
4776 const [ inviteEmails , setInviteEmails ] = useState < string [ ] > ( [ ] ) ;
4877 const [ emailInput , setEmailInput ] = useState ( '' ) ;
4978 const [ hasUserChanges , setHasUserChanges ] = useState ( false ) ;
79+ const [ invitations , setInvitations ] = useState < Invitation [ ] > ( [ ] ) ;
80+ const [ loadingInvitations , setLoadingInvitations ] = useState ( false ) ;
81+ const [ cancelingInvitation , setCancelingInvitation ] = useState < string | null > (
82+ null
83+ ) ;
84+
85+ // Fetch invitations when component mounts or activeOrg changes
86+ useEffect ( ( ) => {
87+ const fetchInvitations = async ( ) => {
88+ if ( ! activeOrg ?. betterAuthOrgId ) return ;
89+
90+ setLoadingInvitations ( true ) ;
91+ try {
92+ const data = await listBetterAuthInvitations ( activeOrg . betterAuthOrgId ) ;
93+ setInvitations ( data || [ ] ) ;
94+ } catch ( error ) {
95+ console . error ( 'Error fetching invitations:' , error ) ;
96+ toast . error ( 'Failed to load invitations' ) ;
97+ } finally {
98+ setLoadingInvitations ( false ) ;
99+ }
100+ } ;
101+
102+ fetchInvitations ( ) ;
103+ } , [ activeOrg ?. betterAuthOrgId ] ) ;
50104
51- const handleInvite = ( ) => {
105+ const handleInvite = async ( ) => {
52106 if ( inviteEmails . length > 0 && activeOrgId ) {
53- inviteMember ( activeOrgId , inviteEmails ) ;
107+ await inviteMember ( activeOrgId , inviteEmails ) ;
54108 setInviteEmails ( [ ] ) ;
55109 setEmailInput ( '' ) ;
110+
111+ // Refresh invitations list after sending new invites
112+ if ( activeOrg ?. betterAuthOrgId ) {
113+ try {
114+ const data = await listBetterAuthInvitations (
115+ activeOrg . betterAuthOrgId
116+ ) ;
117+ setInvitations ( data || [ ] ) ;
118+ } catch ( error ) {
119+ console . error ( 'Error refreshing invitations:' , error ) ;
120+ }
121+ }
56122 }
57123 } ;
58124
@@ -73,11 +139,76 @@ export default function MembersTab({ onSave }: MembersTabProps) {
73139 }
74140 } ;
75141
76- const handleRemoveMember = ( memberId : string ) => {
77- if ( ! activeOrgId ) return ;
78- const m = members . find ( x => x . id === memberId ) ;
79- if ( ! m ) return ;
80- removeMember ( activeOrgId , m . email ) ;
142+ const handleRemoveMember = async ( memberId : string ) => {
143+ if ( ! activeOrgId ) {
144+ toast . error ( 'No organization selected' ) ;
145+ return ;
146+ }
147+
148+ // Check if user is owner
149+ const userIsOwner = await isOwner ( activeOrgId ) ;
150+ if ( ! userIsOwner ) {
151+ toast . error ( 'Only organization owners can remove members' ) ;
152+ return ;
153+ }
154+
155+ const member = members . find ( x => x . id === memberId ) ;
156+ if ( ! member ) {
157+ toast . error ( 'Member not found' ) ;
158+ return ;
159+ }
160+
161+ try {
162+ // Check if organization uses Better Auth
163+ if ( activeOrg ?. betterAuthOrgId ) {
164+ // Use Better Auth API
165+ await removeBetterAuthMember ( member . email , activeOrg . betterAuthOrgId ) ;
166+ toast . success ( `${ member . email } has been removed from the organization` ) ;
167+ } else {
168+ // Fallback to custom API for legacy organizations
169+ toast . error (
170+ 'This organization does not support Better Auth member removal'
171+ ) ;
172+ return ;
173+ }
174+
175+ // Refresh organization data to reflect changes
176+ await refreshOrganization ( ) ;
177+ setHasUserChanges ( true ) ;
178+ } catch ( error ) {
179+ const msg = error instanceof Error ? error . message : String ( error ) ;
180+ toast . error ( `Failed to remove member: ${ msg } ` ) ;
181+ console . error ( 'Error removing member:' , error ) ;
182+ }
183+ } ;
184+
185+ const handleCancelInvitation = async ( invitationId : string ) => {
186+ if ( ! activeOrgId ) {
187+ toast . error ( 'No organization selected' ) ;
188+ return ;
189+ }
190+
191+ // Check if user is owner
192+ const userIsOwner = await isOwner ( activeOrgId ) ;
193+ if ( ! userIsOwner ) {
194+ toast . error ( 'Only organization owners can cancel invitations' ) ;
195+ return ;
196+ }
197+
198+ setCancelingInvitation ( invitationId ) ;
199+ try {
200+ await cancelBetterAuthInvitation ( invitationId ) ;
201+ toast . success ( 'Invitation cancelled successfully' ) ;
202+
203+ // Remove invitation from local state
204+ setInvitations ( prev => prev . filter ( inv => inv . id !== invitationId ) ) ;
205+ } catch ( error ) {
206+ const msg = error instanceof Error ? error . message : String ( error ) ;
207+ toast . error ( `Failed to cancel invitation: ${ msg } ` ) ;
208+ console . error ( 'Error canceling invitation:' , error ) ;
209+ } finally {
210+ setCancelingInvitation ( null ) ;
211+ }
81212 } ;
82213
83214 const handleSave = async ( ) => {
@@ -100,6 +231,74 @@ export default function MembersTab({ onSave }: MembersTabProps) {
100231 onInvite = { handleInvite }
101232 />
102233
234+ { /* Pending Invitations Section */ }
235+ { activeOrg ?. betterAuthOrgId && (
236+ < div className = 'space-y-4' >
237+ < div className = 'flex items-center justify-between' >
238+ < h3 className = 'text-lg font-semibold text-white' >
239+ Pending Invitations
240+ </ h3 >
241+ { loadingInvitations && (
242+ < span className = 'text-sm text-zinc-400' > Loading...</ span >
243+ ) }
244+ </ div >
245+
246+ { invitations . length > 0 ? (
247+ < div className = 'space-y-2' >
248+ { invitations . map ( invitation => (
249+ < div
250+ key = { invitation . id }
251+ className = 'flex items-center justify-between rounded-lg border border-zinc-800 bg-zinc-900/50 p-4'
252+ >
253+ < div className = 'flex items-center gap-3' >
254+ < div className = 'flex h-10 w-10 items-center justify-center rounded-full bg-zinc-800' >
255+ < Mail className = 'h-5 w-5 text-zinc-400' />
256+ </ div >
257+ < div >
258+ < p className = 'font-medium text-white' >
259+ { invitation . email }
260+ </ p >
261+ < div className = 'flex items-center gap-2 text-sm text-zinc-400' >
262+ < span > Role: { invitation . role } </ span >
263+ < span > •</ span >
264+ < div className = 'flex items-center gap-1' >
265+ < Clock className = 'h-3 w-3' />
266+ < span >
267+ Expires:{ ' ' }
268+ { new Date (
269+ invitation . expiresAt
270+ ) . toLocaleDateString ( ) }
271+ </ span >
272+ </ div >
273+ </ div >
274+ </ div >
275+ </ div >
276+ { userIsOwner }
277+ { userIsOwner && (
278+ < button
279+ onClick = { ( ) => handleCancelInvitation ( invitation . id ) }
280+ disabled = { cancelingInvitation === invitation . id }
281+ className = 'flex items-center gap-2 rounded-lg border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-400 transition-colors hover:bg-red-500/20 disabled:opacity-50'
282+ title = 'Cancel invitation'
283+ >
284+ < X className = 'h-4 w-4' />
285+ { cancelingInvitation === invitation . id
286+ ? 'Canceling...'
287+ : 'Cancel' }
288+ </ button >
289+ ) }
290+ </ div >
291+ ) ) }
292+ </ div >
293+ ) : (
294+ < div className = 'rounded-lg border border-zinc-800 bg-zinc-900/50 p-8 text-center' >
295+ < Mail className = 'mx-auto mb-2 h-8 w-8 text-zinc-600' />
296+ < p className = 'text-sm text-zinc-400' > No pending invitations</ p >
297+ </ div >
298+ ) }
299+ </ div >
300+ ) }
301+
103302 < PermissionsTable />
104303
105304 < TeamManagementSection
0 commit comments