Connect your users
Link the users authenticated in your app to their Octopus community profile through SSO, then handle refused connections, entitlements and guest sessions.
New to the SDK? Start with the Quickstart.
Before you begin
- The SDK is installed and initialized in SSO mode: see the Quickstart.
- Your backend has a route that returns a JWT for the signed-in user, signed with your community's secret: see Generate a signed JWT.
How it works
Your app stays in charge of sign-in. When a user signs in, your app passes their id and, optionally, their nickname, bio and picture to connectUser. When they sign out, it calls disconnectUser. Between the two, members who are not signed in browse the community as guests: they can read, and the SDK asks your app to sign them in when they try an action that needs an account.
The userId is the string that identifies your user for Octopus. It must be unique and always refer to the same user.
Client user token
To prove that the connected user is legitimate, connectUser takes a token provider: a callback that returns a JWT signed by your backend. The SDK calls it when it connects the user and again each time it needs a fresh token, for example when the JWT expires or when you refresh entitlements. Add a route such as /generateOctopusSsoToken to your backend and call it from the token provider. The signing secret stays on your servers: see Generate a signed JWT.
App-managed profile fields
The nickname, bio and picture you pass to connectUser are used in one of two ways, depending on whether you declared the field as app-managed when you initialized the SDK:
- App-managed field: your app owns it. Each call to
connectUseroverwrites the field in the member's community profile, so it always matches your app. Members cannot edit it in the community: when they try, the SDK asks your app to open its own profile editor. - Other fields: the value you pass only fills the community profile the first time. Once the member edits their community profile, it stays separate from yours, and later changes on either side are not copied to the other.
Call connectUser again each time the user's profile changes in your app. The back office can also lock profile fields for the whole community: see Profile field lock.
1. Declare app-managed fields
Declare the fields your app owns when you initialize the SDK, and open your own profile editor when a member tries to edit one of them. Skip this step if members may edit every profile field in the community.
- Android
- iOS
- Flutter
- React Native
- Unity
Pass the fields to ConnectionMode.SSO, and open your profile editor from onNavigateToProfileEdit. The ProfileField it receives is the field the member tried to edit, or null.
import com.octopuscommunity.sdk.domain.model.ConnectionMode
import com.octopuscommunity.sdk.domain.model.ProfileField
OctopusSDK.initialize(
context = this,
apiKey = "YOUR_API_KEY",
connectionMode = ConnectionMode.SSO(
appManagedFields = setOf(ProfileField.NICKNAME, ProfileField.PICTURE),
),
)
// Where you host the community:
OctopusHomeScreen(
navController = navController,
onNavigateToLogin = { /* Open your sign-in screen */ },
onNavigateToProfileEdit = { field -> /* Open your profile editor */ },
)
Pass the fields to the SSO configuration. modifyUser receives the field the member tried to edit, or nil.
let octopus = try OctopusSDK(
apiKey: "YOUR_API_KEY",
connectionMode: .sso(.init(
appManagedFields: [.nickname, .picture],
loginRequired: { /* Start your sign-in flow */ },
modifyUser: { field in /* Open your profile editor */ }
))
)
Pass the fields to initialize, and open your profile editor from onModifyUser on OctopusHomeScreen. It receives the field the member tried to edit, or null.
await OctopusSDK().initialize(
apiKey: 'YOUR_API_KEY',
appManagedFields: [ProfileField.nickname, ProfileField.picture],
);
OctopusHomeScreen(
onNavigateToLogin: () {/* Open your sign-in screen */},
onModifyUser: (fieldToEdit) {/* Open your profile editor */},
);
Pass the fields in connectionMode, and listen for edit requests with addEditUserListener. fieldToEdit is 'username', 'biography', 'profilePicture' or null.
import { addEditUserListener, initialize } from '@octopus-community/react-native';
await initialize({
apiKey: 'YOUR_API_KEY',
connectionMode: { type: 'sso', appManagedFields: ['username', 'profilePicture'] },
});
const subscription = addEditUserListener(({ fieldToEdit }) => {
// Open your profile editor
});
Pass the fields to ConnectionMode.SSO, and handle OnModifyUser. It receives the field the member tried to edit, or null.
OctopusSDK.Initialize("YOUR_API_KEY", ConnectionMode.SSO(ProfileField.NICKNAME, ProfileField.PICTURE));
OctopusSDK.OnModifyUser += (ProfileField? field) =>
{
// Open your profile editor
};
2. Link your user to the SDK
Call connectUser as soon as your user is signed in, and again whenever their profile changes.
- Android
- iOS
- Flutter
- React Native
- Unity
Parameters of OctopusSDK.connectUser, a suspending function:
user: ClientUser?:ClientUser(userId, profile), whereprofileis an optionalClientUser.Profile(nickname, bio, picture).pictureis aResource?:Resource.Remote(url)for a URL,Resource.Local(uri)for a local file.tokenProvider: suspend () -> String: returns a fresh JWT from your backend.
It returns OctopusResult<ConnectionState, ClientUserError>.
import com.octopuscommunity.sdk.OctopusSDK
import com.octopuscommunity.sdk.domain.model.ClientUser
import com.octopuscommunity.sdk.domain.model.Resource
import com.octopuscommunity.sdk.domain.network.OctopusResult
import com.octopuscommunity.sdk.domain.repository.ClientUserError
val result = OctopusSDK.connectUser(
user = ClientUser(
userId = user.id,
profile = ClientUser.Profile(
nickname = user.nickname,
bio = user.bio,
picture = user.pictureUrl?.let { Resource.Remote(it) },
),
),
tokenProvider = { yourBackend.fetchOctopusToken(user.id) },
)
when (result) {
is OctopusResult.Success -> { /* Connected */ }
is OctopusResult.Failure.InvalidArguments -> when (val error = result.error) {
is ClientUserError.UserBanned -> showMessage(error.errorMessage)
else -> { /* MissingToken, ProfileError or Other: see step 3 */ }
}
is OctopusResult.Failure.Connection -> { /* Network or server failure: retry later */ }
}
Parameters of connectUser:
ClientUser(userId:profile:), whereprofileis aClientUser.Profile(nickname:bio:picture:). All three profile fields are optional.pictureisDatathatUIImage(data:)can read.tokenProvider: an async closure that returns a fresh JWT from your backend.
The async form throws an OctopusConnectUserError. A non-async form with the same parameters also exists, for code that cannot await: it starts the connection and returns immediately, without reporting the result.
do {
try await octopus.connectUser(
ClientUser(
userId: user.id,
profile: ClientUser.Profile(nickname: user.nickname, bio: user.bio, picture: user.pictureData)
),
tokenProvider: { try await yourBackend.fetchOctopusToken(userId: user.id) }
)
} catch {
// error is an OctopusConnectUserError: see step 3
}
Parameters of OctopusSDK().connectUser:
userId: String, required.nickname,bio:String?.picture:String?, the URL of the picture.tokenProvider: Future<String> Function(): returns a fresh JWT from your backend. Thetokenparameter, which takes a static JWT, is deprecated: a static token fails once it expires.
It returns OctopusResult<void, ClientUserError>. Handle it: the analyzer warns when the result is ignored.
final result = await OctopusSDK().connectUser(
userId: user.id,
nickname: user.nickname,
bio: user.bio,
picture: user.pictureUrl,
tokenProvider: () => yourBackend.fetchOctopusToken(user.id),
);
switch (result) {
case OctopusSuccess():
break; // Connected
case OctopusInvalidArguments<OctopusServerError>(:final errors):
// The explicit <OctopusServerError> keeps this switch exhaustive.
for (final error in errors.whereType<ClientUserError>()) {
// See step 3 for the error types
}
case OctopusConnectionFailure():
break; // Network or server failure: retry later
}
Register a token provider first. In a component that stays mounted, use the useUserTokenProvider hook; outside React components, use addUserTokenRequestListener and keep the subscription.
import { connectUser, useUserTokenProvider } from '@octopus-community/react-native';
function App() {
useUserTokenProvider(async () => {
const response = await fetch('https://your-backend.example/generateOctopusSsoToken');
const { token } = await response.json();
return token;
});
// ...
}
await connectUser({
userId: user.id,
profile: {
username: user.nickname,
biography: user.bio,
profilePicture: user.pictureUrl, // a URL or a local file path
},
});
connectUser rejects when the connection is refused: see step 3.
Call ConnectUser on the Unity main thread, after Initialize in SSO mode, and do not start a second connection before the first one completes. Parameters:
userId: string, andnickname,bio,picture:string, which acceptnull.tokenProvider: Func<Task<string>>: returns a fresh JWT from your backend.onSuccess: ActionandonError: Action<OctopusClientUserError>≥ 1.13.0: the callback overload. The overload without callbacks returns aTaskand does not report errors.
OctopusSDK.ConnectUser(userId, nickname, bio, picture, FetchOctopusToken,
onSuccess: () => { /* Connected */ },
onError: error => { /* See step 3 */ });
static async Task<string> FetchOctopusToken()
{
// Call your backend with System.Net.Http and return the signed JWT
}
The token provider can run on a background thread while the community UI is open and the game loop is paused, when the SDK needs a fresh JWT. Use I/O that does not depend on the player loop, such as System.Net.Http, not UnityWebRequest, and do not await continuations that return to the main thread.
3. Handle a refused connection
A connection can be refused, for example when the token is invalid or when the member is banned. Show the ban message to the member: it comes from the server and is written for display. For the other errors, show your own message.
- Android
- iOS
- Flutter
- React Native
- Unity
connectUser returns OctopusResult.Failure.InvalidArguments holding a ClientUserError:
| Error | Meaning |
|---|---|
ClientUserError.UserBanned | The member is banned. errorMessage is displayable. |
ClientUserError.MissingToken | The token provider returned no token. |
ClientUserError.ProfileError | A profile field was refused, for example a nickname already taken. |
ClientUserError.Other | Any other refusal. |
A network or server failure returns OctopusResult.Failure.Connection instead.
The async connectUser throws an OctopusConnectUserError:
| Case | Meaning |
|---|---|
.userBanned(message) | The member is banned. message is displayable. |
.jwtError | The token was refused. |
.communityAccessDenied | The member has no access to this community. |
.profileError(_) | A profile field was refused. |
.noNetwork | The device is offline. |
.server(_), .other(_) | A server or unexpected failure. |
do {
try await octopus.connectUser(clientUser, tokenProvider: tokenProvider)
} catch {
switch error {
case .userBanned(let message): showMessage(message)
case .noNetwork: showRetry()
default: showGenericError()
}
}
The errors are the subtypes of ClientUserError:
| Type | Meaning |
|---|---|
ClientUserBannedError | The member is banned. errorMessage is displayable. |
ClientUserInvalidTokenError | The token was refused. Reported on iOS. |
ClientUserCommunityAccessDeniedError | The member has no access to this community. Reported on iOS. |
ClientUserMissingTokenError | The token provider returned no token. Reported on Android. |
ClientUserProfileError | A profile field was refused. |
ClientUserOtherError | Any other refusal. |
for (final error in errors.whereType<ClientUserError>()) {
switch (error) {
case ClientUserBannedError(:final errorMessage):
showMessage(errorMessage);
default:
showGenericError();
}
}
On iOS, a token refused on the first connection does not reach this result: the SDK falls back to a guest session and connectUser succeeds. Confirm the outcome with OctopusSDK.connectionState, which then emits OctopusConnected(isGuest: true).
connectUser rejects with a ConnectUserError. Check it with isConnectUserError, then read code and message:
code | Meaning |
|---|---|
USER_BANNED | The member is banned. message is displayable. |
MISSING_TOKEN, INVALID_TOKEN | The token provider returned no token, or the token was refused. |
TOKEN_REQUEST_CANCELLED, TOKEN_REQUEST_TIMEOUT | The token request did not complete. |
PROFILE_ERROR | A profile field was refused. |
COMMUNITY_ACCESS_DENIED | The member has no access to this community. |
NO_NETWORK | The device is offline. |
SERVER_ERROR, USER_NOT_AUTHENTICATED, PERMISSION_DENIED, CONTENT_UNAVAILABLE | A server-side refusal. |
CONNECT_USER_ERROR | Any other failure. |
import { connectUser, isConnectUserError } from '@octopus-community/react-native';
try {
await connectUser({ userId: user.id });
} catch (error) {
if (isConnectUserError(error) && error.code === 'USER_BANNED') {
showMessage(error.message);
}
}
onError receives an OctopusClientUserError. Read its Code and Message:
Code | Meaning |
|---|---|
UserBanned | The member is banned. Message is displayable. |
MissingToken, InvalidToken | The token provider returned no token, or the token was refused. |
CommunityAccessDenied | The member has no access to this community. |
ProfileError | A profile field was refused. |
Other | Any other failure, including network and token provider failures. |
onError: error =>
{
if (error.Code == OctopusClientUserErrorCode.UserBanned) ShowMessage(error.Message);
}
4. Disconnect your user
Call disconnectUser when your user signs out of your app. The member goes back to browsing as a guest.
- Android
- iOS
- Flutter
- React Native
- Unity
disconnectUser is a suspending function. isUserConnected is a Flow<Boolean> you can collect to update your own UI:
OctopusSDK.disconnectUser()
OctopusSDK.isUserConnected.collect { isConnected ->
// Update your UI
}
You can collect the OctopusSDK flows, such as isUserConnected, connectionState or profile, before initialize runs ≥ 1.13.2: they stay silent until the SDK is initialized, then emit the real state.
The async form throws when the server cannot be reached. The local session is cleared either way. A non-async disconnectUser() also exists.
do {
try await octopus.disconnectUser()
} catch {
// Network or server failure: the local session is cleared anyway
}
await OctopusSDK().disconnectUser();
import { disconnectUser } from '@octopus-community/react-native';
await disconnectUser();
await OctopusSDK.DisconnectUser();
5. Read connected-user entitlements ≥ 1.12.0
The SDK exposes the entitlements of the connected user through their profile. Entitlements are opaque identifiers that your backend puts in the SSO JWT. The SDK does not compare them with group requirements: the server decides group access and the SDK reports it in OctopusGroup.canAccess (see Gate access to locked groups). Use entitlements only for your own UI, such as badges. The profile is empty until a user is connected.
- Android
- iOS
- Flutter
- React Native
- Unity
OctopusSDK.profile.collect { profile ->
val entitlements: Set<String> = profile?.entitlements ?: emptySet()
// Update your UI
}
octopus.$profile
.sink { profile in
let entitlements: Set<String> = profile?.entitlements ?? []
// Update your UI
}
OctopusSDK.profile.listen((profile) {
final Set<String> entitlements = profile?.entitlements ?? <String>{};
// Update your UI
});
A listener added after the SDK published a profile receives the current one immediately. getProfile() reads it once.
import { addProfileListener } from '@octopus-community/react-native';
const subscription = addProfileListener((profile) => {
const entitlements: string[] = profile?.entitlements ?? [];
// Update your UI
});
// Later: subscription.remove();
CurrentProfile holds the latest profile and OnProfileChanged reports updates on the Unity main thread. Entitlements is an unordered set. ClientUserId is your user id, or null for guests.
System.Action<OctopusProfile> onProfileChanged = profile =>
{
if (profile == null) return;
foreach (string entitlement in profile.Entitlements)
{
// Update your UI
}
};
OctopusSDK.OnProfileChanged += onProfileChanged;
onProfileChanged(OctopusSDK.CurrentProfile);
// Later: OctopusSDK.OnProfileChanged -= onProfileChanged;
6. Refresh entitlements ≥ 1.12.0
When a user's entitlements change in your backend, for example after a purchase, call refreshEntitlements(). The SDK calls your token provider for a fresh JWT, updates the profile, and refetches the groups so that open screens show the new access.
Refreshing works only for a connected member who is not a guest and was connected with a token provider.
- Android
- iOS
- Flutter
- React Native
- Unity
when (val result = OctopusSDK.refreshEntitlements()) {
is OctopusResult.Success -> { /* OctopusSDK.profile and OctopusSDK.groups emit again */ }
is OctopusResult.Failure.InvalidArguments -> when (val error = result.error) {
is RefreshEntitlementsError.NoClientTokenProvider -> {} // Not SSO, or no token provider
is RefreshEntitlementsError.UserNotConnected -> {} // No connected user
is RefreshEntitlementsError.NoNetwork -> {} // Device offline
is RefreshEntitlementsError.UserBanned -> showMessage(error.errorMessage)
is RefreshEntitlementsError.ServerError -> {} // Server failure
}
else -> { /* Other transport failures */ }
}
refreshEntitlements() throws an OctopusRefreshEntitlementsError:
do {
try await octopus.refreshEntitlements()
// octopus.$profile and octopus.$groups emit again
} catch {
switch error {
case .noClientTokenProvider: break // Not SSO, or no token provider
case .userNotConnected: break // No connected user
case .noNetwork: break // Device offline
case .userBanned(let message): showMessage(message)
case .serverError: break // Server failure
}
}
The member must be connected with connectUser(tokenProvider:): a static token cannot be renewed.
final result = await OctopusSDK().refreshEntitlements();
switch (result) {
case OctopusSuccess():
break; // OctopusSDK.profile and OctopusSDK.groups emit again
case OctopusInvalidArguments<OctopusServerError>(:final errors):
// RefreshEntitlementsNoClientTokenProviderError, RefreshEntitlementsUserNotConnectedError,
// RefreshEntitlementsNoNetworkError, RefreshEntitlementsUserBannedError (errorMessage
// is displayable), RefreshEntitlementsServerError
for (final error in errors.whereType<RefreshEntitlementsError>()) {
// Handle error
}
case OctopusConnectionFailure():
break; // Network or server failure
}
refreshEntitlements rejects with an error you can check with isRefreshEntitlementsError:
import { isRefreshEntitlementsError, refreshEntitlements } from '@octopus-community/react-native';
try {
await refreshEntitlements();
// addProfileListener and addGroupsListener emit again
} catch (error) {
if (isRefreshEntitlementsError(error)) {
switch (error.code) {
case 'NO_CLIENT_TOKEN_PROVIDER': break; // Not SSO, or no token provider
case 'USER_NOT_CONNECTED': break; // No connected user
case 'NO_NETWORK': break; // Device offline
case 'USER_BANNED': showMessage(error.message); break;
case 'SERVER_ERROR': break; // Server failure
case 'NOT_INITIALIZED': break; // initialize has not run
default: break; // REFRESH_ENTITLEMENTS_ERROR: any other failure
}
}
}
Call RefreshEntitlements on the Unity main thread. onError receives an OctopusRefreshEntitlementsError with a Kind and a Message. The callbacks run on the main thread, and profile updates arrive through OnProfileChanged.
OctopusSDK.RefreshEntitlements(
onSuccess: () => { /* Entitlements refreshed */ },
onError: error =>
{
switch (error.Kind)
{
case OctopusRefreshEntitlementsErrorKind.NoClientTokenProvider: break;
case OctopusRefreshEntitlementsErrorKind.UserNotConnected: break;
case OctopusRefreshEntitlementsErrorKind.NoNetwork: break;
case OctopusRefreshEntitlementsErrorKind.UserBanned: ShowMessage(error.Message); break;
case OctopusRefreshEntitlementsErrorKind.ServerError: break;
}
});
7. Detect a guest session ≥ 1.12.6
Tell a guest apart from a signed-in member. When the community requires sign-in for actions, the SDK opens a guest session again right after disconnectUser(), so a connection state alone does not prove that a member is signed in. Check the guest flag before you enable features for signed-in members.
- Android
- iOS
- Flutter
- React Native
- Unity
import com.octopuscommunity.sdk.domain.repository.ConnectionRepository.ConnectionState
OctopusSDK.connectionState.collect { state ->
val isGuest = (state as? ConnectionState.Connected)?.isGuest ?: false
// Enable member features when !isGuest
}
octopus.$profile
.sink { profile in
let isGuest = profile?.isGuest ?? true
// Enable member features when !isGuest
}
OctopusSDK.connectionState.listen((state) {
final isGuest = state is OctopusConnected ? state.isGuest : false;
// Enable member features when !isGuest
});
isUserConnected() and addIsUserConnectedListener() already report only signed-in members who are not guests.
import { addConnectionStateListener } from '@octopus-community/react-native';
const subscription = addConnectionStateListener((state) => {
const isGuest = state.connected ? state.isGuest : false;
// Enable member features when !isGuest
});
// Later: subscription.remove();
Read IsGuest on the profile from OnProfileChanged or CurrentProfile. A null profile means that no profile is available yet.
OctopusSDK.OnProfileChanged += profile =>
{
bool isGuest = profile == null || profile.IsGuest;
// Enable member features when !isGuest
};
8. Handle guest connection failures ≥ 1.14.0
A banned member cannot be connected: connectUser is refused with the ban message (see Handle a refused connection), and the member keeps browsing as a guest. From then on, when they try to post, comment, reply or open their own profile, the SDK shows the ban message from the server instead of your sign-in prompt, in an alert with an OK button only. Reactions, votes, reporting and blocking stay available. Your app has nothing to add beyond handling the connectUser result.
The SDK keeps the refusal in memory. After a cold start, it learns about the ban again at the next connectUser, so keep calling connectUser at each start as usual.
- Android
- iOS
- Flutter
- React Native
- Unity
≥ 1.14.2 The ban message appears before posting, commenting, replying or opening the member's own profile, whatever the community's sign-in settings.
connectUser returns the ban as ClientUserError.UserBanned inside OctopusResult.Failure.InvalidArguments, with a displayable errorMessage. If your app checks the connection itself with OctopusSDK.connectionRepository.checkConnection(), the same ban comes back as GuestError.UserBanned. Do not call connectionRepository.connectAsGuest() to check a connection: on success it replaces the stored session, including a connected member's, with a guest session.
The async connectUser throws OctopusConnectUserError.userBanned(message), where message is displayable. The SDK then shows the same message before the member's actions.
connectUser returns a ClientUserBannedError with a displayable errorMessage. For now, only iOS apps show the ban message before actions; Android apps get it in a later Flutter release.
connectUser rejects with the code USER_BANNED and a displayable message. For now, only iOS apps show the ban message before actions; Android apps get it in a later React Native release.
onError receives an OctopusClientUserError with the code UserBanned and a displayable Message. For now, only iOS builds show the ban message before actions; Android builds get it in a later Unity release.
Behavior and limits
- A token refused by the server leaves the member browsing as a guest. Check the
connectUserresult rather than assuming success. - App-managed fields are copied at each
connectUser. The other fields are copied only until the member edits their community profile. - The back office can require explicit terms acceptance, lock profile fields or hide some content options: see Community settings.
Next steps
- Generate a signed JWT: the backend side of the token provider.
- Unified Profile: open member profiles and react to profile taps.
- Groups: gate groups on entitlements.
- Community settings: settings that change what members can edit.