Skip to content

[MOB-11903] consent logging refactoring #938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: feature/itbl_track_anon_user
Choose a base branch
from
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
96 changes: 20 additions & 76 deletions swift-sdk/Internal/InternalIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -159,18 +149,21 @@ 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
}
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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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?([:])
}

Expand Down
89 changes: 67 additions & 22 deletions swift-sdk/Internal/UnknownUserManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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)")
}
}
}
4 changes: 4 additions & 0 deletions swift-sdk/Internal/UnknownUserManagerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Loading