Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 25 additions & 33 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,18 @@ class _MyAppState extends State<MyApp> {
OptimizelyDecideOption.includeReasons,
OptimizelyDecideOption.excludeVariables
};

final customLogger = CustomLogger();

var flutterSDK = OptimizelyFlutterSdk("X9mZd2WDywaUL9hZXyh9A",
datafilePeriodicDownloadInterval: 10 * 60,
eventOptions: const EventOptions(
batchSize: 1, timeInterval: 60, maxQueueSize: 10000),
defaultLogLevel: OptimizelyLogLevel.debug,
defaultDecideOptions: defaultOptions,
logger: customLogger,
);
var flutterSDK = OptimizelyFlutterSdk(
"X9mZd2WDywaUL9hZXyh9A",
datafilePeriodicDownloadInterval: 10 * 60,
eventOptions: const EventOptions(
batchSize: 1, timeInterval: 60, maxQueueSize: 10000),
defaultLogLevel: OptimizelyLogLevel.debug,
defaultDecideOptions: defaultOptions,
logger: customLogger,
);
var response = await flutterSDK.initializeClient();

setState(() {
Expand All @@ -60,7 +62,7 @@ class _MyAppState extends State<MyApp> {
"stringValue": "121"
});

// To add decide listener
// Add decide listener
var decideListenerId =
await flutterSDK.addDecisionNotificationListener((notification) {
print("Parsed decision event ....................");
Expand All @@ -73,17 +75,13 @@ class _MyAppState extends State<MyApp> {
Set<OptimizelyDecideOption> options = {
OptimizelyDecideOption.ignoreUserProfileService,
};

// Decide call
var decideResponse = await userContext.decide('flag1', options);
uiResponse +=
"\nFirst decide call variationKey: ${decideResponse.decision!.variationKey}";

// should return following response without forced decision
// flagKey: flag1
// ruleKey: default-rollout-7371-20896892800
// variationKey: off

// Setting forced decision
// Set forced decision
await userContext.setForcedDecision(
OptimizelyDecisionContext("flag1", "flag1_experiment"),
OptimizelyForcedDecision("variation_a"));
Expand All @@ -93,11 +91,6 @@ class _MyAppState extends State<MyApp> {
uiResponse +=
"\nSecond decide call variationKey: ${decideResponse.decision!.variationKey}";

// should return following response with forced decision
// flagKey: flag1
// ruleKey: flag1_experiment
// variationKey: variation_a

// removing forced decision
await userContext.removeForcedDecision(
OptimizelyDecisionContext("flag1", "flag1_experiment"));
Expand All @@ -111,14 +104,6 @@ class _MyAppState extends State<MyApp> {
uiResponse = uiResponse;
});

// should return original response without forced decision
// flagKey: flag1
// ruleKey: default-rollout-7371-20896892800
// variationKey: off

// To cancel decide listener
// await flutterSDK.removeNotificationListener(decideListenerId);

// To add track listener
var trackListenerID =
await flutterSDK.addTrackNotificationListener((notification) {
Expand All @@ -137,12 +122,19 @@ class _MyAppState extends State<MyApp> {
print("log event notification received");
});

// Track call
// Track call with nested objects
response = await userContext.trackEvent("myevent", {
"age": 20,
"doubleValue": 12.12,
"boolValue": false,
"stringValue": "121"
"revenue": 99.99,
"user": {
"id": "user123",
"premium": true,
"tags": ["vip", "loyal"]
},
"items": [
{"name": "Product A", "quantity": 2, "price": 49.99},
{"name": "Product B", "quantity": 1, "price": 50.00}
],
"metadata": {"source": "mobile_app", "platform": "ios"}
});

// To cancel track listener
Expand Down
2 changes: 2 additions & 0 deletions ios/Classes/HelperClasses/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,6 @@ struct TypeValue {
static let int = "int"
static let double = "double"
static let bool = "bool"
static let map = "map"
static let list = "list"
}
75 changes: 49 additions & 26 deletions ios/Classes/HelperClasses/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,59 @@ public class Utils: NSObject {
}
var typedDictionary = [String: Any]()
for (k,v) in args {
if let typedValue = v as? Dictionary<String, Any?>, let value = typedValue["value"] as? Any, let type = typedValue["type"] as? String {
switch type {
case TypeValue.string:
if let strValue = value as? String {
typedDictionary[k] = strValue
}
break
case TypeValue.int:
if let intValue = value as? Int {
typedDictionary[k] = NSNumber(value: intValue).intValue
}
break
case TypeValue.double:
if let doubleValue = value as? Double {
typedDictionary[k] = NSNumber(value: doubleValue).doubleValue
}
break
case TypeValue.bool:
if let booleanValue = value as? Bool {
typedDictionary[k] = NSNumber(value: booleanValue).boolValue
}
break
default:
break
}
if let processedValue = processTypedValue(v) {
typedDictionary[k] = processedValue
}
continue
}
return typedDictionary
}

/// Recursively processes typed values from Flutter to native Swift types
private static func processTypedValue(_ value: Any?) -> Any? {
guard let typedValue = value as? Dictionary<String, Any?>,
let val = typedValue["value"],
let type = typedValue["type"] as? String else {
return nil
}

switch type {
case TypeValue.string:
return val as? String
case TypeValue.int:
if let intValue = val as? Int {
return NSNumber(value: intValue).intValue
}
return nil
case TypeValue.double:
if let doubleValue = val as? Double {
return NSNumber(value: doubleValue).doubleValue
}
return nil
case TypeValue.bool:
if let booleanValue = val as? Bool {
return NSNumber(value: booleanValue).boolValue
}
return nil
case TypeValue.map:
guard let nestedMap = val as? Dictionary<String, Any?> else {
return nil
}
var result = [String: Any]()
for (k, v) in nestedMap {
if let processedValue = processTypedValue(v) {
result[k] = processedValue
}
}
return result
case TypeValue.list:
guard let nestedArray = val as? [Any?] else {
return nil
}
return nestedArray.compactMap { processTypedValue($0) }
default:
return nil
}
}

/// Returns callback required for LogEventListener
static func getLogEventCallback(id: Int, sdkKey: String) -> LogEventListener {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/utils/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class Constants {
static const String intType = "int";
static const String doubleType = "double";
static const String boolType = "bool";
static const String mapType = "map";
static const String listType = "list";

// Supported Method Names
static const String initializeMethod = "initialize";
Expand Down
108 changes: 74 additions & 34 deletions lib/src/utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ class Utils {
OptimizelySegmentOption.resetCache: "resetCache",
};

static Map<String, dynamic> convertToTypedMap(Map<String, dynamic> map) {
/// Converts a map to platform-specific typed format
///
/// On iOS, returns a typed map with type information for proper native conversion.
/// On Android, returns the original primitive map.
///
/// The [forceIOSFormat] parameter is used for testing purposes only to test
/// iOS format conversion without running on actual iOS platform.
static Map<String, dynamic> convertToTypedMap(
Map<String, dynamic> map, {
bool forceIOSFormat = false,
}) {
if (map.isEmpty) {
return map;
}
Expand All @@ -43,48 +53,78 @@ class Utils {
// Only keep primitive values
Map<String, dynamic> primitiveMap = {};
for (MapEntry e in map.entries) {
if (e.value is String) {
dynamic processedValue = _processValue(e.value);
if (processedValue != null) {
primitiveMap[e.key] = e.value;
typedMap[e.key] = {
Constants.value: e.value,
Constants.type: Constants.stringType
};
continue;
typedMap[e.key] = processedValue;
}
if (e.value is double) {
primitiveMap[e.key] = e.value;
typedMap[e.key] = {
Constants.value: e.value,
Constants.type: Constants.doubleType
};
continue;
}
if (e.value is int) {
primitiveMap[e.key] = e.value;
typedMap[e.key] = {
Constants.value: e.value,
Constants.type: Constants.intType
};
continue;
}
if (e.value is bool) {
primitiveMap[e.key] = e.value;
typedMap[e.key] = {
Constants.value: e.value,
Constants.type: Constants.boolType
};
continue;
}
// ignore: avoid_print
print('Unsupported value type for key: ${e.key}.');
}

if (Platform.isIOS) {
if (Platform.isIOS || forceIOSFormat) {
return typedMap;
}
return primitiveMap;
}

/// Recursively processes values to add type information for iOS
static dynamic _processValue(dynamic value) {
if (value is String) {
return {
Constants.value: value,
Constants.type: Constants.stringType
};
}
if (value is double) {
return {
Constants.value: value,
Constants.type: Constants.doubleType
};
}
if (value is int) {
return {
Constants.value: value,
Constants.type: Constants.intType
};
}
if (value is bool) {
return {
Constants.value: value,
Constants.type: Constants.boolType
};
}
if (value is Map) {
// Handle nested maps
Map<String, dynamic> nestedMap = {};
(value as Map).forEach((k, v) {
dynamic processedValue = _processValue(v);
if (processedValue != null) {
nestedMap[k.toString()] = processedValue;
}
});
return {
Constants.value: nestedMap,
Constants.type: Constants.mapType
};
}
if (value is List) {
// Handle arrays
List<dynamic> nestedList = [];
for (var item in value) {
dynamic processedValue = _processValue(item);
if (processedValue != null) {
nestedList.add(processedValue);
}
}
return {
Constants.value: nestedList,
Constants.type: Constants.listType
};
}
// ignore: avoid_print
print('Unsupported value type: ${value.runtimeType}');
return null;
}

static List<String> convertDecideOptions(
Set<OptimizelyDecideOption> options) {
return options.map((option) => Utils.decideOptions[option]!).toList();
Expand Down
Loading