Skip to content

Commit 0ac62f3

Browse files
authored
[FSSDK-9028]: Adds native iOS implementation for ODP (#52)
1 parent 66400f3 commit 0ac62f3

File tree

5 files changed

+198
-11
lines changed

5 files changed

+198
-11
lines changed

ios/Classes/HelperClasses/Constants.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ struct API {
3838
static let removeNotificationListener = "removeNotificationListener"
3939
static let clearNotificationListeners = "clearNotificationListeners"
4040
static let clearAllNotificationListeners = "clearAllNotificationListeners"
41+
42+
// ODP
43+
static let sendOdpEvent = "sendOdpEvent"
44+
static let getVuid = "getVuid"
45+
static let getQualifiedSegments = "getQualifiedSegments"
46+
static let setQualifiedSegments = "setQualifiedSegments"
47+
static let isQualifiedFor = "isQualifiedFor"
48+
static let fetchQualifiedSegments = "fetchQualifiedSegments"
4149
}
4250

4351
struct NotificationType {
@@ -56,6 +64,11 @@ struct DecideOption {
5664
static let excludeVariables = "excludeVariables"
5765
}
5866

67+
struct SegmentOption {
68+
static let ignoreCache = "ignoreCache"
69+
static let resetCache = "resetCache"
70+
}
71+
5972
struct RequestParameterKey {
6073
static let sdkKey = "sdkKey"
6174
static let userId = "userId"
@@ -83,6 +96,23 @@ struct RequestParameterKey {
8396
static let datafilePeriodicDownloadInterval = "datafilePeriodicDownloadInterval"
8497
static let datafileHostPrefix = "datafileHostPrefix"
8598
static let datafileHostSuffix = "datafileHostSuffix"
99+
100+
// ODP
101+
static let vuid = "vuid"
102+
static let qualifiedSegments = "qualifiedSegments"
103+
static let segment = "segment"
104+
static let action = "action"
105+
static let identifiers = "identifiers"
106+
static let data = "data"
107+
static let type = "type"
108+
static let optimizelySegmentOption = "optimizelySegmentOption"
109+
110+
static let optimizelySdkSettings = "optimizelySdkSettings"
111+
static let segmentsCacheSize = "segmentsCacheSize"
112+
static let segmentsCacheTimeoutInSecs = "segmentsCacheTimeoutInSecs"
113+
static let timeoutForSegmentFetchInSecs = "timeoutForSegmentFetchInSecs"
114+
static let timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs"
115+
static let disableOdp = "disableOdp"
86116
}
87117

88118
struct ResponseKey {
@@ -97,6 +127,7 @@ struct ErrorMessage {
97127
static let optimizelyConfigNotFound = "No optimizely config found."
98128
static let optlyClientNotFound = "Optimizely client not found."
99129
static let userContextNotFound = "User context not found."
130+
static let qualifiedSegmentsNotFound = "Qualified Segments not found."
100131
}
101132

102133
//Sohail: There is one issue, can we make sure the types remain same, probably we will need to write unit test separately for type.

ios/Classes/HelperClasses/Utils.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,24 @@ public class Utils: NSObject {
152152
return convertedOptions
153153
}
154154

155+
/// Converts and returns string segment options to array of OptimizelySegmentOption
156+
static func getSegmentOptions(options: [String]?) -> [OptimizelySegmentOption]? {
157+
guard let finalOptions = options else {
158+
return nil
159+
}
160+
var convertedOptions = [OptimizelySegmentOption]()
161+
for option in finalOptions {
162+
switch option {
163+
case SegmentOption.ignoreCache:
164+
convertedOptions.append(OptimizelySegmentOption.ignoreCache)
165+
case SegmentOption.resetCache:
166+
convertedOptions.append(OptimizelySegmentOption.resetCache)
167+
default: break
168+
}
169+
}
170+
return convertedOptions
171+
}
172+
155173
static func convertDecisionToDictionary(decision: OptimizelyDecision?) -> [String: Any?] {
156174
let userContext: [String: Any?] =
157175
[RequestParameterKey.userId : decision?.userContext.userId,

ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift

Lines changed: 146 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
4545

4646
/// Part of FlutterPlugin protocol to handle communication with flutter sdk
4747
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
48-
48+
4949
switch call.method {
5050
case API.initialize: initialize(call, result: result)
5151
case API.addNotificationListener: addNotificationListener(call, result: result)
@@ -67,6 +67,14 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
6767
case API.removeForcedDecision: removeForcedDecision(call, result: result)
6868
case API.removeAllForcedDecisions: removeAllForcedDecisions(call, result: result)
6969
case API.close: close(call, result: result)
70+
71+
// ODP
72+
case API.getQualifiedSegments: getQualifiedSegments(call, result: result)
73+
case API.setQualifiedSegments: setQualifiedSegments(call, result: result)
74+
case API.getVuid: getVuid(call, result: result)
75+
case API.isQualifiedFor: isQualifiedFor(call, result: result)
76+
case API.sendOdpEvent: sendOdpEvent(call, result: result)
77+
case API.fetchQualifiedSegments: fetchQualifiedSegments(call, result: result)
7078
default: result(FlutterMethodNotImplemented)
7179
}
7280
}
@@ -99,6 +107,31 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
99107
}
100108
let defaultDecideOptions = Utils.getDecideOptions(options: decideOptions)
101109

110+
// SDK Settings Default Values
111+
var segmentsCacheSize: Int = 100
112+
var segmentsCacheTimeoutInSecs: Int = 600
113+
var timeoutForSegmentFetchInSecs: Int = 10
114+
var timeoutForOdpEventInSecs: Int = 10
115+
var disableOdp: Bool = false
116+
if let sdkSettings = parameters[RequestParameterKey.optimizelySdkSettings] as? Dictionary<String, Any?> {
117+
if let cacheSize = sdkSettings[RequestParameterKey.segmentsCacheSize] as? Int {
118+
segmentsCacheSize = cacheSize
119+
}
120+
if let segmentsCacheTimeout = sdkSettings[RequestParameterKey.segmentsCacheTimeoutInSecs] as? Int {
121+
segmentsCacheTimeoutInSecs = segmentsCacheTimeout
122+
}
123+
if let timeoutForSegmentFetch = sdkSettings[RequestParameterKey.timeoutForSegmentFetchInSecs] as? Int {
124+
timeoutForSegmentFetchInSecs = timeoutForSegmentFetch
125+
}
126+
if let timeoutForOdpEvent = sdkSettings[RequestParameterKey.timeoutForOdpEventInSecs] as? Int {
127+
timeoutForOdpEventInSecs = timeoutForOdpEvent
128+
}
129+
if let isOdpDisabled = sdkSettings[RequestParameterKey.disableOdp] as? Bool {
130+
disableOdp = isOdpDisabled
131+
}
132+
}
133+
let optimizelySdkSettings = OptimizelySdkSettings(segmentsCacheSize: segmentsCacheSize, segmentsCacheTimeoutInSecs: segmentsCacheTimeoutInSecs, timeoutForSegmentFetchInSecs: timeoutForSegmentFetchInSecs, timeoutForOdpEventInSecs: timeoutForOdpEventInSecs, disableOdp: disableOdp)
134+
102135
// Datafile Download Interval
103136
var datafilePeriodicDownloadInterval = 10 * 60 // seconds
104137

@@ -119,7 +152,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
119152
optimizelyClientsTracker.removeValue(forKey: sdkKey)
120153

121154
// Creating new instance
122-
let optimizelyInstance = OptimizelyClient(sdkKey:sdkKey, eventDispatcher: eventDispatcher, datafileHandler: datafileHandler, periodicDownloadInterval: datafilePeriodicDownloadInterval, defaultDecideOptions: defaultDecideOptions)
155+
let optimizelyInstance = OptimizelyClient(sdkKey:sdkKey, eventDispatcher: eventDispatcher, datafileHandler: datafileHandler, periodicDownloadInterval: datafilePeriodicDownloadInterval, defaultDecideOptions: defaultDecideOptions, settings: optimizelySdkSettings)
123156

124157
optimizelyInstance.start{ [weak self] res in
125158
switch res {
@@ -198,7 +231,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
198231
guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else {
199232
return
200233
}
201-
234+
202235
if let type = parameters[RequestParameterKey.notificationType] as? String, let convertedNotificationType = Utils.getNotificationType(type: type) {
203236
// Remove listeners only for the provided type
204237
optimizelyClient.notificationCenter?.clearNotificationListeners(type: convertedNotificationType)
@@ -302,7 +335,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
302335
let success = optimizelyClient.setForcedVariation(experimentKey: experimentKey, userId: userId, variationKey: variationKey)
303336
result(self.createResponse(success: success))
304337
}
305-
338+
306339
/// Creates a context of the user for which decision APIs will be called.
307340
/// A user context will only be created successfully when the SDK is fully configured using initializeClient.
308341
func createUserContext(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@@ -312,13 +345,15 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
312345
guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else {
313346
return
314347
}
315-
guard let userId = parameters[RequestParameterKey.userId] as? String else {
316-
result(createResponse(success: false, reason: ErrorMessage.invalidParameters))
317-
return
318-
}
319348

320349
let userContextId = uuid
321-
let userContext = optimizelyClient.createUserContext(userId: userId, attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any))
350+
var userContext: OptimizelyUserContext!
351+
352+
if let userId = parameters[RequestParameterKey.userId] as? String {
353+
userContext = optimizelyClient.createUserContext(userId: userId, attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any))
354+
} else {
355+
userContext = optimizelyClient.createUserContext(attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any))
356+
}
322357
if userContextsTracker[sdkKey] != nil {
323358
userContextsTracker[sdkKey]![userContextId] = userContext
324359
} else {
@@ -359,6 +394,108 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
359394
result(createResponse(success: true))
360395
}
361396

397+
/// Returns an array of segments that the user is qualified for.
398+
func getQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
399+
guard let (_, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else {
400+
return
401+
}
402+
guard let qualifiedSegments = userContext.qualifiedSegments else {
403+
result(createResponse(success: false, reason: ErrorMessage.qualifiedSegmentsNotFound))
404+
return
405+
}
406+
result(createResponse(success: true, result: [RequestParameterKey.qualifiedSegments: qualifiedSegments]))
407+
}
408+
409+
/// Sets qualified segments for the user context.
410+
func setQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
411+
guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else {
412+
return
413+
}
414+
guard let qualifiedSegments = parameters[RequestParameterKey.qualifiedSegments] as? [String] else {
415+
result(createResponse(success: false, reason: ErrorMessage.invalidParameters))
416+
return
417+
}
418+
userContext.qualifiedSegments = qualifiedSegments
419+
result(createResponse(success: true))
420+
}
421+
422+
/// Returns the device vuid.
423+
func getVuid(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
424+
guard let (_, sdkKey) = getParametersAndSdkKey(arguments: call.arguments, result: result) else {
425+
return
426+
}
427+
guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else {
428+
return
429+
}
430+
result(self.createResponse(success: true, result: [RequestParameterKey.vuid: optimizelyClient.vuid]))
431+
}
432+
433+
/// Checks if the user is qualified for the given segment.
434+
func isQualifiedFor(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
435+
guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else {
436+
return
437+
}
438+
guard let segment = parameters[RequestParameterKey.segment] as? String else {
439+
result(createResponse(success: false, reason: ErrorMessage.invalidParameters))
440+
return
441+
}
442+
result(self.createResponse(success: userContext.isQualifiedFor(segment: segment)))
443+
}
444+
445+
/// Send an event to the ODP server.
446+
func sendOdpEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
447+
guard let (parameters, sdkKey) = getParametersAndSdkKey(arguments: call.arguments, result: result) else {
448+
return
449+
}
450+
guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else {
451+
return
452+
}
453+
guard let action = parameters[RequestParameterKey.action] as? String else {
454+
result(createResponse(success: false, reason: ErrorMessage.invalidParameters))
455+
return
456+
}
457+
458+
var type: String?
459+
var identifiers: [String: String] = [:]
460+
var data: [String: Any?] = [:]
461+
462+
if let _type = parameters[RequestParameterKey.type] as? String {
463+
type = _type
464+
}
465+
if let _identifiers = parameters[RequestParameterKey.identifiers] as? Dictionary<String, String> {
466+
identifiers = _identifiers
467+
}
468+
if let _data = Utils.getTypedMap(arguments: parameters[RequestParameterKey.data] as? Any) {
469+
data = _data
470+
}
471+
472+
do {
473+
try optimizelyClient.sendOdpEvent(type: type, action: action, identifiers: identifiers, data: data)
474+
result(self.createResponse(success: true))
475+
} catch {
476+
result(self.createResponse(success: false, reason: error.localizedDescription))
477+
}
478+
}
479+
480+
/// Fetch all qualified segments for the user context.
481+
func fetchQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
482+
guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else {
483+
return
484+
}
485+
var segmentOptions: [String]?
486+
if let options = parameters[RequestParameterKey.optimizelySegmentOption] as? [String] {
487+
segmentOptions = options
488+
}
489+
490+
let options = Utils.getSegmentOptions(options: segmentOptions)
491+
do {
492+
try userContext.fetchQualifiedSegments(options: options ?? [])
493+
result(createResponse(success: true))
494+
} catch {
495+
result(self.createResponse(success: false, reason: error.localizedDescription))
496+
}
497+
}
498+
362499
/// Tracks an event.
363500
func trackEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
364501
guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else {

ios/optimizely_flutter_sdk.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
1313
s.source = { :path => '.' }
1414
s.source_files = 'Classes/**/*'
1515
s.dependency 'Flutter'
16-
s.dependency 'OptimizelySwiftSDK', '3.10.1'
16+
s.dependency 'OptimizelySwiftSDK', '4.0.0-beta'
1717
s.platform = :ios, '10.0'
1818
# Flutter.framework does not contain a i386 slice.
1919
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }

lib/src/data_objects/get_qualified_segments_response.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart';
1818
import 'package:optimizely_flutter_sdk/src/utils/constants.dart';
1919

2020
class GetQualifiedSegmentsResponse extends BaseResponse {
21-
List<String> qualifiedSegments = [];
21+
List<String>? qualifiedSegments = [];
2222

2323
GetQualifiedSegmentsResponse(Map<String, dynamic> json) : super(json) {
24+
qualifiedSegments = null;
2425
if (json[Constants.responseResult] is Map<dynamic, dynamic>) {
2526
var response = Map<String, dynamic>.from(json[Constants.responseResult]);
2627
if (response[Constants.qualifiedSegments] is List<dynamic>) {

0 commit comments

Comments
 (0)