Bridges
Link a page or an object of your app (an article, a product, a recipe…) to a community post that Octopus creates for it, so your users can discuss it and jump back to it from the community.
Android availableiOS availableFlutter ≥ 1.12.0React Native ≥ 1.13.0Unity ≥ 1.13.0
Before you begin
- Complete the Quickstart: the SDK is initialized and the community UI opens.
- Give each object you want to link a stable, unique id. The SDK uses it to find the post again, and hands it back to you when a user taps the post's button.
- If your community requires signed bridge posts, add a signing route to your backend: see Generate fingerprint for bridge and Generate token for bridge.
- To label the post with a group, get the group id from the group list.
How it works
A bridge is a post that belongs to one of your objects:
- Your app describes the object: its id, the text of the post, and optionally an image, a catchphrase, a button label and a group.
- The SDK fetches the post linked to this id. The first time, it creates the post from your description and asks your token provider for a signature. Later calls return the existing post unchanged: the description is only used at creation.
- The SDK returns the post, with its Octopus post id. Open the Octopus UI on this post id to show it.
- When a user taps the post's button in the community, the SDK calls your callback with the object id, and your app opens the matching content.
- Optionally, your app observes the post to show its reaction, comment and view counts in its own UI, and sets the user's reaction from there.
1. Prepare the post content
Describe the object and the post to create for it. Only the object id and the text are required.
| Field | Required | Constraint |
|---|---|---|
| Object id | Yes | Stable and unique for your object. |
| Text | Yes | Between 10 and 5000 characters. |
| Catchphrase | No | Shown in bold below the text, for example "What do you think about this?". Fewer than 84 characters; 6 to 38 recommended. |
| Button text | No | Label of the button that opens your object, for example "Read the article" or "Buy it". Fewer than 28 characters; 4 to 28 recommended. Without it, or without the callback of step 3, no button is shown. |
| Group id | No | The group the post is labelled with. Without it, the group follows your community settings. |
| Image | No | A local image or a remote URL, within the image constraints below. |
The image must meet these constraints:
| Constraint | Value |
|---|---|
| File size | Less than 50 MB |
| Format | JPG or PNG |
| Side length | Between 50 px and 4000 px |
| Aspect ratio | At most 32:9 (longer side divided by shorter side) |
| Remote image | A public URL that points at the image file itself |
- Android
- iOS
- Flutter
- React Native
- Unity
ClientPost(objectId, text, attachment, catchPhrase, viewObjectButtonText, groupId)— onlyobjectIdandtextare required.attachment: Resource?—Resource.Remote(url)for a remote image,Resource.Local(uri)for a local one.
val clientPost = ClientPost(
objectId = "recipe-129302938", // A unique identifier for your content
text = "The perfect Canelés", // Between 10 and 5000 chars
attachment = Resource.Remote(url = imageUrl), // or Resource.Local(uri)
groupId = foodRecipeGroupId, // The id of the Octopus group. Null if default.
catchPhrase = "Tried the canelés? Tell us how good they were!", // Less than 84 characters
viewObjectButtonText = "Read the recipe" // Less than 28 characters
)
ClientPost(clientObjectId:groupId:text:catchPhrase:attachment:viewClientObjectButtonText:)—groupIdandcatchPhrasecan be omitted; passnilforattachmentorviewClientObjectButtonTextto leave them out.attachment: Attachment?—.distantImage(url)for a remote image,.localImage(data)for a local one.
let clientPost = ClientPost(
clientObjectId: "recipe-129302938", // A unique identifier for your content
groupId: foodRecipeGroupId, // The id of the Octopus group. Nil if default.
text: "The perfect Canelés", // Between 10 and 5000 chars
catchPhrase: "Tried the canelés? Tell us how good they were!", // Less than 84 characters
attachment: .distantImage(imageUrl), // or .localImage(data)
viewClientObjectButtonText: "Read the recipe" // Less than 28 characters
)
ClientPost(objectId:, text:, attachment:, catchPhrase:, viewObjectButtonText:, groupId:)— onlyobjectIdandtextare required.attachment: OctopusClientPostAttachment?—OctopusRemoteImageAttachment(uri)for a remote image,OctopusLocalImageAttachment(bytes)for image bytes you already hold.
final clientPost = ClientPost(
objectId: "recipe-129302938", // A unique identifier for your content
text: "The perfect Canelés", // Between 10 and 5000 chars
attachment: OctopusRemoteImageAttachment(Uri.parse(imageUrl)), // or OctopusLocalImageAttachment(bytes)
groupId: foodRecipeGroupId, // The id of the Octopus group. Null if default.
catchPhrase: "Tried the canelés? Tell us how good they were!", // Less than 84 characters
viewObjectButtonText: "Read the recipe", // Less than 28 characters
);
ClientPost— an object withobjectIdandtextrequired, andattachment,catchPhrase,viewObjectButtonText,groupIdoptional.attachment: OctopusClientPostAttachment—{ type: 'remoteImage', url }for a remote image,{ type: 'localImage', uri }for afile://URI or the name of an image bundled with the app.
import type { ClientPost } from '@octopus-community/react-native';
const clientPost: ClientPost = {
objectId: 'recipe-129302938', // A unique identifier for your content
text: 'The perfect Canelés', // Between 10 and 5000 chars
attachment: { type: 'remoteImage', url: imageUrl }, // or { type: 'localImage', uri }
groupId: foodRecipeGroupId, // The id of the Octopus group. Omit if default.
catchPhrase: 'Tried the canelés? Tell us how good they were!', // Less than 84 characters
viewObjectButtonText: 'Read the recipe', // Less than 28 characters
};
OctopusClientObject— setObjectIdandText;CatchPhrase,ViewObjectButtonText,GroupIdandSignBridgeShareare optional.ImageUrlfor a remote image (an absolute HTTP(S) URL),ImagePathfor a local file. Set at most one.
var clientObject = new OctopusClientObject
{
ObjectId = "recipe-129302938", // A unique identifier for your content
Text = "The perfect Canelés", // Between 10 and 5000 chars
ImageUrl = imageUrl, // or ImagePath = localImagePath
GroupId = foodRecipeGroupId, // The id of the Octopus group. Null if default.
CatchPhrase = "Tried the canelés? Tell us how good they were!", // Less than 84 characters
ViewObjectButtonText = "Read the recipe" // Less than 28 characters
};
2. Get or create the post
Pass the content to the SDK. It returns the post linked to the object id, and creates it the first time. An existing post is returned as is: changing the content afterwards does not update it.
When the SDK creates a post, it calls your token provider with a bridge fingerprint and waits for a signature. For the best security, ignore this fingerprint: compute it on your backend from your own copy of the content (see Generate fingerprint for bridge). Otherwise, send the fingerprint to your backend. In both cases, your backend signs it (see Generate token for bridge). If your community does not require a signature, return no token.
Avoid repeatedly deleting and recreating a bridge post for the same content id. Recreating a post starts a brand-new bridge: its comments, reactions and engagement counters all reset to zero. On top of that, each content id only supports a limited number of delete-and-recreate cycles; past that limit, creating a new post for that content will fail. Treat deletion as occasional, not as part of a regular publishing loop.
- Android
- iOS
- Flutter
- React Native
- Unity
clientPost: ClientPost— the content from step 1.tokenProvider: (suspend (String) -> String?)?— receives the bridge fingerprint and returns the signature, ornullwhen your community does not require one.- Returns
OctopusResult<OctopusPost, ClientPostError>; the function issuspend.
val result = OctopusSDK.fetchOrCreateClientObjectRelatedPost(
clientPost = clientPost,
tokenProvider = { bridgeFingerprint ->
// `server` represents your own backend client — replace with your actual implementation.
// Return null if your community does not require a signature.
server.getBridgeSignature(bridgeFingerprint)
}
)
when (result) {
is OctopusResult.Success -> {
val postId = result.data.id // Octopus post id, used in steps 4 and 6
}
is OctopusResult.Failure -> {
// Invalid content (ClientPostError), no network, user not authenticated…
}
}
content: ClientPost— the content from step 1.tokenProvider: (String) async throws -> String?— receives the bridge fingerprint and returns the signature, ornilwhen your community does not require one.- Returns
any OctopusPost; the function isasync throws(ClientPostError).
do {
let post = try await octopus.fetchOrCreateClientObjectRelatedPost(
content: clientPost,
tokenProvider: { bridgeFingerprint in
// `server` represents your own backend client — replace with your actual implementation.
// Return nil if your community does not require a signature.
return try await server.getBridgeSignature(bridgeFingerprint: bridgeFingerprint)
}
)
let postId = post.id // Octopus post id, used in steps 4 and 6
} catch {
// error: ClientPostError — invalid content, no network, user not authenticated…
}
clientPost: ClientPost— the content from step 1.tokenProvider: Future<String?> Function(String)?— receives the bridge fingerprint and returns the signature, ornull. Omit it when your community does not require a signature.- Returns
Future<OctopusResult<OctopusPost, ClientPostError>>.
final octopus = OctopusSDK();
final result = await octopus.fetchOrCreateClientObjectRelatedPost(
clientPost,
tokenProvider: (bridgeFingerprint) async {
// `server` represents your own backend client — replace with your actual implementation.
// Return null if your community does not require a signature.
return server.getBridgeSignature(bridgeFingerprint);
},
);
switch (result) {
case OctopusSuccess(:final data):
final postId = data.id; // Octopus post id, used in steps 4 and 6
case OctopusInvalidArguments<OctopusServerError>(:final errors):
// Invalid content. Naming `<OctopusServerError>` keeps the switch exhaustive.
for (final error in errors.whereType<ClientPostError>()) {
debugPrint(error.errorMessage);
}
case OctopusConnectionFailure():
// No network, user not authenticated…
}
clientPost: ClientPost— the content from step 1.- Returns
Promise<OctopusPost>; a failure rejects with an error you can test withisClientPostError. - The token provider is not an argument: register it once with
useBridgeShareTokenProvider(a hook) oraddBridgeShareTokenRequestListener(a plain function). It receives the bridge fingerprint and returns the signature, ornull.
import {
fetchOrCreateClientObjectRelatedPost,
isClientPostError,
useBridgeShareTokenProvider,
} from '@octopus-community/react-native';
// Register once, in a component mounted for the app's lifetime.
useBridgeShareTokenProvider(async (bridgeFingerprint) => {
// `server` represents your own backend client — replace with your actual implementation.
// Return null if your community does not require a signature.
return server.getBridgeSignature(bridgeFingerprint);
});
try {
const post = await fetchOrCreateClientObjectRelatedPost(clientPost);
const postId = post.id; // Octopus post id, used in steps 4 and 6
} catch (error) {
if (isClientPostError(error)) {
console.warn(`${error.code}: ${error.message}`); // Invalid content, no network…
}
}
The same registered provider also signs the images of the create-post editor, described in Sign a Bridge Share image: register it once and it serves both.
clientObject: OctopusClientObject— the content from step 1. Set itsSignBridgeShareto provide the signature: it receives the bridge fingerprint and returns the token. Leave it null when your community does not require one.onResult: Action<string>— receives the Octopus post id.onError: Action<OctopusClientPostError>— inspectCodeandMessage.
clientObject.SignBridgeShare = async bridgeFingerprint =>
{
// `Http` is a System.Net.Http.HttpClient; replace the URL with your backend signing route.
return await Http.GetStringAsync(
"https://your-backend.example/generateBridgeSignature?fingerprint=" +
System.Uri.EscapeDataString(bridgeFingerprint)).ConfigureAwait(false);
};
OctopusSDK.FetchOrCreateClientObjectRelatedPost(clientObject,
onResult: postId => { /* Octopus post id, used in steps 4 and 6 */ },
onError: error => { /* Invalid content, no network… see error.Code and error.Message */ });
SignBridgeShare runs off the Unity player loop: use loop-independent I/O such as System.Net.Http, not UnityWebRequest. Throwing or returning an empty token fails the request. onResult and onError run on the Unity main thread.
3. Handle taps on the content button
Register a callback to open your object when a user taps the post's button in the community. The SDK passes the object id you gave in step 1. Register it before opening the Octopus UI: without a callback, the button is not shown.
- Android
- iOS
- Flutter
- React Native
- Unity
onNavigateToClientObject: (String) -> Unit— a parameter ofoctopusComposables, called with the object id.
// In your NavHost setup
octopusComposables(
// ...
onNavigateToClientObject = { objectId ->
// Display the content that has the given objectId
}
)
set(displayClientObjectCallback:)— called with the object id.
octopus.set(displayClientObjectCallback: { objectId in
// Display the content that has the given objectId
})
OctopusSDK.setNavigateToClientObjectCallback(callback)— called with the object id. Returns a function that unregisters the callback. Registering again replaces the previous callback.
// Register early, for example in initState
final cancel = OctopusSDK.setNavigateToClientObjectCallback((objectId) {
// Display the content that has the given objectId
});
// Later, in dispose
cancel();
setNavigateToClientObjectCallback(callback)— called with the object id. Returns a function that unregisters the callback; it only removes its own registration.
import { setNavigateToClientObjectCallback } from '@octopus-community/react-native';
// Register once, at app start
useEffect(() => {
return setNavigateToClientObjectCallback((objectId) => {
// Display the content that has the given objectId
});
}, []);
Keep the callback registered while the Octopus UI is on screen. On Android, a late registration leaves the button hidden until the UI is reopened. On iOS, the button stays visible after you unregister, and its taps do nothing.
OctopusSDK.OnNavigateToClientObject: Action<string>— a static event raised with the object id.
public class ClientObjectHandler : MonoBehaviour
{
void Start() => OctopusSDK.OnNavigateToClientObject += OnNavigateToClientObject;
void OnDestroy() => OctopusSDK.OnNavigateToClientObject -= OnNavigateToClientObject;
void OnNavigateToClientObject(string objectId)
{
// Display the content that has the given objectId
}
}
4. Display the post
Open the Octopus UI on the post id returned in step 2, for example when the user taps a "Discuss" button on your object page. See Open a specific screen for the other entry points of each platform.
- Android
- iOS
- Flutter
- React Native
- Unity
OctopusPostDetailsContent(navController, postId, …)— the post detail screen as a composable.
OctopusPostDetailsContent(
navController = navController,
postId = postId,
modifier = Modifier.fillMaxSize(),
// ...
)
initialScreen: .post(.init(postId:))— opensOctopusHomeScreenon the post.
OctopusHomeScreen(octopus: octopus, initialScreen: .post(.init(postId: postId)))
initialScreen: OctopusInitialScreen.post(PostScreenInfo(postId:))— opensOctopusHomeScreenon the post. TheOctopusPostDetailsScreen(postId:)widget shows the post alone.
OctopusHomeScreen(
initialScreen: OctopusInitialScreen.post(PostScreenInfo(postId: postId)),
)
openUI({ initialScreen: { type: 'post', postId } })— opens the Octopus UI on the post.
import { openUI } from '@octopus-community/react-native';
await openUI({ initialScreen: { type: 'post', postId } });
OctopusSDK.OpenPost(postId, navigationMode)— opens the Octopus UI on the post.navigationModeis optional; it picks the iOS navigation container and is ignored on Android. An emptypostIdopens the main feed.
OctopusSDK.OpenPost(postId);
5. Show live post data
Observe the post linked to an object to show its reaction, comment and view counts in your own UI, along with the current user's reaction. The observation emits the latest state of the post, and nothing (or an empty value) until the post exists.
The post carries:
| Field | Meaning |
|---|---|
| Id | The Octopus post id. |
| Reactions | One entry per reaction kind, with its count. |
| Comment count | Number of comments. |
| View count | Number of views. |
| User reaction | The current user's reaction, or empty when the user has not reacted. |
- Android
- iOS
- Flutter
- React Native
- Unity
OctopusSDK.getClientObjectRelatedPostFlow(clientObjectId): Flow<OctopusPost?>— emitsnulluntil the post exists.OctopusPost:id,reactions: List<OctopusReactionCount>(each withreactionKindandcount),commentCount,viewCount,userReactionKind.
OctopusSDK.getClientObjectRelatedPostFlow(clientObjectId = "recipe-129302938")
.filterNotNull()
.collect { post ->
val reactions = post.reactions
val commentCount = post.commentCount
val viewCount = post.viewCount
val userReaction = post.userReactionKind // null if the user has not reacted
}
octopus.getClientObjectRelatedPostPublisher(clientObjectId:)— anAnyPublisher<(any OctopusPost)?, Never>that emitsniluntil the post exists.OctopusPost:id,reactions: [OctopusReactionCount],commentCount,viewCount,userReaction.
octopus.getClientObjectRelatedPostPublisher(clientObjectId: "recipe-129302938")
.compactMap { $0 }
.sink { post in
let reactions = post.reactions
let commentCount = post.commentCount
let viewCount = post.viewCount
let userReaction = post.userReaction // nil if the user has not reacted
}
.store(in: &cancellables)
OctopusSDK.getClientObjectRelatedPostFlow(clientObjectId): Stream<OctopusPost?>— emitsnulluntil the post exists. Each subscription runs its own observation; cancel it to stop.OctopusPost:id,reactions: List<OctopusReactionCount>(each withreactionKindandcount),commentCount,viewCount,userReactionKind.
final subscription = OctopusSDK.getClientObjectRelatedPostFlow("recipe-129302938")
.where((post) => post != null)
.listen((post) {
final reactions = post!.reactions;
final commentCount = formatOctopusCompactCount(post.commentCount);
final viewCount = formatOctopusCompactCount(post.viewCount);
final userReaction = post.userReactionKind; // null if the user has not reacted
});
formatOctopusCompactCount(count, {locale}) renders a count in the same compact K / M / B style as the community UI.
addClientObjectRelatedPostListener(clientObjectId, callback)— callscallbackwith the post, ornulluntil the post exists. Returns a subscription; each call runs its own observation, andremove()stops only that one.OctopusPost:id,reactions(each withreactionKindandcount),commentCount,viewCount,userReactionKind.
import { addClientObjectRelatedPostListener, formatOctopusCompactCount } from '@octopus-community/react-native';
useEffect(() => {
const subscription = addClientObjectRelatedPostListener('recipe-129302938', (post) => {
if (!post) return; // No post created yet for this object
const reactions = post.reactions;
const commentCount = formatOctopusCompactCount(post.commentCount);
const viewCount = formatOctopusCompactCount(post.viewCount);
const userReaction = post.userReactionKind; // null if the user has not reacted
});
return () => subscription.remove();
}, []);
formatOctopusCompactCount(count, { locale }) renders a count in the same compact K / M / B style as the community UI. Pass the locale you gave to overrideDefaultLocale so both match.
OctopusSDK.StartObservingClientObjectRelatedPost(objectId)/StopObservingClientObjectRelatedPost(objectId)— one observation per object id; a repeated start does nothing.OctopusSDK.OnClientObjectRelatedPostChanged: Action<string, OctopusPost>— raised on the Unity main thread with the object id and its post, ornulluntil the post exists. Subscribe before you start the observation.OctopusPost:Id,Reactions(each withReactionKindandCount),CommentCount,ViewCount,UserReactionKind.
System.Action<string, OctopusPost> onPost = (objectId, post) =>
{
if (objectId != "recipe-129302938" || post == null) return;
var reactions = post.Reactions;
string commentCount = OctopusSDK.FormatOctopusCompactCount(post.CommentCount);
string viewCount = OctopusSDK.FormatOctopusCompactCount(post.ViewCount);
OctopusReactionKind? userReaction = post.UserReactionKind; // null if the user has not reacted
};
OctopusSDK.OnClientObjectRelatedPostChanged += onPost;
OctopusSDK.StartObservingClientObjectRelatedPost("recipe-129302938");
// Later, to stop observing
OctopusSDK.StopObservingClientObjectRelatedPost("recipe-129302938");
OctopusSDK.OnClientObjectRelatedPostChanged -= onPost;
OctopusSDK.FormatOctopusCompactCount(count) renders a count in the compact K / M / B style, with a dot as decimal separator. Initialization, a community switch, reset and stop cancel the observations: start them again afterwards.
6. Set a reaction from your UI
Android ≥ 1.12.0iOS ≥ 1.12.0Flutter ≥ 1.12.0React Native ≥ 1.13.0Unity ≥ 1.13.0
Let users react to the post without opening the community. Pass a reaction kind to set the user's reaction, or an empty value to remove it. Setting the same reaction twice has no further effect.
The call takes the Octopus post id returned in step 2 or read from the observed post, not your object id. It works on any post, bridge or not.
The reaction kinds are heart ❤️, joy 😂, mouthOpen 😮, clap 👏, cry 😢 and rage 😡.
- Android
- iOS
- Flutter
- React Native
- Unity
reaction: OctopusReactionKind?—OctopusReactionKind.Heart,Joy,MouthOpen,Clap,CryorRage;nullremoves the reaction.postId: String— the Octopus post id.- Returns
OctopusResult<Unit, SetReactionError>; the function issuspend.
// Set a reaction; pass reaction = null to remove it
when (val result = OctopusSDK.setReaction(reaction = OctopusReactionKind.Heart, postId = postId)) {
is OctopusResult.Success -> {
// Reaction updated
}
is OctopusResult.Failure -> {
// SetReactionError (UnknownReaction, PostNotFound, ReactionError), no network…
}
}
Removing a reaction when the user has none succeeds without doing anything.
reaction: OctopusReactionKind?—.heart,.joy,.mouthOpen,.clap,.cryor.rage;nilremoves the reaction.postId: String— the Octopus post id.- The function is
async throws(OctopusSetReactionError).
// Set a reaction; pass reaction: nil to remove it
do {
try await octopus.set(reaction: .heart, postId: postId)
} catch {
// error: OctopusSetReactionError (.postNotFound, .notConnected, .noNetwork…)
}
Removing a reaction when the user has none throws .other.
reaction: OctopusReactionKind?—OctopusReactionKind.heart,joy,mouthOpen,clap,cryorrage;nullremoves the reaction.postId: String— the Octopus post id.- Returns
Future<OctopusResult<void, SetReactionError>>.
// Set a reaction; pass null to remove it
final result = await octopus.setReaction(OctopusReactionKind.heart, postId);
result
..onSuccess((_) {
// Reaction updated
})
..onError((error) {
// SetReactionError: SetReactionPostNotFoundError, SetReactionReactionError…
debugPrint(error.errorMessage);
});
SetReactionError is sealed: SetReactionUnknownReactionError, SetReactionPostNotFoundError or SetReactionReactionError.
postId: string— the Octopus post id. It comes first.reaction: OctopusReactionKind | null—'heart','joy','mouthOpen','clap','cry'or'rage';nullremoves the reaction.- Returns
Promise<void>; a failure rejects with an error you can test withisSetReactionError.
import { setReaction, isSetReactionError } from '@octopus-community/react-native';
try {
// Set a reaction; pass null to remove it
await setReaction(postId, 'heart');
} catch (error) {
if (isSetReactionError(error)) {
console.warn(`${error.code}: ${error.message}`); // POST_NOT_FOUND, NO_NETWORK…
}
}
The error code is one of UNKNOWN_REACTION, POST_NOT_FOUND, NO_NETWORK, NOT_CONNECTED, SERVER_ERROR or SET_REACTION_ERROR. Passing null when the user has no reaction does nothing.
contentId: string— the Octopus post id.kind: OctopusReactionKind?—Heart,Joy,MouthOpen,Clap,CryorRage;nullorNoneremoves the reaction.onCompleted: ActionandonError: Action<OctopusSetReactionError>— inspectCode(UnknownReaction,PostNotFound,ReactionError) andMessage.
// Set a reaction; pass null to remove it
OctopusSDK.SetReaction(postId, OctopusReactionKind.Heart,
onCompleted: () => { /* Reaction updated */ },
onError: error => { /* See error.Code and error.Message */ });
On iOS, removing a reaction when the user has none can report ReactionError. The callbacks run on a later Unity update, and wait while the Octopus UI pauses Unity.
Sign a Bridge Share image for a picture-restricted community
Android ≥ 1.12.1iOS ≥ 1.12.3Flutter ≥ 1.12.2React Native ≥ 1.13.0Unity ≥ 1.12.2
This section applies to the create-post editor opened with prefilled content, not to the bridge posts of the steps above. When you open that editor prefilled with an image and the target community forbids member pictures, the SDK needs a signed token that authorizes the image.
Provide a signing callback. The SDK computes a content fingerprint (SHA-256 of the post's text, call to action and image) and calls your callback with it. Your backend returns a compact JWT signed HS256 with your shared secret, whose bridge_fingerprint claim equals the fingerprint. Sign on your backend: never ship the secret in the app. Without a callback, an image in such a community is rejected. Text-only posts, and communities that allow pictures, need no signature.
- Android
- iOS
- Flutter
- React Native
- Unity
CreatePostScreenInfo(prefilledPost, bridgeShareTokenProvider)—bridgeShareTokenProvider: (suspend (String) -> String?)?receives the fingerprint and returns the JWT. Returningnullsends the post unsigned.- Both entry points accept it:
navigateToOctopusCreatePost(info = …)in the community navigation, andOctopusCreatePostScreen(navController, info)as a standalone screen.
val info = CreatePostScreenInfo(
prefilledPost = prefilledPost,
bridgeShareTokenProvider = { bridgeFingerprint ->
// Ask YOUR backend to sign the fingerprint and return the JWT
myBackend.signBridgeShare(bridgeFingerprint)
},
)
// In the community navigation
navController.navigateToOctopusCreatePost(info = info)
// Or as a standalone screen
OctopusCreatePostScreen(navController = navController, info = info)
OctopusPrefilledPost(text:image:topicId:cta:sign:)—sign: (String) async throws -> Stringreceives the fingerprint and returns the JWT. The initializer throws when the content is invalid.- Open the editor with
initialScreen: .createPost(.init(prefilledPost:)).
let prefilledPost = try OctopusPrefilledPost(
text: "The perfect Canelés",
image: imageData,
topicId: foodRecipeGroupId,
sign: { bridgeFingerprint in
// Ask YOUR backend to sign the fingerprint and return the JWT
try await myBackend.signBridgeShare(bridgeFingerprint)
}
)
OctopusHomeScreen(octopus: octopus, initialScreen: .createPost(.init(prefilledPost: prefilledPost)))
The closure has no unsigned outcome: if it throws, the post is not published.
CreatePostScreenInfo(prefilledPost:, bridgeShareTokenProvider:)—bridgeShareTokenProvider: Future<String?> Function(String)?receives the fingerprint and returns the JWT.- Pass it to
showOctopusCreatePostScreen(info:).
await octopus.showOctopusCreatePostScreen(
info: CreatePostScreenInfo(
prefilledPost: prefilledPost,
bridgeShareTokenProvider: (bridgeFingerprint) async {
// Ask YOUR backend to sign the fingerprint and return the JWT
return myBackend.signBridgeShare(bridgeFingerprint);
},
),
);
Returning null sends the post unsigned on Android, where the server refuses it; on iOS the post is not published.
useBridgeShareTokenProvider(provider)(a hook) oraddBridgeShareTokenRequestListener(provider)(a plain function) — registered once, globally.providerreceives the fingerprint and returns the JWT, ornull.- The same registration serves
fetchOrCreateClientObjectRelatedPost(step 2).
import { useBridgeShareTokenProvider } from '@octopus-community/react-native';
useBridgeShareTokenProvider(async (bridgeFingerprint) => {
// Ask YOUR backend to sign the fingerprint and return the JWT
return myBackend.signBridgeShare(bridgeFingerprint);
});
The SDK only asks for a signature while a provider is registered, and treats a request left unanswered for 60 seconds as declined. Declining (returning null or throwing) sends the post unsigned on Android, where the server refuses it; on iOS the post is not published.
OctopusPrefilledPost.SignBridgeShare: Func<string, Task<string>>— receives the fingerprint and returns the JWT. Set it only when the prefilled post carries an image and the community restricts member pictures.- Pass the prefilled post to
OctopusSDK.OpenCreatePost(prefilled). If the callback throws or returns an empty value, the post is aborted with an error.
static readonly System.Net.Http.HttpClient Http = new System.Net.Http.HttpClient();
public void ShareWithSignedImage(string groupId, string imagePath)
{
OctopusSDK.OpenCreatePost(new OctopusPrefilledPost
{
Text = "The perfect Canelés",
TopicId = groupId,
ImagePath = imagePath,
SignBridgeShare = async bridgeFingerprint =>
{
// Ask YOUR backend to sign the fingerprint and return the JWT
return await Http.GetStringAsync(
"https://your-backend.example/sign-bridge-share?fingerprint=" +
System.Uri.EscapeDataString(bridgeFingerprint)).ConfigureAwait(false);
}
});
}
SignBridgeShare runs on a background thread while the game loop is suspended. Use loop-independent I/O such as System.Net.Http, not UnityWebRequest, and do not await continuations that return to the main thread.
See Generate a JWT for a bridge "Share" with an image for the JWT contract your backend implements.
See it in the samples
Each sample app implements the full bridge flow.
- Android
- iOS
- Flutter
- React Native
- Unity
The Octopus Sample app: MainViewModel prepares the client post and gets the post.
The Bridge to Client Object scenario: RecipeViewModel prepares the client post and gets the post id, and BridgeToClientObjectViewModel sets the callback.
The Flutter example app.
A bridge scenario is not yet available in the React Native example app. Follow the steps above.
The UnityExample sample: BridgeScenario.cs prepares the client object and sets the callback.
Behavior and limits
- The content is used only when the post is created. To change a live post, edit it from the back office; do not delete and recreate it.
- The token provider is called only when a post is created, never for an existing post.
- The reaction call takes the Octopus post id, not your object id.
- Validate the content against the constraints of step 1 before you send it: an invalid field fails the request.
Next steps
- Open a specific screen — the other ways to open a post or a group.
- Groups — get the group id to label bridge posts with.
- Generate token for bridge — the backend side of the signature.
- Events reference — track the posts your users create.