Repository navigation
MOBILE-557: Три исхода, стратегия загрузки и анимация появления встроенного блока - #224
Conversation
… 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 component now reports the same three. `onEmpty` is new and carries no reason. `onFail` takes a `MindboxEmbeddedBlockFailReason` — a string with the `networkError` and `internalError` constants, the same raw values as on the native side, open to words a later SDK adds. The two outcome events of the native component are folded into one, `onBlockOutcome`, carrying the outcome word and the reason; 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 collapses and reports `internalError`, which the component did not do before. The empty `MindboxEmbeddedBlockFailure` placeholder type goes: the reason is what it was reserved for.
… 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 component takes both, fixed at creation like `timeoutMs`, and hands them to the native block. Three things the JS side has to do itself: The first look. Both natives decide it synchronously, before the container exists, through a static `initialAppearance`; JS cannot reach anything synchronously. `placeholder` and `hidden` are decided from the strategy alone. `automatic` asks the `MindboxSdk` native module — the one module the package already has, on both renderers — through a new `embeddedBlockInitialAppearance` method that the JS side does not export, 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 native view exists, outranks a later answer. The slot. A block that waits hidden used to be impossible: both native halves build the SDK container only once their view has a frame, and the native view sat in a slot of zero. The layout now gives the slot — 0 for a collapsed block — and the block inside keeps its full height under the slot's clip, 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, Compose and Flutter. 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` on the appearance event; 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, an Animated value on the JS side; every other change lands at once.
…ck's to ignore iOS strips the whitespace since MOBILE-419, and Android's PlaceKey trims it too, so a name pasted with a stray space finds its place on both. The component never trimmed or warned about it — the test that the name reaches the native block as given stands — and its doc now says so the way the Flutter widget does, rather than describing what the SDK does with it.
…rst look An `automatic` block asked the `MindboxSdk` module what it starts with — the place's memory, read through the native `initialAppearance` — because the native view was believed to stay unbuilt while the slot is collapsed. It does not: the view inside the slot keeps the block's full height, both native halves build the block as soon as they have a frame, and the block reports its look the moment it is observed. That report reaches JS in the same mount, no later than the module's promise could, with the same word read from the same memory; `hasHeardFromNative` existed only to throw the module's answer away. The channel goes — the TS module, the `@ReactMethod` and the `@objc` method with its `RCT_EXTERN_METHOD`, the SPI import the Swift module needed for it — and the first-look tests drive the native report instead of a mocked promise.
…rst look is not lost
`MindboxEmbeddedBlockHost` subscribed to the block inside `init`, and the SDK
answers a subscription at once with the current look — for an `automatic`
block, the place's memory. `onAppearance` was assigned by the component view
only after `init` returned, so that first report went to nobody. JS learned
the look only through the `.loading` the provider re-emits when the block
enters a window and starts, and a block mounted with `active={false}` did not
start: its remembered placeholder waited for the screen to become active,
while Android, whose emitters are wired before the block exists, showed it at
once.
Both callbacks are now parameters of the host's initializer and non-optional
`let`s, with `isTornDown` as the one guard after teardown; the observer is set
last in `init`. Both renderers pass their blocks in: under Fabric the event
emitter is set before `finalizeUpdates:`, under Paper the event blocks are
props set before `didSetProps:` and any layout pass.
With the emitter always in place when the host is built, the pending-event
buffers of the Fabric view had no path left; they are gone, and the emit
methods keep a plain early return that states the invariant.
…BILE-557 # Conflicts: # CHANGELOG.md
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Declared native dependencies lack required APIs, and Android event coalescing can discard outcome and appearance transitions.
Review effort: Balanced
Findings: 2
Open (4)
What changed in this PR
Aligns MindboxEmbeddedBlock with the native SDK APIs for outcomes, loading strategies, and reveal animations.
Changes:
- Adds three outcome callbacks and failure reasons.
- Bridges loading strategies and native-controlled reveal animations.
- Fixes initial iOS appearance delivery and expands tests and documentation.
| File | Description |
|---|---|
| src/MindboxEmbeddedBlockNativeComponent.ts | Updates native props and event payloads. |
| src/MindboxEmbeddedBlockLoadingStrategy.ts | Defines loading strategies. |
| src/MindboxEmbeddedBlockFailReason.ts | Defines extensible failure reasons. |
| src/MindboxEmbeddedBlock.tsx | Implements callbacks, strategies, and animated height. |
| src/index.tsx | Exports the updated public API. |
| src/__tests__/MindboxEmbeddedBlock.unsupported.test.tsx | Tests unsupported-platform behavior. |
| src/__tests__/MindboxEmbeddedBlock.test.tsx | Tests outcomes, strategies, and reveal behavior. |
| README.md | Documents the updated component API. |
| ios/EmbeddedBlock/MindboxEmbeddedBlockView.mm | Bridges new props and preserves initial appearance delivery. |
| ios/EmbeddedBlock/MindboxEmbeddedBlockHost.swift | Integrates updated native callbacks and configuration. |
| ios/EmbeddedBlock/EmbeddedBlockWire.swift | Maps iOS values to bridge payloads. |
| CHANGELOG.md | Records embedded-block features. |
| android/src/main/java/com/mindboxsdk/embedded/MindboxEmbeddedBlockViewManager.kt | Bridges new props and events. |
| android/src/main/java/com/mindboxsdk/embedded/MindboxEmbeddedBlockHostView.kt | Integrates native strategies and outcomes. |
| android/src/main/java/com/mindboxsdk/embedded/MindboxEmbeddedBlockEvents.kt | Adds outcome and animation payloads. |
| android/src/main/java/com/mindboxsdk/embedded/EmbeddedBlockWire.kt | Maps Android values to bridge payloads. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ck out of coalescing React Native's `Event` coalesces by default: same-named events of one view that are still queued when a frame is dispatched are folded into the last one, on Paper in `EventDispatcherImpl` and under Fabric through the `canCoalesce` flag handed to the emitter. The JS side reads every change — a reveal is content arriving where a placeholder or nothing stood, and every changed outcome is promised to the host — so a report folded into the next one would be a report lost. iOS never coalesced these: direct events and the generated Fabric emitter dispatch each one.
justSmK
left a comment
There was a problem hiding this comment.
Первый круг. Прогнал: jest 76, eslint чистый, tsc по src/ чистый — ошибки только в example/, его PR не трогает. Ключевые тесты краснеют на своих поломках, проверил мутациями: 7 из 8, оставшаяся эквивалентна коду. Нативные половины не собирал, сигнатуры сверил с ios-sdk #783 и android-sdk mission/stories. Починку порядка в iOS-хосте и удаление _pending* проверил по RCTMountingManager — верно.
Нужны три правки. Свой placeholder пропадает рывком, когда натив проявляет контент. Пустое имя обещает onFail, а приходит onEmpty. CHANGELOG молчит о ломающих изменениях для тех, кто уже на 2.16.0-rc. Остальное в комментариях — мелочи.
Вне кода:
- В задаче MOBILE-557 раздел «Имплементация» описывает
embeddedBlockInitialAppearance, а метод удалён в 122511f. - Дока
errorмолчит, что добавленный позжеerrorне раскрывает уже схлопнутый блок. Так было и до PR, во Flutter это написано. - Пункт DoD про сцену в Пушке и видео — в react-native-app !89, здесь не смотрел.
| setAppearance(next) | ||
| if (opens && revealDurationMs != null && revealDurationMs > 0) { | ||
| reveal.setValue(0) | ||
| Animated.timing(reveal, { toValue: 1, duration: revealDurationMs, easing: Easing.inOut(Easing.ease), useNativeDriver: false }).start() |
There was a problem hiding this comment.
Мелочь, про Android. Хост шлёт номинальные 250 мс (MindboxEmbeddedBlockHostView.kt:198). Нативный fade — это ValueAnimator, и система умножает его на множитель анимаций из настроек разработчика. Animated.timing этот множитель не видит. На 0.5x или 2x слот и контент разъезжаются.
Кривая тоже другая. На iOS Easing.inOut(Easing.ease) отличается от easeInOut не больше чем на 3%, там всё в порядке. На Android натив идёт по (0.4, 0, 0.2, 1).
Предлагаю на Android умножать длительность на ValueAnimator.getDurationScale() и брать Easing.bezier(0.4, 0, 0.2, 1). Чище, если натив сам отдаст действующую длительность — тогда Flutter получит это без своих правок.
There was a problem hiding this comment.
Кривую поправил в ac4b92a: на Android слот и оверлей идут по Easing.bezier(0.4, 0, 0.2, 1), на iOS остаётся inOut(ease). Множитель длительности здесь не трогаю: ValueAnimator.getDurationScale() публичен только с API 33, ниже нужна рефлексия или Settings.Global, и это дублирование того, что натив уже знает. Согласен с вашим вторым вариантом — пусть MindboxEmbeddedBlockView отдаёт действующую длительность, тогда RN и Flutter получат её одним местом. Заведу задачей в android-sdk, здесь номинальные 250 мс остаются до неё.
…by, on the native curve The native block fades its content in from transparent, and the stand-in the host's placeholder or error screen is drawn over is clear. The overlay went away in the frame the look changed, so for the length of the fade the screen's background showed through where the placeholder had been. SwiftUI writes the look inside `withAnimation` and Compose fades its stand-in under the content; here the overlay lies above the native view, so it stays and fades out over the same duration, touches passing through to the content meanwhile. Any other change takes it away at once, a fade interrupted included. The slot's growth and this fade now follow the native curve per platform: Material's standard curve on Android, ease-in-out on iOS. Tests cover the fade, its interruption, the opaque placeholder of a reload after it, and the cell the old reveal test missed — a placeholder or an error screen arriving mid-growth takes the full height at once and stops the growth. A nameless place is also told right: both native blocks report it empty, so the docs, the warning and the test say `onEmpty`, not `onFail`.
…rror, and where the layout moves The docs promised an `error` screen and a layout that does not jump; neither is true for a block that waits hidden, which under the default strategy is every place on a fresh install. Such a block draws neither `placeholder` nor `error` until its content has been shown once — a failure keeps it collapsed and only `onFail` tells — and an `automatic` block at a remembered place takes its space a frame after the mount, not from the first one. `onLoad` of a block that waited hidden comes while it is still growing from zero. The targeting data a `networkError` may stand for is fetched that way on iOS only.
…r the release workflow expects `mindbox-sdk@2.16.0-rc` is on npm under the `rc` tag with `onFail(failure)`, the `MindboxEmbeddedBlockFailure` type, a placeholder from the first frame and a README that hid the stories section in `onFail`. After this release an empty place goes to `onEmpty`, so that section stays, and the compiler says nothing about it. The release workflow inserts `## [Unreleased]` with brackets; the header without them would have left two sections.
… name is known Fabric sets props in no fixed order: a strategy word arriving before the name was logged against place 'null'. The word is kept and logged when the block is built, next to the empty-name log, as the iOS host does.
justSmK
left a comment
There was a problem hiding this comment.
Всё поправлено, спасибо. Тред про длительность на Android закрою, когда появится задача в android-sdk. Мержить после релиза нативов и подъёма пинов.


Зачем
Довести
MindboxEmbeddedBlockдо API нативных блоков из MOBILE-516 / MOBILE-556: логика уже готова в iOS- и Android-SDK, задача React Native — провести её в кроссплатформу без своей интерпретации.Что меняется
Три исхода вместо двух.
onLoad,onEmptyиonFail(reason)— те же, что у SwiftUI, Compose и Flutter. Пустое место больше не маскируется под ошибку. Причина — строка изMindboxEmbeddedBlockFailReason(networkError,internalError), для логов и аналитики, с запасом на новые значения. Нативные половины шлют один ивентonBlockOutcomeс полемreason, вместо двух пустых.loadingStrategyиanimatesReveal.automatic(по умолчанию) /placeholder/hidden, фиксируются при создании блока, какtimeoutMs. Блок, ждавший скрытым, раскрывается на высоту за время анимации SDK: нативный блок сам решает, анимировать ли это раскрытие (isRevealAnimated, все гейты у него), JS только анимирует высоту слота по его слову. Нативная вьюха живёт внутри обёртки полной высоты под обрезанным слотом, иначе блок нулевого размера никогда бы не построился.Первый вид
automatic-блока отдаёт сам нативный блок. Отдельный каналembeddedBlockInitialAppearanceчерез модульMindboxSdkубран: нативная вьюха строится сразу, как только у неё есть фрейм, и отчёт о стартовом виде доходит до JS в том же маунте, не позже, чем мог бы ответ модуля. Из-за этого всплыла и починена ошибка в iOS-хосте: он подписывался на блок вinit, когдаonAppearanceещё не был присвоен, и стартовый отчёт терялся — до JS он доходил только побочно, через повторный.loadingпри старте контента, а блок сactive={false}при маунте не резервировал плейсхолдер до активации экрана. Теперь оба замыкания идут параметрами вinitхоста, под Fabric и Paper. Заодно удалены недостижимые буферы_pending*во Fabric-вьюхе: эмиттер всегда установлен доfinalizeUpdates:.Пробелы вокруг имени места — забота нативного блока, компонент передаёт имя как есть и предупреждает только о пустом.
Как проверялось
yarn lint,yarn test— 76/76.MindboxSdkиз Pods-проекта приложения собран для симулятора против локальногоios-sdkна веткеfeature/MOBILE-556.android-sdkсorigin/mission/storiesчерезMINDBOX_ANDROID_SDK_VERSION.react-native-app(веткаfeature/MOBILE-557): все три исхода на локальном моке,automatic/placeholder/hiddenсо сбросом памяти места, анимация раскрытия.Открытые вопросы
Релиз только после нативов.
mission/storiesуже пинитMindboxиmobile-sdkна2.16.0-rc, но опубликованные теги2.16.0-rcобеих платформ ещё не содержат нужного API (isRevealAnimated,revealAnimationDuration,onEmpty,loadingStrategy): на iOS оно наfeature/MOBILE-556, на Android — наmission/storiesбез релиза. Без локальных сборок пакет не соберётся; пины надо выставить на реальные версии нативов при их выходе, и об этом стоит сказать в CHANGELOG.Нет нативного теста на доставку первого вида. JS-тесты эмулируют
onAppearanceChangeсами, так что порядок подписки в хосте ничем, кроме кода, не закреплён.🤖 Generated with Claude Code