diff --git a/example/lib/main.dart b/example/lib/main.dart index 18f89e2..a717223 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -43,7 +43,8 @@ class _MyAppState extends State { var randomUserName = "${rng.nextInt(1000)}"; // Create user context - var userContext = await flutterSDK.createUserContext(randomUserName); + var userContext = + await flutterSDK.createUserContext(userId: randomUserName); // Set attributes response = await userContext!.setAttributes({ diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index d534c9d..775130f 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -21,6 +21,8 @@ import 'package:optimizely_flutter_sdk/src/data_objects/activate_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/event_options.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/sdk_settings.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_variation_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart'; @@ -33,7 +35,7 @@ export 'package:optimizely_flutter_sdk/src/user_context/optimizely_forced_decisi export 'package:optimizely_flutter_sdk/src/user_context/optimizely_decision_context.dart' show OptimizelyDecisionContext; export 'package:optimizely_flutter_sdk/src/user_context/optimizely_user_context.dart' - show OptimizelyUserContext, OptimizelyDecideOption; + show OptimizelyUserContext, OptimizelyDecideOption, OptimizelySegmentOption; export 'package:optimizely_flutter_sdk/src/data_objects/decide_response.dart' show Decision; export 'package:optimizely_flutter_sdk/src/data_objects/track_listener_response.dart' @@ -44,6 +46,8 @@ export 'package:optimizely_flutter_sdk/src/data_objects/logevent_listener_respon show LogEventListenerResponse; export 'package:optimizely_flutter_sdk/src/data_objects/event_options.dart' show EventOptions; +export 'package:optimizely_flutter_sdk/src/data_objects/sdk_settings.dart' + show SDKSettings; export 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart' show DatafileHostOptions; @@ -59,17 +63,19 @@ class OptimizelyFlutterSdk { final int _datafilePeriodicDownloadInterval; final Map _datafileHostOptions; final Set _defaultDecideOptions; - OptimizelyFlutterSdk( - this._sdkKey, { - EventOptions eventOptions = const EventOptions(), - int datafilePeriodicDownloadInterval = - 10 * 60, // Default time interval in seconds - Map datafileHostOptions = const {}, - Set defaultDecideOptions = const {}, - }) : _eventOptions = eventOptions, + final SDKSettings _sdkSettings; + OptimizelyFlutterSdk(this._sdkKey, + {EventOptions eventOptions = const EventOptions(), + int datafilePeriodicDownloadInterval = + 10 * 60, // Default time interval in seconds + Map datafileHostOptions = const {}, + Set defaultDecideOptions = const {}, + SDKSettings sdkSettings = const SDKSettings()}) + : _eventOptions = eventOptions, _datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval, _datafileHostOptions = datafileHostOptions, - _defaultDecideOptions = defaultDecideOptions; + _defaultDecideOptions = defaultDecideOptions, + _sdkSettings = sdkSettings; /// Starts Optimizely SDK (Synchronous) with provided sdkKey. Future initializeClient() async { @@ -78,7 +84,8 @@ class OptimizelyFlutterSdk { _eventOptions, _datafilePeriodicDownloadInterval, _datafileHostOptions, - _defaultDecideOptions); + _defaultDecideOptions, + _sdkSettings); } /// Use the activate method to start an experiment. @@ -140,17 +147,40 @@ class OptimizelyFlutterSdk { return await OptimizelyClientWrapper.getOptimizelyConfig(_sdkKey); } + /// Send an event to the ODP server. + /// + /// Takes [action] The event action name. + /// Optional [type] The event type (default = "fullstack"). + /// Optional [identifiers] A dictionary for identifiers. + /// Optional [data] A dictionary for associated data. The default event data will be added to this data before sending to the ODP server. + /// Returns [BaseResponse] A object containing success result or reason of failure. + Future sendOdpEvent(String action, + {String? type, + Map identifiers = const {}, + Map data = const {}}) async { + return await OptimizelyClientWrapper.sendOdpEvent(_sdkKey, action, + type: type, identifiers: identifiers, data: data); + } + + /// Returns the device vuid. + /// + /// Returns [GetVuidResponse] A object containing device vuid + Future getVuid() async { + return await OptimizelyClientWrapper.getVuid(_sdkKey); + } + /// Creates a context of the user for which decision APIs will be called. /// /// NOTE: A user context will only be created successfully when the SDK is fully configured using initializeClient. /// - /// Takes [userId] the [String] user ID to be used for bucketing. + /// Optional [userId] the [String] user ID to be used for bucketing. + /// The device vuid will be used as an userId when userId is not provided. /// Takes [attributes] An Optional [Map] of attribute names to current user attribute values. /// Returns An [OptimizelyUserContext] associated with this OptimizelyClient. - Future createUserContext(String userId, - [Map attributes = const {}]) async { - return await OptimizelyClientWrapper.createUserContext( - _sdkKey, userId, attributes); + Future createUserContext( + {String? userId, Map attributes = const {}}) async { + return await OptimizelyClientWrapper.createUserContext(_sdkKey, + userId: userId, attributes: attributes); } /// Allows user to remove notification listener using id. diff --git a/lib/src/data_objects/get_qualified_segments_response.dart b/lib/src/data_objects/get_qualified_segments_response.dart new file mode 100644 index 0000000..130c79e --- /dev/null +++ b/lib/src/data_objects/get_qualified_segments_response.dart @@ -0,0 +1,32 @@ +/// ************************************************************************** +/// Copyright 2023, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +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 = []; + + GetQualifiedSegmentsResponse(Map json) : super(json) { + if (json[Constants.responseResult] is Map) { + var response = Map.from(json[Constants.responseResult]); + if (response[Constants.qualifiedSegments] is List) { + qualifiedSegments = + List.from(response[Constants.qualifiedSegments]); + } + } + } +} diff --git a/lib/src/data_objects/get_vuid_response.dart b/lib/src/data_objects/get_vuid_response.dart new file mode 100644 index 0000000..4be045b --- /dev/null +++ b/lib/src/data_objects/get_vuid_response.dart @@ -0,0 +1,31 @@ +/// ************************************************************************** +/// Copyright 2023, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; +import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; + +class GetVuidResponse extends BaseResponse { + String vuid = ""; + + GetVuidResponse(Map json) : super(json) { + if (json[Constants.responseResult] is Map) { + var response = Map.from(json[Constants.responseResult]); + if (response[Constants.vuid] is String) { + vuid = response[Constants.vuid]; + } + } + } +} diff --git a/lib/src/data_objects/sdk_settings.dart b/lib/src/data_objects/sdk_settings.dart new file mode 100644 index 0000000..412432a --- /dev/null +++ b/lib/src/data_objects/sdk_settings.dart @@ -0,0 +1,37 @@ +/// ************************************************************************** +/// Copyright 2023, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +class SDKSettings { + // The maximum size of audience segments cache (optional. default = 100). Set to zero to disable caching. + final int segmentsCacheSize; + // The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout. + final int segmentsCacheTimeoutInSecs; + // The timeout in seconds of odp segment fetch (optional. default = 10) - OS default timeout will be used if this is set to zero. + final int timeoutForSegmentFetchInSecs; + // The timeout in seconds of odp event dispatch (optional. default = 10) - OS default timeout will be used if this is set to zero. + final int timeoutForOdpEventInSecs; + // Set this flag to true (default = false) to disable ODP features + final bool disableOdp; + + const SDKSettings({ + this.segmentsCacheSize = 100, // Default segmentsCacheSize + this.segmentsCacheTimeoutInSecs = 600, // Default segmentsCacheTimeoutInSecs + this.timeoutForSegmentFetchInSecs = + 10, // Default timeoutForSegmentFetchInSecs + this.timeoutForOdpEventInSecs = 10, // Default timeoutForOdpEventInSecs + this.disableOdp = false, // Default disableOdp + }); +} diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index 0d7737d..e26b74f 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -22,6 +22,7 @@ import 'package:optimizely_flutter_sdk/src/data_objects/activate_listener_respon import 'package:optimizely_flutter_sdk/src/data_objects/activate_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_variation_response.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; @@ -59,7 +60,8 @@ class OptimizelyClientWrapper { EventOptions eventOptions, int datafilePeriodicDownloadInterval, Map datafileHostOptions, - Set defaultDecideOptions) async { + Set defaultDecideOptions, + SDKSettings sdkSettings) async { _channel.setMethodCallHandler(methodCallHandler); final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions); Map requestDict = { @@ -72,6 +74,18 @@ class OptimizelyClientWrapper { Constants.eventMaxQueueSize: eventOptions.maxQueueSize, }; + // Odp Request params + Map optimizelySdkSettings = { + Constants.segmentsCacheSize: sdkSettings.segmentsCacheSize, + Constants.segmentsCacheTimeoutInSecs: + sdkSettings.segmentsCacheTimeoutInSecs, + Constants.timeoutForSegmentFetchInSecs: + sdkSettings.timeoutForSegmentFetchInSecs, + Constants.timeoutForOdpEventInSecs: sdkSettings.timeoutForOdpEventInSecs, + Constants.disableOdp: sdkSettings.disableOdp, + }; + requestDict[Constants.optimizelySdkSettings] = optimizelySdkSettings; + // clearing notification listeners, if they are mapped to the same sdkKey. activateCallbacksById.remove(sdkKey); decisionCallbacksById.remove(sdkKey); @@ -164,6 +178,35 @@ class OptimizelyClientWrapper { return OptimizelyConfigResponse(result); } + /// Send an event to the ODP server. + static Future sendOdpEvent(String sdkKey, String action, + {String? type, + Map identifiers = const {}, + Map data = const {}}) async { + Map request = { + Constants.sdkKey: sdkKey, + Constants.action: action, + Constants.identifiers: identifiers, + Constants.data: Utils.convertToTypedMap(data) + }; + if (type != null) { + request[Constants.type] = type; + } + + final result = Map.from( + await _channel.invokeMethod(Constants.sendOdpEventMethod, request)); + return BaseResponse(result); + } + + /// Returns the device vuid (read only) + static Future getVuid(String sdkKey) async { + final result = Map.from( + await _channel.invokeMethod(Constants.getVuidMethod, { + Constants.sdkKey: sdkKey, + })); + return GetVuidResponse(result); + } + /// Remove notification listener by notification id. static Future removeNotificationListener( String sdkKey, int id) async { @@ -217,15 +260,17 @@ class OptimizelyClientWrapper { /// 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. - static Future createUserContext( - String sdkKey, String userId, - [Map attributes = const {}]) async { - final result = Map.from( - await _channel.invokeMethod(Constants.createUserContextMethod, { + static Future createUserContext(String sdkKey, + {String? userId, Map attributes = const {}}) async { + Map request = { Constants.sdkKey: sdkKey, - Constants.userId: userId, Constants.attributes: Utils.convertToTypedMap(attributes) - })); + }; + if (userId != null) { + request[Constants.userId] = userId; + } + final result = Map.from(await _channel.invokeMethod( + Constants.createUserContextMethod, request)); if (result[Constants.responseSuccess] == true) { final response = diff --git a/lib/src/user_context/optimizely_user_context.dart b/lib/src/user_context/optimizely_user_context.dart index 4f261d7..906951f 100644 --- a/lib/src/user_context/optimizely_user_context.dart +++ b/lib/src/user_context/optimizely_user_context.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -21,6 +21,7 @@ import 'package:optimizely_flutter_sdk/src/data_objects/decide_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_attributes_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_forced_decision_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_user_id_response.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/get_qualified_segments_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; @@ -43,6 +44,16 @@ enum OptimizelyDecideOption { excludeVariables } +/// Options controlling audience segments. +/// +enum OptimizelySegmentOption { + /// ignore odp cache (save/lookup) + ignoreCache, + + /// resets odp cache + resetCache, +} + /// An object for user contexts that the SDK will use to make decisions for. /// class OptimizelyUserContext { @@ -86,6 +97,62 @@ class OptimizelyUserContext { return BaseResponse(result); } + /// Returns [GetQualifiedSegmentsResponse] object containing an array of segment names that the user is qualified for. + Future getQualifiedSegments() async { + final result = Map.from( + await _channel.invokeMethod(Constants.getQualifiedSegmentsMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + })); + return GetQualifiedSegmentsResponse(result); + } + + /// Sets qualified segments for the user context. + /// + /// Takes [qualifiedSegments] A [List] of strings specifying qualified segments for the user. + /// Returns [BaseResponse] + Future setQualifiedSegments( + List qualifiedSegments) async { + final result = Map.from( + await _channel.invokeMethod(Constants.setQualifiedSegmentsMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + Constants.qualifiedSegments: qualifiedSegments + })); + return BaseResponse(result); + } + + /// Checks if the user is qualified for the given segment. + /// + /// Takes [segment] The segment name to check qualification for. + /// Returns [BaseResponse] + Future isQualifiedFor(String segment) async { + final result = Map.from( + await _channel.invokeMethod(Constants.isQualifiedForMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + Constants.segment: segment + })); + return BaseResponse(result); + } + + /// Fetch all qualified segments for the user context. + /// + /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time using **getQualifiedSegments**. + /// On failure, **qualifiedSegments** will be nil and an error will be returned. + /// Optional [options] A set of [OptimizelySegmentOption] for fetching qualified segments. + /// Returns [BaseResponse] + Future fetchQualifiedSegments( + [Set options = const {}]) async { + final result = Map.from( + await _channel.invokeMethod(Constants.fetchQualifiedSegmentsMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + Constants.optimizelySegmentOption: Utils.convertSegmentOptions(options), + })); + return BaseResponse(result); + } + /// Tracks an event. /// /// Takes [eventKey] The event name. diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 07f0f0a..f6a61d5 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -46,6 +46,15 @@ class Constants { "clearNotificationListeners"; static const String clearAllNotificationListenersMethod = "clearAllNotificationListeners"; + + // Odp Supported Method Names + static const String sendOdpEventMethod = "sendOdpEvent"; + static const String getVuidMethod = "getVuid"; + static const String getQualifiedSegmentsMethod = "getQualifiedSegments"; + static const String setQualifiedSegmentsMethod = "setQualifiedSegments"; + static const String isQualifiedForMethod = "isQualifiedFor"; + static const String fetchQualifiedSegmentsMethod = "fetchQualifiedSegments"; + // Request parameter keys static const String id = "id"; static const String sdkKey = "sdkKey"; @@ -54,8 +63,11 @@ class Constants { static const String experiment = "experiment"; static const String variation = "variation"; static const String userId = "userId"; + static const String vuid = "vuid"; static const String experimentKey = "experimentKey"; static const String attributes = "attributes"; + static const String qualifiedSegments = "qualifiedSegments"; + static const String segment = "segment"; static const String decisionInfo = "decisionInfo"; static const String variables = "variables"; static const String reasons = "reasons"; @@ -69,9 +81,14 @@ class Constants { static const String ruleKey = "ruleKey"; static const String enabled = "enabled"; static const String optimizelyDecideOption = "optimizelyDecideOption"; + static const String optimizelySegmentOption = "optimizelySegmentOption"; + static const String optimizelySdkSettings = "optimizelySdkSettings"; static const String payload = "payload"; static const String value = "value"; static const String type = "type"; + static const String action = "action"; + static const String identifiers = "identifiers"; + static const String data = "data"; static const String callbackIds = "callbackIds"; static const String eventBatchSize = "eventBatchSize"; static const String eventTimeInterval = "eventTimeInterval"; @@ -105,6 +122,14 @@ class Constants { static const String variationsMap = "variationsMap"; static const String variablesMap = "variablesMap"; + // Odp Request params + static const String segmentsCacheSize = "segmentsCacheSize"; + static const String segmentsCacheTimeoutInSecs = "segmentsCacheTimeoutInSecs"; + static const String timeoutForSegmentFetchInSecs = + "timeoutForSegmentFetchInSecs"; + static const String timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs"; + static const String disableOdp = "disableOdp"; + // Response keys static const String responseSuccess = "success"; static const String responseResult = "result"; diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index 58344ae..da36841 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -28,6 +28,11 @@ class Utils { OptimizelyDecideOption.excludeVariables: "excludeVariables", }; + static Map segmentOptions = { + OptimizelySegmentOption.ignoreCache: "ignoreCache", + OptimizelySegmentOption.resetCache: "resetCache", + }; + static Map convertToTypedMap(Map map) { if (map.isEmpty) { return map; @@ -84,4 +89,9 @@ class Utils { Set options) { return options.map((option) => Utils.decideOptions[option]!).toList(); } + + static List convertSegmentOptions( + Set options) { + return options.map((option) => Utils.segmentOptions[option]!).toList(); + } } diff --git a/test/optimizely_flutter_sdk_test.dart b/test/optimizely_flutter_sdk_test.dart index 104355a..fa81adb 100644 --- a/test/optimizely_flutter_sdk_test.dart +++ b/test/optimizely_flutter_sdk_test.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -35,15 +35,26 @@ void main() { const String ruleKey = "rule_1"; const String variationKey = "var_1"; const String eventKey = "event-key"; + const String segment = "segment"; + const String action = "action1"; + const String type = "type1"; + const String vuid = "vuid_123"; + const Map identifiers = {"abc": "123"}; + const Map data = {"abc": 12345}; const Map attributes = {"abc": 123}; const Map attributes1 = {"abc": 1234}; const Map eventTags = {"abcd": 1234}; + const List qualifiedSegments = ["1", "2", "3"]; + const String userContextId = "123"; // To check if decide options properly reached the native sdk through channel List decideOptions = []; - // To check if event options and datafileOptions reached the native sdk through channel + // To check if event options, datafileOptions and sdkSettings reached the native sdk through channel EventOptions eventOptions = const EventOptions(); + // To check if segment options properly reached the native sdk through channel + List segmentOptions = []; DatafileHostOptions datafileHostOptions = const DatafileHostOptions("", ""); + SDKSettings sdkSettings = const SDKSettings(); int datafilePeriodicDownloadInterval = 0; const MethodChannel channel = MethodChannel("optimizely_flutter_sdk"); @@ -80,6 +91,21 @@ void main() { datafilePeriodicDownloadInterval = methodCall.arguments[Constants.datafilePeriodicDownloadInterval]; + // To Check if sdkSettings were received + var settings = methodCall.arguments[Constants.optimizelySdkSettings]; + if (settings is Map) { + sdkSettings = SDKSettings( + segmentsCacheSize: settings[Constants.segmentsCacheSize], + segmentsCacheTimeoutInSecs: + settings[Constants.segmentsCacheTimeoutInSecs], + timeoutForSegmentFetchInSecs: + settings[Constants.timeoutForSegmentFetchInSecs], + timeoutForOdpEventInSecs: + settings[Constants.timeoutForOdpEventInSecs], + disableOdp: settings[Constants.disableOdp], + ); + } + // Resetting to default for every test datafileHostOptions = const DatafileHostOptions("", ""); if (methodCall.arguments[Constants.datafileHostPrefix] != null && @@ -146,9 +172,13 @@ void main() { }; case Constants.createUserContextMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); - expect(methodCall.arguments[Constants.userId], equals(userId)); - expect(methodCall.arguments[Constants.attributes]["abc"], - equals(attributes["abc"])); + if (methodCall.arguments[Constants.userId] != null) { + expect(methodCall.arguments[Constants.userId], equals(userId)); + } + if (methodCall.arguments[Constants.attributes]["abc"] != null) { + expect(methodCall.arguments[Constants.attributes]["abc"], + equals(attributes["abc"])); + } expect(methodCall.arguments[Constants.userContextId], isNull); return { Constants.responseSuccess: true, @@ -183,6 +213,60 @@ void main() { return { Constants.responseSuccess: true, }; + case Constants.getQualifiedSegmentsMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + return { + Constants.responseSuccess: true, + Constants.responseResult: { + Constants.qualifiedSegments: qualifiedSegments, + }, + }; + case Constants.setQualifiedSegmentsMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + expect(methodCall.arguments[Constants.qualifiedSegments], + equals(qualifiedSegments)); + return { + Constants.responseSuccess: true, + }; + case Constants.fetchQualifiedSegmentsMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + segmentOptions.addAll(List.from( + methodCall.arguments[Constants.optimizelySegmentOption])); + return { + Constants.responseSuccess: true, + }; + case Constants.isQualifiedForMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + expect(methodCall.arguments[Constants.segment], equals(segment)); + return { + Constants.responseSuccess: true, + }; + case Constants.sendOdpEventMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], isNull); + expect(methodCall.arguments[Constants.action], equals(action)); + expect(methodCall.arguments[Constants.type], equals(type)); + expect( + methodCall.arguments[Constants.identifiers], equals(identifiers)); + expect(methodCall.arguments[Constants.data], equals(data)); + return { + Constants.responseSuccess: true, + }; + case Constants.getVuidMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], isNull); + return { + Constants.responseSuccess: true, + Constants.responseResult: {Constants.vuid: vuid}, + }; case Constants.trackEventMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); expect(methodCall.arguments[Constants.userContextId], @@ -311,12 +395,19 @@ void main() { expect(response.success, isTrue); }); - test("with no eventOptions and no datafileOptions", () async { + test("with no eventOptions, datafileOptions and sdkSettings", () async { // default values const expectedEventOptions = EventOptions(batchSize: 10, timeInterval: 60, maxQueueSize: 10000); debugDefaultTargetPlatformOverride = TargetPlatform.iOS; const expectedDatafileHostOptions = DatafileHostOptions("", ""); + const expectedSDKSettings = SDKSettings( + segmentsCacheSize: 100, + segmentsCacheTimeoutInSecs: 600, + timeoutForSegmentFetchInSecs: 10, + timeoutForOdpEventInSecs: 10, + disableOdp: false, + ); const expectedDatafilePeriodicDownloadInterval = 10 * 60; var sdk = OptimizelyFlutterSdk(testSDKKey); var response = await sdk.initializeClient(); @@ -333,22 +424,40 @@ void main() { equals(expectedDatafileHostOptions.datafileHostPrefix)); expect(datafileHostOptions.datafileHostSuffix, equals(expectedDatafileHostOptions.datafileHostSuffix)); + + expect(sdkSettings.segmentsCacheSize, + equals(expectedSDKSettings.segmentsCacheSize)); + expect(sdkSettings.segmentsCacheTimeoutInSecs, + equals(expectedSDKSettings.segmentsCacheTimeoutInSecs)); + expect(sdkSettings.timeoutForSegmentFetchInSecs, + equals(expectedSDKSettings.timeoutForSegmentFetchInSecs)); + expect(sdkSettings.timeoutForOdpEventInSecs, + equals(expectedSDKSettings.timeoutForOdpEventInSecs)); + expect(sdkSettings.disableOdp, equals(expectedSDKSettings.disableOdp)); debugDefaultTargetPlatformOverride = null; }); - test("with eventOptions and datafileOptions", () async { + test("with eventOptions, datafileOptions and sdkSettings", () async { const expectedEventOptions = EventOptions(batchSize: 20, timeInterval: 30, maxQueueSize: 200); debugDefaultTargetPlatformOverride = TargetPlatform.iOS; const expectedDatafileHostOptions = DatafileHostOptions("123", "456"); const expectedDatafilePeriodicDownloadInterval = 40; + const expectedSDKSettings = SDKSettings( + segmentsCacheSize: 111, + segmentsCacheTimeoutInSecs: 222, + timeoutForSegmentFetchInSecs: 333, + timeoutForOdpEventInSecs: 444, + disableOdp: true, + ); var sdk = OptimizelyFlutterSdk(testSDKKey, eventOptions: expectedEventOptions, datafilePeriodicDownloadInterval: expectedDatafilePeriodicDownloadInterval, datafileHostOptions: { ClientPlatform.iOS: expectedDatafileHostOptions - }); + }, + sdkSettings: expectedSDKSettings); var response = await sdk.initializeClient(); expect(response.success, isTrue); @@ -363,6 +472,16 @@ void main() { equals(expectedDatafileHostOptions.datafileHostPrefix)); expect(datafileHostOptions.datafileHostSuffix, equals(expectedDatafileHostOptions.datafileHostSuffix)); + + expect(sdkSettings.segmentsCacheSize, + equals(expectedSDKSettings.segmentsCacheSize)); + expect(sdkSettings.segmentsCacheTimeoutInSecs, + equals(expectedSDKSettings.segmentsCacheTimeoutInSecs)); + expect(sdkSettings.timeoutForSegmentFetchInSecs, + equals(expectedSDKSettings.timeoutForSegmentFetchInSecs)); + expect(sdkSettings.timeoutForOdpEventInSecs, + equals(expectedSDKSettings.timeoutForOdpEventInSecs)); + expect(sdkSettings.disableOdp, equals(expectedSDKSettings.disableOdp)); debugDefaultTargetPlatformOverride = null; }); @@ -495,7 +614,26 @@ void main() { group("createUserContext()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); + expect(userContext, isNotNull); + }); + + test("should succeed null userId", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(attributes: attributes); + expect(userContext, isNotNull); + }); + + test("should succeed null attributes", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + expect(userContext, isNotNull); + }); + + test("should succeed null userId and attributes", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(); expect(userContext, isNotNull); }); }); @@ -503,7 +641,8 @@ void main() { group("getUserId()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.getUserId(); expect(response.success, isTrue); @@ -513,7 +652,8 @@ void main() { group("getAttributes()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.getAttributes(); expect(response.success, isTrue); @@ -524,17 +664,93 @@ void main() { group("setAttributes()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.setAttributes(attributes1); expect(response.success, isTrue); }); }); + group("getQualifiedSegments()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + var response = await userContext!.getQualifiedSegments(); + + expect(response.qualifiedSegments, qualifiedSegments); + }); + }); + + group("setQualifiedSegments()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + var response = + await userContext!.setQualifiedSegments(qualifiedSegments); + + expect(response.success, isTrue); + }); + }); + + group("isQualifiedFor()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + var response = await userContext!.isQualifiedFor(segment); + + expect(response.success, isTrue); + }); + }); + + group("fetchQualifiedSegments()", () { + bool assertSegmentOptions( + Set options, List convertedOptions) { + for (var option in options) { + if (!convertedOptions.contains(option.name)) { + return false; + } + } + return true; + } + + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + Set options = { + OptimizelySegmentOption.ignoreCache, + OptimizelySegmentOption.resetCache, + }; + var response = await userContext!.fetchQualifiedSegments(options); + expect(response.success, isTrue); + expect(segmentOptions.length == 2, isTrue); + expect(assertSegmentOptions(options, segmentOptions), isTrue); + }); + }); + + group("sendOdpEvent()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var response = await sdk.sendOdpEvent(action, + type: type, identifiers: identifiers, data: data); + expect(response.success, isTrue); + }); + }); + + group("getVuid()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var response = await sdk.getVuid(); + expect(response.success, isTrue); + expect(response.vuid, equals(vuid)); + }); + }); + group("trackEvent()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.trackEvent(eventKey, eventTags); expect(response.success, isTrue); @@ -569,7 +785,8 @@ void main() { }; var sdk = OptimizelyFlutterSdk(testSDKKey, defaultDecideOptions: defaultDecideOptions); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var decideKey = "decide-key"; var response = await userContext!.decide(decideKey, options); @@ -588,7 +805,8 @@ void main() { test("decideForKeys should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey, defaultDecideOptions: defaultDecideOptions); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var decideKeys = ["decide-key-1", "decide-key-2"]; var response = await userContext!.decideForKeys(decideKeys, options); @@ -604,7 +822,8 @@ void main() { test("decideAll() should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey, defaultDecideOptions: defaultDecideOptions); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.decideAll(options); @@ -636,7 +855,8 @@ void main() { group("setForcedDecision()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.setForcedDecision( OptimizelyDecisionContext(flagKey, ruleKey), @@ -649,7 +869,8 @@ void main() { group("getForcedDecision()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext! .getForcedDecision(OptimizelyDecisionContext(flagKey, ruleKey)); @@ -662,7 +883,8 @@ void main() { group("removeForcedDecision()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext! .removeForcedDecision(OptimizelyDecisionContext(flagKey, ruleKey)); @@ -674,7 +896,8 @@ void main() { test("removeAllForcedDecisions() should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.removeAllForcedDecisions();