diff --git a/FlagShip.podspec b/FlagShip.podspec index 21a2c651..c754181a 100644 --- a/FlagShip.podspec +++ b/FlagShip.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = "FlagShip" - s.version = "5.0.0-beta.6" + s.version = "5.1.0-beta.1" s.summary = "Flagship SDK" # This description is used to generate tags and improve search results. diff --git a/FlagShip/FlagShipTests/FSEmotion/FSettingsTest.swift b/FlagShip/FlagShipTests/FSEmotion/FSettingsTest.swift index e7a0577c..0ef639b2 100644 --- a/FlagShip/FlagShipTests/FSEmotion/FSettingsTest.swift +++ b/FlagShip/FlagShipTests/FSEmotion/FSettingsTest.swift @@ -78,8 +78,7 @@ final class FSettingsTest: XCTestCase { XCTAssertEqual(score, "Immediacy") expectationSync.fulfill() - } - wait(for: [expectationSync], timeout: 5.0) + wait(for: [expectationSync], timeout: 6.0) } } diff --git a/FlagShip/Flagship.xcodeproj/project.pbxproj b/FlagShip/Flagship.xcodeproj/project.pbxproj index 1156e149..248c3781 100644 --- a/FlagShip/Flagship.xcodeproj/project.pbxproj +++ b/FlagShip/Flagship.xcodeproj/project.pbxproj @@ -1880,7 +1880,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "5.0.0-beta.6"; + MARKETING_VERSION = "5.1.0-beta.1"; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ABTasty.FlagShip; @@ -1914,7 +1914,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "5.0.0-beta.6"; + MARKETING_VERSION = "5.1.0-beta.1"; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ABTasty.FlagShip; diff --git a/FlagShip/Source/Cache/FSCacheDelegate.swift b/FlagShip/Source/Cache/FSCacheDelegate.swift index cf46ca47..78a61b78 100644 --- a/FlagShip/Source/Cache/FSCacheDelegate.swift +++ b/FlagShip/Source/Cache/FSCacheDelegate.swift @@ -17,6 +17,10 @@ import Foundation /// Called when a visitor set consent to false. Must erase visitor data related to the given visitor /// Id from the database. func flushVisitor(visitorId: String) + + /// Called to check if the visitor data is already in the database + @objc optional + func isVisitorCacheExist(visitorId: String) -> Bool } @objc public protocol FSHitCacheDelegate { diff --git a/FlagShip/Source/Cache/FSCacheManager.swift b/FlagShip/Source/Cache/FSCacheManager.swift index 36561d2f..cddb8f32 100644 --- a/FlagShip/Source/Cache/FSCacheManager.swift +++ b/FlagShip/Source/Cache/FSCacheManager.swift @@ -38,6 +38,26 @@ import Foundation } /// Cache the visitor + /// + /// + + /// Check if visitor cache exists for the given visitor ID + /// - Parameter visitorId: The visitor identifier to check + /// - Returns: `true` if cache exists, `false` otherwise (including if delegate is not set) + func isVisitorCacheExist(_ visitorId: String) -> Bool { + guard let visitorDelegate = cacheVisitorDelegate else { + FlagshipLogManager.Log( + level: .WARNING, + tag: .STORAGE, + messageToDisplay: FSLogMessage.MESSAGE("Cache delegate is not set") + ) + return false + } + + // Call optional protocol method with safe unwrapping + return visitorDelegate.isVisitorCacheExist?(visitorId: visitorId) ?? false + } + /// - Parameter visitor: visitor instance func cacheVisitor(_ visitor: FSVisitor) { /// Create visitor cache object @@ -58,7 +78,7 @@ import Foundation /// - Parameters: /// - visitoId: id of the visitor /// - onCompletion: callback ob finishing the job - public func lookupVisitorCache(visitoId: String, onCompletion: @escaping (Error?, FSCacheVisitor?)->Void) { + public func lookupVisitorCache(visitoId: String, onCompletion: @escaping (FlagshipError?, FSCacheVisitor?) -> Void) { /// Create a thread let fsCacheQueue = DispatchQueue(label: "com.flagshipCache.queue", attributes: .concurrent) /// Init the semaphore @@ -72,10 +92,10 @@ import Foundation let result = try JSONDecoder().decode(FSCacheVisitor.self, from: dataJson) onCompletion(nil, result) } catch { - onCompletion(error, nil) + onCompletion(FlagshipError(message: "Error on decode visitor data from cache", type: .internalError, code: 400), nil) } } else { - onCompletion(FlagshipError(type: .internalError, code: 400), nil) + onCompletion(FlagshipError(message: "The visitorId \(visitoId) not found in cache", type: .internalError, code: 404), nil) } semaphore.signal() } @@ -102,7 +122,7 @@ import Foundation hitCacheDelegate?.cacheHits(hits: hits) } - func lookupHits(onCompletion: @escaping (Error?, [FSTrackingProtocol]?)->Void) { + func lookupHits(onCompletion: @escaping (Error?, [FSTrackingProtocol]?) -> Void) { /// Create a Thread let fsHitCacheQueue = DispatchQueue(label: "com.flagshipLookupHitCache.queue", attributes: .concurrent) /// Init the semaphore diff --git a/FlagShip/Source/Cache/FSDefaultCache.swift b/FlagShip/Source/Cache/FSDefaultCache.swift index a7c56bcc..6b603cf1 100644 --- a/FlagShip/Source/Cache/FSDefaultCache.swift +++ b/FlagShip/Source/Cache/FSDefaultCache.swift @@ -41,6 +41,10 @@ public class FSDefaultCacheVisitor: FSVisitorCacheDelegate { public func flushVisitor(visitorId: String) { dbMgt_visitor.delete(idItemToDelete: visitorId) } + + public func isVisitorCacheExist(visitorId: String) -> Bool { + return dbMgt_visitor.isVisitorExist(visitorId) + } } ////////////////////////////|| diff --git a/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift b/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift index ac2fef03..723a2166 100644 --- a/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift +++ b/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift @@ -15,10 +15,6 @@ public extension FSVisitor { /// - Important: After using this method, you should use Flagship.fetchFlags method to update the visitor informations /// - Requires: Make sure that the experience continuity option is enabled on the flagship platform before using this method @objc func authenticate(visitorId: String) { - if configManager.flagshipConfig.mode != .DECISION_API { - FlagshipLogManager.Log(level: .ALL, tag: .AUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_AUTHENTICATE) - return - } self.strategy?.getStrategy().authenticateVisitor(visitorId: visitorId) self.updateStateAndTriggerCallback(true) // Troubleshooting xpc @@ -27,10 +23,6 @@ public extension FSVisitor { /// Use authenticate methode to go from Logged in session to logged out session @objc func unauthenticate() { - if configManager.flagshipConfig.mode != .DECISION_API { - FlagshipLogManager.Log(level: .ALL, tag: .UNAUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_AUTHENTICATE) - return - } self.strategy?.getStrategy().unAuthenticateVisitor() self.updateStateAndTriggerCallback(false) // Troubleshooting xpc @@ -43,4 +35,33 @@ public extension FSVisitor { // Set the fetch state to required state self.fetchStatus = .FETCH_REQUIRED } + + // Copy method actually used only in bucketing mode - that explain why we put here in this extension + func copy() -> FSVisitor { + let copiedVisitor = FSVisitor( + aVisitorId: self.visitorId, + aContext: self.context.getCurrentContext(), + aConfigManager: self.configManager, + aHasConsented: self.hasConsented, + aIsAuthenticated: self.isAuthenticated, + pOnFlagStatusChanged: self._onFlagStatusChanged, + pOnFlagStatusFetchRequired: self._onFlagStatusFetchRequired, + pOnFlagStatusFetched: self._onFlagStatusFetched + ) + + // Copy additional properties + copiedVisitor.anonymousId = self.anonymousId + copiedVisitor.currentFlags = self.currentFlags + copiedVisitor.assignedVariationHistory = self.assignedVariationHistory + copiedVisitor.requiredFetchReason = self.requiredFetchReason + copiedVisitor.eaiVisitorScored = self.eaiVisitorScored + copiedVisitor.emotionScoreAI = self.emotionScoreAI + copiedVisitor.fetchStatus = self.fetchStatus + + // Copy strategy if needed + if let strategy = self.strategy { + copiedVisitor.strategy = FSStrategy(copiedVisitor) + } + return copiedVisitor + } } diff --git a/FlagShip/Source/Core/FSVisitor.swift b/FlagShip/Source/Core/FSVisitor.swift index 05fe1adf..1bb156de 100644 --- a/FlagShip/Source/Core/FSVisitor.swift +++ b/FlagShip/Source/Core/FSVisitor.swift @@ -143,6 +143,11 @@ import Foundation // Go to ING state while the fetch is ongoing self.fetchStatus = .FETCHING + + /// Look for the visitor in local storage + self.strategy?.getStrategy().lookupVisitor() + + // Synchronize the visitor self.strategy?.getStrategy().synchronize(onSyncCompleted: { state, reason in // After the synchronize completion we cache the visitor @@ -154,6 +159,16 @@ import Foundation self.sendHit(FSSegment(self.getContext())) self.context.needToUpload = false } + + // Another task for bucketing in xpc mode is to save the anonymous when has no cache + + if let ano = self.anonymousId { + if !self.configManager.flagshipConfig.cacheManager.isVisitorCacheExist(ano) { + let anoVisitor: FSVisitor = self.copy() + anoVisitor.visitorId = ano + self.configManager.flagshipConfig.cacheManager.cacheVisitor(anoVisitor) + } + } } // Update the reason status self.requiredFetchReason = reason diff --git a/FlagShip/Source/Core/Flagship.swift b/FlagShip/Source/Core/Flagship.swift index 82156e6f..724285a6 100644 --- a/FlagShip/Source/Core/Flagship.swift +++ b/FlagShip/Source/Core/Flagship.swift @@ -98,11 +98,7 @@ public class Flagship: NSObject { newVisitor.strategy = FSStrategy(newVisitor) if hasConsented { - // Read the cached visitor - newVisitor.strategy?.getStrategy().lookupVisitor() - // Read the cacheed hits from data base - newVisitor.strategy?.getStrategy().lookupHits() - + newVisitor.strategy?.getStrategy().lookupHits() } else { // user not consent then flush the cache related newVisitor.strategy?.getStrategy().flushVisitor() diff --git a/FlagShip/Source/Logger/FSError.swift b/FlagShip/Source/Logger/FSError.swift index 129a46ba..81ebc7c0 100644 --- a/FlagShip/Source/Logger/FSError.swift +++ b/FlagShip/Source/Logger/FSError.swift @@ -15,7 +15,7 @@ enum ErrorType { } // Flagship Error -class FlagshipError: Error { +public class FlagshipError: Error { var message = "" var error: ErrorType let codeError: Int diff --git a/FlagShip/Source/Storage/FSVisitorDbMgt.swift b/FlagShip/Source/Storage/FSVisitorDbMgt.swift index 2ece703c..64c7e3fd 100644 --- a/FlagShip/Source/Storage/FSVisitorDbMgt.swift +++ b/FlagShip/Source/Storage/FSVisitorDbMgt.swift @@ -10,6 +10,9 @@ import Foundation import SQLite3 class FSVisitorDbMgt: FSQLiteWrapper { + // Add this constant at the class level + private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + public init() { super.init(.DatabaseVisitor) } @@ -54,4 +57,35 @@ class FSVisitorDbMgt: FSQLiteWrapper { } return nil } + + // Add this function to FSVisitorDbMgt class + public func isVisitorExist(_ visitorId: String) -> Bool { + var queryPointer: OpaquePointer? + let queryStatementString = "SELECT COUNT(*) FROM table_visitors WHERE id = ?;" + + // Prepare the query with parameter binding for safety + guard sqlite3_prepare_v2(db_opaquePointer, queryStatementString, -1, &queryPointer, nil) == SQLITE_OK else { + FlagshipLogManager.Log(level: .ERROR, tag: .STORAGE, messageToDisplay: FSLogMessage.MESSAGE("sqlite3 prepare error")) + return false + } + + defer { + // Always cleanup the prepared statement + sqlite3_finalize(queryPointer) + } + + // Bind the visitor ID parameter (safer than string interpolation) + guard sqlite3_bind_text(queryPointer, 1, (visitorId as NSString).utf8String, -1, SQLITE_TRANSIENT) == SQLITE_OK else { + FlagshipLogManager.Log(level: .ERROR, tag: .STORAGE, messageToDisplay: FSLogMessage.MESSAGE("sqlite3 binding error")) + return false + } + + // Execute the query and get the count + if sqlite3_step(queryPointer) == SQLITE_ROW { + let count = sqlite3_column_int(queryPointer, 0) + return count > 0 + } + + return false + } } diff --git a/FlagShip/Source/Strategy/FSDefaultStrategy.swift b/FlagShip/Source/Strategy/FSDefaultStrategy.swift index f3b48465..3fe56add 100644 --- a/FlagShip/Source/Strategy/FSDefaultStrategy.swift +++ b/FlagShip/Source/Strategy/FSDefaultStrategy.swift @@ -177,39 +177,30 @@ class FSDefaultStrategy: FSDelegateStrategy { } func authenticateVisitor(visitorId: String) { - if visitor.configManager.flagshipConfig.mode == .DECISION_API { - /// Update the visitor an anonymous id - if visitor.anonymousId == nil { - visitor.anonymousId = visitor.visitorId - } + /// Update the visitor an anonymous id + if visitor.anonymousId == nil { + visitor.anonymousId = visitor.visitorId + } - // Set the authenticated visitorId - visitor.visitorId = visitorId + // Set the authenticated visitorId + visitor.visitorId = visitorId - // Update fs_users for context - visitor.context.currentContext.updateValue(visitorId, forKey: FS_USERS) - #if os(iOS) - // Update the xpc info for the emotion AI - visitor.emotionCollect?.updateTupleId(visitorId: visitor.visitorId, anonymousId: visitor.anonymousId) - #endif - } else { - FlagshipLogManager.Log(level: .ALL, tag: .AUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_AUTHENTICATE) - } + // Update fs_users for context + visitor.context.currentContext.updateValue(visitorId, forKey: FS_USERS) + #if os(iOS) + // Update the xpc info for the emotion AI + visitor.emotionCollect?.updateTupleId(visitorId: visitor.visitorId, anonymousId: visitor.anonymousId) + #endif } func unAuthenticateVisitor() { - if visitor.configManager.flagshipConfig.mode == .DECISION_API { - if let anonymId = visitor.anonymousId { - visitor.visitorId = anonymId - // Update fs_users for context - visitor.context.currentContext.updateValue(anonymId, forKey: FS_USERS) - } - - visitor.anonymousId = nil - - } else { - FlagshipLogManager.Log(level: .ALL, tag: .AUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_UNAUTHENTICATE) + if let anonymId = visitor.anonymousId { + visitor.visitorId = anonymId + // Update fs_users for context + visitor.context.currentContext.updateValue(anonymId, forKey: FS_USERS) } + + visitor.anonymousId = nil #if os(iOS) // Update the xpc info for the emotion AI visitor.emotionCollect?.updateTupleId(visitorId: visitor.visitorId, anonymousId: visitor.anonymousId) @@ -217,6 +208,11 @@ class FSDefaultStrategy: FSDelegateStrategy { } /// _ Cache Managment + + func isVistorCacheExist() -> Bool { + return visitor.configManager.flagshipConfig.cacheManager.isVisitorCacheExist(visitor.visitorId) + } + func cacheVisitor() { DispatchQueue.main.async { /// Before replacing the oldest visitor cache we should keep the oldest variation @@ -226,21 +222,40 @@ class FSDefaultStrategy: FSDelegateStrategy { /// _ Lookup visitor func lookupVisitor() { - /// Read the visitor cache from storage - visitor.configManager.flagshipConfig.cacheManager.lookupVisitorCache(visitoId: visitor.visitorId) { error, cachedVisitor in - - if error == nil { - if let aCachedVisitor = cachedVisitor { - self.visitor.mergeCachedVisitor(aCachedVisitor) - /// Get the oldest assignation history before saving and loose the information - self.visitor.assignedVariationHistory.merge(aCachedVisitor.data?.assignationHistory ?? [:]) { _, new in new } - } - } else { + var userId = visitor.visitorId + if visitor.configManager.flagshipConfig.cacheManager.isVisitorCacheExist(visitor.visitorId) == false, let anId = visitor.anonymousId, anId != visitor.visitorId { + userId = anId + } + lookupVisitorWithId(userId) + } + + // MARK: - Private Helper Methods + + private func lookupVisitorWithId(_ visitorId: String) { + visitor.configManager.flagshipConfig.cacheManager.lookupVisitorCache(visitoId: visitorId) { [weak self] error, cachedVisitor in + guard let strongSelf = self else { return } + if let cachedVisitor = cachedVisitor { + strongSelf.processCachedVisitor(cachedVisitor) + } else if let error = error { + FlagshipLogManager.Log(level: .ALL, tag: .STORAGE, messageToDisplay: FSLogMessage.MESSAGE("Failed to lookup visitor with id \(visitorId): \(error.localizedDescription)")) FlagshipLogManager.Log(level: .ALL, tag: .STORAGE, messageToDisplay: .ERROR_ON_READ_FILE) + } else { + FlagshipLogManager.Log(level: .ALL, tag: .STORAGE, messageToDisplay: FSLogMessage.MESSAGE("No cached visitor found with id \(visitorId)")) } } } + private func processCachedVisitor(_ cachedVisitor: FSCacheVisitor) { + // Ensure thread safety for visitor property modifications + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return } + strongSelf.visitor.mergeCachedVisitor(cachedVisitor) + // Safely merge assignation history + let newHistory = cachedVisitor.data?.assignationHistory ?? [:] + strongSelf.visitor.assignedVariationHistory.merge(newHistory) { _, new in new } + } + } + /// _ Flush visitor func flushVisitor() { /// Flush the visitor @@ -322,6 +337,9 @@ protocol FSDelegateStrategy { /// _Cache Managment func cacheVisitor() + /// _ Is Visitor cache Exist + func isVistorCacheExist() -> Bool + /// _ Lookup Visitor func lookupVisitor() diff --git a/FlagShip/Source/Tools/FSTools.swift b/FlagShip/Source/Tools/FSTools.swift index 6b10c379..3e8c8c8a 100644 --- a/FlagShip/Source/Tools/FSTools.swift +++ b/FlagShip/Source/Tools/FSTools.swift @@ -71,18 +71,6 @@ class FSTools: NSObject { return newVisitor } - // Is Connexion Available - class func isConnexionAvailable() -> Bool { -#if os(watchOS) - return FSTools.available -#else - let reachability = SCNetworkReachabilityCreateWithName(nil, FlagshipUniversalEndPoint) - var flags = SCNetworkReachabilityFlags() - SCNetworkReachabilityGetFlags(reachability!, &flags) - return flags.contains(.reachable) -#endif - } - class func generateUuidv4() -> String { return UUID().uuidString } diff --git a/FlagShip/Source/Tools/FlagShipVersion.swift b/FlagShip/Source/Tools/FlagShipVersion.swift index 3ea9f2fb..12e67c58 100644 --- a/FlagShip/Source/Tools/FlagShipVersion.swift +++ b/FlagShip/Source/Tools/FlagShipVersion.swift @@ -8,4 +8,4 @@ import Foundation /// This file is automatically updated 2.0.0 -public let FlagShipVersion = "5.0.0-beta.6" +public let FlagShipVersion = "5.1.0-beta.1" diff --git a/iOS Example/QApp/Base.lproj/Main.storyboard b/iOS Example/QApp/Base.lproj/Main.storyboard index 9f2c797c..2afc3ad4 100644 --- a/iOS Example/QApp/Base.lproj/Main.storyboard +++ b/iOS Example/QApp/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -22,7 +22,7 @@ - + @@ -277,7 +277,7 @@ - + - + - + @@ -338,7 +338,7 @@ - + @@ -349,7 +349,7 @@ @@ -414,33 +414,33 @@ - + - + - +