diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index d52f8c2a1..a8a3a9de6 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -48,17 +48,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { } } - // MARK: - Pending Consent Tracking - - /// Holds consent data that should be sent once user creation is confirmed - private struct PendingConsentData { - let consentTimestamp: Int64 - let email: String? - let userId: String? - let isUserKnown: Bool - } - - private var pendingConsentData: PendingConsentData? + var deviceMetadata: DeviceMetadata { DeviceMetadata(deviceId: deviceId, @@ -159,6 +149,14 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { self._email = email self._userId = nil + // Prepare consent for replay scenario before any async login/registration flow + if config.enableUnknownUserActivation, let email = email { + let replayPref = identityResolution?.replayOnVisitorToKnown ?? config.identityResolution.replayOnVisitorToKnown + if let replayPref, replayPref, localStorage.userIdUnknownUser == nil { + unknownUserManager.prepareConsent(email: email, userId: nil, isUserKnown: true) + } + } + self.onLogin(authToken) { [weak self] in guard let config = self?.config else { return @@ -166,11 +164,6 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { let merge = identityResolution?.mergeOnUnknownUserToKnown ?? config.identityResolution.mergeOnUnknownUserToKnown let replay = identityResolution?.replayOnVisitorToKnown ?? config.identityResolution.replayOnVisitorToKnown if config.enableUnknownUserActivation, let email = email { - // Prepare consent for replay scenario before merge - // Check if this is truly a replay scenario (no existing anonymous user before merge) - if let replay, replay, self?.localStorage.userIdUnknownUser == nil { - self?.prepareConsent(email: email, userId: nil) - } self?.attemptAndProcessMerge( merge: merge ?? true, @@ -208,6 +201,14 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { self._email = nil self._userId = userId + // Prepare consent for replay scenario before any async login/registration flow + if config.enableUnknownUserActivation { + let replayPref = identityResolution?.replayOnVisitorToKnown ?? config.identityResolution.replayOnVisitorToKnown + if let userId, let replayPref, replayPref, localStorage.userIdUnknownUser == nil { + unknownUserManager.prepareConsent(email: nil, userId: userId, isUserKnown: true) + } + } + self.onLogin(authToken) { [weak self] in guard let config = self?.config else { return @@ -217,12 +218,6 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { let merge = identityResolution?.mergeOnUnknownUserToKnown ?? config.identityResolution.mergeOnUnknownUserToKnown let replay = identityResolution?.replayOnVisitorToKnown ?? config.identityResolution.replayOnVisitorToKnown - // Prepare consent for replay scenario before merge - // Check if this is truly a replay scenario (no existing anonymous user before merge) - if let replay, replay, self?.localStorage.userIdUnknownUser == nil { - self?.prepareConsent(email: nil, userId: userId) - } - self?.attemptAndProcessMerge( merge: merge ?? true, replay: replay ?? true, @@ -290,58 +285,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { return self.localStorage.visitorUsageTracked } - /// Prepares consent data to be sent when user registration is confirmed during "replay scenario". - /// - /// A "replay scenario" occurs when a user signs up or logs in but does not meet the criteria - /// for immediate consent tracking. This method stores consent data to be sent once user - /// registration is confirmed through the registration success callback. - /// - /// This method is typically called during user sign-up or sign-in processes to ensure that - /// consent data is properly recorded for compliance and analytics purposes. - private func prepareConsent(email: String?, userId: String?) { - guard let consentTimestamp = localStorage.visitorConsentTimestamp else { - return - } - - // Only prepare consent if we have previous anonymous tracking consent but no anonymous user ID - guard localStorage.userIdUnknownUser == nil && localStorage.visitorUsageTracked else { - return - } - - // Store the consent data to be sent when user registration is confirmed - pendingConsentData = PendingConsentData( - consentTimestamp: consentTimestamp, - email: email, - userId: userId, - isUserKnown: true - ) - - ITBInfo("Consent data prepared for replay scenario - will send after user registration is confirmed") - } - /// Sends any pending consent data now that user creation is confirmed - private func sendPendingConsent() { - guard let consentData = pendingConsentData else { - ITBDebug("No pending consent to send") - return - } - - ITBDebug("Sending pending consent after user registration: email set=\(consentData.email != nil), userId set=\(consentData.userId != nil), timestamp=\(consentData.consentTimestamp)") - - apiClient.trackConsent( - consentTimestamp: consentData.consentTimestamp, - email: consentData.email, - userId: consentData.userId, - isUserKnown: consentData.isUserKnown - ).onSuccess { _ in - ITBInfo("Pending consent tracked successfully after user registration") - }.onError { error in - ITBError("Failed to track pending consent after user registration: \(error)") - } - - // Clear the pending consent data - pendingConsentData = nil - } // MARK: - API Request Calls @@ -382,14 +326,14 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onSuccess: { (_ data: [AnyHashable: Any]?) in // Send any pending consent now that user registration is confirmed ITBDebug("Device registration succeeded; attempting to send pending consent if any") - self.sendPendingConsent() + self.unknownUserManager.sendPendingConsent() self._successCallback?(data) onSuccess?(data) }, onFailure: { (_ reason: String?, _ data: Data?) in // Clear any pending consent on failure ITBDebug("Device registration failed; clearing any pending consent") - self.pendingConsentData = nil + self.unknownUserManager.clearPendingConsent() self._failureCallback?(reason, data) onFailure?(reason, data) } @@ -897,7 +841,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { // If auto push registration is disabled, send pending consent here // since register() won't be called automatically ITBDebug("Auto push registration disabled; attempting to send pending consent after login") - sendPendingConsent() + unknownUserManager.sendPendingConsent() _successCallback?([:]) } diff --git a/swift-sdk/Internal/UnknownUserManager.swift b/swift-sdk/Internal/UnknownUserManager.swift index fe5bf03da..9e7b67c05 100644 --- a/swift-sdk/Internal/UnknownUserManager.swift +++ b/swift-sdk/Internal/UnknownUserManager.swift @@ -30,6 +30,16 @@ public class UnknownUserManager: UnknownUserManagerProtocol { private(set) var lastCriteriaFetch: Double = 0 private var isCriteriaMatched = false + // MARK: - Pending Consent Tracking + /// Holds consent data that should be sent once user creation or registration is confirmed + private struct PendingConsentData { + let consentTimestamp: Int64 + let email: String? + let userId: String? + let isUserKnown: Bool + } + private var pendingConsentData: PendingConsentData? + /// Tracks an unknown user event and store it locally public func trackUnknownUserEvent(name: String, dataFields: [AnyHashable: Any]?) { var body = [AnyHashable: Any]() @@ -87,6 +97,60 @@ public class UnknownUserManager: UnknownUserManagerProtocol { localStorage.unknownUserSessions = unknownUserSessionWrapper } } + + // MARK: - Consent helpers (moved from InternalIterableAPI) + /// Prepares consent data to be sent when user registration is confirmed during "replay scenario". + public func prepareConsent(email: String?, userId: String?, isUserKnown: Bool) { + guard let consentTimestamp = localStorage.visitorConsentTimestamp else { + return + } + + // prepare consent if we have previous anonymous tracking consent + guard localStorage.visitorUsageTracked else { + return + } + + pendingConsentData = PendingConsentData( + consentTimestamp: consentTimestamp, + email: email, + userId: userId, + isUserKnown: isUserKnown + ) + ITBInfo("Prepared pending consent for replay scenario - will send after user registration is confirmed") + } + + /// Sends any pending consent data now that user creation/registration is confirmed + public func sendPendingConsent() { + guard let consentData = pendingConsentData else { + ITBDebug("No pending consent to send") + return + } + + ITBDebug("Sending pending consent: email set=\(consentData.email != nil), userId set=\(consentData.userId != nil), known=\(consentData.isUserKnown), timestamp=\(consentData.consentTimestamp)") + + IterableAPI.implementation?.apiClient.trackConsent( + consentTimestamp: consentData.consentTimestamp, + email: consentData.email, + userId: consentData.userId, + isUserKnown: consentData.isUserKnown + ).onSuccess { _ in + ITBInfo("Pending consent tracked successfully") + }.onError { error in + ITBError("Failed to track pending consent: \(error)") + } + + // Clear the pending consent data + pendingConsentData = nil + } + + /// Clears any pending consent data without sending + public func clearPendingConsent() { + pendingConsentData = nil + } + + + + // Removed build-and-send overload; callers should `prepareConsent(..., isUserKnown:)` then `sendPendingConsent()` /// Syncs unsynced data which might have failed to sync when calling syncEvents for the first time after criterias met public func syncNonSyncedEvents() { @@ -191,10 +255,10 @@ public class UnknownUserManager: UnknownUserManagerProtocol { self.localStorage.userIdUnknownUser = userId self.config.unknownUserHandler?.onUnknownUserCreated(userId: userId) - IterableAPI.implementation?.setUserId(userId, isUnknownUser: true) + // Prepare and send consent after anonymous session creation + self.prepareConsent(email: nil, userId: userId, isUserKnown: false) - // Send consent data after session creation - self.sendConsentAfterCriteriaMatch(userId: userId) + IterableAPI.implementation?.setUserId(userId, isUnknownUser: true) self.syncNonSyncedEvents() } @@ -269,23 +333,4 @@ public class UnknownUserManager: UnknownUserManagerProtocol { localStorage.unknownUserEvents = eventsDataObjects } - - /// Sends consent data after user meets criteria and anonymous user is created - private func sendConsentAfterCriteriaMatch(userId: String) { - guard let consentTimestamp = localStorage.visitorConsentTimestamp else { - ITBInfo("No consent timestamp found, skipping consent tracking") - return - } - - IterableAPI.implementation?.apiClient.trackConsent( - consentTimestamp: consentTimestamp, - email: nil, - userId: userId, - isUserKnown: false - ).onSuccess { _ in - ITBInfo("Consent tracked successfully for criteria match") - }.onError { error in - ITBError("Failed to track consent for criteria match: \(error)") - } - } } diff --git a/swift-sdk/Internal/UnknownUserManagerProtocol.swift b/swift-sdk/Internal/UnknownUserManagerProtocol.swift index 5627f1ee3..aa7709dcf 100644 --- a/swift-sdk/Internal/UnknownUserManagerProtocol.swift +++ b/swift-sdk/Internal/UnknownUserManagerProtocol.swift @@ -17,4 +17,8 @@ import Foundation func getUnknownUserCriteria() func syncEvents() func clearVisitorEventsAndUserData() + // Consent helpers + func prepareConsent(email: String?, userId: String?, isUserKnown: Bool) + func sendPendingConsent() + func clearPendingConsent() }