Skip to content

MOBILE-425: Reserve show budget slots and answer the bridge by outcome - #768

Merged
justSmK merged 29 commits into
mission/storiesfrom
feature/MOBILE-425-embedded-blocks
Sep 8, 2026
Merged

justSmK merged 29 commits into
mission/storiesfrom
feature/MOBILE-425-embedded-blocks

Conversation

@justSmK

@justSmK justSmK commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Follow-ups from the MOBILE-328/332 acceptance, paired with Android MOBILE-421. The show budget is now checked and taken in one step before a show starts, so overlays and blocks can no longer pass on the same count; the bridge answers showInApp once, by its outcome, and refuses actions it does not know; ready reports permissions as the system sees them now. 29 commits, each feature as a code commit plus its tests, review fixes folded into their features, a final comment trim on top.

What changed and why:

  • Show budget with slot reservation (InappShowBudget): the budget check and the show that spends it were seconds apart for a block and a whole delayTime apart for a delayed overlay. The slot is keyed by in-app, taken when the timer fires even in the background and held for the foreground, given back when the show fails, is closed early or no block of the place is left to take it. A show commit releases only the slot taken for its own in-app, so a show accounted late leaves the slot the place has since taken for the next in-app in place. The minimum interval is compared in milliseconds, a scheduled show is decided on the main queue, and the place registry hops its init/deinit-driven entry points to the main queue itself.
  • Terminal showInApp: the page took the handover ack for a show and could not tell a window from a miss. One answer per request, on the main thread, with the contract's reasons unknown_inapp, source_dismissed, show_failed; a show closed before its window was on screen answers with an error.
  • Unknown bridge actions are refused instead of acknowledged (rule 6 of the bridge contract).
  • One network failure per session goes to the funnel, keyed by (inappId, reason) as on Android; a failed segmentation request no longer replays into Inapp.ShowFailure on every resolve. A block's failures sent past the buffer are reported once per in-app and reason per session too, whatever the reason: a block that failed the same way on every return to the screen sent presentation_failed each time (MOBILE-419 acceptance finding). The overlay's buffered failures keep repeating, as on Android.
  • The block owns the whole wait for its config: the config manager's own 30 s cap raced the block's budget and answered the place "empty" with no failure event.
  • ready always carries permissions and asks the system for the notifications status instead of a flag refreshed on foreground; the start payload became asynchronous, a stale initDataUpdated is dropped.
  • Logs: a valid null displayConditions is logged as routine, the page's answer is timed.
  • Separately, a session-reset defect rather than a DoD item: a show discarded by a session reset now completes through its own completion, so a pending showInApp is answered. It is not a close: no cooldown is written and the host is not told the in-app was dismissed, as on Android.

Wire/UX changes worth release notes:

  • minIntervalBetweenShows is compared in milliseconds: a sub-second interval now works, a fractional one is no longer rounded down.
  • An unknown bridge action gets error, not success; an old template that sends one will see the refusal.
  • showInApp answers when the outcome is known instead of instantly; a request whose show was closed before the window appeared gets error.
  • permissions is always present in the ready payload, {} when nothing is granted.
  • Fewer Inapp.ShowFailure per session: network and image failures are reported once, and a block's failure of any reason once.

Note for review: show limits read as before, zero, negative or missing all mean no limit, verified against Android's config validator. 13 pre-existing defects found while comparing the platforms are out of this branch's scope and tracked separately. Full test plan 1962/0, swiftlint --strict clean.

https://tracker.yandex.ru/MOBILE-425

Update, 8 Sep. Three review findings fixed plus one from the follow-up pass, eight commits on top (29 in total), each as a code commit plus its tests. Full plan 1971/0, swiftlint strict clean.

  • An empty answer for the place now drops the page still loading for the previous winner. Before, that page's late contentRendered set the block ready, expanded it with content the SDK had just rejected, sent Inapp.Show and committed the budget with no reservation behind it. Android releases the page on an empty answer.
  • A delay announced for a new winner no longer stands down the wait budget of a page already loading; the announcement matters only to a block with no page. Before, a page that never reported kept its shimmer for the whole delayTime instead of failing on its own budget. Android resets only the config wait there.
  • An overlay's presentation failure is sent on its own, past the shared buffer (InappShowFailureManager.sendFailure, no dedup, like Android's sendFailure). Before, the scheduler's error path flushed the buffer and took along the targeting failures another pass had collected and was about to drop: they went out, and their outage keys were recorded as reported for the session. Now only the end of a pass flushes the buffer.
  • A place answer parked while the block was off screen is applied before the page is resumed, as Android does: a page rendered off screen and then replaced or emptied before the return no longer sends Inapp.Show or commits the budget with no reservation behind it (P6 of the pre-existing registry). A data push parked for the return is armed once.

The blanket success told the page the SDK acted on an action nobody
owns; a page of the next vocabulary would take that for a real answer.
In sync with Android and rule 6 of the bridge contract.
A failed segmentation request is cached for the session, so every later
resolve of the place replayed the same refusal into Inapp.ShowFailure. It is
reported once per session by (inappId, reason), the key shared with Android;
a failed enqueue un-records so the kept buffer can retry.

The check runs at collect time as well as at the send: the buffer keeps one
failure per in-app by priority, so an outage already reported must not shadow
a fresh failure of the same in-app. An image download failure is reported
once per session too.
The config wait's own 30s cap raced the block's budget: when the cap
won, the place was answered "empty" with no failure event and the block
stayed alive for a late revival its public contract forbids. A place
resolve now waits the download out — the block's timeout is the only
give-up, so it is final (team decision 02.09), the failure always goes
out, and a timeout longer than 30s starts working. Page requests keep
the cap.
A valid null displayConditions logged as an error, and a slow answer to
the page's in-app question was invisible in the SDK log — "the feed is
empty" could only be reconstructed from the page's own console.
The budget check and the show that spends it were seconds apart for a block
and a whole delayTime apart for a delayed one, so two shows could pass on the
same count. One InappShowBudget now checks and takes the slot under a single
lock: a show commits it, a failure or an early close gives it back, a session
reset drops it. The presentation validator's rules moved into the budget, the
mapper no longer checks budgets at the resolve, and a non-positive or missing
limit still means no limit, as Android's config validator reads it.

The overlay's slot is keyed by its in-app and reserved before the window opens;
a moment that comes in the background takes the slot and waits for the
foreground, and the screen check, the reservation and the present share one
main-queue turn. A block's slot is taken at the last point before a page is
built and given back when no block of the place is left to take it. The
minimum interval is compared in milliseconds, and the registry hops its
lifetime-driven entry points to the main queue itself.
The page took the handover ack for a show and could not tell an opened
window from a miss; the web client holds a one-shot promise per id, so a
second envelope was never an option. The bridge now answers exactly once,
on the main thread, when the outcome is known, with the contract's reasons:
unknown_inapp, source_dismissed, show_failed. A show closed before its
window was on screen answers with an error and nothing goes to the funnel;
a window that fails after it appeared is reported but not answered again.
In sync with Android.

Rule 4 of the contract lands with it: the two in-app request actions refuse
on a surface without an in-app service instead of keeping silent.
iOS omitted `permissions` when nothing was granted and read the notifications
status from a flag refreshed only on foreground, so a permission the user granted
from a page stayed invisible to the next page until the app was backgrounded.
Android sends the key always and asks the system at `ready`; the payload now does
the same, which makes the start payload asynchronous.

A late payload is dropped when the page closed or its params changed meanwhile,
so an older config update can never overwrite a newer one.
The simulator grants the test host the camera and the microphone, so the checks
rely on the notifications status alone, which the test container controls.
The discard observer dismissed through an empty onClose, so a show cut off by
a session reset never completed and its request stayed unanswered. The discard
now runs the show's completion and tells it which one it was: the request
behind the show is answered, while the minimum-interval cooldown and the
host's dismissed callback stay with a real close, as Android suppresses both
for a session reset on purpose.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Reservation identity and asynchronous show races can violate budgets, user presence, and terminal outcomes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Centralizes in-app show budgeting and makes bridge responses reflect actual outcomes, while refreshing permission status and deduplicating failures.

Changes:

  • Adds atomic show-slot reservation across overlays and embedded blocks.
  • Makes showInApp, unknown actions, and ready responses outcome-aware and asynchronous.
  • Adds session failure deduplication and discard-aware presentation handling.
File summaries
File Description
MindboxTests/Validators/UnlimitedFrequencyTests.swift Removes obsolete validator tests.
MindboxTests/Validators/InAppTrackingServiceTests.swift Updates tracking expectations.
MindboxTests/Validators/InAppPresentationValidatorTests.swift Removes replaced validator suite.
MindboxTests/InApp/Tests/WebView/TransparentViewJSBridgeTests.swift Updates asynchronous payload mock.
MindboxTests/InApp/Tests/WebView/MindboxWebBridgeTests.swift Tests unknown-action refusal.
MindboxTests/InApp/Tests/WebView/BridgeHandlers/WebViewStartPayloadBuilderTests.swift Tests asynchronous permissions payloads.
MindboxTests/InApp/Tests/WebView/BridgeHandlers/ReadyActionHandlerTests.swift Tests delayed ready responses.
MindboxTests/InApp/Tests/WebView/BridgeHandlers/LifecycleActionHandlerTests.swift Updates payload callback mock.
MindboxTests/InApp/Tests/WebView/BridgeHandlers/InappRequestActionHandlersTests.swift Tests terminal show outcomes.
MindboxTests/InApp/Tests/WebView/BridgeHandlers/ContentRenderedActionHandlerTests.swift Updates payload callback mock.
MindboxTests/InApp/Tests/WebView/BridgeHandlers/BridgeHandlerDoubles.swift Adds deferred payload support.
MindboxTests/InApp/Tests/TimeToDisplayBackgroundTests.swift Adopts show-budget dependencies.
MindboxTests/InApp/Tests/InappShowFailureManagerTests.swift Tests failure deduplication.
MindboxTests/InApp/Tests/InappShowBudgetTests.swift Covers reservation and limit rules.
MindboxTests/InApp/Tests/InappShowAccountantTests.swift Tests budget-backed accounting.
MindboxTests/InApp/Tests/InappScheduleManagerTests.swift Covers scheduling, reservations, and outcomes.
MindboxTests/InApp/Tests/InAppPresentationManagerTests.swift Tests close versus discard behavior.
MindboxTests/InApp/Tests/InAppCoreManagerTests.swift Updates scheduling protocol mock.
MindboxTests/InApp/Tests/InAppConfigurationManagerTests.swift Tests uncapped place waits.
MindboxTests/InApp/Tests/InAppConfigResponseTests/EmbeddedBlockResolveTests.swift Moves budget checks from resolution.
MindboxTests/InApp/Mock/InAppPresentationManagerMock.swift Models discard and completion state.
MindboxTests/EmbeddedBlocks/EmbeddedBlockWebViewProviderTests.swift Tests block slots and show outcomes.
MindboxTests/EmbeddedBlocks/EmbeddedBlockWebViewPageTests.swift Tests deferred show replies.
MindboxTests/EmbeddedBlocks/EmbeddedBlockResolverTests.swift Tests priority propagation.
MindboxTests/EmbeddedBlocks/EmbeddedBlockPlaceRegistryTests.swift Tests place-slot lifecycle.
MindboxTests/EmbeddedBlocks/EmbeddedBlockMocks.swift Adds budget and outcome doubles.
MindboxTests/EmbeddedBlocks/EmbeddedBlockInappServiceTests.swift Tests terminal service outcomes.
MindboxTests/EmbeddedBlocks/EmbeddedBlockContentProviderFactoryTests.swift Supplies budget dependencies.
MindboxTests/DI/DIMainModuleRegistrationTests.swift Verifies budget registration.
Mindbox/Validators/InappPresentationValidator.swift Removes superseded validator.
Mindbox/Validators/InappFrequencyValidator.swift Accepts session-show snapshots.
Mindbox/Utilities/SystemInfo/SystemInfoProvider.swift Makes permissions asynchronous.
Mindbox/Utilities/SystemInfo/MBSystemInfoProvider.swift Delegates asynchronous permissions.
Mindbox/Utilities/SessionTemporaryStorage.swift Stores unified budget state.
Mindbox/Utilities/InappShowBudget.swift Implements atomic slot budgeting.
Mindbox/Utilities/InappSessionLedger.swift Tracks reported network failures.
Mindbox/Permissions/PermissionProvider.swift Revises permission provider contract.
Mindbox/Permissions/MBPermissionProvider.swift Queries live notification status.
Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift Supports asynchronous bridge payloads.
Mindbox/InAppMessages/Presentation/Views/WebView/MindboxWebViewFacade.swift Builds payloads asynchronously.
Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/Handlers/WebViewStartPayloadBuilder.swift Always emits live permissions.
Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/Handlers/WebBridgeHost.swift Defines terminal show outcomes.
Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/Handlers/ShowInAppActionHandler.swift Answers from show results.
Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/Handlers/ReadyActionHandler.swift Waits for payload completion.
Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/Handlers/FilterShowableInappsActionHandler.swift Refuses unsupported surfaces.
Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/BridgeMessageDispatcher.swift Refuses unknown actions.
Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/BridgeMessage.swift Updates permissions documentation.
Mindbox/InAppMessages/Presentation/InAppPresentationManager.swift Adds discard-aware completion.
Mindbox/InAppMessages/Models/Config/InappModel/DisplayConditions.swift Treats null conditions routinely.
Mindbox/InAppMessages/InappTrackingService.swift Delegates session accounting.
Mindbox/InAppMessages/InappShowFailureManager.swift Deduplicates session failures.
Mindbox/InAppMessages/InappShowAccountant.swift Commits shows through budgets.
Mindbox/InAppMessages/InappScheduleManager.swift Reserves slots and reports outcomes.
Mindbox/InAppMessages/InAppConfigurationMapper/InappMapper.swift Defers place budget checks.
Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift Removes place wait cap.
Mindbox/EmbeddedBlocks/WebView/EmbeddedBlockWebViewProvider.swift Manages block slots and outcomes.
Mindbox/EmbeddedBlocks/WebView/EmbeddedBlockWebViewPage.swift Propagates deferred show results.
Mindbox/EmbeddedBlocks/WebView/EmbeddedBlockPageHosting.swift Updates page callback contract.
Mindbox/EmbeddedBlocks/WebView/EmbeddedBlockInappService.swift Maps presentation outcomes to bridge results.
Mindbox/EmbeddedBlocks/Resolver/EmbeddedBlockWebContent.swift Carries in-app priority.
Mindbox/EmbeddedBlocks/Resolver/EmbeddedBlockResolver.swift Maps priority into block content.
Mindbox/EmbeddedBlocks/Resolver/EmbeddedBlockPlaceRegistry.swift Reserves and releases place slots.
Mindbox/DI/Injections/InjectUtilities.swift Injects live notification status.
Mindbox/DI/Injections/InjectInappTools.swift Registers budget-backed services.
Mindbox/DI/Injections/InjectEmbeddedBlocks.swift Injects budgets into block registry.
Mindbox/DI/Injections/InjectCore.swift Removes validator dependency.
Mindbox.xcodeproj/project.pbxproj Updates source and test membership.
Review details
  • Files reviewed: 67/67 changed files
  • Comments generated: 3
  • Review effort level: Balanced

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

Comment thread Mindbox/InAppMessages/InappScheduleManager.swift
Comment thread Mindbox/EmbeddedBlocks/WebView/EmbeddedBlockWebViewProvider.swift
Comment thread Mindbox/Utilities/InappShowBudget.swift
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor
TestsPassed ✅SkippedFailedTime ⏱
Unit tests report2187 ran2187 ✅2m 21s 851ms

A block that failed the same way on every return to the screen sent
presentation_failed each time, while the overlay's network failures were
already reported once per session. A failure a block sends past the buffer
now goes out once per in-app and reason per session, on the key the network
dedup uses; the overlay's buffered failures keep repeating, as on Android.
…step

The once-per-session rule on the direct path held only through its single
caller, the block's provider factory, and nothing in the signature said so.
The check, record, enqueue and rollback sequence was also written twice, once
per path; one helper now owns it, the paths differ only in which reasons
count. Mocks and tests follow the rename, so the commit compiles as a whole.
A place is one owner for whatever it shows next, so a show accounted after
the place had already reserved another in-app took that reservation with
it, and the next show ran uncovered by the budget until its own commit.
In sync with Android.
Review asked for fewer comments. What stays: threading pins, contracts
shared with Android and the bridge, and deliberate deviations. One stale
claim went with the rest: the block's direct failure path no longer says
"in sync with Android", whose block failures are not yet deduplicated.
An empty answer collapsed the block but left the page of the previous winner loading. Its late contentRendered set the block ready, expanded it with content the SDK had just rejected and committed a show with no reservation behind it. Android releases the page on an empty answer; so does iOS now.
…a delay

The announcement stood the wait budget down for every block at the place, including one whose page was already loading. A page that never reported then kept its shimmer for the whole delay instead of failing on its own budget. Android stands only the config wait down.
…ffer

The scheduler's error path added its failure to the buffer and flushed it, taking along the targeting failures another pass had collected and was about to drop: they went out, and their outage keys were recorded as reported for the session. Android sends a presentation failure on its own; now only the end of a pass flushes the buffer.
On return the provider announced the page's outcome and accounted its show first, then applied the answer parked while it was off screen. A page rendered off screen and replaced or emptied before the return sent Inapp.Show and committed the budget with no reservation behind it. Android applies the deferred content first; so does iOS now. A parked data push is armed once: the resume re-arms only a wait the stop suspended.
@justSmK
justSmK merged commit 0b1567f into mission/stories Sep 8, 2026
6 checks passed
@justSmK
justSmK deleted the feature/MOBILE-425-embedded-blocks branch September 8, 2026 13:11
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