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 ofOrganizationMembershipRequestobjects.- Each
OrganizationMembershipRequestobject has anaccept()andreject()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 returnsdata, which is an array ofOrganizationMembershipRequestobjects. - 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()andreject()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