octopus_sdk_flutter 1.13.2
octopus_sdk_flutter: ^1.13.2 copied to clipboard
White-label social community SDK for Flutter — embed a moderated community in your app.
1.13.2 #
New Features #
-
OctopusSDK.showOctopusHomeScreen()andOctopusSDK.openNotification()now acceptonBackandnavBarLeadingAction.onBackis a notification, not a delegation: the helper still pops the route it owns — before this change it popped and told the host nothing, so a host had no way to learn the user had left the community without dropping down to theOctopusHomeScreenwidget and rebuilding the route itself. The callback runs before the pop, and a callback that throws is reported throughFlutterError.reportErrorwhile the route pops anyway, so a host handler can never strand the user inside the SDK. The pop is guarded on the helper's own route still being the current one when the callback returns, so a callback that navigates by itself — pops the route, or pushes a dialog — is neither double-popped nor has its own route closed from under it; a callback that only does bookkeeping pops exactly as before.navBarLeadingActionis forwarded verbatim to the embedded view, so the full-screen helper can now show the close (X) icon instead of the back chevron. Both parameters are optional; omitting them leaves the previous behaviour unchanged, except that a leading-icon tap arriving while the route is already leaving is now a no-op instead of popping whatever sits under it.Only the tap on the SDK's root leading icon reaches
onBack; no OS-level gesture does. While the SDK is on its root screen, Android system / predictive back pops the Flutter route directly, without abackRequestedevent. Deeper inside the SDK the native navigation stack consumes the gesture and navigates up inside itself, so neitheronBacknor the returnedFuturefires and the Flutter route stays up. On iOS the helper pushes a plainMaterialPageRoute, so the swipe-from-left-edge gesture it enables is a Flutter-level pop that emits nobackRequested, while the native SDK runs its own navigation stack for its internal screens. The returnedFuture— which completes on every path that actually dismisses the route — remains the way to observe those.
Bug Fixes #
- Android: fixed a crash (
UninitializedPropertyAccessExceptioninOctopusSDK.getKoinApp) when Android restores the native create-post editor in a process where the SDK was never initialised — after a process death (low memory, another crash, "Don't keep activities") while the editor was in the foreground. The editor activity now finishes itself instead of rendering, handing control back to the host app as the OS restored it. On Android,showCreatePostScreen()called beforeinitialize()now fails with the sameNOT_INITIALIZEDerror as on iOS instead of starting the editor.
Dependencies #
- Android Octopus SDK: 1.13.2 → 1.13.4. No API change. Fixes four crashes in
the native Android layer:
- two in the embedded UI when a host sets
overrideDefaultLocale— the text-selection "process text" actions and the fullscreen image viewer (1.13.3); - the
UninitializedPropertyAccessExceptiononkoinAppwhen Android restores an Octopus screen — including the embedded view — in a process where the SDK has not been initialised yet: the native UI now renders nothing and finishes cleanly instead of crashing (1.13.4); - a
NoClassDefFoundError/NoSuchMethodErroratinitialize()in host apps that shrink their build with R8, because gRPC's Guava surface was stripped: the SDK now ships the consumer keep rules it needs (1.13.4).
- two in the embedded UI when a host sets
Example App #
- New Presentation modes → Embedded Back Button scenario: pick the leading
icon (
showBackButton,navBarLeadingAction, and the case where both are set and disagree), pick the container (the embeddedOctopusHomeScreenwidget or theshowOctopusHomeScreenhelper), and watch a live counter of how many timesonBackfired and from which container — the two callback contracts side by side. - Dark-theme nav accent, SDK brand theme and remaining
ColorSchemeteal values corrected to the shared sample tokens contract, ported from the Android sample: the earlier navy pass had already retired teal/salmon but predated that later contrast fix, so dark theme still used the light-theme accent#1D88FEdirectly as a label, a selected-segment label and a selected-control fill, and the SDK theme's darkprimaryMain/onPrimarywere#1D88FE/black.#1D88FEis fill-only (3.50:1 on white — under the 4.5:1 label/text floor in either theme): dark theme now uses the dedicatedAccentDark #6FB2FFfor labels/icons and selected-control fills, withAccentInk #142238as the ink on any text drawn over anAccentDark/#1D88FEfill — including the selected segmented-button label, which previously read white-on-#1D88FEin light theme (3.50:1) — while the switch thumb, a graphical control rather than text, keeps the white/AccentInksplit its 3:1 non-text floor allows. The SDK theme's dark block is nowprimaryMain #6FB2FF · primaryLow #142238 · primaryHigh #DCE9FC · onPrimary #142238, the nav indicator is the accent at a posed 15% alpha instead of a hardcoded tint hex, and every remaining tealColorSchemecontainer/tertiary/surface value (both themes) is now a navy tint instead. No wording or layout change. - iOS sample: Release now signs against a
productionpush entitlement. All three build configurations sharedRunner.entitlements, which declaresaps-environment = development, while an App Store provisioning profile carriesproduction— a mismatch on the configuration the TestFlight archive is built from. Release now points at a dedicatedRunner-Release.entitlements; Debug and Profile keep the development value, the same per-configuration split the Swift and React Native samples use. - Sample visual identity aligned with the shared cross-platform design
spec (cadrage report 24): the host-app footer notice is gone from every
screen (Home, Scenarios, each of the 21 scenario shells, Settings) — it
duplicated identity already conveyed elsewhere and fell under the shared
readability floor. The app label is now the attributive
Octopus Sample for Flutter(AndroidManifest.xml,Info.plist,MaterialApp.title), matching Flutter's own brand guidelines instead of juxtaposing the SDK name with the platform's. A "Flutter" platform badge now sits on the Home tab (slot hue#02569B, and its lighter pair#54C5F8in dark mode where the brand hue falls under the contrast floor; tonal — never the Flutter logo, which the brand guidelines reserve for in-app use only), and Settings carries the required plain-text attribution ("Built with Flutter™" + Google's trademark line), since no local asset for the official "Build with Flutter" lockup exists in this repo. - Fixed embedded SDK surfaces rendering a near-black CTA by default.
AppState.effectiveOctopusTheme()returnednulluntil the Theme scenario's presets were touched by hand, so every site embedding the SDK fell through to the SDK's own unthemed#141414primary — an omitted default, not a theming bug. It now falls back tobrandOctopusTheme(), matching what the Android sample'sDefaultbranch already does. The Theme scenario's Preset 1 is renamed "Brand theme" to reflect that it's no longer the no-theme oracle, and a new Preset 5 ("SDK default (no theme)") passes an explicit emptyOctopusThemeto keep that oracle reachable. buildAppTheme()no longer derives itsColorSchemeviaColorScheme.fromSeed. A seeded scheme rendered aprimarynobody chose, shifted with the Material/Flutter version, and left every role besidessecondaryunreviewed. It is now hand-built role-by-role, ported verbatim from the Android sample'sTheme.kt— the one complete, Figma-annotated source for this palette — with an explicit nav-bar accent (teal in light mode, salmon in dark: salmon on white measures ~2.1:1, under the 4.5:1 contrast floor) instead of the Material default. The unified SDK brand theme handed to the embedded SDK (primaryMain/primaryLow/primaryHigh/onPrimary) now matches the values shared with the Android sample instead of Flutter's own ad hoc set. The bottom nav itself moves fromBottomNavigationBar(Material 2) toNavigationBar(Material 3), same four tabs and order.- Sample shell reduced to 4 tabs (Home / Scenarios / Community /
Settings): the Debug tab is gone, replaced by a modal opened from Settings
(
debug-open-button), matching the native samples' navigation. Settings also gains a visually separated danger zone — "Back to Config" (non-destructive) in its own card, away from "Reset Configuration" (destructive, error-tinted icon and title, and which now fully stops the SDK session before clearing local state). - The Server field on the Config and Settings screens is read-only and now
shows the resolved host (e.g.
Resolved host: … (set by --dart-define=OCTOPUS_API_HOST)) instead of an editable value. settings-version-labelnow reads the installed build's real version and build number viapackage_info_plus, replacing a hand-kept string constant that had no automated link topubspec.yamland could silently drift from it.- The production-environment warning banner (
env-warning-banner) is now visible to accessibility tooling, so on-device QA passes (uiautomator / XCUITest) can assert its presence. It always rendered correctly but never reached the accessibility tree at all: it was painted before the Navigator, whose route machinery blocks the semantics of everything painted before it. The app builder now paints it after the Navigator while keeping it visually on top, and the banner carries an accessibility label; a widget test locks the semantics node's presence.
1.13.1 #
New Features #
-
Three internal test affordances bridged from the native SDKs, for QA scenarios only — not part of the supported public API and may change or be removed at any time:
OctopusSDK.debugOverrideProfileFieldsLock,debugOverrideContentOptions,debugOverrideTermsAcceptanceMode, with newProfileFieldsLock,ContentOptions(+PostOptions/CommentOptions/ReplyOptions), andTermsAcceptanceModeDart types mirroring the native models. Passingnullclears the override and falls back to the backend-provided config, matching both natives. Android, and iOS via CocoaPods; not yet on iOS via Swift Package Manager. The three nativedebugOverride*entry points live onOctopusSDKin theOctopusmodule (reachable on both iOS integration paths), but the three parameter types they construct (ProfileFieldsLock,ContentOptions,TermsAcceptanceMode) live inoctopus-sdk-swift'sOctopusCoretarget. CocoaPods has no concept of a package "product": this plugin declaresOctopusCommunityCoreas a pod dependency and importsOctopusCoredirectly, so the CocoaPods path is fully implemented. Swift Package Manager only exposes a dependency's declaredproducts, andOctopusCoreis not one ofoctopus-sdk-swift's SPM products — so on the SPM path these three calls resolve with anUNSUPPORTED_PLATFORMPlatformExceptionuntil upstream exposes the type another way. -
OctopusThemegains two optional color overrides,backgroundandlink, wired end-to-end to the native Android and iOS color schemes (background: the community screens' background color;link: the color of clickable links in posts and comments). Both default tonull, which keeps the native SDK's own default — no behavior change for existing hosts. -
OctopusThemegainsfontFamilyandfontWeight, applied to every SDK text style (titles, body, captions) and to the top app bar / navigation bar title.fontWeightis a plain 100-900 Int (no native setup needed).fontFamilyis not read from the Flutter asset bundle — it must additionally be registered natively with the exact same name: as a font resource underandroid/app/src/main/res/font/on Android, and as a PostScript name declared underUIAppFontsinios/Runner/Info.pliston iOS. An unregistered name is logged as a warning on both platforms and falls back to the SDK's default font rather than being silently ignored. Both fields default tonull— no behavior change for existing hosts. Note the navigation-bar scope differs slightly by platform: on Android it restyles only the top app bar's title text (back/close are icon-only, with no font to override); on iOS it also affects the navigation bar's icon buttons.fontWeightmust be within 100-900 — theOctopusThemeconstructor now asserts it, since an out-of-range value used to reach Android as a crash. -
OctopusTheme.fontSizeNavBarItem— font size for navigation-bar items. iOS only: the native iOS theme has a dedicatednavBarItemfont slot and the native Android typography has no counterpart, so the Android bridge documents the key as ignored rather than approximating it by resizing another slot.nullkeeps the previous behaviour (nav-bar items followfontSizeBody1). Reaches both theme consumption paths — the embeddedPlatformViewand the standalone create-post screen — so it applies toshowOctopusCreatePostScreentoo, not only to the embedded community view. -
Member-scoped entry points: open one member's posts or profile directly. Two additive
OctopusInitialScreencases, plus a dedicated widget, all reaching the same native screens the SDK already opens when a user taps a member inside the community:OctopusInitialScreen.activity(ActivityScreenInfo.clientUserId(id))/.activity(ActivityScreenInfo.profileId(id))— that member's posts-only activity screen. The two named constructors are mutually exclusive and take different paths natively:clientUserIdis resolved through the client-user-id lookup, so it needs a community that exposes client user ids, whileprofileIdis already resolved and opens with no lookup. An id that does not resolve shows the empty state; it never falls back to another member. Pointed at the connected user's own id, the natives open their two-tab activity screen instead.OctopusInitialScreen.profile({String? clientUserId})— that member's read-only Octopus profile; omit the id (or passnull) for the connected user's own, editable profile, whereonModifyUserfires your edit page.OctopusProfileScreen({String? clientUserId, …})— the standalone shorthand for the case above, same shape as the existingOctopusPostDetailsScreen/OctopusGroupDetailsScreenwidgets.
These entry points open on the screen's own default tab; there is no way to preselect another one.
Member ids are trimmed, so surrounding whitespace never reaches the lookup, and an id left blank or whitespace-only counts as no id at all:
OctopusInitialScreen.profilethen opens the connected user's own profile, exactly as omitting it does, while.activity— which has no own-user form — opens the main feed.This is the other half of Unified Profile: once your app intercepts every profile tap with
onNavigateToProfileand renders its own page, these are the entry points that let it hand the user back to the SDK on purpose.
Known Issues #
- Setting
OctopusTheme.fontFamilyorOctopusTheme.fontWeightalso sets the Android navigation bar title to the size offontSizeBody1, which is smaller than the title size applied when no font override is set.fontSizeBody1controls the resulting size. Kept as-is in this release.
Fixed #
-
iOS: an unset theme slot no longer overrides the native default. Both iOS theme builders substituted a value for anything the host left unset, so a partial
OctopusThemesilently redefined the rest of the theme:- Colors were substituted with
systemBlue/white, replacing the SDK's adaptive primary palette with a fixed blue. - The six font sizes were substituted with
26 / 20 / 17 / 14 / 12 / 10points — numbers that were never the SDK's own defaults (26 / 22 / 18 / 16 / 14 / 12), and that are fixed sizes where the native defaults areUIFontMetrics-scaled. A host setting any theme key therefore got community text both mis-sized and frozen against the reader's Dynamic Type setting.
An unset color or font size now keeps the SDK's own default instead of being substituted — the real default font sizes are
26 / 22 / 18 / 16 / 14 / 12.This mattered immediately for the three keys above, since each of them alone now builds a theme. It also changes the pre-existing
themeMode-only path: a theme carrying nothing butthemeModeused to yield a blue primary and wrapper-invented type sizes, and now keeps the SDK's palette and type scale. - Colors were substituted with
-
iOS: the same six wrong sizes were still reachable through
fontFamily/fontWeight. A custom family or weight needs a concrete point size even for a slot whose size the host left unset, and the sizes used for that were the same26 / 20 / 17 / 14 / 12 / 10— so a host setting onlyfontFamilystill got five of six slots mis-sized. They now repeat the natives' own scale (26 / 22 / 18 / 16 / 14 / 12, plus 17 fornavBarItem, the base size of the.bodystyle its native default resolves to). -
iOS: an explicit font size now follows the reader's text-size setting, as the Android bridge's
<size>.spalways did and as the iOS SDK's own defaults do.Font.system(size:)renders a fixed point size, so setting any size at all used to opt that slot out of Dynamic Type on iOS only — on the very keys a host reaches for to make text bigger. Every size the bridge resolves, explicit or reference, is now run throughUIFontMetrics(forTextStyle:).scaledValue(for:)with the slot's own text style, and the custom-family path usesFont.custom(_:fixedSize:)so the value is scaled once rather than twice. A size set here is a base size on both platforms, not a frozen one.
Example App #
- The Initial-screen scenario gains four presets for the new entry points —
member activity by
clientUserIdand byprofileId(resolving one withfetchCommunityDatawhen the field is left empty, so it runs in one tap), plusOctopusProfileScreenwith and without an id. Both member id fields fall back to the connected user's own id. - The host-rendered profile page (
ClientProfilePage, the pageonNavigateToProfilepushes) now ends with those same member entry points, so the round trip host page → back into the SDK is exercised from the place a real host actually needs it. - New "Create Post (Bridge Share)" scenario. The Scenarios tab now has a dedicated screen for opening the post editor prefilled with a payload the host app builds: five one-tap presets cover text only, text + call-to-action, text + image, the full payload and image only, over an editable form (text, CTA label and url, target group picked from the live groups stream, bundled image). An invalid payload — text out of bounds, half-filled CTA — is reported in the result panel with the concrete rejection instead of opening the editor. The Initial Screen scenario still covers the editor mounted inside a host route; this one presents it as a full screen.
- The reactions scenario walks the capability instead of the enum. Its first three presets are now react, change the reaction, then unreact — the sequence a tester needs to see — with the remaining reaction kinds moved to presets 4 through 7. Renamed from "Set Reaction" to "Reactions", and its automation identifiers now match the shared QA catalog verbatim.
Documentation #
PARITY_MATRIX.mdrewritten against native 1.13.2: 6 real gaps from 2 root causes, replacing a 2026-05-28 table whose 53 "missing" APIs were ~46 stale. It now carries its anchors, the genuinely-N/A and Android-only-by-design verdicts, and a section on how to regenerate it — a matrix nobody can regenerate is the defect being fixed.PARITY_VERIFICATION_2026-05-29.mdandREFACTOR_PLAN.md§4bis are marked superseded instead of contradicting it.- The install snippet in
README.mdwas still^1.12.3after 1.13.0 was published, and the one indoc/push-notifications-with-firebase.mdhad been^1.11.0since that release. Both now read^1.13.0, and the coherence guard checks every doc's snippet against the package version so a release cannot ship a snippet naming an older version again. example/README.mdnow states the Android prerequisite up front: the sample applies the Google Services Gradle plugin for push, so every Android build fails without agoogle-services.json, which is per-project and gitignored. The step was only documented in a nested notifications README.
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.