MOBILE-324: added embedded block - #751
Conversation
Stands in for the backend contract (MOBILE-344) until it lands: the inlineBlocks config section, the block page bridge name and the page message protocol. Everything here is prefixed Temp on purpose — it is replaced wholesale once the real contract arrives and the block moves to the shared JS bridge, so nothing else should grow a dependency on these shapes.
Injects a hardcoded inlineBlocks section into the fetched mobile config and serves a mock feed page with switchable scenarios (success, empty, error, slow), so the block can be driven end to end before the backend sends anything. MUST NOT REACH develop. The injection is guarded by BuildConfig.DEBUG, so a release build never substitutes the staging page into a host app's config — verified on a release build.
A host marks a place by placeSystemName and gives it a fixed height; the mobile config decides what goes into it. MindboxEmbeddedBlockView is the whole public surface, with a Compose wrapper in the new mindbox-embedded-compose module. The block owns its behavior: visible while loading and while showing content, GONE when the place ends up without content unless the host set an error view. The listener only observes. Inside, the controller decides when to resolve, when to drop and when to give the page its 7s budget; the factory turns the config into one of three outcomes, telling apart 'nothing here' from 'config has not arrived yet' so a slow start-up never collapses a block; the webview layer keeps the untrusted page contained — https only, no navigation, a rate-limited bridge, and a dead renderer that no longer takes the host app down. Shared SDK code touched: - SessionStorageManager: listeners can now unsubscribe, and the list is copy-on-write since blocks subscribe from the main thread while the keepalive timer notifies from another. - MindboxEventManager/Event: the embeddedPlaceRequested signal. - InAppEventManagerImpl: that signal reaches the in-app pipeline, so a place can be filled by targeting once embedded in-apps exist. - Constants: the 7s WebView readiness budget, shared with the in-app holder that already used the same number.
128 tests over the block: the controller's decisions (resolve once, reload on a new session, wait for a config that has not arrived, budget only the page), the view's states and callbacks, the provider's reading of page heights, and the page hardening (https, navigation, bridge rate limit, dead renderer). Robolectric never raises a window out of GONE and the block starts its content only on a visible window, so the tests dispatch that callback themselves through Robolectric's own helper.
There was a problem hiding this comment.
Pull request overview
Adds embedded Mindbox blocks for Android Views and Compose, including WebView rendering, lifecycle handling, configuration lookup, and tests.
Changes:
- Introduces embedded-block APIs and Compose integration.
- Adds WebView messaging, targeting events, placeholders, and lifecycle management.
- Adds extensive unit tests and temporary mock configuration/content.
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
.editorconfig |
Allows PascalCase composables. |
build.gradle |
Adds Compose module coverage. |
gradle/libs.versions.toml |
Adds Compose and lifecycle dependencies. |
mindbox-embedded-compose/build.gradle |
Configures the Compose module. |
mindbox-embedded-compose/gradle.properties |
Defines artifact metadata. |
mindbox-embedded-compose/proguard-rules.pro |
Adds module ProGuard placeholder. |
mindbox-embedded-compose/src/main/java/cloud/mindbox/mobile_sdk/embedded/compose/MindboxEmbeddedBlock.kt |
Implements the Compose wrapper. |
mindbox-embedded-compose/src/test/java/cloud/mindbox/mobile_sdk/embedded/compose/MindboxEmbeddedBlockTest.kt |
Tests Compose behavior. |
mindbox-embedded-compose/src/test/java/cloud/mindbox/mobile_sdk/embedded/compose/WindowVisibility.kt |
Adds Robolectric visibility helper. |
modulesCommon.gradle |
Enables JVM interface defaults. |
sdk/build.gradle |
Exposes Compose artifact and lifecycle runtime. |
sdk/consumer-rules.pro |
Preserves JavaScript bridge methods. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedBlockContentController.kt |
Coordinates block resolution and lifecycle. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedBlockContentFactory.kt |
Builds block content from configuration. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedBlockDefaultViews.kt |
Implements the loading placeholder. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedBlockState.kt |
Defines block states. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedContentProvider.kt |
Defines the content-provider contract. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedContentResolution.kt |
Defines resolution outcomes. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/MindboxEmbeddedBlockListener.kt |
Adds the public listener API. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/MindboxEmbeddedBlockView.kt |
Implements the embedded block View. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/TempEmbeddedBlocksConfig.kt |
Parses temporary block configuration. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/mock/TempEmbeddedBlocksMockConfigSection.kt |
Injects temporary mock placements. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/mock/TempMindboxStoriesFeedMock.kt |
Exposes mock scenarios. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/mock/TempStoriesFeedMockPage.kt |
Provides mock feed HTML. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/webview/EmbeddedBlockPage.kt |
Defines the page abstraction. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/webview/EmbeddedBlockWebViewPage.kt |
Implements secure WebView rendering. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/webview/EmbeddedBlockWebViewProvider.kt |
Maps page events to block states. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/webview/TempEmbeddedBlockPageContract.kt |
Defines bridge constants. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/webview/TempEmbeddedBlockPageMessage.kt |
Parses bridge messages. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/SessionStorageManager.kt |
Adds removable thread-safe listeners. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImpl.kt |
Injects temporary embedded configuration. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppEventManagerImpl.kt |
Accepts embedded-place events. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt |
Reuses the shared WebView timeout. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/MindboxEventManager.kt |
Emits embedded-place requests. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Event.kt |
Defines the embedded request event. |
sdk/src/main/java/cloud/mindbox/mobile_sdk/utils/Constants.kt |
Centralizes WebView timeout. |
sdk/src/main/res/values-night/colors.xml |
Adds dark placeholder color. |
sdk/src/main/res/values/attrs.xml |
Adds the place-name XML attribute. |
sdk/src/main/res/values/colors.xml |
Adds light placeholder color. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedBlockContentControllerTest.kt |
Tests controller behavior. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/embedded/EmbeddedBlockContentFactoryTest.kt |
Tests configuration resolution. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/embedded/MindboxEmbeddedBlockViewLookupTest.kt |
Tests public configuration lookup. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/embedded/MindboxEmbeddedBlockViewTest.kt |
Tests View behavior and lifecycle. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/embedded/WindowVisibility.kt |
Adds SDK Robolectric helper. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/embedded/webview/EmbeddedBlockWebViewPageTest.kt |
Tests WebView security and messaging. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/embedded/webview/EmbeddedBlockWebViewProviderTest.kt |
Tests provider state handling. |
sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppEventManagerTest.kt |
Tests embedded event validation. |
settings.gradle |
Registers the Compose module. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 48 changed files in this pull request and generated no new comments.
Suppressed comments (2)
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/webview/EmbeddedBlockWebViewProvider.kt:42
- A failed page is paused when the failure is detected, but every later
start()resumes it before replayingFailed. After a detach/reattach this lets the broken page run JS and emit bridge messages again; for example, a laterheightChangedcan move the provider back toReady, despite the documented behavior that a page failure remains failed until a new session reloads it. Keep failed pages paused when replaying this terminal state.
if (isLoaded) {
page.resume()
report(lastState)
sdk/src/main/java/cloud/mindbox/mobile_sdk/embedded/MindboxEmbeddedBlockView.kt:145
- Clearing an error view while the block is already in
Empty/Failedonly clears this field; the old error child remains attached and the block remains visible. The Compose wrapper now calls this setter on a non-null→null slot update, so that child recomposes to blank content and leaves a permanent layout hole. When removing an existing error view in a no-content state, clear the shown child and reapply the default collapsed visibility (while preserving the intentional behavior that adding an error view after collapse waits for the next outcome).
public fun setErrorView(view: View?) {
errorView = view
}
justSmK
left a comment
There was a problem hiding this comment.
Ревью в паре с iOS-стеком (mindbox-cloud/ios-sdk#751, #754, #755, #756) — смотрел обе платформы, чтобы поймать расхождения. Проверял состояние ветки на a7e7383c, замечания Copilot, закрытые в «follow review», не повторяю.
Сначала о хорошем
Веб-слой здесь заметно крепче, чем на iOS, и это стоит зафиксировать как эталон, а не как местную деталь:
- блокировка навигации на обеих перегрузках
shouldOverrideUrlLoading(API 21+); - rate limit моста 30 сообщений/с и лимит размера сообщения 16 КБ;
- санитизация логов страницы (обрезка + вычистка переводов строк);
onRenderProcessGone— без него дохлый рендерер роняет приложение хоста на API 26+;- https-only с разбором URI и проверкой
userInfo,MIXED_CONTENT_NEVER_ALLOW,allowFileAccess=false; - proguard-правило для
@JavascriptInterface.
На iOS из этого нет ничего — там даже decidePolicyFor не реализован, и PR это признаёт. Завёл это как отдельный разговор на iOS-стороне.
Плюс ожидание конфига через NotReadyYet + подписка на inAppConfigFlow, перезагрузка по новой сессии и BuildConfig.DEBUG-гард на инъекции мока — всего этого на iOS тоже нет, там хардкод прод-URL вообще ничем не закрыт. И 134 теста без единого sleep (на iOS 16 реальных снов суммарно ~5.5 секунды).
Что вижу главным
Три пункта DoD не закрыты, и ровно те же три не закрыты на iOS — значит это не локальный недосмотр, а решение, которое надо принять один раз на обе платформы: делегат «загрузка», дефолтный экран ошибки, «одинаковые ID → тот же объект». Подробности инлайном.
Отдельно: раздел «Имплементация» в задаче описывает EmbeddedBlockContentRegistry, DeferredContentProvider, StoriesEmbeddedContentProvider и EmbeddedBlockDefaultViews.error — в ветке этих классов нет. Описание задачи стоит привести в соответствие с кодом, иначе приёмка пойдёт по несуществующему плану.
Разное поведение при общем веб-контракте
Контракт со фронтом один, а платформы трактуют его по-разному — это надо свести до того, как фронт начнёт писать страницы:
| Android | iOS | |
|---|---|---|
heightChanged |
обрабатывается как Ready |
игнорируется полностью |
ready с нулевой высотой |
Empty («таргетинг ничего не выбрал») |
Failed («сломанная вёрстка») |
| бюджет таймаута | сбрасывается в полный на каждое появление | накапливает потраченное, пауза в фоне |
| аналитика | шлёт embeddedPlaceRequested |
не шлёт ничего |
Мелочь, о которой уже договорились
Фолбэк на мок-страницу при pageUrl == null в EmbeddedBlockContentFactory ещё в ветке — на комментарий Copilot ответили «Да выпилим моки», просто чтобы не потерялось.
https://tracker.yandex.ru/MOBILE-324