Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub.

## 4.15.0

### Enhancements

- Adds install attribution matching support. If you set up performance marketing integrations on the Superwall dashboard, the SDK will attempt to match the install and track an `attribution_match` event. The attribution properties will be added to user attributes so that they can be used as breakdowns and filters in the charts.

## 4.14.2

### Enhancements
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import AdServices
#endif

final class AttributionFetcher {
private static let zeroAdvertisingIdentifier = "00000000-0000-0000-0000-000000000000"

var integrationAttributes: [String: String] {
queue.sync {
_integrationAttributes
Expand Down Expand Up @@ -43,7 +45,12 @@ final class AttributionFetcher {
return nil
}

return identifierValue.uuidString
let identifier = identifierValue.uuidString
if identifier.caseInsensitiveCompare(Self.zeroAdvertisingIdentifier) == .orderedSame {
return nil
}

return identifier
}
#endif
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,21 @@ final class AttributionPoster {
)

let data = await network.sendToken(token)
Superwall.shared.setUserAttributes(data)
if let data, !data.isEmpty {
Superwall.shared.setUserAttributes(data)
}

let matched = !(data?.isEmpty ?? true)
await Superwall.shared.track(
InternalSuperwallEvent.AttributionMatch(
info: AttributionMatchInfo(
provider: .appleSearchAds,
matched: matched,
source: matched ? (data?["acquisition_source"] as? String ?? "apple_search_ads") : nil,
reason: data == nil ? "request_failed" : (matched ? nil : "no_attribution")
)
)
)
} catch {
await Superwall.shared.track(
InternalSuperwallEvent.AdServicesTokenRetrieval(state: .fail(error))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,38 @@ enum InternalSuperwallEvent {
var audienceFilterParams: [String: Any] = [:]
}

struct AttributionMatch: TrackableSuperwallEvent {
let info: AttributionMatchInfo

var superwallEvent: SuperwallEvent {
return .attributionMatch(info: info)
}

func getSuperwallParameters() async -> [String: Any] { [:] }

var audienceFilterParams: [String: Any] {
var parameters: [String: Any] = [
"provider": info.provider.rawValue,
"matched": info.matched,
]

if let source = info.source {
parameters["source"] = source
}
if let confidence = info.confidence {
parameters["confidence"] = confidence.rawValue
}
if let matchScore = info.matchScore {
parameters["match_score"] = matchScore
}
if let reason = info.reason {
parameters["reason"] = reason
}

return parameters
}
}

struct IntegrationAttributes: TrackableSuperwallEvent {
var superwallEvent: SuperwallEvent {
return .integrationAttributes(audienceFilterParams)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,77 @@
/// These placement are tracked internally by the SDK and sent to the delegate method ``SuperwallDelegate/handleSuperwallEvent(withInfo:)-50exd``.
public typealias SuperwallPlacement = SuperwallEvent

/// Information about an install attribution result emitted by Superwall.
public struct AttributionMatchInfo: Sendable {
/// The attribution provider that produced the result.
public enum Provider: String, Sendable {
/// Superwall's mobile measurement matching flow.
case mmp

/// Apple Search Ads attribution.
case appleSearchAds = "apple_search_ads"
}

/// The confidence level of the attribution result.
public enum Confidence: String, Decodable, Sendable {
/// A high-confidence attribution result.
case high

/// A medium-confidence attribution result.
case medium

/// A low-confidence attribution result.
case low
}

/// The attribution provider that produced the result.
public let provider: Provider

/// Whether the attribution attempt resulted in a match.
public let matched: Bool

/// The resolved acquisition source, if one was found.
///
/// For example, `meta` or `apple_search_ads`.
public let source: String?

/// The confidence label returned by the provider, if available.
public let confidence: Confidence?

/// The numeric match score between 0 and 100 returned by the provider, if available.
public let matchScore: Double?

/// The reason for a non-match or failure, if available.
///
/// For example, `below_threshold`, `no_attribution`, or `request_failed`.
public let reason: String?

/// Creates a new install attribution result.
///
/// - Parameters:
/// - provider: The attribution provider that produced the result.

Check warning on line 64 in Sources/SuperwallKit/Analytics/Superwall Placement/SuperwallEvent.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Indentation Width Violation: Code should be indented using one tab or 2 spaces (indentation_width)
/// - matched: Whether the attribution attempt matched.
/// - source: The resolved acquisition source, if one was found.
/// - confidence: The provider's confidence label, if available.
/// - matchScore: The provider's numeric match score, if available.
/// - reason: The reason for a non-match or failure, if available.
public init(
provider: Provider,
matched: Bool,
source: String? = nil,
confidence: Confidence? = nil,
matchScore: Double? = nil,
reason: String? = nil
) {
self.provider = provider
self.matched = matched
self.source = source
self.confidence = confidence
self.matchScore = matchScore
self.reason = reason
}
}

/// Analytical events that are automatically tracked by Superwall.
///
/// These events are tracked internally by the SDK and sent to the delegate method ``SuperwallDelegate/handleSuperwallEvent(withInfo:)-50exd``.
Expand Down Expand Up @@ -105,6 +176,9 @@
/// When the user attributes are set.
case userAttributes(_ attributes: [String: Any])

/// When install attribution is resolved or fails to resolve.
case attributionMatch(info: AttributionMatchInfo)

/// When the user purchased a non recurring product.
case nonRecurringProductPurchase(product: TransactionProduct, paywallInfo: PaywallInfo)

Expand Down Expand Up @@ -374,6 +448,8 @@
return .init(objcEvent: .transactionRestore)
case .userAttributes:
return .init(objcEvent: .userAttributes)
case .attributionMatch:
return .init(objcEvent: .attributionMatch)
case .nonRecurringProductPurchase:
return .init(objcEvent: .nonRecurringProductPurchase)
case .paywallResponseLoadStart:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ public enum SuperwallEventObjc: Int, CaseIterable {
/// When a user navigates to a page in a multi-page paywall.
case paywallPageView

/// When install attribution is resolved or fails to resolve.
case attributionMatch

public init(event: SuperwallEvent) {
self = event.backingData.objcEvent
}
Expand Down Expand Up @@ -312,6 +315,8 @@ public enum SuperwallEventObjc: Int, CaseIterable {
return "transaction_restore"
case .userAttributes:
return "user_attributes"
case .attributionMatch:
return "attribution_match"
case .nonRecurringProductPurchase:
return "nonRecurringProduct_purchase"
case .paywallResponseLoadStart:
Expand Down
12 changes: 12 additions & 0 deletions Sources/SuperwallKit/Config/Options/SuperwallOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/// ``Superwall/configure(apiKey:purchaseController:options:completion:)-52tke``.
@objc(SWKSuperwallOptions)
@objcMembers
public final class SuperwallOptions: NSObject, Encodable {

Check warning on line 16 in Sources/SuperwallKit/Config/Options/SuperwallOptions.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Type Body Length Violation: Class body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines (type_body_length)
/// Configures the appearance and behaviour of paywalls.
public var paywalls = PaywallOptions()

Expand Down Expand Up @@ -227,6 +227,18 @@
}
}

var mmpHost: String {
switch self {
case .developer,
.custom:
return "mmp.superwall.dev"
case .local:
return "localhost:3045"
default:
return "mmp.superwall.com"
}
}

private enum CodingKeys: String, CodingKey {
case networkEnvironment
case customDomain
Expand Down Expand Up @@ -389,4 +401,4 @@
return [:]
}
}
}

Check warning on line 404 in Sources/SuperwallKit/Config/Options/SuperwallOptions.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

File Length Violation: File should contain 400 lines or less: currently contains 404 (file_length)
2 changes: 2 additions & 0 deletions Sources/SuperwallKit/Dependencies/FactoryProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ protocol ApiFactory: AnyObject {
requestId: String
) async -> [String: String]

func makeDeviceId() -> String

func makeDefaultComponents(
host: EndpointHost
) -> ApiHostConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The `Superwall` class is used to access all the features of the SDK. Before usin
- `PaywallInfo`
- `SuperwallEvent`
- `SuperwallEventObjc`
- `AttributionMatchInfo`
- `PaywallSkippedReason`
- `PaywallSkippedReasonObjc`
- `PaywallViewController`
Expand Down
2 changes: 1 addition & 1 deletion Sources/SuperwallKit/Misc/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ let sdkVersion = """
*/

let sdkVersion = """
4.14.2
4.15.0
"""
40 changes: 40 additions & 0 deletions Sources/SuperwallKit/Models/AdServicesResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,43 @@ import Foundation
struct AdServicesResponse: Decodable {
let attribution: [String: JSON]
}

// MARK: - MMP Attribution

struct MMPMatchRequest: Encodable {
let platform: String
let appUserId: String?
let deviceId: String?
let vendorId: String?
let idfa: String?
let idfv: String?
let advertiserTrackingEnabled: Bool
let applicationTrackingEnabled: Bool
let appVersion: String
let sdkVersion: String
let osVersion: String
let deviceModel: String
let deviceLocale: String
let deviceLanguageCode: String
let timezoneOffsetSeconds: Int
let screenWidth: Int
let screenHeight: Int
let devicePixelRatio: Double
let bundleId: String
let clientTimestamp: String
let metadata: [String: String]
}

struct MMPMatchResponse: Decodable {
let matched: Bool
let confidence: AttributionMatchInfo.Confidence?
let matchScore: Double?
let clickId: Int?
let linkId: String?
let network: String?
let redirectUrl: String?
let queryParams: [String: String]?
let acquisitionAttributes: [String: JSON]?
let matchedAt: String?
let breakdown: [String: JSON]?
}
15 changes: 15 additions & 0 deletions Sources/SuperwallKit/Network/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum EndpointHost {
case enrichment
case adServices
case subscriptionsApi
case mmp
}

protocol ApiHostConfig {
Expand All @@ -34,13 +35,15 @@ struct Api {
let enrichment: Enrichment
let adServices: AdServices
let subscriptionsApi: SubscriptionsAPI
let mmp: MMP

init(networkEnvironment: SuperwallOptions.NetworkEnvironment) {
base = Base(networkEnvironment: networkEnvironment)
collector = Collector(networkEnvironment: networkEnvironment)
enrichment = Enrichment(networkEnvironment: networkEnvironment)
adServices = AdServices(networkEnvironment: networkEnvironment)
subscriptionsApi = SubscriptionsAPI(networkEnvironment: networkEnvironment)
mmp = MMP(networkEnvironment: networkEnvironment)
}

func getConfig(host: EndpointHost) -> ApiHostConfig {
Expand All @@ -55,6 +58,8 @@ struct Api {
return adServices
case .subscriptionsApi:
return subscriptionsApi
case .mmp:
return mmp
}
}

Expand Down Expand Up @@ -109,4 +114,14 @@ struct Api {
self.networkEnvironment = networkEnvironment
}
}

struct MMP: ApiHostConfig {
let networkEnvironment: SuperwallOptions.NetworkEnvironment
var host: String { return networkEnvironment.mmpHost }
var path: String { return "/" }

init(networkEnvironment: SuperwallOptions.NetworkEnvironment) {
self.networkEnvironment = networkEnvironment
}
}
}
16 changes: 16 additions & 0 deletions Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ class DeviceHelper {
"\(Int(TimeZone.current.secondsFromGMT()))"
}

var timezoneOffsetSeconds: Int {
TimeZone.current.secondsFromGMT()
}

var screenWidth: Int {
Int(UIScreen.main.bounds.width.rounded())
}

var screenHeight: Int {
Int(UIScreen.main.bounds.height.rounded())
}

var devicePixelRatio: Double {
Double(UIScreen.main.scale)
}

var isFirstAppOpen: Bool {
return !storage.didTrackFirstSession
}
Expand Down
Loading
Loading