Skip to main content
Docs

Build a custom flow for managing Organization membership requests

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

This guide will demonstrate how to use the Clerk API to build a custom flow for managing Organization membership requests.

Tip

Examples for this SDK aren't available yet. For now, try adapting the available example to fit your SDK.

The following example:

  1. Uses the useOrganization() hook to get membershipRequests, which is a list of the membership requests.
    • membershipRequests is an object with data that contains an array of OrganizationMembershipRequest objects.
    • Each OrganizationMembershipRequest object has an accept() and reject() method to accept or reject the membership request, respectively.
  2. Maps over the data array to display the membership requests in a table, providing an "Accept" and "Reject" button for each request that calls the accept() and reject() methods, respectively.
app/components/MembershipRequests.tsx
'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>
    </>
  )
}
app/components/manage-membership-requests.tsx
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:

  1. Calls the getMembershipRequests() method to retrieve the list of membership requests for the . This method returns data, which is an array of OrganizationMembershipRequest objects.
  2. Maps over the data array to display the membership requests in a table.
  3. 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.

index.html
<!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>
main.js
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)
}
ManageMembershipRequestsView.swift
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

What did you think of this content?

Last updated on