Skip to content

MOBILE-556: Report three outcomes with a reason, and let the block start hidden, with a placeholder, or by the place's memory - #219

Merged
Vailence merged 8 commits into
mission/storiesfrom
feature/MOBILE-556
Oct 6, 2026
Merged

Vailence merged 8 commits into
mission/storiesfrom
feature/MOBILE-556

Conversation

@Vailence

@Vailence Vailence commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Problem

MindboxEmbeddedBlock reported two outcomes, onLoad and onFail, and always took its space up front behind a shimmer. The native blocks have moved on: iOS (MOBILE-504/516) and Android (MOBILE-505/517) report three outcomes — onLoad, onEmpty, onFail(reason) — and take a loadingStrategy with a per-place memory and an animatesReveal flag. A Flutter host could not tell an empty place from a breakage, and every place with no campaign behind it flashed reserved space on every launch.

Change

Three outcomes (af19de7). onEmpty is new; onFail now takes a MindboxEmbeddedBlockFailReason — a string-backed value (networkError, internalError) rather than an enum, so a later SDK can add a reason without breaking an exhaustive switch. The wire carries the native word unchanged. Deduplication stays by outcome, as in the native blocks: a retry that fails for another reason is not delivered again. Breaking for a host on mission/stories that wrote onFail: () {}; nothing has shipped with the old signature.

Loading strategy and reveal (67ce7ce). loadingStrategy (automatic by default, placeholder, hidden) and animatesReveal go down as creation params, fixed for the block's life like timeout. The first look is decided in Dart for placeholder and hidden; for automatic it depends on the SDK's memory of the place, so the widget asks the plugin channel …/embedded_block/plugin initialAppearance and starts collapsed until it answers — a place that never showed content must not flash reserved space. The native block's own report outranks a late answer. The widget lays the block out as a zero-height clipped slot over a block of full height (ClipRect + OverflowBox): Android never creates a platform view sized to nothing, so a block that waited hidden would otherwise never load. The growth 0→height is the wrapper's animation on every platform; the native block reports animated and revealDurationMs with the look (isRevealAnimated, REVEAL_ANIMATION_DURATION_MS on Android; @_spi(Internal) isRevealAnimated, revealAnimationDuration on iOS from mindbox-cloud/ios-sdk#783), so the gates — animatesReveal, Reduce Motion, "only the arrival of content is a reveal" — live in one place, the view, and the Dart side holds no constants.

Padded names (c770dd7). The warning about whitespace around placeSystemName goes: both native SDKs trim the name since MOBILE-419 / MOBILE-454, so the Dart doc now says so instead.

Verification

Dart: platform interface 55 tests, mindbox 78 tests (new groups: strategy and flag, first look, reveal, outcome), analyzer clean through to-local-dependensies.sh. Both native halves compiled and checked by hand against local SDK builds (ios-sdk feature/MOBILE-556, android-sdk mission/stories) with the Pushok demo: all five strategy cases on the outcome screen, the loading strategy scene — automatic with and without the place's memory, hidden growing from zero with the SDK's animation and landing at once with animatesReveal: false, onFail(networkError) with and without errorBuilder, onEmpty on an unknown place. Logs confirm the first look on the native side (starts with hidden, taking no space (strategy automatic)) and the memory being written on content.

Blocked on

The plugin is pinned to the native 2.16.0-rc tags (build.gradle, Package.swift, podspec), and those tags predate the API: the Kotlin half overrides onFail(view) without reason and does not compile, the Swift half implements the delegate without reason and silently stops receiving onFail. Dart CI is green regardless; the native halves need the next native release and a pin bump in a follow-up. Do not merge before that.

Demo: mindbox-cloud flutter-app feature/MOBILE-556 (GitLab) — the outcome screen, the loading strategy scene and a mock server for both platforms.

🤖 Generated with Claude Code

Vailence added 3 commits October 1, 2026 15:38
… onFail(reason)

The native blocks report three outcomes since MOBILE-504 (iOS) and MOBILE-505
(Android): the content is shown, the place has nothing to show, or the block
could not be shown, with a reason. The widget now reports the same three.

`onEmpty` is new and carries no reason. `onFail` takes a
`MindboxEmbeddedBlockFailReason` — a string-backed class with `networkError`
and `internalError`, the same raw values as on the native side, open to words
a later SDK adds. The report on the per-view channel carries the reason under
`reason`, and both native halves implement the new listener and delegate
methods: the Android one no longer compiles against the old `onFail(view)`,
the iOS one would otherwise keep compiling against a protocol whose default
implementation silently swallows the failure.

Outcomes are still deduplicated by kind: a failure that repeats with another
reason is the same outcome. A platform without a native block reports
`internalError`.
… place's memory, and reveal with the SDK's animation

The native blocks take a loading strategy since MOBILE-516 (iOS) and
MOBILE-517 (Android): `automatic` — the default — keeps the block hidden
until the place has shown content once on this device and puts a
placeholder there from then on, `placeholder` takes the space up front,
`hidden` never takes it before the content. Content is revealed with the
SDK's own animation unless `animatesReveal` is off. The widget takes both,
fixed at creation like `timeout`, and hands them to the native block.

Three things the Dart side has to do itself:

The first look. Both natives decide it synchronously, before the container
exists, through a static `initialAppearance`; Dart cannot reach anything
synchronously. `placeholder` and `hidden` are decided from the strategy
alone. `automatic` asks the plugin on a channel shared by every block
(`.../embedded_block/plugin`, method `initialAppearance`) and takes no
space until it answers: a place that never showed content must not flash
reserved space, and one that has gets its placeholder a frame late rather
than a frame early. The native block's own report, once the platform view
exists, outranks a later answer.

The slot. A block that waits hidden used to be impossible on Android: the
engine never creates a platform view sized to nothing, so the block would
never load and never grow. The layout now gives the slot — 0 for a
collapsed block — and the block inside keeps its full height under a
ClipRect, so it runs its whole cycle unseen, as the native blocks do.

The growth. The container fades the content in on its own; the growth of a
block that waited hidden is the wrapper's, as in SwiftUI and Compose. The
native block says whether the change is the animated reveal — it owns the
gates: animatesReveal, Reduce Motion, "only the arrival of content" — and
how long it takes (`animated` and `revealDurationMs` in the report; Android's
isRevealAnimated and REVEAL_ANIMATION_DURATION_MS, the same two added to iOS
as @_spi in ios-sdk feature/MOBILE-556). The slot then grows from 0 to the
height over that duration; everything else lands at once.
The widget used to say in the log that a padded name "is used as it is, so
it will not match the place", because the native blocks once matched the
name byte for byte. They no longer do: iOS strips the whitespace around
the name since MOBILE-419 ("in sync with Android"), and Android's PlaceKey
trims it too. A name pasted from the admin panel with a stray space finds
its place on both, so the warning was wrong and the doc with it.

The widget still passes the name down as given — normalizing is the SDK's
business, not the wrapper's — and the test now pins that, with the log
staying quiet.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The documented native SDK pin incompatibilities block Android compilation and iOS failure callback delivery.

Review effort: Balanced
Findings: 2 High severity

Open (2)
What changed in this PR

Aligns Flutter embedded blocks with native SDK outcomes, loading strategies, and reveal behavior.

Changes:

  • Adds onEmpty and reason-bearing onFail.
  • Adds loading strategies, initial-appearance lookup, and animated reveal.
  • Updates public documentation and Dart tests.
File Description
mindbox/​test/​embedded_block_test.dart Tests outcomes, loading strategies, and reveal.
mindbox/​test/​embedded_block_fail_reason_test.dart Tests failure-reason values and equality.
mindbox/​README.md Documents callbacks and loading options.
mindbox/​lib/​src/​embedded_block.dart Implements callbacks, initial appearance, and reveal.
mindbox/​lib/​src/​embedded_block_loading_strategy.dart Defines loading strategies.
mindbox/​lib/​src/​embedded_block_fail_reason.dart Defines string-backed failure reasons.
mindbox/​lib/​mindbox.dart Exports new public types.
mindbox/​CHANGELOG.md Records embedded-block enhancements.
mindbox_platform_interface/​test/​src/​embedded_block_test.dart Tests channel constants and report parsing.
mindbox_platform_interface/​lib/​src/​embedded_block.dart Extends the channel protocol.
mindbox_ios/​ios/​mindbox_ios/​Sources/​mindbox_ios/​MindboxIosPlugin.swift Registers initial-appearance channel.
mindbox_ios/​ios/​mindbox_ios/​Sources/​mindbox_ios/​EmbeddedBlockPlatformView.swift Bridges new iOS block APIs.
mindbox_android/​android/​src/​main/​kotlin/​cloud/​mindbox/​mindbox_android/​MindboxAndroidPlugin.kt Manages initial-appearance channel lifecycle.
mindbox_android/​android/​src/​main/​kotlin/​cloud/​mindbox/​mindbox_android/​EmbeddedBlockPlatformView.kt Bridges new Android block APIs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@justSmK justSmK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Первый круг. Прогнал тесты: mindbox_platform_interface 55, mindbox 78, анализатор чистый. Ключевые тесты краснеют на своих поломках, проверил мутациями: 7 из 9. Одна зелёная эквивалентна коду, про вторую — в комментарии к тесту. Нативные половины не собирал, сигнатуры сверил с ios-sdk #783 и android-sdk mission/stories — сходится, включая делегат с reason:.

Нужны две правки. Свой placeholder хоста пропадает рывком, когда натив проявляет контент. Доки errorBuilder и onFail обещают экран ошибки, которого при стратегии по умолчанию не будет. Остальное в комментариях — мелочи.

Вне кода:

  • В доке класса осталось «Both outcomes can be customized», а README уже говорит «Both looks».
  • CHANGELOG mindbox_platform_interface, mindbox_android и mindbox_ios молчат про канал plugin, исход empty, reason и поля раскрытия.
  • Пины и ios-sdk #783 — как в описании PR, не повторяю.

Comment thread mindbox/lib/src/embedded_block.dart
Comment thread mindbox/lib/src/embedded_block.dart
Comment thread mindbox/README.md Outdated
Comment thread mindbox/lib/src/embedded_block.dart Outdated
Comment thread mindbox/lib/src/embedded_block_fail_reason.dart Outdated
Comment thread mindbox/lib/src/embedded_block.dart
Comment thread mindbox/test/embedded_block_test.dart
Comment thread mindbox/lib/src/embedded_block.dart
Vailence added 3 commits October 5, 2026 22:51
…fades in

A host's placeholder or error screen is drawn by the widget, and the native
block's own layer under it is a transparent stand-in. When the content arrived
with the SDK's reveal the widget dropped its layer on the first frame while
the native block was still fading the content in from transparent: for most
of the 250 ms there was nothing to see but the screen's background. SwiftUI
writes the look inside withAnimation and Compose keeps the host's placeholder
inside the block, so neither shows the gap.

The widget now keeps the layer the content replaces for as long as the native
block says the reveal runs, fading it out under IgnorePointer, and a look that
arrives mid-fade takes it down at once. The slot and the fade run on the
curve the native reveal uses: the standard Material curve on Android,
ease-in-out on iOS.

Along the way, from the review: a height that reserves no space builds no
native block on iOS either, as the log already claimed; a creation value
changed after creation is said out loud once per value given, as Compose says
it; and the docs say that a block waiting hidden builds neither placeholder
nor errorBuilder before its first content, that onLoad on such a block finds
it still growing, and that errorBuilder does not apply to the default's first
wait on a fresh install.

Tests: the reveal is reported with a duration that is not the SDK's default,
so a hardcoded 250 would show; an error screen or a placeholder arriving
mid-growth is pinned to take the full height at once; the fade of a host
placeholder and of an error screen, their removal on a look arriving mid-fade,
and the instant drop without the native block's word are covered.
The nominal 250 ms went down the channel, but the native fade is a
ValueAnimator and the system multiplies its duration by the animator scale
from the developer settings; a Flutter animation knows nothing of it, so at
0.5x or 2x the slot and the content came apart. The platform view now sends
the nominal duration times Settings.Global.ANIMATOR_DURATION_SCALE, which is
what the content actually takes.
… changelogs

The README and the strategy's doc promised that the layout does not jump,
while an automatic block at a place with memory stands at zero for a frame
and takes its height when the plugin answers; both now say so, as the doc of
`automatic` already did. The README example names `loadingStrategy:
placeholder` and says that a block waiting hidden builds neither placeholder
nor errorBuilder, so a host copying it gets the error screen it reads about.
The reason doc marks the targeting fetch as iOS-only: Android answers that
failure with onEmpty. The changelogs of the platform interface and the two
native halves record the plugin channel, the `empty` outcome, the reason and
the reveal fields.
@Vailence

Vailence commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Первый круг отработан тремя коммитами: fcf391e (затухание слоя хоста под проявлением, остановка роста, нулевая высота на iOS, предупреждение раз на значение, доки виджета), 36db10a (эффективная длительность с Android), 34eaf27 (README, шапка enum, причина сбоя, CHANGELOG mindbox_platform_interface, mindbox_android, mindbox_ios). Dart: 55 + 84 тестов, анализатор чистый; Kotlin собран против локальной сборки mission/stories. Пины и ios-sdk #783 без изменений, как в описании.

@justSmK justSmK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Остальное поправлено, спасибо. Осталась одна правка, она в треде про высоту.

Vailence added 2 commits October 6, 2026 15:02
fcf391e built no native block for a height that reserves no space, but
checked the height on every build: a host taking a live block through
104 → 0 → 104 lost the platform view on the way down and got a new one on
the way up, page reloaded and all, against the promise of a live `height`
with no reload. The check now holds only until the block has had a height
once. Two tests: a block created with no height builds nothing; a live block
passing through zero keeps the one native block it has.
…pin two fade cases

The warning for a height that reserves no space said "nothing loads", which
stopped being true once a live block kept its native view through zero: a
block created at 0 and given a height later does load. The log now says so.

Two cases of the host-layer fade were open to a mutation: a reveal that
names no duration must drop the host's placeholder at once rather than
fade it, and a tap during the fade must go past the fading placeholder to
the content under it. Both are pinned; the second through a hit test, with
the test placeholder painted so it takes touches the way a real one does.
justSmK

This comment was marked as spam.

@Vailence
Vailence requested a review from justSmK October 6, 2026 10:53
@Vailence
Vailence merged commit 7a98ee8 into mission/stories Oct 6, 2026
8 checks passed
@Vailence
Vailence deleted the feature/MOBILE-556 branch October 6, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants