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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Fixed a crash showing out-of-the-box embedded messages on CocoaPods. `IterableEmbeddedView.xib` moved out of `Resources/` in 6.5.9, but the podspec only bundled `Resources/`, so the nib was missing from the CocoaPods resource bundle and `loadViewFromNib()` crashed. The podspec now bundles the UI component XIBs too. SPM was not affected.

### Changed
- Upgrade-then-downgrade hazard on UUA storage: the sessions blob in `UserDefaults` now encodes with `totalUnknownSessionCount` / `lastUnknownSession` / `firstUnknownSession`, and stored UUA events use `eventType` as the type discriminator. A customer who installs an SDK build with this change and later rolls back to a pre-SDK-412 build will hit a decode failure on the sessions blob (unknown user session counter resets to zero) and stored UUA events will be skipped on flush since the older SDK looks for `dataType`. Limited blast radius, but worth flagging for customers who pin or roll back versions.


## [6.7.2]
### Added
- Added optional `shouldLog(level:) -> Bool` to `IterableLogDelegate`. Defaults to `true` when not implemented. Lets the delegate short-circuit log calls before any message formatting work happens. `DefaultLogDelegate` routes its `minLogLevel` filter through this, and `NoneLogDelegate` returns `false` for every level.
Expand Down
4 changes: 4 additions & 0 deletions swift-sdk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
18E23AE02C6CDE97002B2D92 /* CombinationLogicEventTypeCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E23ADF2C6CDE97002B2D92 /* CombinationLogicEventTypeCriteria.swift */; };
18E5B5D12CC77BCE00A558EC /* IterableTokenGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E5B5D02CC77BCE00A558EC /* IterableTokenGenerator.swift */; };
18E5B5D32CC7853D00A558EC /* ValidateTokenForDestinationUserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E5B5D22CC7853D00A558EC /* ValidateTokenForDestinationUserTest.swift */; };
BDA4120000000000000DA001 /* UUANormalizationMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA4120000000000000DA002 /* UUANormalizationMigrationTests.swift */; };
1CBFFE1A2A97AEEF00ED57EE /* EmbeddedManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */; };
1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */; };
1CBFFE1C2A97AEEF00ED57EE /* EmbeddedSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */; };
Expand Down Expand Up @@ -595,6 +596,7 @@
18E23ADF2C6CDE97002B2D92 /* CombinationLogicEventTypeCriteria.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombinationLogicEventTypeCriteria.swift; sourceTree = "<group>"; };
18E5B5D02CC77BCE00A558EC /* IterableTokenGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTokenGenerator.swift; sourceTree = "<group>"; };
18E5B5D22CC7853D00A558EC /* ValidateTokenForDestinationUserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidateTokenForDestinationUserTest.swift; sourceTree = "<group>"; };
BDA4120000000000000DA002 /* UUANormalizationMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUANormalizationMigrationTests.swift; sourceTree = "<group>"; };
1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedManagerTests.swift; sourceTree = "<group>"; };
1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedMessagingProcessorTests.swift; sourceTree = "<group>"; };
1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedSessionManagerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1744,6 +1746,7 @@
181063DE2C9D51000078E0ED /* ValidateStoredEventCheckUnknownToKnownUserTest.swift */,
1802C00E2CA2C99E009DEA2B /* CombinationComplexCriteria.swift */,
18E5B5D22CC7853D00A558EC /* ValidateTokenForDestinationUserTest.swift */,
BDA4120000000000000DA002 /* UUANormalizationMigrationTests.swift */,
);
name = "unknown-user-tracking-tests";
sourceTree = "<group>";
Expand Down Expand Up @@ -2422,6 +2425,7 @@
55CC257B2462064F00A77FD5 /* InAppPresenterTests.swift in Sources */,
AC4BA00224163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift in Sources */,
18E5B5D32CC7853D00A558EC /* ValidateTokenForDestinationUserTest.swift in Sources */,
BDA4120000000000000DA001 /* UUANormalizationMigrationTests.swift in Sources */,
55B37FC822975A840042F13A /* InboxMessageViewModelTests.swift in Sources */,
182A2A152C661C9A002FF058 /* DataTypeComparatorSearchQueryCriteria.swift in Sources */,
55E6F462238E066400808BCE /* DeepLinkTests.swift in Sources */,
Expand Down
15 changes: 11 additions & 4 deletions swift-sdk/Core/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ enum Const {
static let getRemoteConfiguration = "mobile/getRemoteConfiguration"
static let mergeUser = "users/merge";
static let getCriteria = "unknownuser/list";
static let trackUnknownUserSession = "unknownuser/events/session";
static let trackUnknownSession = "unknownuser/events/session";
static let trackConsent = "unknownuser/consent";
static let getEmbeddedMessages = "embedded-messaging/messages"
static let embeddedMessageReceived = "embedded-messaging/events/received"
Expand All @@ -67,7 +67,7 @@ enum Const {
getRemoteConfiguration,
mergeUser,
getCriteria,
trackUnknownUserSession,
trackUnknownSession,
trackConsent,
]

Expand Down Expand Up @@ -226,9 +226,16 @@ enum JsonKey {

static let contentType = "Content-Type"

// AUT
// UUA
static let createNewFields = "createNewFields"
static let eventType = "dataType"
static let eventType = "eventType"
/// Legacy event-type discriminator key written to local storage prior to UUA naming
/// normalization. Read for one-shot migration only.
static let legacyEventType = "dataType"
/// Criteria payload field (from `/unknownuser/list`) that specifies which event type
/// a criteria applies to. Distinct from ``eventType``; happens to share the same string
/// as ``legacyEventType``, but is a permanent backend contract, not a storage migration.
static let criteriaDataType = "dataType"
static let eventTimeStamp = "eventTimeStamp"
static let criteriaSets = "criteriaSets"
static let matchedCriteriaId = "matchedCriteriaId"
Expand Down
22 changes: 11 additions & 11 deletions swift-sdk/Internal/InternalIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
self.dependencyContainer.createAuthManager(config: self.config)
}()

lazy var unknownUserManager: UnknownUserManagerProtocol = {
lazy var unknownUserManager: UnknownUserManager = {
self.dependencyContainer.createUnknownUserManager(config: self.config)
}()

Expand Down Expand Up @@ -165,7 +165,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
guard let config = self?.config else {
return
}
let merge = identityResolution?.mergeOnUnknownUserToKnown ?? config.identityResolution.mergeOnUnknownUserToKnown
let merge = identityResolution?.mergeOnUnknownToKnown ?? config.identityResolution.mergeOnUnknownToKnown
let replay = identityResolution?.replayOnVisitorToKnown ?? config.identityResolution.replayOnVisitorToKnown
if config.enableUnknownUserActivation, let email = email {
// Prepare consent for replay scenario before merge
Expand Down Expand Up @@ -216,7 +216,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
}
if config.enableUnknownUserActivation {
if let userId = userId, userId != (self?.localStorage.userIdUnknownUser ?? "") {
let merge = identityResolution?.mergeOnUnknownUserToKnown ?? config.identityResolution.mergeOnUnknownUserToKnown
let merge = identityResolution?.mergeOnUnknownToKnown ?? config.identityResolution.mergeOnUnknownToKnown
let replay = identityResolution?.replayOnVisitorToKnown ?? config.identityResolution.replayOnVisitorToKnown

// Prepare consent for replay scenario before merge
Expand Down Expand Up @@ -313,8 +313,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {

if isVisitorUsageTracked && config.enableUnknownUserActivation {
ITBInfo("CONSENT GIVEN and UNKNOWN USER TRACKING ENABLED - Criteria fetched")
self.unknownUserManager.getUnknownUserCriteria()
self.unknownUserManager.updateUnknownUserSession()
self.unknownUserManager.getUnknownCriteria()
self.unknownUserManager.updateUnknownSession()
}
}

Expand Down Expand Up @@ -407,7 +407,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {

if !isEitherUserIdOrEmailSet() && localStorage.userIdUnknownUser == nil {
if config.enableUnknownUserActivation {
unknownUserManager.trackUnknownUserTokenRegistration(token: token)
unknownUserManager.trackUnknownTokenRegistration(token: token)
}
onFailure?("Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods", nil)
return
Expand Down Expand Up @@ -497,7 +497,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
if !isEitherUserIdOrEmailSet() && localStorage.userIdUnknownUser == nil {
if config.enableUnknownUserActivation {
ITBInfo("UUA ENABLED - unknown user update user")
unknownUserManager.trackUnknownUserUpdateUser(dataFields)
unknownUserManager.trackUnknownUpdateUser(dataFields)
}
return rejectWithInitializationError(onFailure: onFailure)
}
Expand Down Expand Up @@ -529,7 +529,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
if !isEitherUserIdOrEmailSet() && localStorage.userIdUnknownUser == nil {
if config.enableUnknownUserActivation {
ITBInfo("UUA ENABLED - unknown user update cart")
unknownUserManager.trackUnknownUserUpdateCart(items: items)
unknownUserManager.trackUnknownUpdateCart(items: items)
}
return rejectWithInitializationError(onFailure: onFailure)
}
Expand Down Expand Up @@ -562,7 +562,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
if !isEitherUserIdOrEmailSet() {
if config.enableUnknownUserActivation {
ITBInfo("UUA ENABLED - unknown user track purchase")
unknownUserManager.trackUnknownUserPurchaseEvent(total: total, items: items, dataFields: dataFields)
unknownUserManager.trackUnknownPurchaseEvent(total: total, items: items, dataFields: dataFields)
}
return rejectWithInitializationError(onFailure: onFailure)
}
Expand Down Expand Up @@ -636,7 +636,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
if !isEitherUserIdOrEmailSet() && localStorage.userIdUnknownUser == nil {
if config.enableUnknownUserActivation {
ITBInfo("UUA ENABLED - unknown user track custom event")
unknownUserManager.trackUnknownUserEvent(name: eventName, dataFields: dataFields)
unknownUserManager.trackUnknownEvent(name: eventName, dataFields: dataFields)
}
return rejectWithInitializationError(onFailure: onFailure)
}
Expand Down Expand Up @@ -1072,7 +1072,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
&& (currentTime - unknownUserManager.getLastCriteriaFetch() >= Const.criteriaFetchingCooldown) {

unknownUserManager.updateLastCriteriaFetch(currentTime: currentTime)
unknownUserManager.getUnknownUserCriteria()
unknownUserManager.getUnknownCriteria()
ITBInfo("Fetching unknown user criteria - Foreground")
}
}
Expand Down
19 changes: 16 additions & 3 deletions swift-sdk/Internal/IterableIdentityResolution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,24 @@ import Foundation
public var replayOnVisitorToKnown: Bool?

/// When true, merges the unknown user profile with the known user profile on identification.
public let mergeOnUnknownUserToKnown: Bool?
public let mergeOnUnknownToKnown: Bool?

public init(replayOnVisitorToKnown: Bool?,
mergeOnUnknownUserToKnown: Bool?) {
mergeOnUnknownToKnown: Bool?) {
self.replayOnVisitorToKnown = replayOnVisitorToKnown
self.mergeOnUnknownUserToKnown = mergeOnUnknownUserToKnown
self.mergeOnUnknownToKnown = mergeOnUnknownToKnown
}

// MARK: - Deprecated alias (remove in next major)

/// - Note: This property is kept for backward compatibility. Use ``mergeOnUnknownToKnown`` instead.
@available(*, deprecated, renamed: "mergeOnUnknownToKnown")
public var mergeOnUnknownUserToKnown: Bool? { mergeOnUnknownToKnown }

@available(*, deprecated, renamed: "init(replayOnVisitorToKnown:mergeOnUnknownToKnown:)")
public convenience init(replayOnVisitorToKnown: Bool?,
mergeOnUnknownUserToKnown: Bool?) {
self.init(replayOnVisitorToKnown: replayOnVisitorToKnown,
mergeOnUnknownToKnown: mergeOnUnknownUserToKnown)
}
}
40 changes: 37 additions & 3 deletions swift-sdk/Internal/IterableUserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,53 @@ class IterableUserDefaults {

var unknownUserEvents: [[AnyHashable: Any]]? {
get {
return eventData(withKey: .unknownUserEvents)
guard let raw = eventData(withKey: .unknownUserEvents) else { return nil }
let (normalized, didMigrate) = Self.normalizeLegacyEventTypeKey(in: raw)
if didMigrate {
saveEventData(unknownUserEvents: normalized, withKey: .unknownUserEvents)
}
return normalized
} set {
saveEventData(unknownUserEvents: newValue, withKey: .unknownUserEvents)
}
}

var unknownUserUpdate: [AnyHashable: Any]? {
get {
return userUpdateData(withKey: .unknownUserUpdate)
guard let raw = userUpdateData(withKey: .unknownUserUpdate) else { return nil }
let (normalized, didMigrate) = Self.normalizeLegacyEventTypeKey(in: raw)
if didMigrate {
saveUserUpdate(normalized, withKey: .unknownUserUpdate)
}
return normalized
} set {
saveUserUpdate(newValue, withKey: .unknownUserUpdate)
}
}

/// Migrates locally stored entries written prior to UUA naming normalization,
/// where the event-type discriminator was keyed as `"dataType"`. Returns the
/// normalized payload and whether any migration occurred.
private static func normalizeLegacyEventTypeKey(in events: [[AnyHashable: Any]]) -> ([[AnyHashable: Any]], Bool) {
var didMigrate = false
let normalized = events.map { event -> [AnyHashable: Any] in
let (out, migrated) = normalizeLegacyEventTypeKey(in: event)
if migrated { didMigrate = true }
return out
}
return (normalized, didMigrate)
}

private static func normalizeLegacyEventTypeKey(in event: [AnyHashable: Any]) -> ([AnyHashable: Any], Bool) {
guard event[JsonKey.eventType] == nil,
let legacy = event[JsonKey.legacyEventType] else {
return (event, false)
}
var migrated = event
migrated[JsonKey.eventType] = legacy
migrated.removeValue(forKey: JsonKey.legacyEventType)
return (migrated, true)
}

var criteriaData: Data? {
get {
Expand Down
39 changes: 39 additions & 0 deletions swift-sdk/Internal/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@ struct IterableUnknownUserSessions: Codable {
var totalUnknownUserSessionCount: Int
var lastUnknownUserSession: Int
var firstUnknownUserSession: Int

// Cross-SDK alignment (SDK-412): backend payload uses the de-"User"'d
// names that Android already sends. Swift property names stay put to keep
// call sites unchanged.
enum CodingKeys: String, CodingKey {
case totalUnknownUserSessionCount = "totalUnknownSessionCount"
case lastUnknownUserSession = "lastUnknownSession"
case firstUnknownUserSession = "firstUnknownSession"
}

// Legacy keys, kept only so on-disk blobs written by pre-SDK-412 builds
// continue to decode after upgrade.
private enum LegacyCodingKeys: String, CodingKey {
case totalUnknownUserSessionCount
case lastUnknownUserSession
case firstUnknownUserSession
}

init(totalUnknownUserSessionCount: Int,
lastUnknownUserSession: Int,
firstUnknownUserSession: Int) {
self.totalUnknownUserSessionCount = totalUnknownUserSessionCount
self.lastUnknownUserSession = lastUnknownUserSession
self.firstUnknownUserSession = firstUnknownUserSession
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let count = try container.decodeIfPresent(Int.self, forKey: .totalUnknownUserSessionCount) {
self.totalUnknownUserSessionCount = count
self.lastUnknownUserSession = try container.decode(Int.self, forKey: .lastUnknownUserSession)
self.firstUnknownUserSession = try container.decode(Int.self, forKey: .firstUnknownUserSession)
} else {
let legacy = try decoder.container(keyedBy: LegacyCodingKeys.self)
self.totalUnknownUserSessionCount = try legacy.decode(Int.self, forKey: .totalUnknownUserSessionCount)
self.lastUnknownUserSession = try legacy.decode(Int.self, forKey: .lastUnknownUserSession)
self.firstUnknownUserSession = try legacy.decode(Int.self, forKey: .firstUnknownUserSession)
}
}
}

struct IterableUnknownUserSessionsWrapper: Codable {
Expand Down
Loading
Loading