MOBILE-425: Reserve show budget slots and answer the bridge by outcome - #768
Merged
Merged
Conversation
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.
Contributor
There was a problem hiding this comment.
🟡 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, andreadyresponses 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.
Contributor
|
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.
Vailence
approved these changes
Sep 8, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
showInApponce, by its outcome, and refuses actions it does not know;readyreports 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:
InappShowBudget): the budget check and the show that spends it were seconds apart for a block and a wholedelayTimeapart 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 itsinit/deinit-driven entry points to the main queue itself.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 reasonsunknown_inapp,source_dismissed,show_failed; a show closed before its window was on screen answers with an error.Inapp.ShowFailureon 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 sentpresentation_failedeach time (MOBILE-419 acceptance finding). The overlay's buffered failures keep repeating, as on Android.readyalways carriespermissionsand asks the system for the notifications status instead of a flag refreshed on foreground; the start payload became asynchronous, a staleinitDataUpdatedis dropped.displayConditionsis logged as routine, the page's answer is timed.showInAppis 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:
minIntervalBetweenShowsis compared in milliseconds: a sub-second interval now works, a fractional one is no longer rounded down.error, notsuccess; an old template that sends one will see the refusal.showInAppanswers when the outcome is known instead of instantly; a request whose show was closed before the window appeared getserror.permissionsis always present in thereadypayload,{}when nothing is granted.Inapp.ShowFailureper 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 --strictclean.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.
contentRenderedset the block ready, expanded it with content the SDK had just rejected, sentInapp.Showand committed the budget with no reservation behind it. Android releases the page on an empty answer.delayTimeinstead of failing on its own budget. Android resets only the config wait there.InappShowFailureManager.sendFailure, no dedup, like Android'ssendFailure). 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.Inapp.Showor commits the budget with no reservation behind it (P6 of the pre-existing registry). A data push parked for the return is armed once.