diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index 22fdef9..c674389 100644 --- a/ios/Classes/HelperClasses/Constants.swift +++ b/ios/Classes/HelperClasses/Constants.swift @@ -38,6 +38,14 @@ struct API { static let removeNotificationListener = "removeNotificationListener" static let clearNotificationListeners = "clearNotificationListeners" static let clearAllNotificationListeners = "clearAllNotificationListeners" + + // ODP + static let sendOdpEvent = "sendOdpEvent" + static let getVuid = "getVuid" + static let getQualifiedSegments = "getQualifiedSegments" + static let setQualifiedSegments = "setQualifiedSegments" + static let isQualifiedFor = "isQualifiedFor" + static let fetchQualifiedSegments = "fetchQualifiedSegments" } struct NotificationType { @@ -56,6 +64,11 @@ struct DecideOption { static let excludeVariables = "excludeVariables" } +struct SegmentOption { + static let ignoreCache = "ignoreCache" + static let resetCache = "resetCache" +} + struct RequestParameterKey { static let sdkKey = "sdkKey" static let userId = "userId" @@ -83,6 +96,23 @@ struct RequestParameterKey { static let datafilePeriodicDownloadInterval = "datafilePeriodicDownloadInterval" static let datafileHostPrefix = "datafileHostPrefix" static let datafileHostSuffix = "datafileHostSuffix" + + // ODP + static let vuid = "vuid" + static let qualifiedSegments = "qualifiedSegments" + static let segment = "segment" + static let action = "action" + static let identifiers = "identifiers" + static let data = "data" + static let type = "type" + static let optimizelySegmentOption = "optimizelySegmentOption" + + static let optimizelySdkSettings = "optimizelySdkSettings" + static let segmentsCacheSize = "segmentsCacheSize" + static let segmentsCacheTimeoutInSecs = "segmentsCacheTimeoutInSecs" + static let timeoutForSegmentFetchInSecs = "timeoutForSegmentFetchInSecs" + static let timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs" + static let disableOdp = "disableOdp" } struct ResponseKey { @@ -97,6 +127,7 @@ struct ErrorMessage { static let optimizelyConfigNotFound = "No optimizely config found." static let optlyClientNotFound = "Optimizely client not found." static let userContextNotFound = "User context not found." + static let qualifiedSegmentsNotFound = "Qualified Segments not found." } //Sohail: There is one issue, can we make sure the types remain same, probably we will need to write unit test separately for type. diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/Classes/HelperClasses/Utils.swift index 7062bae..82f858a 100644 --- a/ios/Classes/HelperClasses/Utils.swift +++ b/ios/Classes/HelperClasses/Utils.swift @@ -152,6 +152,24 @@ public class Utils: NSObject { return convertedOptions } + /// Converts and returns string segment options to array of OptimizelySegmentOption + static func getSegmentOptions(options: [String]?) -> [OptimizelySegmentOption]? { + guard let finalOptions = options else { + return nil + } + var convertedOptions = [OptimizelySegmentOption]() + for option in finalOptions { + switch option { + case SegmentOption.ignoreCache: + convertedOptions.append(OptimizelySegmentOption.ignoreCache) + case SegmentOption.resetCache: + convertedOptions.append(OptimizelySegmentOption.resetCache) + default: break + } + } + return convertedOptions + } + static func convertDecisionToDictionary(decision: OptimizelyDecision?) -> [String: Any?] { let userContext: [String: Any?] = [RequestParameterKey.userId : decision?.userContext.userId, diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 2d1e594..89bac9e 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -45,7 +45,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { /// Part of FlutterPlugin protocol to handle communication with flutter sdk public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - + switch call.method { case API.initialize: initialize(call, result: result) case API.addNotificationListener: addNotificationListener(call, result: result) @@ -67,6 +67,14 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { case API.removeForcedDecision: removeForcedDecision(call, result: result) case API.removeAllForcedDecisions: removeAllForcedDecisions(call, result: result) case API.close: close(call, result: result) + + // ODP + case API.getQualifiedSegments: getQualifiedSegments(call, result: result) + case API.setQualifiedSegments: setQualifiedSegments(call, result: result) + case API.getVuid: getVuid(call, result: result) + case API.isQualifiedFor: isQualifiedFor(call, result: result) + case API.sendOdpEvent: sendOdpEvent(call, result: result) + case API.fetchQualifiedSegments: fetchQualifiedSegments(call, result: result) default: result(FlutterMethodNotImplemented) } } @@ -99,6 +107,31 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { } let defaultDecideOptions = Utils.getDecideOptions(options: decideOptions) + // SDK Settings Default Values + var segmentsCacheSize: Int = 100 + var segmentsCacheTimeoutInSecs: Int = 600 + var timeoutForSegmentFetchInSecs: Int = 10 + var timeoutForOdpEventInSecs: Int = 10 + var disableOdp: Bool = false + if let sdkSettings = parameters[RequestParameterKey.optimizelySdkSettings] as? Dictionary { + if let cacheSize = sdkSettings[RequestParameterKey.segmentsCacheSize] as? Int { + segmentsCacheSize = cacheSize + } + if let segmentsCacheTimeout = sdkSettings[RequestParameterKey.segmentsCacheTimeoutInSecs] as? Int { + segmentsCacheTimeoutInSecs = segmentsCacheTimeout + } + if let timeoutForSegmentFetch = sdkSettings[RequestParameterKey.timeoutForSegmentFetchInSecs] as? Int { + timeoutForSegmentFetchInSecs = timeoutForSegmentFetch + } + if let timeoutForOdpEvent = sdkSettings[RequestParameterKey.timeoutForOdpEventInSecs] as? Int { + timeoutForOdpEventInSecs = timeoutForOdpEvent + } + if let isOdpDisabled = sdkSettings[RequestParameterKey.disableOdp] as? Bool { + disableOdp = isOdpDisabled + } + } + let optimizelySdkSettings = OptimizelySdkSettings(segmentsCacheSize: segmentsCacheSize, segmentsCacheTimeoutInSecs: segmentsCacheTimeoutInSecs, timeoutForSegmentFetchInSecs: timeoutForSegmentFetchInSecs, timeoutForOdpEventInSecs: timeoutForOdpEventInSecs, disableOdp: disableOdp) + // Datafile Download Interval var datafilePeriodicDownloadInterval = 10 * 60 // seconds @@ -119,7 +152,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { optimizelyClientsTracker.removeValue(forKey: sdkKey) // Creating new instance - let optimizelyInstance = OptimizelyClient(sdkKey:sdkKey, eventDispatcher: eventDispatcher, datafileHandler: datafileHandler, periodicDownloadInterval: datafilePeriodicDownloadInterval, defaultDecideOptions: defaultDecideOptions) + let optimizelyInstance = OptimizelyClient(sdkKey:sdkKey, eventDispatcher: eventDispatcher, datafileHandler: datafileHandler, periodicDownloadInterval: datafilePeriodicDownloadInterval, defaultDecideOptions: defaultDecideOptions, settings: optimizelySdkSettings) optimizelyInstance.start{ [weak self] res in switch res { @@ -198,7 +231,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { return } - + if let type = parameters[RequestParameterKey.notificationType] as? String, let convertedNotificationType = Utils.getNotificationType(type: type) { // Remove listeners only for the provided type optimizelyClient.notificationCenter?.clearNotificationListeners(type: convertedNotificationType) @@ -302,7 +335,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { let success = optimizelyClient.setForcedVariation(experimentKey: experimentKey, userId: userId, variationKey: variationKey) result(self.createResponse(success: success)) } - + /// Creates a context of the user for which decision APIs will be called. /// A user context will only be created successfully when the SDK is fully configured using initializeClient. func createUserContext(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -312,13 +345,15 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { return } - guard let userId = parameters[RequestParameterKey.userId] as? String else { - result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) - return - } let userContextId = uuid - let userContext = optimizelyClient.createUserContext(userId: userId, attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any)) + var userContext: OptimizelyUserContext! + + if let userId = parameters[RequestParameterKey.userId] as? String { + userContext = optimizelyClient.createUserContext(userId: userId, attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any)) + } else { + userContext = optimizelyClient.createUserContext(attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any)) + } if userContextsTracker[sdkKey] != nil { userContextsTracker[sdkKey]![userContextId] = userContext } else { @@ -359,6 +394,108 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { result(createResponse(success: true)) } + /// Returns an array of segments that the user is qualified for. + func getQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (_, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + guard let qualifiedSegments = userContext.qualifiedSegments else { + result(createResponse(success: false, reason: ErrorMessage.qualifiedSegmentsNotFound)) + return + } + result(createResponse(success: true, result: [RequestParameterKey.qualifiedSegments: qualifiedSegments])) + } + + /// Sets qualified segments for the user context. + func setQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + guard let qualifiedSegments = parameters[RequestParameterKey.qualifiedSegments] as? [String] else { + result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) + return + } + userContext.qualifiedSegments = qualifiedSegments + result(createResponse(success: true)) + } + + /// Returns the device vuid. + func getVuid(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (_, sdkKey) = getParametersAndSdkKey(arguments: call.arguments, result: result) else { + return + } + guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { + return + } + result(self.createResponse(success: true, result: [RequestParameterKey.vuid: optimizelyClient.vuid])) + } + + /// Checks if the user is qualified for the given segment. + func isQualifiedFor(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + guard let segment = parameters[RequestParameterKey.segment] as? String else { + result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) + return + } + result(self.createResponse(success: userContext.isQualifiedFor(segment: segment))) + } + + /// Send an event to the ODP server. + func sendOdpEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, sdkKey) = getParametersAndSdkKey(arguments: call.arguments, result: result) else { + return + } + guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { + return + } + guard let action = parameters[RequestParameterKey.action] as? String else { + result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) + return + } + + var type: String? + var identifiers: [String: String] = [:] + var data: [String: Any?] = [:] + + if let _type = parameters[RequestParameterKey.type] as? String { + type = _type + } + if let _identifiers = parameters[RequestParameterKey.identifiers] as? Dictionary { + identifiers = _identifiers + } + if let _data = Utils.getTypedMap(arguments: parameters[RequestParameterKey.data] as? Any) { + data = _data + } + + do { + try optimizelyClient.sendOdpEvent(type: type, action: action, identifiers: identifiers, data: data) + result(self.createResponse(success: true)) + } catch { + result(self.createResponse(success: false, reason: error.localizedDescription)) + } + } + + /// Fetch all qualified segments for the user context. + func fetchQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + var segmentOptions: [String]? + if let options = parameters[RequestParameterKey.optimizelySegmentOption] as? [String] { + segmentOptions = options + } + + let options = Utils.getSegmentOptions(options: segmentOptions) + do { + try userContext.fetchQualifiedSegments(options: options ?? []) + result(createResponse(success: true)) + } catch { + result(self.createResponse(success: false, reason: error.localizedDescription)) + } + } + /// Tracks an event. func trackEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { diff --git a/ios/optimizely_flutter_sdk.podspec b/ios/optimizely_flutter_sdk.podspec index 15d0f02..2a528d5 100644 --- a/ios/optimizely_flutter_sdk.podspec +++ b/ios/optimizely_flutter_sdk.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'OptimizelySwiftSDK', '3.10.1' + s.dependency 'OptimizelySwiftSDK', '4.0.0-beta' s.platform = :ios, '10.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/lib/src/data_objects/get_qualified_segments_response.dart b/lib/src/data_objects/get_qualified_segments_response.dart index 130c79e..e687ebe 100644 --- a/lib/src/data_objects/get_qualified_segments_response.dart +++ b/lib/src/data_objects/get_qualified_segments_response.dart @@ -18,9 +18,10 @@ import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; class GetQualifiedSegmentsResponse extends BaseResponse { - List qualifiedSegments = []; + List? qualifiedSegments = []; GetQualifiedSegmentsResponse(Map json) : super(json) { + qualifiedSegments = null; if (json[Constants.responseResult] is Map) { var response = Map.from(json[Constants.responseResult]); if (response[Constants.qualifiedSegments] is List) {