Build a custom flow for managing Organization membership requests
This guide will demonstrate how to use the Clerk API to build a custom flow for managing Organization membership requests.
The following example:
- Uses the useOrganization() hook to get
membershipRequests, which is a list of the membership requests.membershipRequestsis an object withdatathat contains an array of OrganizationMembershipRequest objects.- Each
OrganizationMembershipRequestobject has an accept() and reject() method to accept or reject the membership request, respectively.
- Maps over the
dataarray to display the membership requests in a table, providing an "Accept" and "Reject" button for each request that calls theaccept()andreject()methods, respectively.
'use client'
import { useOrganization } from '@clerk/nextjs'
export const MembershipRequestsParams = {
membershipRequests: {
pageSize: 5,
keepPreviousData: true,
},
}
// List of organization membership requests.
export const MembershipRequests = () => {
const { isLoaded, membershipRequests } = useOrganization(MembershipRequestsParams)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<h1>Membership requests</h1>
<table>
<thead>
<tr>
<th>User</th>
<th>Date requested</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{membershipRequests?.data?.map((mem) => (
<tr key={mem.id}>
<td>{mem.publicUserData.identifier}</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td>
<button
onClick={async () => {
await mem.accept()
}}
>
Accept
</button>
<button
onClick={async () => {
await mem.reject()
}}
>
Reject
</button>
</td>
</tr>
))}
</tbody>
</table>
<div>
<button
disabled={!membershipRequests?.hasPreviousPage || membershipRequests?.isFetching}
onClick={() => membershipRequests?.fetchPrevious?.()}
>
Previous
</button>
<button
disabled={!membershipRequests?.hasNextPage || membershipRequests?.isFetching}
onClick={() => membershipRequests?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useOrganization } from '@clerk/expo'
import * as React from 'react'
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native'
export const MembershipRequestsParams = {
membershipRequests: {
pageSize: 5,
keepPreviousData: true,
},
}
// List of organization membership requests.
export const MembershipRequests = () => {
const { isLoaded, membershipRequests } = useOrganization(MembershipRequestsParams)
if (!isLoaded) {
return (
<ThemedView style={styles.center}>
<ActivityIndicator size="large" />
<ThemedText>Loading</ThemedText>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Membership requests
</ThemedText>
{membershipRequests?.data && membershipRequests.data.length > 0 ? (
<>
<ScrollView style={styles.scrollView}>
{membershipRequests?.data?.map((mem) => (
<View key={mem.id} style={styles.card}>
<ThemedText style={styles.label}>User:</ThemedText>
<ThemedText style={styles.value}>
{mem.publicUserData?.identifier || 'N/A'}
</ThemedText>
<ThemedText style={styles.label}>Date requested:</ThemedText>
<ThemedText style={styles.value}>{mem.createdAt.toLocaleDateString()}</ThemedText>
<View style={styles.buttonRow}>
<Pressable
style={({ pressed }) => [styles.acceptButton, pressed && styles.buttonPressed]}
onPress={async () => {
await mem.accept()
}}
>
<ThemedText style={styles.buttonText}>Accept</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.rejectButton, pressed && styles.buttonPressed]}
onPress={async () => {
await mem.reject()
}}
>
<ThemedText style={styles.buttonText}>Reject</ThemedText>
</Pressable>
</View>
</View>
))}
</ScrollView>
<View style={styles.pagination}>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!membershipRequests?.hasPreviousPage || membershipRequests?.isFetching) &&
styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!membershipRequests?.hasPreviousPage || membershipRequests?.isFetching}
onPress={() => membershipRequests?.fetchPrevious?.()}
>
<ThemedText style={styles.buttonText}>Previous</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!membershipRequests?.hasNextPage || membershipRequests?.isFetching) &&
styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!membershipRequests?.hasNextPage || membershipRequests?.isFetching}
onPress={() => membershipRequests?.fetchNext?.()}
>
<ThemedText style={styles.buttonText}>Next</ThemedText>
</Pressable>
</View>
</>
) : (
<ThemedText>No membership requests</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,
},
buttonRow: {
flexDirection: 'row',
gap: 12,
marginTop: 8,
},
acceptButton: {
flex: 1,
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
rejectButton: {
flex: 1,
backgroundColor: '#dc3545',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
pagination: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 20,
gap: 12,
},
paginationButton: {
flex: 1,
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
})The following example:
- Calls the getMembershipRequests() method to retrieve the list of membership requests for the . This method returns
data, which is an array of OrganizationMembershipRequest objects. - Maps over the
dataarray to display the membership requests in a table. - Provides an "Accept" and "Reject" button for each request that calls the accept() and reject() methods, respectively.
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>Membership Requests</h1>
<table>
<thead>
<tr>
<th>User</th>
<th>Date requested</th>
<th>Accept</th>
<th>Reject</th>
</tr>
</thead>
<tbody id="requests-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) {
// Check for an Active Organization
if (clerk.organization) {
const requestsTable = document.getElementById('requests-table-body')
const { data } = await clerk.organization
.getMembershipRequests()
.then((res) => console.log(`Membership requests:`, data).catch((err) => console.error(err)))
const requests = data
requests.map((request) => {
const row = requestsTable.insertRow()
row.insertCell().textContent = request.publicUserData.identifier
row.insertCell().textContent = request.createdAt.toLocaleDateString()
// Accept request
const acceptBtn = document.createElement('button')
acceptBtn.textContent = 'Accept'
acceptBtn.addEventListener('click', async function (e) {
e.preventDefault()
await request.accept()
})
row.insertCell().appendChild(acceptBtn)
// Reject request
const rejectBtn = document.createElement('button')
rejectBtn.textContent = 'Reject'
rejectBtn.addEventListener('click', async function (e) {
e.preventDefault()
await request.reject()
})
row.insertCell().appendChild(rejectBtn)
})
} else {
// If there is no Active Organization,
// mount Clerk's <OrganizationSwitcher />
// to allow the user to set an organization as active
document.getElementById('app').innerHTML = `
<h2>Select an organization to set it as active</h2>
<div id="org-switcher"></div>
`
const orgSwitcherDiv = document.getElementById('org-switcher')
clerk.mountOrganizationSwitcher(orgSwitcherDiv)
}
} 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 ManageMembershipRequestsView: View {
@State private var membershipRequests: [OrganizationMembershipRequest] = []
let organization: Organization
var body: some View {
VStack {
ForEach(membershipRequests) { request in
HStack {
Text(request.publicUserData?.identifier ?? "Unknown user")
Button("Accept") {
Task {
await acceptMembershipRequest(request)
await fetchMembershipRequests()
}
}
Button("Reject") {
Task {
await rejectMembershipRequest(request)
await fetchMembershipRequests()
}
}
}
}
}
.task { await fetchMembershipRequests() }
}
}
extension ManageMembershipRequestsView {
func fetchMembershipRequests() async {
do {
membershipRequests = try await organization.getMembershipRequests(status: "pending").data
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
membershipRequests = []
}
}
func acceptMembershipRequest(_ request: OrganizationMembershipRequest) async {
do {
try await request.accept()
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
func rejectMembershipRequest(_ request: OrganizationMembershipRequest) async {
do {
try await request.reject()
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
}import com.clerk.api.Clerk
import com.clerk.api.network.serialization.ClerkResult
import com.clerk.api.organizations.OrganizationMembershipRequest
import com.clerk.api.organizations.accept
import com.clerk.api.organizations.getMembershipRequests
import com.clerk.api.organizations.reject
import com.clerk.api.user.getOrganizationMemberships
suspend fun fetchMembershipRequests(): List<OrganizationMembershipRequest> {
val activeOrganizationId = Clerk.session?.lastActiveOrganizationId ?: return emptyList()
val membershipsResult = Clerk.user?.getOrganizationMemberships() ?: return emptyList()
if (membershipsResult !is ClerkResult.Success) return emptyList()
val activeOrganization =
membershipsResult.value.data.firstOrNull { it.organization.id == activeOrganizationId }?.organization
?: return emptyList()
return when (val requestsResult = activeOrganization.getMembershipRequests(status = "pending")) {
is ClerkResult.Success -> requestsResult.value.data
is ClerkResult.Failure -> emptyList()
}
}
suspend fun acceptMembershipRequest(request: OrganizationMembershipRequest) {
request.accept()
}
suspend fun rejectMembershipRequest(request: OrganizationMembershipRequest) {
request.reject()
}Feedback
Last updated on