Build a custom flow for managing a user's Organization invitations
This guide will demonstrate how to use the Clerk API to build a custom flow for managing a user's Organization invitations.
The following example:
- Uses the useOrganizationList() hook to get
userInvitations, which is a list of the user's Organization invitations.userInvitationsis an object withdatathat contains an array ofUserOrganizationInvitationobjects.- Each
UserOrganizationInvitationobject has anaccept()method that accepts the invitation to the Organization.
- Maps over the
dataarray to display the invitations in a table, providing an "Accept" button for each invitation that calls theaccept()method.
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack Start.
'use client'
import { useOrganizationList } from '@clerk/nextjs'
import React from 'react'
export default function UserInvitationsList() {
const { isLoaded, userInvitations } = useOrganizationList({
userInvitations: {
infinite: true,
keepPreviousData: true,
},
})
if (!isLoaded || userInvitations.isLoading) {
return <>Loading</>
}
return (
<>
<h1>Organization invitations</h1>
<table>
<thead>
<tr>
<th>Email</th>
<th>Organization name</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{userInvitations.data?.map((invitation) => (
<tr key={invitation.id}>
<td>{invitation.emailAddress}</td>
<td>{invitation.publicOrganizationData.name}</td>
<td>{invitation.role}</td>
<td>
<button onClick={() => invitation.accept()}>Accept</button>
</td>
</tr>
))}
</tbody>
</table>
<button disabled={!userInvitations.hasNextPage} onClick={userInvitations.fetchNext}>
Load more
</button>
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useOrganizationList } from '@clerk/expo'
import * as React from 'react'
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native'
export default function UserInvitationsList() {
const { isLoaded, userInvitations } = useOrganizationList({
userInvitations: {
infinite: true,
keepPreviousData: true,
},
})
if (!isLoaded || userInvitations.isLoading) {
return (
<ThemedView style={styles.center}>
<ActivityIndicator size="large" />
<ThemedText>Loading</ThemedText>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Organization invitations
</ThemedText>
{userInvitations.data && userInvitations.data.length > 0 ? (
<>
<ScrollView style={styles.scrollView}>
{userInvitations.data?.map((invitation) => (
<View key={invitation.id} style={styles.card}>
<ThemedText style={styles.label}>Email:</ThemedText>
<ThemedText style={styles.value}>{invitation.emailAddress}</ThemedText>
<ThemedText style={styles.label}>Organization name:</ThemedText>
<ThemedText style={styles.value}>
{invitation.publicOrganizationData?.name || 'N/A'}
</ThemedText>
<ThemedText style={styles.label}>Role:</ThemedText>
<ThemedText style={styles.value}>{invitation.role}</ThemedText>
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={() => invitation.accept()}
>
<ThemedText style={styles.buttonText}>Accept</ThemedText>
</Pressable>
</View>
))}
</ScrollView>
<Pressable
style={({ pressed }) => [
styles.loadMoreButton,
!userInvitations.hasNextPage && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!userInvitations.hasNextPage}
onPress={() => userInvitations.fetchNext?.()}
>
<ThemedText style={styles.buttonText}>Load more</ThemedText>
</Pressable>
</>
) : (
<ThemedText>No invitations found</ThemedText>
)}
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
title: {
marginBottom: 20,
},
label: {
fontWeight: '600',
fontSize: 14,
},
value: {
fontSize: 16,
marginBottom: 8,
},
scrollView: {
flex: 1,
},
card: {
backgroundColor: 'rgba(128, 128, 128, 0.1)',
borderRadius: 8,
padding: 16,
marginBottom: 12,
gap: 8,
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
loadMoreButton: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 20,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
})The following example:
- Calls the
getOrganizationInvitations()method to retrieve the list of Organization invitations for the active user. This method returnsdata, which is an array ofUserOrganizationInvitationobjects. - Maps over the
dataarray to display the invitations in a table. - Provides an "Accept" button for each invitation that calls the
accept()method.
Use the following tabs to view the code necessary for the index.html and main.js files.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clerk + JavaScript App</title>
</head>
<body>
<div id="app"></div>
<h1>Organization invitations</h1>
<table>
<thead>
<tr>
<th>Email</th>
<th>Organization name</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="invitations-table-body"></tbody>
</table>
<script type="module" src="/src/main.js" async crossorigin="anonymous"></script>
</body>
</html>import { Clerk } from '@clerk/clerk-js'
const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!pubKey) {
throw new Error('Add your VITE_CLERK_PUBLISHABLE_KEY to .env file')
}
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY')
await clerk.load()
if (clerk.isSignedIn) {
const { data } = await clerk.user.getOrganizationInvitations()
const invitations = data
invitations.map((invitation) => {
const tableBody = document.getElementById('invitations-table-body')
const row = tableBody.insertRow()
row.insertCell().textContent = invitation.emailAddress
row.insertCell().textContent = invitation.publicOrganizationData.name
row.insertCell().textContent = invitation.role
row.insertCell().textContent = invitation.status
// Show accept button for pending invitations
if (invitation.status === 'pending') {
const acceptBtn = document.createElement('button')
acceptBtn.textContent = 'Accept'
acceptBtn.addEventListener('click', async function (e) {
e.preventDefault()
await invitation.accept()
})
row.insertCell().appendChild(acceptBtn)
}
})
} else {
// If there is no active user, mount Clerk's <SignIn />
document.getElementById('app').innerHTML = `
<div id="sign-in"></div>
`
const signInDiv = document.getElementById('sign-in')
clerk.mountSignIn(signInDiv)
}import SwiftUI
import ClerkKit
struct ManageUserOrgInvitationsView: View {
@Environment(Clerk.self) private var clerk
@State private var invitations: [UserOrganizationInvitation] = []
var body: some View {
VStack {
ForEach(invitations) { invitation in
HStack {
Text(invitation.publicOrganizationData.name)
Button("Accept") {
Task {
await acceptInvitation(invitation)
await fetchUserOrganizationInvitations()
}
}
}
}
}
.task { await fetchUserOrganizationInvitations() }
}
}
extension ManageUserOrgInvitationsView {
func fetchUserOrganizationInvitations() async {
do {
invitations = try await clerk.user?.getOrganizationInvitations().data ?? []
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
invitations = []
}
}
func acceptInvitation(_ invitation: UserOrganizationInvitation) async {
do {
try await invitation.accept()
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
}import com.clerk.api.network.serialization.ClerkResult
import com.clerk.api.organizations.UserOrganizationInvitation
import com.clerk.api.organizations.accept
import com.clerk.api.user.User
suspend fun fetchUserOrganizationInvitations(): List<UserOrganizationInvitation> {
return when (val invitationsResult = User.getOrganizationInvitations(status = "pending")) {
is ClerkResult.Success -> invitationsResult.value.data
is ClerkResult.Failure -> emptyList()
}
}
suspend fun acceptInvitation(invitation: UserOrganizationInvitation) {
invitation.accept()
}Feedback
Last updated on