Skip to main content

Groups

List the community groups, keep the user's follow state in sync with your app, and send taps on locked groups to your own upsell screen.

Before you begin​

  • Complete the Quickstart: the SDK is initialized and the community UI opens.
  • Create the groups in the back office. Mark the groups that require an entitlement as locked there: the backend decides access, not the app.
  • Following and unfollowing act on the connected user. Guests are provisioned automatically, so the calls work before a user signs in.

How it works​

A group is a content category that users follow. For the connected user, each group carries:

FieldMeaning
idStable group id. Use it to open the group or to change its follow state.
nameDisplay name, in the user's language.
isFollowedtrue when the user follows the group.
canChangeFollowStatusfalse for a group the user cannot follow or unfollow (for example an essential group).
canAccessfalse for a locked group: it is listed, but the user cannot open it.
canCreateChildrenfalse when the user cannot post in the group.

The SDK keeps the group list as observable state and refreshes it after every follow change. Your app can change the follow state in two ways: one group at a time, or a batch of actions that the backend reconciles by date. Access to a locked group is never enforced by the app: the SDK calls your callback and lets you decide what to show.


1. List available groups and read their state​

Observe the group list to render it in your app, and fetch it to force a refresh from the server.

  • OctopusSDK.groups: Flow<List<OctopusGroup>> — the cached group list, updated on every change.
  • OctopusSDK.fetchGroups() — a suspend function that returns OctopusResult<List<OctopusGroup>, Nothing>.
// Observe the cached groups
OctopusSDK.groups.collect { groups ->
// groups: List<OctopusGroup>
// each has: id, name, isFollowed, canChangeFollowStatus, canAccess, canCreateChildren
}

// Fetch the latest groups from the server
when (val result = OctopusSDK.fetchGroups()) {
is OctopusResult.Success -> {
val groups = result.data // List<OctopusGroup>
}
is OctopusResult.Failure -> {
// No network, user not authenticated, server error…
}
}
tip

Render a group whose canAccess is false as locked, and send its taps to your access-denied callback — see Gate access to locked groups.


2. Follow or unfollow one group​

Android availableiOS not availableFlutter availableReact Native ≥ 1.13.0Unity ≥ 1.13.0

Follow or unfollow a single group when the user acts on it in your own screens. The group list updates when the call succeeds. To send several changes at once, or changes observed earlier, use Sync follow state in batch instead.

  • groupId: String — the Octopus group id.
  • Returns OctopusResult<Unit, GroupFollowUnfollowError>. The errors are MissingGroup, UnfollowableGroup, GroupAlreadyFollowed, GroupAlreadyUnfollowed, LastFollowedGroup and Unknown, each with an errorMessage.
when (val result = OctopusSDK.followGroup(groupId = "GROUP_ID")) {
is OctopusResult.Success -> {
// The user now follows the group
}
is OctopusResult.Failure -> {
// Connection failure, or InvalidArguments carrying a GroupFollowUnfollowError
}
}

// To unfollow:
// OctopusSDK.unfollowGroup(groupId = "GROUP_ID")
info

unfollowGroup refuses to unfollow the user's last followed group and returns LastFollowedGroup.


3. Sync follow state in batch​

Push a batch of follow and unfollow actions for the connected user, each with a timestamp. The backend compares each action with the last recorded action on that group, whether it came from an earlier sync or from the user in the community UI. The action with the most recent actionDate wins; an action whose actionDate is older than or equal to the stored one is skipped. Essential groups always stay followed: an unfollow on them returns notUnfollowable.

Each action carries:

  • groupId — the id of the group to follow or unfollow.
  • followed — true to follow, false to unfollow.
  • actionDate — when your app observed the action. The backend compares it with the last recorded action for this user and group.

The call returns one result per action, with the groupId and a status: applied, skipped, alreadyFollowed, alreadyUnfollowed, groupNotFound, notFollowable, notUnfollowable or unknownError. Match results to actions by groupId, not by position. An empty list completes at once with no network call.

  • actions: List<SyncFollowGroupAction> — SyncFollowGroupAction(groupId, followed, actionDate: Date).
  • Returns OctopusResult<List<SyncFollowGroupResult>, Nothing>.
val actions = listOf(
SyncFollowGroupAction(groupId = "g1", followed = true, actionDate = Date()),
SyncFollowGroupAction(groupId = "g2", followed = false, actionDate = Date()),
)

when (val result = OctopusSDK.syncFollowGroups(actions = actions)) {
is OctopusResult.Success -> {
for (r in result.data) {
// r.groupId — the group this result refers to
when (r.status) {
is SyncFollowGroupStatus.Applied -> {} // action was applied
is SyncFollowGroupStatus.Skipped -> {} // user acted more recently — client action ignored
is SyncFollowGroupStatus.AlreadyFollowed -> {} // user already follows — no change
is SyncFollowGroupStatus.AlreadyUnfollowed -> {} // user already does not follow — no change
is SyncFollowGroupStatus.GroupNotFound -> {} // no group with that id
is SyncFollowGroupStatus.NotFollowable -> {} // group is admin-restricted
is SyncFollowGroupStatus.NotUnfollowable -> {} // essential group
is SyncFollowGroupStatus.UnknownError -> {} // unclassified server error for this action
}
}
}
is OctopusResult.Failure -> {
// Transport-level failure: no network, user not authenticated, server error…
}
}

How actionDate resolves conflicts​

Starting from a group g1 with no recorded state, the action with the most recent actionDate wins, whatever the order in which the backend receives the actions:

#SourceActionactionDateResultFollow state after
1client syncfollowJan 10Appliedfollowed (Jan 10)
2user, in the appunfollowJan 12 (now)Appliednot followed (Jan 12)
3client syncfollowJan 11Skippednot followed (Jan 12)

A manual action in the community UI carries the current time as its actionDate, so it wins over older client syncs. Action #3 is skipped because its actionDate (Jan 11) is older than the last recorded one (Jan 12). Only actionDate matters, not the order of arrival.

actionDate is not a scheduler​

A future actionDate is applied at once, and it becomes the baseline that later actions must beat. It does not defer the action to that date:

#SourceActionactionDateResultEffect
1client syncfollowMar 31 (future)Applieduser follows g2 now; baseline set to Mar 31
2user, in the appunfollowJan 12 (now)SkippedJan 12 ≤ Mar 31 → the user cannot unfollow until Mar 31
warning

Do not use a future actionDate to schedule a change. The action takes effect at once and then locks the group against any earlier-dated action, including the user's own, until that date. To apply a change later, keep the timing in your app and send the action with the current time when it should take effect.


4. Gate access to locked groups​

A group can require an entitlement before it opens (for example a premium group). A locked group stays in the list and in the community UI, but canAccess is false and tapping it does not open it: the SDK calls your callback with the group id, so your app can show an upsell screen or a paywall. The backend resolves access; the SDK never enforces it on the device and never navigates for you.

The callback fires when the user:

  • taps the group in the group list,
  • taps the follow button of a locked group,
  • picks a locked group in the create-post group picker,
  • acts on a group screen after access was revoked during the session (for example when an entitlement expired).

Read canAccess on a group​

Render locked groups differently in your own screens, from the server-resolved fields. Do not compute access from the user's entitlements in the app.

OctopusSDK.groups.collect { groups ->
groups.forEach { group ->
if (group.canAccess) {
// Render normally
} else {
// Render as locked (for example with a lock icon). Taps go through the callback below.
}
}
}

Handle the locked-group callback​

Register one callback. It receives the id of the locked group the user tried to open.

  • OctopusSDK.setGroupAccessDeniedCallback(callback: (groupId: String) -> Unit) — applies to every Octopus screen.
  • onGroupAccessDenied on octopusComposables(...) — applies to the navigation graph you register, and takes precedence when both are set.
// SDK-level callback
OctopusSDK.setGroupAccessDeniedCallback { groupId ->
// Open your upsell flow, paywall, etc.
}

// Or per navigation graph
octopusComposables(
navController = navController,
onGroupAccessDenied = { groupId ->
// Open your upsell flow, paywall, etc.
},
// … other callbacks
)
info

Without a callback, taps on a locked group do nothing in the community UI. Register one so the user gets a next step.

Behavior and limits​

  • The group list refreshes after every follow change, whether it comes from your app or from the user in the community UI.
  • actionDate is a precedence timestamp, not a schedule: every applied action takes effect at once.
  • Essential groups cannot be unfollowed. canChangeFollowStatus is false for them.
  • Access to a locked group is decided by the backend. Changing an entitlement in your app does not unlock a group until the backend resolves it.
  • The SDK never navigates when access is denied: your callback decides what the user sees.

Next steps​