Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup

### Enhancements

- Adds `demandScore` and `demandTier` to device attributes. A user is assigned these based on a variety of factors to determine whether they're more or less likely to pay higher prices and can be used within audience filters.
- Updates Superscript to 0.2.4.

## 4.0.6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -883,4 +883,44 @@ enum InternalSuperwallEvent {
return params
}
}

struct EnrichmentLoad: TrackableSuperwallEvent {
enum State {
case start
case complete(Enrichment)
case fail
}
let state: State

var superwallEvent: SuperwallEvent {
switch state {
case .start:
return .enrichmentStart
case .complete(let enrichment):
return .enrichmentComplete(
userEnrichment: enrichment.user.dictionaryObject,
deviceEnrichment: enrichment.device.dictionaryObject
)
case .fail:
return .enrichmentFail
}
}
let audienceFilterParams: [String: Any] = [:]

func getSuperwallParameters() async -> [String: Any] {
var output: [String: Any] = [:]
switch state {
case .complete(let enrichment):
for (key, value) in enrichment.user {
output["user_\(key)"] = value
}
for (key, value) in enrichment.device {
output["device_\(key)"] = value
}
return output
default:
return [:]
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ public enum SuperwallEvent {
/// When the shimmer view stops showing.
case shimmerViewComplete

/// When the enrichment request starts.
case enrichmentStart

/// When the enrichment request completes.
case enrichmentComplete(userEnrichment: [String: Any]?, deviceEnrichment: [String: Any]?)

/// When the enrichment request fails.
case enrichmentFail

var canImplicitlyTriggerPaywall: Bool {
switch self {
case .appInstall,
Expand Down Expand Up @@ -358,6 +367,12 @@ extension SuperwallEvent {
return .init(objcEvent: .shimmerViewStart)
case .shimmerViewComplete:
return .init(objcEvent: .shimmerViewComplete)
case .enrichmentFail:
return .init(objcEvent: .enrichmentFail)
case .enrichmentStart:
return .init(objcEvent: .enrichmentStart)
case .enrichmentComplete:
return .init(objcEvent: .enrichmentComplete)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ public enum SuperwallEventObjc: Int, CaseIterable {
/// When the shimmer view stops showing.
case shimmerViewComplete

/// When the enrichment request starts.
case enrichmentStart

/// When the enrichment request completes.
case enrichmentComplete

/// When the enrichment request fails.
case enrichmentFail

public init(event: SuperwallEvent) {
self = event.backingData.objcEvent
}
Expand Down Expand Up @@ -303,6 +312,12 @@ public enum SuperwallEventObjc: Int, CaseIterable {
return "shimmerView_start"
case .shimmerViewComplete:
return "shimmerView_complete"
case .enrichmentStart:
return "enrichment_start"
case .enrichmentFail:
return "enrichment_fail"
case .enrichmentComplete:
return "enrichment_complete"
}
}
}
60 changes: 14 additions & 46 deletions Sources/SuperwallKit/Config/ConfigManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class ConfigManager {
}

do {
Task {
try? await deviceHelper.getEnrichment()
}
let startAt = Date()
let newConfig = try await network.getConfig { [weak self] attempt in
self?.configRetryCount = attempt
Expand Down Expand Up @@ -143,10 +146,7 @@ class ConfigManager {
if let cachedConfig = cachedConfig,
enableConfigRefresh {
do {
let result = try await self.fetchWithTimeout({
try await self.network.getConfig(maxRetry: 0)
},
timeout: timeout)
let result = try await self.network.getConfig(maxRetry: 0, timeout: timeout)
return (result, false)
} catch {
// Return the cached config and set isUsingCached to true
Expand All @@ -161,34 +161,31 @@ class ConfigManager {
}
}()

async let isUsingCachedGeo: Bool = { [weak self] in
async let isUsingCachedEnrichment: Bool = { [weak self] in
guard let self = self else {
return false
}
let cachedGeoInfo = self.storage.get(LatestGeoInfo.self)
let cachedEnrichment = self.storage.get(LatestEnrichment.self)

if let cachedGeoInfo = cachedGeoInfo,
if let cachedEnrichment = cachedEnrichment,
enableConfigRefresh {
do {
let geoInfo = try await self.fetchWithTimeout({
try await self.network.getGeoInfo(maxRetry: 0)
},
timeout: timeout)
self.deviceHelper.geoInfo = geoInfo
let enrichment = try await self.deviceHelper.getEnrichment(maxRetry: 0, timeout: timeout)
self.deviceHelper.enrichment = enrichment
return false
} catch {
self.deviceHelper.geoInfo = cachedGeoInfo
self.deviceHelper.enrichment = cachedEnrichment
return true
}
} else {
await self.deviceHelper.getGeoInfo()
_ = try? await self.deviceHelper.getEnrichment()
return false
}
}()

let (config, isUsingCachedConfig) = try await configResult
let configFetchDuration = Date().timeIntervalSince(startAt)
let isUsingCachedGeoInfo = await isUsingCachedGeo
let usingCachedEnrichment = await isUsingCachedEnrichment

let cacheStatus: InternalSuperwallEvent.ConfigCacheStatus =
isUsingCachedConfig ? .cached : .notCached
Expand All @@ -214,9 +211,9 @@ class ConfigManager {
Task {
await preloadPaywalls()
}
if isUsingCachedGeoInfo {
if usingCachedEnrichment {
Task {
await deviceHelper.getGeoInfo()
try? await deviceHelper.getEnrichment()
}
}
if isUsingCachedConfig {
Expand All @@ -242,35 +239,6 @@ class ConfigManager {
}
}

private func fetchWithTimeout<T>(
_ task: @escaping () async throws -> T,
timeout: TimeInterval
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await task()
}

group.addTask {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
throw CancellationError()
}

do {
let result = try await group.next()
group.cancelAll()
if let result = result {
return result
} else {
throw CancellationError()
}
} catch {
group.cancelAll()
throw error
}
}
}

private func processConfig(
_ config: Config,
isFirstTime: Bool
Expand Down
4 changes: 2 additions & 2 deletions Sources/SuperwallKit/Config/Options/SuperwallOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ public final class SuperwallOptions: NSObject, Encodable {
}
}

var geoHost: String {
"geo-api.superwall.com"
var enrichmentHost: String {
"enrichment-api.superwall.com"
}

var adServicesHost: String {
Expand Down
8 changes: 6 additions & 2 deletions Sources/SuperwallKit/Dependencies/DependencyContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,18 @@ final class DependencyContainer {
}
}

// MARK: - IdentityInfoFactory
extension DependencyContainer: IdentityInfoFactory {
// MARK: - IdentityFactory
extension DependencyContainer: IdentityFactory {
func makeIdentityInfo() -> IdentityInfo {
return IdentityInfo(
aliasId: identityManager.aliasId,
appUserId: identityManager.appUserId
)
}

func makeIdentityManager() -> IdentityManager {
return identityManager
}
}

extension DependencyContainer: TransactionManagerFactory {
Expand Down
3 changes: 2 additions & 1 deletion Sources/SuperwallKit/Dependencies/FactoryProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ protocol ConfigManagerFactory: AnyObject {
) -> Paywall?
}

protocol IdentityInfoFactory: AnyObject {
protocol IdentityFactory: AnyObject {
func makeIdentityInfo() async -> IdentityInfo
func makeIdentityManager() -> IdentityManager
}

protocol TransactionManagerFactory: AnyObject {
Expand Down
70 changes: 46 additions & 24 deletions Sources/SuperwallKit/Misc/Extensions/Task+Retrying.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// File.swift
//
//
//
// Created by Yusuf Tör on 16/09/2022.
//
Expand All @@ -9,43 +9,65 @@
import Foundation

extension Task where Failure == Error {
/// Retries the given async operation up to `maxRetryCount` times if it throws.
/// Supports optional retry intervals, exponential backoff, retry callbacks, and a timeout.
/// Cancels all tasks once one completes or the timeout triggers.
@discardableResult
static func retrying(
priority: TaskPriority? = nil,
maxRetryCount: Int,
retryInterval: Seconds? = nil,
timeout: Seconds? = nil,
isRetryingCallback: ((Int) -> Void)?,
operation: @Sendable @escaping () async throws -> Success
) -> Task {
Task(priority: priority) {
for attempt in 0..<maxRetryCount {
do {
let result = try await operation()
if let (_, response) = result as? (Data, URLResponse),
let httpResponse = response as? HTTPURLResponse,
!(200...299).contains(httpResponse.statusCode) {
throw URLError(.badServerResponse)
try await withThrowingTaskGroup(of: Success.self) { group in
group.addTask {
for attempt in 0..<maxRetryCount {
do {
let result = try await operation()
if let (_, response) = result as? (Data, URLResponse),
let httpResponse = response as? HTTPURLResponse,
!(200...299).contains(httpResponse.statusCode) {
throw URLError(.badServerResponse)
}
return result
} catch {
isRetryingCallback?(attempt + 1)
if let retryInterval = retryInterval {
let oneSecond = TimeInterval(1_000_000_000)
let nanoseconds = UInt64(oneSecond * retryInterval)
try await Task<Never, Never>.sleep(nanoseconds: nanoseconds)
} else if let delay = TaskRetryLogic.delay(
forAttempt: attempt,
maxRetries: maxRetryCount
) {
try await Task<Never, Never>.sleep(nanoseconds: delay)
}
continue
}
}
return result
} catch {
isRetryingCallback?(attempt + 1)
if let retryInterval = retryInterval {
let oneSecond = TimeInterval(1_000_000_000)
let nanoseconds = UInt64(oneSecond * retryInterval)
try await Task<Never, Never>.sleep(nanoseconds: nanoseconds)
} else if let delay = TaskRetryLogic.delay(
forAttempt: attempt,
maxRetries: maxRetryCount
) {
try await Task<Never, Never>.sleep(nanoseconds: delay)

try Task<Never, Never>.checkCancellation()
return try await operation()
}

if let timeout = timeout {
group.addTask {
try await Task<Never, Never>.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
throw _Concurrency.CancellationError()
}
}

continue
if let result = try await group.next() {
group.cancelAll()
return result
} else {
group.cancelAll()
throw _Concurrency.CancellationError()
}
}

try Task<Never, Never>.checkCancellation()
return try await operation()
}
}
}
15 changes: 15 additions & 0 deletions Sources/SuperwallKit/Models/Enrichment/Enrichment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// File.swift
// SuperwallKit
//
// Created by Yusuf Tör on 10/04/2025.
//

import Foundation

struct Enrichment: Codable {
let user: JSON
let device: JSON
}

typealias EnrichmentRequest = Enrichment
Loading
Loading