Conversation
The install match used to fire on every fresh install. It now waits for `attributionOptions.mmp.enabled` in config, the same way Apple Search Ads waits for its switch. The eligibility check still runs at launch so a later launch can retry if this one ends before the match completes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed the change that makes the MMP install match wait until config enables the MMP.
MMPAttributionManager.matchInstallOnceEnabled: subscribes toconfigStateand runsstartMatchthe first timeattribution.mmp.enabled == true..first { $0 }limits it to one match per launch even if config refreshes, and it runs straight away if config is already loaded.Superwallconfigure task:shouldAttemptInitialMMPInstallAttributionMatchstill runs at launch after identity is configured, so the eligibility record survives a launch that ends before config loads. OnlyrecordMMPInstallAttributionMatchwaits for the flag.DependencyContainer: passesconfigManagerintoMMPAttributionManager.MMPInstallMatchConfigTests: four Swift Testing cases covering a wait for enable, no match when the flag is nil or false, an immediate match when config is already loaded, and a single match across a true → false → true refresh. Each assertion is exact, so each test would fail if its guard were removed.
ℹ️ Match timing and rollout depend on the config publish and the backend flag
The match now waits for configState to publish .retrieved, and that happens later than the PR's "usually under a second" suggests in two cases. On a first install there is no saved customer info to publish early from, so ConfigManager.processConfig awaits loadPurchasedProducts before it publishes. In test mode it publishes only after the tester closes the test-mode modal. Neither is a problem for a time-decayed match, but acquisition_* can arrive later than the description suggests. If the config fetch fails, configState completes and there is no match this launch. The saved eligibility flag lets a later launch within the 7-day window retry, as intended.
Technical details
# Match timing and rollout ordering
## Affected sites
- Sources/SuperwallKit/Config/ConfigManager.swift:511 — first-install path awaits `loadPurchasedProducts` before the `.retrieved` send at :538
- Sources/SuperwallKit/Config/ConfigManager.swift:525-533 — test mode publishes only after `presentTestModeModal` returns
- Sources/SuperwallKit/Config/ConfigManager.swift:432 — fetch failure completes `configState`, so `matchInstallOnceEnabled` never fires that launch
## Required outcome
- The backend sends `attributionOptions.mmp.enabled: true` for every app using the MMP before an SDK release containing this change ships. Otherwise install matching stops for those apps without any error.
## Open questions for the human
- Is the backend flag already rolled out for current MMP customers, or does the release need to wait for it?Claude Opus | 𝕏
The match can wait a while for config, so check that the app hasn't opted out of tracking in the meantime. Hold the dependency container weakly so a match that never fires doesn't keep it alive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed a64dd1f, which hardens the deferred MMP match callback in Superwall.swift.
- Checked the opt-out again when the match fires: the
matchInstallOnceEnabledclosure now returns early ifeventTrackingBehaviorhas changed to.nonewhile it waited for config. Because.first { $0 }has already fired, this skips the match for the rest of the launch. The eligibility record from launch still lets a later launch within the 7-day window retry. - Captured
dependencyContainerweakly: this breaks the container →mmpAttributionManager→pendingMatch→ closure → container cycle. That cycle would otherwise last as long as the MMP stays disabled.
Claude Opus | 𝕏
On a first launch the match starts when config arrives, the same moment an app_open paywall is unblocked, so its audiences were checked before the acquisition attributes existed. If the MMP is on, a match is still pending and the placement's audiences use acquisition_ attributes, the paywall now waits for the match, for up to 2 seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The match was skipped for the rest of the launch if tracking was off when config enabled the MMP. It now starts when the app opts back in, and the launch-time setup no longer depends on the tracking setting. The match still starts at most once per launch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues. One test suggestion is inline.
Reviewed changes
I reviewed b8e9694 and 7bec056, which make paywalls wait for a running install match and restart a skipped match when the app turns tracking back on.
- Paywalls wait for a pending match:
waitForSubsStatusAndConfignow ends withwaitForPendingMatch(ifUsedBy:). It waits up to 2 s only when a match is pending, the MMP is on, and an audienceexpressionmentionsacquisition_.throwableAsyncis built onAsyncThrowingStream, which stops when its task is cancelled, sogroup.cancelAll()does end the wait at the timeout. The attribute merge (queue.async) and the audience read (queue.sync) use the same serial queue, so theacquisition_*values are visible once the match task finishes. - Matches skipped by the opt-out now retry: the start closure returns
Task<Void, Never>?, andnilmeans it was skipped. TheSuperwall.eventTrackingBehaviorsetter callsstartMatchIfEnabled()when tracking isn't.none, andhasStartedMatchunderNSLockkeeps it to one match per launch. This resolves the open greptile thread about opting back in. - The config subscription stays open for the whole launch:
.first { $0 }becameremoveDuplicates().filter { $0 }, andhasStartedMatchnow enforces the single match. - Added tests: opt-in retry, opt-in while the MMP is off,
usesAcquisitionAttributes, and three wait/no-wait cases.
Claude Opus | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed c8669ed. It changes only tests, so that the paywall-wait tests fail if waitForPendingMatch stops waiting.
- Made
paywallWaitsForARunningMatchdepend on a real wait: the match task now sleeps 200 ms before it incrementsfinished.isMatchPendingflips to false only afterawait match.value, and that happens after the increment. Sofinished.count == 1can only pass if the waiter blocked until the match finished. This fixes the earlier race where the gate was released before the waiter started. - Gave
paywallStopsWaitingAfterTheTimeouta lower bound:elapsed >= 0.1is safe from flakes, because the timeout'sTask.sleepstarts insidewaitForPendingMatch, afterstartis recorded.
Claude Opus | 𝕏

Changes in this pull request
The MMP install match used to run on every fresh install, whether or not the app uses the Superwall MMP. It now waits for
attributionOptions.mmp.enabledin config before firing, the same way Apple Search Ads waits forattributionOptions.appleSearchAds.enabled. It's off unless the backend turns it on.MMPAttributionManager.matchInstallOnceEnabledwatches the config and starts the match the first time the switch is on. That's straight away if config is already loaded, and at most once per launch even if config refreshes.shouldAttemptInitialMMPInstallAttributionMatch) still runs at launch, before waiting for config. It records that this install may be matched, and a later launch relies on that record to retry. If it waited on config, an install whose first launch ended before config loaded would never be matched.acquisition_*attributes may land slightly later for paywalls shown right at launch.This is stacked on #524, which adds the
attributionOptions.mmp.enabledconfig option and uses it for IP collection. Merge that first. The backend needs to sendattributionOptions.mmp.enabled: truefor apps using the MMP, or matching stops for them.Validation: 65 tests pass across
MMPInstallMatchConfigTests(new),MMPInstallAttributionTests,AdServicesAttributionTests,DeviceHelperTestsandDeviceIPCollectorTestson the iPhone 17 Pro / iOS 26.5 simulator.scripts/lint.shis clean for the changed files (thearray_constructorwarning onSuperwall.swiftis pre-existing).Checklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.swiftlintin the main directory and fixed any issues.🤖 Generated with Claude Code
The PR should not merge until an opt-out at config arrival can no longer permanently consume the launch’s matching opportunity.
Findings
Fix with agent prompt
Summary
The PR waits for remote configuration to enable MMP install matching, adds a tracking opt-out check immediately before matching, and tests the config gate.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Install eligible] --> B[Wait for MMP-enabled config] B --> C{Tracking disabled?} C -->|No| D[Start match] C -->|Yes| E[Return without match] E --> F[One-shot subscription completed] F --> G[Later tracking opt-in does not retry]Reviews (2) · Last reviewed commit: "Recheck the tracking opt-out when the MM..."