From 749009e5675217c2debaccefe44c505cec5ca104 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Mon, 1 May 2023 17:49:45 -0400 Subject: [PATCH 1/6] Adding support for sendOdpEvent, SdkSettings and getVuid. --- example/lib/main.dart | 3 +- lib/optimizely_flutter_sdk.dart | 59 +++++++++++++----- lib/src/data_objects/get_vuid_response.dart | 31 ++++++++++ lib/src/data_objects/sdk_settings.dart | 37 ++++++++++++ lib/src/optimizely_client_wrapper.dart | 60 ++++++++++++++++--- lib/src/utils/constants.dart | 19 +++++- test/optimizely_flutter_sdk_test.dart | 66 ++++++++++++++++----- 7 files changed, 233 insertions(+), 42 deletions(-) create mode 100644 lib/src/data_objects/get_vuid_response.dart create mode 100644 lib/src/data_objects/sdk_settings.dart 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..3477fd2 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'; @@ -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,6 +147,28 @@ class OptimizelyFlutterSdk { return await OptimizelyClientWrapper.getOptimizelyConfig(_sdkKey); } + /// Send an event to the ODP server. + /// + /// Takes [action] The event action name. + /// Takes [type] The event type (default = "fullstack"). + /// Takes [identifiers] A dictionary for identifiers. + /// Takes [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. @@ -147,10 +176,10 @@ class OptimizelyFlutterSdk { /// Takes [userId] the [String] user ID to be used for bucketing. /// 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_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..dac83fd 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 = { @@ -70,6 +72,15 @@ class OptimizelyClientWrapper { Constants.eventBatchSize: eventOptions.batchSize, Constants.eventTimeInterval: eventOptions.timeInterval, Constants.eventMaxQueueSize: eventOptions.maxQueueSize, + + // Odp Request params + Constants.segmentsCacheSize: sdkSettings.segmentsCacheSize, + Constants.segmentsCacheTimeoutInSecs: + sdkSettings.segmentsCacheTimeoutInSecs, + Constants.timeoutForSegmentFetchInSecs: + sdkSettings.timeoutForSegmentFetchInSecs, + Constants.timeoutForOdpEventInSecs: sdkSettings.timeoutForOdpEventInSecs, + Constants.disableOdp: sdkSettings.disableOdp, }; // clearing notification listeners, if they are mapped to the same sdkKey. @@ -164,6 +175,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.getVuid, { + Constants.sdkKey: sdkKey, + })); + return GetVuidResponse(result); + } + /// Remove notification listener by notification id. static Future removeNotificationListener( String sdkKey, int id) async { @@ -217,15 +257,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/utils/constants.dart b/lib/src/utils/constants.dart index 07f0f0a..0d49c9a 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,11 @@ class Constants { "clearNotificationListeners"; static const String clearAllNotificationListenersMethod = "clearAllNotificationListeners"; + + // Odp Supported Method Names + static const String sendOdpEventMethod = "sendOdpEvent"; + static const String getVuid = "getVuid"; + // Request parameter keys static const String id = "id"; static const String sdkKey = "sdkKey"; @@ -54,6 +59,7 @@ 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 decisionInfo = "decisionInfo"; @@ -72,6 +78,9 @@ class Constants { 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 +114,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/test/optimizely_flutter_sdk_test.dart b/test/optimizely_flutter_sdk_test.dart index 104355a..e8b24d4 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. * @@ -146,9 +146,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, @@ -495,7 +499,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 +526,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 +537,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,7 +549,8 @@ 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); @@ -534,7 +560,8 @@ void main() { 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 +596,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 +616,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 +633,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 +666,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 +680,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 +694,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 +707,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(); From 636cf22c1cf563d28c336123c3f946d3a4c15005 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Tue, 2 May 2023 14:00:53 -0400 Subject: [PATCH 2/6] 1. Adding odp api's to user context. 2. Adding new unit tests. --- lib/optimizely_flutter_sdk.dart | 10 +- .../fetch_qualified_segments_response.dart | 32 +++ .../get_qualified_segments_response.dart | 32 +++ lib/src/optimizely_client_wrapper.dart | 2 +- .../user_context/optimizely_user_context.dart | 70 +++++- lib/src/utils/constants.dart | 9 +- lib/src/utils/utils.dart | 10 + test/optimizely_flutter_sdk_test.dart | 199 +++++++++++++++++- 8 files changed, 352 insertions(+), 12 deletions(-) create mode 100644 lib/src/data_objects/fetch_qualified_segments_response.dart create mode 100644 lib/src/data_objects/get_qualified_segments_response.dart diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index 3477fd2..b1cef55 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -35,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' @@ -150,9 +150,9 @@ class OptimizelyFlutterSdk { /// Send an event to the ODP server. /// /// Takes [action] The event action name. - /// Takes [type] The event type (default = "fullstack"). - /// Takes [identifiers] A dictionary for identifiers. - /// Takes [data] A dictionary for associated data. The default event data will be added to this data before sending to the ODP server. + /// 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, @@ -173,7 +173,7 @@ class OptimizelyFlutterSdk { /// /// 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. /// Takes [attributes] An Optional [Map] of attribute names to current user attribute values. /// Returns An [OptimizelyUserContext] associated with this OptimizelyClient. Future createUserContext( diff --git a/lib/src/data_objects/fetch_qualified_segments_response.dart b/lib/src/data_objects/fetch_qualified_segments_response.dart new file mode 100644 index 0000000..d0e84eb --- /dev/null +++ b/lib/src/data_objects/fetch_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 FetchQualifiedSegmentsResponse extends BaseResponse { + List qualifiedSegments = []; + + FetchQualifiedSegmentsResponse(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_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/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index dac83fd..5b8382b 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -198,7 +198,7 @@ class OptimizelyClientWrapper { /// Returns the device vuid (read only) static Future getVuid(String sdkKey) async { final result = Map.from( - await _channel.invokeMethod(Constants.getVuid, { + await _channel.invokeMethod(Constants.getVuidMethod, { Constants.sdkKey: sdkKey, })); return GetVuidResponse(result); diff --git a/lib/src/user_context/optimizely_user_context.dart b/lib/src/user_context/optimizely_user_context.dart index 4f261d7..10b98a5 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,8 @@ 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/data_objects/fetch_qualified_segments_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; @@ -43,6 +45,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 +98,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 [FetchQualifiedSegmentsResponse] On success, it returns an array of segment names that the user is qualified for. On failure, ir returns the reason of failure. + 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 FetchQualifiedSegmentsResponse(result); + } + /// Tracks an event. /// /// Takes [eventKey] The event name. diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 0d49c9a..58ac985 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -49,7 +49,11 @@ class Constants { // Odp Supported Method Names static const String sendOdpEventMethod = "sendOdpEvent"; - static const String getVuid = "getVuid"; + 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"; @@ -62,6 +66,8 @@ class Constants { 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"; @@ -75,6 +81,7 @@ 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 payload = "payload"; static const String value = "value"; static const String type = "type"; 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 e8b24d4..2af8b9e 100644 --- a/test/optimizely_flutter_sdk_test.dart +++ b/test/optimizely_flutter_sdk_test.dart @@ -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,19 @@ void main() { datafilePeriodicDownloadInterval = methodCall.arguments[Constants.datafilePeriodicDownloadInterval]; + // To Check if sdkSettings were received + sdkSettings = SDKSettings( + segmentsCacheSize: + methodCall.arguments[Constants.segmentsCacheSize], + segmentsCacheTimeoutInSecs: + methodCall.arguments[Constants.segmentsCacheTimeoutInSecs], + timeoutForSegmentFetchInSecs: + methodCall.arguments[Constants.timeoutForSegmentFetchInSecs], + timeoutForOdpEventInSecs: + methodCall.arguments[Constants.timeoutForOdpEventInSecs], + disableOdp: methodCall.arguments[Constants.disableOdp], + ); + // Resetting to default for every test datafileHostOptions = const DatafileHostOptions("", ""); if (methodCall.arguments[Constants.datafileHostPrefix] != null && @@ -187,6 +211,63 @@ 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, + Constants.responseResult: { + Constants.qualifiedSegments: qualifiedSegments, + }, + }; + 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], @@ -315,12 +396,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(); @@ -337,22 +425,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); @@ -367,6 +473,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; }); @@ -557,6 +673,81 @@ void main() { }); }); + 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.qualifiedSegments, equals(qualifiedSegments)); + 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); From f66cce10841c8d62a23da55cefe301c199841895 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Wed, 3 May 2023 10:56:03 -0400 Subject: [PATCH 3/6] fixes for optimizely settings. --- lib/src/optimizely_client_wrapper.dart | 5 ++++- lib/src/utils/constants.dart | 1 + test/optimizely_flutter_sdk_test.dart | 24 +++++++++++++----------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index 5b8382b..e26b74f 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -72,8 +72,10 @@ class OptimizelyClientWrapper { Constants.eventBatchSize: eventOptions.batchSize, Constants.eventTimeInterval: eventOptions.timeInterval, Constants.eventMaxQueueSize: eventOptions.maxQueueSize, + }; - // Odp Request params + // Odp Request params + Map optimizelySdkSettings = { Constants.segmentsCacheSize: sdkSettings.segmentsCacheSize, Constants.segmentsCacheTimeoutInSecs: sdkSettings.segmentsCacheTimeoutInSecs, @@ -82,6 +84,7 @@ class OptimizelyClientWrapper { 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); diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 58ac985..f6a61d5 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -82,6 +82,7 @@ class Constants { 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"; diff --git a/test/optimizely_flutter_sdk_test.dart b/test/optimizely_flutter_sdk_test.dart index 2af8b9e..01db3d8 100644 --- a/test/optimizely_flutter_sdk_test.dart +++ b/test/optimizely_flutter_sdk_test.dart @@ -92,17 +92,19 @@ void main() { methodCall.arguments[Constants.datafilePeriodicDownloadInterval]; // To Check if sdkSettings were received - sdkSettings = SDKSettings( - segmentsCacheSize: - methodCall.arguments[Constants.segmentsCacheSize], - segmentsCacheTimeoutInSecs: - methodCall.arguments[Constants.segmentsCacheTimeoutInSecs], - timeoutForSegmentFetchInSecs: - methodCall.arguments[Constants.timeoutForSegmentFetchInSecs], - timeoutForOdpEventInSecs: - methodCall.arguments[Constants.timeoutForOdpEventInSecs], - disableOdp: methodCall.arguments[Constants.disableOdp], - ); + 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("", ""); From 7cc23f356ca78607f985c283fb8a6dbb3171a154 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Wed, 3 May 2023 15:37:42 -0400 Subject: [PATCH 4/6] Suggested changes made. --- lib/optimizely_flutter_sdk.dart | 1 + .../fetch_qualified_segments_response.dart | 32 ------------------- .../user_context/optimizely_user_context.dart | 7 ++-- test/optimizely_flutter_sdk_test.dart | 4 --- 4 files changed, 4 insertions(+), 40 deletions(-) delete mode 100644 lib/src/data_objects/fetch_qualified_segments_response.dart diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index b1cef55..775130f 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -174,6 +174,7 @@ class OptimizelyFlutterSdk { /// NOTE: A user context will only be created successfully when the SDK is fully configured using initializeClient. /// /// 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( diff --git a/lib/src/data_objects/fetch_qualified_segments_response.dart b/lib/src/data_objects/fetch_qualified_segments_response.dart deleted file mode 100644 index d0e84eb..0000000 --- a/lib/src/data_objects/fetch_qualified_segments_response.dart +++ /dev/null @@ -1,32 +0,0 @@ -/// ************************************************************************** -/// 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 FetchQualifiedSegmentsResponse extends BaseResponse { - List qualifiedSegments = []; - - FetchQualifiedSegmentsResponse(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/user_context/optimizely_user_context.dart b/lib/src/user_context/optimizely_user_context.dart index 10b98a5..906951f 100644 --- a/lib/src/user_context/optimizely_user_context.dart +++ b/lib/src/user_context/optimizely_user_context.dart @@ -22,7 +22,6 @@ import 'package:optimizely_flutter_sdk/src/data_objects/get_attributes_response. 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/data_objects/fetch_qualified_segments_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; @@ -142,8 +141,8 @@ class OptimizelyUserContext { /// 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 [FetchQualifiedSegmentsResponse] On success, it returns an array of segment names that the user is qualified for. On failure, ir returns the reason of failure. - Future fetchQualifiedSegments( + /// Returns [BaseResponse] + Future fetchQualifiedSegments( [Set options = const {}]) async { final result = Map.from( await _channel.invokeMethod(Constants.fetchQualifiedSegmentsMethod, { @@ -151,7 +150,7 @@ class OptimizelyUserContext { Constants.userContextId: _userContextId, Constants.optimizelySegmentOption: Utils.convertSegmentOptions(options), })); - return FetchQualifiedSegmentsResponse(result); + return BaseResponse(result); } /// Tracks an event. diff --git a/test/optimizely_flutter_sdk_test.dart b/test/optimizely_flutter_sdk_test.dart index 01db3d8..fa81adb 100644 --- a/test/optimizely_flutter_sdk_test.dart +++ b/test/optimizely_flutter_sdk_test.dart @@ -240,9 +240,6 @@ void main() { methodCall.arguments[Constants.optimizelySegmentOption])); return { Constants.responseSuccess: true, - Constants.responseResult: { - Constants.qualifiedSegments: qualifiedSegments, - }, }; case Constants.isQualifiedForMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); @@ -725,7 +722,6 @@ void main() { OptimizelySegmentOption.resetCache, }; var response = await userContext!.fetchQualifiedSegments(options); - expect(response.qualifiedSegments, equals(qualifiedSegments)); expect(response.success, isTrue); expect(segmentOptions.length == 2, isTrue); expect(assertSegmentOptions(options, segmentOptions), isTrue); From edc1d61ec7caf4bea7b22ed91b66c73c45350132 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Wed, 3 May 2023 16:45:26 -0400 Subject: [PATCH 5/6] Adding native ios implementation for odp --- ios/Classes/HelperClasses/Constants.swift | 31 ++++ ios/Classes/HelperClasses/Utils.swift | 18 ++ .../SwiftOptimizelyFlutterSdkPlugin.swift | 155 +++++++++++++++++- ios/optimizely_flutter_sdk.podspec | 2 +- 4 files changed, 196 insertions(+), 10 deletions(-) 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..c3014de 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 segment names 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' } From 09a7d9a4c8103b742f8788051515c9237898c24f Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Tue, 9 May 2023 13:17:42 -0400 Subject: [PATCH 6/6] fixes. --- ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift | 4 ++-- lib/src/data_objects/get_qualified_segments_response.dart | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index c3014de..89bac9e 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -68,7 +68,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { case API.removeAllForcedDecisions: removeAllForcedDecisions(call, result: result) case API.close: close(call, result: result) - // ODP + // ODP case API.getQualifiedSegments: getQualifiedSegments(call, result: result) case API.setQualifiedSegments: setQualifiedSegments(call, result: result) case API.getVuid: getVuid(call, result: result) @@ -394,7 +394,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { result(createResponse(success: true)) } - /// Returns an array of segment names that the user is qualified for. + /// 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 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) {