octopus_sdk_flutter 1.13.0
octopus_sdk_flutter: ^1.13.0 copied to clipboard
White-label social community SDK for Flutter — embed a moderated community in your app.
1.13.0 #
Dependencies #
- Android Octopus SDK: 1.12.1 → 1.13.2
- iOS Octopus SDK: 1.12.6 → 1.13.2
- New direct dependency:
meta: ^1.9.0, for the@useResultannotation on the result-returning APIs (package:flutter/foundation.dartonly re-exports a subset ofmetaand not that one). Already in every Flutter app's transitive graph, and pinned there: theflutterpackage itself depends onmetaat an exact version (1.17.0on the toolchain this repo builds against), so declaring^1.9.0adds a name topubspec.yamlwithout giving the solver anything new to choose — the SDK's pin still wins.
Both native pins move to the 1.13 line. Most of what 1.13 brings is inherited
with no wrapper change: 24 SDK languages (15 new, including Arabic with
RTL), large-screen content width (content capped and centered on tablets
instead of stretching edge-to-edge), a "View group" entry in the post
menu, the community background color applied on every native screen
(iOS 1.13.2), design-system polish, and — on iOS — Xcode 27 compatibility
(1.13.1) plus a fix for a banned user being logged out under a non-English
locale (1.13.2). Android 1.13.2 additionally fixes in-app browser theming
(links the SDK opens no longer mix a toolbar from one color scheme with content
from the other, and a translucent color slot is dropped instead of rendering as
a see-through toolbar) and text legibility on hosts that darken background
without redefining its content colors — most visibly the profile overflow
menu, whose labels were invisible. It also makes the native public Flows safe to
collect before initialize(), re-binding them on every re-initialization:
inherited plumbing, no wrapper API change.
Breaking #
-
SettingsAboutScreenremoved from theScreenhierarchy. The native SDKs removed the "About the community" screen in 1.13.0 — its three legal links were already duplicated in the Activity and Profile overflow menus — so thesettingsAboutscreen-displayed event no longer exists on either platform and the wrapper can no longer receive it. Source-breaking only for hosts with an exhaustiveswitchoverScreenthat has nodefault/wildcard arm: delete theSettingsAboutScreen()arm. No runtime behavior change — the event was already unreachable once the native screen was gone. See MIGRATING.md. -
connectUsernow returns anOctopusResult— it used to report success on a refused connection (see Fixed below for the defect and the typed errors). Calls keep compiling (voidis a top type); a host class that overrides or implementsconnectUser/connectUserWithTokenProviderwith the oldFuture<void>return type does not. Compiling is not the same as analyzing clean: a bare statement that drops the result now raisesunused_result, see the@useResultentry below. See MIGRATING.md. -
Behaviour change (not source-breaking) — on iOS,
await connectUser(...)now waits for the connection attempt. It could not report a refusal without waiting for one: the iOS bridge used to fire the reply before the native call had done anything, so theFuturecompleted in a few milliseconds no matter what happened next. It now completes when the attempt does. Android already awaited, so this closes an asymmetry rather than opening one — but read this if youawait connectUser(...)on a blocking path (a splash screen, a navigation guard): on iOS thatawaitcan now take as long as the network does, and up to the 60 s token-provider timeout if yourtokenProvidernever answers. Timing is unchanged for a call you never await — but dropping the result as a bare statement now raises anunused_resultwarning, whereunawaited(...)does not, see the@useResultentry below. See MIGRATING.md. -
connectUserandconnectUserWithTokenProviderare@useResult. Ignoring their result raises anunused_resultwarning, andflutter analyze/dart analyze --fatal-warningsexit non-zero on warnings alone — so a host that upgrades without changing a line can see its own CI go red on those two call sites. Nothing breaks at runtime and no behaviour changes; handle the result, or discard it explicitly withfinal _ = await …(Dart 3.7+ for a non-binding_; below that, use// ignore: unused_result— see MIGRATING.md). The annotation is deliberately not extended to the otherOctopusResult-returning methods here: that is a separate, separately-migratable break and belongs in its own change. -
Behaviour change (not source-breaking) —
OctopusPrefilledPostaccepts a content-less payload. Matching the native 1.13 relaxation, a payload carrying only atopicIdand/or actais now accepted and opens the editor on the preselected group with empty, user-editable fields; it previously threwOctopusPrefilledPostContentEmptyError. Read this if you relied on that throw as your only empty-payload check: you now get an empty editor instead of an error, so validate host-side before constructing the payload. The error class is kept and still exported so existingswitchstatements overOctopusPrefilledPostValidationErrorstay exhaustive, but it is never thrown again — the natives kept theirContentEmptycase for the same reason. Whatever content is provided is still validated (text length, CTA label/url). The Android bridge's full-screen create-post entry point had the same pre-1.13 rule baked in and silently dropped atopicId/cta-only prefill; it now only drops a wholly empty one. See MIGRATING.md. -
Behaviour change (not source-breaking) — on Android,
bottomSafeAreaInset: 0resolves the inset from the mount point. On the four embedded widgets,0no longer means "reserve nothing" but "reserve whatever bottom padding the ambientMediaQuerystill has left". That is what keeps the native floating "Write a post" pill clear of the system navigation bar on edge-to-edge Android (API 35+) with no host configuration — the full contract is under### Fixedbelow. Read this if you pass a literal0(or any value ≤ 0) to mean "reserve nothing", or if you mount one of these widgets in a host that pads the layout without consuming the padding — aColumnabove a fixed footer, a plainPadding, aStackbottom overlay: those shapes now reserve the navigation-bar inset a second time inside the native view. Both are addressed by consuming the padding (MediaQuery.removePadding(context: context, removeBottom: true, child: …)) or by passing the total you want. No signature, type or default changed, and iOS is unaffected — it deliberately does not resolve. See MIGRATING.md.
New Features #
-
Two new screen-displayed events from the native Unified Profile work. Both are additive members of the
Screenhierarchy, delivered through the existingOctopusSDK.eventsstream:OtherUserPostsScreen(profileId:)— another member's activity (their posts list), kept distinct from the profile summary reported byOtherUserProfileScreenso host analytics can tell the two apart. Emitted on both platforms.ActivityScreen— the connected user's own activity, emitted instead ofProfileScreenwhen the community runs in Unified Profile mode. Android only; the iOS native SDK has no equivalent screen event.
When they fire. Both come from the native activity screen, but they are not gated the same way, and only one of them needs Unified Profile:
OtherUserPostsScreenfires on the ordinary path — tap another member's avatar or name and the SDK opens that member's activity, with no Unified Profile involved. Verified on device: the event arrives withonNavigateToProfileunwired.ActivityScreenis the connected user's own activity, which is what replacesProfileScreenonce the community runs in Unified Profile mode — so that one does depend on the gate below.
Add both arms to keep your switch exhaustive.
-
Unified Profile: read a member's community data. Enrich your own profile screen with a member's Octopus stats instead of sending the user to the SDK's native profile screen:
OctopusSDK().fetchCommunityData(profileId: …)/fetchCommunityData(clientUserId: …)— a one-shot refreshed snapshot, returningOctopusCommunityData?.OctopusSDK.communityDataFlow(profileId: …)/communityDataFlow(clientUserId: …)— the reactive counterpart; emits the current value then again on every refresh. Each subscription drives its own native observation and is torn down on cancel.- New models
OctopusCommunityData(profileId,messageCount,gamification) andOctopusGamification(level,score). Every field pastprofileIdis nullable —nullmeans "the community does not publish this", not zero.gamificationisnullwhen the community has no gamification configured.scoreis alwaysnullthrough this API, your own profile included: both natives resolve community data from a member's public profile, which never carries the score (back-office consumers only). It exists for forward-compatibility, matching the native models — uselevelto show a member's standing. - Identify the member by exactly one of
profileId(their Octopus id, e.g. fromOtherUserProfileScreen) orclientUserId(your app's own id, which requires the community to expose client user ids). Passing both, or neither, throws anArgumentErrorin every build — thrown synchronously forcommunityDataFlow, when the stream is built. An unknown member yieldsnullrather than an error, so a UI binding never breaks. - Naming note: the native Android SDK calls the Octopus id
userIdand splits the API in two (fetchCommunityData/fetchCommunityDataByClientUserId); this wrapper usesprofileIdeverywhere — matching iOS and the id already reported by the screen events — and takes the id kind as a named parameter instead.
-
OctopusProfile.clientUserId— the connected user's id in your system, as passed toconnectUser. Populated in SSO mode for a non-guest user;nullin Octopus-authentication mode and for a guest. The native SDKs hold it locally, independent of the community's expose-client-user-ids setting — that setting gates other members' client user ids, not the connected user's. Use it to correlate the Octopus profile with your own user record before callingfetchCommunityData. -
Unified Profile: handle profile taps yourself — new optional
onNavigateToProfileon all six public entry points: the four widgets (OctopusHomeScreen,OctopusHomeContent,OctopusPostDetailsScreen,OctopusGroupDetailsScreen) and the two navigation helpers (OctopusSDK.showOctopusHomeScreen,OctopusSDK.openNotification). When you pass it, the SDK routes every profile tap to you — another member's and the connected user's own — with that member'sclientUserId, and stops showing its native profile screens. Pair it withfetchCommunityDatato render your own profile page.Passing the callback is the activation switch, so it is opt-in: leave it null (the default) and nothing changes — existing hosts keep the SDK's profile screens. Activation is an AND gate: wiring it alone is not enough, the community must also be configured to expose client user ids. A member with no client user id (a guest, or a back-office-created profile) opens the Octopus activity screen instead, so you never receive a tap you cannot resolve.
It is mount-time: Android takes it as a parameter of the native composable, so changing it on an already-mounted view has no effect until the view is rebuilt. The iOS SDK exposes a runtime setter instead; the bridge absorbs that asymmetry — it sets the native callback when a view mounts opted in, and clears it when a view mounts opted out — so the Dart-facing contract reads the same on both platforms.
One caveat that is not symmetric, because the iOS setter lives on the shared SDK instance: on iOS it is last-mount-wins. If you keep two embedded views alive at once (one behind a pushed route or a modal) and they disagree about opting in, the most recent mount decides for both — and popping back does not restore the first view's choice, since it does not remount. Keep a single embedded view alive, or opt every one of them in the same way. Android is per-view and unaffected.
This is also what unlocks the
ActivityScreenevent above — the connected user's own activity only replaces the profile screen once Unified Profile is active.OtherUserPostsScreendoes not depend on it and already fires without this callback; see the note on that entry.On iOS this additionally wires the native SDK's separate
onNavigateToProfileEditCallback, which the activity screen uses to gate its "Edit my profile" item — it is routed to your existingonModifyUser, so you still get a single hook. Because the native SDK hides that item when the callback is nil, iOS only offers it to hosts that passedonModifyUser. Known divergence: Android wires its equivalent unconditionally (one native parameter serves both edit paths there), so on Android the item is shown even to a host with noonModifyUser, where tapping it does nothing. Gating it would take more than mirroring the iOS check — the native Android SDK requires that callback in SSO mode with app-managed profile fields — so it is left as-is for now. PassonModifyUseralongsideonNavigateToProfileand both platforms behave identically.Beneath those entry points,
OctopusSDK.embeddedView— the low-level platform view, for hosts that mount it themselves — gains two matching flags:interceptProfileTaps(opt into the native callback) andhasModifyUserHandler(tells iOS whether to offer the activity screen's "Edit my profile" item, per the divergence above). The four widgets derive both from the callbacks you pass them, so you only set them yourself if you build directly onembeddedView.
Still not exposed from the native Unified Profile surface: the standalone
activity/profile screen entry points (Android's navigateToOctopusActivity /
navigateToOctopusProfile, iOS's OctopusInitialScreen.activity and
OctopusProfileScreen). They are additive on top of the callback above and left
to a follow-up.
Fixed #
connectUserno longer reports success when the connection was refused — with one native iOS path that still resolves, described below. Both bridges called the native SDK'sconnectUserand discarded its result — Android threw away the returnedOctopusResult, iOS bound the non-throwing overload that only debug-logs — so a banned user, a JWT the backend rejects, or a missing token resolved exactly like a successful connection. A host had nothing to display and no way to know: the typical symptom was a login screen that appeared to do nothing.connectUserand the deprecatedconnectUserWithTokenProvidernow returnFuture<OctopusResult<void, ClientUserError>>, carrying the refusal.- Call sites keep compiling; overrides do not.
voidis a top type in Dart, so every existing call still compiles —await octopus.connectUser(...)as a statement, an assignment to aFuture<void>,unawaited(...),Future.wait, a tearoff stored in aFuture<void> Function({...})typedef. Compiling is not analyzing clean, though: the first of those, the bare statement, now raises anunused_resultwarning — see the@useResultentry in Breaking. The others assign, pass or return the value, which counts as using it. What breaks is a class that overrides or implementsconnectUser(orconnectUserWithTokenProvider) while still declaring the oldFuture<void>return type — a host wrapper aroundOctopusSDK, or a hand-writtenOctopusSDKPlatformfake in host tests:invalid_override. Widen the override's return type. Mocks that never redeclare the method (mocktail-stylenoSuchMethod) are unaffected. See MIGRATING.md. - The typed leaves are deliberately not symmetric across platforms.
ClientUserMissingTokenErroronly ever comes from Android;ClientUserInvalidTokenErrorandClientUserCommunityAccessDeniedErroronly from iOS;ClientUserBannedError,ClientUserProfileErrorandClientUserOtherErrorfrom both. The per-platform table in theClientUserErrordoc comment is the reference, and it is machine-checked against both bridges byscripts/verify_connect_user_parity.dart— a native bump that adds a variant on one side fails that guard until the table is updated. Switch on these exhaustively, with nodefault/wildcard arm: the hierarchy is sealed, so a catch-all raisesunreachable_switch_default(orunreachable_switch_casefor a_wildcard) andflutter analyzefails on warnings. Narrow witherrors.cast<ClientUserError>()first — the result bindsList<OctopusServerError>, which is not sealed. An unknown wiretypefolds intoClientUserOtherError, and a new leaf only ever arrives by upgrading this package — as a source-breaking change documented here and inMIGRATING.md. - iOS: a refused token does not always surface, and the bridge cannot fix that. When the token exchange fails while nothing is connected yet — the ordinary first login — the native
SSOConnectionRepository.connect()falls back toconnectAsGuest()and returns normally, so this bridge receives no error andconnectUserreturnsOctopusSuccesswhile the user browses anonymously. The refusal does reach you when a connection already existed (reconnecting after a previous failure, for instance), because that path rethrows. The sameconnect()also opens withguard !isConnecting else { return }, so a concurrent guest connection can make the call return without ever requesting a token — a narrow window, since the caller first waits up to 3 s for that connection to end and throws if it does not. Android has no such fallback and reports every refusal. Consequence: on iOS anOctopusSuccessmeans "the SDK is usable", not "your SSO user is authenticated" — confirm that with the connection state, which does tell the two apart. Both are streams that emit independently of theFuture, so subscribe rather than sample:OctopusSDK.isUserConnectedemitsfalseunder the guest fallback, andOctopusSDK.connectionStateemitsOctopusConnected(isGuest: true). What is actionable is the cause — sign a valid token, and answer the provider promptly. - A token request that is never answered no longer parks forever. With the outcome now awaited, a persistent
tokenProviderthat never replies would have left the returnedFuturepending indefinitely. Both bridges bound the wait at 60 s and fall back to an empty token, which Android reports asClientUserMissingTokenError; on iOS the empty token still goes through the backend exchange, so it can come back as a connection-level failure instead — or, on a first connect, not come back as a failure at all (see the guest fallback above). The registeredtokenProvidersurvives a failed connect, whatever the failure: both native SDKs assign their own reference to it before attempting the connection and clear it only on logout, and they re-invoke it on every later refresh (refreshEntitlements()) — so a Dart-side drop would make the next round-trip answer an empty token, permanently. It is released bydisconnectUser().
- Call sites keep compiling; overrides do not.
- The four embedded widgets now reserve the Android bottom inset by default.
OctopusHomeScreen,OctopusHomeContent,OctopusPostDetailsScreenandOctopusGroupDetailsScreen— andOctopusSDK.embeddedViewbeneath them — resolvebottomSafeAreaInsetfrom the ambientMediaQueryon Android when it is left at its0default. Mounted full-screen on edge-to-edge Android (API 35+), the SDK's floating "Write a post" button previously sat behind the system navigation bar unless the host computed and passed the inset itself; only theshowOctopusHomeScreen/openNotificationhelpers did that (added in 1.12.3). A full-screen mount now clears the navigation bar on all six entry points, with no host configuration.0means "resolve it from the mount point", not "reserve nothing". That was already its effective meaning on the wire: the Dart layer drops the key when the resolved value is0, so0and "not provided" were indistinguishable (the Android bridge additionally gates on> 0; the iOS bridge gates on the key being present at all). Nothing changes for a value the host passes above0. To reserve nothing, wrap the widget in an ancestor that consumes the bottom padding:MediaQuery.removePadding(context: context, removeBottom: true, child: OctopusHomeScreen(...)).- No API change: no signature, type or default was touched, and hosts already passing an explicit value above
0keep their exact behaviour. The case that changes is an Android host passing0— or any value ≤ 0 — to mean "reserve nothing": it now resolves like the default, and should consume the padding as shown above instead. - On these four widgets, iOS deliberately does not resolve, and its behaviour is unchanged. There is nothing to fix there: the embedded view already sits inside the safe area, so the pill was never occluded. And resolving would actively cost height — since the iOS bridge reads this value as a total and subtracts the safe area the view occupies (see the entry below), an inferred 34 pt over a 34 pt inset yields
max(max(0, 34 − 34), 0.01)= 0.01 pt, where an absent key yields the bridge's historical additive 10 pt. Inferring a preference the host never expressed must not override a native default that is already correct. Note this scopes to the widgets: theshowOctopusHomeScreen/openNotificationhelpers have inferred an inset on both platforms since 1.12.3, and this release does not change that. - Hosts whose ancestors consume the
MediaQuerypadding are unaffected — aSafeArea, an explicitMediaQuery.removePadding, or aScaffoldbottomNavigationBar/persistentFooterButtonswithoutextendBody: true: the value resolves to0, the key stays off the wire, and each platform keeps its own native default. - A
ScaffoldwithextendBody: truenow reserves its bottom bar — which is the intent of the parameter. UnderextendBody,Scaffoldre-injectsmax(padding.bottom, bottomWidgetsHeight)intopaddingfor its body, so the resolved value is the bar's height. The embedded view runs behind the bar in that shape, so the pill previously sat behind it unless the host passed the height itself. It is still a change: such hosts now reserve the bar height where they reserved nothing before. - Known Android case that now over-reserves. Padding the layout is not the same as consuming the padding: a plain
Padding, aColumnabove a fixed footer, or aStackbottom overlay leaves the ambientMediaQueryuntouched. A host of that shape — e.g.Scaffold(body: Column(children: [Expanded(child: OctopusHomeScreen()), myFooter]))— now reserves the navigation-bar inset inside the embedded view on top of its own gap: the SDK's default content padding is replaced by that inset (roughly 24 dp with gesture navigation, 48 dp with 3-button). Pass the total you want, or consume the padding as shown above. - The resolution reads
MediaQuery.padding— what is left to reserve once ancestors consumed their share — because it is also the only field carrying abottomNavigationBar's height underextendBody: true(viewPaddingis zeroed there). It sits in an unconditionalBuilder: a wrapper that came and went between rebuilds would reparent the embedded platform view, which disposes and recreates the native view and restarts the SDK on its main feed. - Known limitation (Android): a widget first built while a keyboard is up resolves
0and keeps it, because the engine folds the bottom inset intoviewInsetsand creation params are read once. This holds for any host, aScaffoldbody included:resizeToAvoidBottomInsetzeroesviewInsetsfor the body, butMediaQueryData.removeViewInsetsonly zeroesviewInsetsand lowersviewPadding— it never writespadding, so thepadding.bottomthe engine already folded to 0 stays 0. It is not decidable at that moment either: "an ancestor consumed the padding" and "the keyboard folded it away" look identical, so resolving fromviewPaddinginstead would break the opt-out above. A host that may mount the SDK with the keyboard already up should pass the inset explicitly. This is a missed improvement rather than a regression — these widgets resolved nothing at all before, so such a mount behaves exactly as it did.
- iOS:
bottomSafeAreaInsetno longer double-counts the system safe area. The parameter is documented as a total bottom padding, and that is how Android behaves — its bridge consumes the system-bar insets before mounting the native view, so the host's value is the only bottom padding applied. iOS did not: the bridge forwarded the value untouched to the nativeOctopusHomeScreen(bottomSafeAreaInset:), which applies it through SwiftUI's.safeAreaInset(edge: .bottom)— additively, on top of the safe area the embedded view already sat in. The same Dart value therefore reserved roughly twice the intended band on iOS. The iOS bridge now subtracts the safe area the embedded view sits in before handing the value to the native SDK. The native iOS SDK's own additive contract is unchanged; only the Flutter bridge is affected.- What each platform reserves, for a requested value
RandS= the system safe area the embedded view itself sits in (not the device's —Sis 0 for the common case of aScaffoldbody above a bottom bar, and 34 pt on a notched iPhone only when the view runs to the bottom of the screen): AndroidR, iOSmax(R, S). The two agree wheneverR >= S— the intended usage, sinceRis meant to cover the host's bottom chrome, which itself sits above the system inset. BelowS, iOS still never reserves less than the safe area it already occupies. On iOS 14 the native SDK ignores the value entirely (its inset modifier requires iOS 15+), so nothing extra is reserved there. - Fixes a regression shipped in 1.12.3:
showOctopusHomeScreen/openNotificationauto-reserve the launching view's raw bottom safe area, and 1.12.3 claimed iOS was unaffected because of the native 10 pt floor. It was not — the helper sends the device inset (34 pt on a notched iPhone), which overrides that floor and then gets added to the safe area again. Measured on iPhone 16 / iOS 18.6 before this fix: 34 pt sent, 34 pt of system inset, both applied. These two helpers now reserve the intended amount on iOS. - Hosts that never passed the parameter are unaffected. Normalization applies only to a value the host explicitly sent; with none, the bridge keeps its historical 10 pt added on top of the system safe area, so existing layouts do not shift. Note that Dart omits the key when the value is
0, so0and "not provided" are the same thing on the wire. - Rendering change to be aware of. iOS hosts that did pass a value and worked around the old behaviour by sending only their bar's height (instead of the documented height + safe-area inset) will see the reserved band change from
H + Stomax(H, S). They should now send the total padding they want — the same value they already send on Android. - The normalized value is recomputed whenever the embedded view's geometry changes, instead of being captured once at mount before the view had reached its final position. To make that safe, a host-provided inset is emitted with a 0.01 pt floor: the native SDK gates its inset on
bottomSafeAreaInset > 0, and crossing that boundary would rebuild the displayed screen and discard its state, including text and image already entered in the create-post editor. The floor keeps the gate on a single branch and renders nothing. - Verified on iPhone 16 / iOS 18.6 by logging the container's resolved safe area: a full-screen route, a
fullscreenDialogpush and ashowModalBottomSheetall hold the correct value throughout their slide-up, and a portrait→landscape→portrait round-trip settles correctly (34 pt / 21 pt / 34 pt). One transient remains: on the first frame of the landscape→portrait restore, the container still reports its stale landscape geometry (bottom safe area 0) while the window already reports 34 pt, so that single frame over-reserves before the next layout corrects it — roughly 120 ms, mid-rotation. It is left uncorrected on purpose: every "is the geometry settled yet" heuristic tried here risked freezing a wrong value permanently, which is far worse than one frame. iPad multitasking was not exercised. - Known divergence with a hardware keyboard. The native SDK compares the reported keyboard height against the same value it uses as a reserved height, so normalizing the value also moves that threshold. The outcome changes for any reported keyboard height in
(R - S, R]and is identical everywhere else, so a full-height software keyboard is never affected. The reachable case is a hardware keyboard (iPad, or a Bluetooth keyboard on iPhone), which reports only the accessory bar: a host passing e.g.R = 70overS = 20with 55 pt reported previously kept its band and now drops it, letting host bottom chrome overlap the composer while typing. One scalar cannot satisfy both meanings from the bridge side; a proper fix needs the native SDK to take the total and the threshold separately. - Also fixes a pre-existing memory leak on iOS: the embedded view's login callback was a bound method, forming a container → hosting controller → root view → container retain cycle that leaked the whole SwiftUI tree and the SDK's managers on every mount.
- What each platform reserves, for a requested value
Example App #
- The published example app compiles again. From 1.12.0 to 1.12.3, the packaging step stripped
example/lib/debug/wholesale, but two files under it are part of the running sample — the Debug tab and the recordermain.dartstarts at launch. Their imports survived the strip, so the example shipped on pub.dev and on the public repository could not be built at all. The stripped boundary is nowexample/lib/debug/internal/, which holds only the Settings debug-console sheet; the Debug tab and its recorder ship. Nothing changes for the published SDK itself — this was a packaging defect, not a code one. - A key pasted into the Config screen is no longer stored, and no longer auto-starts the app. The sample persists its config and auto-starts it on the next launch, and it reaches the production backend whenever the build injects no
OCTOPUS_API_HOST(the published SDK exposes no host setter). Pasting a key intoCustom…therefore used to write it verbatim to plaintext on-device preferences and re-enter a client-facing backend with it on every subsequent launch, without the Config screen — or its production banner — ever being shown again. No API key value is written to storage now (an injected named key was already stored by id only), a config that resolves to no key lands on the Config screen instead of auto-starting, and a key left behind by an older build is stripped from storage on first load. Every other choice — key slot, user id, theme — is still restored, and builds that inject a key via--dart-define(including the QA launcher) keep auto-starting exactly as before. Sample-only; the published package is unaffected.
Documentation #
- What this package's version number means, written down.
MAJOR.MINORis locked to theMAJOR.MINORof the native SDKs inside it, on both platforms:^1.13.0means native 1.13 on Android and iOS.PATCHis each stream's own counter, so Android 1.13.1 with iOS 1.13.2 under package 1.13.0 is normal — the two badges inREADME.mdgive the exact pins. The practical consequence for you: a future release may bump this package's minor with no change to the Dart API at all, because the natives moved a minor. Nothing changes in 1.13.0 itself; this only states the rule the repo already followed.
1.12.3 #
Fixed #
showOctopusHomeScreen/openNotificationnow reserve the Android system navigation-bar inset by default. On edge-to-edge Android (API 35+), the full-screen helper previously let the SDK's floating "Write a post" button sit behind the system navigation bar: the route usesSafeArea(bottom: false)and the embedded native view consumes the system-bar insets, so nothing reserved the bottom. The helper now auto-reserves the launching view's bottom safe area. A new optionaldouble? bottomSafeAreaInseton both methods overrides it —null(default) = auto,0= the previous edge-to-edge look, a larger value = clear extra host bottom chrome. Additive, no breaking change; iOS is unaffected (native 10pt floor). TheOctopusHomeScreenwidget's own contract is unchanged — hosts that mount it directly still passbottomSafeAreaInsetthemselves (as the sample scenarios do).
1.12.2 #
New Features #
- Sign prefilled image shares on the create-post editor: new optional
bridgeShareTokenProvideronCreatePostScreenInfo—OctopusSDK().showOctopusCreatePostScreen(info: CreatePostScreenInfo(prefilledPost: ..., bridgeShareTokenProvider: (fingerprint) async => jwt)). When a community is configured to forbid member pictures, the server rejects a prefilled (Bridge / Share-in-game) post that carries an image unless it's signed. At publish time the SDK computes a SHA-256fingerprintof the final content and invokes this provider; your backend returns a JWT (HS256, the same shared secret as your SSO tokens) carrying that value in itsbridge_fingerprintclaim — ornullto send the post unsigned. Reuses the same native→Dart token round-trip asfetchOrCreateClientObjectRelatedPost; the provider is scoped to the editor session (opening another editor supersedes it). Wired on both platforms — Android maps it to the nativeCreatePostScreenInfo.bridgeShareTokenProvider, iOS toOctopusPrefilledPost.sign(native iOS pods bumped to1.12.4, where the signer landed). Additive and non-breaking; communities that allow member pictures don't need it. Platform note: the iOS native signer returns a non-optional token, so when the provider repliesnullthe iOS editor surfaces a signing error and stays open (a pictures-off community rejects the unsigned image anyway), whereas Android sends the attempt unsigned and lets the server reject it — the outcome is identical for the intended use case (return a real JWT). Register the provider only for communities that actually require signing.
Deprecations #
connectUsernow accepts atokenProvider— connect with atokenProvidercallback the SDK invokes whenever it needs a freshly-signed JWT (initial connect and every refresh, e.g.refreshEntitlements()). This matches the single nativeconnectUser(user, tokenProvider:)contract on Android and iOS.- New shape:
OctopusSDK().connectUser({required userId, Future<String> Function()? tokenProvider, nickname, bio, picture, @Deprecated token}). Provide exactly one oftokenProviderortoken— passing both (or neither) throwsArgumentErrorin every build. See MIGRATING.md for the full before/after. - The
tokenparameter (a pre-minted static JWT) is deprecated: a static token can't be re-minted when the SDK re-authenticates the user, so it fails once the JWT expires. Migrate by wrapping it in a provider —tokenProvider: () async => token. connectUserWithTokenProvider(...)is deprecated — callconnectUser(tokenProvider:)instead (identical behavior).- Backward compatible: existing
connectUser(token:)andconnectUserWithTokenProvider(...)calls keep working (with a deprecation hint). The native bridge is unchanged. The deprecated surface will be removed in a future major version.
- New shape:
Changed #
navBarLeadingActiononOctopusHomeScreennow works on Android too (was iOS-only). The optionalOctopusNavBarLeadingAction? navBarLeadingActionparameter (valuesclose/back) now drives the root leading nav-bar icon on Android by mapping to the nativeOctopusHomeScreen(leadingNavigationIcon:)(wrapped native Android SDK1.12.1+,NavigationIconType.Close/.Back). When set, the requested icon overrides the root leading icon regardless ofshowBackButton, and tapping it fires the existingonBackcallback — the same contract as iOS. Whennull(default), Android keeps its existing behaviour: a back arrow gated byshowBackButton. This lets a Flutter-hosted modal show a Close (X) affordance on both platforms with a single parameter. No API change —navBarLeadingActionwas already public; it is only newly functional on Android. (OctopusHomeContent/ theshowNavBar: falsevariant renders no top app bar, so it stays a no-op there, consistent withtitleCentered.)- iOS:
connectionState/isUserConnectednow distinguish guest sessions.OctopusConnected.isGuestis now populated on iOS fromOctopusProfile.isGuest(native iOS SDK1.12.6+), soOctopusSDK.isUserConnectedistrueonly for a fully authenticated (non-guest) user on iOS too — previously iOS reported every connection as non-guest. Behaviour now matches Android. No API change; gate community features (e.g. a "join the community" button) onisUserConnectedrather than on "a profile exists".
Bug Fixes #
- Android:
syncFollowGroupsnow persists the requested follow state. The wrapped native Android SDK had a bug wheresyncFollowGroupsalways sentfollowed=false, so following a group programmatically never persisted as followed. Fixed by bumping the native Android dependency to1.12.1. No Dart change — the Dart→Kotlin bridge already forwardedfollowedfaithfully; the fix is entirely in the native SDK. - Android: spurious "Invalid token" on devices whose clock runs ahead. The wrapped native Android SDK rejected valid bridge tokens as
Invalid tokenwhen the device clock was ahead of the server; the1.12.1bump resolves it. - iOS: the embedded
OctopusHomeScreenfeed could not be scrolled inside ashowModalBottomSheet. When the SDK was hosted in a Flutter modal bottom sheet on iOS, the sheet's drag-to-dismiss recognizer claimed every vertical drag, so the native feed (and post detail) stayed unscrollable. The embeddedUiKitViewnow attaches anEagerGestureRecognizer(matching Android), so the SDK's internal scroll wins body drags and scrolling works inside the sheet. As a result the gesture behaviour is now identical on both platforms: a drag on the body scrolls the feed rather than dismissing the sheet — dismiss via the Material drag handle, an explicit close button, ornavBarLeadingAction: close. (flutter/flutter#26425 and flutter/flutter#66270.) - Android: embedded SDK sub-screens now respect the host's
bottomSafeAreaInset. The inset was applied only to the main feed, so when the host deep-linked into (or navigated to) a sub-screen — post/comment detail, create-post — its pinned bottom bar (e.g. the comment composer and its legal disclaimer) rendered inside the system gesture-navigation area and was clipped. Because the embedded platform view consumes the system-bar insets, the host-suppliedbottomSafeAreaInsetis now propagated to those sub-screens too, so their bottom content clears the gesture area. - Android: the standalone create-post editor (
showOctopusCreatePostScreen) now actually closes. The editor Activity hosted the native screen as the sole/start destination of its ownNavHost, so the wrapped SDK's internal dismissal (popBackStack()— used both when the user taps the X and after a successful publish) had nothing to pop and silently no-opped: the screen stayed open,showOctopusCreatePostScreen's returnedFuturenever completed, and only a raw system back gesture happened to close it (by falling through to the OS default, since Navigation-Compose stops intercepting back at the root). A lightweight root destination now sits below the editor so the dismissal has something real to pop to; reaching it back finishes the Activity as originally intended. No API change. iOS was never affected (its dismissal is aSwiftUIenvironment action, not back-stack-dependent). - Android: the native back chevron now works at bridge-mode start destinations of
OctopusHomeScreen/OctopusPostDetailsScreen/OctopusGroupDetailsScreen(OctopusInitialScreen.post/.group/.createPost). Same root cause and fix shape as the create-post editor above: those screens dismiss themselves via the wrapped SDK's internalnavigateUp(), which no-ops when they're the sole/start destination — so tapping the chevron did nothing even though the host'sonBackwas correctly wired, and only system back worked. A lightweight root destination now sits below the bridge-mode target; reaching it back invokes the host'sonBackas originally documented. No API change.
Example App #
- Refresh Entitlements scenario no longer shows a stale result. The scenario read
app.profile?.entitlementssynchronously, in the same run-loop turnrefreshEntitlements()resolved — but the refreshed profile arrives through an independent reactive channel (native DB-observation → theprofilestream), so the synchronous read could race ahead of it and print the pre-refresh value even though the request actually succeeded. The Result text now points at the reactively-updated "Live state" card instead of taking its own snapshot. Sample-only; not an SDK bug.
Dependencies #
- Android native Octopus SDK
1.12.0→1.12.1(com.octopuscommunity:octopus-sdkandoctopus-sdk-ui). No public API change. - iOS native Octopus pods
1.12.2→1.12.6(OctopusCommunity/OctopusCommunityUIin the podspec, and the SPM pin inPackage.swift— kept in lockstep). Brings the iOS bridge-share signer (OctopusPrefilledPost.sign, from1.12.3) that the newbridgeShareTokenProviderwires to, the publicOctopusProfile.isGuestflag (1.12.6) behind the guest-session change above, plus backend-driven parity fixes the embedded UI inherits automatically (Android already had them from its1.12.1pin): per-field profile lock and per-content-type gating (both from iOS1.12.3), and an Xcode 27 / Swift 6.4 build fix (1.12.4). No public Dart API change from the bump itself.
Build #
- iOS: Swift Package Manager (SPM) support — dual with CocoaPods. The plugin now ships an
ios/octopus_sdk_flutter/Package.swiftalongside the existing podspec. Apps that use Flutter's Swift Package Manager integration (the default since Flutter 3.44) resolve the plugin — and the nativeOctopus/OctopusUISDK fromoctopus-sdk-swift— via SPM, while CocoaPods-based apps keep working unchanged. On the SPM path the native gRPC dependency resolves transitively via the Swift package, without the CocoaPods modular-headers handling the pod path needs for gRPC (use_modular_headers!/pod 'gRPC-Swift', :modular_headers => true). No public Dart/API change; minimum iOS is still 14.0. Both paths are exercised in CI.
1.12.1 #
Documentation #
- README rewritten from scratch — pub.dev landing page rebuilt around what a Flutter dev needs in the first 5 minutes: requirements table up top, three-step quick start (init → embed → connect), separate sections for theming, presentation modes, the Bridge pattern, push wiring, and a scannable streams table. Catalogs the rest of the public surface with depth-links to doc.octopuscommunity.com. All snippets verified against the public API.
No code changes — octopus_sdk_flutter 1.12.1 ships the exact same Dart, Android, and iOS surface as 1.12.0.
1.12.0 #
New Features #
- Custom API server endpoint: new
ApiServer(host, port)model and an optionalapiServerparameter oninitialize(...)andinitializeOctopusAuth(...). Route the SDK's gRPC traffic to a custom host/port over TLS. OmittingapiServer(the default) keeps the Octopus default endpoint. The host is validated at construction (ApiServerValidationError); scheme, port, path, and whitespace are rejected, bracketed/unbracketed IPv6 literals are accepted. - Multi-community switching:
OctopusSDK().switchCommunity(apiKey, appManagedFields, apiServer)(SSO) andswitchCommunityOctopusAuth(apiKey, deepLink, apiServer)(Octopus Auth) disconnect the current user, clear cached data, and reinitialize against another community at runtime. Give embedded UI akey: ValueKey(apiKey)so the native view is rebuilt for the new community. - SDK lifecycle:
OctopusSDK().reset()disconnects the user and returns the SDK to a clean state while staying initialized;OctopusSDK().stop()tears the SDK down to an uninitialized state. - Initialization state:
OctopusSDK.isInitialisedsynchronous getter andOctopusSDK.isInitialisedFlowStream<bool>(replays the current value to late subscribers and collapses consecutive duplicates). On iOS — which has no nativereset/stop/isInitialised— these are ported on top of the available native surface;reset()disconnects the user only (no public cache-clearing API on iOS). setGroupAccessDeniedCallback(...): register a callback invoked with thegroupIdwhen the connected user taps a group they cannot access (locked group / follow button / detail CTA). The SDK never navigates on the user's behalf — your app decides (upsell, paywall, …). Returns aVoidCallbackto unregister (call it indispose); registering again replaces the previous callback (last-write-wins).refreshEntitlements(): newOctopusSDK().refreshEntitlements()returningFuture<OctopusResult<void, RefreshEntitlementsError>>. Refreshes the connected user's community entitlements from the backend (SSO mode only). Typed errors:RefreshEntitlementsNoClientTokenProviderError,RefreshEntitlementsUserNotConnectedError,RefreshEntitlementsNoNetworkError,RefreshEntitlementsUserBannedError(backend message, displayable),RefreshEntitlementsServerError.- Community groups (
groups): newOctopusSDK.groupsStream<List<OctopusGroup>>emitting the community's groups (content categories) and re-emitting on any change (follow/unfollow, admin updates), with the latest list replayed to late subscribers and consecutive duplicates collapsed.OctopusGroupis a lean model exposingid,name,isFollowed,canChangeFollowStatus,canAccess(false = visible-but-locked; route throughsetGroupAccessDeniedCallback), andcanCreateChildren. Mirrors the nativeOctopusSDK.groupspublic surface. - Connected user profile (
profile): newOctopusSDK.profileStream<OctopusProfile?>emitting the connected user'sOctopusProfile(ornullwhen not connected). Emits on every profile change — including afterrefreshEntitlements()— and replays the latest value to late subscribers.OctopusProfileexposes the user's heldentitlements(Set<String>, opaque tokens defined by the host app; display-only). Mirrors the nativeOctopusSDK.profile. setReaction(...): newOctopusSDK().setReaction(OctopusReactionKind? reaction, String postId)returningFuture<OctopusResult<void, SetReactionError>>. Sets (or removes, withnull) the connected user's reaction on any post — bridge posts and community posts.OctopusReactionKindis a sealed class (not an enum) with the const singletonsOctopusReactionKind.heart,.joy,.mouthOpen,.clap,.cry,.rage, and anOctopusUnknownReaction(serverValue)forward-compat fallback — so a reaction added by a newer backend decodes without an SDK release. Business failures are typedSetReactionErrors (SetReactionUnknownReactionError,SetReactionPostNotFoundError,SetReactionReactionError) carried byOctopusInvalidArguments; transport/auth failures surface through the orthogonalOctopusConnectionFailurebranch. Platform note: on iOS, removing a reaction withnullwhen none is set currently reports aSetReactionReactionErrorrather than the Android silent no-op.- Typed results (
OctopusResult): newOctopusResult<D, E>sealed hierarchy mirroring the native SDK —OctopusSuccess, connection failures (OctopusNoNetwork,OctopusContentUnavailable,OctopusUserNotAuthenticated,OctopusPermissionDenied,OctopusStatusError), andOctopusInvalidArguments<E>carrying typedOctopusServerErrors — with helpers (mapSuccess,mapErrors,onSuccess,onFailure,onError,getOrNull,getOrElse). Use the helpers, or pattern-match (exhaustive switches must annotateOctopusInvalidArguments<OctopusServerError>— see MIGRATING.md). - Bridge create-post models: new immutable, value-equal input models for the upcoming Bridge create-post APIs (programmatic client-object posts and the prefilled post editor).
OctopusPrefilledPost({String? text, Uint8List? image, String? topicId, OctopusPostCTA? cta})validates its payload eagerly and throws a sealedOctopusPrefilledPostValidationError(...ContentEmptyError,...TextTooShortError,...TextTooLongError,...CtaLabelEmptyError,...CtaUrlEmptyError); empty text/image and blanktopicIdare normalised tonull, and text length is bounded to 10–5000. Image bytes are passed asUint8List(the host materialises them — the SDK does not fetch remote URLs) and are dimension-checked later in the editor, matching the native Android behaviour.OctopusPostCTA({Uri url, String label})is the host-supplied call-to-action (invisible in the editor, attached to the published post).CreatePostScreenInfo({OctopusPrefilledPost? prefilledPost})describes the create-post entry point.ClientPost({String objectId, String text, OctopusClientPostAttachment? attachment, String? catchPhrase, String? viewObjectButtonText, String? groupId})describes a post linked to one of your app's objects; its imageattachmentis a sealedOctopusClientPostAttachment(OctopusLocalImageAttachment(bytes)/OctopusRemoteImageAttachment(url)). All four mirror the lean intersection of the nativeClientPost/OctopusPrefilledPost/OctopusPostCTA/CreatePostScreenInfopublic surfaces; the consuming methods and editor widget land in a later release. - Bridge post API (
fetchOrCreateClientObjectRelatedPost): newOctopusSDK().fetchOrCreateClientObjectRelatedPost(ClientPost clientPost, {Future<String?> Function(String fingerprint)? tokenProvider})returningFuture<OctopusResult<OctopusPost, ClientPostError>>. Fetches the Octopus post linked to one of your app's objects, creating it fromclientPostif it doesn't exist yet (links community discussion to your content). The optionaltokenProvideris invoked only when a new post must be created and your community requires a bridge signature: it receives the SHA-256 content fingerprint and returns a JWT signed by your backend (ornull).OctopusPostis the lean read view (id,reactionsasOctopusReactionCounts,commentCount,viewCount,userReactionKind). Content errors are typedClientPostErrors (ClientPostTextMissingError,ClientPostTextTooLongError,ClientPostFileEmptyError,ClientPostFileTooLargeError,ClientPostFileBadFormatError,ClientPostFileUploadError,ClientPostFileDownloadError,ClientPostMissingObjectIdError,ClientPostMissingCtaError,ClientPostUnavailableError,ClientPostNotFoundError,ClientPostAlreadyExistsError,ClientPostInvalidGroupIdError,ClientPostInvalidAuthorError,ClientPostTokenInvalidError,ClientPostTokenExpiredError,ClientPostOtherError) carried byOctopusInvalidArguments; transport/auth failures surface through the orthogonalOctopusConnectionFailurebranch. Platform note: on iOS the native SDK does not expose the specific validation error kind publicly, so content errors there surface asClientPostOtherError; Android produces the fine-grained subtypes. setNavigateToClientObjectCallback(...): register a callback invoked with theobjectIdwhen the user taps the "view object" button on a bridge post (a post created viafetchOrCreateClientObjectRelatedPostwith aviewObjectButtonText). The SDK never navigates on the user's behalf — your app opens its own article/product screen for that object. Returns aVoidCallbackto unregister (call it indispose); registering again replaces the previous callback (last-write-wins). Mirrors the native per-screenonNavigateToClientObject(Android) / globaldisplayClientObjectCallback(iOS).- Bridge post observation (
getClientObjectRelatedPostFlow): newOctopusSDK.getClientObjectRelatedPostFlow(String clientObjectId)returningStream<OctopusPost?>— observe the Octopus post linked to one of your app's objects (nulluntil it exists). The current value is replayed to a new subscriber, then the stream re-emits whenever the post changes — including right afterfetchOrCreateClientObjectRelatedPostcreates it, and on internal updates (reactions, comment count). Each subscription drives its own native observation, so observing the sameclientObjectIdfrom two places is safe; cancel the subscription to stop observing. Mirrors the nativegetClientObjectRelatedPostFlow(Android) /getClientObjectRelatedPostPublisher(iOS). - Create-post editor (
showOctopusCreatePostScreen): newOctopusSDK().showOctopusCreatePostScreen({CreatePostScreenInfo? info, OctopusTheme? theme})method opens the native Octopus post editor (Bridge Share) presentation-style — a dedicated Activity on Android, a full-screen modal on iOS — so the editor's chrome (close button, post button, group picker) is owned by the SDK on both platforms with no inline-mount limitation. Pass aCreatePostScreenInfo(prefilledPost: OctopusPrefilledPost(text, image, topicId, cta))to preset the editor's text, image (rawUint8Listbytes — the SDK never fetches remote URLs), target group, and an invisibleOctopusPostCTAattached to the published post; omitinfoto open an empty editor. ReturnsFuture<void>that completes when the editor closes (publish or cancel). Login / profile-edit / view-object intents originating inside the editor are routed back to the host via the existing global events (onNavigateToLogin,onModifyUser,setNavigateToClientObjectCallback). Mirrors the nativeOctopusCreatePostScreen(Android) /OctopusInitialScreen.createPostonOctopusHomeScreen(iOS). bottomSafeAreaInsetonOctopusHomeScreen: new optionaldouble bottomSafeAreaInsetparameter (default0) that pads the embedded view's bottom area so the floating "Write a post" button clears the host app's bottom chrome (MaterialBottomNavigationBar, custom shell, …). Plumbed end-to-end to native: AndroidcontentPadding, iOSbottomSafeAreaInset. PasskBottomNavigationBarHeight + MediaQuery.viewPaddingOf(context).bottomfor a standard Material shell.titleCenteredonOctopusHomeScreen: new optionalbool titleCenteredparameter (defaultfalse) that centers the title in the SDK's native top app bar. Supported on both platforms — Android maps it to the nativeOctopusHomeScreen.titleCenteredcomposable param, iOS toOctopusMainFeedTitle.Placement.center(.leadingwhenfalse) on the main feed. Not added toOctopusHomeContent: the native AndroidOctopusHomeContentrenders no top app bar, so the flag would have no effect there.navigationModeonOctopusHomeScreen(iOS-only): new optionalOctopusNavigationMode navigationModeparameter (defaultOctopusNavigationMode.navigationStack) selecting which navigation container the native iOS SDK uses internally. The Flutter wrapper's default deliberately differs from the native iOS default (OctopusNavigationMode.automatic, currently the legacyNavigationView): every Flutter route is by definition a UIKit-hosted reparented presentation, and the legacyNavigationViewwill silently drop sub-navigation pushes there (a post tap not opening its detail, a "Yes" confirmation on the unsaved-changes alert that leaves the New Post screen in place, …).navigationStackkeeps them working (iOS 16+, with aNavigationViewfallback below). PassOctopusNavigationMode.automaticexplicitly to opt back into the native iOS default. Maps to the nativeOctopusHomeScreen(navigationMode:)(wrapped iOS SDK 1.12.2+). No-op on Android — the bridge already drives the SDK through a ComposeNavHostthat keeps its back stack across modal hosting, so there is no equivalent setting. This is the supported fix for the modal/sheet sub-navigation limitation noted in earlier 1.12.0 development.navBarLeadingActiononOctopusHomeScreen(iOS-only): new optionalOctopusNavBarLeadingAction? navBarLeadingActionparameter (defaultnull, valuesclose/back) that asks the native iOS SDK to render a host-driven leading nav-bar button (close icon or back chevron) on its root screen; tapping it fires the existingonBackcallback. This is the native replacement for theleadingWidget/trailingWidgetFlutter overlays on iOS — the iOS SDK only paints its own close button when presented natively (.sheet/.fullScreenCover), which never happens for a Flutter-hostedUiKitView, so before this a Flutter-hosted modal had no native dismiss affordance. The native button lives inside the SDK's own nav bar, so it never reparents the embeddedPlatformViewand is hidden automatically on deeper screens. Maps to the nativeOctopusHomeScreen(navBarLeadingAction:)(wrapped iOS SDK 1.12.2+). No-op on Android — the native AndroidOctopusHomeScreenalready renders a leading back arrow on the root screen (controlled byshowBackButton, also routed toonBack); pairnavBarLeadingAction(iOS) withshowBackButton: true(Android) for a native dismiss affordance on both platforms.formatOctopusCompactCount(int, {Locale? locale}): new top-level helper that formats post / reaction counts (OctopusPost.commentCount,OctopusPost.viewCount,OctopusReactionCount.count, or any host-side integer) in the same compactK/M/Bstyle as the embedded community UI —0–999raw, then1.2K/12K/999K/1.2M/1B(band-floor, not round). The optionallocaleflips the decimal separator (1,2Kfor French / German / Spanish / …,1.2Kfor English and the default). Mirrors the native AndroidInt.toCompactString(Locale); the iOS native helper renders the same shape with an English-only decimal point.- Screen entry points (
OctopusInitialScreen): new sealedOctopusInitialScreenwith four variants —OctopusInitialScreen.mainFeed()(default),OctopusInitialScreen.post(PostScreenInfo(postId: ...)),OctopusInitialScreen.group(GroupScreenInfo(groupId: ...)), andOctopusInitialScreen.createPost(CreatePostScreenInfo)— passed via the new optionalinitialScreen:parameter onOctopusHomeScreenandOctopusHomeContent. Bridge-mode entry points (.post/.group) open a single post or group feed with no main-feed back navigation. Also adds dedicatedOctopusPostDetailsScreen(postId:)/OctopusGroupDetailsScreen(groupId:)widgets as ergonomic shorthands wrapping the same flow. Precedence rule: when anotificationwith a non-emptylinkPathis supplied at the same mount, the deep link wins andinitialScreenis ignored. Platform note: image bytes carried inOctopusPrefilledPost.imageare dropped on the embeddedcreatePostroute on both platforms — useshowOctopusCreatePostScreenfor the image-share flow. Back navigation note: the SDK paints a back chevron at the bridge-mode start destination on both platforms (Android natively; iOS via theshowBackButton: true→navBarLeadingAction: .backbridge wiring). To wire the tap, pass anonBack:callback to the wrapper (OctopusPostDetailsScreen/OctopusGroupDetailsScreennow forwardonBackto the innerOctopusHomeScreen) that pops the host route or otherwise dismisses the screen. Hosts that don't passonBackkeep relying on the system back gesture / their ownAppBarback button.
Changed #
- BREAKING —
overrideCommunityAccess(bool)now returnsFuture<OctopusResult<void, OverrideCommunityAccessError>>(wasFuture<void>). It no longer throws on handled SDK failures; inspect the returned result instead. Fire-and-forget callers can ignore the result (await octopus.overrideCommunityAccess(true);still compiles). See MIGRATING.md. On iOS the native call surfaces all handled failures asOctopusInvalidArguments([OverrideCommunityAccessUnknownError(...)])(iOS has no typed error for this call). connectUserWithTokenProvideris now persistent —refreshEntitlements()works. The wrapper used to be a one-shot: it awaited the provider once at connect time and passed the resulting JWT string to the native SDK. The native SDK never received the closure, sorefreshEntitlements()had no way to ask Dart for a fresh JWT and always returnedRefreshEntitlementsNoClientTokenProviderError. The wrapper now stores the provider for the connection's lifetime; the native SDK re-invokes it on every refresh, exactly matching the Android (OctopusSDK.connectUser(user, tokenProvider: suspend () -> String)) and iOS (octopus.connectUser(clientUser) { @Sendable in … }) public contracts. Behavior change visible to existing callers:connectUserWithTokenProviderno longer discards the closure after the first invocation. The dartdoc never documented one-shot semantics — this is the documented behavior catching up to the documented intent. Cleared ondisconnectUser.
Bug Fixes #
-
Android: embedded SDK feed now scrolls inside a
showModalBottomSheet/ any vertical-drag ancestor:OctopusSDK.embeddedViewconfigured the underlyingAndroidViewwith an emptygestureRecognizersset — the Flutter default, which means the native view only receives pointer events no ancestor recognizer has claimed. AshowModalBottomSheetancestor installs aVerticalDragGestureRecognizer(drag-to-dismiss) and won the gesture arena on every vertical drag, so the SDK's nativeLazyColumnnever saw the gesture and the feed was unscrollable inside the Sheet scenario. The Android branch now passes anEagerGestureRecognizer, so every pointer landing on the embedded view is dispatched straight to the native side — the feed scrolls, the sheet's drag-handle still dismisses the sheet (it lives outside the AndroidView bounds), and there is no behavioural change on iOS (UiKitViewdoesn't expose the same knob; UIKit's gesture recognizer delegation lets the innerUIScrollViewwin automatically — the documented platform asymmetry behindflutter/flutter#26425/#66270). The sample's Sheet scenario gainsshowDragHandle: trueso drag-to-dismiss remains discoverable via the Material handle (rendered above the AndroidView), matching M3's recommended pattern for sheets with scrollable content. -
Sub-navigation drop when
leadingWidget/trailingWidgetoverlays are configured (the #63 report — root cause found and fixed): when a leading/trailing overlay was set onOctopusHomeScreen, the widget swapped its tree shape betweenStack(Positioned.fill(view), overlay)(main feed) and the bare view (deeper screens). That reparenting disposed and recreated the native PlatformView on the very first sub-navigation: the tap registered (view count incremented,screenDisplayed(postDetail)fired) but the freshly-recreated view restarted on the main feed — the user never saw the detail screen. This is the mechanism behind the 1.12.0-dev "modal sub-navigation drop" report (#63): the 1.12.0-dev helper attached a default close-overlay (so it reproduced), while the later end-to-end validation exercised overlay-less shapes (so it could not reproduce). The tree shape is now stable — only the overlay children come and go — and post taps push detail in every route shape (modal / fullscreen / sheet), verified end-to-end on Android and iOS. -
Android top app bar now tracks
themeMode: the bridge previously passedColor.Unspecifiedfor the top app barcontainerColorand let the native SDK fall back toOctopusColorScheme.gray900/.background. In the SDK's darkOctopusColorSchemethose tokens are still dark, but the title-content fallback also resolved togray900— producing dark-on-dark text that made the "Community" / "Post detail" titles invisible in dark mode (and, whennavBarPrimaryColorwas set, a brand-pink top app bar regardless ofthemeMode).OctopusFlutterTheme.ktnow resolves the colour scheme first and passes the correspondingbackgroundascontainerColor, plus an explicit foreground (Color.Whitein dark mode, default elsewhere) for title / nav-icon / action-icon. iOS is unaffected — itsUINavigationBaralready followstraitCollection.userInterfaceStyle. -
iOS top app bar now shows a configured custom-theme logo over a text title: when an
OctopusThemecarrying a logo was supplied together with anavBarTitle, the iOS bridge always rendered the text title, so the brand logo never appeared (e.g. on the sample's Community tab). A configured logo now takes precedence over the text title — matching Android — and the main-feed title placement followstitleCentered. iOS only. -
iOS now honours
showBackButtononOctopusHomeScreen/OctopusSDK.embeddedView: the iOS bridge previously readnavBarLeadingActionbut silently ignoredshowBackButton, so Flutter hosts that followed the documented cross-platform pattern (showBackButton: truewith no explicitnavBarLeadingAction) got a back chevron on Android and nothing on iOS — the SDK'sUINavigationBarhad no visible leading action. The bridge now backfills the missing leading action: whenshowBackButton: trueis on the wire and no explicitnavBarLeadingActionwas provided, the iOS SDK renders a back chevron whose tap routes through the existingbackRequestedevent →OctopusHomeScreen.onBack. An explicitnavBarLeadingAction(.close/.back) still wins. Hosts that wiredshowBackButton: truethinking it was a no-op on iOS will now see a leading chevron in 1.12.0 — make sureonBackis wired (it is null-safe: a missing handler leaves the chevron rendered but inert). -
onBacknow forwarded byOctopusPostDetailsScreen/OctopusGroupDetailsScreento the innerOctopusHomeScreen: the two standalone bridge-mode wrappers added with the screen entry points didn't forward theonBackcallback to the inner widget. Combined with the iOSshowBackButtonwiring above, this left the back chevron visible but inert on iOS for hosts using the wrapper widgets. Both wrappers now expose an optionalVoidCallback? onBackparameter and forward it. Hosts that don't passonBackkeep the prior "host-owned back via system gesture /AppBar" behavior — additive, no breaking change.
Known Issues #
- Modal-hosted sub-navigation drop — root cause identified and fixed in this release. During 1.12.0 development, mounting
OctopusHomeScreeninside a modal-style route was reported to silently drop the SDK's internal sub-navigation — taps on a post body incremented the view count but never pushed the post detail. The helpers were briefly@Deprecatedover it, then later end-to-end validation could not reproduce the drop. The discrepancy is now explained: the drop required aleadingWidget/trailingWidgetoverlay to be configured (the 1.12.0-dev helper attached a default close overlay; the later validation exercised overlay-less shapes). See the Bug Fixes entry above for the mechanism and the fix; the route shapes themselves (showModalBottomSheet,fullscreenDialog, the helpers) were never at fault. Tracked internally; related native hardening tracked internally. - Host overlays don't reappear after a back-pop to the feed.
leadingWidget/trailingWidgetare hidden while the SDK shows a deeper screen (they would collide with the SDK's own chrome). The native SDKs emitscreenDisplayedon forward navigation only — no event fires when popping back to the main feed (verified on both platforms) — so the overlays cannot reappear until the widget remounts. For a robust modal dismissal affordance on iOS, prefer the new nativenavBarLeadingActionover atrailingWidgetclose button: it lives inside the SDK's own nav bar (no overlay gating, noPlatformViewreparenting) and the SDK hides it automatically on deeper screens. The host-controllable nav-bar leading action on iOS (tracked internally) shipped in the wrapped iOS SDK 1.12.2 and is exposed here asnavBarLeadingAction.
Dependencies #
- Android Octopus SDK: 1.11.0 → 1.12.0
- iOS Octopus SDK: 1.11.0 → 1.12.2 (1.12.1: gamification sheet now shown only while the Octopus screen is visible — relevant to the embedded
PlatformView; 1.12.2: ships thenavigationMode/navBarLeadingActionAPIs wired below). The Android native SDK has no 1.12.1/1.12.2 release, so the wrapped versions intentionally diverge by patch (Z) per platform.
Example App #
- Theme picker now drives the SDK content: the sample's Light / Dark / System choice on the Config screen is now propagated to
OctopusTheme.themeModefor every embedded SDK surface in the sample (Community tab and the Modal / Fullscreen / Sheet / Initial-screen / Not-seen-notifications scenarios). Previously onlyMaterialApp.themeModeflipped — the SDK kept observing the device, producing a visible mismatch (e.g. dark Flutter chrome with a light SDK feed). System mode still passesnullso the SDK natively tracks the device. NewAppState.effectiveOctopusTheme()is the single source of truth; the brandOctopusThemefrom the Theme scenario no longer hardcodesthemeMode: light— the Config choice wins. - Three distinct presentation-mode scenarios: the sample demos the three non-embedded integration shapes side by side in the Scenarios tab — Modal (
MaterialPageRoute(fullscreenDialog: true), the.fullScreenCoverequivalent: slide-up on iOS, native SDK close button vianavBarLeadingAction: close+navigationMode: navigationStackso sub-navigation works in the modal), Fullscreen (standardMaterialPageRoutepush, theshowOctopusHomeScreenhelper shape), and Sheet (showModalBottomSheetat 90% height, drag-to-dismiss, alsonavigationMode: navigationStack). The duplicate launcher cards on the Home tab were removed — the Home tab is now a pure read-only dashboard. All three scenarios forward the device's physical bottom inset (MediaQueryData.fromView(View.of(context)).viewPadding.bottom, immune to ancestorSafeAreaconsumption) asbottomSafeAreaInsetso the SDK's floating "Write a post" pill clears the device gesture pill / home indicator. - Bundle id unified across platforms —
com.octopuscommunity.sdk.flutter.sample: the sample's iOS bundle (wascom.octopuscommunity.octopusSdkFlutterExample) and AndroidapplicationId+namespace+ Kotlin package +MainActivity.ktlocation (wascom.octopuscommunity.octopus_sdk_flutter_example) all migrate to the same dedicated Flutter-sample identifier — distinct from the native Swift / Android SDK samples (com.octopuscommunity.sdk.sample) so the three can coexist on a single device. iOS team migrated from4KE44M8274(useradgents, the team that originally bootstrapped the SDK) to8W7579HZX7(Octopus Community) so push provisioning lives on the same Apple Developer account as the native samples. (#116, #117, #118) - iOS push provisioning end-to-end: new
example/ios/Runner/Runner.entitlementsdeclaringaps-environment = development, wired viaCODE_SIGN_ENTITLEMENTSin the three Runner build configs. The dedicated bundle has a matching APNs Auth Key + AWS SNS platform applications on the Octopus backend, so a sandbox push from the BE routes end-to-end to the sample. (#116, #117) - Android notification icon — monochrome white silhouette: new
@drawable/ic_stat_notification(Octopus chat-bubble silhouette painted white on transparent) in the five canonical densities (24 / 36 / 48 / 72 / 96 px), referenced byflutter_local_notifications'AndroidInitializationSettingsand by Firebase Messaging'sdefault_notification_iconmeta-data;notification_color = #02569B(Flutter Sky Blue) for the status-bar accent chip. Replaces the full-color@mipmap/ic_launcherthat Android 5+ rendered as an opaque grey square. iOS uses the app icon for notifications by default; no separate asset needed. (#118, #119) - App icon redesign: Octopus chat-bubble + a centred "Flutter" pill badge at the top (Chrome-Beta-style, Flutter Sky Blue
#02569B) — distinguishes the Flutter sample from the native Swift / Android samples on the home screen. Generated for iOS (Assets.xcassets) + Android (legacy mipmap + adaptive icon foreground / background) by the newflutter_launcher_icons: ^0.14.4dev-dependency fromexample/assets/icon/app_icon_flutter.png. App display name set to"Octopus SDK Sample - Flutter"on both platforms. (#119) OCTOPUS_FLUTTER_PUSH_API_KEYpicker slot: a new named API-key slotflutterPush("Flutter push (dedicated bundle)") wired to the new bundle's push provisioning. Selectable from the Config screen's API-key picker, or as the default withOCTOPUS_KEY_NAME=OCTOPUS_FLUTTER_PUSH_API_KEY. The key is injected at build time via--dart-defineand is never committed. (#119)
1.11.0 #
New Features #
- Push notification
syncFollowGroups: batch follow/unfollow groups in one round-trip viaOctopusSDK().syncFollowGroups([...]). Returns per-actionSyncFollowGroupResultwith a typedSyncFollowGroupStatus(withunknownErrorfallback for future native variants). RPC-level failures surface asPlatformExceptionwith codesnot_connected,no_network,server, orother.- Typed Dart
OctopusEvent/Screenclasses for new 1.11 events:GroupFollowingChangedEvent,MainFeedScreen,GroupsScreen,GroupDetailScreen(withGroupDetailSourceenum:bridge/community/unknown).
Bug Fixes #
ProfileField.picturewas silently dropped fromappManagedFields: the Dart side serialized it as'AVATAR', but both native bridges only recognize'PICTURE'and silently dropped the rest. Customers who initialized withappManagedFields: [..., ProfileField.picture]had the field never reach the native SDK, so users could still edit their profile picture inside the Octopus UI. Wire format is now correct (PICTURE). Customers usingProfileField.pictureshould re-test their profile flows — pictures will now be blocked in the Octopus UI and routed throughonModifyUser('PICTURE')as documented.
Dependencies #
- Android Octopus SDK: 1.9.0 → 1.11.0
- iOS Octopus SDK: 1.9.0 → 1.11.0
1.9.1 #
Bug Fixes #
- Late subscriber replay:
notSeenNotificationsCountandhasAccessToCommunitystreams now cache the latest native emission so that Dart listeners attached afterinitialize()don't miss the initial value - screenDisplayed event parsing: Fixed
type '_Map<Object?, Object?>' is not a subtype of type 'Map<String, dynamic>'crash when receivingscreenDisplayedevents
Example App #
- Load logo from bundled asset instead of hardcoded base64 string
- Enable custom logo display (
logoBase64) - Set
ProfileField.nicknameas app-managed field - Remove hardcoded
navBarTitlefromOctopusHomeScreen
1.9.0 #
New Features #
- Notification Badge Count:
Stream<int> notSeenNotificationsCountfor reactive badge updates,updateNotSeenNotificationsCount()to force refresh - Community Access / A/B Testing:
Stream<bool> hasAccessToCommunityfor reactive access state,overrideCommunityAccess(bool)to override cohort,trackCommunityAccess(bool)for analytics - URL Interception:
onNavigateToUrlcallback onOctopusHomeScreenwithUrlOpeningStrategyenum (handledByApp/handledByOctopus) - Locale Override:
overrideDefaultLocale(Locale?)to override the SDK UI language (e.g.Locale('fr'),Locale('en', 'US'), ornullto reset) - Custom Analytics:
trackCustomEvent(String name, Map<String, String> properties)to send custom events to Octopus analytics - SDK Events:
Stream<OctopusEvent> events— typed event stream covering 20 event types (content creation/deletion, reactions, polls, gamification, screen navigation, clicks, profile changes, sessions). UseOctopusSDK.events.listen(...)with Dart pattern matching
Breaking Changes #
See MIGRATING.md for details
appManagedFieldsparameter changed fromList<String>?toList<ProfileField>?— useProfileField.nickname,.picture,.bioinstead of raw strings- Renamed
OctopusViewtoOctopusHomeScreen - Renamed
OctopusSdkFluttertoOctopusSDK - Renamed
initializeOctopusSDK()toinitialize() - Renamed
showOctopusHome()toshowOctopusHomeScreen() - Renamed
OctopusSdkFlutterPlugintoOctopusSDKFlutterPlugin(internal) - Renamed
OctopusSdkFlutterPlatformtoOctopusSDKPlatform(internal) - Renamed
MethodChannelOctopusSdkFluttertoOctopusSDKMethodChannel(internal) - Renamed
OctopusComposeWidgettoOctopusHomeScreen(Android internal) - Removed legacy
showNativeUI()andcloseNativeUI()methods
Dependencies #
- Android Octopus SDK updated to 1.9.0
- iOS Octopus SDK updated to 1.9.0
Improvements #
- Simplified callback mechanism: replaced dual callback registry with single event stream
- SDK now initializes automatically on app start (removed manual "Init" button in example)
- Added user connection state persistence between app restarts
- Auto-reconnect user after SDK init if previously connected
Example App #
- Display unread notification count and community access state in SDK Status panel
- Consolidated connect/disconnect into single conditional button
- Added
SafeAreato Configuration tab for edge-to-edge display fix - Shortened toast durations
- Community tab now recreates on each tap (removed IndexedStack)
- Moved secrets (API key, JWT token) to gitignored
secrets.dartfile
1.7.1 #
- Moving Android Octopus SDK to 1.7.2 (fixes Protobuf dependencies conflicts with Firebase)
- Moving iOS Octopus SDK to 1.7.2 (fixes Cocoapods package name conflicts between GRPC-Swift and GRPC-Core)
1.7.0 #
- First public release aligned with Octopus native SDK 1.7 on iOS and Android.
- Added SSO (
initializeOctopusSDK) - Added user session helpers:
connectUser,connectUserWithTokenProvider, anddisconnectUser. - Introduced embedded
OctopusViewwidget with callbacks for login navigation, profile edits, and back events. - Added theme customization (colors, font sizz, logo, light/dark modes) passed through to the native UI.