Add install attribution matching support#456
Open
yusuftor wants to merge 19 commits into
Open
Conversation
Resolves conflicts across CHANGELOG, Constants/podspec (4.16.0), Package.resolved, and the attribution stack: - AttributionFetcher: keep develop's zero-IDFA UUID comparison - Network.sendToken: adopt develop's throwing AdServicesResponse API - Superwall: rely on develop's config-gated AdServices auto-fire; keep MMP install-match path (hadTrackedAppInstallBeforeConfigure) - AttributionPoster: take develop's refactored token flow and re-graft the .appleSearchAds AttributionMatch tracking onto it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oder - DeviceHelper: guard screenWidth/screenHeight/devicePixelRatio behind #if os(visionOS) (UIScreen.main is meaningless there), matching the existing interfaceStyle pattern. - SK2StoreProduct / ProductPurchaserSK2: add visionOS 26.4 to the billing-plan availability checks so the 26.4-only StoreKit APIs aren't used under a guard that only covered iOS, fixing the visionOS build. - Storage: document that the post-ATT MMP match deliberately re-runs to upgrade the pre-ATT (no-IDFA) match with the real IDFA, not a bug. - EndpointKind: add a per-kind jsonEncoder mirroring jsonDecoder; route all Endpoint bodies through Kind.jsonEncoder so casing follows the backend (core=snake_case, SubscriptionsAPI=camelCase) instead of being hand-picked per call site. Behaviour-preserving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
screenWidth/screenHeight/devicePixelRatio were computed via UIScreen.main (deprecated since iOS 16, main-thread-only) but read from the background async matchMMPInstall, a data race under strict concurrency. Cache them once at init on the main thread, preferring the connected UIWindowScene's screen and falling back to UIScreen.main only when no scene is attached. Safe to cache: the values feed only the MMP install payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move `[weak self]` from the inner `Task` to the enclosing completion closure in `TransactionManager.observe` and `WebEntitlementRedeemer`'s Stripe checkout `onClose` handlers, so the stored closures don't strongly retain `self`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Decode `MMPMatchResponse.queryParams` as `[String: JSON]` instead of `[String: String]`. The backend returns array values when a query key repeats in the click URL, which previously threw and failed the whole response decode — silently reporting a real match as `request_failed` and dropping the acquisition attributes. - Decode `confidence` leniently so an unrecognised value (e.g. a future tier) degrades to `nil` rather than failing the entire response. - Document the real `matchScore` range (75-117). - Add unit tests for the install-attribution gating logic and response decoding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
Resolved conflicts: - CHANGELOG.md: combined into a single 4.16.0 section (MMP install attribution + develop's annual-subscriptions / EventTrackingBehavior / crash & intro-offer fixes); dropped the orphan 4.15.4 heading since develop folded that content into the unreleased 4.16.0. - SK2StoreProduct.swift / ProductPurchaserSK2.swift: took develop's broadened availability list (iOS/macOS/tvOS/watchOS/visionOS 26.4) from the "Fix for billing plans with visionOS" commit, a superset of the branch's iOS+visionOS-only check. - Example app Package.resolved: took develop's newer RevenueCat pin (5.80.0). - project.pbxproj: kept both new test files; regenerated via xcodegen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
attribution_matchevent and write sharedacquisition_*user attributesChecklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.swiftlintin the main directory and fixed any issues.Greptile Summary
This PR adds install attribution matching to the SDK. The main changes are:
attribution_matchevent for MMP and Apple Search Ads.Confidence Score: 4/5
This is close, but the attribution lifecycle issues should be fixed before merging.
Sources/SuperwallKit/Superwall.swift and Sources/SuperwallKit/Storage/Cache/CacheKeys.swift
Important Files Changed
Comments Outside Diff (5)
Sources/SuperwallKit/Network/Network.swift, line 592-596 (link)info: ["payload": request]passes the fullMMPMatchRequeststruct, which includesidfa,idfv, anddeviceId, into the structured log on every failed/api/matchcall. Once the user grants ATT — exactly when the retry fires — a real IDFA will appear in crash/debug logs captured by any connected logging tool.The existing
sendTokenfailure already follows this same pattern (info: ["payload": token]), so this isn't unique to the new code, but it's worth flagging as the IDFA is a more sensitive identifier than an AdServices token. Consider redacting or omitting the identifier fields from the error payload:Prompt To Fix With AI
Sources/SuperwallKit/Network/Network.swift, line 587-588 (link)matchMMPInstallreturnstrueeven on server-side "no match"matchMMPInstallreturnstruewhenever the HTTP request succeeds (regardless of whetherresponse.matchedistrueorfalse), andfalseonly on a network error. This causesDidCompleteMMPInstallAttributionMatchto be saved even when the server returnsmatched: falsewith no attribution, which permanently prevents the initial match path from retrying on future launches.The tracking-permission retry path (
shouldAttemptTrackingPermissionMMPInstallAttributionMatch) uses a different gate (IsEligibleForMMPInstallAttributionMatch) and will still fire correctly after ATT is granted — so the retry does work. The namingdidCompleteMatchand the stored flagDidCompleteMMPInstallAttributionMatchare ambiguous because they conflate "HTTP request completed" with "attribution was found."Consider renaming the return value and the flag to
DidCompleteMMPInstallAttributionRequestto make the intent explicit, or leaving a comment at thereturn truesite clarifying that success here means "request processed; no need to retry the initial path."Prompt To Fix With AI
Sources/SuperwallKit/Storage/Storage.swift, line 716-731 (link)shouldAttemptInitialMMPInstallAttributionMatchskips only when BOTH conditions are trueThe early-exit guard is:
This means a fresh install (
hadTrackedAppInstallBeforeConfigure == false) will always fall through to check the attribution window — even ifDidCompleteMMPInstallAttributionMatchwas somehow already persisted (e.g. from a previous install that wasn't fully cleaned up). In that edge case, an extra MMP request is fired unnecessarily.A more defensive guard would check
didCompleteMatchindependently ofhadTrackedAppInstallBeforeConfigure:The
hadTrackedAppInstallBeforeConfigure == falsepath always hasdidCompleteMatch == falsein practice, so removing the conjunction doesn't change real-world behavior while making the intent clearer.Prompt To Fix With AI
Sources/SuperwallKit/Superwall.swift, line 613-618 (link)Initial retry race
When ATT is granted while the initial install match is still running, this retry can start a second
/api/matchrequest with the real IDFA before the fire-and-forget initial request finishes. Both calls mergeacquisition_*attributes, so the earlier pre-ATT response can finish last and overwrite the deterministic post-ATT match. The same in-flight gate needs to cover the initial request, or the retry should wait for or cancel the initial match before sending the upgraded request.Sources/SuperwallKit/Storage/Cache/CacheKeys.swift, line 62-80 (link)Reset keeps completion
These new MMP flags live in
.appSpecificDocuments, butStorage.reset()only clears user-specific storage. Afterreset()clears user attributes for a logout or new user, the persistedDidCompleteMMPInstallAttributionRequestflag can still makeshouldAttemptInitialMMPInstallAttributionMatchreturn false on the next configure. The new user then never receives the sharedacquisition_*attributes, even though the attribution values were removed during reset. Either clear these MMP flags during reset or store the completion state in the same user-scoped area as the attributes it gates.Prompt To Fix All With AI
Reviews (13): Last reviewed commit: "Merge origin/develop into feature/mmp" | Re-trigger Greptile
Context used: